@graphmemory/server 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +7 -1
  3. package/dist/api/index.js +151 -54
  4. package/dist/api/rest/code.js +2 -1
  5. package/dist/api/rest/docs.js +2 -1
  6. package/dist/api/rest/embed.js +8 -1
  7. package/dist/api/rest/index.js +39 -18
  8. package/dist/api/rest/knowledge.js +4 -2
  9. package/dist/api/rest/skills.js +2 -1
  10. package/dist/api/rest/tasks.js +2 -1
  11. package/dist/api/rest/tools.js +8 -1
  12. package/dist/api/rest/validation.js +41 -40
  13. package/dist/api/rest/websocket.js +24 -7
  14. package/dist/api/tools/code/search-code.js +12 -9
  15. package/dist/api/tools/code/search-files.js +1 -1
  16. package/dist/api/tools/docs/cross-references.js +3 -2
  17. package/dist/api/tools/docs/explain-symbol.js +2 -1
  18. package/dist/api/tools/docs/find-examples.js +2 -1
  19. package/dist/api/tools/docs/search-files.js +1 -1
  20. package/dist/api/tools/docs/search-snippets.js +1 -1
  21. package/dist/api/tools/docs/search.js +5 -4
  22. package/dist/api/tools/file-index/search-all-files.js +1 -1
  23. package/dist/api/tools/knowledge/add-attachment.js +15 -3
  24. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  25. package/dist/api/tools/knowledge/search-notes.js +5 -4
  26. package/dist/api/tools/skills/add-attachment.js +15 -3
  27. package/dist/api/tools/skills/recall-skills.js +1 -1
  28. package/dist/api/tools/skills/remove-attachment.js +5 -1
  29. package/dist/api/tools/skills/search-skills.js +6 -5
  30. package/dist/api/tools/tasks/add-attachment.js +15 -3
  31. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  32. package/dist/api/tools/tasks/search-tasks.js +5 -4
  33. package/dist/cli/index.js +63 -52
  34. package/dist/cli/indexer.js +62 -29
  35. package/dist/graphs/attachment-types.js +5 -0
  36. package/dist/graphs/code.js +99 -10
  37. package/dist/graphs/docs.js +20 -5
  38. package/dist/graphs/file-index.js +22 -6
  39. package/dist/graphs/file-lang.js +1 -1
  40. package/dist/graphs/knowledge.js +31 -7
  41. package/dist/graphs/skill.js +35 -9
  42. package/dist/graphs/task.js +35 -9
  43. package/dist/lib/defaults.js +78 -0
  44. package/dist/lib/embedder.js +11 -12
  45. package/dist/lib/embedding-codec.js +63 -0
  46. package/dist/lib/graph-persistence.js +68 -0
  47. package/dist/lib/jwt.js +4 -4
  48. package/dist/lib/mirror-watcher.js +4 -3
  49. package/dist/lib/multi-config.js +6 -1
  50. package/dist/lib/parsers/code.js +158 -31
  51. package/dist/lib/parsers/codeblock.js +11 -6
  52. package/dist/lib/parsers/docs.js +60 -31
  53. package/dist/lib/parsers/languages/registry.js +2 -2
  54. package/dist/lib/parsers/languages/typescript.js +214 -46
  55. package/dist/lib/project-manager.js +21 -11
  56. package/dist/lib/search/bm25.js +23 -5
  57. package/dist/lib/search/code.js +13 -3
  58. package/dist/lib/search/docs.js +2 -1
  59. package/dist/lib/search/file-index.js +2 -1
  60. package/dist/lib/search/files.js +3 -2
  61. package/dist/lib/search/knowledge.js +2 -1
  62. package/dist/lib/search/skills.js +2 -1
  63. package/dist/lib/search/tasks.js +2 -1
  64. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  65. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  66. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  67. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  68. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  69. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  70. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  71. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  72. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  73. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  74. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  75. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  76. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  77. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  78. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  79. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  80. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  81. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  82. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  83. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  84. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  85. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  86. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  87. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  88. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  89. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  90. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  91. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  92. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  93. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  94. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  95. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  96. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  97. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  98. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  99. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  100. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  101. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  102. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  103. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  104. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  105. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  106. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  107. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  108. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  109. package/dist/ui/index.html +11 -3
  110. package/package.json +6 -3
  111. package/dist/ui/assets/index-0hRezICt.js +0 -1702
package/LICENSE CHANGED
@@ -1,15 +1,87 @@
1
- ISC License
1
+ Copyright (c) 2025-2026 Graph Memory Team
2
2
 
