@graphmemory/server 1.1.0 → 1.2.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/README.md CHANGED
@@ -6,86 +6,90 @@ then exposes them as **58 MCP tools** + **REST API** + **Web UI**.
6
6
 
7
7
  ## Quick start
8
8
 
9
- ### Docker (recommended)
10
-
11
9
  ```bash
12
- # 1. Create graph-memory.yaml
13
- cat > graph-memory.yaml << 'EOF'
14
- server:
15
- host: "0.0.0.0"
16
- port: 3000
17
- modelsDir: "/data/models"
10
+ npm install -g @graphmemory/server
11
+ cd /path/to/my-project
12
+ graphmemory serve
13
+ ```
18
14
 
19
- projects:
20
- my-app:
21
- projectDir: "/data/projects/my-app"
22
- EOF
15
+ That's it. No config file needed — the current directory becomes your project. Open http://localhost:3000 for the web UI.
23
16
 
24
- # 2. Run
25
- docker run -d \
26
- --name graph-memory \
27
- -p 3000:3000 \
28
- -v $(pwd)/graph-memory.yaml:/data/config/graph-memory.yaml:ro \
29
- -v /path/to/my-app:/data/projects/my-app:ro \
30
- -v graph-memory-models:/data/models \
31
- ghcr.io/graph-memory/graphmemory-server
32
- ```
17
+ The embedding model (~560 MB) downloads on first startup and is cached at `~/.graph-memory/models/`.
33
18
 
34
- Open http://localhost:3000 the web UI is ready. The embedding model (~560 MB) downloads on first startup.
19
+ ### Connect an MCP client
35
20
 
36
- ### npm
21
+ **Claude Code:**
37
22
 
38
23
  ```bash
39
- npm install -g @graphmemory/server
40
- graphmemory serve --config graph-memory.yaml
24
+ claude mcp add --transport http --scope project graph-memory http://localhost:3000/mcp/my-project
41
25
  ```
42
26
 
43
- ### From source
27
+ **Claude Desktop** — add via **Settings > Connectors**, enter the URL:
44
28
 
45
- ```bash
46
- git clone https://github.com/graph-memory/graphmemory.git
47
- cd graphmemory
48
- npm install && cd ui && npm install && cd ..
49
- npm run build
50
- node dist/cli/index.js serve --config graph-memory.yaml
29
+ ```
30
+ http://localhost:3000/mcp/my-project
31
+ ```
32
+
33
+ **Cursor / Windsurf / other clients** — enter the URL directly in settings:
34
+
35
+ ```
36
+ http://localhost:3000/mcp/my-project
51
37
  ```
52
38
 
53
- ## Connect an MCP client
39
+ The project ID is your directory name. Multiple clients can connect simultaneously.
54
40
 
55
- Start the server, then connect MCP clients to `http://localhost:3000/mcp/{projectId}`.
41
+ ### With a config file
56
42
 
57
- **Claude Desktop** add via **Settings > Connectors** in the app, enter the URL:
43
+ For multi-project setups, custom embedding models, auth, or workspaces create `graph-memory.yaml`:
58
44
 
45
+ ```yaml
46
+ projects:
47
+ my-app:
48
+ projectDir: "/path/to/my-app"
49
+ docs-site:
50
+ projectDir: "/path/to/docs"
51
+ graphs:
52
+ code:
53
+ enabled: false
59
54
  ```
60
- http://localhost:3000/mcp/my-app
55
+
56
+ ```bash
57
+ graphmemory serve --config graph-memory.yaml
61
58
  ```
62
59
 
63
- **Claude Code** run in your project directory:
60
+ See [docs/configuration.md](docs/configuration.md) for full reference and [graph-memory.yaml.example](graph-memory.yaml.example) for all options.
61
+
62
+ ### Docker
64
63
 
