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
This commit is contained in:
121
src/mcp/index.ts
Normal file
121
src/mcp/index.ts
Normal file
@@ -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<string, any> = {};
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user