3
- Copyright (c) 2026 prih
3
+ Elastic License 2.0 (ELv2)
4
4
 
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
5
+ URL: https://www.elastic.co/licensing/elastic-license
8
6
 
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
- PERFORMANCE OF THIS SOFTWARE.
7
+ ## Acceptance
8
+
9
+ By using the software, you agree to all of the terms and conditions below.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
14
+ non-sublicensable, non-transferable license to use, copy, distribute, make
15
+ available, and prepare derivative works of the software, in each case subject
16
+ to the limitations and conditions below.
17
+
18
+ ## Limitations
19
+
20
+ You may not provide the software to third parties as a hosted or managed
21
+ service, where the service provides users with access to any substantial set
22
+ of the features or functionality of the software.
23
+
24
+ You may not move, change, disable, or circumvent the license key functionality
25
+ in the software, and you may not remove or obscure any functionality in the
26
+ software that is protected by the license key.
27
+
28
+ You may not alter, remove, or obscure any licensing, copyright, or other
29
+ notices of the licensor in the software. Any use of the licensor's trademarks
30
+ is subject to applicable law.
31
+
32
+ ## Patents
33
+
34
+ The licensor grants you a license, under any patent claims the licensor can
35
+ license, or becomes able to license, to make, have made, use, sell, offer for
36
+ sale, import and have imported the software, in each case subject to the
37
+ limitations and conditions in this license. This license does not cover any
38
+ patent claims that you cause to be infringed by modifications or additions to
39
+ the software. If you or your company make any written claim that the software
40
+ infringes or contributes to infringement of any patent, your patent license
41
+ for the software granted under these terms ends immediately. If your company
42
+ makes such a claim, your patent license ends immediately for work on behalf
43
+ of your company.
44
+
45
+ ## Notices
46
+
47
+ You must ensure that anyone who gets a copy of any part of the software from
48
+ you also gets a copy of these terms.
49
+
50
+ If you modify the software, you must include in any modified copies of the
51
+ software prominent notices stating that you have modified the software.
52
+
53
+ ## No Other Rights
54
+
55
+ These terms do not imply any licenses other than those expressly granted in
56
+ these terms.
57
+
58
+ ## Termination
59
+
60
+ If you use the software in violation of these terms, such use is not licensed,
61
+ and your licenses will automatically terminate. If the licensor provides you
62
+ with a notice of your violation, and you cease all violation of this license
63
+ no later than 30 days after you receive that notice, your licenses will be
64
+ reinstated retroactively. However, if you violate these terms after such
65
+ reinstatement, any additional violation of these terms will cause your
66
+ licenses to terminate automatically and permanently.
67
+
68
+ ## No Liability
69
+
70
+ *As far as the law allows, the software comes as is, without any warranty or
71
+ condition, and the licensor will not be liable to you for any damages arising
72
+ out of these terms or the use or nature of the software, under any kind of
73
+ legal claim.*
74
+
75
+ ## Definitions
76
+
77
+ The **licensor** is the entity offering these terms, and the **software** is
78
+ the software the licensor makes available under these terms, including any
79
+ portion of it.
80
+
81
+ **you** refers to the individual or entity agreeing to these terms.
82
+
83
+ **your company** is any legal entity, sole proprietorship, or other kind of
84
+ organization that you work for, plus all organizations that have control over,
85
+ are under the control of, or are under common control with that organization.
86
+ **control** means ownership of substantially all the assets of an entity, or
87
+ the power to direct the management and policies of an entity.
package/README.md CHANGED
@@ -170,6 +170,12 @@ Full documentation is in [docs/](docs/README.md):
170
170
  - **UI**: [architecture](docs/ui-architecture.md), [features](docs/ui-features.md), [patterns](docs/ui-patterns.md)
171
171
  - **Development**: [testing](docs/testing.md), [API patterns](docs/api-patterns.md)
172
172
 
173
+ ## Contributing
174
+
175
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
176
+
177
+ For security vulnerabilities, see [SECURITY.md](SECURITY.md).
178
+
173
179
  ## License
174
180
 
175
- ISC
181
+ [Elastic License 2.0 (ELv2)](LICENSE) — free to use, modify, and self-host. Not permitted to offer as a managed/hosted service.
package/dist/api/index.js CHANGED
@@ -46,6 +46,9 @@ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamable
46
46
  const embedder_1 = require("../lib/embedder");
