@coreidentitylabs/open-graph-memory-mcp 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.agents/skills/mcp-builder/LICENSE.txt +202 -0
  2. package/.agents/skills/mcp-builder/SKILL.md +236 -0
  3. package/.agents/skills/mcp-builder/reference/evaluation.md +602 -0
  4. package/.agents/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  5. package/.agents/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  6. package/.agents/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  7. package/.agents/skills/mcp-builder/scripts/connections.py +151 -0
  8. package/.agents/skills/mcp-builder/scripts/evaluation.py +373 -0
  9. package/.agents/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  10. package/.agents/skills/mcp-builder/scripts/requirements.txt +2 -0
  11. package/.env.example +26 -0
  12. package/Implementation Plan.md +358 -0
  13. package/README.md +187 -0
  14. package/dist/constants.d.ts +34 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +40 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/encoding/embedder.d.ts +12 -0
  19. package/dist/encoding/embedder.d.ts.map +1 -0
  20. package/dist/encoding/embedder.js +85 -0
  21. package/dist/encoding/embedder.js.map +1 -0
  22. package/dist/encoding/pipeline.d.ts +28 -0
  23. package/dist/encoding/pipeline.d.ts.map +1 -0
  24. package/dist/encoding/pipeline.js +146 -0
  25. package/dist/encoding/pipeline.js.map +1 -0
  26. package/dist/evolution/consolidator.d.ts +12 -0
  27. package/dist/evolution/consolidator.d.ts.map +1 -0
  28. package/dist/evolution/consolidator.js +212 -0
  29. package/dist/evolution/consolidator.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +53 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/llm/openai-provider.d.ts +23 -0
  35. package/dist/llm/openai-provider.d.ts.map +1 -0
  36. package/dist/llm/openai-provider.js +141 -0
  37. package/dist/llm/openai-provider.js.map +1 -0
  38. package/dist/llm/prompts.d.ts +10 -0
  39. package/dist/llm/prompts.d.ts.map +1 -0
  40. package/dist/llm/prompts.js +63 -0
  41. package/dist/llm/prompts.js.map +1 -0
  42. package/dist/llm/provider.d.ts +7 -0
  43. package/dist/llm/provider.d.ts.map +1 -0
  44. package/dist/llm/provider.js +25 -0
  45. package/dist/llm/provider.js.map +1 -0
  46. package/dist/resources/context-resource.d.ts +8 -0
  47. package/dist/resources/context-resource.d.ts.map +1 -0
  48. package/dist/resources/context-resource.js +51 -0
  49. package/dist/resources/context-resource.js.map +1 -0
  50. package/dist/retrieval/search.d.ts +24 -0
  51. package/dist/retrieval/search.d.ts.map +1 -0
  52. package/dist/retrieval/search.js +143 -0
  53. package/dist/retrieval/search.js.map +1 -0
  54. package/dist/storage/factory.d.ts +10 -0
  55. package/dist/storage/factory.d.ts.map +1 -0
  56. package/dist/storage/factory.js +35 -0
  57. package/dist/storage/factory.js.map +1 -0
  58. package/dist/storage/json-store.d.ts +34 -0
  59. package/dist/storage/json-store.d.ts.map +1 -0
  60. package/dist/storage/json-store.js +248 -0
  61. package/dist/storage/json-store.js.map +1 -0
  62. package/dist/storage/neo4j-store.d.ts +31 -0
  63. package/dist/storage/neo4j-store.d.ts.map +1 -0
  64. package/dist/storage/neo4j-store.js +440 -0
  65. package/dist/storage/neo4j-store.js.map +1 -0
  66. package/dist/tools/memory-tools.d.ts +4 -0
  67. package/dist/tools/memory-tools.d.ts.map +1 -0
  68. package/dist/tools/memory-tools.js +873 -0
  69. package/dist/tools/memory-tools.js.map +1 -0
  70. package/dist/types.d.ts +129 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +5 -0
  73. package/dist/types.js.map +1 -0
  74. package/implementation_plan.md.resolved.md +322 -0
  75. package/package.json +43 -0
  76. package/src/constants.ts +52 -0
  77. package/src/encoding/embedder.ts +93 -0
  78. package/src/encoding/pipeline.ts +197 -0
  79. package/src/evolution/consolidator.ts +281 -0
  80. package/src/index.ts +67 -0
  81. package/src/llm/openai-provider.ts +208 -0
  82. package/src/llm/prompts.ts +66 -0
  83. package/src/llm/provider.ts +37 -0
  84. package/src/resources/context-resource.ts +74 -0
  85. package/src/retrieval/search.ts +203 -0
  86. package/src/storage/factory.ts +48 -0
  87. package/src/storage/json-store.ts +325 -0
  88. package/src/storage/neo4j-store.ts +564 -0
  89. package/src/tools/memory-tools.ts +1067 -0
  90. package/src/types.ts +207 -0
  91. package/tsconfig.json +21 -0
