@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.
- package/LICENSE +84 -12
- package/README.md +66 -101
- package/dist/api/index.js +279 -169
- package/dist/api/rest/index.js +36 -16
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/websocket.js +22 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/create-relation.js +2 -2
- package/dist/api/tools/knowledge/delete-relation.js +2 -2
- package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/create-task-link.js +1 -1
- package/dist/api/tools/tasks/delete-task-link.js +1 -1
- package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +69 -311
- package/dist/cli/indexer.js +61 -29
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +20 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +60 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +10 -4
- package/dist/lib/parsers/languages/typescript.js +195 -48
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- 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
|
|
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.
|
|
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 (
|
|
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
|
|
224
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
244
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
407
|
-
projectId = maybeWs;
|
|
445
|
+
projectId = decodeURIComponent(projMatch[1]);
|
|
408
446
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
});
|