47
47
  const index_1 = require("../api/rest/index");
48
48
  const websocket_1 = require("../api/rest/websocket");
49
+ const access_1 = require("../lib/access");
50
+ const defaults_1 = require("../lib/defaults");
51
+ const multi_config_1 = require("../lib/multi-config");
49
52
  const docs_1 = require("../graphs/docs");
50
53
  const code_1 = require("../graphs/code");
51
54
  const knowledge_1 = require("../graphs/knowledge");
@@ -146,7 +149,7 @@ function buildInstructions(ctx) {
146
149
  }
147
150
  /**
148
151
  * Creates the McpServer with all tools wired to the given graphs.
149
- * Pass docGraph to enable the 10 doc tools (5 base + 5 code-block tools);
152
+ * Pass docGraph to enable the 9 doc tools (5 base + 4 code-block) + 1 cross_references (needs codeGraph too);
150
153
  * pass codeGraph to enable the 5 code tools;
151
154
  * pass fileIndexGraph to enable the 3 file index tools.
152
155
  * cross_references requires both docGraph and codeGraph.
@@ -156,7 +159,7 @@ function buildInstructions(ctx) {
156
159
  * Tests typically pass a single function; CLI passes a map for per-graph models.
157
160
  * @param mutationQueue Optional PromiseQueue to serialize mutation tool handlers.
158
161
  */