65
64
  ```bash
66
- claude mcp add --transport http --scope project graph-memory http://localhost:3000/mcp/my-app
65
+ docker run -d \
66
+ --name graph-memory \
67
+ -p 3000:3000 \
68
+ -v $(pwd)/graph-memory.yaml:/data/config/graph-memory.yaml:ro \
69
+ -v /path/to/my-app:/data/projects/my-app:ro \
70
+ -v graph-memory-models:/data/models \
71
+ ghcr.io/graph-memory/graphmemory-server
67
72
  ```
68
73
 
69
- Or add to `.mcp.json` manually:
70
-
71
- ```json
72
- {
73
- "mcpServers": {
74
- "graph-memory": {
75
- "type": "http",
76
- "url": "http://localhost:3000/mcp/my-app"
77
- }
78
- }
79
- }
80
- ```
74
+ Docker Compose:
81
75
 
82
- **Cursor / Windsurf / other clients** — enter the URL directly in settings:
76
+ ```yaml
77
+ services:
78
+ graph-memory:
79
+ image: ghcr.io/graph-memory/graphmemory-server
80
+ ports:
81
+ - "3000:3000"
82
+ volumes:
83
+ - ./graph-memory.yaml:/data/config/graph-memory.yaml:ro
84
+ - /path/to/my-app:/data/projects/my-app
85
+ - models:/data/models
86
+ restart: unless-stopped
83
87
 
84
- ```
85
- http://localhost:3000/mcp/my-app
88
+ volumes:
89
+ models:
86
90
  ```
87
91
 
88
- See [docs/cli.md](docs/cli.md) for stdio transport and other connection options.
92
+ See [docs/docker.md](docs/docker.md) for details.
89
93
 
90
94
  ## What it does
91
95
 
@@ -99,7 +103,7 @@ See [docs/cli.md](docs/cli.md) for stdio transport and other connection options.
99
103
  | **Skills** | Reusable recipes with steps, triggers, and usage tracking |
100
104
  | **Hybrid search** | BM25 keyword + vector cosine similarity with BFS graph expansion |
101
105
  | **Real-time** | File watching + WebSocket push to UI |
102
- | **Multi-project** | One process manages multiple projects with YAML hot-reload |
106
+ | **Multi-project** | One process manages multiple projects from a single config |
103
107
  | **Workspaces** | Share knowledge/tasks/skills across related projects |
104
108
  | **Auth & ACL** | Password login (JWT), API keys, 4-level access control |
105
109
 
@@ -125,31 +129,6 @@ Graph (Cytoscape.js visualization), Tools (MCP explorer), Help.
125
129
 
126
130
  Light/dark theme. Real-time WebSocket updates. Login page when auth is configured.
127
131
 
128
- ## Configuration
129
-
130
- All configuration via `graph-memory.yaml`. Only `projects.<id>.projectDir` is required:
131
-
132
- ```yaml
133
- server:
134
- host: "127.0.0.1"
135
- port: 3000
136
- embedding:
137
- model: "Xenova/bge-m3"
138
-
139
- projects:
140
- my-app:
141
- projectDir: "/path/to/my-app"
142
- graphs:
143
- docs:
144
- include: "**/*.md" # default
145
- code:
146
- include: "**/*.{js,ts,jsx,tsx}" # default
147
- skills:
148
- enabled: false
149
- ```
150
-
151
- See [docs/configuration.md](docs/configuration.md) for full reference and [graph-memory.yaml.example](graph-memory.yaml.example) for all options.
152
-
153
132
  ## Authentication
154
133
 
155
134
  ```yaml
@@ -171,26 +150,6 @@ server:
171
150
 
172
151
  See [docs/authentication.md](docs/authentication.md).
173
152
 
174
- ## Docker Compose
175
-
176
- ```yaml
177
- services:
178
- graph-memory:
179
- image: ghcr.io/graph-memory/graphmemory-server
180
- ports:
181
- - "3000:3000"
182
- volumes:
183
- - ./graph-memory.yaml:/data/config/graph-memory.yaml:ro
184
- - /path/to/my-app:/data/projects/my-app
185
- - models:/data/models
186
- restart: unless-stopped
187
-
188
- volumes:
189
- models:
190
- ```
191
-
192
- See [docs/docker.md](docs/docker.md).
193
-
194
153
  ## Development
