@imenam/mcp-documentor 1.0.65 → 1.0.67

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 (81) hide show
  1. package/README.md +18 -11
  2. package/dist/shared/schemas/doc.schema.d.ts +15 -3
  3. package/dist/shared/schemas/doc.schema.js +35 -6
  4. package/dist/src/core/project-scope.d.ts +24 -0
  5. package/dist/src/core/project-scope.js +28 -0
  6. package/dist/src/core/storage-engine.d.ts +0 -4
  7. package/dist/src/core/storage-engine.js +4 -9
  8. package/dist/src/gui-worker.js +49 -8
  9. package/dist/src/index.js +126 -41
  10. package/dist/src/tools/check-freshness.d.ts +2 -2
  11. package/dist/src/tools/check-freshness.js +8 -7
  12. package/dist/src/tools/create-doc.js +4 -6
  13. package/dist/src/tools/delete-doc.js +14 -7
  14. package/dist/src/tools/get-doc-metadata.d.ts +2 -2
  15. package/dist/src/tools/get-doc-metadata.js +6 -5
  16. package/dist/src/tools/get-doc-version.js +5 -2
  17. package/dist/src/tools/get-doc.d.ts +2 -2
  18. package/dist/src/tools/get-doc.js +6 -5
  19. package/dist/src/tools/git-diff-doc.js +8 -4
  20. package/dist/src/tools/list-doc-versions.js +5 -2
  21. package/dist/src/tools/move-doc.js +5 -8
  22. package/dist/src/tools/patch-doc.js +6 -5
  23. package/dist/src/tools/refresh-file-snapshot.js +8 -4
  24. package/dist/src/tools/restore-doc-version.js +5 -2
  25. package/dist/src/tools/update-doc.d.ts +2 -2
  26. package/dist/src/tools/update-doc.js +6 -5
  27. package/dist/src/tools/upload-image.js +12 -3
  28. package/dist/src/version.d.ts +1 -1
  29. package/dist/src/version.js +1 -1
  30. package/gui/dist/assets/{arc-sHJlsIyi.js → arc-Carsq0k3.js} +1 -1
  31. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BiF2hxSq.js → architectureDiagram-3BPJPVTR-caqcwdTw.js} +1 -1
  32. package/gui/dist/assets/{blockDiagram-GPEHLZMM-BCnnBvty.js → blockDiagram-GPEHLZMM-0wUO8k2I.js} +1 -1
  33. package/gui/dist/assets/{c4Diagram-AAUBKEIU-Bu3KJ-ij.js → c4Diagram-AAUBKEIU-DS6q7fMy.js} +1 -1
  34. package/gui/dist/assets/channel-D51HexOc.js +1 -0
  35. package/gui/dist/assets/{chunk-2J33WTMH-C_ekvnEC.js → chunk-2J33WTMH-CrJc-G6D.js} +1 -1
  36. package/gui/dist/assets/{chunk-4BX2VUAB-Dmp27nYT.js → chunk-4BX2VUAB-C5vtPFbB.js} +1 -1
  37. package/gui/dist/assets/{chunk-55IACEB6-Jrgqw_BR.js → chunk-55IACEB6-BchyQUFM.js} +1 -1
  38. package/gui/dist/assets/{chunk-727SXJPM-BYwz54rU.js → chunk-727SXJPM-BCIaWtKq.js} +1 -1
  39. package/gui/dist/assets/{chunk-AQP2D5EJ-CuvuX4qY.js → chunk-AQP2D5EJ-BEQJDJt5.js} +1 -1
  40. package/gui/dist/assets/{chunk-FMBD7UC4-CNL_nFEL.js → chunk-FMBD7UC4-IfQ3QtrC.js} +1 -1
  41. package/gui/dist/assets/{chunk-ND2GUHAM-DECudjWh.js → chunk-ND2GUHAM-4Du33w7t.js} +1 -1
  42. package/gui/dist/assets/{chunk-QZHKN3VN-BAtGKt_6.js → chunk-QZHKN3VN-HR0CP6fa.js} +1 -1
  43. package/gui/dist/assets/classDiagram-4FO5ZUOK-CU0dQ3zy.js +1 -0
  44. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CU0dQ3zy.js +1 -0
  45. package/gui/dist/assets/{cose-bilkent-S5V4N54A-DQ2M_2Lo.js → cose-bilkent-S5V4N54A-C9Re3ENx.js} +1 -1
  46. package/gui/dist/assets/{dagre-BM42HDAG-D6vlHW8t.js → dagre-BM42HDAG-DZmcvMMp.js} +1 -1
  47. package/gui/dist/assets/{diagram-2AECGRRQ-s2G4QZQa.js → diagram-2AECGRRQ-EyJrAGht.js} +1 -1
  48. package/gui/dist/assets/{diagram-5GNKFQAL-YyU3AqQI.js → diagram-5GNKFQAL-CHd3T9_b.js} +1 -1
  49. package/gui/dist/assets/{diagram-KO2AKTUF-BuFjXIUa.js → diagram-KO2AKTUF-CIyFrZSR.js} +1 -1
  50. package/gui/dist/assets/{diagram-LMA3HP47-CjO5OS2W.js → diagram-LMA3HP47-BlkJALyz.js} +1 -1
  51. package/gui/dist/assets/{diagram-OG6HWLK6-DVYIxc4z.js → diagram-OG6HWLK6-P8LA7IZE.js} +1 -1
  52. package/gui/dist/assets/{erDiagram-TEJ5UH35-BsxhnVVZ.js → erDiagram-TEJ5UH35-BPywgWN1.js} +1 -1
  53. package/gui/dist/assets/{flowDiagram-I6XJVG4X-D2BV25QW.js → flowDiagram-I6XJVG4X-ByL0kGiV.js} +1 -1
  54. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-BR95C5C6.js → ganttDiagram-6RSMTGT7-_pnaYCU4.js} +1 -1
  55. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-bHC_XC7V.js → gitGraphDiagram-PVQCEYII-DN67CDGs.js} +1 -1
  56. package/gui/dist/assets/{index-DhwbMWam.js → index-Bjc5MOe1.js} +88 -88
  57. package/gui/dist/assets/{index-Cbo0HWer.css → index-hjkEDRmc.css} +1 -1
  58. package/gui/dist/assets/{infoDiagram-5YYISTIA-DE0rWygZ.js → infoDiagram-5YYISTIA-Bm5LEpzH.js} +1 -1
  59. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-C03LZBFa.js → ishikawaDiagram-YF4QCWOH-DUYSULkT.js} +1 -1
  60. package/gui/dist/assets/{journeyDiagram-JHISSGLW-tRPsZRYs.js → journeyDiagram-JHISSGLW-BvJ1_f0L.js} +1 -1
  61. package/gui/dist/assets/{kanban-definition-UN3LZRKU-BYePPZ0q.js → kanban-definition-UN3LZRKU-DDYKg6RY.js} +1 -1
  62. package/gui/dist/assets/{linear-Bgo5v91h.js → linear-HlpG9u6P.js} +1 -1
  63. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-C87iCyRq.js → mindmap-definition-RKZ34NQL-CPa_wo_s.js} +1 -1
  64. package/gui/dist/assets/{pieDiagram-4H26LBE5-BINdJQBG.js → pieDiagram-4H26LBE5-BWo0c4DP.js} +1 -1
  65. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BNcy-pgy.js → quadrantDiagram-W4KKPZXB-CLahU-6B.js} +1 -1
  66. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-D_DCIFsd.js → requirementDiagram-4Y6WPE33-BoYbgzQ7.js} +1 -1
  67. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-nCf_iEpj.js → sankeyDiagram-5OEKKPKP-CGm79aMo.js} +1 -1
  68. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-3rvwaBlu.js → sequenceDiagram-3UESZ5HK-BkknTD52.js} +1 -1
  69. package/gui/dist/assets/{stateDiagram-AJRCARHV-BRPOJHIL.js → stateDiagram-AJRCARHV-DSBurgwC.js} +1 -1
  70. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DfKn22XF.js +1 -0
  71. package/gui/dist/assets/{timeline-definition-PNZ67QCA-C75DjOcj.js → timeline-definition-PNZ67QCA-CBHyrGuz.js} +1 -1
  72. package/gui/dist/assets/{vennDiagram-CIIHVFJN-CLZG8u4c.js → vennDiagram-CIIHVFJN-Dn9fr_bR.js} +1 -1
  73. package/gui/dist/assets/{wardley-L42UT6IY-BJnZFkDe.js → wardley-L42UT6IY-nMuySeAb.js} +1 -1
  74. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-CBvWRjIO.js → wardleyDiagram-YWT4CUSO-Byn2STLI.js} +1 -1
  75. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-BGMFJ2t3.js → xychartDiagram-2RQKCTM6-Bi-Y9wp5.js} +1 -1
  76. package/gui/dist/index.html +2 -2
  77. package/package.json +1 -1
  78. package/gui/dist/assets/channel-fkOAmVLo.js +0 -1
  79. package/gui/dist/assets/classDiagram-4FO5ZUOK-D_v2HS-t.js +0 -1
  80. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-D_v2HS-t.js +0 -1
  81. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BUd1OQnG.js +0 -1