159
- function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, mutationQueue, projectDir, skillGraph, sessionContext) {
162
+ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, mutationQueue, projectDir, skillGraph, sessionContext, readonlyGraphs, userAccess) {
160
163
  // Backward-compat: single EmbedFn → use for both document and query
161
164
  const defaultPair = { document: (q) => (0, embedder_1.embed)(q, ''), query: (q) => (0, embedder_1.embed)(q, '') };
162
165
  const fns = typeof embedFn === 'function'
@@ -181,11 +184,34 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
181
184
  const server = new mcp_js_1.McpServer({ name: 'graphmemory', version: '1.2.0' }, instructions ? { instructions } : undefined);
182
185
  // Mutation tools are registered through mutServer to serialize concurrent writes
183
186
  const mutServer = mutationQueue ? createMutationServer(server, mutationQueue) : server;
187
+ // Check if mutation tools should be registered for a graph:
188
+ // - graph must not be readonly (global setting — tools hidden for all)
189
+ // - user must have write access (per-user — tools hidden for this user)
190
+ // - if no userAccess map, all mutations are allowed (no auth configured)
191
+ const canMutate = (graphName) => {
192
+ if (readonlyGraphs?.has(graphName))
193
+ return false;
194
+ if (userAccess) {
195
+ const level = userAccess.get(graphName);
196
+ if (level && !(0, access_1.canWrite)(level))
197
+ return false;
198
+ }
199
+ return true;
200
+ };
201
+ // Check if a graph's tools should be registered at all (deny = no tools)
202
+ const canAccess = (graphName) => {
203
+ if (!userAccess)
204
+ return true;
205
+ const level = userAccess.get(graphName);
206
+ if (level && !(0, access_1.canRead)(level))
207
+ return false;
208
+ return true;
209
+ };
184
210
  // Context tool (always registered)
185
211
  getContext.register(server, sessionContext);
186
212
  const ext = { docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph };
187
- // Docs tools (only when docGraph is provided)
188
- if (docGraph) {
213
+ // Docs tools (only when docGraph is provided and user has access)
214
+ if (docGraph && canAccess('docs')) {
189
215
  const docMgr = new docs_1.DocGraphManager(docGraph, fns.docs, ext);
190
216
  listTopics.register(server, docMgr);
191
217
  getToc.register(server, docMgr);
@@ -197,13 +223,13 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
197
223
  listSnippets.register(server, docMgr);
198
224
  explainSymbol.register(server, docMgr);
199
225
  // Cross-graph tools (require both docGraph and codeGraph)
200
- if (codeGraph) {
226
+ if (codeGraph && canAccess('code')) {
201
227
  const codeMgrForCross = new code_1.CodeGraphManager(codeGraph, fns.code, ext);
202
228
  crossReferences.register(server, docMgr, codeMgrForCross);
203
229
  }
204
230
  }
205
- // Code tools (only when codeGraph is provided)
206
- if (codeGraph) {
231
+ // Code tools (only when codeGraph is provided and user has access)
232
+ if (codeGraph && canAccess('code')) {
207
233
  const codeMgr = new code_1.CodeGraphManager(codeGraph, fns.code, ext);
208
234
  listFiles.register(server, codeMgr);
209
235
  getFileSymbols.register(server, codeMgr);
@@ -211,93 +237,96 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
211
237
  getSymbol.register(server, codeMgr);
212
238
  searchCodeFiles.register(server, codeMgr);
213
239
  }
214
- // File index tools (always registered when fileIndexGraph is provided)
215
- if (fileIndexGraph) {
240
+ // File index tools (when fileIndexGraph is provided and user has access)
241
+ if (fileIndexGraph && canAccess('files')) {
216
242
  const fileIndexMgr = new file_index_1.FileIndexGraphManager(fileIndexGraph, fns.files, ext);
217
243
  listAllFiles.register(server, fileIndexMgr);
218
244
  searchAllFiles.register(server, fileIndexMgr);
219
245
  getFileInfo.register(server, fileIndexMgr);
220
246
  }
221
- // Knowledge tools (always registered)
222
- // Mutations (create/update/delete) go through mutServer for queue serialization
223
- if (knowledgeGraph) {
247
+ // Knowledge tools read tools gated by canAccess, mutation tools gated by canMutate
248
+ if (knowledgeGraph && canAccess('knowledge')) {
224
249
  const ctx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
225
250
  const knowledgeMgr = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, fns.knowledge, ctx, {
226
251
  docGraph, codeGraph, fileIndexGraph, taskGraph, skillGraph,
227
252
  });
228
- createNote.register(mutServer, knowledgeMgr);
229
- updateNote.register(mutServer, knowledgeMgr);
230
- deleteNote.register(mutServer, knowledgeMgr);
231
253
  getNote.register(server, knowledgeMgr);
232
254
  listNotes.register(server, knowledgeMgr);
233
255
  searchNotes.register(server, knowledgeMgr);
234
- createRelation.register(mutServer, knowledgeMgr);
235
- deleteRelation.register(mutServer, knowledgeMgr);
236
256
  listRelations.register(server, knowledgeMgr);
237
257
  findLinkedNotes.register(server, knowledgeMgr);
238
- addNoteAttachment.register(mutServer, knowledgeMgr);
239
- removeNoteAttachment.register(mutServer, knowledgeMgr);
258
+ if (canMutate('knowledge')) {
259
+ createNote.register(mutServer, knowledgeMgr);
260
+ updateNote.register(mutServer, knowledgeMgr);
261
+ deleteNote.register(mutServer, knowledgeMgr);
262
+ createRelation.register(mutServer, knowledgeMgr);
263
+ deleteRelation.register(mutServer, knowledgeMgr);
264
+ addNoteAttachment.register(mutServer, knowledgeMgr);
265
+ removeNoteAttachment.register(mutServer, knowledgeMgr);
266
+ }
240
267
  }
241
- // Task tools (always registered when taskGraph is provided)
242
- // Mutations go through mutServer for queue serialization
243
- if (taskGraph) {
268
+ // Task tools read tools gated by canAccess, mutation tools gated by canMutate
269
+ if (taskGraph && canAccess('tasks')) {
244
270
  const taskCtx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
245
271
  const taskMgr = new task_1.TaskGraphManager(taskGraph, fns.tasks, taskCtx, {
246
272
  docGraph, codeGraph, knowledgeGraph, fileIndexGraph, skillGraph,
247
273
  });
248
- createTask.register(mutServer, taskMgr);
249
- updateTask.register(mutServer, taskMgr);
250
- deleteTask.register(mutServer, taskMgr);
251
274
  getTask.register(server, taskMgr);
252
275
  listTasksTool.register(server, taskMgr);
253
276
  searchTasksTool.register(server, taskMgr);
254
- moveTask.register(mutServer, taskMgr);
255
- linkTask.register(mutServer, taskMgr);
256
- createTaskLink.register(mutServer, taskMgr);
257
- deleteTaskLink.register(mutServer, taskMgr);
258
277
  findLinkedTasks.register(server, taskMgr);
259
- addTaskAttachment.register(mutServer, taskMgr);
260
- removeTaskAttachment.register(mutServer, taskMgr);
278
+ if (canMutate('tasks')) {
279
+ createTask.register(mutServer, taskMgr);
280
+ updateTask.register(mutServer, taskMgr);
281
+ deleteTask.register(mutServer, taskMgr);
282
+ moveTask.register(mutServer, taskMgr);
283
+ linkTask.register(mutServer, taskMgr);
284
+ createTaskLink.register(mutServer, taskMgr);
285
+ deleteTaskLink.register(mutServer, taskMgr);
286
+ addTaskAttachment.register(mutServer, taskMgr);
287
+ removeTaskAttachment.register(mutServer, taskMgr);
288
+ }
261
289
  }
262
- // Skill tools (always registered when skillGraph is provided)
263
- if (skillGraph) {
290
+ // Skill tools read tools gated by canAccess, mutation tools gated by canMutate
291
+ if (skillGraph && canAccess('skills')) {
264
292
  const skillCtx = projectDir ? { ...(0, manager_types_1.noopContext)(), projectDir } : (0, manager_types_1.noopContext)();
265
293
  const skillMgr = new skill_1.SkillGraphManager(skillGraph, fns.skills, skillCtx, {
266
294
  docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph,
267
295
  });
268
- createSkillTool.register(mutServer, skillMgr);
269
- updateSkillTool.register(mutServer, skillMgr);
270
- deleteSkillTool.register(mutServer, skillMgr);
271
296
  getSkillTool.register(server, skillMgr);
272
297
  listSkillsTool.register(server, skillMgr);
273
298
  searchSkillsTool.register(server, skillMgr);
274
- linkSkill.register(mutServer, skillMgr);
275
- createSkillLink.register(mutServer, skillMgr);
276
- deleteSkillLink.register(mutServer, skillMgr);
277
299
  findLinkedSkills.register(server, skillMgr);
278
- addSkillAttachment.register(mutServer, skillMgr);
279
- removeSkillAttachment.register(mutServer, skillMgr);
280
300
  recallSkills.register(server, skillMgr);
281
- bumpSkillUsage.register(mutServer, skillMgr);
301
+ if (canMutate('skills')) {
302
+ createSkillTool.register(mutServer, skillMgr);
303
+ updateSkillTool.register(mutServer, skillMgr);
304
+ deleteSkillTool.register(mutServer, skillMgr);
305
+ linkSkill.register(mutServer, skillMgr);
306
+ createSkillLink.register(mutServer, skillMgr);
307
+ deleteSkillLink.register(mutServer, skillMgr);
308
+ addSkillAttachment.register(mutServer, skillMgr);
309
+ removeSkillAttachment.register(mutServer, skillMgr);
310
+ bumpSkillUsage.register(mutServer, skillMgr);
311
+ }
282
312
  }
283
313
  return server;
284
314
  }
285
315
  // ---------------------------------------------------------------------------
286
316
  // HTTP transport (Streamable HTTP)
287
317
  // ---------------------------------------------------------------------------
288
- const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
289
318
  async function collectBody(req) {
290
319
  const chunks = [];
291
320
  let size = 0;
292
321
  for await (const chunk of req) {
293
322
  size += chunk.length;
294
- if (size > MAX_BODY_SIZE)
323
+ if (size > defaults_1.MAX_BODY_SIZE)
295
324
  throw new Error('Request body too large');
296
325
  chunks.push(chunk);
297
326
  }
298
327
  return JSON.parse(Buffer.concat(chunks).toString());
299
328
  }
300
- 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) {
301
330
  const sessions = new Map();
302
331
  // Sweep stale sessions every 60s
303
332
  const sweepInterval = setInterval(() => {
@@ -309,7 +338,7 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
309
338
  process.stderr.write(`[http] Session ${sid} timed out\n`);
310
339
  }
311
340
  }
312
- }, 60_000);
341
+ }, defaults_1.SESSION_SWEEP_INTERVAL_MS);
313
342
  sweepInterval.unref();
314
343
  const httpServer = http_1.default.createServer(async (req, res) => {
315
344
  try {
@@ -344,7 +373,7 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
344
373
  if (sid)
345
374
  sessions.delete(sid);
346
375
  };
347
- const mcpServer = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext);
376
+ const mcpServer = createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFn, undefined, projectDir, skillGraph, sessionContext, readonlyGraphs);
348
377
  await mcpServer.connect(transport);
349
378
  await transport.handleRequest(req, res, body);
350
379
  }
@@ -372,7 +401,7 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
372
401
  process.stderr.write(`[http] Session ${sid} (project: ${s.projectId}) timed out\n`);
373
402
  }
