@imenam/mcp-documentor 1.0.66 → 1.0.68

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 (85) hide show
  1. package/README.md +34 -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/cli/setup-mcp.d.ts +5 -0
  5. package/dist/src/cli/setup-mcp.js +94 -0
  6. package/dist/src/core/log-dir.d.ts +6 -0
  7. package/dist/src/core/log-dir.js +12 -0
  8. package/dist/src/core/project-scope.d.ts +24 -0
  9. package/dist/src/core/project-scope.js +28 -0
  10. package/dist/src/core/storage-engine.d.ts +0 -4
  11. package/dist/src/core/storage-engine.js +4 -9
  12. package/dist/src/gui-worker.js +48 -8
  13. package/dist/src/index.js +135 -43
  14. package/dist/src/tools/check-freshness.d.ts +2 -2
  15. package/dist/src/tools/check-freshness.js +8 -7
  16. package/dist/src/tools/create-doc.js +4 -6
  17. package/dist/src/tools/delete-doc.js +14 -7
  18. package/dist/src/tools/get-doc-metadata.d.ts +2 -2
  19. package/dist/src/tools/get-doc-metadata.js +6 -5
  20. package/dist/src/tools/get-doc-version.js +5 -2
  21. package/dist/src/tools/get-doc.d.ts +2 -2
  22. package/dist/src/tools/get-doc.js +6 -5
  23. package/dist/src/tools/git-diff-doc.js +8 -4
  24. package/dist/src/tools/list-doc-versions.js +5 -2
  25. package/dist/src/tools/move-doc.js +5 -8
  26. package/dist/src/tools/patch-doc.js +6 -5
  27. package/dist/src/tools/refresh-file-snapshot.js +8 -4
  28. package/dist/src/tools/restore-doc-version.js +5 -2
  29. package/dist/src/tools/update-doc.d.ts +2 -2
  30. package/dist/src/tools/update-doc.js +6 -5
  31. package/dist/src/tools/upload-image.js +12 -3
  32. package/dist/src/version.d.ts +1 -1
  33. package/dist/src/version.js +1 -1
  34. package/gui/dist/assets/{arc-sHJlsIyi.js → arc-Carsq0k3.js} +1 -1
  35. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BiF2hxSq.js → architectureDiagram-3BPJPVTR-caqcwdTw.js} +1 -1
  36. package/gui/dist/assets/{blockDiagram-GPEHLZMM-BCnnBvty.js → blockDiagram-GPEHLZMM-0wUO8k2I.js} +1 -1
  37. package/gui/dist/assets/{c4Diagram-AAUBKEIU-Bu3KJ-ij.js → c4Diagram-AAUBKEIU-DS6q7fMy.js} +1 -1
  38. package/gui/dist/assets/channel-D51HexOc.js +1 -0
  39. package/gui/dist/assets/{chunk-2J33WTMH-C_ekvnEC.js → chunk-2J33WTMH-CrJc-G6D.js} +1 -1
  40. package/gui/dist/assets/{chunk-4BX2VUAB-Dmp27nYT.js → chunk-4BX2VUAB-C5vtPFbB.js} +1 -1
  41. package/gui/dist/assets/{chunk-55IACEB6-Jrgqw_BR.js → chunk-55IACEB6-BchyQUFM.js} +1 -1
  42. package/gui/dist/assets/{chunk-727SXJPM-BYwz54rU.js → chunk-727SXJPM-BCIaWtKq.js} +1 -1
  43. package/gui/dist/assets/{chunk-AQP2D5EJ-CuvuX4qY.js → chunk-AQP2D5EJ-BEQJDJt5.js} +1 -1
  44. package/gui/dist/assets/{chunk-FMBD7UC4-CNL_nFEL.js → chunk-FMBD7UC4-IfQ3QtrC.js} +1 -1
  45. package/gui/dist/assets/{chunk-ND2GUHAM-DECudjWh.js → chunk-ND2GUHAM-4Du33w7t.js} +1 -1
  46. package/gui/dist/assets/{chunk-QZHKN3VN-BAtGKt_6.js → chunk-QZHKN3VN-HR0CP6fa.js} +1 -1
  47. package/gui/dist/assets/classDiagram-4FO5ZUOK-CU0dQ3zy.js +1 -0
  48. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CU0dQ3zy.js +1 -0
  49. package/gui/dist/assets/{cose-bilkent-S5V4N54A-DQ2M_2Lo.js → cose-bilkent-S5V4N54A-C9Re3ENx.js} +1 -1
  50. package/gui/dist/assets/{dagre-BM42HDAG-D6vlHW8t.js → dagre-BM42HDAG-DZmcvMMp.js} +1 -1
  51. package/gui/dist/assets/{diagram-2AECGRRQ-s2G4QZQa.js → diagram-2AECGRRQ-EyJrAGht.js} +1 -1
  52. package/gui/dist/assets/{diagram-5GNKFQAL-YyU3AqQI.js → diagram-5GNKFQAL-CHd3T9_b.js} +1 -1
  53. package/gui/dist/assets/{diagram-KO2AKTUF-BuFjXIUa.js → diagram-KO2AKTUF-CIyFrZSR.js} +1 -1
  54. package/gui/dist/assets/{diagram-LMA3HP47-CjO5OS2W.js → diagram-LMA3HP47-BlkJALyz.js} +1 -1
  55. package/gui/dist/assets/{diagram-OG6HWLK6-DVYIxc4z.js → diagram-OG6HWLK6-P8LA7IZE.js} +1 -1
  56. package/gui/dist/assets/{erDiagram-TEJ5UH35-BsxhnVVZ.js → erDiagram-TEJ5UH35-BPywgWN1.js} +1 -1
  57. package/gui/dist/assets/{flowDiagram-I6XJVG4X-D2BV25QW.js → flowDiagram-I6XJVG4X-ByL0kGiV.js} +1 -1
  58. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-BR95C5C6.js → ganttDiagram-6RSMTGT7-_pnaYCU4.js} +1 -1
  59. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-bHC_XC7V.js → gitGraphDiagram-PVQCEYII-DN67CDGs.js} +1 -1
  60. package/gui/dist/assets/{index-DhwbMWam.js → index-Bjc5MOe1.js} +88 -88
  61. package/gui/dist/assets/{index-Cbo0HWer.css → index-hjkEDRmc.css} +1 -1
  62. package/gui/dist/assets/{infoDiagram-5YYISTIA-DE0rWygZ.js → infoDiagram-5YYISTIA-Bm5LEpzH.js} +1 -1
  63. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-C03LZBFa.js → ishikawaDiagram-YF4QCWOH-DUYSULkT.js} +1 -1
  64. package/gui/dist/assets/{journeyDiagram-JHISSGLW-tRPsZRYs.js → journeyDiagram-JHISSGLW-BvJ1_f0L.js} +1 -1
  65. package/gui/dist/assets/{kanban-definition-UN3LZRKU-BYePPZ0q.js → kanban-definition-UN3LZRKU-DDYKg6RY.js} +1 -1
  66. package/gui/dist/assets/{linear-Bgo5v91h.js → linear-HlpG9u6P.js} +1 -1
  67. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-C87iCyRq.js → mindmap-definition-RKZ34NQL-CPa_wo_s.js} +1 -1
  68. package/gui/dist/assets/{pieDiagram-4H26LBE5-BINdJQBG.js → pieDiagram-4H26LBE5-BWo0c4DP.js} +1 -1
  69. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BNcy-pgy.js → quadrantDiagram-W4KKPZXB-CLahU-6B.js} +1 -1
  70. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-D_DCIFsd.js → requirementDiagram-4Y6WPE33-BoYbgzQ7.js} +1 -1
  71. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-nCf_iEpj.js → sankeyDiagram-5OEKKPKP-CGm79aMo.js} +1 -1
  72. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-3rvwaBlu.js → sequenceDiagram-3UESZ5HK-BkknTD52.js} +1 -1
  73. package/gui/dist/assets/{stateDiagram-AJRCARHV-BRPOJHIL.js → stateDiagram-AJRCARHV-DSBurgwC.js} +1 -1
  74. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DfKn22XF.js +1 -0
  75. package/gui/dist/assets/{timeline-definition-PNZ67QCA-C75DjOcj.js → timeline-definition-PNZ67QCA-CBHyrGuz.js} +1 -1
  76. package/gui/dist/assets/{vennDiagram-CIIHVFJN-CLZG8u4c.js → vennDiagram-CIIHVFJN-Dn9fr_bR.js} +1 -1
  77. package/gui/dist/assets/{wardley-L42UT6IY-BJnZFkDe.js → wardley-L42UT6IY-nMuySeAb.js} +1 -1
  78. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-CBvWRjIO.js → wardleyDiagram-YWT4CUSO-Byn2STLI.js} +1 -1
  79. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-BGMFJ2t3.js → xychartDiagram-2RQKCTM6-Bi-Y9wp5.js} +1 -1
  80. package/gui/dist/index.html +2 -2
  81. package/package.json +1 -1
  82. package/gui/dist/assets/channel-fkOAmVLo.js +0 -1
  83. package/gui/dist/assets/classDiagram-4FO5ZUOK-D_v2HS-t.js +0 -1
  84. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-D_v2HS-t.js +0 -1
  85. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BUd1OQnG.js +0 -1
