@prih/mcp-graph-memory 1.0.3

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const zod_1 = require("zod");
5
+ const manager_types_1 = require("../../../graphs/manager-types");
6
+ function register(server, mgr) {
7
+ server.registerTool('update_task', {
8
+ description: 'Update an existing task. Only provided fields are changed. ' +
9
+ 'Re-embeds automatically when title or description changes. ' +
10
+ 'Status changes auto-manage completedAt (set on done/cancelled, cleared on reopen). ' +
11
+ 'Use move_task for a simpler status-only change. ' +
12
+ 'Pass expectedVersion to enable optimistic locking.',
13
+ inputSchema: {
14
+ taskId: zod_1.z.string().describe('Task ID to update'),
15
+ title: zod_1.z.string().optional().describe('New title'),
16
+ description: zod_1.z.string().optional().describe('New description'),
17
+ status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional()
18
+ .describe('New status'),
19
+ priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional().describe('New priority'),
20
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe('Replace tags array'),
21
+ dueDate: zod_1.z.number().nullable().optional().describe('New due date (ms timestamp) or null to clear'),
22
+ estimate: zod_1.z.number().nullable().optional().describe('New estimate (hours) or null to clear'),
23
+ expectedVersion: zod_1.z.number().int().positive().optional().describe('Current version for optimistic locking — request fails with version_conflict if the task has been updated since'),
24
+ },
25
+ }, async ({ taskId, title, description, status, priority, tags, dueDate, estimate, expectedVersion }) => {
26
+ const patch = {};
27
+ if (title !== undefined)
28
+ patch.title = title;
29
+ if (description !== undefined)
30
+ patch.description = description;
31
+ if (status !== undefined)
32
+ patch.status = status;
33
+ if (priority !== undefined)
34
+ patch.priority = priority;
35
+ if (tags !== undefined)
36
+ patch.tags = tags;
37
+ if (dueDate !== undefined)
38
+ patch.dueDate = dueDate;
39
+ if (estimate !== undefined)
40
+ patch.estimate = estimate;
41
+ try {
42
+ const updated = await mgr.updateTask(taskId, patch, expectedVersion);
43
+ if (!updated) {
44
+ return { content: [{ type: 'text', text: `Task "${taskId}" not found.` }], isError: true };
45
+ }
46
+ return { content: [{ type: 'text', text: JSON.stringify({ taskId, updated: true }, null, 2) }] };
47
+ }
48
+ catch (err) {
49
+ if (err instanceof manager_types_1.VersionConflictError) {
50
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'version_conflict', current: err.current, expected: err.expected }) }], isError: true };
51
+ }
52
+ throw err;
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const multi_config_1 = require("../lib/multi-config");
10
+ const project_manager_1 = require("../lib/project-manager");
11
+ const embedder_1 = require("../lib/embedder");
12
+ const docs_1 = require("../graphs/docs");
13
+ const code_1 = require("../graphs/code");
14
+ const knowledge_1 = require("../graphs/knowledge");
15
+ const file_index_1 = require("../graphs/file-index");
16
+ const task_1 = require("../graphs/task");
17
+ const skill_1 = require("../graphs/skill");
18
+ const index_1 = require("../api/index");
19
+ const indexer_1 = require("../cli/indexer");
20
+ const watcher_1 = require("../lib/watcher");
21
+ const program = new commander_1.Command();
22
+ program
23
+ .name('mcp-graph-memory')
24
+ .description('MCP server for semantic graph memory from markdown docs and source code')
25
+ .version('1.0.0');
26
+ const parseIntArg = (v) => parseInt(v, 10);
27
+ // ---------------------------------------------------------------------------
28
+ // Helper: resolve a single project from YAML config + --project flag
29
+ // ---------------------------------------------------------------------------
30
+ function resolveProject(configPath, projectId) {
31
+ const mc = (0, multi_config_1.loadMultiConfig)(configPath);
32
+ const ids = Array.from(mc.projects.keys());
33
+ if (ids.length === 0) {
34
+ process.stderr.write('[cli] No projects defined in config\n');
35
+ process.exit(1);
36
+ }
37
+ const id = projectId ?? ids[0];
38
+ const project = mc.projects.get(id);
39
+ if (!project) {
40
+ process.stderr.write(`[cli] Project "${id}" not found in config. Available: ${ids.join(', ')}\n`);
41
+ process.exit(1);
42
+ }
43
+ return { id, project, server: mc.server };
44
+ }
45
+ async function loadAllModels(projectId, config, modelsDir) {
46
+ for (const gn of multi_config_1.GRAPH_NAMES) {
47
+ await (0, embedder_1.loadModel)(config.graphEmbeddings[gn], modelsDir, config.embedMaxChars, `${projectId}:${gn}`);
48
+ }
49
+ }
50
+ function buildEmbedFns(projectId) {
51
+ const pair = (gn) => ({
52
+ document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
53
+ query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
54
+ });
55
+ return {
56
+ docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
57
+ tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
58
+ };
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Command: index — scan one project and exit
62
+ // ---------------------------------------------------------------------------
63
+ program
64
+ .command('index')
65
+ .description('Scan and embed all matching files, then exit (all projects or one with --project)')
66
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
67
+ .option('--project <id>', 'Project ID to index (omit to index all)')
68
+ .option('--reindex', 'Discard persisted graphs and re-index from scratch')
69
+ .action((opts) => {
70
+ (async () => {
71
+ const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
72
+ const reindex = !!opts.reindex;
73
+ if (reindex)
74
+ process.stderr.write('[index] Re-indexing from scratch\n');
75
+ const manager = new project_manager_1.ProjectManager(mc.server);
76
+ // Build workspace membership lookup
77
+ const projectWorkspace = new Map();
78
+ for (const [wsId, wsConfig] of mc.workspaces) {
79
+ for (const projId of wsConfig.projects) {
80
+ projectWorkspace.set(projId, wsId);
81
+ }
82
+ }
83
+ // Add workspaces first
84
+ for (const [wsId, wsConfig] of mc.workspaces) {
85
+ await manager.addWorkspace(wsId, wsConfig, reindex);
86
+ }
87
+ // Add projects (workspace projects share knowledge/task/skill graphs)
88
+ const ids = opts.project ? [opts.project] : Array.from(mc.projects.keys());
89
+ if (ids.length === 0) {
90
+ process.stderr.write('[index] No projects defined in config\n');
91
+ process.exit(1);
92
+ }
93
+ for (const id of ids) {
94
+ const project = mc.projects.get(id);
95
+ if (!project) {
96
+ process.stderr.write(`[index] Project "${id}" not found in config. Available: ${Array.from(mc.projects.keys()).join(', ')}\n`);
97
+ process.exit(1);
98
+ }
99
+ await manager.addProject(id, project, reindex, projectWorkspace.get(id));
100
+ }
101
+ // Load models (workspaces first, then projects)
102
+ for (const wsId of manager.listWorkspaces()) {
103
+ await manager.loadWorkspaceModels(wsId);
104
+ }
105
+ for (const id of ids) {
106
+ await manager.loadModels(id);
107
+ }
108
+ // Index all projects
109
+ for (const id of ids) {
110
+ process.stderr.write(`[index] Indexing project "${id}"...\n`);
111
+ await manager.startIndexing(id);
112
+ const instance = manager.getProject(id);
113
+ if (instance.docGraph) {
114
+ process.stderr.write(`[index] "${id}" docs: ${instance.docGraph.order} nodes, ${instance.docGraph.size} edges\n`);
115
+ }
116
+ if (instance.codeGraph) {
117
+ process.stderr.write(`[index] "${id}" code: ${instance.codeGraph.order} nodes, ${instance.codeGraph.size} edges\n`);
118
+ }
119
+ process.stderr.write(`[index] "${id}" files: ${instance.fileIndexGraph.order} nodes, ${instance.fileIndexGraph.size} edges\n`);
120
+ }
121
+ // Save workspaces
122
+ for (const wsId of manager.listWorkspaces()) {
123
+ const ws = manager.getWorkspace(wsId);
124
+ process.stderr.write(`[index] Workspace "${wsId}" knowledge: ${ws.knowledgeGraph.order} nodes, tasks: ${ws.taskGraph.order} nodes, skills: ${ws.skillGraph.order} nodes\n`);
125
+ }
126
+ await manager.shutdown();
127
+ process.stderr.write(`[index] Done. Indexed ${ids.length} project${ids.length > 1 ? 's' : ''}.\n`);
128
+ })().catch((err) => {
129
+ process.stderr.write(`[index] Fatal: ${err}\n`);
130
+ process.exit(1);
131
+ });
132
+ });
133
+ // ---------------------------------------------------------------------------
134
+ // Command: mcp — single-project stdio mode
135
+ // ---------------------------------------------------------------------------
136
+ program
137
+ .command('mcp')
138
+ .description('Index one project (or workspace), keep watching for changes, and start MCP server on stdio')
139
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
140
+ .option('--project <id>', 'Project ID (defaults to first project)')
141
+ .option('--workspace <id>', 'Workspace ID (loads all workspace projects with shared graphs)')
142
+ .option('--reindex', 'Discard persisted graphs and re-index from scratch')
143
+ .action(async (opts) => {
144
+ // Workspace mode: load all projects in the workspace with shared graphs
145
+ if (opts.workspace) {
146
+ const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
147
+ const wsConfig = mc.workspaces.get(opts.workspace);
148
+ if (!wsConfig) {
149
+ process.stderr.write(`[mcp] Workspace "${opts.workspace}" not found in config. Available: ${Array.from(mc.workspaces.keys()).join(', ')}\n`);
150
+ process.exit(1);
151
+ }
152
+ const fresh = !!opts.reindex;
153
+ if (fresh)
154
+ process.stderr.write(`[mcp] Re-indexing workspace "${opts.workspace}" from scratch\n`);
155
+ const manager = new project_manager_1.ProjectManager(mc.server);
156
+ await manager.addWorkspace(opts.workspace, wsConfig, fresh);
157
+ for (const projId of wsConfig.projects) {
158
+ const projConfig = mc.projects.get(projId);
159
+ if (!projConfig) {
160
+ process.stderr.write(`[mcp] Project "${projId}" referenced by workspace not found\n`);
161
+ process.exit(1);
162
+ }
163
+ await manager.addProject(projId, projConfig, fresh, opts.workspace);
164
+ }
165
+ // Use specified project (or first) for stdio server
166
+ const targetId = opts.project ?? wsConfig.projects[0];
167
+ if (!wsConfig.projects.includes(targetId)) {
168
+ process.stderr.write(`[mcp] Project "${targetId}" is not part of workspace "${opts.workspace}"\n`);
169
+ process.exit(1);
170
+ }
171
+ const instance = manager.getProject(targetId);
172
+ const sessionCtx = {
173
+ projectId: targetId,
174
+ workspaceId: opts.workspace,
175
+ workspaceProjects: wsConfig.projects,
176
+ };
177
+ await (0, index_1.startStdioServer)(instance.docGraph, instance.codeGraph, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.embedFns, instance.config.projectDir, instance.skillGraph, sessionCtx);
178
+ // Load models and index in background
179
+ (async () => {
180
+ await manager.loadWorkspaceModels(opts.workspace);
181
+ for (const projId of wsConfig.projects) {
182
+ await manager.loadModels(projId);
183
+ await manager.startIndexing(projId);
184
+ }
185
+ await manager.startWorkspaceMirror(opts.workspace);
186
+ process.stderr.write(`[mcp] Workspace "${opts.workspace}" fully indexed\n`);
187
+ })().catch((err) => {
188
+ process.stderr.write(`[mcp] Workspace indexer error: ${err}\n`);
189
+ });
190
+ let shuttingDown = false;
191
+ async function shutdown() {
192
+ if (shuttingDown) {
193
+ process.stderr.write('[mcp] Force exit\n');
194
+ process.exit(1);
195
+ }
196
+ shuttingDown = true;
197
+ process.stderr.write('[mcp] Shutting down...\n');
198
+ const forceTimer = setTimeout(() => { process.stderr.write('[mcp] Shutdown timeout, force exit\n'); process.exit(1); }, 5000);
199
+ try {
200
+ await manager.shutdown();
201
+ }
202
+ catch { /* ignore */ }
203
+ clearTimeout(forceTimer);
204
+ // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
205
+ }
206
+ process.on('SIGINT', () => { void shutdown(); });
207
+ process.on('SIGTERM', () => { void shutdown(); });
208
+ return;
209
+ }
210
+ const { id, project, server } = resolveProject(opts.config, opts.project);
211
+ const projectDir = path_1.default.resolve(project.projectDir);
212
+ const fresh = !!opts.reindex;
213
+ if (fresh)
214
+ process.stderr.write(`[mcp] Re-indexing project "${id}" from scratch\n`);
215
+ const ge = project.graphEmbeddings;
216
+ // Load persisted graphs (or create fresh ones if reindexing / model changed) and start MCP server immediately
217
+ const docGraph = project.docsPattern ? (0, docs_1.loadGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.docs)) : undefined;
218
+ const codeGraph = project.codePattern ? (0, code_1.loadCodeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.code)) : undefined;
219
+ const knowledgeGraph = (0, knowledge_1.loadKnowledgeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
220
+ const fileIndexGraph = (0, file_index_1.loadFileIndexGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.files));
221
+ const taskGraph = (0, task_1.loadTaskGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
222
+ const skillGraph = (0, skill_1.loadSkillGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(ge.skills));
223
+ const embedFns = buildEmbedFns(id);
224
+ const sessionCtx = { projectId: id };
225
+ await (0, index_1.startStdioServer)(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFns, project.projectDir, skillGraph, sessionCtx);
226
+ // Load models and start watcher in the background
227
+ let watcher;
228
+ let indexer;
229
+ async function startIndexing() {
230
+ await loadAllModels(id, project, server.modelsDir);
231
+ indexer = (0, indexer_1.createProjectIndexer)(docGraph, codeGraph, {
232
+ projectDir,
233
+ docsPattern: project.docsPattern || undefined,
234
+ codePattern: project.codePattern || undefined,
235
+ excludePattern: project.excludePattern || undefined,
236
+ chunkDepth: project.chunkDepth,
237
+ tsconfig: project.tsconfig,
238
+ docsModelName: `${id}:docs`,
239
+ codeModelName: `${id}:code`,
240
+ filesModelName: `${id}:files`,
241
+ }, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph);
242
+ watcher = indexer.watch();
243
+ await watcher.whenReady;
244
+ await indexer.drain();
245
+ if (docGraph) {
246
+ (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.docs));
247
+ process.stderr.write(`[mcp] Docs indexed. ${docGraph.order} nodes, ${docGraph.size} edges.\n`);
248
+ }
249
+ if (codeGraph) {
250
+ (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.code));
251
+ process.stderr.write(`[mcp] Code indexed. ${codeGraph.order} nodes, ${codeGraph.size} edges.\n`);
252
+ }
253
+ (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.files));
254
+ process.stderr.write(`[mcp] File index done. ${fileIndexGraph.order} nodes, ${fileIndexGraph.size} edges.\n`);
255
+ }
256
+ startIndexing().catch((err) => {
257
+ process.stderr.write(`[mcp] Indexer error: ${err}\n`);
258
+ });
259
+ let shuttingDown = false;
260
+ async function shutdown() {
261
+ if (shuttingDown) {
262
+ process.stderr.write('[mcp] Force exit\n');
263
+ process.exit(1);
264
+ }
265
+ shuttingDown = true;
266
+ process.stderr.write('[mcp] Shutting down...\n');
267
+ const forceTimer = setTimeout(() => {
268
+ process.stderr.write('[mcp] Shutdown timeout, force exit\n');
269
+ process.exit(1);
270
+ }, 5000);
271
+ try {
272
+ if (watcher)
273
+ await watcher.close();
274
+ if (indexer)
275
+ await indexer.drain();
276
+ if (docGraph)
277
+ (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.docs));
278
+ if (codeGraph)
279
+ (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.code));
280
+ (0, knowledge_1.saveKnowledgeGraph)(knowledgeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
281
+ (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.files));
282
+ (0, task_1.saveTaskGraph)(taskGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
283
+ }
284
+ catch { /* ignore */ }
285
+ clearTimeout(forceTimer);
286
+ // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
287
+ }
288
+ process.on('SIGINT', () => { void shutdown(); });
289
+ process.on('SIGTERM', () => { void shutdown(); });
290
+ });
291
+ // ---------------------------------------------------------------------------
292
+ // Command: serve — multi-project HTTP mode
293
+ // ---------------------------------------------------------------------------
294
+ program
295
+ .command('serve')
296
+ .description('Start multi-project MCP server over HTTP')
297
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
298
+ .option('--host <addr>', 'HTTP server bind address')
299
+ .option('--port <n>', 'HTTP server port', parseIntArg)
300
+ .option('--reindex', 'Discard persisted graphs and re-index from scratch')
301
+ .action(async (opts) => {
302
+ const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
303
+ const host = opts.host ?? mc.server.host;
304
+ const port = opts.port ?? mc.server.port;
305
+ const sessionTimeoutMs = mc.server.sessionTimeout * 1000;
306
+ const reindex = !!opts.reindex;
307
+ if (reindex)
308
+ process.stderr.write('[serve] Re-indexing all projects from scratch\n');
309
+ const manager = new project_manager_1.ProjectManager(mc.server);
310
+ // Build workspace membership lookup
311
+ const projectWorkspace = new Map();
312
+ for (const [wsId, wsConfig] of mc.workspaces) {
313
+ for (const projId of wsConfig.projects) {
314
+ projectWorkspace.set(projId, wsId);
315
+ }
316
+ }
317
+ // Add workspaces first (loads shared knowledge/task/skill graphs)
318
+ for (const [wsId, wsConfig] of mc.workspaces) {
319
+ await manager.addWorkspace(wsId, wsConfig, reindex);
320
+ }
321
+ // Add all projects (workspace projects share knowledge/task/skill graphs)
322
+ for (const [id, config] of mc.projects) {
323
+ await manager.addProject(id, config, reindex, projectWorkspace.get(id));
324
+ }
325
+ // Start HTTP server immediately (before models are loaded)
326
+ const httpServer = await (0, index_1.startMultiProjectHttpServer)(host, port, sessionTimeoutMs, manager);
327
+ // Track open connections for graceful shutdown
328
+ const openSockets = new Set();
329
+ httpServer.on('connection', (socket) => {
330
+ openSockets.add(socket);
331
+ socket.on('close', () => openSockets.delete(socket));
332
+ });
333
+ // Start auto-save
334
+ manager.startAutoSave();
335
+ // Load models and start indexing in background (workspaces first, then projects)
336
+ async function initProjects() {
337
+ // Load workspace models
338
+ for (const wsId of manager.listWorkspaces()) {
339
+ try {
340
+ await manager.loadWorkspaceModels(wsId);
341
+ }
342
+ catch (err) {
343
+ process.stderr.write(`[serve] Failed to load workspace "${wsId}" models: ${err}\n`);
344
+ }
345
+ }
346
+ // Load project models and start indexing
347
+ for (const id of manager.listProjects()) {
348
+ try {
349
+ await manager.loadModels(id);
350
+ await manager.startIndexing(id);
351
+ }
352
+ catch (err) {
353
+ process.stderr.write(`[serve] Failed to initialize project "${id}": ${err}\n`);
354
+ }
355
+ }
356
+ // Start workspace mirror watchers (after all projects are indexed)
357
+ for (const wsId of manager.listWorkspaces()) {
358
+ try {
359
+ await manager.startWorkspaceMirror(wsId);
360
+ }
361
+ catch (err) {
362
+ process.stderr.write(`[serve] Failed to start workspace "${wsId}" mirror: ${err}\n`);
363
+ }
364
+ }
365
+ }
366
+ initProjects().catch((err) => {
367
+ process.stderr.write(`[serve] Init error: ${err}\n`);
368
+ });
369
+ // Watch YAML config for hot-reload
370
+ let reloading = false;
371
+ const configWatcher = (0, watcher_1.startWatcher)(path_1.default.dirname(path_1.default.resolve(opts.config)), {
372
+ onAdd: () => { },
373
+ onChange: async (f) => {
374
+ if (path_1.default.resolve(f) !== path_1.default.resolve(opts.config))
375
+ return;
376
+ if (reloading)
377
+ return;
378
+ reloading = true;
379
+ try {
380
+ process.stderr.write('[serve] Config changed, reloading...\n');
381
+ const newMc = (0, multi_config_1.loadMultiConfig)(opts.config);
382
+ const currentIds = new Set(manager.listProjects());
383
+ const newIds = new Set(newMc.projects.keys());
384
+ // Remove projects no longer in config
385
+ for (const id of currentIds) {
386
+ if (!newIds.has(id)) {
387
+ await manager.removeProject(id);
388
+ }
389
+ }
390
+ // Add new projects
391
+ for (const [id, config] of newMc.projects) {
392
+ if (!currentIds.has(id)) {
393
+ await manager.addProject(id, config);
394
+ await manager.loadModels(id);
395
+ await manager.startIndexing(id);
396
+ }
397
+ }
398
+ // Re-add changed projects
399
+ for (const [id, config] of newMc.projects) {
400
+ if (currentIds.has(id)) {
401
+ const existing = manager.getProject(id);
402
+ if (existing && JSON.stringify(existing.config) !== JSON.stringify(config)) {
403
+ await manager.removeProject(id);
404
+ await manager.addProject(id, config);
405
+ await manager.loadModels(id);
406
+ await manager.startIndexing(id);
407
+ }
408
+ }
409
+ }
410
+ process.stderr.write('[serve] Config reload complete\n');
411
+ }
412
+ catch (err) {
413
+ process.stderr.write(`[serve] Config reload error: ${err}\n`);
414
+ }
415
+ finally {
416
+ reloading = false;
417
+ }
418
+ },
419
+ onUnlink: () => { },
420
+ }, path_1.default.basename(opts.config));
421
+ let shuttingDown = false;
422
+ async function shutdown() {
423
+ if (shuttingDown) {
424
+ process.stderr.write('[serve] Force exit\n');
425
+ process.exit(1);
426
+ }
427
+ shuttingDown = true;
428
+ process.stderr.write('[serve] Shutting down...\n');
429
+ // Force exit after 5s if graceful shutdown hangs
430
+ const forceTimer = setTimeout(() => {
431
+ process.stderr.write('[serve] Shutdown timeout, force exit\n');
432
+ process.exit(1);
433
+ }, 5000);
434
+ try {
435
+ httpServer.close();
436
+ // Destroy all open connections (including WebSocket) so the server can close
437
+ for (const socket of openSockets) {
438
+ socket.destroy();
439
+ }
440
+ openSockets.clear();
441
+ await configWatcher.close();
442
+ await manager.shutdown();
443
+ }
444
+ catch { /* ignore */ }
445
+ clearTimeout(forceTimer);
446
+ // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
447
+ }
448
+ process.on('SIGINT', () => { void shutdown(); });
449
+ process.on('SIGTERM', () => { void shutdown(); });
450
+ });
451
+ program.parse();