@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 +59 -100
- package/dist/api/index.js +136 -123
- package/dist/api/rest/index.js +1 -1
- package/dist/api/rest/websocket.js +22 -1
- 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/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/cli/index.js +9 -261
- package/dist/cli/indexer.js +1 -1
- package/dist/graphs/file-index.js +3 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +54 -0
- package/dist/lib/parsers/languages/registry.js +8 -2
- package/dist/lib/parsers/languages/typescript.js +2 -6
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/{index-D6oxrVF7.js → index-0hRezICt.js} +30 -87
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
### Connect an MCP client
|
|
35
20
|
|
|
36
|
-
|
|
21
|
+
**Claude Code:**
|
|
37
22
|
|
|
38
23
|
```bash
|
|
39
|
-
|
|
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
|
-
|
|
27
|
+
**Claude Desktop** — add via **Settings > Connectors**, enter the URL:
|
|
44
28
|
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
39
|
+
The project ID is your directory name. Multiple clients can connect simultaneously.
|
|
54
40
|
|
|
55
|
-
|
|
41
|
+
### With a config file
|
|
56
42
|
|
|
57
|
-
|
|
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
|
-
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
graphmemory serve --config graph-memory.yaml
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
volumes:
|
|
89
|
+
models:
|
|
86
90
|
```
|
|
87
91
|
|
|
88
|
-
See [docs/
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
407
|
-
projectId = maybeWs;
|
|
416
|
+
projectId = decodeURIComponent(projMatch[1]);
|
|
408
417
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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`);
|
package/dist/api/rest/index.js
CHANGED
|
@@ -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/
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
},
|