package/dist/src/index.js CHANGED
@@ -8,6 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { APP_VERSION } from "./version.js";
10
10
  import { GuiLauncher, createLogger, setupLogging } from "@imenam/mcp-gui-interface";
11
+ import { resolveLogDir } from "./core/log-dir.js";
11
12
  import { StorageEngine } from "./core/storage-engine.js";
12
13
  import { EmbeddingStore } from "./core/embedding-store.js";
13
14
  import { EmbeddingEngine } from "./core/embedding-engine.js";
@@ -45,6 +46,12 @@ import { syncStatus, pullDocs, pushDocs, syncDocs, resolveSyncConflict } from ".
45
46
  import { isReadOnly, WRITE_TOOLS } from "../shared/read-only.js";
46
47
  const __filename = fileURLToPath(import.meta.url);
47
48
  const __dirname = path.dirname(__filename);
49
+ // Les flags CLI sont traités avant toute initialisation (storage, sqlite, logs) :
50
+ // ils s'exécutent dans le répertoire de l'utilisateur, pas dans un serveur MCP.
51
+ if (process.argv.slice(2).includes("--claude-setup-mcp")) {
52
+ const { runSetupMcp } = await import("./cli/setup-mcp.js");
53
+ process.exit(runSetupMcp({ cwd: process.cwd(), force: process.argv.slice(2).includes("--force") }));
54
+ }
48
55
  function findProjectRoot(startDir) {
49
56
  let current = startDir;
50
57
  while (current !== path.dirname(current)) {
@@ -57,7 +64,7 @@ function findProjectRoot(startDir) {
57
64
  }
58
65
  const rootDir = findProjectRoot(__dirname);
59
66
  const packageEnvPath = path.join(rootDir, ".env");
60
- setupLogging({ processLabel: "mcp-documentor", logDir: path.join(rootDir, ".docs-data", "logs") });
67
+ setupLogging({ processLabel: "mcp-documentor", logDir: resolveLogDir(rootDir) });
61
68
  const logger = createLogger("mcp-documentor");
62
69
  function getConfig() {
63
70
  if (fs.existsSync(packageEnvPath)) {
@@ -154,6 +161,9 @@ function handleIPCMessage(msg) {
154
161
  else if (msg.type === "UPDATE_DOC") {
155
162
  handleUpdateDocIPC(msg);
156
163
  }
164
+ else if (msg.type === "DELETE_DOC") {
165
+ handleDeleteDocIPC(msg);
166
+ }
157
167
  else if (msg.type === "GET_FILE_SECTION") {
158
168
  handleGetFileSection(msg);
159
169
  }
@@ -161,6 +171,16 @@ function handleIPCMessage(msg) {
161
171
  logger.debug(`Unhandled message from GUI: ${msg.type}`);
162
172
  }
163
173
  }
174
+ /**
175
+ * GUI requests that target a specific doc must name their project: the GUI can be
176
+ * showing project A while an agent switches the server's active project to B.
177
+ */
178
+ function requireProjectFromIPC(msg) {
179
+ const project = msg.data?.project;
180
+ if (!project)
181
+ throw new Error("project is required for this operation");
182
+ return project;
183
+ }
164
184
  async function handleGetInitialState(msg) {
165
185
  try {
166
186
  const config = storageEngine.getConfig();
@@ -272,12 +292,13 @@ async function handleListDocs(msg) {
272
292
  }
273
293
  async function handleGetDoc(msg) {
274
294
  try {
275
- const resolved = await storageEngine.resolveDoc(msg.data.id ?? msg.data.path);
295
+ const project = requireProjectFromIPC(msg);
296
+ const doc = await storageEngine.getDoc(msg.data.id ?? msg.data.path, project);
276
297
  broadcastToGUI({
277
298
  type: "GET_DOC_RESPONSE",
278
299
  correlationId: msg.correlationId,
279
- data: resolved ? { doc: resolved.doc } : null,
280
- error: resolved ? undefined : "Doc not found",
300
+ data: doc ? { doc } : null,
301
+ error: doc ? undefined : `Doc not found in project "${project}"`,
281
302
  timestamp: new Date().toISOString(),
282
303
  });
283
304
  }
@@ -330,15 +351,16 @@ async function handleCreateDocIPC(msg) {
330
351
  return;
331
352
  }
332
353
  try {
333
- const { title, content, folder, tags, project } = msg.data ?? {};
354
+ const { title, content, folder, tags } = msg.data ?? {};
334
355
  if (!title || !content)
335
356
  throw new Error("title and content are required");
357
+ const project = requireProjectFromIPC(msg);
336
358
  const result = await createDoc(storageEngine, embeddingEngine, {
337
359
  title,
338
360
  content,
361
+ project,
339
362
  ...(folder && { folder }),
340
363
  ...(tags && { tags }),
341
- ...(project && { project }),
342
364
  });
343
365
  if (result.isError) {
344
366
  const text = result.content[0]?.type === "text" ? result.content[0].text : "Create failed";
@@ -396,6 +418,7 @@ async function handleUpdateDocIPC(msg) {
396
418
  try {
397
419
  const result = await updateDoc(storageEngine, embeddingEngine, {
398
420
  path: msg.data.id,
421
+ project: requireProjectFromIPC(msg),
399
422
  content: msg.data.content,
400
423
  ...(msg.data.title !== undefined && { title: msg.data.title }),
401
424
  });
@@ -426,16 +449,72 @@ async function handleUpdateDocIPC(msg) {
426
449
  });
427
450
  }
428
451
  }
452
+ async function handleDeleteDocIPC(msg) {
453
+ if (isReadOnly()) {
454
+ broadcastToGUI({
455
+ type: "DELETE_DOC_RESPONSE",
456
+ correlationId: msg.correlationId,
457
+ error: "READ_ONLY mode: deletion disabled",
458
+ timestamp: new Date().toISOString(),
459
+ });
460
+ return;
461
+ }
462
+ try {
463
+ const project = requireProjectFromIPC(msg);
464
+ const result = await deleteDoc(storageEngine, embeddingEngine, { path: msg.data.id, project });
465
+ if (result.isError) {
466
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "Delete failed";
467
+ broadcastToGUI({
468
+ type: "DELETE_DOC_RESPONSE",
469
+ correlationId: msg.correlationId,
470
+ error: text,
471
+ timestamp: new Date().toISOString(),
472
+ });
473
+ return;
474
+ }
475
+ const message = result.content[0]?.type === "text" ? result.content[0].text : undefined;
476
+ broadcastToGUI({
477
+ type: "DELETE_DOC_RESPONSE",
478
+ correlationId: msg.correlationId,
479
+ data: { success: true, message },
480
+ timestamp: new Date().toISOString(),
481
+ });
482
+ const config = storageEngine.getConfig();
483
+ const docs = await storageEngine.listDocs();
484
+ for (const doc of docs) {
485
+ const full = await storageEngine.getDoc(doc.id);
486
+ if (full && full.tracked_files.length > 0) {
487
+ const report = computeFreshness(full.tracked_files, config.currentProject);
488
+ doc.isStale = report.status === "stale" || report.status === "broken";
489
+ doc.isBroken = report.status === "broken";
490
+ }
491
+ }
492
+ broadcastToGUI({
493
+ type: "INITIAL_STATE",
494
+ data: { config, docs, projects: config.projects },
495
+ timestamp: new Date().toISOString(),
496
+ });
497
+ }
498
+ catch (err) {
499
+ broadcastToGUI({
500
+ type: "DELETE_DOC_RESPONSE",
501
+ correlationId: msg.correlationId,
502
+ error: err.message,
503
+ timestamp: new Date().toISOString(),
504
+ });
505
+ }
506
+ }
429
507
  async function handleGetFileSection(msg) {
430
508
  try {
431
509
  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);
510
+ const project = requireProjectFromIPC(msg);
511
+ const doc = await storageEngine.getDoc(docId, project);
512
+ if (!doc)
513
+ throw new Error(`Doc not found in project "${project}"`);
514
+ const trackedFile = doc.tracked_files.find((f) => f.path === filePath);
436
515
  if (!trackedFile || !trackedFile.sections?.length)
437
516
  throw new Error("No sections for this file");
438
- const absolutePath = toAbsolutePath(trackedFile.path, resolved.project);
517
+ const absolutePath = toAbsolutePath(trackedFile.path, project);
439
518
  if (!absolutePath)
440
519
  throw new Error("Cannot resolve file on this machine (no code root configured)");
441
520
  const fileLines = fs.readFileSync(absolutePath, "utf-8").split("\n");
@@ -468,18 +547,18 @@ async function handleGetFileSection(msg) {
468
547
  }
469
548
  async function handleCheckFreshness(msg) {
470
549
  try {
471
- const resolved = await storageEngine.resolveDoc(msg.data.id ?? msg.data.path);
472
- const doc = resolved?.doc;
550
+ const project = requireProjectFromIPC(msg);
551
+ const doc = await storageEngine.getDoc(msg.data.id ?? msg.data.path, project);
473
552
  if (!doc) {
474
553
  broadcastToGUI({
475
554
  type: "CHECK_FRESHNESS_RESPONSE",
476
555
  correlationId: msg.correlationId,
477
- error: "Doc not found",
556
+ error: `Doc not found in project "${project}"`,
478
557
  timestamp: new Date().toISOString(),
479
558
  });
480
559
  return;
481
560
  }
482
- const report = computeFreshness(doc.tracked_files, resolved.project);
561
+ const report = computeFreshness(doc.tracked_files, project);
483
562
  broadcastToGUI({
484
563
  type: "CHECK_FRESHNESS_RESPONSE",
485
564
  correlationId: msg.correlationId,
@@ -536,7 +615,10 @@ async function handleSearchDocs(msg) {
536
615
  }
537
616
  async function handleGitDiff(msg) {
538
617
  try {
539
- const result = await gitDiffDoc(storageEngine, { paths: [msg.data.id ?? msg.data.path] });
618
+ const result = await gitDiffDoc(storageEngine, {
619
+ paths: [msg.data.id ?? msg.data.path],
620
+ project: requireProjectFromIPC(msg),
621
+ });
540
622
  const text = result.content[0]?.text ?? "";
541
623
  broadcastToGUI({
542
624
  type: "GIT_DIFF_RESPONSE",
@@ -599,8 +681,7 @@ async function handleUpdateConfig(msg) {
599
681
  async function handleGetDocHistory(msg) {
600
682
  try {
601
683
  const id = msg.data?.id ?? msg.data?.path;
602
- const config = storageEngine.getConfig();
603
- const project = msg.data?.project ?? config.currentProject;
684
+ const project = requireProjectFromIPC(msg);
604
685
  const versions = historyStore.listVersions(project, id);
605
686
  broadcastToGUI({
606
687
  type: "GET_DOC_HISTORY_RESPONSE",
@@ -622,8 +703,7 @@ async function handleGetDocVersionIPC(msg) {
622
703
  try {
623
704
  const id = msg.data?.id ?? msg.data?.path;
624
705
  const version = msg.data?.version;
625
- const config = storageEngine.getConfig();
626
- const project = msg.data?.project ?? config.currentProject;
706
+ const project = requireProjectFromIPC(msg);
627
707
  const entry = historyStore.getVersion(project, id, version);
628
708
  broadcastToGUI({
629
709
  type: "GET_DOC_VERSION_RESPONSE",
@@ -711,11 +791,12 @@ async function reconnectGui(args) {
711
791
  };
712
792
  }
713
793
  return {
714
- content: [{ type: "text", text: "GUI worker started but did not confirm proxy registration within 15s. Check the logs (.docs-data/logs), then retry." }],
794
+ content: [{ type: "text", text: `GUI worker started but did not confirm proxy registration within 15s. Check the logs (${resolveLogDir(rootDir)}), then retry.` }],
715
795
  isError: true,
716
796
  };
717
797
  }
718
798
  server.setRequestHandler(ListToolsRequestSchema, async () => {
799
+ 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
800
  const tools = [
720
801
  {
721
802
  name: "create_doc",
@@ -767,20 +848,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
767
848
  items: { type: "string" },
768
849
  description: "IDs of related documentation entries",
769
850
  },
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." },
851
+ 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
852
  },
772
- required: ["title"],
853
+ required: ["title", "project"],
773
854
  },
774
855
  },
775
856
  {
776
857
  name: "get_doc",
777
- description: "Retrieve a documentation entry by its path, including full content, tracked files, and metadata.",
858
+ 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
859
  inputSchema: {
779
860
  type: "object",
780
861
  properties: {
781
862
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication' or 'authentication' for root docs" },
863
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
782
864
  },
783
- required: ["path"],
865
+ required: ["path", "project"],
784
866
  },
785
867
  },
786
868
  {
@@ -846,8 +928,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
846
928
  items: { type: "string" },
847
929
  description: "Paths of related documentation entries (replaces existing), e.g. ['backend/auth/authentication']",
848
930
  },
931
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
849
932
  },
850
- required: ["path"],
933
+ required: ["path", "project"],
851
934
  },
852
935
  },
853
936
  {
@@ -858,8 +941,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
858
941
  properties: {
859
942
  path: { type: "string", description: "Current path of the documentation entry, e.g. 'backend/authentication'" },
860
943
  new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both." },
944
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " The doc stays in this project — move_doc never moves an entry across projects." },
861
945
  },
862
- required: ["path", "new_path"],
946
+ required: ["path", "new_path", "project"],
863
947
  },
864
948
  },
865
949
  {
@@ -881,7 +965,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
881
965
  type: "object",
882
966
  properties: {
883
967
  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." },
968
+ 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
969
  },
886
970
  required: [],
887
971
  },
@@ -921,8 +1005,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
921
1005
  items: { type: "string" },
922
1006
  description: "Specific tracked file paths to refresh (all tracked files if omitted)",
923
1007
  },
1008
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " Every entry in paths must belong to it — call once per project otherwise." },
924
1009
  },
925
- required: ["paths"],
1010
+ required: ["paths", "project"],
926
1011
  },
927
1012
  },
928
1013
  {
@@ -932,8 +1017,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
932
1017
  type: "object",
933
1018
  properties: {
934
1019
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1020
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
935
1021
  },
936
- required: ["path"],
1022
+ required: ["path", "project"],
937
1023
  },
938
1024
  },
939
1025
  {
@@ -947,8 +1033,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
947
1033
  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
1034
  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
1035
  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)." },
1036
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
950
1037
  },
951
- required: ["path", "search"],
1038
+ required: ["path", "search", "project"],
952
1039
  },
953
1040
  },
954
1041
  {
@@ -967,8 +1054,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
967
1054
  items: { type: "string" },
968
1055
  description: "Specific tracked file paths to diff (all tracked files if omitted)",
969
1056
  },
1057
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " Every entry in paths must belong to it — call once per project otherwise." },
970
1058
  },
971
- required: ["paths"],
1059
+ required: ["paths", "project"],
972
1060
  },
973
1061
  },
974
1062
  {
@@ -984,13 +1072,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
984
1072
  },
985
1073
  {
986
1074
  name: "delete_doc",
987
- description: "Permanently delete a documentation entry. Works across all projects — no need to set the active project first.",
1075
+ 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
1076
  inputSchema: {
989
1077
  type: "object",
990
1078
  properties: {
991
1079
  path: { type: "string", description: "Path of the documentation entry to delete, e.g. 'backend/auth/authentication'" },
1080
+ project: {
1081
+ type: "string",
1082
+ description: "Project the documentation entry belongs to. Required — never guessed, and never defaulted to the current project.",
1083
+ },
992
1084
  },
993
- required: ["path"],
1085
+ required: ["path", "project"],
994
1086
  },
995
1087
  },
996
1088
  {
@@ -1010,9 +1102,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1010
1102
  type: "object",
1011
1103
  properties: {
1012
1104
  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)" },
1105
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1014
1106
  },
1015
- required: ["path"],
1107
+ required: ["path", "project"],
1016
1108
  },
1017
1109
  },
1018
1110
  {
@@ -1023,9 +1115,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1023
1115
  properties: {
1024
1116
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1025
1117
  version: { type: "number", description: "Version number to retrieve" },
1026
- project: { type: "string", description: "Project to look in (defaults to current project)" },
1118
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1027
1119
  },
1028
- required: ["path", "version"],
1120
+ required: ["path", "version", "project"],
1029
1121
  },
1030
1122
  },
1031
1123
  {
@@ -1036,9 +1128,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1036
1128
  properties: {
1037
1129
  path: { type: "string", description: "Path of the documentation entry, e.g. 'backend/auth/authentication'" },
1038
1130
  version: { type: "number", description: "Version number to restore" },
1039
- project: { type: "string", description: "Project to look in (defaults to current project)" },
1131
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
1040
1132
  },
1041
- required: ["path", "version"],
1133
+ required: ["path", "version", "project"],
1042
1134
  },
1043
1135
  },
1044
1136
  {
@@ -1064,7 +1156,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1064
1156
  docs: {
1065
1157
  type: "array",
1066
1158
  items: { type: "string" },
1067
- description: "Doc references to include: 'project:doc-id' for cross-project, or 'doc-id' for current project",
1159
+ 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
1160
  },
1069
1161
  description: { type: "string", description: "Short description of the bundle (optional)" },
1070
1162
  },
@@ -1090,10 +1182,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1090
1182
  properties: {
1091
1183
  filename: { type: "string", description: "Filename with extension, e.g. 'screenshot.png'" },
1092
1184
  filePath: { type: "string", description: "Absolute path to the image file on the server" },
1093
- project: { type: "string", description: "Target project (defaults to current project)" },
1185
+ project: { type: "string", description: "Target project whose assets receive the image. REQUIRED — never defaulted to the current project." },
1094
1186
  alt: { type: "string", description: "Alt text for the markdown image link (optional)" },
1095
1187
  },
1096
- required: ["filename", "filePath"],
1188
+ required: ["filename", "filePath", "project"],
1097
1189
  },
1098
1190
  },
1099
1191
  {
@@ -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) {