195
154
 
196
155
  ```bash
package/dist/api/index.js CHANGED
@@ -37,13 +37,11 @@ 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");
@@ -180,7 +178,7 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
180
178
  };
181
179
  // Build instructions for MCP clients (workspace/project context)
182
180
  const instructions = sessionContext ? buildInstructions(sessionContext) : undefined;
183
- const server = new mcp_js_1.McpServer({ name: 'graphmemory', version: '1.1.0' }, instructions ? { instructions } : undefined);
181
+ const server = new mcp_js_1.McpServer({ name: 'graphmemory', version: '1.2.0' }, instructions ? { instructions } : undefined);
184
182
  // Mutation tools are registered through mutServer to serialize concurrent writes
185
183
  const mutServer = mutationQueue ? createMutationServer(server, mutationQueue) : server;
186
184
  // Context tool (always registered)
@@ -225,7 +223,7 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
225
223
  if (knowledgeGraph) {
226
224
  const ctx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
227
225
  const knowledgeMgr = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, fns.knowledge, ctx, {
228
- docGraph, codeGraph, fileIndexGraph, taskGraph,
226
+ docGraph, codeGraph, fileIndexGraph, taskGraph, skillGraph,
229
227
  });
230
228
  createNote.register(mutServer, knowledgeMgr);
231
229
  updateNote.register(mutServer, knowledgeMgr);
@@ -245,7 +243,7 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
245
243
  if (taskGraph) {
246
244
  const taskCtx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
247
245
  const taskMgr = new task_1.TaskGraphManager(taskGraph, fns.tasks, taskCtx, {
248
- docGraph, codeGraph, knowledgeGraph, fileIndexGraph,
246
+ docGraph, codeGraph, knowledgeGraph, fileIndexGraph, skillGraph,
249
247
  });
250
248
  createTask.register(mutServer, taskMgr);
251
249
  updateTask.register(mutServer, taskMgr);
@@ -284,19 +282,19 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
284
282
  }
285
283
  return server;
286
284
  }
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
285
  // ---------------------------------------------------------------------------
294
286
  // HTTP transport (Streamable HTTP)
295
287
  // ---------------------------------------------------------------------------
288
+ const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
296
289
  async function collectBody(req) {
297
290
  const chunks = [];
298
- for await (const chunk of req)
291
+ let size = 0;
292
+ for await (const chunk of req) {
293
+ size += chunk.length;
294
+ if (size > MAX_BODY_SIZE)
295
+ throw new Error('Request body too large');
299
296
  chunks.push(chunk);
297
+ }
300
298
  return JSON.parse(Buffer.concat(chunks).toString());
301
299
  }
302
300
  async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, projectDir, skillGraph, sessionContext) {
@@ -314,40 +312,46 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
314
312
  }, 60_000);
315
313
  sweepInterval.unref();
316
314
  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;
315
+ try {
316
+ if (req.url !== '/mcp') {
317
+ res.writeHead(404).end();
318
+ return;
319
+ }
320
+ const sessionId = req.headers['mcp-session-id'];
321
+ // Existing session route to its transport
322
+ if (sessionId && sessions.has(sessionId)) {
323
+ const session = sessions.get(sessionId);
324
+ session.lastActivity = Date.now();
325
+ const body = req.method === 'POST' ? await collectBody(req) : undefined;
326
+ await session.transport.handleRequest(req, res, body);
327
+ return;
328
+ }
329
+ // New session — only POST (initialize) can create one
330
+ if (req.method !== 'POST') {
331
+ res.writeHead(400).end('No session');
332
+ return;
333
+ }
334
+ const body = await collectBody(req);
335
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
336
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
337
+ onsessioninitialized: (sid) => {
338
+ sessions.set(sid, { server: mcpServer, transport, lastActivity: Date.now() });
339
+ process.stderr.write(`[http] Session ${sid} started\n`);
340
+ },
341
+ });
342
+ transport.onclose = () => {
343
+ const sid = transport.sessionId;
344
+ if (sid)
345
+ sessions.delete(sid);
346
+ };
347
+ const mcpServer = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext);
348
+ await mcpServer.connect(transport);
349
+ await transport.handleRequest(req, res, body);
329
350
  }
330
- // New session — only POST (initialize) can create one
331
- if (req.method !== 'POST') {
332
- res.writeHead(400).end('No session');
333
- return;
351
+ catch (err) {
352
+ if (!res.headersSent)
353
+ res.writeHead(400).end(String(err));
334
354
  }
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
355
  });
352
356
  return new Promise((resolve) => {
353
357
  httpServer.listen(port, host, () => {
@@ -373,95 +377,104 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
373
377
  // Express app handles /api/* routes
374
378
  const restApp = (0, index_1.createRestApp)(projectManager, restOptions);
375
379
  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;
380
+ try {
381
+ // Route /api/* to Express
382
+ if (req.url?.startsWith('/api/')) {
383
+ restApp(req, res);
384
+ return;
385
+ }
386
+ // Route: /mcp/{workspaceId}/{projectId} or /mcp/{projectId}
387
+ const wsMatch = req.url?.match(/^\/mcp\/([^/?]+)\/([^/?]+)/);
388
+ const projMatch = !wsMatch ? req.url?.match(/^\/mcp\/([^/?]+)/) : null;
389
+ if (!wsMatch && !projMatch) {
390
+ // Everything else (UI static files, SPA fallback) goes through Express
391
+ restApp(req, res);
392
+ return;
393
+ }
394
+ // Resolve project and optional workspace from URL
395
+ let projectId;
396
+ let workspaceId;
397
+ if (wsMatch) {
398
+ const maybeWs = decodeURIComponent(wsMatch[1]);
399
+ const maybeProjId = decodeURIComponent(wsMatch[2]);
400
+ const ws = projectManager.getWorkspace(maybeWs);
401
+ if (ws) {
402
+ // Valid workspace route
403
+ if (!ws.config.projects.includes(maybeProjId)) {
404
+ res.writeHead(404).end(JSON.stringify({ error: `Project "${maybeProjId}" is not part of workspace "${maybeWs}"` }));
405
+ return;
406
+ }
407
+ workspaceId = maybeWs;
408
+ projectId = maybeProjId;
409
+ }
410
+ else {
411
+ // Not a workspace — treat first segment as projectId (fallback)
412
+ projectId = maybeWs;
401
413
  }
402
- workspaceId = maybeWs;
403
- projectId = maybeProjId;
404
414
  }
405
415
  else {
406
- // Not a workspace — treat first segment as projectId (fallback)
407
- projectId = maybeWs;
416
+ projectId = decodeURIComponent(projMatch[1]);
408
417
  }
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');
418
+ const sessionId = req.headers['mcp-session-id'];
419
+ // Existing session — route to its transport
420
+ if (sessionId && sessions.has(sessionId)) {
421
+ const session = sessions.get(sessionId);
422
+ if (session.projectId !== projectId) {
423
+ res.writeHead(400).end('Session belongs to a different project');
424
+ return;
425
+ }
426
+ session.lastActivity = Date.now();
427
+ const body = req.method === 'POST' ? await collectBody(req) : undefined;
428
+ await session.transport.handleRequest(req, res, body);
419
429
  return;
420
430
  }
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;
431
+ // New session only POST (initialize) can create one
432
+ if (req.method !== 'POST') {
433
+ res.writeHead(400).end('No session');
434
+ return;
435
+ }
436
+ // Validate project exists
437
+ const project = projectManager.getProject(projectId);
438
+ if (!project) {
439
+ res.writeHead(404).end(JSON.stringify({ error: `Project "${projectId}" not found` }));
440
+ return;
441
+ }
442
+ // Build session context (auto-detect workspace if not in URL)
443
+ const ws = workspaceId
444
+ ? projectManager.getWorkspace(workspaceId)
445
+ : projectManager.getProjectWorkspace(projectId);
446
+ const sessionCtx = {
447
+ projectId,
448
+ workspaceId: ws?.id,
449
+ workspaceProjects: ws?.config.projects,
450
+ };
451
+ const body = await collectBody(req);
452
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
453
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
454
+ onsessioninitialized: (sid) => {
455
+ sessions.set(sid, { projectId, workspaceId: ws?.id, server: mcpServer, transport, lastActivity: Date.now() });
456
+ process.stderr.write(`[http] Session ${sid} started (project: ${projectId}${ws ? `, workspace: ${ws.id}` : ''})\n`);
457
+ },
458
+ });
459
+ transport.onclose = () => {
460
+ const sid = transport.sessionId;
461
+ if (sid)
462
+ sessions.delete(sid);
463
+ };
464
+ const mcpServer = createMcpServer(project.docGraph, project.codeGraph, project.knowledgeGraph, project.fileIndexGraph, project.taskGraph, project.embedFns, project.mutationQueue, project.config.projectDir, project.skillGraph, sessionCtx);
465
+ await mcpServer.connect(transport);
466
+ await transport.handleRequest(req, res, body);
430
467
  }
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;
468
+ catch (err) {
469
+ if (!res.headersSent)
470
+ res.writeHead(400).end(String(err));
436
471
  }
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
472
  });
463
473
  // Attach WebSocket server for real-time events
464
- (0, websocket_1.attachWebSocket)(httpServer, projectManager);
474
+ (0, websocket_1.attachWebSocket)(httpServer, projectManager, {
475
+ jwtSecret: restOptions?.serverConfig?.jwtSecret,
476
+ users: restOptions?.users,
477
+ });
465
478
  return new Promise((resolve) => {
466
479
  httpServer.listen(port, host, () => {
467
480
  process.stderr.write(`[server] MCP endpoints: http://${host}:${port}/mcp/{projectId} and /mcp/{workspaceId}/{projectId}\n`);
