@graphmemory/server 1.1.0 → 1.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.
Files changed (104) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +66 -101
  3. package/dist/api/index.js +279 -169
  4. package/dist/api/rest/index.js +36 -16
  5. package/dist/api/rest/tools.js +8 -1
  6. package/dist/api/rest/websocket.js +22 -1
  7. package/dist/api/tools/code/search-code.js +12 -9
  8. package/dist/api/tools/code/search-files.js +1 -1
  9. package/dist/api/tools/docs/cross-references.js +3 -2
  10. package/dist/api/tools/docs/explain-symbol.js +2 -1
  11. package/dist/api/tools/docs/find-examples.js +2 -1
  12. package/dist/api/tools/docs/search-files.js +1 -1
  13. package/dist/api/tools/docs/search-snippets.js +1 -1
  14. package/dist/api/tools/docs/search.js +5 -4
  15. package/dist/api/tools/file-index/search-all-files.js +1 -1
  16. package/dist/api/tools/knowledge/add-attachment.js +14 -3
  17. package/dist/api/tools/knowledge/create-relation.js +2 -2
  18. package/dist/api/tools/knowledge/delete-relation.js +2 -2
  19. package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
  20. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  21. package/dist/api/tools/knowledge/search-notes.js +5 -4
  22. package/dist/api/tools/skills/add-attachment.js +14 -3
  23. package/dist/api/tools/skills/recall-skills.js +1 -1
  24. package/dist/api/tools/skills/remove-attachment.js +5 -1
  25. package/dist/api/tools/skills/search-skills.js +6 -5
  26. package/dist/api/tools/tasks/add-attachment.js +14 -3
  27. package/dist/api/tools/tasks/create-task-link.js +1 -1
  28. package/dist/api/tools/tasks/delete-task-link.js +1 -1
  29. package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
  30. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  31. package/dist/api/tools/tasks/search-tasks.js +5 -4
  32. package/dist/cli/index.js +69 -311
  33. package/dist/cli/indexer.js +61 -29
  34. package/dist/graphs/code.js +70 -7
  35. package/dist/graphs/docs.js +15 -2
  36. package/dist/graphs/file-index.js +20 -6
  37. package/dist/graphs/file-lang.js +1 -1
  38. package/dist/graphs/knowledge.js +20 -3
  39. package/dist/graphs/manager-types.js +1 -1
  40. package/dist/graphs/skill.js +23 -4
  41. package/dist/graphs/task.js +23 -4
  42. package/dist/lib/embedding-codec.js +65 -0
  43. package/dist/lib/file-mirror.js +7 -7
  44. package/dist/lib/frontmatter.js +3 -2
  45. package/dist/lib/jwt.js +4 -4
  46. package/dist/lib/mirror-watcher.js +5 -4
  47. package/dist/lib/multi-config.js +60 -1
  48. package/dist/lib/parsers/code.js +158 -31
  49. package/dist/lib/parsers/codeblock.js +11 -6
  50. package/dist/lib/parsers/docs.js +59 -31
  51. package/dist/lib/parsers/languages/registry.js +10 -4
  52. package/dist/lib/parsers/languages/typescript.js +195 -48
  53. package/dist/lib/project-manager.js +14 -10
  54. package/dist/lib/search/bm25.js +18 -1
  55. package/dist/lib/search/code.js +12 -3
  56. package/dist/lib/watcher.js +17 -9
  57. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  58. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  59. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  60. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  61. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  62. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  63. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  64. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  65. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  66. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  67. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  68. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  69. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  70. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  71. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  72. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  73. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  74. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  75. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  76. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  77. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  78. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  79. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  80. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  81. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  82. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  83. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  84. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  85. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  86. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  87. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  88. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  89. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  90. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  91. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  92. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  93. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  94. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  95. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  96. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  97. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  98. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  99. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  100. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  101. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  102. package/dist/ui/index.html +11 -3
  103. package/package.json +2 -2
  104. package/dist/ui/assets/index-D6oxrVF7.js +0 -1759
package/dist/api/index.js CHANGED
@@ -37,17 +37,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.createMcpServer = createMcpServer;
40
- exports.startStdioServer = startStdioServer;
41
40
  exports.startHttpServer = startHttpServer;
