diff --git a/src/memory/README.md b/src/memory/README.md index fdb8c4613e..cbcd767425 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -125,6 +125,13 @@ Example: - Relations between requested entities - Silently skips non-existent nodes +### Resources + +- **`memory://graph`** (`knowledge-graph`) + - The full knowledge graph (all entities and relations) as JSON + - MIME type: `application/json` + - Read-only; lets MCP clients attach the whole graph as context without calling the `read_graph` tool + # Usage with Claude Desktop ### Setup diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 236242413a..87a87d23ba 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { KnowledgeGraphManager, Entity, Relation, KnowledgeGraph } from '../index.js'; +import { KnowledgeGraphManager, Entity, Relation, KnowledgeGraph, readGraphResource } from '../index.js'; describe('KnowledgeGraphManager', () => { let manager: KnowledgeGraphManager; @@ -516,3 +516,49 @@ describe('KnowledgeGraphManager', () => { }); }); }); + +describe('readGraphResource', () => { + let manager: KnowledgeGraphManager; + let testFilePath: string; + + beforeEach(() => { + testFilePath = path.join( + path.dirname(fileURLToPath(import.meta.url)), + `test-resource-${Date.now()}.jsonl` + ); + manager = new KnowledgeGraphManager(testFilePath); + }); + + afterEach(async () => { + try { + await fs.unlink(testFilePath); + } catch { + // Ignore if the file doesn't exist + } + }); + + it('returns the full graph as a JSON resource', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['likes coffee'] }, + ]); + + const result = await readGraphResource(manager, 'memory://graph'); + + expect(result.contents).toHaveLength(1); + expect(result.contents[0].uri).toBe('memory://graph'); + expect(result.contents[0].mimeType).toBe('application/json'); + + const data = JSON.parse(result.contents[0].text); + expect(data.entities).toHaveLength(1); + expect(data.entities[0].name).toBe('Alice'); + expect(data).toHaveProperty('relations'); + }); + + it('returns an empty graph when nothing has been stored', async () => { + const result = await readGraphResource(manager); + + const data = JSON.parse(result.contents[0].text); + expect(data.entities).toEqual([]); + expect(data.relations).toEqual([]); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index 7b4c683300..0c532e2245 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -253,6 +253,24 @@ const RelationSchema = z.object({ }); // The server instance and tools exposed to Claude +// Build the MCP resource contents for the full knowledge graph. +// Exported so it can be unit-tested independently of the server transport. +export async function readGraphResource( + manager: KnowledgeGraphManager, + uri: string = "memory://graph", +) { + const graph = await manager.readGraph(); + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify(graph, null, 2), + }, + ], + }; +} + const server = new McpServer({ name: "memory-server", version: "0.6.3", @@ -523,6 +541,20 @@ server.registerTool( } ); +// Expose the full knowledge graph as a readable resource (read-only), +// complementing the `read_graph` tool. Resources are the idiomatic way to +// surface read-only data so MCP clients can attach it as context. +server.registerResource( + "knowledge-graph", + "memory://graph", + { + title: "Knowledge Graph", + description: "The full knowledge graph (all entities and relations) as JSON", + mimeType: "application/json", + }, + async (uri) => readGraphResource(knowledgeGraphManager, uri.href), +); + async function main() { // Initialize memory file path with backward compatibility MEMORY_FILE_PATH = await ensureMemoryFilePath();