@@ -63,7 +63,7 @@ function createRestApp(projectManager, options) {
63
63
  }
64
64
  if (rl && rl.search > 0) {
65
65
  const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: 60_000, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
66
- app.use('/api/projects/:projectId/knowledge/notes/search', searchLimiter);
66
+ app.use('/api/projects/:projectId/knowledge/search', searchLimiter);
67
67
  app.use('/api/projects/:projectId/tasks/search', searchLimiter);
68
68
  app.use('/api/projects/:projectId/skills/search', searchLimiter);
69
69
  app.use('/api/projects/:projectId/docs/search', searchLimiter);
@@ -2,19 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.attachWebSocket = attachWebSocket;
4
4
  const ws_1 = require("ws");
5
+ const jwt_1 = require("../../lib/jwt");
5
6
  /**
6
7
  * Attach a WebSocket server to the HTTP server at /api/ws.
7
8
  * Broadcasts all ProjectManager events to connected clients.
8
9
  * Each event includes projectId — clients filter on their side.
9
10
  */
10
- function attachWebSocket(httpServer, projectManager) {
11
+ function attachWebSocket(httpServer, projectManager, options) {
11
12
  const wss = new ws_1.WebSocketServer({ noServer: true });
13
+ const jwtSecret = options?.jwtSecret;
14
+ const users = options?.users ?? {};
15
+ const hasUsers = Object.keys(users).length > 0;
12
16
  // Handle upgrade requests for /api/ws
13
17
  httpServer.on('upgrade', (req, socket, head) => {
14
18
  if (req.url !== '/api/ws') {
15
19
  socket.destroy();
16
20
  return;
17
21
  }
22
+ // Auth: if users are configured, require valid JWT cookie or reject
23
+ if (hasUsers && jwtSecret) {
24
+ const cookie = req.headers.cookie ?? '';
25
+ const match = cookie.match(/(?:^|;\s*)mgm_access=([^;]+)/);
26
+ const token = match?.[1];
27
+ if (!token) {
28
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
29
+ socket.destroy();
30
+ return;
31
+ }
32
+ const payload = (0, jwt_1.verifyToken)(token, jwtSecret);
33
+ if (!payload || payload.type !== 'access' || !users[payload.userId]) {
34
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
35
+ socket.destroy();
36
+ return;
37
+ }
38
+ }
18
39
  wss.handleUpgrade(req, socket, head, (ws) => {
19
40
  ws.on('error', (err) => {
20
41
  process.stderr.write(`[ws] Client error: ${err}\n`);
@@ -12,8 +12,8 @@ function register(server, mgr) {
12
12
  fromId: zod_1.z.string().describe('Source note ID'),
13
13
  toId: zod_1.z.string().describe('Target note ID, or target node ID in docs/code/files/tasks graph'),
14
14
  kind: zod_1.z.string().describe('Relation type, e.g. "depends_on", "references"'),
15
- targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).optional()
16
- .describe('Set to "docs", "code", "files", or "tasks" to create a cross-graph link instead of note-to-note'),
15
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).optional()
16
+ .describe('Set to "docs", "code", "files", "tasks", or "skills" to create a cross-graph link instead of note-to-note'),
17
17
  projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
18
18
  },
19
19
  }, async ({ fromId, toId, kind, targetGraph, projectId }) => {
@@ -9,8 +9,8 @@ function register(server, mgr) {
9
9
  inputSchema: {
10
10
  fromId: zod_1.z.string().describe('Source note ID'),
11
11
  toId: zod_1.z.string().describe('Target note ID, or target node ID in docs/code/files/tasks graph'),
12
- targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).optional()
13
- .describe('Set to "docs", "code", "files", or "tasks" when deleting a cross-graph link'),
12
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).optional()
13
+ .describe('Set to "docs", "code", "files", "tasks", or "skills" when deleting a cross-graph link'),
14
14
  projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
15
15
  },
16
16
  }, async ({ fromId, toId, targetGraph, projectId }) => {
@@ -11,7 +11,7 @@ function register(server, mgr) {
11
11
  'Use get_note to fetch full content of a returned note.',
12
12
  inputSchema: {
13
13
  targetId: zod_1.z.string().describe('Target node ID in the external graph (e.g. "src/config.ts", "src/auth.ts::login", "docs/api.md::Setup")'),
14
- targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).describe('Which graph the target belongs to: "docs", "code", "files", or "tasks"'),
14
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).describe('Which graph the target belongs to'),
15
15
  kind: zod_1.z.string().optional().describe('Filter by relation kind (e.g. "references", "depends_on"). If omitted, returns all relations.'),
16
16
  projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
17
17
  },
@@ -10,7 +10,7 @@ function register(server, mgr) {
10
10
  inputSchema: {
11
11
  taskId: zod_1.z.string().describe('Source task ID'),
12
12
  targetId: zod_1.z.string().describe('Target node ID in the external graph (e.g. "src/auth.ts::login", "api.md::Setup", "my-note")'),
13
- targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
13
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
14
14
  .describe('Which graph the target belongs to'),
15
15
  kind: zod_1.z.string().describe('Relation type, e.g. "references", "fixes", "implements"'),
16
16
  projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
@@ -9,7 +9,7 @@ function register(server, mgr) {
9
9
  inputSchema: {
10
10
  taskId: zod_1.z.string().describe('Source task ID'),
11
11
  targetId: zod_1.z.string().describe('Target node ID in the external graph'),
12
- targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
12
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
13
13
  .describe('Which graph the target belongs to'),
14
14
  projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
15
15
  },