@@ -0,0 +1,1067 @@
1
+ // =============================================================================
2
+ // MCP Tools — Agent-Driven Memory Operations
3
+ // =============================================================================
4
+
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { z } from "zod";
7
+ import { v4 as uuidv4 } from "uuid";
8
+ import type {
9
+ StorageBackend,
10
+ MemoryNode,
11
+ MemoryEdge,
12
+ NodeType,
13
+ ConsolidationStrategy,
14
+ LLMProvider,
15
+ } from "../types.js";
16
+ import { generateLocalEmbedding } from "../encoding/embedder.js";
17
+ import { hybridSearch, getContextForTopic } from "../retrieval/search.js";
18
+ import { consolidateMemory } from "../evolution/consolidator.js";
19
+ import { encodeText } from "../encoding/pipeline.js";
20
+ import {
21
+ DEFAULT_PAGE_SIZE,
22
+ MAX_PAGE_SIZE,
23
+ CHARACTER_LIMIT,
24
+ } from "../constants.js";
25
+
26
+ // =============================================================================
27
+ // Tool Registration
28
+ // =============================================================================
29
+
30
+ export function registerMemoryTools(
31
+ server: McpServer,
32
+ store: StorageBackend,
33
+ llm?: LLMProvider | null,
34
+ ): void {
35
+ // ---- WRITE TOOLS ----
36
+
37
+ registerAddEntities(server, store);
38
+ registerAddRelations(server, store);
39
+ registerSaveConversation(server, store);
40
+
41
+ // ---- READ TOOLS ----
42
+
43
+ registerSearch(server, store);
44
+ registerGetEntity(server, store);
45
+ registerListEntities(server, store);
46
+ registerGetRelations(server, store);
47
+ registerGetContext(server, store);
48
+
49
+ // ---- MANAGEMENT TOOLS ----
50
+
51
+ registerDeleteEntity(server, store);
52
+ registerConsolidate(server, store);
53
+ registerStatus(server, store);
54
+
55
+ // ---- OPTIONAL SERVER-SIDE ENCODING ----
56
+
57
+ if (llm) {
58
+ registerEncodeText(server, store, llm);
59
+ console.error(
60
+ `[open-memory] memory_encode_text tool enabled (LLM configured)`,
61
+ );
62
+ }
63
+ }
64
+
65
+ // =============================================================================
66
+ // Write Tools
67
+ // =============================================================================
68
+
69
+ function registerAddEntities(server: McpServer, store: StorageBackend): void {
70
+ const EntitySchema = z.object({
71
+ name: z
72
+ .string()
73
+ .min(1)
74
+ .describe("Entity name (e.g., 'React Query', 'UserService')"),
75
+ type: z
76
+ .enum([
77
+ "entity",
78
+ "concept",
79
+ "event",
80
+ "code_pattern",
81
+ "decision",
82
+ "conversation",
83
+ ])
84
+ .default("entity")
85
+ .describe("Node type"),
86
+ description: z.string().min(1).describe("Description of the entity"),
87
+ metadata: z
88
+ .record(z.unknown())
89
+ .optional()
90
+ .describe("Optional key-value metadata"),
91
+ });
92
+
93
+ const InputSchema = z.object({
94
+ entities: z
95
+ .array(EntitySchema)
96
+ .min(1)
97
+ .max(50)
98
+ .describe("Array of entities to add to memory"),
99
+ });
100
+
101
+ server.registerTool(
102
+ "memory_add_entities",
103
+ {
104
+ title: "Add Entities to Memory",
105
+ description: `Store entities (people, tools, concepts, code patterns, decisions) in the memory graph.
106
+ The agent extracts entities from conversation and passes structured data here.
107
+ Server auto-generates embeddings for semantic search.
108
+
109
+ Returns: IDs and names of stored entities.`,
110
+ inputSchema: InputSchema,
111
+ annotations: {
112
+ readOnlyHint: false,
113
+ destructiveHint: false,
114
+ idempotentHint: false,
115
+ openWorldHint: false,
116
+ },
117
+ },
118
+ async (params: z.infer<typeof InputSchema>) => {
119
+ try {
120
+ const results: { id: string; name: string; status: string }[] = [];
121
+
122
+ for (const entity of params.entities) {
123
+ // Check for existing entity by name
124
+ const existing = await store.getNodeByName(entity.name);
125
+ if (existing) {
126
+ // Update existing entity
127
+ await store.updateNode(existing.id, {
128
+ description: entity.description,
129
+ metadata: { ...existing.metadata, ...entity.metadata },
130
+ embedding: generateLocalEmbedding(
131
+ `${entity.name} ${entity.description}`,
132
+ ),
133
+ });
134
+ results.push({
135
+ id: existing.id,
136
+ name: entity.name,
137
+ status: "updated",
138
+ });
139
+ } else {
140
+ // Create new entity
141
+ const now = new Date().toISOString();
142
+ const node: MemoryNode = {
143
+ id: uuidv4(),
144
+ name: entity.name,
145
+ type: entity.type as NodeType,
146
+ description: entity.description,
147
+ embedding: generateLocalEmbedding(
148
+ `${entity.name} ${entity.description}`,
149
+ ),
150
+ metadata: entity.metadata ?? {},
151
+ createdAt: now,
152
+ updatedAt: now,
153
+ source: "agent",
154
+ accessCount: 0,
155
+ };
156
+ await store.addNode(node);
157
+ results.push({ id: node.id, name: entity.name, status: "created" });
158
+ }
159
+ }
160
+
161
+ const output = { stored: results.length, entities: results };
162
+ return {
163
+ content: [
164
+ { type: "text" as const, text: JSON.stringify(output, null, 2) },
165
+ ],
166
+ };
167
+ } catch (error) {
168
+ return errorResponse(error);
169
+ }
170
+ },
171
+ );
172
+ }
173
+
174
+ function registerAddRelations(server: McpServer, store: StorageBackend): void {
175
+ const RelationSchema = z.object({
176
+ source: z.string().min(1).describe("Source entity name or ID"),
177
+ target: z.string().min(1).describe("Target entity name or ID"),
178
+ relation: z
179
+ .string()
180
+ .min(1)
181
+ .describe("Relationship type (e.g., 'uses', 'depends_on', 'decided_to')"),
182
+ description: z
183
+ .string()
184
+ .optional()
185
+ .describe("Description of the relationship"),
186
+ weight: z
187
+ .number()
188
+ .min(0)
189
+ .max(1)
190
+ .default(0.8)
191
+ .describe("Confidence weight (0-1)"),
192
+ });
193
+
194
+ const InputSchema = z.object({
195
+ relations: z
196
+ .array(RelationSchema)
197
+ .min(1)
198
+ .max(50)
199
+ .describe("Array of relationships to add"),
200
+ });
201
+
202
+ server.registerTool(
203
+ "memory_add_relations",
204
+ {
205
+ title: "Add Relations to Memory",
206
+ description: `Store relationships between entities in the memory graph.
207
+ Source and target can be entity names (resolved automatically) or IDs.
208
+
209
+ Returns: IDs and details of stored edges.`,
210
+ inputSchema: InputSchema,
211
+ annotations: {
212
+ readOnlyHint: false,
213
+ destructiveHint: false,
214
+ idempotentHint: false,
215
+ openWorldHint: false,
216
+ },
217
+ },
218
+ async (params: z.infer<typeof InputSchema>) => {
219
+ try {
220
+ const results: {
221
+ id: string;
222
+ source: string;
223
+ target: string;
224
+ relation: string;
225
+ status: string;
226
+ }[] = [];
227
+
228
+ for (const rel of params.relations) {
229
+ // Resolve source and target by name or ID
230
+ const sourceNode =
231
+ (await store.getNode(rel.source)) ??
232
+ (await store.getNodeByName(rel.source));
233
+ const targetNode =
234
+ (await store.getNode(rel.target)) ??
235
+ (await store.getNodeByName(rel.target));
236
+
237
+ if (!sourceNode) {
238
+ results.push({
239
+ id: "",
240
+ source: rel.source,
241
+ target: rel.target,
242
+ relation: rel.relation,
243
+ status: `error: source '${rel.source}' not found`,
244
+ });
245
+ continue;
246
+ }
247
+ if (!targetNode) {
248
+ results.push({
249
+ id: "",
250
+ source: rel.source,
251
+ target: rel.target,
252
+ relation: rel.relation,
253
+ status: `error: target '${rel.target}' not found`,
254
+ });
255
+ continue;
256
+ }
257
+
258
+ const now = new Date().toISOString();
259
+ const edge: MemoryEdge = {
260
+ id: uuidv4(),
261
+ source: sourceNode.id,
262
+ target: targetNode.id,
263
+ relation: rel.relation,
264
+ description:
265
+ rel.description ??
266
+ `${sourceNode.name} ${rel.relation} ${targetNode.name}`,
267
+ weight: rel.weight,
268
+ metadata: {},
269
+ createdAt: now,
270
+ updatedAt: now,
271
+ };
272
+
273
+ await store.addEdge(edge);
274
+ results.push({
275
+ id: edge.id,
276
+ source: sourceNode.name,
277
+ target: targetNode.name,
278
+ relation: rel.relation,
279
+ status: "created",
280
+ });
281
+ }
282
+
283
+ const output = {
284
+ stored: results.filter((r) => r.status === "created").length,
285
+ relations: results,
286
+ };
287
+ return {
288
+ content: [
289
+ { type: "text" as const, text: JSON.stringify(output, null, 2) },
290
+ ],
291
+ };
292
+ } catch (error) {
293
+ return errorResponse(error);
294
+ }
295
+ },
296
+ );
297
+ }
298
+
299
+ function registerSaveConversation(
300
+ server: McpServer,
301
+ store: StorageBackend,
302
+ ): void {
303
+ const MessageSchema = z.object({
304
+ role: z.string().describe("Message role (user, assistant, system)"),
305
+ content: z.string().describe("Message content"),
306
+ });
307
+
308
+ const InputSchema = z.object({
309
+ messages: z
310
+ .array(MessageSchema)
311
+ .min(1)
312
+ .describe("Conversation messages to store"),
313
+ context: z
314
+ .string()
315
+ .optional()
316
+ .describe("Optional summary or context label for this conversation"),
317
+ });
318
+
319
+ server.registerTool(
320
+ "memory_save_conversation",
321
+ {
322
+ title: "Save Conversation to Memory",
323
+ description: `Store a conversation snapshot as an event node in the memory graph.
324
+ Useful for preserving conversation history across sessions.
325
+
326
+ Returns: ID of the stored conversation node.`,
327
+ inputSchema: InputSchema,
328
+ annotations: {
329
+ readOnlyHint: false,
330
+ destructiveHint: false,
331
+ idempotentHint: false,
332
+ openWorldHint: false,
333
+ },
334
+ },
335
+ async (params: z.infer<typeof InputSchema>) => {
336
+ try {
337
+ const now = new Date().toISOString();
338
+ const content = params.messages
339
+ .map((m) => `[${m.role}]: ${m.content}`)
340
+ .join("\n");
341
+ const summary = params.context ?? `Conversation at ${now}`;
342
+
343
+ const node: MemoryNode = {
344
+ id: uuidv4(),
345
+ name: summary,
346
+ type: "conversation",
347
+ description:
348
+ content.length > 2000
349
+ ? content.substring(0, 2000) + "..."
350
+ : content,
351
+ embedding: generateLocalEmbedding(
352
+ `${summary} ${content.substring(0, 500)}`,
353
+ ),
354
+ metadata: {
355
+ messageCount: params.messages.length,
356
+ fullContent: content,
357
+ },
358
+ createdAt: now,
359
+ updatedAt: now,
360
+ source: "conversation",
361
+ accessCount: 0,
362
+ };
363
+
364
+ await store.addNode(node);
365
+
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text" as const,
370
+ text: JSON.stringify(
371
+ {
372
+ id: node.id,
373
+ name: node.name,
374
+ messageCount: params.messages.length,
375
+ status: "saved",
376
+ },
377
+ null,
378
+ 2,
379
+ ),
380
+ },
381
+ ],
382
+ };
383
+ } catch (error) {
384
+ return errorResponse(error);
385
+ }
386
+ },
387
+ );
388
+ }
389
+
390
+ // =============================================================================
391
+ // Read Tools
392
+ // =============================================================================
393
+
394
+ function registerSearch(server: McpServer, store: StorageBackend): void {
395
+ const InputSchema = z.object({
396
+ query: z.string().min(1).describe("Search query (natural language)"),
397
+ topK: z.number().int().min(1).max(50).default(10).describe("Max results"),
398
+ type: z
399
+ .enum([
400
+ "entity",
401
+ "concept",
402
+ "event",
403
+ "code_pattern",
404
+ "decision",
405
+ "conversation",
406
+ ])
407
+ .optional()
408
+ .describe("Filter by node type"),
409
+ timeRange: z
410
+ .object({
411
+ after: z
412
+ .string()
413
+ .optional()
414
+ .describe("ISO timestamp — only results after this"),
415
+ before: z
416
+ .string()
417
+ .optional()
418
+ .describe("ISO timestamp — only results before this"),
419
+ })
420
+ .optional()
421
+ .describe("Time range filter"),
422
+ });
423
+
424
+ server.registerTool(
425
+ "memory_search",
426
+ {
427
+ title: "Search Memory",
428
+ description: `Hybrid search across the memory graph: combines text matching, semantic similarity, and graph traversal.
429
+ Returns entities, their scores, and related edges.
430
+
431
+ Use this to find relevant context before making decisions or writing code.`,
432
+ inputSchema: InputSchema,
433
+ annotations: {
434
+ readOnlyHint: true,
435
+ destructiveHint: false,
436
+ idempotentHint: true,
437
+ openWorldHint: false,
438
+ },
439
+ },
440
+ async (params: z.infer<typeof InputSchema>) => {
441
+ try {
442
+ const results = await hybridSearch(store, {
443
+ query: params.query,
444
+ topK: params.topK,
445
+ type: params.type as NodeType | undefined,
446
+ timeRange: params.timeRange,
447
+ });
448
+
449
+ const output = {
450
+ totalMatches: results.totalNodes,
451
+ returnedCount: results.nodes.length,
452
+ results: results.nodes.map((r) => ({
453
+ id: r.node.id,
454
+ name: r.node.name,
455
+ type: r.node.type,
456
+ description: r.node.description,
457
+ score: Math.round(r.score * 1000) / 1000,
458
+ matchType: r.matchType,
459
+ updatedAt: r.node.updatedAt,
460
+ })),
461
+ relatedEdges: results.relatedEdges.slice(0, 20).map((e) => ({
462
+ source: e.source,
463
+ target: e.target,
464
+ relation: e.relation,
465
+ description: e.description,
466
+ })),
467
+ };
468
+
469
+ let text = JSON.stringify(output, null, 2);
470
+ if (text.length > CHARACTER_LIMIT) {
471
+ output.results = output.results.slice(0, 5);
472
+ output.relatedEdges = output.relatedEdges.slice(0, 5);
473
+ text = JSON.stringify({ ...output, truncated: true }, null, 2);
474
+ }
475
+
476
+ return {
477
+ content: [{ type: "text" as const, text }],
478
+ };
479
+ } catch (error) {
480
+ return errorResponse(error);
481
+ }
482
+ },
483
+ );
484
+ }
485
+
486
+ function registerGetEntity(server: McpServer, store: StorageBackend): void {
487
+ const InputSchema = z
488
+ .object({
489
+ name: z.string().optional().describe("Entity name to look up"),
490
+ id: z.string().optional().describe("Entity ID to look up"),
491
+ })
492
+ .refine((data) => data.name || data.id, {
493
+ message: "Either 'name' or 'id' must be provided",
494
+ });
495
+
496
+ server.registerTool(
497
+ "memory_get_entity",
498
+ {
499
+ title: "Get Entity Details",
500
+ description: `Get full details of a specific entity including all its relationships.
501
+ Provide either entity name or ID.`,
502
+ inputSchema: InputSchema,
503
+ annotations: {
504
+ readOnlyHint: true,
505
+ destructiveHint: false,
506
+ idempotentHint: true,
507
+ openWorldHint: false,
508
+ },
509
+ },
510
+ async (params: z.infer<typeof InputSchema>) => {
511
+ try {
512
+ let node: MemoryNode | null = null;
513
+ if (params.id) {
514
+ node = await store.getNode(params.id);
515
+ } else if (params.name) {
516
+ node = await store.getNodeByName(params.name);
517
+ }
518
+
519
+ if (!node) {
520
+ return {
521
+ content: [
522
+ {
523
+ type: "text" as const,
524
+ text: `Entity not found: ${params.name ?? params.id}`,
525
+ },
526
+ ],
527
+ };
528
+ }
529
+
530
+ // Update access count
531
+ await store.updateNode(node.id, {
532
+ accessCount: node.accessCount + 1,
533
+ lastAccessedAt: new Date().toISOString(),
534
+ });
535
+
536
+ // Get relationships
537
+ const edges = await store.getEdgesForNode(node.id);
538
+
539
+ // Resolve edge node names
540
+ const edgeDetails = await Promise.all(
541
+ edges.map(async (e) => {
542
+ const otherNodeId = e.source === node!.id ? e.target : e.source;
543
+ const otherNode = await store.getNode(otherNodeId);
544
+ return {
545
+ relation: e.relation,
546
+ direction: e.source === node!.id ? "outgoing" : "incoming",
547
+ relatedEntity: otherNode?.name ?? otherNodeId,
548
+ relatedEntityType: otherNode?.type,
549
+ description: e.description,
550
+ weight: e.weight,
551
+ };
552
+ }),
553
+ );
554
+
555
+ const output = {
556
+ id: node.id,
557
+ name: node.name,
558
+ type: node.type,
559
+ description: node.description,
560
+ metadata: node.metadata,
561
+ createdAt: node.createdAt,
562
+ updatedAt: node.updatedAt,
563
+ validFrom: node.validFrom,
564
+ validUntil: node.validUntil,
565
+ accessCount: node.accessCount,
566
+ relationships: edgeDetails,
567
+ };
568
+
569
+ return {
570
+ content: [
571
+ { type: "text" as const, text: JSON.stringify(output, null, 2) },
572
+ ],
573
+ };
574
+ } catch (error) {
575
+ return errorResponse(error);
576
+ }
577
+ },
578
+ );
579
+ }
580
+
581
+ function registerListEntities(server: McpServer, store: StorageBackend): void {
582
+ const InputSchema = z.object({
583
+ type: z
584
+ .enum([
585
+ "entity",
586
+ "concept",
587
+ "event",
588
+ "code_pattern",
589
+ "decision",
590
+ "conversation",
591
+ ])
592
+ .optional()
593
+ .describe("Filter by node type"),
594
+ limit: z
595
+ .number()
596
+ .int()
597
+ .min(1)
598
+ .max(MAX_PAGE_SIZE)
599
+ .default(DEFAULT_PAGE_SIZE)
600
+ .describe("Max results per page"),
601
+ offset: z.number().int().min(0).default(0).describe("Pagination offset"),
602
+ nameContains: z.string().optional().describe("Filter by name substring"),
603
+ });
604
+
605
+ server.registerTool(
606
+ "memory_list_entities",
607
+ {
608
+ title: "List Memory Entities",
609
+ description: `List all entities in the memory graph with optional filtering and pagination.`,
610
+ inputSchema: InputSchema,
611
+ annotations: {
612
+ readOnlyHint: true,
613
+ destructiveHint: false,
614
+ idempotentHint: true,
615
+ openWorldHint: false,
616
+ },
617
+ },
618
+ async (params: z.infer<typeof InputSchema>) => {
619
+ try {
620
+ let results;
621
+ if (params.type || params.nameContains) {
622
+ const allMatches = await store.findNodes({
623
+ type: params.type as NodeType | undefined,
624
+ nameContains: params.nameContains,
625
+ });
626
+ results = {
627
+ nodes: allMatches.slice(
628
+ params.offset,
629
+ params.offset + params.limit,
630
+ ),
631
+ total: allMatches.length,
632
+ };
633
+ } else {
634
+ results = await store.getAllNodes(params.limit, params.offset);
635
+ }
636
+
637
+ const output = {
638
+ total: results.total,
639
+ count: results.nodes.length,
640
+ offset: params.offset,
641
+ entities: results.nodes.map((n) => ({
642
+ id: n.id,
643
+ name: n.name,
644
+ type: n.type,
645
+ description: n.description.substring(0, 200),
646
+ updatedAt: n.updatedAt,
647
+ accessCount: n.accessCount,
648
+ })),
649
+ hasMore: results.total > params.offset + results.nodes.length,
650
+ nextOffset:
651
+ results.total > params.offset + results.nodes.length
652
+ ? params.offset + results.nodes.length
653
+ : undefined,
654
+ };
655
+
656
+ return {
657
+ content: [
658
+ { type: "text" as const, text: JSON.stringify(output, null, 2) },
659
+ ],
660
+ };
661
+ } catch (error) {
662
+ return errorResponse(error);
663
+ }
664
+ },
665
+ );
666
+ }
667
+
668
+ function registerGetRelations(server: McpServer, store: StorageBackend): void {
669
+ const InputSchema = z.object({
670
+ entity: z.string().min(1).describe("Entity name or ID"),
671
+ relation: z.string().optional().describe("Filter by relation type"),
672
+ direction: z
673
+ .enum(["in", "out", "both"])
674
+ .default("both")
675
+ .describe("Edge direction filter"),
676
+ });
677
+
678
+ server.registerTool(
679
+ "memory_get_relations",
680
+ {
681
+ title: "Get Entity Relations",
682
+ description: `Get all relationships for a given entity, optionally filtered by type and direction.`,
683
+ inputSchema: InputSchema,
684
+ annotations: {
685
+ readOnlyHint: true,
686
+ destructiveHint: false,
687
+ idempotentHint: true,
688
+ openWorldHint: false,
689
+ },
690
+ },
691
+ async (params: z.infer<typeof InputSchema>) => {
692
+ try {
693
+ const node =
694
+ (await store.getNode(params.entity)) ??
695
+ (await store.getNodeByName(params.entity));
696
+
697
+ if (!node) {
698
+ return {
699
+ content: [
700
+ {
701
+ type: "text" as const,
702
+ text: `Entity not found: ${params.entity}`,
703
+ },
704
+ ],
705
+ };
706
+ }
707
+
708
+ let edges = await store.getEdgesForNode(node.id, params.direction);
709
+
710
+ if (params.relation) {
711
+ edges = edges.filter((e) => e.relation === params.relation);
712
+ }
713
+
714
+ const edgeDetails = await Promise.all(
715
+ edges.map(async (e) => {
716
+ const otherNodeId = e.source === node.id ? e.target : e.source;
717
+ const otherNode = await store.getNode(otherNodeId);
718
+ return {
719
+ id: e.id,
720
+ relation: e.relation,
721
+ direction: e.source === node.id ? "outgoing" : "incoming",
722
+ relatedEntity: otherNode?.name ?? otherNodeId,
723
+ relatedEntityType: otherNode?.type,
724
+ description: e.description,
725
+ weight: e.weight,
726
+ };
727
+ }),
728
+ );
729
+
730
+ return {
731
+ content: [
732
+ {
733
+ type: "text" as const,
734
+ text: JSON.stringify(
735
+ {
736
+ entity: node.name,
737
+ totalRelations: edgeDetails.length,
738
+ relations: edgeDetails,
739
+ },
740
+ null,
741
+ 2,
742
+ ),
743
+ },
744
+ ],
745
+ };
746
+ } catch (error) {
747
+ return errorResponse(error);
748
+ }
749
+ },
750
+ );
751
+ }
752
+
753
+ function registerGetContext(server: McpServer, store: StorageBackend): void {
754
+ const InputSchema = z.object({
755
+ topic: z.string().min(1).describe("Topic to retrieve context for"),
756
+ maxTokens: z
757
+ .number()
758
+ .int()
759
+ .min(100)
760
+ .max(8000)
761
+ .default(2000)
762
+ .describe("Approximate max tokens in returned context"),
763
+ });
764
+
765
+ server.registerTool(
766
+ "memory_get_context",
767
+ {
768
+ title: "Get Context for Topic",
769
+ description: `Retrieve summarized context for a topic, formatted for direct injection into prompts.
770
+ Use before making decisions or writing code to get historical context.`,
771
+ inputSchema: InputSchema,
772
+ annotations: {
773
+ readOnlyHint: true,
774
+ destructiveHint: false,
775
+ idempotentHint: true,
776
+ openWorldHint: false,
777
+ },
778
+ },
779
+ async (params: z.infer<typeof InputSchema>) => {
780
+ try {
781
+ const context = await getContextForTopic(
782
+ store,
783
+ params.topic,
784
+ params.maxTokens,
785
+ );
786
+
787
+ return {
788
+ content: [{ type: "text" as const, text: context }],
789
+ };
790
+ } catch (error) {
791
+ return errorResponse(error);
792
+ }
793
+ },
794
+ );
795
+ }
796
+
797
+ // =============================================================================
798
+ // Management Tools
799
+ // =============================================================================
800
+
801
+ function registerDeleteEntity(server: McpServer, store: StorageBackend): void {
802
+ const InputSchema = z
803
+ .object({
804
+ name: z.string().optional().describe("Entity name to delete"),
805
+ id: z.string().optional().describe("Entity ID to delete"),
806
+ })
807
+ .refine((data) => data.name || data.id, {
808
+ message: "Either 'name' or 'id' must be provided",
809
+ });
810
+
811
+ server.registerTool(
812
+ "memory_delete_entity",
813
+ {
814
+ title: "Delete Entity from Memory",
815
+ description: `Remove an entity and all its edges from the memory graph. This is destructive.`,
816
+ inputSchema: InputSchema,
817
+ annotations: {
818
+ readOnlyHint: false,
819
+ destructiveHint: true,
820
+ idempotentHint: true,
821
+ openWorldHint: false,
822
+ },
823
+ },
824
+ async (params: z.infer<typeof InputSchema>) => {
825
+ try {
826
+ let node = null;
827
+ if (params.id) node = await store.getNode(params.id);
828
+ else if (params.name) node = await store.getNodeByName(params.name);
829
+
830
+ if (!node) {
831
+ return {
832
+ content: [
833
+ {
834
+ type: "text" as const,
835
+ text: `Entity not found: ${params.name ?? params.id}`,
836
+ },
837
+ ],
838
+ };
839
+ }
840
+
841
+ const deleted = await store.deleteNode(node.id);
842
+ return {
843
+ content: [
844
+ {
845
+ type: "text" as const,
846
+ text: JSON.stringify(
847
+ {
848
+ deleted,
849
+ id: node.id,
850
+ name: node.name,
851
+ },
852
+ null,
853
+ 2,
854
+ ),
855
+ },
856
+ ],
857
+ };
858
+ } catch (error) {
859
+ return errorResponse(error);
860
+ }
861
+ },
862
+ );
863
+ }
864
+
865
+ function registerConsolidate(server: McpServer, store: StorageBackend): void {
866
+ const InputSchema = z.object({
867
+ strategy: z
868
+ .enum(["full", "merge_only", "prune_only", "infer_only"])
869
+ .default("full")
870
+ .describe("Consolidation strategy"),
871
+ });
872
+
873
+ server.registerTool(
874
+ "memory_consolidate",
875
+ {
876
+ title: "Consolidate Memory",
877
+ description: `Run memory consolidation: merge duplicates, infer transitive edges, and prune stale nodes.else
878
+ Strategies: full (all), merge_only, prune_only, infer_only.`,
879
+ inputSchema: InputSchema,
880
+ annotations: {
881
+ readOnlyHint: false,
882
+ destructiveHint: false,
883
+ idempotentHint: true,
884
+ openWorldHint: false,
885
+ },
886
+ },
887
+ async (params: z.infer<typeof InputSchema>) => {
888
+ try {
889
+ const result = await consolidateMemory(
890
+ store,
891
+ params.strategy as ConsolidationStrategy,
892
+ );
893
+
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text" as const,
898
+ text: JSON.stringify(
899
+ {
900
+ ...result,
901
+ durationMs: result.duration,
902
+ status: "completed",
903
+ },
904
+ null,
905
+ 2,
906
+ ),
907
+ },
908
+ ],
909
+ };
910
+ } catch (error) {
911
+ return errorResponse(error);
912
+ }
913
+ },
914
+ );
915
+ }
916
+
917
+ function registerStatus(server: McpServer, store: StorageBackend): void {
918
+ server.registerTool(
919
+ "memory_status",
920
+ {
921
+ title: "Memory Status",
922
+ description: `Get memory graph health: node/edge counts by type, storage backend, and last consolidation time.`,
923
+ inputSchema: {},
924
+ annotations: {
925
+ readOnlyHint: true,
926
+ destructiveHint: false,
927
+ idempotentHint: true,
928
+ openWorldHint: false,
929
+ },
930
+ },
931
+ async () => {
932
+ try {
933
+ const stats = await store.getStats();
934
+
935
+ return {
936
+ content: [
937
+ {
938
+ type: "text" as const,
939
+ text: JSON.stringify(stats, null, 2),
940
+ },
941
+ ],
942
+ };
943
+ } catch (error) {
944
+ return errorResponse(error);
945
+ }
946
+ },
947
+ );
948
+ }
949
+
950
+ // =============================================================================
951
+ // Optional Server-Side Encoding Tool
952
+ // =============================================================================
953
+
954
+ function registerEncodeText(
955
+ server: McpServer,
956
+ store: StorageBackend,
957
+ llm: LLMProvider,
958
+ ): void {
959
+ const InputSchema = z.object({
960
+ text: z
961
+ .string()
962
+ .min(1)
963
+ .describe(
964
+ "Raw text to process (conversation excerpt, code comment, meeting notes, etc.)",
965
+ ),
966
+ autoStore: z
967
+ .boolean()
968
+ .default(true)
969
+ .describe(
970
+ "Automatically store extracted entities and relations in memory (default: true)",
971
+ ),
972
+ });
973
+
974
+ server.registerTool(
975
+ "memory_encode_text",
976
+ {
977
+ title: "Encode Text (Server-Side LLM)",
978
+ description: `Extract entities and relationships from raw text using the server-side LLM.
979
+ This is the automated alternative to manually calling memory_add_entities + memory_add_relations.
980
+ Requires LLM_API_KEY to be configured.
981
+
982
+ Pipeline: text → LLM extraction → entity resolution → embedding → storage
983
+
984
+ Set autoStore=false to preview extraction results without saving.`,
985
+ inputSchema: InputSchema,
986
+ annotations: {
987
+ readOnlyHint: false,
988
+ destructiveHint: false,
989
+ idempotentHint: false,
990
+ openWorldHint: true,
991
+ },
992
+ },
993
+ async (params: z.infer<typeof InputSchema>) => {
994
+ try {
995
+ if (params.autoStore) {
996
+ // Full pipeline: extract + store
997
+ const result = await encodeText(params.text, store, llm);
998
+
999
+ return {
1000
+ content: [
1001
+ {
1002
+ type: "text" as const,
1003
+ text: JSON.stringify(
1004
+ {
1005
+ status: "encoded",
1006
+ entitiesCreated: result.entitiesCreated,
1007
+ entitiesUpdated: result.entitiesUpdated,
1008
+ relationsCreated: result.relationsCreated,
1009
+ details: result.details,
1010
+ },
1011
+ null,
1012
+ 2,
1013
+ ),
1014
+ },
1015
+ ],
1016
+ };
1017
+ } else {
1018
+ // Preview mode: extract only, don't store
1019
+ const { nodes: existingNodes } = await store.getAllNodes(10000, 0);
1020
+ const existingNames = existingNodes.map((n) => n.name);
1021
+ const extraction = await llm.extractEntitiesAndRelations(
1022
+ params.text,
1023
+ existingNames,
1024
+ );
1025
+
1026
+ return {
1027
+ content: [
1028
+ {
1029
+ type: "text" as const,
1030
+ text: JSON.stringify(
1031
+ {
1032
+ status: "preview",
1033
+ message:
1034
+ "Extraction preview — nothing stored. Set autoStore=true to save.",
1035
+ entities: extraction.entities,
1036
+ relations: extraction.relations,
1037
+ },
1038
+ null,
1039
+ 2,
1040
+ ),
1041
+ },
1042
+ ],
1043
+ };
1044
+ }
1045
+ } catch (error) {
1046
+ return errorResponse(error);
1047
+ }
1048
+ },
1049
+ );
1050
+ }
1051
+
1052
+ // =============================================================================
1053
+ // Helpers
1054
+ // =============================================================================
1055
+
1056
+ function errorResponse(error: unknown) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ return {
1059
+ isError: true,
1060
+ content: [
1061
+ {
1062
+ type: "text" as const,
1063
+ text: `Error: ${message}`,
1064
+ },
1065
+ ],
1066
+ };
1067
+ }