feat(memory): expose knowledge graph as MCP Resource (#3323)

* feat(memory): expose knowledge graph as MCP Resource

Register the knowledge graph as a readable MCP Resource at
memory://knowledge-graph, enabling clients to discover and read
the graph via the Resources protocol without invoking a tool.

Mutation tools (create_entities, create_relations, add_observations,
delete_entities, delete_observations, delete_relations) now emit
notifications/resources/updated so subscribed clients see live
graph changes.

Resource registration is extracted into the exported
registerKnowledgeGraphResource() function so it can be unit-tested
in isolation, mirroring the everything server's registration
pattern. New tests assert registerResource is called with the
correct kebab-case name, URI, mime type, and that the handler
returns the graph as JSON in the contents array.

Addresses PR #3323 review feedback:
- kebab-case resource name per CLAUDE.md style guide
- single-line comment per CLAUDE.md style guide
- title metadata for client discoverability
- resource update notifications after mutations
- test coverage matching everything/registrations.test.ts pattern
- README documents the new Resource

* feat(memory): support resources/subscribe for the knowledge-graph resource

PR #3323 emitted notifications/resources/updated after every mutation but
the server only advertised resources.listChanged, so resources/subscribe
returned -32601 and a spec-compliant client could never actually subscribe.

Declare resources.subscribe, add subscribe/unsubscribe request handlers that
track subscribed URIs, and only emit notifications/resources/updated when the
client has subscribed to that URI.

Co-authored-by: Lazreg Raed <lazregraed222@gmail.com>

---------

Co-authored-by: olaservo <olahungerford@gmail.com>
This commit is contained in:
Lazreg Raed 2026-06-17 03:40:51 +02:00 • committed by GitHub
commit 7b1170d1da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2,6 +2,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { promises as fs } from 'fs';
import path from 'path';
@ -258,6 +259,20 @@ const server = new McpServer({
version: "0.6.3",
});
const RESOURCE_URI = "memory://knowledge-graph";
// Track which resource URIs the connected client has subscribed to, so we only
// emit notifications/resources/updated to a client that asked for them.
const resourceSubscribers = new Set<string>();
// Notify subscribers that the knowledge graph resource changed. No-op when the
// client has not subscribed.
function notifyGraphUpdated() {
if (resourceSubscribers.has(RESOURCE_URI)) {
server.server.sendResourceUpdated({ uri: RESOURCE_URI });
}
}
// Register create_entities tool
server.registerTool(
"create_entities",
@ -279,6 +294,7 @@ server.registerTool(
},
async ({ entities }) => {
const result = await knowledgeGraphManager.createEntities(entities);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
structuredContent: { entities: result }
@ -307,6 +323,7 @@ server.registerTool(
},
async ({ relations }) => {
const result = await knowledgeGraphManager.createRelations(relations);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
structuredContent: { relations: result }
@ -341,6 +358,7 @@ server.registerTool(
},
async ({ observations }) => {
const result = await knowledgeGraphManager.addObservations(observations);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
structuredContent: { results: result }
@ -370,6 +388,7 @@ server.registerTool(
},
async ({ entityNames }) => {
await knowledgeGraphManager.deleteEntities(entityNames);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: "Entities deleted successfully" }],
structuredContent: { success: true, message: "Entities deleted successfully" }
@ -402,6 +421,7 @@ server.registerTool(
},
async ({ deletions }) => {
await knowledgeGraphManager.deleteObservations(deletions);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: "Observations deleted successfully" }],
structuredContent: { success: true, message: "Observations deleted successfully" }
@ -431,6 +451,7 @@ server.registerTool(
},
async ({ relations }) => {
await knowledgeGraphManager.deleteRelations(relations);
notifyGraphUpdated();
return {
content: [{ type: "text" as const, text: "Relations deleted successfully" }],
structuredContent: { success: true, message: "Relations deleted successfully" }
@ -523,12 +544,52 @@ server.registerTool(
}
);
async function main() {
// Initialize memory file path with backward compatibility
MEMORY_FILE_PATH = await ensureMemoryFilePath();
export function registerKnowledgeGraphResource(
server: McpServer,
manager: KnowledgeGraphManager,
) {
server.registerResource(
"knowledge-graph",
RESOURCE_URI,
{
title: "Knowledge Graph",
description: "The full knowledge graph with all entities and relations",
mimeType: "application/json",
},
async (uri) => {
const graph = await manager.readGraph();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(graph, null, 2),
},
],
};
},
);
}
// Initialize knowledge graph manager with the memory file path
// Enable clients to subscribe to the knowledge-graph resource and receive
// notifications/resources/updated when mutation tools change the graph.
export function registerKnowledgeGraphSubscriptions(server: McpServer) {
server.server.registerCapabilities({ resources: { subscribe: true } });
server.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
resourceSubscribers.add(request.params.uri);
return {};
});
server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
resourceSubscribers.delete(request.params.uri);
return {};
});
}
async function main() {
MEMORY_FILE_PATH = await ensureMemoryFilePath();
knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_FILE_PATH);
registerKnowledgeGraphResource(server, knowledgeGraphManager);
registerKnowledgeGraphSubscriptions(server);
const transport = new StdioServerTransport();
await server.connect(transport);