collective-memory-mcp 0.1.0 → 0.3.0

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.
package/src/server.js ADDED
@@ -0,0 +1,793 @@
1
+ /**
2
+ * Collective Memory MCP Server
3
+ * A persistent, graph-based memory system for AI agents.
4
+ */
5
+
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import {
9
+ CallToolRequestSchema,
10
+ ListToolsRequestSchema,
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { getStorage } from "./storage.js";
13
+ import { Entity, Relation, ENTITY_TYPES, RELATION_TYPES } from "./models.js";
14
+
15
+ /**
16
+ * Create and configure the MCP server
17
+ */
18
+ function createServer() {
19
+ const storage = getStorage();
20
+
21
+ const server = new Server(
22
+ {
23
+ name: "collective-memory",
24
+ version: "0.1.0",
25
+ },
26
+ {
27
+ capabilities: {
28
+ tools: {},
29
+ },
30
+ }
31
+ );
32
+
33
+ /**
34
+ * List available tools
35
+ */
36
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
37
+ return {
38
+ tools: [
39
+ {
40
+ name: "create_entities",
41
+ description:
42
+ "Create multiple new entities in the knowledge graph. " +
43
+ "Silently ignores entities with duplicate names. Returns list of successfully created entities. " +
44
+ "Valid entity types: agent, task, structure, artifact, session",
45
+ inputSchema: {
46
+ type: "object",
47
+ properties: {
48
+ entities: {
49
+ type: "array",
50
+ items: {
51
+ type: "object",
52
+ properties: {
53
+ name: {
54
+ type: "string",
55
+ description: "Unique entity name",
56
+ },
57
+ entityType: {
58
+ type: "string",
59
+ enum: ENTITY_TYPES,
60
+ description: "Type of entity",
61
+ },
62
+ observations: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ description: "List of atomic facts about this entity",
66
+ },
67
+ },
68
+ required: ["name", "entityType"],
69
+ },
70
+ },
71
+ },
72
+ required: ["entities"],
73
+ },
74
+ },
75
+ {
76
+ name: "create_relations",
77
+ description:
78
+ "Create multiple directed relations between entities. " +
79
+ "Skips duplicate relations. Fails gracefully if source or target entity doesn't exist. " +
80
+ "Valid relation types: executed_by, created, modified, documented, depends_on, part_of, similar_to, uses, implements",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ relations: {
85
+ type: "array",
86
+ items: {
87
+ type: "object",
88
+ properties: {
89
+ from: {
90
+ type: "string",
91
+ description: "Source entity name",
92
+ },
93
+ to: {
94
+ type: "string",
95
+ description: "Target entity name",
96
+ },
97
+ relationType: {
98
+ type: "string",
99
+ enum: RELATION_TYPES,
100
+ description: "Type of relation",
101
+ },
102
+ },
103
+ required: ["from", "to", "relationType"],
104
+ },
105
+ },
106
+ },
107
+ required: ["relations"],
108
+ },
109
+ },
110
+ {
111
+ name: "add_observations",
112
+ description:
113
+ "Add new observations to existing entities. " +
114
+ "Appends observations to existing list. Fails if entity doesn't exist. " +
115
+ "Returns count of added observations per entity.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ observations: {
120
+ type: "array",
121
+ items: {
122
+ type: "object",
123
+ properties: {
124
+ entityName: {
125
+ type: "string",
126
+ description: "Name of the entity",
127
+ },
128
+ contents: {
129
+ type: "array",
130
+ items: { type: "string" },
131
+ description: "Observations to add",
132
+ },
133
+ },
134
+ required: ["entityName", "contents"],
135
+ },
136
+ },
137
+ },
138
+ required: ["observations"],
139
+ },
140
+ },
141
+ {
142
+ name: "delete_entities",
143
+ description:
144
+ "Remove entities and cascade delete their relations. " +
145
+ "Removes all relations where entity is source or target. " +
146
+ "Silent operation if entity doesn't exist. Irreversible operation.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ entityNames: {
151
+ type: "array",
152
+ items: { type: "string" },
153
+ description: "Names of entities to delete",
154
+ },
155
+ },
156
+ required: ["entityNames"],
157
+ },
158
+ },
159
+ {
160
+ name: "delete_observations",
161
+ description:
162
+ "Remove specific observations from entities. " +
163
+ "Removes exact string matches. Silent if observation doesn't exist. " +
164
+ "Preserves other observations on the entity.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ deletions: {
169
+ type: "array",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ entityName: {
174
+ type: "string",
175
+ description: "Name of the entity",
176
+ },
177
+ observations: {
178
+ type: "array",
179
+ items: { type: "string" },
180
+ description: "Observations to remove (exact matches)",
181
+ },
182
+ },
183
+ required: ["entityName", "observations"],
184
+ },
185
+ },
186
+ },
187
+ required: ["deletions"],
188
+ },
189
+ },
190
+ {
191
+ name: "delete_relations",
192
+ description:
193
+ "Remove specific relations from the graph. " +
194
+ "Requires exact match of all three fields. Silent if relation doesn't exist. " +
195
+ "Does not affect entities, only the connection.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ relations: {
200
+ type: "array",
201
+ items: {
202
+ type: "object",
203
+ properties: {
204
+ from: { type: "string", description: "Source entity name" },
205
+ to: { type: "string", description: "Target entity name" },
206
+ relationType: { type: "string", description: "Type of relation" },
207
+ },
208
+ required: ["from", "to", "relationType"],
209
+ },
210
+ },
211
+ },
212
+ required: ["relations"],
213
+ },
214
+ },
215
+ {
216
+ name: "read_graph",
217
+ description:
218
+ "Read the entire knowledge graph. " +
219
+ "Returns all entities and relations. Useful for debugging, inspection, " +
220
+ "generating graph visualizations, and exporting memory for analysis.",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {},
224
+ },
225
+ },
226
+ {
227
+ name: "search_collective_memory",
228
+ description:
229
+ "Search for relevant past work based on a natural language query. " +
230
+ "Search scope includes entity names, entity types, all observation content, and relation types. " +
231
+ "Returns entities with their immediate relations, ordered by relevance.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ query: {
236
+ type: "string",
237
+ description: "Natural language search query",
238
+ },
239
+ },
240
+ required: ["query"],
241
+ },
242
+ },
243
+ {
244
+ name: "open_nodes",
245
+ description:
246
+ "Retrieve specific nodes by exact name match. " +
247
+ "Returns only requested entities and includes relations between returned entities. " +
248
+ "Silently skips non-existent nodes.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ names: {
253
+ type: "array",
254
+ items: { type: "string" },
255
+ description: "Entity names to retrieve",
256
+ },
257
+ },
258
+ required: ["names"],
259
+ },
260
+ },
261
+ {
262
+ name: "record_task_completion",
263
+ description:
264
+ "[Primary tool for agents] Document a completed task with full context. " +
265
+ "Automatically creates the task entity, agent entity if needed, all artifact entities, " +
266
+ "and establishes all relevant relations (executed_by, created, modified, part_of). " +
267
+ "Returns the complete task node with all relations.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ agent_name: {
272
+ type: "string",
273
+ description: "Name of the agent completing the task",
274
+ },
275
+ task_name: {
276
+ type: "string",
277
+ description: "Unique name for the task",
278
+ },
279
+ task_type: {
280
+ type: "string",
281
+ description: "Type of task (e.g., implementation, debugging, refactoring)",
282
+ },
283
+ description: {
284
+ type: "string",
285
+ description: "High-level description of what was accomplished",
286
+ },
287
+ observations: {
288
+ type: "array",
289
+ items: { type: "string" },
290
+ description: "Detailed observations about the task execution",
291
+ },
292
+ created_artifacts: {
293
+ type: "array",
294
+ items: {
295
+ type: "object",
296
+ properties: {
297
+ name: { type: "string" },
298
+ observations: {
299
+ type: "array",
300
+ items: { type: "string" },
301
+ },
302
+ },
303
+ required: ["name"],
304
+ },
305
+ description: "Artifacts created during the task",
306
+ },
307
+ modified_structures: {
308
+ type: "array",
309
+ items: {
310
+ type: "object",
311
+ properties: {
312
+ name: { type: "string" },
313
+ observations: {
314
+ type: "array",
315
+ items: { type: "string" },
316
+ },
317
+ },
318
+ required: ["name"],
319
+ },
320
+ description: "Structures modified during the task",
321
+ },
322
+ session_id: {
323
+ type: "string",
324
+ description: "Optional session identifier",
325
+ },
326
+ },
327
+ required: ["agent_name", "task_name"],
328
+ },
329
+ },
330
+ {
331
+ name: "find_similar_procedures",
332
+ description:
333
+ "Search for tasks that match a procedural query and return their full implementation details. " +
334
+ "Searches task entities and observations. Returns complete subgraphs (tasks + related artifacts + structures). " +
335
+ "Sorted by relevance and recency.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ query: {
340
+ type: "string",
341
+ description: "Procedural search query (e.g., 'authentication implementation')",
342
+ },
343
+ },
344
+ required: ["query"],
345
+ },
346
+ },
347
+ ],
348
+ };
349
+ });
350
+
351
+ /**
352
+ * Handle tool calls
353
+ */
354
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
355
+ const { name, arguments: args } = request.params;
356
+
357
+ try {
358
+ switch (name) {
359
+ case "create_entities":
360
+ return { content: [{ type: "text", text: JSON.stringify(createEntities(args), null, 2) }] };
361
+
362
+ case "create_relations":
363
+ return { content: [{ type: "text", text: JSON.stringify(createRelations(args), null, 2) }] };
364
+
365
+ case "add_observations":
366
+ return { content: [{ type: "text", text: JSON.stringify(addObservations(args), null, 2) }] };
367
+
368
+ case "delete_entities":
369
+ return { content: [{ type: "text", text: JSON.stringify(deleteEntities(args), null, 2) }] };
370
+
371
+ case "delete_observations":
372
+ return { content: [{ type: "text", text: JSON.stringify(deleteObservations(args), null, 2) }] };
373
+
374
+ case "delete_relations":
375
+ return { content: [{ type: "text", text: JSON.stringify(deleteRelations(args), null, 2) }] };
376
+
377
+ case "read_graph":
378
+ return { content: [{ type: "text", text: JSON.stringify(readGraph(), null, 2) }] };
379
+
380
+ case "search_collective_memory":
381
+ return { content: [{ type: "text", text: JSON.stringify(searchCollectiveMemory(args), null, 2) }] };
382
+
383
+ case "open_nodes":
384
+ return { content: [{ type: "text", text: JSON.stringify(openNodes(args), null, 2) }] };
385
+
386
+ case "record_task_completion":
387
+ return { content: [{ type: "text", text: JSON.stringify(recordTaskCompletion(args), null, 2) }] };
388
+
389
+ case "find_similar_procedures":
390
+ return { content: [{ type: "text", text: JSON.stringify(findSimilarProcedures(args), null, 2) }] };
391
+
392
+ default:
393
+ throw new Error(`Unknown tool: ${name}`);
394
+ }
395
+ } catch (error) {
396
+ return {
397
+ content: [
398
+ {
399
+ type: "text",
400
+ text: JSON.stringify({
401
+ status: "error",
402
+ message: error.message,
403
+ }),
404
+ },
405
+ ],
406
+ isError: true,
407
+ };
408
+ }
409
+ });
410
+
411
+ // ========== Tool Implementations ==========
412
+
413
+ function createEntities({ entities = [] }) {
414
+ const created = [];
415
+
416
+ for (const data of entities) {
417
+ const { name, entityType, observations = [] } = data;
418
+
419
+ if (!name || !entityType) continue;
420
+ if (!ENTITY_TYPES.includes(entityType)) continue;
421
+
422
+ const entity = new Entity({ name, entityType, observations });
423
+ if (storage.createEntity(entity)) {
424
+ created.push(name);
425
+ }
426
+ }
427
+
428
+ return { status: "success", created_entities: created, count: created.length };
429
+ }
430
+
431
+ function createRelations({ relations = [] }) {
432
+ const created = [];
433
+
434
+ for (const data of relations) {
435
+ const { from, to, relationType } = data;
436
+
437
+ if (!from || !to || !relationType) continue;
438
+
439
+ // Verify entities exist
440
+ if (!storage.entityExists(from) || !storage.entityExists(to)) continue;
441
+
442
+ const relation = new Relation({ from, to, relationType });
443
+ if (storage.createRelation(relation)) {
444
+ created.push(`${from} -> ${to} (${relationType})`);
445
+ }
446
+ }
447
+
448
+ return { status: "success", created_relations: created, count: created.length };
449
+ }
450
+
451
+ function addObservations({ observations = [] }) {
452
+ const results = {};
453
+
454
+ for (const data of observations) {
455
+ const { entityName, contents = [] } = data;
456
+
457
+ if (!entityName) {
458
+ continue;
459
+ }
460
+
461
+ const entity = storage.getEntity(entityName);
462
+ if (!entity) {
463
+ results[entityName] = { error: "Entity not found" };
464
+ continue;
465
+ }
466
+
467
+ // Add new observations (avoid duplicates)
468
+ const newObs = contents.filter((o) => !entity.observations.includes(o));
469
+ entity.observations.push(...newObs);
470
+
471
+ if (storage.updateEntity(entityName, { observations: entity.observations })) {
472
+ results[entityName] = { added: newObs.length };
473
+ } else {
474
+ results[entityName] = { error: "Failed to update" };
475
+ }
476
+ }
477
+
478
+ return { status: "success", results };
479
+ }
480
+
481
+ function deleteEntities({ entityNames = [] }) {
482
+ const count = storage.deleteEntities(entityNames);
483
+ return { status: "success", deleted_count: count };
484
+ }
485
+
486
+ function deleteObservations({ deletions = [] }) {
487
+ const results = {};
488
+
489
+ for (const data of deletions) {
490
+ const { entityName, observations: toRemove = [] } = data;
491
+
492
+ const entity = storage.getEntity(entityName);
493
+ if (!entity) {
494
+ results[entityName] = { error: "Entity not found" };
495
+ continue;
496
+ }
497
+
498
+ // Remove exact matches
499
+ const originalCount = entity.observations.length;
500
+ entity.observations = entity.observations.filter((o) => !toRemove.includes(o));
501
+ const removedCount = originalCount - entity.observations.length;
502
+
503
+ storage.updateEntity(entityName, { observations: entity.observations });
504
+ results[entityName] = { removed: removedCount };
505
+ }
506
+
507
+ return { status: "success", results };
508
+ }
509
+
510
+ function deleteRelations({ relations = [] }) {
511
+ const toDelete = relations.map((r) => [r.from, r.to, r.relationType]);
512
+ const count = storage.deleteRelations(toDelete);
513
+ return { status: "success", deleted_count: count };
514
+ }
515
+
516
+ function readGraph() {
517
+ const entities = storage.getAllEntities().map((e) => e.toJSON());
518
+ const relations = storage.getAllRelations().map((r) => r.toJSON());
519
+
520
+ return {
521
+ entities,
522
+ relations,
523
+ summary: {
524
+ entity_count: entities.length,
525
+ relation_count: relations.length,
526
+ },
527
+ };
528
+ }
529
+
530
+ function searchCollectiveMemory({ query = "" }) {
531
+ const matchingEntities = storage.searchEntities(query);
532
+
533
+ const results = matchingEntities.map((entity) => {
534
+ const related = storage.getRelatedEntities(entity.name);
535
+ return {
536
+ name: entity.name,
537
+ entityType: entity.entityType,
538
+ observations: entity.observations,
539
+ createdAt: entity.createdAt,
540
+ related_entities: related.connected.map((e) => ({
541
+ name: e.name,
542
+ entityType: e.entityType,
543
+ })),
544
+ };
545
+ });
546
+
547
+ return { matching_entities: results, count: results.length };
548
+ }
549
+
550
+ function openNodes({ names = [] }) {
551
+ const entities = [];
552
+ const entityNamesSet = new Set();
553
+
554
+ for (const name of names) {
555
+ const entity = storage.getEntity(name);
556
+ if (entity) {
557
+ entities.push(entity.toJSON());
558
+ entityNamesSet.add(name);
559
+ }
560
+ }
561
+
562
+ // Get relations between requested entities
563
+ const allRelations = storage.getAllRelations();
564
+ const filteredRelations = allRelations
565
+ .filter((r) => entityNamesSet.has(r.from) && entityNamesSet.has(r.to))
566
+ .map((r) => r.toJSON());
567
+
568
+ return {
569
+ entities,
570
+ relations_between_requested_nodes: filteredRelations,
571
+ };
572
+ }
573
+
574
+ function recordTaskCompletion({
575
+ agent_name,
576
+ task_name,
577
+ task_type = "task",
578
+ description = "",
579
+ observations = [],
580
+ created_artifacts = [],
581
+ modified_structures = [],
582
+ session_id = null,
583
+ }) {
584
+ if (!agent_name || !task_name) {
585
+ return {
586
+ status: "error",
587
+ message: "agent_name and task_name are required",
588
+ };
589
+ }
590
+
591
+ // Ensure agent entity exists
592
+ let agentEntity = storage.getEntity(agent_name);
593
+ if (!agentEntity) {
594
+ agentEntity = new Entity({
595
+ name: agent_name,
596
+ entityType: "agent",
597
+ observations: [`Created during task ${task_name}`],
598
+ });
599
+ storage.createEntity(agentEntity);
600
+ }
601
+
602
+ // Create task entity
603
+ const taskObservations = [...observations];
604
+ if (description) {
605
+ taskObservations.unshift(`Description: ${description}`);
606
+ }
607
+
608
+ const taskEntity = new Entity({
609
+ name: task_name,
610
+ entityType: "task",
611
+ observations: taskObservations,
612
+ metadata: { task_type },
613
+ });
614
+ storage.createEntity(taskEntity);
615
+
616
+ // Create executed_by relation
617
+ storage.createRelation(
618
+ new Relation({
619
+ from: task_name,
620
+ to: agent_name,
621
+ relationType: "executed_by",
622
+ })
623
+ );
624
+
625
+ // Handle session
626
+ if (session_id) {
627
+ let sessionEntity = storage.getEntity(session_id);
628
+ if (!sessionEntity) {
629
+ sessionEntity = new Entity({
630
+ name: session_id,
631
+ entityType: "session",
632
+ observations: [`Started for task ${task_name}`],
633
+ });
634
+ storage.createEntity(sessionEntity);
635
+ }
636
+
637
+ storage.createRelation(
638
+ new Relation({
639
+ from: task_name,
640
+ to: session_id,
641
+ relationType: "part_of",
642
+ })
643
+ );
644
+ }
645
+
646
+ // Create artifacts
647
+ const createdArtifactNames = [];
648
+ for (const artifactData of created_artifacts) {
649
+ const { name: artifactName, observations: artifactObs = [] } = artifactData;
650
+ if (!artifactName) continue;
651
+
652
+ let artifactEntity;
653
+ if (!storage.entityExists(artifactName)) {
654
+ artifactEntity = new Entity({
655
+ name: artifactName,
656
+ entityType: "artifact",
657
+ observations: artifactObs,
658
+ });
659
+ storage.createEntity(artifactEntity);
660
+ } else {
661
+ artifactEntity = storage.getEntity(artifactName);
662
+ const newObs = artifactObs.filter((o) => !artifactEntity.observations.includes(o));
663
+ artifactEntity.observations.push(...newObs);
664
+ storage.updateEntity(artifactName, { observations: artifactEntity.observations });
665
+ }
666
+
667
+ storage.createRelation(
668
+ new Relation({
669
+ from: task_name,
670
+ to: artifactName,
671
+ relationType: "created",
672
+ })
673
+ );
674
+ createdArtifactNames.push(artifactName);
675
+ }
676
+
677
+ // Handle modified structures
678
+ for (const structureData of modified_structures) {
679
+ const { name: structureName, observations: structureObs = [] } = structureData;
680
+ if (!structureName) continue;
681
+
682
+ let relationType;
683
+ if (!storage.entityExists(structureName)) {
684
+ const structureEntity = new Entity({
685
+ name: structureName,
686
+ entityType: "structure",
687
+ observations: structureObs,
688
+ });
689
+ storage.createEntity(structureEntity);
690
+ relationType = "documented";
691
+ } else {
692
+ const existing = storage.getEntity(structureName);
693
+ const newObs = structureObs.filter((o) => !existing.observations.includes(o));
694
+ if (newObs.length > 0) {
695
+ existing.observations.push(...newObs);
696
+ storage.updateEntity(structureName, { observations: existing.observations });
697
+ }
698
+ relationType = "modified";
699
+ }
700
+
701
+ storage.createRelation(
702
+ new Relation({
703
+ from: task_name,
704
+ to: structureName,
705
+ relationType,
706
+ })
707
+ );
708
+ }
709
+
710
+ // Build response
711
+ const taskRelations = storage.getRelations({ fromEntity: task_name });
712
+
713
+ return {
714
+ status: "success",
715
+ task: taskEntity.toJSON(),
716
+ relations: taskRelations.map((r) => r.toJSON()),
717
+ created_artifacts: createdArtifactNames,
718
+ };
719
+ }
720
+
721
+ function findSimilarProcedures({ query = "" }) {
722
+ const searchQuery = query.toLowerCase();
723
+
724
+ // Search for matching task entities
725
+ const allEntities = storage.getAllEntities();
726
+ const matchingTasks = allEntities.filter(
727
+ (e) =>
728
+ e.entityType === "task" &&
729
+ (e.name.toLowerCase().includes(searchQuery) ||
730
+ e.observations.some((obs) => obs.toLowerCase().includes(searchQuery)))
731
+ );
732
+
733
+ const results = [];
734
+ for (const task of matchingTasks) {
735
+ const taskRelations = storage.getRelations({ fromEntity: task.name });
736
+
737
+ const artifacts = [];
738
+ const structures = [];
739
+
740
+ for (const rel of taskRelations) {
741
+ if (rel.relationType === "created" || rel.relationType === "uses") {
742
+ const artifact = storage.getEntity(rel.to);
743
+ if (artifact) artifacts.push(artifact.toJSON());
744
+ } else if (rel.relationType === "modified" || rel.relationType === "documented") {
745
+ const structure = storage.getEntity(rel.to);
746
+ if (structure) structures.push(structure.toJSON());
747
+ }
748
+ }
749
+
750
+ // Get execution context (agent info)
751
+ let agentName = null;
752
+ for (const rel of taskRelations) {
753
+ if (rel.relationType === "executed_by") {
754
+ agentName = rel.to;
755
+ break;
756
+ }
757
+ }
758
+
759
+ const executionContext = {};
760
+ if (agentName) {
761
+ const agent = storage.getEntity(agentName);
762
+ if (agent) {
763
+ executionContext.agent = agent.toJSON();
764
+ }
765
+ }
766
+
767
+ results.push({
768
+ task: task.toJSON(),
769
+ artifacts,
770
+ structures,
771
+ execution_context: executionContext,
772
+ });
773
+ }
774
+
775
+ return { similar_tasks: results, count: results.length };
776
+ }
777
+
778
+ return server;
779
+ }
780
+
781
+ /**
782
+ * Main entry point
783
+ */
784
+ async function main() {
785
+ const server = createServer();
786
+ const transport = new StdioServerTransport();
787
+ await server.connect(transport);
788
+ }
789
+
790
+ main().catch((err) => {
791
+ console.error("Fatal error:", err);
792
+ process.exit(1);
793
+ });