374
403
  }
375
- }, 60_000);
404
+ }, defaults_1.SESSION_SWEEP_INTERVAL_MS);
376
405
  sweepInterval.unref();
377
406
  // Express app handles /api/* routes
378
407
  const restApp = (0, index_1.createRestApp)(projectManager, restOptions);
@@ -439,6 +468,23 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
439
468
  res.writeHead(404).end(JSON.stringify({ error: `Project "${projectId}" not found` }));
440
469
  return;
441
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
+ }
442
488
  // Build session context (auto-detect workspace if not in URL)
443
489
  const ws = workspaceId
444
490
  ? projectManager.getWorkspace(workspaceId)
@@ -447,13 +493,38 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
447
493
  projectId,
448
494
  workspaceId: ws?.id,
449
495
  workspaceProjects: ws?.config.projects,
496
+ userId,
450
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
+ }
451
522
  const body = await collectBody(req);
452
523
  const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
453
524
  sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
454
525
  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`);
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`);
457
528
  },
458
529
  });
459
530
  transport.onclose = () => {
@@ -461,7 +532,7 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
461
532
  if (sid)
462
533
  sessions.delete(sid);
463
534
  };
464
- const mcpServer = createMcpServer(project.docGraph, project.codeGraph, project.knowledgeGraph, project.fileIndexGraph, project.taskGraph, project.embedFns, project.mutationQueue, project.config.projectDir, project.skillGraph, sessionCtx);
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);
465
536
  await mcpServer.connect(transport);
