From d64a80281c6bcf4ea06ead21f4ebea87859bb363 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 2 Feb 2026 23:10:38 +0100 Subject: [PATCH] Add MCP server for Claude Code memory integration - Create stdio MCP server wrapping core memory functions (query, show, list, children, add, link) - Add CLAUDE.md with memory-querying instructions for Claude - Register MCP server in .mcp.json - Document MCP setup and tools in USAGE.md --- .mcp.json | 8 ++ CLAUDE.md | 23 ++++ USAGE.md | 41 +++++++ package-lock.json | 281 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 7 +- src/mcp/index.ts | 121 ++++++++++++++++++++ 6 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 .mcp.json create mode 100644 CLAUDE.md create mode 100644 src/mcp/index.ts diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..cf9b60b --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "memory": { + "command": "node", + "args": ["./dist/mcp/index.js"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..78bbf4b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# Project Memory Guidelines + +## Before Making Changes +- **Always query memory** before modifying code or making architectural decisions: use `memory_query` to search for relevant context. +- Use `memory_show` to check existing context on a specific topic before acting. +- Check for existing decisions, gotchas, and component info that may affect your changes. + +## After Making Changes +- **Store new decisions** as memory nodes (kind: `decision`) when architectural choices are made. +- **Store gotchas** as memory nodes (kind: `memory`) when you discover non-obvious behavior. +- **Store component info** (kind: `component`) when creating or significantly modifying a module. +- Tag nodes appropriately for future retrieval. + +## Memory Tools (MCP) +- `memory_query` — search memory by text +- `memory_show` — show a node by ID or prefix +- `memory_list` — list nodes filtered by kind/status/tags +- `memory_children` — list children of a node +- `memory_add` — add a new memory node +- `memory_link` — create a relationship between nodes + +## Git Preferences +- Do not add `Co-Authored-By` lines to commits. diff --git a/USAGE.md b/USAGE.md index 6f13618..6ce7feb 100644 --- a/USAGE.md +++ b/USAGE.md @@ -260,3 +260,44 @@ The web server exposes these endpoints under `/api`: | `DELETE` | `/api/edges/:id` | Delete edge | | `GET` | `/api/graph` | Get full graph (nodes + edges) for visualization | | `POST` | `/api/search` | Search. Body: `{ text, options?: { kind?, tags?, limit?, includeStale? } }` | + +--- + +## MCP Server (Claude Code Integration) + +Cortex includes an MCP (Model Context Protocol) server that exposes memory tools directly to Claude Code. + +### Setup + +1. Build the project: `npm run build` +2. Ensure `.mcp.json` exists in the project root: + +```json +{ + "mcpServers": { + "memory": { + "command": "node", + "args": ["./dist/mcp/index.js"] + } + } +} +``` + +3. Restart Claude Code — the memory tools will appear automatically. + +### MCP Tools + +| Tool | Description | Parameters | +|---|---|---| +| `memory_query` | Hybrid search (BM25 + vector + freshness) | `text`, `kind?`, `limit?` | +| `memory_show` | Show node by ID or prefix with connections | `id` | +| `memory_list` | List nodes with filters | `kind?`, `status?`, `tags?`, `limit?` | +| `memory_children` | List children of a node | `id`, `kind?` | +| `memory_add` | Add a new node | `kind`, `title`, `content?`, `tags?`, `status?`, `sections?` | +| `memory_link` | Create an edge between nodes | `fromId`, `toId`, `type` | + +### Manual Testing + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | node dist/mcp/index.js +``` diff --git a/package-lock.json b/package-lock.json index f5991fb..713cf2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,18 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", "better-sqlite3": "^12.6.2", "chalk": "^4.1.2", "commander": "^14.0.3", "cors": "^2.8.6", "express": "^5.2.1", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "zod": "^4.3.6" }, "bin": { - "memory": "dist/cli/index.js" + "memory": "dist/cli/index.js", + "memory-mcp": "dist/mcp/index.js" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", @@ -28,6 +31,57 @@ "typescript": "^5.9.3" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", + "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -166,6 +220,39 @@ "node": ">= 0.6" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -427,6 +514,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -569,6 +670,27 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -621,6 +743,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -769,6 +928,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -852,6 +1021,33 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1018,6 +1214,15 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -1028,6 +1233,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -1145,6 +1359,15 @@ "node": ">= 6" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1250,6 +1473,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1527,11 +1771,44 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 05b9027..f1de3a5 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,18 @@ "license": "ISC", "description": "AI project memory - knowledge graph backed by SQLite + Ollama embeddings", "bin": { - "memory": "./dist/cli/index.js" + "memory": "./dist/cli/index.js", + "memory-mcp": "./dist/mcp/index.js" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", "better-sqlite3": "^12.6.2", "chalk": "^4.1.2", "commander": "^14.0.3", "cors": "^2.8.6", "express": "^5.2.1", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "zod": "^4.3.6" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..6c74d81 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,121 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod/v3'; +import { query, listNodes, getNode, findNodeByPrefix, addNode, addEdge } from '../core/store'; +import { getConnections } from '../core/graph'; +import { NodeKind, EdgeType } from '../types'; + +const server = new McpServer({ + name: 'memory', + version: '1.0.0', +}); + +server.tool( + 'memory_query', + 'Search memory nodes by text (hybrid BM25 + vector search)', + { + text: z.string().describe('Search query text'), + kind: z.enum(['memory', 'component', 'task', 'decision']).optional().describe('Filter by node kind'), + limit: z.number().optional().describe('Max results (default 10)'), + }, + async ({ text, kind, limit }) => { + const results = await query(text, { kind: kind as NodeKind, limit }); + return { content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }] }; + } +); + +server.tool( + 'memory_show', + 'Show a memory node by ID or ID prefix', + { + id: z.string().describe('Full node ID or prefix'), + }, + async ({ id }) => { + const node = getNode(id) ?? findNodeByPrefix(id); + if (!node) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Node not found' }) }], isError: true }; + } + const connections = getConnections(node.id); + return { content: [{ type: 'text' as const, text: JSON.stringify({ node, connections }, null, 2) }] }; + } +); + +server.tool( + 'memory_list', + 'List memory nodes with optional filters', + { + kind: z.enum(['memory', 'component', 'task', 'decision']).optional().describe('Filter by node kind'), + status: z.string().optional().describe('Filter by status'), + tags: z.array(z.string()).optional().describe('Filter by tags'), + limit: z.number().optional().describe('Max results'), + }, + async ({ kind, status, tags, limit }) => { + const nodes = listNodes({ kind: kind as NodeKind, status, tags, limit }); + return { content: [{ type: 'text' as const, text: JSON.stringify(nodes, null, 2) }] }; + } +); + +server.tool( + 'memory_children', + 'List children of a memory node (outgoing "contains" edges)', + { + id: z.string().describe('Parent node ID or prefix'), + kind: z.enum(['memory', 'component', 'task', 'decision']).optional().describe('Filter children by kind'), + }, + async ({ id, kind }) => { + const node = getNode(id) ?? findNodeByPrefix(id); + if (!node) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Node not found' }) }], isError: true }; + } + const { outgoing } = getConnections(node.id); + let children = outgoing.filter(e => e.type === 'contains').map(e => (e as any).node); + if (kind) { + children = children.filter((n: any) => n.kind === kind); + } + return { content: [{ type: 'text' as const, text: JSON.stringify(children, null, 2) }] }; + } +); + +server.tool( + 'memory_add', + 'Add a new memory node', + { + kind: z.enum(['memory', 'component', 'task', 'decision']).describe('Node kind'), + title: z.string().describe('Node title'), + content: z.string().optional().describe('Node content/body'), + tags: z.array(z.string()).optional().describe('Tags'), + status: z.string().optional().describe('Status (e.g. active, todo, done)'), + sections: z.array(z.object({ label: z.string(), body: z.string() })).optional().describe('Structured sections'), + }, + async ({ kind, title, content, tags, status, sections }) => { + const metadata: Record = {}; + if (sections) metadata.sections = sections; + const node = await addNode({ kind: kind as NodeKind, title, content, tags, status, metadata }); + return { content: [{ type: 'text' as const, text: JSON.stringify(node, null, 2) }] }; + } +); + +server.tool( + 'memory_link', + 'Create an edge between two memory nodes', + { + fromId: z.string().describe('Source node ID'), + toId: z.string().describe('Target node ID'), + type: z.enum(['depends_on', 'contains', 'implements', 'blocked_by', 'subtask_of', 'relates_to', 'supersedes', 'about']).describe('Edge type'), + }, + async ({ fromId, toId, type }) => { + const edge = addEdge(fromId, toId, type as EdgeType); + return { content: [{ type: 'text' as const, text: JSON.stringify(edge, null, 2) }] }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Memory MCP server running on stdio'); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +});