atlas-mcp 0.1.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.
Files changed (54) hide show
  1. package/.env.example +32 -0
  2. package/README.md +282 -0
  3. package/package.json +72 -0
  4. package/public/app/assets/app-CxbS1w9p.js +3981 -0
  5. package/public/app/assets/index-BA6nxCuI.css +1 -0
  6. package/public/app/assets/index-BXmIRrQH.js +177 -0
  7. package/public/app/index.html +27 -0
  8. package/public/assets/brain-atlas.LICENSE.txt +16 -0
  9. package/public/assets/brain-atlas.glb +0 -0
  10. package/public/assets/brain.obj +27282 -0
  11. package/public/fonts/DepartureMono-Regular.woff +0 -0
  12. package/public/fonts/DepartureMono-Regular.woff2 +0 -0
  13. package/scripts/sync-memory-vectors.js +46 -0
  14. package/src/audit.js +9 -0
  15. package/src/cli/args.js +87 -0
  16. package/src/cli/commands/add.js +103 -0
  17. package/src/cli/commands/config.js +228 -0
  18. package/src/cli/commands/delete.js +75 -0
  19. package/src/cli/commands/entities.js +39 -0
  20. package/src/cli/commands/entity.js +47 -0
  21. package/src/cli/commands/get.js +46 -0
  22. package/src/cli/commands/list.js +53 -0
  23. package/src/cli/commands/related.js +56 -0
  24. package/src/cli/commands/search.js +68 -0
  25. package/src/cli/commands/update.js +58 -0
  26. package/src/cli/deps.js +114 -0
  27. package/src/cli/env-file.js +44 -0
  28. package/src/cli/format.js +246 -0
  29. package/src/cli.js +187 -0
  30. package/src/cognitive-worker.js +381 -0
  31. package/src/db.js +2674 -0
  32. package/src/extraction-context.js +31 -0
  33. package/src/ingestion-service.js +387 -0
  34. package/src/ingestion-worker.js +225 -0
  35. package/src/llm-config.js +31 -0
  36. package/src/llm.js +789 -0
  37. package/src/logger.js +51 -0
  38. package/src/mcp-server.js +577 -0
  39. package/src/memory-comparison.js +421 -0
  40. package/src/related-memories.js +232 -0
  41. package/src/run-cognitive-worker.js +12 -0
  42. package/src/run-ingestion-worker.js +13 -0
  43. package/src/run-vector-worker.js +12 -0
  44. package/src/schemas.js +413 -0
  45. package/src/semantic-validation.js +430 -0
  46. package/src/server.js +827 -0
  47. package/src/shared/brain-regions.js +61 -0
  48. package/src/shared/entity-lens.js +249 -0
  49. package/src/shared/memory-placement.js +171 -0
  50. package/src/shared/memory-search.js +55 -0
  51. package/src/shared/region-anchors.js +112 -0
  52. package/src/shared/region-mapper.js +247 -0
  53. package/src/vector-store.js +546 -0
  54. package/src/vector-worker.js +71 -0
package/src/logger.js ADDED
@@ -0,0 +1,51 @@
1
+ const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
2
+
3
+ const activeLevel = LEVELS[process.env.LOG_LEVEL || "info"] ?? LEVELS.info;
4
+ const useStderr = process.env.LOG_STREAM === "stderr";
5
+
6
+ function timestamp() {
7
+ return new Date().toISOString();
8
+ }
9
+
10
+ function format(level, ctx, msg, data) {
11
+ const base = `${timestamp()} ${level.toUpperCase().padEnd(5)} [${ctx}] ${msg}`;
12
+ if (data !== undefined) {
13
+ return `${base} ${typeof data === "string" ? data : JSON.stringify(data)}`;
14
+ }
15
+ return base;
16
+ }
17
+
18
+ function createLogger(context) {
19
+ const write = (method, message) => {
20
+ if (useStderr) {
21
+ console.error(message);
22
+ return;
23
+ }
24
+ console[method](message);
25
+ };
26
+
27
+ return {
28
+ debug(msg, data) {
29
+ if (activeLevel <= LEVELS.debug) {
30
+ write("debug", format("debug", context, msg, data));
31
+ }
32
+ },
33
+ info(msg, data) {
34
+ if (activeLevel <= LEVELS.info) {
35
+ write("info", format("info", context, msg, data));
36
+ }
37
+ },
38
+ warn(msg, data) {
39
+ if (activeLevel <= LEVELS.warn) {
40
+ write("warn", format("warn", context, msg, data));
41
+ }
42
+ },
43
+ error(msg, data) {
44
+ if (activeLevel <= LEVELS.error) {
45
+ write("error", format("error", context, msg, data));
46
+ }
47
+ },
48
+ };
49
+ }
50
+
51
+ export default createLogger;
@@ -0,0 +1,577 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "dotenv/config";
4
+ import { pathToFileURL } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import {
9
+ deleteMemory,
10
+ findEntities,
11
+ getEntitiesForMemory,
12
+ getLatestExtraction,
13
+ getMemories,
14
+ getMemoriesForEntity,
15
+ getMemory,
16
+ getRegionActivations,
17
+ getRelationshipsForMemory,
18
+ getStructuralMemoryLinks,
19
+ searchMemoriesFts,
20
+ storeMemory,
21
+ updateMemoryGraph,
22
+ updateMemorySummary,
23
+ createMemorySource,
24
+ enqueueAnnotationJob,
25
+ enqueueVectorIndexJob,
26
+ enqueueIngestionJob,
27
+ getAnnotationStatus,
28
+ getIngestionStatus,
29
+ getMemorySource,
30
+ getSourceMemoryLinks,
31
+ getVectorIndexStatus,
32
+ linkSourceMemory,
33
+ updateMemorySourceStatus,
34
+ withTransaction,
35
+ } from "./db.js";
36
+ import * as db from "./db.js";
37
+ import { annotateMemory, extractAtomicMemories } from "./llm.js";
38
+ import { getRelatedMemories as deriveRelatedMemories } from "./related-memories.js";
39
+ import { AddMemorySchema } from "./schemas.js";
40
+ import { createIngestionService } from "./ingestion-service.js";
41
+ import { createCognitiveWorker } from "./cognitive-worker.js";
42
+ import { createVectorWorker } from "./vector-worker.js";
43
+ import { createIngestionWorker } from "./ingestion-worker.js";
44
+ import {
45
+ assertAtlasModeSupported,
46
+ deleteMemoryVector,
47
+ hybridSearchMemories,
48
+ indexMemoryVector,
49
+ searchMemoryVectors,
50
+ } from "./vector-store.js";
51
+
52
+ process.env.LOG_STREAM = "stderr";
53
+
54
+ const memoryIdSchema = z
55
+ .string()
56
+ .min(1)
57
+ .describe("Atlas memory ID, for example mem_12ab34cd");
58
+
59
+ const memoryWriteResultSchema = {
60
+ sourceId: z.string().uuid(),
61
+ status: z.enum(["queued", "completed"]),
62
+ memories: z.array(z.object({
63
+ action: z.enum(["created", "updated", "unchanged"]),
64
+ memory: z.object({ id: memoryIdSchema }).passthrough(),
65
+ matchedMemoryId: memoryIdSchema.nullable(),
66
+ confidence: z.number().min(0).max(1),
67
+ reason: z.string().min(1),
68
+ evidenceSpans: z.array(z.object({
69
+ start: z.number().int().nonnegative(),
70
+ end: z.number().int().positive(),
71
+ text: z.string(),
72
+ })),
73
+ annotationStatus: z.enum(["pending", "processing", "completed", "failed"]),
74
+ indexStatus: z.enum(["pending", "processing", "completed", "failed"]),
75
+ })).default([]),
76
+ };
77
+
78
+ const sourceStatusResultSchema = {
79
+ sourceId: z.string().uuid(),
80
+ status: z.enum(["pending", "processing", "completed", "failed", "extraction_failed"]),
81
+ ingestionStatus: z
82
+ .enum(["pending", "processing", "completed", "failed"])
83
+ .nullable(),
84
+ memories: z.array(z.object({
85
+ action: z.enum(["created", "updated", "unchanged"]),
86
+ memory: z.object({ id: memoryIdSchema }).passthrough(),
87
+ annotationStatus: z.enum(["pending", "processing", "completed", "failed"]),
88
+ indexStatus: z.enum(["pending", "processing", "completed", "failed"]),
89
+ })).default([]),
90
+ };
91
+
92
+ function toolResult(data) {
93
+ return {
94
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
95
+ structuredContent: data,
96
+ };
97
+ }
98
+
99
+ function toolError(message) {
100
+ return {
101
+ content: [{ type: "text", text: message }],
102
+ isError: true,
103
+ };
104
+ }
105
+
106
+ function defaultDependencies() {
107
+ const dependencies = {
108
+ deleteMemory,
109
+ deleteMemoryVector,
110
+ decideMemoryWrite: async (...args) => {
111
+ const module = await import("./llm.js");
112
+ return module.decideMemoryWrite(...args);
113
+ },
114
+ extractAtomicMemories: async (...args) => {
115
+ const module = await import("./llm.js");
116
+ return module.extractAtomicMemories(...args);
117
+ },
118
+ findEntities,
119
+ getEntitiesForMemory,
120
+ getLatestExtraction,
121
+ getMemories,
122
+ getMemoriesForEntity,
123
+ getMemory,
124
+ getRegionActivations,
125
+ getRelationshipsForMemory,
126
+ getStructuralMemoryLinks,
127
+ indexMemoryVector,
128
+ searchMemoryVectors,
129
+ storeMemory,
130
+ updateMemoryGraph,
131
+ updateMemorySummary,
132
+ createMemorySource,
133
+ updateMemorySourceStatus,
134
+ getMemorySource,
135
+ getSourceMemoryLinks,
136
+ linkSourceMemory,
137
+ enqueueAnnotationJob,
138
+ enqueueVectorIndexJob,
139
+ enqueueIngestionJob,
140
+ getAnnotationStatus,
141
+ getIngestionStatus,
142
+ getVectorIndexStatus,
143
+ withTransaction,
144
+ getModel: async () => {
145
+ const module = await import("./llm-config.js");
146
+ return module.model;
147
+ },
148
+ };
149
+
150
+ dependencies.getRelatedMemories = (id, options) =>
151
+ deriveRelatedMemories(
152
+ id,
153
+ {
154
+ getMemory: dependencies.getMemory,
155
+ getStructuralMemoryLinks: dependencies.getStructuralMemoryLinks,
156
+ searchMemoryVectors: dependencies.searchMemoryVectors,
157
+ searchMemoriesFts,
158
+ serializeMemory: (memory) => ({
159
+ ...memory,
160
+ extraction: dependencies.getLatestExtraction(memory.id),
161
+ entities: dependencies.getEntitiesForMemory(memory.id),
162
+ relationships: dependencies.getRelationshipsForMemory(memory.id),
163
+ regions: dependencies.getRegionActivations(memory.id),
164
+ }),
165
+ },
166
+ options,
167
+ );
168
+
169
+ return dependencies;
170
+ }
171
+
172
+ export function createAtlasMcpServer(overrides = {}) {
173
+ const dependencies = { ...defaultDependencies(), ...overrides };
174
+ const server = new McpServer({
175
+ name: "Atlas",
176
+ version: "0.1.0",
177
+ });
178
+
179
+ const getMemoryDetails = (id) => {
180
+ const memory = dependencies.getMemory(id);
181
+ if (!memory) return null;
182
+
183
+ return {
184
+ ...memory,
185
+ extraction: dependencies.getLatestExtraction(id),
186
+ entities: dependencies.getEntitiesForMemory(id),
187
+ relationships: dependencies.getRelationshipsForMemory(id),
188
+ regions: dependencies.getRegionActivations(id),
189
+ };
190
+ };
191
+ dependencies.serializeMemory = (memory) => getMemoryDetails(memory?.id);
192
+ dependencies.ingestionService ||= createIngestionService(dependencies);
193
+
194
+ server.registerTool(
195
+ "add_memory",
196
+ {
197
+ title: "Add memory",
198
+ description:
199
+ "Save durable source text to Atlas. The source is preserved exactly and queued for background processing: it is split into evidence-backed atomic memories, and each atom is created, updated, or left unchanged. Type and title are optional source-level hints. Returns immediately with `status: \"queued\"` and a `sourceId`; call `get_source_status` with that id to see when extraction completes and which memories were produced. Do not use for ephemeral chat, secrets, or transient state.",
200
+ inputSchema: AddMemorySchema.shape,
201
+ outputSchema: memoryWriteResultSchema,
202
+ annotations: {
203
+ readOnlyHint: false,
204
+ destructiveHint: false,
205
+ idempotentHint: false,
206
+ openWorldHint: true,
207
+ },
208
+ },
209
+ async ({ text, type, title, confidence, tags }) => {
210
+ try {
211
+ const metadata = Object.fromEntries(Object.entries({
212
+ type, title, confidence, tags,
213
+ }).filter(([, value]) => value !== undefined));
214
+ return toolResult(await dependencies.ingestionService.enqueue({
215
+ text,
216
+ source: "mcp",
217
+ metadata,
218
+ }));
219
+ } catch (error) {
220
+ return toolError(`Could not add memory: ${error.message}`);
221
+ }
222
+ }
223
+ );
224
+
225
+ server.registerTool(
226
+ "get_source_status",
227
+ {
228
+ title: "Get source status",
229
+ description:
230
+ "Check the background-processing status of a source previously submitted with `add_memory`. Pass the `sourceId` returned by `add_memory`. Reports the source's extraction `status` (pending, processing, completed, failed), the ingestion job status, and—once completed—the atomic memories that were created, updated, or left unchanged. Use this to confirm that a queued `add_memory` finished before relying on the new memories.",
231
+ inputSchema: {
232
+ sourceId: z
233
+ .string()
234
+ .uuid()
235
+ .describe("Source ID returned by add_memory, for example 123e4567-e89b-12d3-a456-426614174000"),
236
+ },
237
+ outputSchema: sourceStatusResultSchema,
238
+ annotations: { readOnlyHint: true, openWorldHint: false },
239
+ },
240
+ async ({ sourceId }) => {
241
+ const sourceRecord = dependencies.getMemorySource?.(sourceId);
242
+ if (!sourceRecord) return toolError(`Source not found: ${sourceId}`);
243
+ const links = dependencies.getSourceMemoryLinks?.(sourceId) || [];
244
+ return toolResult({
245
+ sourceId,
246
+ status: sourceRecord.extraction_status,
247
+ ingestionStatus: dependencies.getIngestionStatus?.(sourceId) ?? null,
248
+ memories: links.map((link) => ({
249
+ action: link.action,
250
+ memory: getMemoryDetails(link.memory_id)
251
+ ?? { id: link.memory_id },
252
+ annotationStatus:
253
+ dependencies.getAnnotationStatus?.(link.memory_id) ?? "pending",
254
+ indexStatus:
255
+ dependencies.getVectorIndexStatus?.(link.memory_id) ?? "pending",
256
+ })),
257
+ });
258
+ }
259
+ );
260
+
261
+ server.registerTool(
262
+ "list_memories",
263
+ {
264
+ title: "List memories",
265
+ description:
266
+ "Browse Atlas's most recently stored memories in insertion order (newest first). Use this for a quick scan of what is already known, to enumerate memories by page, or when `search_memories` returns nothing and the user wants to see what exists. Supports `limit` (1-100, default 20) and `offset` (>=0, default 0) for pagination. Prefer `search_memories` when you need semantically relevant results for a question, and `find_entities` / `get_entity_memories` when you want everything about a specific person, place, or concept.",
267
+ inputSchema: {
268
+ limit: z
269
+ .number()
270
+ .int()
271
+ .min(1)
272
+ .max(100)
273
+ .default(20)
274
+ .describe("Page size, 1-100. Defaults to 20."),
275
+ offset: z
276
+ .number()
277
+ .int()
278
+ .min(0)
279
+ .default(0)
280
+ .describe("Number of memories to skip for pagination. Defaults to 0."),
281
+ },
282
+ annotations: { readOnlyHint: true, openWorldHint: false },
283
+ },
284
+ async ({ limit, offset }) =>
285
+ toolResult({ memories: dependencies.getMemories({ limit, offset }) })
286
+ );
287
+
288
+ server.registerTool(
289
+ "get_memory",
290
+ {
291
+ title: "Get memory",
292
+ description:
293
+ "Fetch ONE Atlas memory by its ID and return its full record: raw text, summary, type, title, confidence, tags, timestamps, the LLM extraction (entities, relationships, emotions, topics, brain-region activations), and any linked entities/relationships. Use this when you already have a memory ID from `add_memory`, `search_memories`, `list_memories`, or `get_related_memories` and need the complete payload. Returns an error if the ID does not exist. Do NOT use this to discover memories - call `search_memories` or `list_memories` first.",
294
+ inputSchema: {
295
+ id: z
296
+ .string()
297
+ .min(1)
298
+ .describe(
299
+ "Atlas memory ID returned by add_memory, search_memories, list_memories, or get_related_memories, for example mem_12ab34cd"
300
+ ),
301
+ },
302
+ annotations: { readOnlyHint: true, openWorldHint: false },
303
+ },
304
+ async ({ id }) => {
305
+ const memory = getMemoryDetails(id);
306
+ return memory
307
+ ? toolResult(memory)
308
+ : toolError(`Memory not found: ${id}`);
309
+ }
310
+ );
311
+
312
+ server.registerTool(
313
+ "search_memories",
314
+ {
315
+ title: "Search memories",
316
+ description:
317
+ "Find memories using hybrid search (semantic + keyword). THIS IS THE PRIMARY WAY TO RECALL MEMORIES. Call this BEFORE asking the user to repeat information, and before `add_memory`, to avoid creating duplicates. Use a short descriptive query such as 'user's coffee preference' or 'project decisions last week'. Returns memories with a `rrfScore` (reciprocal rank fusion score). Supports `limit` (1-100, default 20), `scoreThreshold` in -1..1, and `strategy` ('hybrid', 'vector', or 'bm25', default 'hybrid'). Use `find_entities` instead when you need a known person/place/concept, and `list_memories` for a chronological browse.",
318
+ inputSchema: {
319
+ query: z
320
+ .string()
321
+ .min(1)
322
+ .describe(
323
+ "Natural-language search query describing what you are looking for, e.g. 'user's dietary restrictions' or 'Decisions about the Q4 launch'"
324
+ ),
325
+ limit: z
326
+ .number()
327
+ .int()
328
+ .min(1)
329
+ .max(100)
330
+ .default(20)
331
+ .describe("Max memories to return, 1-100. Defaults to 20."),
332
+ scoreThreshold: z
333
+ .number()
334
+ .min(-1)
335
+ .max(1)
336
+ .optional()
337
+ .describe(
338
+ "Optional minimum similarity score in -1..1; lower returns more permissive results"
339
+ ),
340
+ strategy: z
341
+ .enum(["hybrid", "vector", "bm25"])
342
+ .default("hybrid")
343
+ .describe(
344
+ "Search strategy: 'hybrid' (default) combines semantic + BM25, 'vector' is semantic-only, 'bm25' is keyword-only"
345
+ ),
346
+ },
347
+ annotations: { readOnlyHint: true, openWorldHint: true },
348
+ },
349
+ async ({ query, limit, scoreThreshold, strategy }) => {
350
+ try {
351
+ const hits = await hybridSearchMemories(query, {
352
+ limit,
353
+ scoreThreshold,
354
+ strategy,
355
+ searchMemoriesFts,
356
+ });
357
+ const memories = hits.flatMap(({ id, score }) => {
358
+ const memory = getMemoryDetails(id);
359
+ return memory ? [{ ...memory, rrfScore: score }] : [];
360
+ });
361
+ return toolResult({ query, strategy, memories });
362
+ } catch (error) {
363
+ return toolError(`Could not search memories: ${error.message}`);
364
+ }
365
+ }
366
+ );
367
+
368
+ server.registerTool(
369
+ "get_related_memories",
370
+ {
371
+ title: "Get related memories",
372
+ description:
373
+ "Find memories connected to a given memory through FOUR signals combined: shared entities, shared subject-predicate-object relationships, semantic similarity, and BM25 keyword relevance. Use this AFTER obtaining a memory ID from `search_memories` or `add_memory` to expand context, surface contradictions, or follow a thread (e.g. 'what else do we know about Maya?'). Returns a ranked list of related memories with combined score, human-readable reasons, shared entities, shared relationships, and `semanticAvailable`/`bm25Available` flags. Supports `limit` (1-20, default 5) and `scoreThreshold` in -1..1 (default 0.65). Returns an error if `id` does not exist. This is NOT a general search - start with `search_memories` when you have a free-text question.",
374
+ inputSchema: {
375
+ id: z
376
+ .string()
377
+ .min(1)
378
+ .describe(
379
+ "Source memory ID (e.g. mem_12ab34cd) to find related memories for"
380
+ ),
381
+ limit: z
382
+ .number()
383
+ .int()
384
+ .min(1)
385
+ .max(20)
386
+ .default(5)
387
+ .describe("Max related memories to return, 1-20. Defaults to 5."),
388
+ scoreThreshold: z
389
+ .number()
390
+ .min(-1)
391
+ .max(1)
392
+ .default(0.65)
393
+ .describe(
394
+ "Minimum similarity score in -1..1 for the semantic-similarity signal. Defaults to 0.65."
395
+ ),
396
+ },
397
+ annotations: { readOnlyHint: true, openWorldHint: true },
398
+ },
399
+ async ({ id, limit, scoreThreshold }) => {
400
+ try {
401
+ const result = await dependencies.getRelatedMemories(id, {
402
+ limit,
403
+ scoreThreshold,
404
+ });
405
+ return result
406
+ ? toolResult(result)
407
+ : toolError(`Memory not found: ${id}`);
408
+ } catch (error) {
409
+ return toolError(`Could not get related memories: ${error.message}`);
410
+ }
411
+ }
412
+ );
413
+
414
+ server.registerTool(
415
+ "find_entities",
416
+ {
417
+ title: "Find entities",
418
+ description:
419
+ "Look up canonical entities (people, places, objects, concepts, or organizations) that Atlas has extracted from memories. Use this when the user mentions a person, place, or topic by name and you want to confirm Atlas knows about them and retrieve their numeric `entityId`. Provide a partial name and review the matches. After finding the entity, call `get_entity_memories` with the returned `entityId` to list every memory linked to it. This is a name lookup against the entity graph - use `search_memories` for free-text semantic recall.",
420
+ inputSchema: {
421
+ query: z
422
+ .string()
423
+ .min(1)
424
+ .describe(
425
+ "Partial or full name to search for, e.g. 'Maya', 'Acme', 'Paris'"
426
+ ),
427
+ },
428
+ annotations: { readOnlyHint: true, openWorldHint: false },
429
+ },
430
+ async ({ query }) =>
431
+ toolResult({ entities: dependencies.findEntities(query) })
432
+ );
433
+
434
+ server.registerTool(
435
+ "get_entity_memories",
436
+ {
437
+ title: "Get entity memories",
438
+ description:
439
+ "List every memory linked to a specific Atlas entity (person, place, object, concept, or organization). Use this AFTER `find_entities` returns an `entityId` to retrieve the full history of memories about that entity. Useful for 'tell me everything we know about X' or 'what has the user said about Y'. Do NOT pass a free-text name - resolve it through `find_entities` first to obtain the numeric `entityId`.",
440
+ inputSchema: {
441
+ entityId: z
442
+ .number()
443
+ .int()
444
+ .positive()
445
+ .describe(
446
+ "Numeric entity ID returned by find_entities, e.g. 42"
447
+ ),
448
+ },
449
+ annotations: { readOnlyHint: true, openWorldHint: false },
450
+ },
451
+ async ({ entityId }) =>
452
+ toolResult({
453
+ entityId,
454
+ memories: dependencies.getMemoriesForEntity(entityId),
455
+ })
456
+ );
457
+
458
+ server.registerTool(
459
+ "update_memory_summary",
460
+ {
461
+ title: "Update memory summary",
462
+ description:
463
+ "Replace the editable summary of an existing memory WITHOUT changing the original raw text, type, or extraction graph. Use this when the auto-generated summary is wrong, stale, or unclear and you want to fix only the human-readable summary. The memory's vector embedding is reindexed after the update, so semantic search will reflect the new summary. Prefer `add_memory` (which can return `action: \"updated\"`) when the underlying memory text itself has changed - it preserves the original ID and revision history. Returns the refreshed memory.",
464
+ inputSchema: {
465
+ id: z
466
+ .string()
467
+ .min(1)
468
+ .describe(
469
+ "Atlas memory ID to update, for example mem_12ab34cd"
470
+ ),
471
+ summary: z
472
+ .string()
473
+ .describe(
474
+ "Replacement summary text that replaces the current summary"
475
+ ),
476
+ },
477
+ annotations: {
478
+ readOnlyHint: false,
479
+ destructiveHint: false,
480
+ idempotentHint: true,
481
+ openWorldHint: false,
482
+ },
483
+ },
484
+ async ({ id, summary }) => {
485
+ if (!dependencies.getMemory(id)) {
486
+ return toolError(`Memory not found: ${id}`);
487
+ }
488
+ dependencies.updateMemorySummary(id, summary);
489
+ try {
490
+ await dependencies.indexMemoryVector(dependencies.getMemory(id));
491
+ } catch (error) {
492
+ console.error(`Could not reindex memory ${id}: ${error.message}`);
493
+ }
494
+ return toolResult(dependencies.getMemory(id));
495
+ }
496
+ );
497
+
498
+ server.registerTool(
499
+ "delete_memory",
500
+ {
501
+ title: "Delete memory",
502
+ description:
503
+ "PERMANENTLY delete one Atlas memory, its extraction data, and its vector index entry. This is destructive and cannot be undone. Use this ONLY when the user explicitly asks to forget, remove, or delete a specific memory, or when correcting a test/dev record. Confirm with the user first when in doubt. Returns an error if the ID does not exist. To revise the underlying text or facts, prefer `add_memory` (which may return `action: \"updated\"`) so the original memory ID and revision history are preserved.",
504
+ inputSchema: {
505
+ id: z
506
+ .string()
507
+ .min(1)
508
+ .describe(
509
+ "Atlas memory ID to permanently delete, for example mem_12ab34cd"
510
+ ),
511
+ },
512
+ annotations: {
513
+ readOnlyHint: false,
514
+ destructiveHint: true,
515
+ idempotentHint: true,
516
+ openWorldHint: false,
517
+ },
518
+ },
519
+ async ({ id }) => {
520
+ if (!dependencies.getMemory(id)) {
521
+ return toolError(`Memory not found: ${id}`);
522
+ }
523
+ dependencies.deleteMemory(id);
524
+ try {
525
+ await dependencies.deleteMemoryVector(id);
526
+ } catch (error) {
527
+ console.error(`Could not delete vector for ${id}: ${error.message}`);
528
+ }
529
+ return toolResult({ ok: true, deletedMemoryId: id });
530
+ }
531
+ );
532
+
533
+ return server;
534
+ }
535
+
536
+ function startInProcessWorkers() {
537
+ const flag = process.env.ATLAS_INPROCESS_WORKERS;
538
+ if (flag === "0" || flag === "false") return null;
539
+
540
+ const ingestionService = createIngestionService(defaultDependencies());
541
+ const controller = new AbortController();
542
+ const workers = [
543
+ ["ingestion", createIngestionWorker({ db, ingestionService })],
544
+ ["cognitive", createCognitiveWorker({ db, annotateMemory })],
545
+ ["vector", createVectorWorker({ db, indexMemoryVector })],
546
+ ];
547
+ for (const [name, worker] of workers) {
548
+ worker.run({ signal: controller.signal }).catch((error) => {
549
+ console.error(`Atlas ${name} worker stopped: ${error.message}`);
550
+ });
551
+ }
552
+ for (const signal of ["SIGINT", "SIGTERM"]) {
553
+ process.once(signal, () => controller.abort());
554
+ }
555
+ console.error("Atlas in-process workers running (ingestion, cognitive, vector)");
556
+ return controller;
557
+ }
558
+
559
+ export async function runAtlasMcpServer() {
560
+ assertAtlasModeSupported();
561
+ const server = createAtlasMcpServer();
562
+ const transport = new StdioServerTransport();
563
+ await server.connect(transport);
564
+ console.error("Atlas MCP server running on stdio");
565
+ startInProcessWorkers();
566
+ }
567
+
568
+ const isMain =
569
+ process.argv[1] &&
570
+ import.meta.url === pathToFileURL(process.argv[1]).href;
571
+
572
+ if (isMain) {
573
+ runAtlasMcpServer().catch((error) => {
574
+ console.error("Atlas MCP server failed:", error);
575
+ process.exit(1);
576
+ });
577
+ }