466
537
  await transport.handleRequest(req, res, body);
467
538
  }
@@ -477,9 +548,35 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
477
548
  });
478
549
  return new Promise((resolve) => {
479
550
  httpServer.listen(port, host, () => {
480
- process.stderr.write(`[server] MCP endpoints: http://${host}:${port}/mcp/{projectId} and /mcp/{workspaceId}/{projectId}\n`);
481
- process.stderr.write(`[server] REST API at http://${host}:${port}/api/\n`);
482
- 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');
483
580
  resolve(httpServer);
484
581
  });
485
582
  });
@@ -50,7 +50,8 @@ function createCodeRouter() {
50
50
  const symbol = p.codeManager.getSymbol(symbolId);
51
51
  if (!symbol)
52
52
  return res.status(404).json({ error: 'Symbol not found' });
53
- res.json(symbol);
53
+ const { embedding: _, fileEmbedding: _fe, ...rest } = symbol;
54
+ res.json(rest);
54
55
  }
55
56
  catch (err) {
56
57
  next(err);
@@ -52,7 +52,8 @@ function createDocsRouter() {
52
52
  const node = p.docManager.getNode(nodeId);
53
53
  if (!node)
54
54
  return res.status(404).json({ error: 'Node not found' });
55
- res.json(node);
55
+ const { embedding: _, ...rest } = node;
56
+ res.json(rest);
56
57
  }
57
58
  catch (err) {
58
59
  next(err);
@@ -8,6 +8,7 @@ const crypto_1 = __importDefault(require("crypto"));
8
8
  const express_1 = require("express");
9
9
  const zod_1 = require("zod");
10
10
  const embedder_1 = require("../../lib/embedder");
11
+ const embedding_codec_1 = require("../../lib/embedding-codec");
11
12
  /**
12
13
  * Create an Express router for the embedding API.
13
14
  * POST /api/embed — embed texts using the server's embedding model.
@@ -16,6 +17,7 @@ function createEmbedRouter(apiConfig, modelName) {
16
17
  const router = (0, express_1.Router)();
17
18
  const embedRequestSchema = zod_1.z.object({
18
19
  texts: zod_1.z.array(zod_1.z.string().max(apiConfig.maxTextChars)).min(1).max(apiConfig.maxTexts),
20
+ format: zod_1.z.enum(['json', 'base64']).optional().default('json'),
19
21
  });
20
22
  router.post('/', async (req, res, next) => {
21
23
  try {
@@ -34,7 +36,12 @@ function createEmbedRouter(apiConfig, modelName) {
34
36
  const parsed = embedRequestSchema.parse(req.body);
35
37
  const inputs = parsed.texts.map(text => ({ title: text, content: '' }));
36
38
  const embeddings = await (0, embedder_1.embedBatch)(inputs, modelName);
37
- res.json({ embeddings });
39
+ if (parsed.format === 'base64') {
40
+ res.json({ embeddings: embeddings.map(e => (0, embedding_codec_1.float32ToBase64)(e)), format: 'base64' });
41
+ }
42
+ else {
43
+ res.json({ embeddings });
44
+ }
38
45
  }
39
46
  catch (err) {
40
47
  if (err?.name === 'ZodError') {