fix(memory): convert to modern TypeScript SDK APIs (#3015)

* fix(memory): convert to modern TypeScript SDK APIs

Convert the memory server to use the modern McpServer API instead of
the low-level Server API.

Key changes:
- Replace Server with McpServer from @modelcontextprotocol/sdk/server/mcp.js
- Convert all 9 tools to use registerTool() instead of manual request handlers
- Create reusable Zod schemas for Entity and Relation types
- Use Zod schemas directly in inputSchema/outputSchema
- Add structuredContent to all tool responses
- Fix type literals to use 'as const' assertions

The modern API provides:
- Less boilerplate code (removed ~200 lines of schema definitions)
- Better type safety with Zod
- More declarative tool registration
- Cleaner, more maintainable code

* fix: exclude test files from TypeScript build

Add exclude for test files and vitest.config.ts to tsconfig
This commit is contained in:
adam jones 2025-11-20 19:09:44 +00:00 • committed by GitHub
commit b84637314f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 232 additions and 240 deletions

View file

@ -1,11 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { import { z } from "zod";
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -226,243 +223,235 @@ export class KnowledgeGraphManager {
let knowledgeGraphManager: KnowledgeGraphManager; let knowledgeGraphManager: KnowledgeGraphManager;
// Zod schemas for entities and relations
const EntitySchema = z.object({
name: z.string().describe("The name of the entity"),
entityType: z.string().describe("The type of the entity"),
observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
});
const RelationSchema = z.object({
from: z.string().describe("The name of the entity where the relation starts"),
to: z.string().describe("The name of the entity where the relation ends"),
relationType: z.string().describe("The type of the relation")
});
// The server instance and tools exposed to Claude // The server instance and tools exposed to Claude
const server = new Server({ const server = new McpServer({
name: "memory-server", name: "memory-server",
version: "0.6.3", version: "0.6.3",
}, { });
capabilities: {
tools: {}, // Register create_entities tool
server.registerTool(
"create_entities",
{
title: "Create Entities",
description: "Create multiple new entities in the knowledge graph",
inputSchema: {
entities: z.array(EntitySchema)
}, },
},); outputSchema: {
entities: z.array(EntitySchema)
server.setRequestHandler(ListToolsRequestSchema, async () => { }
return { },
tools: [ async ({ entities }) => {
{ const result = await knowledgeGraphManager.createEntities(entities);
name: "create_entities", return {
description: "Create multiple new entities in the knowledge graph", content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
inputSchema: { structuredContent: { entities: result }
type: "object", };
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "The name of the entity" },
entityType: { type: "string", description: "The type of the entity" },
observations: {
type: "array",
items: { type: "string" },
description: "An array of observation contents associated with the entity"
},
},
required: ["name", "entityType", "observations"],
additionalProperties: false,
},
},
},
required: ["entities"],
additionalProperties: false,
},
},
{
name: "create_relations",
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
inputSchema: {
type: "object",
properties: {
relations: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "string", description: "The name of the entity where the relation starts" },
to: { type: "string", description: "The name of the entity where the relation ends" },
relationType: { type: "string", description: "The type of the relation" },
},
required: ["from", "to", "relationType"],
additionalProperties: false,
},
},
},
required: ["relations"],
additionalProperties: false,
},
},
{
name: "add_observations",
description: "Add new observations to existing entities in the knowledge graph",
inputSchema: {
type: "object",
properties: {
observations: {
type: "array",
items: {
type: "object",
properties: {
entityName: { type: "string", description: "The name of the entity to add the observations to" },
contents: {
type: "array",
items: { type: "string" },
description: "An array of observation contents to add"
},
},
required: ["entityName", "contents"],
additionalProperties: false,
},
},
},
required: ["observations"],
additionalProperties: false,
},
},
{
name: "delete_entities",
description: "Delete multiple entities and their associated relations from the knowledge graph",
inputSchema: {
type: "object",
properties: {
entityNames: {
type: "array",
items: { type: "string" },
description: "An array of entity names to delete"
},
},
required: ["entityNames"],
additionalProperties: false,
},
},
{
name: "delete_observations",
description: "Delete specific observations from entities in the knowledge graph",
inputSchema: {
type: "object",
properties: {
deletions: {
type: "array",
items: {
type: "object",
properties: {
entityName: { type: "string", description: "The name of the entity containing the observations" },
observations: {
type: "array",
items: { type: "string" },
description: "An array of observations to delete"
},
},
required: ["entityName", "observations"],
additionalProperties: false,
},
},
},
required: ["deletions"],
additionalProperties: false,
},
},
{
name: "delete_relations",
description: "Delete multiple relations from the knowledge graph",
inputSchema: {
type: "object",
properties: {
relations: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "string", description: "The name of the entity where the relation starts" },
to: { type: "string", description: "The name of the entity where the relation ends" },
relationType: { type: "string", description: "The type of the relation" },
},
required: ["from", "to", "relationType"],
additionalProperties: false,
},
description: "An array of relations to delete"
},
},
required: ["relations"],
additionalProperties: false,
},
},
{
name: "read_graph",
description: "Read the entire knowledge graph",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
},
{
name: "search_nodes",
description: "Search for nodes in the knowledge graph based on a query",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
},
required: ["query"],
additionalProperties: false,
},
},
{
name: "open_nodes",
description: "Open specific nodes in the knowledge graph by their names",
inputSchema: {
type: "object",
properties: {
names: {
type: "array",
items: { type: "string" },
description: "An array of entity names to retrieve",
},
},
required: ["names"],
additionalProperties: false,
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "read_graph") {
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] };
} }
);
if (!args) { // Register create_relations tool
throw new Error(`No arguments provided for tool: ${name}`); server.registerTool(
"create_relations",
{
title: "Create Relations",
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
inputSchema: {
relations: z.array(RelationSchema)
},
outputSchema: {
relations: z.array(RelationSchema)
}
},
async ({ relations }) => {
const result = await knowledgeGraphManager.createRelations(relations);
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
structuredContent: { relations: result }
};
} }
);
switch (name) { // Register add_observations tool
case "create_entities": server.registerTool(
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; "add_observations",
case "create_relations": {
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; title: "Add Observations",
case "add_observations": description: "Add new observations to existing entities in the knowledge graph",
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; inputSchema: {
case "delete_entities": observations: z.array(z.object({
await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); entityName: z.string().describe("The name of the entity to add the observations to"),
return { content: [{ type: "text", text: "Entities deleted successfully" }] }; contents: z.array(z.string()).describe("An array of observation contents to add")
case "delete_observations": }))
await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); },
return { content: [{ type: "text", text: "Observations deleted successfully" }] }; outputSchema: {
case "delete_relations": results: z.array(z.object({
await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); entityName: z.string(),
return { content: [{ type: "text", text: "Relations deleted successfully" }] }; addedObservations: z.array(z.string())
case "search_nodes": }))
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string), null, 2) }] }; }
case "open_nodes": },
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[]), null, 2) }] }; async ({ observations }) => {
default: const result = await knowledgeGraphManager.addObservations(observations);
throw new Error(`Unknown tool: ${name}`); return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
structuredContent: { results: result }
};
} }
}); );
// Register delete_entities tool
server.registerTool(
"delete_entities",
{
title: "Delete Entities",
description: "Delete multiple entities and their associated relations from the knowledge graph",
inputSchema: {
entityNames: z.array(z.string()).describe("An array of entity names to delete")
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ entityNames }) => {
await knowledgeGraphManager.deleteEntities(entityNames);
return {
content: [{ type: "text" as const, text: "Entities deleted successfully" }],
structuredContent: { success: true, message: "Entities deleted successfully" }
};
}
);
// Register delete_observations tool
server.registerTool(
"delete_observations",
{
title: "Delete Observations",
description: "Delete specific observations from entities in the knowledge graph",
inputSchema: {
deletions: z.array(z.object({
entityName: z.string().describe("The name of the entity containing the observations"),
observations: z.array(z.string()).describe("An array of observations to delete")
}))
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ deletions }) => {
await knowledgeGraphManager.deleteObservations(deletions);
return {
content: [{ type: "text" as const, text: "Observations deleted successfully" }],
structuredContent: { success: true, message: "Observations deleted successfully" }
};
}
);
// Register delete_relations tool
server.registerTool(
"delete_relations",
{
title: "Delete Relations",
description: "Delete multiple relations from the knowledge graph",
inputSchema: {
relations: z.array(RelationSchema).describe("An array of relations to delete")
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ relations }) => {
await knowledgeGraphManager.deleteRelations(relations);
return {
content: [{ type: "text" as const, text: "Relations deleted successfully" }],
structuredContent: { success: true, message: "Relations deleted successfully" }
};
}
);
// Register read_graph tool
server.registerTool(
"read_graph",
{
title: "Read Graph",
description: "Read the entire knowledge graph",
inputSchema: {},
outputSchema: {
entities: z.array(EntitySchema),
relations: z.array(RelationSchema)
}
},
async () => {
const graph = await knowledgeGraphManager.readGraph();
return {
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
structuredContent: { ...graph }
};
}
);
// Register search_nodes tool
server.registerTool(
"search_nodes",
{
title: "Search Nodes",
description: "Search for nodes in the knowledge graph based on a query",
inputSchema: {
query: z.string().describe("The search query to match against entity names, types, and observation content")
},
outputSchema: {
entities: z.array(EntitySchema),
relations: z.array(RelationSchema)
}
},
async ({ query }) => {
const graph = await knowledgeGraphManager.searchNodes(query);
return {
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
structuredContent: { ...graph }
};
}
);
// Register open_nodes tool
server.registerTool(
"open_nodes",
{
title: "Open Nodes",
description: "Open specific nodes in the knowledge graph by their names",
inputSchema: {
names: z.array(z.string()).describe("An array of entity names to retrieve")
},
outputSchema: {
entities: z.array(EntitySchema),
relations: z.array(RelationSchema)
}
},
async ({ names }) => {
const graph = await knowledgeGraphManager.openNodes(names);
return {
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
structuredContent: { ...graph }
};
}
);
async function main() { async function main() {
// Initialize memory file path with backward compatibility // Initialize memory file path with backward compatibility

View file

@ -1,11 +1,14 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "." "rootDir": "."
}, },
"include": [ "include": [
"./**/*.ts" "./**/*.ts"
] ],
} "exclude": [
"**/*.test.ts",
"vitest.config.ts"
]
}