package/dist/src/index.js CHANGED
@@ -154,6 +154,9 @@ function handleIPCMessage(msg) {
154
154
  else if (msg.type === "UPDATE_DOC") {
155
155
  handleUpdateDocIPC(msg);
156
156
  }
157
+ else if (msg.type === "DELETE_DOC") {
158
+ handleDeleteDocIPC(msg);
159
+ }
157
160
  else if (msg.type === "GET_FILE_SECTION") {
158
161
  handleGetFileSection(msg);
159
162
  }
@@ -161,6 +164,16 @@ function handleIPCMessage(msg) {
161
164
  logger.debug(`Unhandled message from GUI: ${msg.type}`);
162
165
  }
163
166
  }
167
+ /**
168
+ * GUI requests that target a specific doc must name their project: the GUI can be
169
+ * showing project A while an agent switches the server's active project to B.
170
+ */
171
+ function requireProjectFromIPC(msg) {
172
+ const project = msg.data?.project;
173
+ if (!project)
174
+ throw new Error("project is required for this operation");
175
+ return project;
176
+ }
164
177
  async function handleGetInitialState(msg) {
165
178
  try {
166
179
  const config = storageEngine.getConfig();
@@ -272,12 +285,13 @@ async function handleListDocs(msg) {
272
285
  }
273
286
  async function handleGetDoc(msg) {
274
287
  try {
275
- const resolved = await storageEngine.resolveDoc(msg.data.id ?? msg.data.path);
288
+ const project = requireProjectFromIPC(msg);
289
+ const doc = await storageEngine.getDoc(msg.data.id ?? msg.data.path, project);
276
290
  broadcastToGUI({
277
291
  type: "GET_DOC_RESPONSE",
278
292
  correlationId: msg.correlationId,
279
- data: resolved ? { doc: resolved.doc } : null,
280
- error: resolved ? undefined : "Doc not found",
293
+ data: doc ? { doc } : null,
294
+ error: doc ? undefined : `Doc not found in project "${project}"`,
281
295
  timestamp: new Date().toISOString(),
282
296
  });
283
297
  }
@@ -330,15 +344,16 @@ async function handleCreateDocIPC(msg) {
330
344
  return;
331
345
  }
332
346
  try {
333
- const { title, content, folder, tags, project } = msg.data ?? {};
347
+ const { title, content, folder, tags } = msg.data ?? {};
334
348
  if (!title || !content)
335
349
  throw new Error("title and content are required");
350
+ const project = requireProjectFromIPC(msg);
336
351
  const result = await createDoc(storageEngine, embeddingEngine, {
337
352
  title,
338
353
  content,
354
+ project,
339
355
  ...(folder && { folder }),
340
356
  ...(tags && { tags }),
341
- ...(project && { project }),
342
357
  });
343
358
  if (result.isError) {
344
359
  const text = result.content[0]?.type === "text" ? result.content[0].text : "Create failed";
@@ -396,6 +411,7 @@ async function handleUpdateDocIPC(msg) {
396
411
  try {
397
412
  const result = await updateDoc(storageEngine, embeddingEngine, {
398
413
  path: msg.data.id,
414
+ project: requireProjectFromIPC(msg),
399
415
  content: msg.data.content,
400
416
  ...(msg.data.title !== undefined && { title: msg.data.title }),
401
417
  });
@@ -426,16 +442,72 @@ async function handleUpdateDocIPC(msg) {
426
442
  });
427
443
  }
428
444
  }
445
+ async function handleDeleteDocIPC(msg) {
446
+ if (isReadOnly()) {
447
+ broadcastToGUI({
448
+ type: "DELETE_DOC_RESPONSE",
449
+ correlationId: msg.correlationId,
450
+ error: "READ_ONLY mode: deletion disabled",
451
+ timestamp: new Date().toISOString(),
452
+ });
453
+ return;
454
+ }
455
+ try {
456
+ const project = requireProjectFromIPC(msg);
457
+ const result = await deleteDoc(storageEngine, embeddingEngine, { path: msg.data.id, project });
458
+ if (result.isError) {
459
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "Delete failed";
460
+ broadcastToGUI({
461
+ type: "DELETE_DOC_RESPONSE",
462
+ correlationId: msg.correlationId,
463
+ error: text,
464
+ timestamp: new Date().toISOString(),
465
+ });
466
+ return;
467
+ }
468
+ const message = result.content[0]?.type === "text" ? result.content[0].text : undefined;
469
+ broadcastToGUI({
470
+ type: "DELETE_DOC_RESPONSE",
471
+ correlationId: msg.correlationId,
472
+ data: { success: true, message },
473
+ timestamp: new Date().toISOString(),
474
+ });
475
+ const config = storageEngine.getConfig();
476
+ const docs = await storageEngine.listDocs();
477
+ for (const doc of docs) {
478
+ const full = await storageEngine.getDoc(doc.id);
479
+ if (full && full.tracked_files.length > 0) {
480
+ const report = computeFreshness(full.tracked_files, config.currentProject);
481
+ doc.isStale = report.status === "stale" || report.status === "broken";
482
+ doc.isBroken = report.status === "broken";
483
+ }
484
+ }
485
+ broadcastToGUI({
486
+ type: "INITIAL_STATE",
487
+ data: { config, docs, projects: config.projects },
488
+ timestamp: new Date().toISOString(),
489
+ });
490
+ }
491
+ catch (err) {
492
+ broadcastToGUI({
493
+ type: "DELETE_DOC_RESPONSE",
494
+ correlationId: msg.correlationId,
495
+ error: err.message,
496
+ timestamp: new Date().toISOString(),
497
+ });
498
+ }
499
+ }
429
500
  async function handleGetFileSection(msg) {
430
501
  try {
431
502
  const { docId, filePath } = msg.data;
432
- const resolved = await storageEngine.resolveDoc(docId);
433
- if (!resolved)
434
- throw new Error("Doc not found");
435
- const trackedFile = resolved.doc.tracked_files.find((f) => f.path === filePath);
503
+ const project = requireProjectFromIPC(msg);
504
+ const doc = await storageEngine.getDoc(docId, project);
505
+ if (!doc)
506
+ throw new Error(`Doc not found in project "${project}"`);
507
+ const trackedFile = doc.tracked_files.find((f) => f.path === filePath);
436
508
  if (!trackedFile || !trackedFile.sections?.length)
437
509
  throw new Error("No sections for this file");
438
- const absolutePath = toAbsolutePath(trackedFile.path, resolved.project);
510
+ const absolutePath = toAbsolutePath(trackedFile.path, project);
439
511
  if (!absolutePath)
440
512
  throw new Error("Cannot resolve file on this machine (no code root configured)");
441
513
  const fileLines = fs.readFileSync(absolutePath, "utf-8").split("\n");
@@ -468,18 +540,18 @@ async function handleGetFileSection(msg) {
468
540
  }
469
541
  async function handleCheckFreshness(msg) {
470
542
  try {
471
- const resolved = await storageEngine.resolveDoc(msg.data.id ?? msg.data.path);
472
- const doc = resolved?.doc;
543
+ const project = requireProjectFromIPC(msg);
544
+ const doc = await storageEngine.getDoc(msg.data.id ?? msg.data.path, project);
473
545
  if (!doc) {
474
546
  broadcastToGUI({
475
547
  type: "CHECK_FRESHNESS_RESPONSE",
476
548
  correlationId: msg.correlationId,
477
- error: "Doc not found",
549
+ error: `Doc not found in project "${project}"`,
478
550
  timestamp: new Date().toISOString(),
479
551
  });
480
552
  return;
481
553
  }
482
- const report = computeFreshness(doc.tracked_files, resolved.project);
554
+ const report = computeFreshness(doc.tracked_files, project);
483
555
  broadcastToGUI({
484
556
  type: "CHECK_FRESHNESS_RESPONSE",
485
557
  correlationId: msg.correlationId,
@@ -536,7 +608,10 @@ async function handleSearchDocs(msg) {
536
608
  }
537
609
  async function handleGitDiff(msg) {
538
610
  try {
539
- const result = await gitDiffDoc(storageEngine, { paths: [msg.data.id ?? msg.data.path] });
611
+ const result = await gitDiffDoc(storageEngine, {
612
+ paths: [msg.data.id ?? msg.data.path],
613
+ project: requireProjectFromIPC(msg),
614
+ });
540
615
  const text = result.content[0]?.text ?? "";
541
616
  broadcastToGUI({
542
617
  type: "GIT_DIFF_RESPONSE",
@@ -599,8 +674,7 @@ async function handleUpdateConfig(msg) {
599
674
  async function handleGetDocHistory(msg) {
600
675
  try {
601
676
  const id = msg.data?.id ?? msg.data?.path;
602
- const config = storageEngine.getConfig();
603
- const project = msg.data?.project ?? config.currentProject;
677
+ const project = requireProjectFromIPC(msg);
604
678
  const versions = historyStore.listVersions(project, id);
605
679
  broadcastToGUI({
606
680
  type: "GET_DOC_HISTORY_RESPONSE",
@@ -622,8 +696,7 @@ async function handleGetDocVersionIPC(msg) {
622
696
  try {
623
697
  const id = msg.data?.id ?? msg.data?.path;
624
698
  const version = msg.data?.version;
625
- const config = storageEngine.getConfig();
626
- const project = msg.data?.project ?? config.currentProject;
699
+ const project = requireProjectFromIPC(msg);
627
700
  const entry = historyStore.getVersion(project, id, version);
628
701
  broadcastToGUI({
629
702
  type: "GET_DOC_VERSION_RESPONSE",
@@ -716,6 +789,7 @@ async function reconnectGui(args) {
716
789
  };
717
790
  }
718
791
  server.setRequestHandler(ListToolsRequestSchema, async () => {
792
+ const PROJECT_PARAM_DESCRIPTION = "Project the documentation entry belongs to. REQUIRED — never guessed, and never defaulted to the current project. Use list_projects or get_doc_tree if you are unsure which project holds the entry.";
719
793
  const tools = [
720
794
  {
721
795
  name: "create_doc",
@@ -767,20 +841,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
767
841
  items: { type: "string" },
768
842
  description: "IDs of related documentation entries",
769
843
  },
770
- project: { type: "string", description: "Project to create the doc in (defaults to current project if omitted). If the project name does not exist yet, it will be created automatically." },
844
+ project: { type: "string", description: "Project to create the doc in. REQUIRED — never defaulted to the current project. If the project name does not exist yet, it will be created automatically." },
771
845
  },
772
- required: ["title"],
846
+ required: ["title", "project"],
773
847
  },
774
848
  },
775
849
  {
776
850
  name: "get_doc",
777
- description: "Retrieve a documentation entry by its path, including full content, tracked files, and metadata.",
851
+ description: "Retrieve a documentation entry by its path, including full content, tracked files, and metadata. The project is required — lookups are never resolved across projects.",
778
852
  inputSchema: {
779
853
  type: "object",
780
854
  properties: {
781
855
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication' or 'authentication' for root docs" },
856
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
782
857
  },
783
- required: ["path"],
858
+ required: ["path", "project"],
784
859
  },
785
860
  },
786
861
  {
@@ -846,8 +921,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
846
921
  items: { type: "string" },
847
922
  description: "Paths of related documentation entries (replaces existing), e.g. ['backend/auth/authentication']",
848
923
  },
924
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
849
925
  },
850
- required: ["path"],
926
+ required: ["path", "project"],
851
927
  },
852
928
  },
853
929
  {
@@ -858,8 +934,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
858
934
  properties: {
859
935
  path: { type: "string", description: "Current path of the documentation entry, e.g. 'backend/authentication'" },
860
936
  new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both." },
937
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " The doc stays in this project — move_doc never moves an entry across projects." },
861
938
  },
862
- required: ["path", "new_path"],
939
+ required: ["path", "new_path", "project"],
863
940
  },
864
941
  },
865
942
  {
@@ -881,7 +958,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
881
958
  type: "object",
882
959
  properties: {
883
960
  path: { type: "string", description: "Path of a single documentation entry to check, e.g. 'backend/auth/authentication'. If omitted, scans all docs in the project." },
884
- project: { type: "string", description: "Project to scan (scans all projects if omitted). Only used when path is omitted." },
961
+ project: { type: "string", description: "Project to work in. REQUIRED whenever path is provided — the freshness of a single entry is never resolved across projects. When path is omitted, it selects the project to scan (all projects if omitted too)." },
885
962
  },
886
963
  required: [],
887
964
  },
@@ -921,8 +998,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
921
998
  items: { type: "string" },
922
999
  description: "Specific tracked file paths to refresh (all tracked files if omitted)",
923
1000
  },
1001
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " Every entry in paths must belong to it — call once per project otherwise." },
924
1002
  },
925
- required: ["paths"],
1003
+ required: ["paths", "project"],
926
1004
  },
927
1005
  },
928
1006
  {
@@ -932,8 +1010,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
932
1010
  type: "object",
933
1011
  properties: {
934
1012
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1013
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
935
1014
  },
936
- required: ["path"],
1015
+ required: ["path", "project"],
937
1016
  },
938
1017
  },
939
1018
  {
@@ -947,8 +1026,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
947
1026
  search_file: { type: "string", description: "Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
948
1027
  replacement: { type: "string", description: "Text to replace it with. Supports [copy-from-codebase|/absolute/path/to/file.ts|startLine|endLine] — the tag is replaced at save time by a fenced code block, and the file+range is automatically added to tracked_files. IMPORTANT: always read the source file first to identify the exact line numbers before writing the tag." },
949
1028
  replacement_file: { type: "string", description: "Absolute path to a text file whose content will be used as the replacement. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
1029
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
950
1030
  },
951
- required: ["path", "search"],
1031
+ required: ["path", "search", "project"],
952
1032
  },
953
1033
  },
954
1034
  {
@@ -967,8 +1047,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
967
1047
  items: { type: "string" },
968
1048
  description: "Specific tracked file paths to diff (all tracked files if omitted)",
969
1049
  },
1050
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " Every entry in paths must belong to it — call once per project otherwise." },
970
1051
  },
971
- required: ["paths"],
1052
+ required: ["paths", "project"],
972
1053
  },
973
1054
  },
974
1055
  {
@@ -984,13 +1065,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
984
1065
  },
985
1066
  {
986
1067
  name: "delete_doc",
987
- description: "Permanently delete a documentation entry. Works across all projects — no need to set the active project first.",
1068
+ description: "Permanently delete a documentation entry from an explicitly named project. The project is MANDATORY: deletion is never resolved across projects, so an entry that exists under the same path in several projects can never be deleted by mistake. Use list_projects / get_doc_tree first if you are unsure which project holds the entry.",
988
1069
  inputSchema: {
989
1070
  type: "object",
990
1071
  properties: {
991
1072
  path: { type: "string", description: "Path of the documentation entry to delete, e.g. 'backend/auth/authentication'" },
1073
+ project: {
1074
+ type: "string",
1075
+ description: "Project the documentation entry belongs to. Required — never guessed, and never defaulted to the current project.",
1076
+ },
992
1077
  },
993
- required: ["path"],
1078
+ required: ["path", "project"],
994
1079
  },
995
1080
  },
996
1081
  {
@@ -1010,9 +1095,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1010
1095
  type: "object",
1011
1096
  properties: {
1012
1097
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1013
- project: { type: "string", description: "Project to look in (defaults to current project)" },
1098
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1014
1099
  },
1015
- required: ["path"],
1100
+ required: ["path", "project"],
1016
1101
  },
1017
1102
  },
1018
1103
  {
@@ -1023,9 +1108,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1023
1108
  properties: {
1024
1109
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1025
1110
  version: { type: "number", description: "Version number to retrieve" },
1026
- project: { type: "string", description: "Project to look in (defaults to current project)" },
1111
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1027
1112
  },
1028
- required: ["path", "version"],
1113
+ required: ["path", "version", "project"],
1029
1114
  },
1030
1115
  },
1031
1116
  {
@@ -1036,9 +1121,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1036
1121
  properties: {
1037
1122
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1038
1123
  version: { type: "number", description: "Version number to restore" },
1039
- project: { type: "string", description: "Project to look in (defaults to current project)" },
1124
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1040
1125
  },
1041
- required: ["path", "version"],
1126
+ required: ["path", "version", "project"],
1042
1127
  },
1043
1128
  },
1044
1129
  {
@@ -1064,7 +1149,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1064
1149
  docs: {
1065
1150
  type: "array",
1066
1151
  items: { type: "string" },
1067
- description: "Doc references to include: 'project:doc-id' for cross-project, or 'doc-id' for current project",
1152
+ description: "Doc references to include, each fully qualified as 'project:doc-id'. The project prefix is REQUIRED — a bare 'doc-id' is rejected so a bundle can never pick up another project's entry.",
1068
1153
  },
1069
1154
  description: { type: "string", description: "Short description of the bundle (optional)" },
1070
1155
  },
@@ -1090,10 +1175,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1090
1175
  properties: {
1091
1176
  filename: { type: "string", description: "Filename with extension, e.g. 'screenshot.png'" },
1092
1177
  filePath: { type: "string", description: "Absolute path to the image file on the server" },
1093
- project: { type: "string", description: "Target project (defaults to current project)" },
1178
+ project: { type: "string", description: "Target project whose assets receive the image. REQUIRED — never defaulted to the current project." },
1094
1179
  alt: { type: "string", description: "Alt text for the markdown image link (optional)" },
1095
1180
  },
1096
- required: ["filename", "filePath"],
1181
+ required: ["filename", "filePath", "project"],
1097
1182
  },
1098
1183
  },
1099
1184
  {
@@ -4,15 +4,15 @@ export declare function computeFileHash(absolutePath: string): string;
4
4
  export declare function computeFreshness(trackedFiles: TrackedFile[], project: string): FreshnessReport;
5
5
  export declare function formatFreshnessReport(title: string, report: FreshnessReport): string;
6
6
  export declare function checkFreshness(storageEngine: StorageEngine, args: unknown): Promise<{
7
- isError: boolean;
8
7
  content: {
9
8
  type: string;
10
9
  text: string;
11
10
  }[];
11
+ isError?: undefined;
12
12
  } | {
13
+ isError: boolean;
13
14
  content: {
14
15
  type: string;
15
16
  text: string;
16
17
  }[];
17
- isError?: undefined;
18
18
  }>;
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import { CheckFreshnessArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { findAndHashSection } from "../core/section-tracker.js";
5
5
  import { toAbsolutePath } from "../core/path-resolver.js";
6
+ import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
6
7
  export function computeFileHash(absolutePath) {
7
8
  return crypto.createHash("sha256").update(fs.readFileSync(absolutePath)).digest("hex");
8
9
  }
@@ -192,13 +193,13 @@ export async function checkFreshness(storageEngine, args) {
192
193
  try {
193
194
  const validated = CheckFreshnessArgsSchema.parse(args);
194
195
  if (validated.path) {
195
- const resolved = await storageEngine.resolveDoc(validated.path);
196
- if (!resolved) {
197
- return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
198
- }
199
- const { doc, project } = resolved;
200
- const report = computeFreshness(doc.tracked_files, project);
201
- return { content: [{ type: "text", text: formatFreshnessReport(doc.title, report) }] };
196
+ // The schema guarantees project is present alongside path.
197
+ const project = validated.project;
198
+ const resolved = await resolveDocInProject(storageEngine, validated.path, project, "freshness check");
199
+ if (isScopeError(resolved))
200
+ return scopeErrorResult(resolved);
201
+ const report = computeFreshness(resolved.doc.tracked_files, project);
202
+ return { content: [{ type: "text", text: formatFreshnessReport(resolved.doc.title, report) }] };
202
203
  }
203
204
  if (validated.project) {
204
205
  const scan = await scanProjectFreshness(storageEngine, validated.project);
@@ -13,7 +13,7 @@ export function slugify(title) {
13
13
  export async function createDoc(storageEngine, embeddingEngine, args) {
14
14
  try {
15
15
  const validated = CreateDocArgsSchema.parse(args);
16
- const { project } = (args ?? {});
16
+ const { project } = validated;
17
17
  const rawContent = validated.content_file
18
18
  ? fs.readFileSync(validated.content_file, "utf-8")
19
19
  : validated.content;
@@ -25,13 +25,11 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
25
25
  if (existing) {
26
26
  return {
27
27
  isError: true,
28
- content: [{ type: "text", text: `A documentation entry with path "${id}" already exists. Use update_doc to modify it.` }],
28
+ content: [{ type: "text", text: `A documentation entry with path "${id}" already exists in project "${project}". Use update_doc to modify it.` }],
29
29
  };
30
30
  }
31
- if (project) {
32
- storageEngine.ensureProject(project);
33
- }
34
- const resolvedProject = project ?? storageEngine.getConfig().currentProject;
31
+ storageEngine.ensureProject(project);
32
+ const resolvedProject = project;
35
33
  const allTrackedInputs = [
36
34
  ...(validated.tracked_files ?? []),
37
35
  ...additionalTrackedFiles,
@@ -1,14 +1,21 @@
1
+ import { DeleteDocArgsSchema } from "../../shared/schemas/doc.schema.js";
2
+ import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
1
3
  export async function deleteDoc(storageEngine, embeddingEngine, args) {
2
4
  try {
3
- const { path: docPath } = args;
4
- const resolved = await storageEngine.resolveDoc(docPath);
5
- if (!resolved) {
6
- return { isError: true, content: [{ type: "text", text: `Documentation entry "${docPath}" not found in any project.` }] };
5
+ const { path: docPath, project } = DeleteDocArgsSchema.parse(args);
6
+ const resolved = await resolveDocInProject(storageEngine, docPath, project, "deletion");
7
+ if (isScopeError(resolved))
8
+ return scopeErrorResult(resolved);
9
+ const deleted = await storageEngine.deleteDoc(resolved.doc.id, project);
10
+ if (!deleted) {
11
+ return {
12
+ isError: true,
13
+ content: [{ type: "text", text: `Failed to delete "${docPath}" from project "${project}".` }],
14
+ };
7
15
  }
8
- await storageEngine.deleteDoc(resolved.doc.id, resolved.project);
9
- await embeddingEngine.removeEmbedding(resolved.doc.id, resolved.project);
16
+ await embeddingEngine.removeEmbedding(resolved.doc.id, project);
10
17
  return {
11
- content: [{ type: "text", text: `Documentation entry "${docPath}" deleted from project "${resolved.project}".` }],
18
+ content: [{ type: "text", text: `Documentation entry "${docPath}" deleted from project "${project}".` }],
12
19
  };
13
20
  }
14
21
  catch (err) {
@@ -1,14 +1,14 @@
1
1
  import type { StorageEngine } from "../core/storage-engine.js";
2
2
  export declare function getDocMetadata(storageEngine: StorageEngine, args: unknown): Promise<{
3
- isError: boolean;
4
3
  content: {
5
4
  type: string;
6
5
  text: string;
7
6
  }[];
7
+ isError?: undefined;
8
8
  } | {
9
+ isError: boolean;
9
10
  content: {
10
11
  type: string;
11
12
  text: string;
12
13
  }[];
13
- isError?: undefined;
14
14
  }>;
@@ -3,14 +3,15 @@ import { GetDocMetadataArgsSchema } from "../../shared/schemas/doc.schema.js";
3
3
  import { computeFreshness } from "./check-freshness.js";
4
4
  import { findAndHashSection } from "../core/section-tracker.js";
5
5
  import { toAbsolutePath } from "../core/path-resolver.js";
6
+ import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
6
7
  export async function getDocMetadata(storageEngine, args) {
7
8
  try {
8
9
  const validated = GetDocMetadataArgsSchema.parse(args);
9
- const resolved = await storageEngine.resolveDoc(validated.path);
10
- if (!resolved) {
11
- return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
12
- }
13
- const { doc, project } = resolved;
10
+ const resolved = await resolveDocInProject(storageEngine, validated.path, validated.project, "lookup");
11
+ if (isScopeError(resolved))
12
+ return scopeErrorResult(resolved);
13
+ const { doc } = resolved;
14
+ const project = validated.project;
14
15
  const report = computeFreshness(doc.tracked_files, project);
15
16
  const fileStatusIcon = (filePath) => {
16
17
  if (report.broken.includes(filePath))
@@ -1,9 +1,12 @@
1
1
  import { GetDocVersionArgsSchema } from "../../shared/schemas/doc.schema.js";
2
+ import { assertProject, scopeErrorResult } from "../core/project-scope.js";
2
3
  export async function getDocVersion(historyStore, storageEngine, args) {
3
4
  try {
4
5
  const validated = GetDocVersionArgsSchema.parse(args);
5
- const config = storageEngine.getConfig();
6
- const project = validated.project ?? config.currentProject;
6
+ const project = validated.project;
7
+ const projectError = assertProject(storageEngine, project);
8
+ if (projectError)
9
+ return scopeErrorResult(projectError);
7
10
  const entry = historyStore.getVersion(project, validated.path, validated.version);
8
11
  if (!entry) {
9
12
  return {
@@ -1,14 +1,14 @@
1
1
  import type { StorageEngine } from "../core/storage-engine.js";
2
2
  export declare function getDoc(storageEngine: StorageEngine, args: unknown): Promise<{
3
- isError: boolean;
4
3
  content: {
5
4
  type: string;
6
5
  text: string;
7
6
  }[];
7
+ isError?: undefined;
8
8
  } | {
9
+ isError: boolean;
9
10
  content: {
10
11
  type: string;
11
12
  text: string;
12
13
  }[];
13
- isError?: undefined;
14
14
  }>;
@@ -3,14 +3,15 @@ import { GetDocArgsSchema } from "../../shared/schemas/doc.schema.js";
3
3
  import { computeFreshness } from "./check-freshness.js";
4
4
  import { findAndHashSection } from "../core/section-tracker.js";
5
5
  import { toAbsolutePath } from "../core/path-resolver.js";
6
+ import { resolveDocInProject, isScopeError, scopeErrorResult } from "../core/project-scope.js";
6
7
  export async function getDoc(storageEngine, args) {
7
8
  try {
8
9
  const validated = GetDocArgsSchema.parse(args);
9
- const resolved = await storageEngine.resolveDoc(validated.path);
10
- if (!resolved) {
11
- return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
12
- }
13
- const { doc, project: resolvedProject } = resolved;
10
+ const resolved = await resolveDocInProject(storageEngine, validated.path, validated.project, "lookup");
11
+ if (isScopeError(resolved))
12
+ return scopeErrorResult(resolved);
13
+ const { doc } = resolved;
14
+ const resolvedProject = validated.project;
14
15
  const trackedFilesText = doc.tracked_files.length > 0
15
16
  ? doc.tracked_files.map((f) => {
16
17
  if (f.sections && f.sections.length > 0) {
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import { GitDiffDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { computeFreshness } from "./check-freshness.js";
5
5
  import { toAbsolutePath, toStoredPath } from "../core/path-resolver.js";
6
+ import { assertProject, scopeErrorResult } from "../core/project-scope.js";
6
7
  function getGitDiffForFile(tracked, project) {
7
8
  const filePath = toAbsolutePath(tracked.path, project);
8
9
  if (!filePath) {
@@ -42,15 +43,18 @@ function getGitDiffForFile(tracked, project) {
42
43
  export async function gitDiffDoc(storageEngine, args) {
43
44
  try {
44
45
  const validated = GitDiffDocArgsSchema.parse(args);
46
+ const project = validated.project;
47
+ const projectError = assertProject(storageEngine, project);
48
+ if (projectError)
49
+ return scopeErrorResult(projectError);
45
50
  const docSections = [];
46
51
  const seenFilePaths = new Set();
47
52
  for (const docPath of validated.paths) {
48
- const resolved = await storageEngine.resolveDoc(docPath);
49
- if (!resolved) {
50
- docSections.push(`## ${docPath}\n\n[ERROR] Documentation entry not found in any project.`);
53
+ const doc = await storageEngine.getDoc(docPath, project);
54
+ if (!doc) {
55
+ docSections.push(`## ${docPath}\n\n[ERROR] Documentation entry not found in project "${project}".`);
51
56
  continue;
52
57
  }
53
- const { doc, project } = resolved;
54
58
  if (!doc.tracked_files || doc.tracked_files.length === 0) {
55
59
  docSections.push(`## ${doc.title}\n\n[INFO] No tracked files.`);
56
60
  continue;
@@ -1,9 +1,12 @@
1
1
  import { ListDocVersionsArgsSchema } from "../../shared/schemas/doc.schema.js";
2
+ import { assertProject, scopeErrorResult } from "../core/project-scope.js";
2
3
  export async function listDocVersions(historyStore, storageEngine, args) {
3
4
  try {
4
5
  const validated = ListDocVersionsArgsSchema.parse(args);
5
- const config = storageEngine.getConfig();
6
- const project = validated.project ?? config.currentProject;
6
+ const project = validated.project;
7
+ const projectError = assertProject(storageEngine, project);
8
+ if (projectError)
9
+ return scopeErrorResult(projectError);
7
10
  const versions = historyStore.listVersions(project, validated.path);
8
11
  if (versions.length === 0) {
9
12
  return {