42
41
  exports.startMultiProjectHttpServer = startMultiProjectHttpServer;
43
42
  const http_1 = __importDefault(require("http"));
44
43
  const crypto_1 = require("crypto");
45
44
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
46
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
47
45
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
48
46
  const embedder_1 = require("../lib/embedder");
49
47
  const index_1 = require("../api/rest/index");
50
48
  const websocket_1 = require("../api/rest/websocket");
49
+ const access_1 = require("../lib/access");
50
+ const multi_config_1 = require("../lib/multi-config");
51
51
  const docs_1 = require("../graphs/docs");
52
52
  const code_1 = require("../graphs/code");
53
53
  const knowledge_1 = require("../graphs/knowledge");
@@ -148,7 +148,7 @@ function buildInstructions(ctx) {
148
148
  }
149
149
  /**
150
150
  * Creates the McpServer with all tools wired to the given graphs.
151
- * Pass docGraph to enable the 10 doc tools (5 base + 5 code-block tools);
151
+ * Pass docGraph to enable the 9 doc tools (5 base + 4 code-block) + 1 cross_references (needs codeGraph too);
152
152
  * pass codeGraph to enable the 5 code tools;
153
153
  * pass fileIndexGraph to enable the 3 file index tools.
154
154
  * cross_references requires both docGraph and codeGraph.
@@ -158,7 +158,7 @@ function buildInstructions(ctx) {
158
158
  * Tests typically pass a single function; CLI passes a map for per-graph models.
159
159
  * @param mutationQueue Optional PromiseQueue to serialize mutation tool handlers.
160
160
  */
161
- function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, mutationQueue, projectDir, skillGraph, sessionContext) {
161
+ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, mutationQueue, projectDir, skillGraph, sessionContext, readonlyGraphs, userAccess) {
162
162
  // Backward-compat: single EmbedFn → use for both document and query
163
163
  const defaultPair = { document: (q) => (0, embedder_1.embed)(q, ''), query: (q) => (0, embedder_1.embed)(q, '') };
164
164
  const fns = typeof embedFn === 'function'
@@ -180,14 +180,37 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
180
180
  };
181
181
  // Build instructions for MCP clients (workspace/project context)
182
182
  const instructions = sessionContext ? buildInstructions(sessionContext) : undefined;
183
- const server = new mcp_js_1.McpServer({ name: 'graphmemory', version: '1.1.0' }, instructions ? { instructions } : undefined);
183
+ const server = new mcp_js_1.McpServer({ name: 'graphmemory', version: '1.2.0' }, instructions ? { instructions } : undefined);
184
184
  // Mutation tools are registered through mutServer to serialize concurrent writes
185
185
  const mutServer = mutationQueue ? createMutationServer(server, mutationQueue) : server;
186
+ // Check if mutation tools should be registered for a graph:
187
+ // - graph must not be readonly (global setting — tools hidden for all)
188
+ // - user must have write access (per-user — tools hidden for this user)
189
+ // - if no userAccess map, all mutations are allowed (no auth configured)
190
+ const canMutate = (graphName) => {
191
+ if (readonlyGraphs?.has(graphName))
192
+ return false;
193
+ if (userAccess) {
194
+ const level = userAccess.get(graphName);
195
+ if (level && !(0, access_1.canWrite)(level))
196
+ return false;
197
+ }
198
+ return true;
199
+ };
200
+ // Check if a graph's tools should be registered at all (deny = no tools)
201
+ const canAccess = (graphName) => {
202
+ if (!userAccess)
203
+ return true;
204
+ const level = userAccess.get(graphName);
205
+ if (level && !(0, access_1.canRead)(level))
206
+ return false;
207
+ return true;
208
+ };
186
209
  // Context tool (always registered)
187
210
  getContext.register(server, sessionContext);
188
211
  const ext = { docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph };
189
- // Docs tools (only when docGraph is provided)
190
- if (docGraph) {
212
+ // Docs tools (only when docGraph is provided and user has access)
213
+ if (docGraph && canAccess('docs')) {
191
214
  const docMgr = new docs_1.DocGraphManager(docGraph, fns.docs, ext);
192
215
  listTopics.register(server, docMgr);
193
216
  getToc.register(server, docMgr);
@@ -199,13 +222,13 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
199
222
  listSnippets.register(server, docMgr);
200
223
  explainSymbol.register(server, docMgr);
201
224
  // Cross-graph tools (require both docGraph and codeGraph)
202
- if (codeGraph) {
225
+ if (codeGraph && canAccess('code')) {
203
226
  const codeMgrForCross = new code_1.CodeGraphManager(codeGraph, fns.code, ext);
204
227
  crossReferences.register(server, docMgr, codeMgrForCross);
205
228
  }
206
229
  }
207
- // Code tools (only when codeGraph is provided)
208
- if (codeGraph) {
230
+ // Code tools (only when codeGraph is provided and user has access)
231
+ if (codeGraph && canAccess('code')) {
209
232
  const codeMgr = new code_1.CodeGraphManager(codeGraph, fns.code, ext);
210
233
  listFiles.register(server, codeMgr);
211
234
  getFileSymbols.register(server, codeMgr);
@@ -213,93 +236,97 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
213
236
  getSymbol.register(server, codeMgr);
214
237
  searchCodeFiles.register(server, codeMgr);
215
238
  }
216
- // File index tools (always registered when fileIndexGraph is provided)
217
- if (fileIndexGraph) {
239
+ // File index tools (when fileIndexGraph is provided and user has access)
240
+ if (fileIndexGraph && canAccess('files')) {
218
241
  const fileIndexMgr = new file_index_1.FileIndexGraphManager(fileIndexGraph, fns.files, ext);
219
242
  listAllFiles.register(server, fileIndexMgr);
220
243
  searchAllFiles.register(server, fileIndexMgr);
221
244
  getFileInfo.register(server, fileIndexMgr);
222
245
  }
223
- // Knowledge tools (always registered)
224
- // Mutations (create/update/delete) go through mutServer for queue serialization
225
- if (knowledgeGraph) {
246
+ // Knowledge tools read tools gated by canAccess, mutation tools gated by canMutate
247
+ if (knowledgeGraph && canAccess('knowledge')) {
226
248
  const ctx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
227
249
  const knowledgeMgr = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, fns.knowledge, ctx, {
228
- docGraph, codeGraph, fileIndexGraph, taskGraph,
250
+ docGraph, codeGraph, fileIndexGraph, taskGraph, skillGraph,
229
251
  });
230
- createNote.register(mutServer, knowledgeMgr);
231
- updateNote.register(mutServer, knowledgeMgr);
232
- deleteNote.register(mutServer, knowledgeMgr);
233
252
  getNote.register(server, knowledgeMgr);
234
253
  listNotes.register(server, knowledgeMgr);
235
254
  searchNotes.register(server, knowledgeMgr);
236
- createRelation.register(mutServer, knowledgeMgr);
237
- deleteRelation.register(mutServer, knowledgeMgr);
238
255
  listRelations.register(server, knowledgeMgr);
239
256
  findLinkedNotes.register(server, knowledgeMgr);
240
- addNoteAttachment.register(mutServer, knowledgeMgr);
241
- removeNoteAttachment.register(mutServer, knowledgeMgr);
257
+ if (canMutate('knowledge')) {
258
+ createNote.register(mutServer, knowledgeMgr);
259
+ updateNote.register(mutServer, knowledgeMgr);
260
+ deleteNote.register(mutServer, knowledgeMgr);
261
+ createRelation.register(mutServer, knowledgeMgr);
262
+ deleteRelation.register(mutServer, knowledgeMgr);
263
+ addNoteAttachment.register(mutServer, knowledgeMgr);
264
+ removeNoteAttachment.register(mutServer, knowledgeMgr);
265
+ }
242
266
  }
243
- // Task tools (always registered when taskGraph is provided)
244
- // Mutations go through mutServer for queue serialization
245
- if (taskGraph) {
267
+ // Task tools read tools gated by canAccess, mutation tools gated by canMutate
268
+ if (taskGraph && canAccess('tasks')) {
246
269
  const taskCtx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
247
270
  const taskMgr = new task_1.TaskGraphManager(taskGraph, fns.tasks, taskCtx, {
248
- docGraph, codeGraph, knowledgeGraph, fileIndexGraph,
271
+ docGraph, codeGraph, knowledgeGraph, fileIndexGraph, skillGraph,
249
272
  });
250
- createTask.register(mutServer, taskMgr);
251
- updateTask.register(mutServer, taskMgr);
252
- deleteTask.register(mutServer, taskMgr);
253
273
  getTask.register(server, taskMgr);
254
274
  listTasksTool.register(server, taskMgr);
255
275
  searchTasksTool.register(server, taskMgr);
256
- moveTask.register(mutServer, taskMgr);
257
- linkTask.register(mutServer, taskMgr);
258
- createTaskLink.register(mutServer, taskMgr);
259
- deleteTaskLink.register(mutServer, taskMgr);
260
276
  findLinkedTasks.register(server, taskMgr);
261
- addTaskAttachment.register(mutServer, taskMgr);
262
- removeTaskAttachment.register(mutServer, taskMgr);
277
+ if (canMutate('tasks')) {
278
+ createTask.register(mutServer, taskMgr);
279
+ updateTask.register(mutServer, taskMgr);
280
+ deleteTask.register(mutServer, taskMgr);
281
+ moveTask.register(mutServer, taskMgr);
282
+ linkTask.register(mutServer, taskMgr);
283
+ createTaskLink.register(mutServer, taskMgr);
284
+ deleteTaskLink.register(mutServer, taskMgr);
285
+ addTaskAttachment.register(mutServer, taskMgr);
286
+ removeTaskAttachment.register(mutServer, taskMgr);
287
+ }
263
288
  }
264
- // Skill tools (always registered when skillGraph is provided)
265
- if (skillGraph) {
289
+ // Skill tools read tools gated by canAccess, mutation tools gated by canMutate
290
+ if (skillGraph && canAccess('skills')) {
266
291
  const skillCtx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
267
292
  const skillMgr = new skill_1.SkillGraphManager(skillGraph, fns.skills, skillCtx, {
268
293
  docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph,
269
294
  });
270
- createSkillTool.register(mutServer, skillMgr);
271
- updateSkillTool.register(mutServer, skillMgr);
272
- deleteSkillTool.register(mutServer, skillMgr);
273
295
  getSkillTool.register(server, skillMgr);
274
296
  listSkillsTool.register(server, skillMgr);
275
297
  searchSkillsTool.register(server, skillMgr);
276
- linkSkill.register(mutServer, skillMgr);
277
- createSkillLink.register(mutServer, skillMgr);
278
- deleteSkillLink.register(mutServer, skillMgr);
279
298
  findLinkedSkills.register(server, skillMgr);
280
- addSkillAttachment.register(mutServer, skillMgr);
281
- removeSkillAttachment.register(mutServer, skillMgr);
282
299
  recallSkills.register(server, skillMgr);
283
- bumpSkillUsage.register(mutServer, skillMgr);
300
+ if (canMutate('skills')) {
301
+ createSkillTool.register(mutServer, skillMgr);
302
+ updateSkillTool.register(mutServer, skillMgr);
303
+ deleteSkillTool.register(mutServer, skillMgr);
304
+ linkSkill.register(mutServer, skillMgr);
305
+ createSkillLink.register(mutServer, skillMgr);
306
+ deleteSkillLink.register(mutServer, skillMgr);
307
+ addSkillAttachment.register(mutServer, skillMgr);
308
+ removeSkillAttachment.register(mutServer, skillMgr);
309
+ bumpSkillUsage.register(mutServer, skillMgr);
310
+ }
284
311
  }
285
312
  return server;
286
313
  }
287
- async function startStdioServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, projectDir, skillGraph, sessionContext) {
288
- const server = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext);
289
- const transport = new stdio_js_1.StdioServerTransport();
290
- await server.connect(transport);
291
- process.stderr.write('[server] MCP server running on stdio\n');
292
- }
293
314
  // ---------------------------------------------------------------------------
294
315
  // HTTP transport (Streamable HTTP)
295
316
  // ---------------------------------------------------------------------------
317
+ const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
296
318
  async function collectBody(req) {
297
319
  const chunks = [];
298
- for await (const chunk of req)
320
+ let size = 0;
321
+ for await (const chunk of req) {
322
+ size += chunk.length;
323
+ if (size > MAX_BODY_SIZE)
324
+ throw new Error('Request body too large');
299
325
  chunks.push(chunk);
326
+ }
300
327
  return JSON.parse(Buffer.concat(chunks).toString());
301
328
  }
302
- async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, projectDir, skillGraph, sessionContext) {
329
+ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, projectDir, skillGraph, sessionContext, readonlyGraphs) {
303
330
  const sessions = new Map();
304
331
  // Sweep stale sessions every 60s
305
332
  const sweepInterval = setInterval(() => {
@@ -314,40 +341,46 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
314
341
  }, 60_000);
315
342
  sweepInterval.unref();
316
343
  const httpServer = http_1.default.createServer(async (req, res) => {
317
- if (req.url !== '/mcp') {
318
- res.writeHead(404).end();
319
- return;
320
- }
321
- const sessionId = req.headers['mcp-session-id'];
322
- // Existing session — route to its transport
323
- if (sessionId && sessions.has(sessionId)) {
324
- const session = sessions.get(sessionId);
325
- session.lastActivity = Date.now();
326
- const body = req.method === 'POST' ? await collectBody(req) : undefined;
327
- await session.transport.handleRequest(req, res, body);
328
- return;
344
+ try {
345
+ if (req.url !== '/mcp') {
346
+ res.writeHead(404).end();
347
+ return;
348
+ }
349
+ const sessionId = req.headers['mcp-session-id'];
350
+ // Existing session route to its transport
351
+ if (sessionId && sessions.has(sessionId)) {
352
+ const session = sessions.get(sessionId);
353
+ session.lastActivity = Date.now();
354
+ const body = req.method === 'POST' ? await collectBody(req) : undefined;
355
+ await session.transport.handleRequest(req, res, body);
356
+ return;
357
+ }
358
+ // New session — only POST (initialize) can create one
359
+ if (req.method !== 'POST') {
360
+ res.writeHead(400).end('No session');
361
+ return;
362
+ }
363
+ const body = await collectBody(req);
364
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
365
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
366
+ onsessioninitialized: (sid) => {
367
+ sessions.set(sid, { server: mcpServer, transport, lastActivity: Date.now() });
368
+ process.stderr.write(`[http] Session ${sid} started\n`);
369
+ },
370
+ });
371
+ transport.onclose = () => {
372
+ const sid = transport.sessionId;
373
+ if (sid)
374
+ sessions.delete(sid);
375
+ };
376
+ const mcpServer = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext, readonlyGraphs);
377
+ await mcpServer.connect(transport);
378
+ await transport.handleRequest(req, res, body);
329
379
  }
330
- // New session — only POST (initialize) can create one
331
- if (req.method !== 'POST') {
332
- res.writeHead(400).end('No session');
333
- return;
380
+ catch (err) {
381
+ if (!res.headersSent)
382
+ res.writeHead(400).end(String(err));
334
383
  }
335
- const body = await collectBody(req);
336
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
337
- sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
338
- onsessioninitialized: (sid) => {
339
- sessions.set(sid, { server: mcpServer, transport, lastActivity: Date.now() });
340
- process.stderr.write(`[http] Session ${sid} started\n`);
341
- },
342
- });
343
- transport.onclose = () => {
344
- const sid = transport.sessionId;
345
- if (sid)
346
- sessions.delete(sid);
347
- };
348
- const mcpServer = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext);
349
- await mcpServer.connect(transport);
350
- await transport.handleRequest(req, res, body);
351
384
  });
