From ffac65c939707235c09860723bf6b0cb1f6fb3a4 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 Aug 2025 21:18:04 +0800 Subject: [PATCH 1/5] [Optimization] Improve Docker build process and add HTTP mode support configuration --- src/memory/Dockerfile | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/memory/Dockerfile b/src/memory/Dockerfile index 2f85d0cfa2..fbdca8ef4c 100644 --- a/src/memory/Dockerfile +++ b/src/memory/Dockerfile @@ -1,24 +1,32 @@ FROM node:22.12-alpine AS builder -COPY src/memory /app -COPY tsconfig.json /tsconfig.json - WORKDIR /app -RUN --mount=type=cache,target=/root/.npm npm install +COPY package*.json ./ +COPY index.ts ./ + +RUN echo '{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./dist", "rootDir": "." }, "include": ["./**/*.ts"], "exclude": ["node_modules"] }' > tsconfig.json -RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev +RUN npm install --only=dev + +RUN npm run build FROM node:22-alpine AS release -COPY --from=builder /app/dist /app/dist -COPY --from=builder /app/package.json /app/package.json -COPY --from=builder /app/package-lock.json /app/package-lock.json +WORKDIR /app + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package*.json ./ + +RUN npm ci --omit=dev --ignore-scripts ENV NODE_ENV=production +ENV MCP_TRANSPORT=http +ENV PORT=3000 +ENV MEMORY_FILE_PATH=/data/memory.json -WORKDIR /app +RUN mkdir -p /data -RUN npm ci --ignore-scripts --omit-dev +EXPOSE 3000 ENTRYPOINT ["node", "dist/index.js"] \ No newline at end of file From 54e3d9703bbbe87e089a360895383b6c56c559a9 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 Aug 2025 21:18:17 +0800 Subject: [PATCH 2/5] [Documentation] Add HTTP/SSE transport mode documentation and Docker usage examples --- src/memory/README.md | 91 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/src/memory/README.md b/src/memory/README.md index 3fd59bbd79..8c8f833367 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -125,13 +125,42 @@ Example: - Relations between requested entities - Silently skips non-existent nodes -# Usage with Claude Desktop +# Usage + +The server supports two transport modes: +1. **STDIO** (default): For use with Claude Desktop and other MCP clients +2. **HTTP/SSE**: For web-based applications and HTTP clients + +## Transport Configuration + +### Environment Variables + +- `MCP_TRANSPORT`: Set to "stdio" (default) or "http"/"sse" for HTTP mode +- `PORT`: HTTP server port (default: 3000, only used in HTTP mode) +- `MEMORY_FILE_PATH`: Path to the memory storage JSON file (default: `memory.json` in the server directory) + +### STDIO Mode (Default) + +Used with Claude Desktop and other MCP clients that support STDIO transport. + +### HTTP Mode + +Run with HTTP transport: +```bash +MCP_TRANSPORT=http PORT=3000 node dist/index.js +``` + +The server will provide SSE endpoints at: +- `GET /sse` - SSE endpoint for establishing MCP connection +- `POST /message` - HTTP endpoint for sending MCP requests + +## Usage with Claude Desktop ### Setup Add this to your claude_desktop_config.json: -#### Docker +#### Docker (STDIO mode) ```json { @@ -144,6 +173,19 @@ Add this to your claude_desktop_config.json: } ``` +#### Docker (HTTP mode) + +```bash +# Run HTTP server on port 3000 +docker run -e MCP_TRANSPORT=http -e PORT=3000 -p 3000:3000 --rm mcp/memory + +# Run with custom memory file path +docker run -e MCP_TRANSPORT=http -e MEMORY_FILE_PATH=/data/custom-memory.json -v /path/to/data:/data -p 3000:3000 --rm mcp/memory + +# Run with all custom settings +docker run -e MCP_TRANSPORT=http -e PORT=8080 -e MEMORY_FILE_PATH=/data/memory.json -v memory-data:/data -p 8080:8080 --rm mcp/memory +``` + #### NPX ```json { @@ -216,7 +258,7 @@ Alternatively, you can add the configuration to a file called `.vscode/mcp.json` } ``` -#### Docker +#### Docker (STDIO mode) ```json { @@ -236,6 +278,10 @@ Alternatively, you can add the configuration to a file called `.vscode/mcp.json` } ``` +#### Docker (HTTP mode) + +For HTTP mode with Docker in VS Code, you would typically run the container separately and connect via HTTP client library in your application. + ### System Prompt The prompt for utilizing memory depends on the use case. Changing the prompt will help the model determine the frequency and types of memories created. @@ -270,12 +316,49 @@ Follow these steps for each interaction: ## Building -Docker: +### Local Build + +```sh +cd src/memory +npm install +npm run build +``` + +### Docker Build ```sh docker build -t mcp/memory -f src/memory/Dockerfile . ``` +## Docker Usage Examples + +### STDIO Mode (Default) +```sh +# For Claude Desktop integration +docker run --rm -i -v claude-memory:/app/dist mcp/memory + +# With custom memory file location +docker run --rm -i -v /path/to/memory:/data -e MEMORY_FILE_PATH=/data/memory.json mcp/memory +``` + +### HTTP Mode +```sh +# Run HTTP server on default port 3000 +docker run --rm -e MCP_TRANSPORT=http -p 3000:3000 mcp/memory + +# Run HTTP server on custom port 8080 +docker run --rm -e MCP_TRANSPORT=http -e PORT=8080 -p 8080:8080 mcp/memory + +# Run with persistent memory storage +docker run --rm -e MCP_TRANSPORT=http -p 3000:3000 -v memory-data:/app -e MEMORY_FILE_PATH=/app/memory.json mcp/memory +``` + +## Environment Variables + +- `MCP_TRANSPORT`: Transport mode - "stdio" (default) or "http"/"sse" +- `PORT`: HTTP server port (default: 3000, only used in HTTP mode) +- `MEMORY_FILE_PATH`: Path to memory storage file (default: "memory.json") + For Awareness: a prior mcp/memory volume contains an index.js file that could be overwritten by the new container. If you are using a docker volume for storage, delete the old docker volume's `index.js` file before starting the new container. ## License From ce2fa5b3e2657a0718f346a0bbcc89d7871f2df7 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 28 Aug 2025 21:18:30 +0800 Subject: [PATCH 3/5] [Feature Addition] Add HTTP/SSE transport mode support for memory server --- src/memory/index.ts | 979 +++++++++++++++++++++++++--------------- src/memory/package.json | 4 +- 2 files changed, 607 insertions(+), 376 deletions(-) diff --git a/src/memory/index.ts b/src/memory/index.ts index 4590a1db6f..fbdd9e2cd7 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -2,420 +2,649 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, + CallToolRequestSchema, + ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import express from 'express'; // Define memory file path using environment variable with fallback const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json'); // If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH - ? path.isAbsolute(process.env.MEMORY_FILE_PATH) - ? process.env.MEMORY_FILE_PATH - : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) - : defaultMemoryPath; + ? path.isAbsolute(process.env.MEMORY_FILE_PATH) + ? process.env.MEMORY_FILE_PATH + : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) + : defaultMemoryPath; // We are storing our memory using entities, relations, and observations in a graph structure interface Entity { - name: string; - entityType: string; - observations: string[]; + name: string; + entityType: string; + observations: string[]; } interface Relation { - from: string; - to: string; - relationType: string; + from: string; + to: string; + relationType: string; } interface KnowledgeGraph { - entities: Entity[]; - relations: Relation[]; + entities: Entity[]; + relations: Relation[]; } // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph class KnowledgeGraphManager { - private async loadGraph(): Promise { - try { - const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") graph.entities.push(item as Entity); - if (item.type === "relation") graph.relations.push(item as Relation); - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; - } - throw error; + private async loadGraph(): Promise { + try { + const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); + const lines = data.split("\n").filter(line => line.trim() !== ""); + return lines.reduce((graph: KnowledgeGraph, line) => { + const item = JSON.parse(line); + if (item.type === "entity") graph.entities.push(item as Entity); + if (item.type === "relation") graph.relations.push(item as Relation); + return graph; + }, { entities: [], relations: [] }); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; + } + } + + private async saveGraph(graph: KnowledgeGraph): Promise { + const lines = [ + ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), + ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), + ]; + await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); + } + + async createEntities(entities: Entity[]): Promise { + const graph = await this.loadGraph(); + const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); + graph.entities.push(...newEntities); + await this.saveGraph(graph); + return newEntities; + } + + async createRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + const newRelations = relations.filter(r => !graph.relations.some(existingRelation => + existingRelation.from === r.from && + existingRelation.to === r.to && + existingRelation.relationType === r.relationType + )); + graph.relations.push(...newRelations); + await this.saveGraph(graph); + return newRelations; + } + + async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { + const graph = await this.loadGraph(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.filter(content => !entity.observations.includes(content)); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; + } + + async deleteEntities(entityNames: string[]): Promise { + const graph = await this.loadGraph(); + graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); + graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); + await this.saveGraph(graph); + } + + async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { + const graph = await this.loadGraph(); + deletions.forEach(d => { + const entity = graph.entities.find(e => e.name === d.entityName); + if (entity) { + entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + } + }); + await this.saveGraph(graph); + } + + async deleteRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + graph.relations = graph.relations.filter(r => !relations.some(delRelation => + r.from === delRelation.from && + r.to === delRelation.to && + r.relationType === delRelation.relationType + )); + await this.saveGraph(graph); + } + + async readGraph(): Promise { + return this.loadGraph(); + } + + // Very basic search function + async searchNodes(query: string): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => + e.name.toLowerCase().includes(query.toLowerCase()) || + e.entityType.toLowerCase().includes(query.toLowerCase()) || + e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) + ); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } + + async openNodes(names: string[]): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => names.includes(e.name)); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; } - } - - private async saveGraph(graph: KnowledgeGraph): Promise { - const lines = [ - ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), - ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), - ]; - await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); - } - - async createEntities(entities: Entity[]): Promise { - const graph = await this.loadGraph(); - const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); - graph.entities.push(...newEntities); - await this.saveGraph(graph); - return newEntities; - } - - async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - const newRelations = relations.filter(r => !graph.relations.some(existingRelation => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - )); - graph.relations.push(...newRelations); - await this.saveGraph(graph); - return newRelations; - } - - async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; - }); - await this.saveGraph(graph); - return results; - } - - async deleteEntities(entityNames: string[]): Promise { - const graph = await this.loadGraph(); - graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); - graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); - await this.saveGraph(graph); - } - - async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { - const graph = await this.loadGraph(); - deletions.forEach(d => { - const entity = graph.entities.find(e => e.name === d.entityName); - if (entity) { - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); - } - }); - await this.saveGraph(graph); - } - - async deleteRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - graph.relations = graph.relations.filter(r => !relations.some(delRelation => - r.from === delRelation.from && - r.to === delRelation.to && - r.relationType === delRelation.relationType - )); - await this.saveGraph(graph); - } - - async readGraph(): Promise { - return this.loadGraph(); - } - - // Very basic search function - async searchNodes(query: string): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => - e.name.toLowerCase().includes(query.toLowerCase()) || - e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) - ); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } - - async openNodes(names: string[]): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => names.includes(e.name)); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } } const knowledgeGraphManager = new KnowledgeGraphManager(); +function createServer() { + const server = new Server({ + name: "memory-server", + version: "0.6.3", + }, { + capabilities: { + tools: {}, + }, + }); -// The server instance and tools exposed to Claude -const server = new Server({ - name: "memory-server", - version: "0.6.3", -}, { - capabilities: { - tools: {}, - }, - },); - -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - 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" - }, + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"], + }, + }, + }, + required: ["entities"], + }, }, - required: ["name", "entityType", "observations"], - }, - }, - }, - required: ["entities"], - }, - }, - { - 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" }, + { + 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"], + }, + }, + }, + required: ["relations"], + }, }, - required: ["from", "to", "relationType"], - }, - }, - }, - required: ["relations"], - }, - }, - { - 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" - }, + { + 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"], + }, + }, + }, + required: ["observations"], + }, }, - required: ["entityName", "contents"], - }, - }, - }, - required: ["observations"], - }, - }, - { - 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"], - }, - }, - { - 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" - }, + { + 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"], + }, }, - required: ["entityName", "observations"], - }, - }, - }, - required: ["deletions"], - }, - }, - { - 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" }, + { + 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"], + }, + }, + }, + required: ["deletions"], + }, }, - required: ["from", "to", "relationType"], - }, - description: "An array of relations to delete" - }, - }, - required: ["relations"], - }, - }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {}, - }, - }, - { - 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"], - }, - }, - { - 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"], - }, - }, - ], - }; -}); + { + 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"], + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], + }, + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + ], + }; + }); -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) { - throw new Error(`No arguments provided for tool: ${name}`); - } - - switch (name) { - case "create_entities": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; - case "create_relations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; - case "add_observations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; - case "delete_entities": - await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); - return { content: [{ type: "text", text: "Entities deleted successfully" }] }; - case "delete_observations": - await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); - return { content: [{ type: "text", text: "Observations deleted successfully" }] }; - case "delete_relations": - await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); - return { content: [{ type: "text", text: "Relations deleted successfully" }] }; - 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) }] }; - default: - throw new Error(`Unknown tool: ${name}`); - } -}); + 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) { + throw new Error(`No arguments provided for tool: ${name}`); + } + + switch (name) { + case "create_entities": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + case "create_relations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + case "add_observations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + case "delete_entities": + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + case "delete_observations": + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + case "delete_relations": + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + 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) }] }; + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + + return { server, cleanup: async () => { } }; +} async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Knowledge Graph MCP Server running on stdio"); + const transportType = process.env.MCP_TRANSPORT || "stdio"; + const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; + + if (transportType === "http" || transportType === "sse") { + const app = express(); + const transports: Map = new Map(); + + // Health check endpoint + app.get("/health", (req, res) => { + res.status(200).json({ + status: "healthy", + timestamp: new Date().toISOString(), + service: "memory-mcp-server", + version: "0.6.3" + }); + }); + + // Readiness check endpoint + app.get("/ready", async (req, res) => { + try { + // Test if we can access the knowledge graph + await knowledgeGraphManager.readGraph(); + res.status(200).json({ + status: "ready", + timestamp: new Date().toISOString(), + service: "memory-mcp-server" + }); + } catch (error) { + res.status(503).json({ + status: "not ready", + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : "Unknown error" + }); + } + }); + + // Add CORS support for MCP clients + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.sendStatus(200); + return; + } + next(); + }); + + // Parse JSON bodies + app.use(express.json()); + + // HTTP JSON-RPC endpoint for MCP clients like Claude CLI + app.post("/", async (req, res) => { + try { + const { method, id, params } = req.body; + let response; + + console.error(`HTTP MCP request: ${method}`); + + if (method === "initialize") { + response = { + jsonrpc: "2.0", + id, + result: { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: { + name: "memory-server", + version: "0.6.3" + } + } + }; + } else if (method === "notifications/initialized") { + // For notifications, we don't send a response + res.status(204).end(); + return; + } else if (method === "tools/list") { + // Return our tools directly + response = { + jsonrpc: "2.0", + id, + result: { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"] + } + } + }, + required: ["entities"] + } + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {} + } + }, + { + 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"] + } + } + ] + } + }; + } else if (method === "tools/call") { + // Handle tool calls directly + const { name, arguments: args } = params; + let result; + + switch (name) { + case "create_entities": + result = await knowledgeGraphManager.createEntities(args.entities); + break; + case "read_graph": + result = await knowledgeGraphManager.readGraph(); + break; + case "search_nodes": + result = await knowledgeGraphManager.searchNodes(args.query); + break; + default: + throw new Error(`Unknown tool: ${name}`); + } + + response = { + jsonrpc: "2.0", + id, + result: { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + } + }; + } else { + response = { + jsonrpc: "2.0", + id, + error: { + code: -32601, + message: "Method not found" + } + }; + } + + res.json(response); + } catch (error) { + console.error("HTTP MCP request error:", error); + res.status(500).json({ + jsonrpc: "2.0", + id: req.body.id, + error: { + code: -32603, + message: "Internal error", + data: error instanceof Error ? error.message : "Unknown error" + } + }); + } + }); + + // SSE endpoint for compatibility + app.get("/sse", async (req, res) => { + const { server, cleanup } = createServer(); + + const transport = new SSEServerTransport("/message", res); + transports.set(transport.sessionId, transport); + + try { + await server.connect(transport); + console.error("MCP Client Connected via SSE: ", transport.sessionId); + + server.onclose = async () => { + console.error("MCP Client Disconnected: ", transport.sessionId); + transports.delete(transport.sessionId); + await cleanup(); + }; + } catch (error) { + console.error("Failed to connect server to transport:", error); + res.status(500).end(); + } + }); + + app.post("/message", async (req, res) => { + const sessionId = (req?.query?.sessionId as string); + const transport = transports.get(sessionId); + if (transport) { + console.error("Client Message from", sessionId); + await transport.handlePostMessage(req, res); + } else { + console.error(`No transport found for sessionId ${sessionId}`) + } + }); + + app.listen(port, () => { + console.error(`Knowledge Graph MCP Server running on HTTP port ${port}`); + }); + } else { + const { server } = createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Knowledge Graph MCP Server running on stdio"); + } } main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); + console.error("Fatal error in main():", error); + process.exit(1); }); diff --git a/src/memory/package.json b/src/memory/package.json index b64cf3b6df..a6111df8b7 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -19,10 +19,12 @@ "watch": "tsc --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.0.1" + "@modelcontextprotocol/sdk": "1.0.1", + "express": "^4.19.2" }, "devDependencies": { "@types/node": "^22", + "@types/express": "^4.17.21", "shx": "^0.3.4", "typescript": "^5.6.2" } From 2a34ec13e817d7c2a3addd05fd86784723765d3b Mon Sep 17 00:00:00 2001 From: Patrick Date: Fri, 29 Aug 2025 09:46:00 +0800 Subject: [PATCH 4/5] [Refactoring] Extract HTTP/SSE functionality to separate file - Restored index.ts to original STDIO-only implementation - Created http.ts with all HTTP/SSE server functionality - Updated Dockerfile to include http.ts and use it as entrypoint --- src/memory/Dockerfile | 3 +- src/memory/http.ts | 506 ++++++++++++++++++++++ src/memory/index.ts | 981 ++++++++++++++++-------------------------- 3 files changed, 884 insertions(+), 606 deletions(-) create mode 100644 src/memory/http.ts diff --git a/src/memory/Dockerfile b/src/memory/Dockerfile index fbdca8ef4c..19ac97f73d 100644 --- a/src/memory/Dockerfile +++ b/src/memory/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /app COPY package*.json ./ COPY index.ts ./ +COPY http.ts ./ RUN echo '{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./dist", "rootDir": "." }, "include": ["./**/*.ts"], "exclude": ["node_modules"] }' > tsconfig.json @@ -29,4 +30,4 @@ RUN mkdir -p /data EXPOSE 3000 -ENTRYPOINT ["node", "dist/index.js"] \ No newline at end of file +ENTRYPOINT ["node", "dist/http.js"] \ No newline at end of file diff --git a/src/memory/http.ts b/src/memory/http.ts new file mode 100644 index 0000000000..bfaa794892 --- /dev/null +++ b/src/memory/http.ts @@ -0,0 +1,506 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { promises as fs } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import express from 'express'; + +// Define memory file path using environment variable with fallback +const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json'); + +// If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script +const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH + ? path.isAbsolute(process.env.MEMORY_FILE_PATH) + ? process.env.MEMORY_FILE_PATH + : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) + : defaultMemoryPath; + +// We are storing our memory using entities, relations, and observations in a graph structure +interface Entity { + name: string; + entityType: string; + observations: string[]; +} + +interface Relation { + from: string; + to: string; + relationType: string; +} + +interface KnowledgeGraph { + entities: Entity[]; + relations: Relation[]; +} + +// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph +class KnowledgeGraphManager { + private async loadGraph(): Promise { + try { + const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); + const lines = data.split("\n").filter(line => line.trim() !== ""); + return lines.reduce((graph: KnowledgeGraph, line) => { + const item = JSON.parse(line); + if (item.type === "entity") graph.entities.push(item as Entity); + if (item.type === "relation") graph.relations.push(item as Relation); + return graph; + }, { entities: [], relations: [] }); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; + } + } + + private async saveGraph(graph: KnowledgeGraph): Promise { + const lines = [ + ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), + ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), + ]; + await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); + } + + async createEntities(entities: Entity[]): Promise { + const graph = await this.loadGraph(); + const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); + graph.entities.push(...newEntities); + await this.saveGraph(graph); + return newEntities; + } + + async createRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + const newRelations = relations.filter(r => !graph.relations.some(existingRelation => + existingRelation.from === r.from && + existingRelation.to === r.to && + existingRelation.relationType === r.relationType + )); + graph.relations.push(...newRelations); + await this.saveGraph(graph); + return newRelations; + } + + async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { + const graph = await this.loadGraph(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.filter(content => !entity.observations.includes(content)); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; + } + + async deleteEntities(entityNames: string[]): Promise { + const graph = await this.loadGraph(); + graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); + graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); + await this.saveGraph(graph); + } + + async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { + const graph = await this.loadGraph(); + deletions.forEach(d => { + const entity = graph.entities.find(e => e.name === d.entityName); + if (entity) { + entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + } + }); + await this.saveGraph(graph); + } + + async deleteRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + graph.relations = graph.relations.filter(r => !relations.some(delRelation => + r.from === delRelation.from && + r.to === delRelation.to && + r.relationType === delRelation.relationType + )); + await this.saveGraph(graph); + } + + async readGraph(): Promise { + return this.loadGraph(); + } + + // Very basic search function + async searchNodes(query: string): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => + e.name.toLowerCase().includes(query.toLowerCase()) || + e.entityType.toLowerCase().includes(query.toLowerCase()) || + e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) + ); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } + + async openNodes(names: string[]): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => names.includes(e.name)); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } +} + +const knowledgeGraphManager = new KnowledgeGraphManager(); + +function createServer() { + const server = new Server({ + name: "memory-server", + version: "0.6.3", + }, { + capabilities: { + tools: {}, + }, + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"], + }, + }, + }, + required: ["entities"], + }, + }, + { + 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"], + }, + }, + }, + required: ["relations"], + }, + }, + { + 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"], + }, + }, + }, + required: ["observations"], + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + }, + required: ["deletions"], + }, + }, + { + 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"], + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], + }, + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + ], + }; + }); + + 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) { + throw new Error(`No arguments provided for tool: ${name}`); + } + + switch (name) { + case "create_entities": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + case "create_relations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + case "add_observations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + case "delete_entities": + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + case "delete_observations": + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + case "delete_relations": + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + 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) }] }; + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + + return { server, cleanup: async () => { } }; +} + +async function main() { + const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; + const app = express(); + const transports: Map = new Map(); + + // Health check endpoint + app.get("/health", (req, res) => { + res.status(200).json({ + status: "healthy", + timestamp: new Date().toISOString(), + service: "memory-mcp-server", + version: "0.6.3" + }); + }); + + // Readiness check endpoint + app.get("/ready", async (req, res) => { + try { + // Test if we can access the knowledge graph + await knowledgeGraphManager.readGraph(); + res.status(200).json({ + status: "ready", + timestamp: new Date().toISOString(), + service: "memory-mcp-server" + }); + } catch (error) { + res.status(503).json({ + status: "not ready", + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : "Unknown error" + }); + } + }); + + // Add CORS support for MCP clients + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.sendStatus(200); + return; + } + next(); + }); + + // Parse JSON bodies + app.use(express.json()); + + // SSE endpoint for MCP clients + app.get("/sse", async (req, res) => { + const { server, cleanup } = createServer(); + + const transport = new SSEServerTransport("/message", res); + transports.set(transport.sessionId, transport); + + try { + await server.connect(transport); + console.error("MCP Client Connected via SSE: ", transport.sessionId); + + server.onclose = async () => { + console.error("MCP Client Disconnected: ", transport.sessionId); + transports.delete(transport.sessionId); + await cleanup(); + }; + } catch (error) { + console.error("Failed to connect server to transport:", error); + res.status(500).end(); + } + }); + + app.post("/message", async (req, res) => { + const sessionId = (req?.query?.sessionId as string); + const transport = transports.get(sessionId); + if (transport) { + console.error("Client Message from", sessionId); + await transport.handlePostMessage(req, res); + } else { + console.error(`No transport found for sessionId ${sessionId}`) + } + }); + + app.listen(port, () => { + console.error(`Knowledge Graph MCP Server running on HTTP/SSE port ${port}`); + }); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/memory/index.ts b/src/memory/index.ts index fbdd9e2cd7..3e065878b7 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -2,649 +2,420 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, + CallToolRequestSchema, + ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import express from 'express'; // Define memory file path using environment variable with fallback const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json'); // If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH - ? path.isAbsolute(process.env.MEMORY_FILE_PATH) - ? process.env.MEMORY_FILE_PATH - : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) - : defaultMemoryPath; + ? path.isAbsolute(process.env.MEMORY_FILE_PATH) + ? process.env.MEMORY_FILE_PATH + : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) + : defaultMemoryPath; // We are storing our memory using entities, relations, and observations in a graph structure interface Entity { - name: string; - entityType: string; - observations: string[]; + name: string; + entityType: string; + observations: string[]; } interface Relation { - from: string; - to: string; - relationType: string; + from: string; + to: string; + relationType: string; } interface KnowledgeGraph { - entities: Entity[]; - relations: Relation[]; + entities: Entity[]; + relations: Relation[]; } // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph class KnowledgeGraphManager { - private async loadGraph(): Promise { - try { - const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") graph.entities.push(item as Entity); - if (item.type === "relation") graph.relations.push(item as Relation); - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; - } - throw error; - } - } - - private async saveGraph(graph: KnowledgeGraph): Promise { - const lines = [ - ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), - ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), - ]; - await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); - } - - async createEntities(entities: Entity[]): Promise { - const graph = await this.loadGraph(); - const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); - graph.entities.push(...newEntities); - await this.saveGraph(graph); - return newEntities; - } - - async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - const newRelations = relations.filter(r => !graph.relations.some(existingRelation => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - )); - graph.relations.push(...newRelations); - await this.saveGraph(graph); - return newRelations; - } - - async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; - }); - await this.saveGraph(graph); - return results; - } - - async deleteEntities(entityNames: string[]): Promise { - const graph = await this.loadGraph(); - graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); - graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); - await this.saveGraph(graph); - } - - async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { - const graph = await this.loadGraph(); - deletions.forEach(d => { - const entity = graph.entities.find(e => e.name === d.entityName); - if (entity) { - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); - } - }); - await this.saveGraph(graph); - } - - async deleteRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - graph.relations = graph.relations.filter(r => !relations.some(delRelation => - r.from === delRelation.from && - r.to === delRelation.to && - r.relationType === delRelation.relationType - )); - await this.saveGraph(graph); - } - - async readGraph(): Promise { - return this.loadGraph(); - } - - // Very basic search function - async searchNodes(query: string): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => - e.name.toLowerCase().includes(query.toLowerCase()) || - e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) - ); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } - - async openNodes(names: string[]): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => names.includes(e.name)); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; + private async loadGraph(): Promise { + try { + const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); + const lines = data.split("\n").filter(line => line.trim() !== ""); + return lines.reduce((graph: KnowledgeGraph, line) => { + const item = JSON.parse(line); + if (item.type === "entity") graph.entities.push(item as Entity); + if (item.type === "relation") graph.relations.push(item as Relation); + return graph; + }, { entities: [], relations: [] }); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; } + } + + private async saveGraph(graph: KnowledgeGraph): Promise { + const lines = [ + ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), + ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), + ]; + await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); + } + + async createEntities(entities: Entity[]): Promise { + const graph = await this.loadGraph(); + const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); + graph.entities.push(...newEntities); + await this.saveGraph(graph); + return newEntities; + } + + async createRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + const newRelations = relations.filter(r => !graph.relations.some(existingRelation => + existingRelation.from === r.from && + existingRelation.to === r.to && + existingRelation.relationType === r.relationType + )); + graph.relations.push(...newRelations); + await this.saveGraph(graph); + return newRelations; + } + + async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { + const graph = await this.loadGraph(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.filter(content => !entity.observations.includes(content)); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; + } + + async deleteEntities(entityNames: string[]): Promise { + const graph = await this.loadGraph(); + graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); + graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); + await this.saveGraph(graph); + } + + async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { + const graph = await this.loadGraph(); + deletions.forEach(d => { + const entity = graph.entities.find(e => e.name === d.entityName); + if (entity) { + entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + } + }); + await this.saveGraph(graph); + } + + async deleteRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + graph.relations = graph.relations.filter(r => !relations.some(delRelation => + r.from === delRelation.from && + r.to === delRelation.to && + r.relationType === delRelation.relationType + )); + await this.saveGraph(graph); + } + + async readGraph(): Promise { + return this.loadGraph(); + } + + // Very basic search function + async searchNodes(query: string): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => + e.name.toLowerCase().includes(query.toLowerCase()) || + e.entityType.toLowerCase().includes(query.toLowerCase()) || + e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) + ); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } + + async openNodes(names: string[]): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => names.includes(e.name)); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } } const knowledgeGraphManager = new KnowledgeGraphManager(); -function createServer() { - const server = new Server({ - name: "memory-server", - version: "0.6.3", - }, { - capabilities: { - tools: {}, - }, - }); - server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - 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"], - }, - }, - }, - required: ["entities"], - }, - }, - { - 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"], - }, - }, - }, - required: ["relations"], - }, +// The server instance and tools exposed to Claude +const server = new Server({ + name: "memory-server", + version: "0.6.3", +}, { + capabilities: { + tools: {}, + }, + },); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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" + }, }, - { - 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"], - }, - }, - }, - required: ["observations"], - }, - }, - { - 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"], - }, - }, - { - 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"], - }, - }, - }, - required: ["deletions"], - }, - }, - { - 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"], - }, - description: "An array of relations to delete" - }, - }, - required: ["relations"], - }, + required: ["name", "entityType", "observations"], + }, + }, + }, + required: ["entities"], + }, + }, + { + 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" }, }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {}, - }, + required: ["from", "to", "relationType"], + }, + }, + }, + required: ["relations"], + }, + }, + { + 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" + }, }, - { - 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"], - }, + required: ["entityName", "contents"], + }, + }, + }, + required: ["observations"], + }, + }, + { + 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"], + }, + }, + { + 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" + }, }, - { - 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"], - }, + required: ["entityName", "observations"], + }, + }, + }, + required: ["deletions"], + }, + }, + { + 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" }, }, - ], - }; - }); - - 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) { - throw new Error(`No arguments provided for tool: ${name}`); - } - - switch (name) { - case "create_entities": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; - case "create_relations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; - case "add_observations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; - case "delete_entities": - await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); - return { content: [{ type: "text", text: "Entities deleted successfully" }] }; - case "delete_observations": - await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); - return { content: [{ type: "text", text: "Observations deleted successfully" }] }; - case "delete_relations": - await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); - return { content: [{ type: "text", text: "Relations deleted successfully" }] }; - 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) }] }; - default: - throw new Error(`Unknown tool: ${name}`); - } - }); + required: ["from", "to", "relationType"], + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], + }, + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + ], + }; +}); - return { server, cleanup: async () => { } }; -} +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) { + throw new Error(`No arguments provided for tool: ${name}`); + } + + switch (name) { + case "create_entities": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + case "create_relations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + case "add_observations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + case "delete_entities": + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + case "delete_observations": + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + case "delete_relations": + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + 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) }] }; + default: + throw new Error(`Unknown tool: ${name}`); + } +}); async function main() { - const transportType = process.env.MCP_TRANSPORT || "stdio"; - const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; - - if (transportType === "http" || transportType === "sse") { - const app = express(); - const transports: Map = new Map(); - - // Health check endpoint - app.get("/health", (req, res) => { - res.status(200).json({ - status: "healthy", - timestamp: new Date().toISOString(), - service: "memory-mcp-server", - version: "0.6.3" - }); - }); - - // Readiness check endpoint - app.get("/ready", async (req, res) => { - try { - // Test if we can access the knowledge graph - await knowledgeGraphManager.readGraph(); - res.status(200).json({ - status: "ready", - timestamp: new Date().toISOString(), - service: "memory-mcp-server" - }); - } catch (error) { - res.status(503).json({ - status: "not ready", - timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : "Unknown error" - }); - } - }); - - // Add CORS support for MCP clients - app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.sendStatus(200); - return; - } - next(); - }); - - // Parse JSON bodies - app.use(express.json()); - - // HTTP JSON-RPC endpoint for MCP clients like Claude CLI - app.post("/", async (req, res) => { - try { - const { method, id, params } = req.body; - let response; - - console.error(`HTTP MCP request: ${method}`); - - if (method === "initialize") { - response = { - jsonrpc: "2.0", - id, - result: { - protocolVersion: "2024-11-05", - capabilities: { - tools: {} - }, - serverInfo: { - name: "memory-server", - version: "0.6.3" - } - } - }; - } else if (method === "notifications/initialized") { - // For notifications, we don't send a response - res.status(204).end(); - return; - } else if (method === "tools/list") { - // Return our tools directly - response = { - jsonrpc: "2.0", - id, - result: { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - 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"] - } - } - }, - required: ["entities"] - } - }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {} - } - }, - { - 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"] - } - } - ] - } - }; - } else if (method === "tools/call") { - // Handle tool calls directly - const { name, arguments: args } = params; - let result; - - switch (name) { - case "create_entities": - result = await knowledgeGraphManager.createEntities(args.entities); - break; - case "read_graph": - result = await knowledgeGraphManager.readGraph(); - break; - case "search_nodes": - result = await knowledgeGraphManager.searchNodes(args.query); - break; - default: - throw new Error(`Unknown tool: ${name}`); - } - - response = { - jsonrpc: "2.0", - id, - result: { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] - } - }; - } else { - response = { - jsonrpc: "2.0", - id, - error: { - code: -32601, - message: "Method not found" - } - }; - } - - res.json(response); - } catch (error) { - console.error("HTTP MCP request error:", error); - res.status(500).json({ - jsonrpc: "2.0", - id: req.body.id, - error: { - code: -32603, - message: "Internal error", - data: error instanceof Error ? error.message : "Unknown error" - } - }); - } - }); - - // SSE endpoint for compatibility - app.get("/sse", async (req, res) => { - const { server, cleanup } = createServer(); - - const transport = new SSEServerTransport("/message", res); - transports.set(transport.sessionId, transport); - - try { - await server.connect(transport); - console.error("MCP Client Connected via SSE: ", transport.sessionId); - - server.onclose = async () => { - console.error("MCP Client Disconnected: ", transport.sessionId); - transports.delete(transport.sessionId); - await cleanup(); - }; - } catch (error) { - console.error("Failed to connect server to transport:", error); - res.status(500).end(); - } - }); - - app.post("/message", async (req, res) => { - const sessionId = (req?.query?.sessionId as string); - const transport = transports.get(sessionId); - if (transport) { - console.error("Client Message from", sessionId); - await transport.handlePostMessage(req, res); - } else { - console.error(`No transport found for sessionId ${sessionId}`) - } - }); - - app.listen(port, () => { - console.error(`Knowledge Graph MCP Server running on HTTP port ${port}`); - }); - } else { - const { server } = createServer(); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Knowledge Graph MCP Server running on stdio"); - } + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Knowledge Graph MCP Server running on stdio"); } main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); + console.error("Fatal error in main():", error); + process.exit(1); +}); \ No newline at end of file From ceb225e2a142363dd91d3ecd1570078ada115bf8 Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 1 Sep 2025 17:40:25 +0800 Subject: [PATCH 5/5] [Refactoring] Consolidate HTTP/SSE functionality into main index.ts - Remove separate http.ts file - Merge HTTP/SSE server transport functionality into index.ts - Update Dockerfile to use index.js as entrypoint instead of http.js - Maintain all existing HTTP/SSE capabilities in unified structure --- src/memory/Dockerfile | 3 +- src/memory/http.ts | 506 ------------------ src/memory/index.ts | 1174 ++++++++++++++++++++++++++++------------- 3 files changed, 815 insertions(+), 868 deletions(-) delete mode 100644 src/memory/http.ts diff --git a/src/memory/Dockerfile b/src/memory/Dockerfile index 19ac97f73d..e4d6f38fa1 100644 --- a/src/memory/Dockerfile +++ b/src/memory/Dockerfile @@ -4,7 +4,6 @@ WORKDIR /app COPY package*.json ./ COPY index.ts ./ -COPY http.ts ./ RUN echo '{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./dist", "rootDir": "." }, "include": ["./**/*.ts"], "exclude": ["node_modules"] }' > tsconfig.json @@ -30,4 +29,4 @@ RUN mkdir -p /data EXPOSE 3000 -ENTRYPOINT ["node", "dist/http.js"] \ No newline at end of file +ENTRYPOINT ["node", "dist/index.js"] diff --git a/src/memory/http.ts b/src/memory/http.ts deleted file mode 100644 index bfaa794892..0000000000 --- a/src/memory/http.ts +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { promises as fs } from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import express from 'express'; - -// Define memory file path using environment variable with fallback -const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json'); - -// If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script -const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH - ? path.isAbsolute(process.env.MEMORY_FILE_PATH) - ? process.env.MEMORY_FILE_PATH - : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) - : defaultMemoryPath; - -// We are storing our memory using entities, relations, and observations in a graph structure -interface Entity { - name: string; - entityType: string; - observations: string[]; -} - -interface Relation { - from: string; - to: string; - relationType: string; -} - -interface KnowledgeGraph { - entities: Entity[]; - relations: Relation[]; -} - -// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph -class KnowledgeGraphManager { - private async loadGraph(): Promise { - try { - const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") graph.entities.push(item as Entity); - if (item.type === "relation") graph.relations.push(item as Relation); - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; - } - throw error; - } - } - - private async saveGraph(graph: KnowledgeGraph): Promise { - const lines = [ - ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), - ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), - ]; - await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); - } - - async createEntities(entities: Entity[]): Promise { - const graph = await this.loadGraph(); - const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); - graph.entities.push(...newEntities); - await this.saveGraph(graph); - return newEntities; - } - - async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - const newRelations = relations.filter(r => !graph.relations.some(existingRelation => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - )); - graph.relations.push(...newRelations); - await this.saveGraph(graph); - return newRelations; - } - - async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; - }); - await this.saveGraph(graph); - return results; - } - - async deleteEntities(entityNames: string[]): Promise { - const graph = await this.loadGraph(); - graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); - graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); - await this.saveGraph(graph); - } - - async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { - const graph = await this.loadGraph(); - deletions.forEach(d => { - const entity = graph.entities.find(e => e.name === d.entityName); - if (entity) { - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); - } - }); - await this.saveGraph(graph); - } - - async deleteRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - graph.relations = graph.relations.filter(r => !relations.some(delRelation => - r.from === delRelation.from && - r.to === delRelation.to && - r.relationType === delRelation.relationType - )); - await this.saveGraph(graph); - } - - async readGraph(): Promise { - return this.loadGraph(); - } - - // Very basic search function - async searchNodes(query: string): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => - e.name.toLowerCase().includes(query.toLowerCase()) || - e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) - ); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } - - async openNodes(names: string[]): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => names.includes(e.name)); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } -} - -const knowledgeGraphManager = new KnowledgeGraphManager(); - -function createServer() { - const server = new Server({ - name: "memory-server", - version: "0.6.3", - }, { - capabilities: { - tools: {}, - }, - }); - - server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - 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"], - }, - }, - }, - required: ["entities"], - }, - }, - { - 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"], - }, - }, - }, - required: ["relations"], - }, - }, - { - 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"], - }, - }, - }, - required: ["observations"], - }, - }, - { - 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"], - }, - }, - { - 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"], - }, - }, - }, - required: ["deletions"], - }, - }, - { - 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"], - }, - description: "An array of relations to delete" - }, - }, - required: ["relations"], - }, - }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {}, - }, - }, - { - 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"], - }, - }, - { - 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"], - }, - }, - ], - }; - }); - - 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) { - throw new Error(`No arguments provided for tool: ${name}`); - } - - switch (name) { - case "create_entities": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; - case "create_relations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; - case "add_observations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; - case "delete_entities": - await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); - return { content: [{ type: "text", text: "Entities deleted successfully" }] }; - case "delete_observations": - await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); - return { content: [{ type: "text", text: "Observations deleted successfully" }] }; - case "delete_relations": - await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); - return { content: [{ type: "text", text: "Relations deleted successfully" }] }; - 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) }] }; - default: - throw new Error(`Unknown tool: ${name}`); - } - }); - - return { server, cleanup: async () => { } }; -} - -async function main() { - const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; - const app = express(); - const transports: Map = new Map(); - - // Health check endpoint - app.get("/health", (req, res) => { - res.status(200).json({ - status: "healthy", - timestamp: new Date().toISOString(), - service: "memory-mcp-server", - version: "0.6.3" - }); - }); - - // Readiness check endpoint - app.get("/ready", async (req, res) => { - try { - // Test if we can access the knowledge graph - await knowledgeGraphManager.readGraph(); - res.status(200).json({ - status: "ready", - timestamp: new Date().toISOString(), - service: "memory-mcp-server" - }); - } catch (error) { - res.status(503).json({ - status: "not ready", - timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : "Unknown error" - }); - } - }); - - // Add CORS support for MCP clients - app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.sendStatus(200); - return; - } - next(); - }); - - // Parse JSON bodies - app.use(express.json()); - - // SSE endpoint for MCP clients - app.get("/sse", async (req, res) => { - const { server, cleanup } = createServer(); - - const transport = new SSEServerTransport("/message", res); - transports.set(transport.sessionId, transport); - - try { - await server.connect(transport); - console.error("MCP Client Connected via SSE: ", transport.sessionId); - - server.onclose = async () => { - console.error("MCP Client Disconnected: ", transport.sessionId); - transports.delete(transport.sessionId); - await cleanup(); - }; - } catch (error) { - console.error("Failed to connect server to transport:", error); - res.status(500).end(); - } - }); - - app.post("/message", async (req, res) => { - const sessionId = (req?.query?.sessionId as string); - const transport = transports.get(sessionId); - if (transport) { - console.error("Client Message from", sessionId); - await transport.handlePostMessage(req, res); - } else { - console.error(`No transport found for sessionId ${sessionId}`) - } - }); - - app.listen(port, () => { - console.error(`Knowledge Graph MCP Server running on HTTP/SSE port ${port}`); - }); -} - -main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); \ No newline at end of file diff --git a/src/memory/index.ts b/src/memory/index.ts index 3e065878b7..3392948c86 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -2,185 +2,187 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, + CallToolRequestSchema, + ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import express from 'express'; // Define memory file path using environment variable with fallback const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json'); // If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH - ? path.isAbsolute(process.env.MEMORY_FILE_PATH) - ? process.env.MEMORY_FILE_PATH - : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) - : defaultMemoryPath; + ? path.isAbsolute(process.env.MEMORY_FILE_PATH) + ? process.env.MEMORY_FILE_PATH + : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH) + : defaultMemoryPath; // We are storing our memory using entities, relations, and observations in a graph structure interface Entity { - name: string; - entityType: string; - observations: string[]; + name: string; + entityType: string; + observations: string[]; } interface Relation { - from: string; - to: string; - relationType: string; + from: string; + to: string; + relationType: string; } interface KnowledgeGraph { - entities: Entity[]; - relations: Relation[]; + entities: Entity[]; + relations: Relation[]; } // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph class KnowledgeGraphManager { - private async loadGraph(): Promise { - try { - const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") graph.entities.push(item as Entity); - if (item.type === "relation") graph.relations.push(item as Relation); - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; - } - throw error; + private async loadGraph(): Promise { + try { + const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); + const lines = data.split("\n").filter(line => line.trim() !== ""); + return lines.reduce((graph: KnowledgeGraph, line) => { + const item = JSON.parse(line); + if (item.type === "entity") graph.entities.push(item as Entity); + if (item.type === "relation") graph.relations.push(item as Relation); + return graph; + }, { entities: [], relations: [] }); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; + } + } + + private async saveGraph(graph: KnowledgeGraph): Promise { + const lines = [ + ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), + ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), + ]; + await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); + } + + async createEntities(entities: Entity[]): Promise { + const graph = await this.loadGraph(); + const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); + graph.entities.push(...newEntities); + await this.saveGraph(graph); + return newEntities; + } + + async createRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + const newRelations = relations.filter(r => !graph.relations.some(existingRelation => + existingRelation.from === r.from && + existingRelation.to === r.to && + existingRelation.relationType === r.relationType + )); + graph.relations.push(...newRelations); + await this.saveGraph(graph); + return newRelations; + } + + async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { + const graph = await this.loadGraph(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.filter(content => !entity.observations.includes(content)); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; + } + + async deleteEntities(entityNames: string[]): Promise { + const graph = await this.loadGraph(); + graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); + graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); + await this.saveGraph(graph); + } + + async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { + const graph = await this.loadGraph(); + deletions.forEach(d => { + const entity = graph.entities.find(e => e.name === d.entityName); + if (entity) { + entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + } + }); + await this.saveGraph(graph); + } + + async deleteRelations(relations: Relation[]): Promise { + const graph = await this.loadGraph(); + graph.relations = graph.relations.filter(r => !relations.some(delRelation => + r.from === delRelation.from && + r.to === delRelation.to && + r.relationType === delRelation.relationType + )); + await this.saveGraph(graph); + } + + async readGraph(): Promise { + return this.loadGraph(); + } + + // Very basic search function + async searchNodes(query: string): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => + e.name.toLowerCase().includes(query.toLowerCase()) || + e.entityType.toLowerCase().includes(query.toLowerCase()) || + e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) + ); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; + } + + async openNodes(names: string[]): Promise { + const graph = await this.loadGraph(); + + // Filter entities + const filteredEntities = graph.entities.filter(e => names.includes(e.name)); + + // Create a Set of filtered entity names for quick lookup + const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + + // Filter relations to only include those between filtered entities + const filteredRelations = graph.relations.filter(r => + filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + ); + + const filteredGraph: KnowledgeGraph = { + entities: filteredEntities, + relations: filteredRelations, + }; + + return filteredGraph; } - } - - private async saveGraph(graph: KnowledgeGraph): Promise { - const lines = [ - ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })), - ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })), - ]; - await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n")); - } - - async createEntities(entities: Entity[]): Promise { - const graph = await this.loadGraph(); - const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); - graph.entities.push(...newEntities); - await this.saveGraph(graph); - return newEntities; - } - - async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - const newRelations = relations.filter(r => !graph.relations.some(existingRelation => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - )); - graph.relations.push(...newRelations); - await this.saveGraph(graph); - return newRelations; - } - - async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; - }); - await this.saveGraph(graph); - return results; - } - - async deleteEntities(entityNames: string[]): Promise { - const graph = await this.loadGraph(); - graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); - graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); - await this.saveGraph(graph); - } - - async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { - const graph = await this.loadGraph(); - deletions.forEach(d => { - const entity = graph.entities.find(e => e.name === d.entityName); - if (entity) { - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); - } - }); - await this.saveGraph(graph); - } - - async deleteRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - graph.relations = graph.relations.filter(r => !relations.some(delRelation => - r.from === delRelation.from && - r.to === delRelation.to && - r.relationType === delRelation.relationType - )); - await this.saveGraph(graph); - } - - async readGraph(): Promise { - return this.loadGraph(); - } - - // Very basic search function - async searchNodes(query: string): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => - e.name.toLowerCase().includes(query.toLowerCase()) || - e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) - ); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } - - async openNodes(names: string[]): Promise { - const graph = await this.loadGraph(); - - // Filter entities - const filteredEntities = graph.entities.filter(e => names.includes(e.name)); - - // Create a Set of filtered entity names for quick lookup - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - - // Filter relations to only include those between filtered entities - const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ); - - const filteredGraph: KnowledgeGraph = { - entities: filteredEntities, - relations: filteredRelations, - }; - - return filteredGraph; - } } const knowledgeGraphManager = new KnowledgeGraphManager(); @@ -188,234 +190,686 @@ const knowledgeGraphManager = new KnowledgeGraphManager(); // The server instance and tools exposed to Claude const server = new Server({ - name: "memory-server", - version: "0.6.3", -}, { + name: "memory-server", + version: "0.6.3", +}, { capabilities: { - tools: {}, + tools: {}, }, - },); +},); server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - 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" - }, + return { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"], + }, + }, + }, + required: ["entities"], }, - required: ["name", "entityType", "observations"], - }, }, - }, - required: ["entities"], - }, - }, - { - 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" }, + { + 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"], + }, + }, + }, + required: ["relations"], }, - required: ["from", "to", "relationType"], - }, }, - }, - required: ["relations"], - }, - }, - { - 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" - }, + { + 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"], + }, + }, + }, + required: ["observations"], }, - required: ["entityName", "contents"], - }, }, - }, - required: ["observations"], - }, - }, - { - 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" + { + 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"], + }, }, - }, - required: ["entityNames"], - }, - }, - { - 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" - }, + { + 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"], + }, + }, + }, + required: ["deletions"], }, - required: ["entityName", "observations"], - }, }, - }, - required: ["deletions"], - }, - }, - { - 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" }, + { + 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"], + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], }, - required: ["from", "to", "relationType"], - }, - description: "An array of relations to delete" }, - }, - required: ["relations"], - }, - }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {}, - }, - }, - { - 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"], - }, - }, - { - 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", + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + }, }, - }, - required: ["names"], - }, - }, - ], - }; + { + 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"], + }, + }, + { + 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"], + }, + }, + ], + }; }); 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) { - throw new Error(`No arguments provided for tool: ${name}`); - } - - switch (name) { - case "create_entities": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; - case "create_relations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; - case "add_observations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; - case "delete_entities": - await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); - return { content: [{ type: "text", text: "Entities deleted successfully" }] }; - case "delete_observations": - await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); - return { content: [{ type: "text", text: "Observations deleted successfully" }] }; - case "delete_relations": - await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); - return { content: [{ type: "text", text: "Relations deleted successfully" }] }; - 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) }] }; - default: - throw new Error(`Unknown tool: ${name}`); - } + const { name, arguments: args } = request.params; + + if (name === "read_graph") { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] }; + } + + if (!args) { + throw new Error(`No arguments provided for tool: ${name}`); + } + + switch (name) { + case "create_entities": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + case "create_relations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + case "add_observations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + case "delete_entities": + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + case "delete_observations": + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + case "delete_relations": + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + 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) }] }; + default: + throw new Error(`Unknown tool: ${name}`); + } }); +function createServer() { + const server = new Server({ + name: "memory-server", + version: "0.6.3", + }, { + capabilities: { + tools: {}, + }, + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"], + }, + }, + }, + required: ["entities"], + }, + }, + { + 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"], + }, + }, + }, + required: ["relations"], + }, + }, + { + 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"], + }, + }, + }, + required: ["observations"], + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + }, + required: ["deletions"], + }, + }, + { + 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"], + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], + }, + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + 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"], + }, + }, + { + 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"], + }, + }, + ], + }; + }); + + 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) { + throw new Error(`No arguments provided for tool: ${name}`); + } + + switch (name) { + case "create_entities": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + case "create_relations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + case "add_observations": + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + case "delete_entities": + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + case "delete_observations": + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + case "delete_relations": + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + 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) }] }; + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + + return { server, cleanup: async () => { } }; +} + + async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Knowledge Graph MCP Server running on stdio"); + const transportType = process.env.MCP_TRANSPORT || "stdio"; + const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; + + if (transportType === "http" || transportType === "sse") { + const app = express(); + const transports: Map = new Map(); + + // Health check endpoint + app.get("/health", (req, res) => { + res.status(200).json({ + status: "healthy", + timestamp: new Date().toISOString(), + service: "memory-mcp-server", + version: "0.6.3" + }); + }); + + // Readiness check endpoint + app.get("/ready", async (req, res) => { + try { + // Test if we can access the knowledge graph + await knowledgeGraphManager.readGraph(); + res.status(200).json({ + status: "ready", + timestamp: new Date().toISOString(), + service: "memory-mcp-server" + }); + } catch (error) { + res.status(503).json({ + status: "not ready", + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : "Unknown error" + }); + } + }); + + // Add CORS support for MCP clients + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.sendStatus(200); + return; + } + next(); + }); + + // Parse JSON bodies + app.use(express.json()); + + // HTTP JSON-RPC endpoint for MCP clients like Claude CLI + app.post("/", async (req, res) => { + try { + const { method, id, params } = req.body; + let response; + + console.error(`HTTP MCP request: ${method}`); + + if (method === "initialize") { + response = { + jsonrpc: "2.0", + id, + result: { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: { + name: "memory-server", + version: "0.6.3" + } + } + }; + } else if (method === "notifications/initialized") { + // For notifications, we don't send a response + res.status(204).end(); + return; + } else if (method === "tools/list") { + // Return our tools directly + response = { + jsonrpc: "2.0", + id, + result: { + tools: [ + { + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + 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"] + } + } + }, + required: ["entities"] + } + }, + { + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {} + } + }, + { + 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"] + } + } + ] + } + }; + } else if (method === "tools/call") { + // Handle tool calls directly + const { name, arguments: args } = params; + let result; + + switch (name) { + case "create_entities": + result = await knowledgeGraphManager.createEntities(args.entities); + break; + case "read_graph": + result = await knowledgeGraphManager.readGraph(); + break; + case "search_nodes": + result = await knowledgeGraphManager.searchNodes(args.query); + break; + default: + throw new Error(`Unknown tool: ${name}`); + } + + response = { + jsonrpc: "2.0", + id, + result: { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + } + }; + } else { + response = { + jsonrpc: "2.0", + id, + error: { + code: -32601, + message: "Method not found" + } + }; + } + + res.json(response); + } catch (error) { + console.error("HTTP MCP request error:", error); + res.status(500).json({ + jsonrpc: "2.0", + id: req.body.id, + error: { + code: -32603, + message: "Internal error", + data: error instanceof Error ? error.message : "Unknown error" + } + }); + } + }); + + // SSE endpoint for compatibility + app.get("/sse", async (req, res) => { + const { server, cleanup } = createServer(); + + const transport = new SSEServerTransport("/message", res); + transports.set(transport.sessionId, transport); + + try { + await server.connect(transport); + console.error("MCP Client Connected via SSE: ", transport.sessionId); + + server.onclose = async () => { + console.error("MCP Client Disconnected: ", transport.sessionId); + transports.delete(transport.sessionId); + await cleanup(); + }; + } catch (error) { + console.error("Failed to connect server to transport:", error); + res.status(500).end(); + } + }); + + app.post("/message", async (req, res) => { + const sessionId = (req?.query?.sessionId as string); + const transport = transports.get(sessionId); + if (transport) { + console.error("Client Message from", sessionId); + await transport.handlePostMessage(req, res); + } else { + console.error(`No transport found for sessionId ${sessionId}`) + } + }); + + app.listen(port, () => { + console.error(`Knowledge Graph MCP Server running on HTTP port ${port}`); + }); + } else { + const { server } = createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Knowledge Graph MCP Server running on stdio"); + } } main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); \ No newline at end of file + console.error("Fatal error in main():", error); + process.exit(1); +});