352
385
  return new Promise((resolve) => {
353
386
  httpServer.listen(port, host, () => {
@@ -373,100 +406,177 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
373
406
  // Express app handles /api/* routes
374
407
  const restApp = (0, index_1.createRestApp)(projectManager, restOptions);
375
408
  const httpServer = http_1.default.createServer(async (req, res) => {
376
- // Route /api/* to Express
377
- if (req.url?.startsWith('/api/')) {
378
- restApp(req, res);
379
- return;
380
- }
381
- // Route: /mcp/{workspaceId}/{projectId} or /mcp/{projectId}
382
- const wsMatch = req.url?.match(/^\/mcp\/([^/?]+)\/([^/?]+)/);
383
- const projMatch = !wsMatch ? req.url?.match(/^\/mcp\/([^/?]+)/) : null;
384
- if (!wsMatch && !projMatch) {
385
- // Everything else (UI static files, SPA fallback) goes through Express
386
- restApp(req, res);
387
- return;
388
- }
389
- // Resolve project and optional workspace from URL
390
- let projectId;
391
- let workspaceId;
392
- if (wsMatch) {
393
- const maybeWs = decodeURIComponent(wsMatch[1]);
394
- const maybeProjId = decodeURIComponent(wsMatch[2]);
395
- const ws = projectManager.getWorkspace(maybeWs);
396
- if (ws) {
397
- // Valid workspace route
398
- if (!ws.config.projects.includes(maybeProjId)) {
399
- res.writeHead(404).end(JSON.stringify({ error: `Project "${maybeProjId}" is not part of workspace "${maybeWs}"` }));
400
- return;
409
+ try {
410
+ // Route /api/* to Express
411
+ if (req.url?.startsWith('/api/')) {
412
+ restApp(req, res);
413
+ return;
414
+ }
415
+ // Route: /mcp/{workspaceId}/{projectId} or /mcp/{projectId}
416
+ const wsMatch = req.url?.match(/^\/mcp\/([^/?]+)\/([^/?]+)/);
417
+ const projMatch = !wsMatch ? req.url?.match(/^\/mcp\/([^/?]+)/) : null;
418
+ if (!wsMatch && !projMatch) {
419
+ // Everything else (UI static files, SPA fallback) goes through Express
420
+ restApp(req, res);
421
+ return;
422
+ }
423
+ // Resolve project and optional workspace from URL
424
+ let projectId;
425
+ let workspaceId;
426
+ if (wsMatch) {
427
+ const maybeWs = decodeURIComponent(wsMatch[1]);
428
+ const maybeProjId = decodeURIComponent(wsMatch[2]);
429
+ const ws = projectManager.getWorkspace(maybeWs);
430
+ if (ws) {
431
+ // Valid workspace route
432
+ if (!ws.config.projects.includes(maybeProjId)) {
433
+ res.writeHead(404).end(JSON.stringify({ error: `Project "${maybeProjId}" is not part of workspace "${maybeWs}"` }));
434
+ return;
435
+ }
436
+ workspaceId = maybeWs;
437
+ projectId = maybeProjId;
438
+ }
439
+ else {
440
+ // Not a workspace — treat first segment as projectId (fallback)
441
+ projectId = maybeWs;
401
442
  }
402
- workspaceId = maybeWs;
403
- projectId = maybeProjId;
404
443
  }
405
444
  else {
406
- // Not a workspace — treat first segment as projectId (fallback)
407
- projectId = maybeWs;
445
+ projectId = decodeURIComponent(projMatch[1]);
408
446
  }
409
- }
410
- else {
411
- projectId = decodeURIComponent(projMatch[1]);
412
- }
413
- const sessionId = req.headers['mcp-session-id'];
414
- // Existing session route to its transport
415
- if (sessionId && sessions.has(sessionId)) {
416
- const session = sessions.get(sessionId);
417
- if (session.projectId !== projectId) {
418
- res.writeHead(400).end('Session belongs to a different project');
447
+ const sessionId = req.headers['mcp-session-id'];
448
+ // Existing session — route to its transport
449
+ if (sessionId && sessions.has(sessionId)) {
450
+ const session = sessions.get(sessionId);
451
+ if (session.projectId !== projectId) {
452
+ res.writeHead(400).end('Session belongs to a different project');
453
+ return;
454
+ }
455
+ session.lastActivity = Date.now();
456
+ const body = req.method === 'POST' ? await collectBody(req) : undefined;
457
+ await session.transport.handleRequest(req, res, body);
419
458
  return;
420
459
  }
421
- session.lastActivity = Date.now();
422
- const body = req.method === 'POST' ? await collectBody(req) : undefined;
423
- await session.transport.handleRequest(req, res, body);
424
- return;
425
- }
426
- // New session — only POST (initialize) can create one
427
- if (req.method !== 'POST') {
428
- res.writeHead(400).end('No session');
429
- return;
460
+ // New session only POST (initialize) can create one
461
+ if (req.method !== 'POST') {
462
+ res.writeHead(400).end('No session');
463
+ return;
464
+ }
465
+ // Validate project exists
466
+ const project = projectManager.getProject(projectId);
467
+ if (!project) {
468
+ res.writeHead(404).end(JSON.stringify({ error: `Project "${projectId}" not found` }));
469
+ return;
470
+ }
471
+ // Auth: if users configured, require valid API key
472
+ const users = restOptions?.users ?? {};
473
+ const hasUsers = Object.keys(users).length > 0;
474
+ let userId;
475
+ if (hasUsers) {
476
+ const auth = req.headers.authorization;
477
+ if (!auth?.startsWith('Bearer ') || auth.length <= 7) {
478
+ res.writeHead(401).end(JSON.stringify({ error: 'API key required' }));
479
+ return;
480
+ }
481
+ const result = (0, access_1.resolveUserFromApiKey)(auth.slice(7), users);
482
+ if (!result) {
483
+ res.writeHead(401).end(JSON.stringify({ error: 'Invalid API key' }));
484
+ return;
485
+ }
486
+ userId = result.userId;
487
+ }
488
+ // Build session context (auto-detect workspace if not in URL)
489
+ const ws = workspaceId
490
+ ? projectManager.getWorkspace(workspaceId)
491
+ : projectManager.getProjectWorkspace(projectId);
492
+ const sessionCtx = {
493
+ projectId,
494
+ workspaceId: ws?.id,
495
+ workspaceProjects: ws?.config.projects,
496
+ userId,
497
+ };
498
+ // Build readonly set from config + workspace overrides
499
+ const mcpReadonlyGraphs = new Set();
500
+ for (const gn of multi_config_1.GRAPH_NAMES) {
501
+ if (project.config.graphConfigs[gn].readonly)
502
+ mcpReadonlyGraphs.add(gn);
503
+ }
504
+ if (project.workspaceId) {
505
+ const wsInst = projectManager.getWorkspace(project.workspaceId);
506
+ if (wsInst) {
507
+ for (const gn of ['knowledge', 'tasks', 'skills']) {
508
+ if (wsInst.config.graphConfigs[gn].readonly)
509
+ mcpReadonlyGraphs.add(gn);
510
+ }
511
+ }
512
+ }
513
+ // Build per-user access map
514
+ let mcpUserAccess;
515
+ if (userId && restOptions?.serverConfig) {
516
+ mcpUserAccess = new Map();
517
+ for (const gn of multi_config_1.GRAPH_NAMES) {
518
+ const level = (0, access_1.resolveAccess)(userId, gn, project.config, restOptions.serverConfig, ws?.config);
519
+ mcpUserAccess.set(gn, level);
520
+ }
521
+ }
522
+ const body = await collectBody(req);
523
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
524
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
525
+ onsessioninitialized: (sid) => {
526
+ sessions.set(sid, { projectId, workspaceId: ws?.id, userId, server: mcpServer, transport, lastActivity: Date.now() });
527
+ process.stderr.write(`[http] Session ${sid} started (project: ${projectId}${ws ? `, workspace: ${ws.id}` : ''}${userId ? `, user: ${userId}` : ''})\n`);
528
+ },
529
+ });
530
+ transport.onclose = () => {
531
+ const sid = transport.sessionId;
532
+ if (sid)
533
+ sessions.delete(sid);
534
+ };
535
+ const mcpServer = createMcpServer(project.docGraph, project.codeGraph, project.knowledgeGraph, project.fileIndexGraph, project.taskGraph, project.embedFns, project.mutationQueue, project.config.projectDir, project.skillGraph, sessionCtx, mcpReadonlyGraphs, mcpUserAccess);
536
+ await mcpServer.connect(transport);
537
+ await transport.handleRequest(req, res, body);
430
538
  }
431
- // Validate project exists
432
- const project = projectManager.getProject(projectId);
433
- if (!project) {
434
- res.writeHead(404).end(JSON.stringify({ error: `Project "${projectId}" not found` }));
435
- return;
539
+ catch (err) {
540
+ if (!res.headersSent)
541
+ res.writeHead(400).end(String(err));
436
542
  }
437
- // Build session context (auto-detect workspace if not in URL)
438
- const ws = workspaceId
439
- ? projectManager.getWorkspace(workspaceId)
440
- : projectManager.getProjectWorkspace(projectId);
441
- const sessionCtx = {
442
- projectId,
443
- workspaceId: ws?.id,
444
- workspaceProjects: ws?.config.projects,
445
- };
446
- const body = await collectBody(req);
447
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
448
- sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
449
- onsessioninitialized: (sid) => {
450
- sessions.set(sid, { projectId, workspaceId: ws?.id, server: mcpServer, transport, lastActivity: Date.now() });
451
- process.stderr.write(`[http] Session ${sid} started (project: ${projectId}${ws ? `, workspace: ${ws.id}` : ''})\n`);
452
- },
453
- });
454
- transport.onclose = () => {
455
- const sid = transport.sessionId;
456
- if (sid)
457
- sessions.delete(sid);
458
- };
459
- const mcpServer = createMcpServer(project.docGraph, project.codeGraph, project.knowledgeGraph, project.fileIndexGraph, project.taskGraph, project.embedFns, project.mutationQueue, project.config.projectDir, project.skillGraph, sessionCtx);
460
- await mcpServer.connect(transport);
461
- await transport.handleRequest(req, res, body);
462
543
  });
463
544
  // Attach WebSocket server for real-time events
464
- (0, websocket_1.attachWebSocket)(httpServer, projectManager);
545
+ (0, websocket_1.attachWebSocket)(httpServer, projectManager, {
546
+ jwtSecret: restOptions?.serverConfig?.jwtSecret,
547
+ users: restOptions?.users,
548
+ });
465
549
  return new Promise((resolve) => {
466
550
  httpServer.listen(port, host, () => {
467
- process.stderr.write(`[server] MCP endpoints: http://${host}:${port}/mcp/{projectId} and /mcp/{workspaceId}/{projectId}\n`);
468
- process.stderr.write(`[server] REST API at http://${host}:${port}/api/\n`);
469
- process.stderr.write(`[server] WebSocket at ws://${host}:${port}/api/ws\n`);
551
+ const base = `http://${host}:${port}`;
552
+ const projects = projectManager.listProjects();
553
+ const workspaces = projectManager.listWorkspaces();
554
+ const lines = [
555
+ '',
556
+ ' ╔══════════════════════════════════════════════╗',
557
+ ' ║ Graph Memory Server Ready ║',
558
+ ' ╚══════════════════════════════════════════════╝',
559
+ '',
560
+ ` UI ${base}/ui/`,
561
+ ` REST API ${base}/api/`,
562
+ ` WebSocket ws://${host}:${port}/api/ws`,
563
+ '',
564
+ ' MCP endpoints:',
565
+ ];
566
+ for (const id of projects) {
567
+ const ws = projectManager.getProjectWorkspace(id);
568
+ const wsLabel = ws ? ` (${ws.id})` : '';
569
+ lines.push(` ${id}${wsLabel} ${base}/mcp/${id}`);
570
+ }
571
+ if (projects.length === 0) {
572
+ lines.push(' (no projects configured)');
573
+ }
574
+ if (workspaces.length > 0) {
575
+ lines.push('');
576
+ lines.push(` Workspaces: ${workspaces.join(', ')}`);
577
+ }
578
+ lines.push('');
579
+ process.stderr.write(lines.join('\n') + '\n');
470
580
  resolve(httpServer);
471
581
  });
472
582
  });