@imenam/mcp-documentor 1.0.66 → 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 +46 -7
  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/README.md CHANGED
@@ -60,6 +60,7 @@ Add to your MCP client config (e.g. Cursor `mcp.json`):
60
60
  | `OPENAI_API_KEY` | No | Enables semantic search via OpenAI embeddings (`search_docs`) |
61
61
  | `MCP_DOCUMENTOR_OPENAI_MODEL` | No | Embedding model (default: `text-embedding-3-small`) |
62
62
  | `PROXY_URL` | No | URL of the proxy server to register and launch the GUI |
63
+ | `APP_GROUP` | No | Groups the app under a collapsible section (folded by default) in the proxy dashboard. Apps sharing the same value appear together; if unset, the app lands in the default "ungrouped" section |
63
64
  | `MCP_DATA_DIR` | No | Overrides the default data directory (see Data storage) |
64
65
  | `MCP_DOCUMENTOR_CODE_ROOT` | No | Base directory that tracked-file paths are stored relative to, keeping docs portable across machines (see [Portable tracked-file paths](#portable-tracked-file-paths)) |
65
66
  | `MCP_DOCUMENTOR_CODE_ROOTS` | No | JSON map of per-project code-root overrides, e.g. `{"my-proj":"/srv/my-proj"}` |
@@ -73,25 +74,31 @@ Optional `.env` at the package root (directory containing `package.json` discove
73
74
 
74
75
  ## Available Tools
75
76
 
77
+ Every tool that targets a specific documentation entry takes a **mandatory `project`**. It is never
78
+ guessed and never falls back to the active project: resolving a doc path across projects is what
79
+ made operations silently hit the wrong project when the same path existed in several. Tools that
80
+ work on a whole project (`list_docs`, `get_doc_tree`, `check_links`, `check_freshness` without a
81
+ `path`, `reindex_embeddings`) keep `project` optional, since omitting it means "all projects".
82
+
76
83
  | Tool | Description |
77
84
  |---|---|
78
- | `create_doc` | Create a new documentation entry linked to source files |
79
- | `get_doc` | Retrieve a documentation entry with full content |
80
- | `get_doc_metadata` | Get metadata and freshness status without loading content |
85
+ | `create_doc` | Create a new documentation entry linked to source files (`project` required) |
86
+ | `get_doc` | Retrieve a documentation entry with full content (`project` required) |
87
+ | `get_doc_metadata` | Get metadata and freshness status without loading content (`project` required) |
81
88
  | `list_docs` | List docs with staleness indicators (optionally filter by project) |
82
89
  | `get_doc_tree` | Return folder/doc structure for a project (or all projects) |
83
- | `update_doc` | Fully replace content and/or metadata (prefer `patch_doc` for small edits) |
84
- | `patch_doc` | Search-and-replace a fragment of a doc’s content |
85
- | `move_doc` | Change a doc’s path (folder/name); use after renames to fix links |
86
- | `delete_doc` | Permanently delete a documentation entry |
90
+ | `update_doc` | Fully replace content and/or metadata (`project` required; prefer `patch_doc` for small edits) |
91
+ | `patch_doc` | Search-and-replace a fragment of a doc’s content (`project` required) |
92
+ | `move_doc` | Change a doc’s path (folder/name) within its project (`project` required) |
93
+ | `delete_doc` | Permanently delete a documentation entry (`project` required) |
87
94
  | `search_docs` | Semantic search over docs (natural language; requires OpenAI) |
88
- | `check_freshness` | Staleness for one doc or a full project scan |
89
- | `refresh_file_snapshot` | Mark tracked files as up-to-date without editing the doc |
90
- | `git_diff_doc` | Git diff for tracked files vs snapshot baseline |
95
+ | `check_freshness` | Staleness for one doc (`project` required) or a full project scan |
96
+ | `refresh_file_snapshot` | Mark tracked files as up-to-date without editing the doc (`project` required) |
97
+ | `git_diff_doc` | Git diff for tracked files vs snapshot baseline (`project` required) |
91
98
  | `check_links` | Validate `related_docs`, `doc://` links, and `#heading` anchors |
92
99
  | `list_projects` | List all documentation projects |
93
100
  | `set_project` | Switch the active project **for the GUI only** (not for MCP tools) |
94
- | `upload_image` | Upload an image from a local file path to a project's assets; returns the markdown link to insert |
101
+ | `upload_image` | Upload an image from a local file path to a project's assets (`project` required); returns the markdown link to insert |
95
102
  | `sync_status` | Show branch, ahead/behind counts, conflicted files, and uncommitted changes |
96
103
  | `pull_docs` | Pull updates from the remote (defensive fast-forward; reindexes new docs) |
97
104
  | `push_docs` | Commit local doc changes and push them to the remote |
@@ -94,6 +94,7 @@ export declare const FreshnessReportSchema: z.ZodObject<{
94
94
  }, z.core.$strip>;
95
95
  export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
96
96
  export declare const CreateDocArgsSchema: z.ZodObject<{
97
+ project: z.ZodString;
97
98
  title: z.ZodString;
98
99
  content: z.ZodOptional<z.ZodString>;
99
100
  content_file: z.ZodOptional<z.ZodString>;
@@ -110,6 +111,7 @@ export declare const CreateDocArgsSchema: z.ZodObject<{
110
111
  }, z.core.$strip>;
111
112
  export declare const UpdateDocArgsSchema: z.ZodObject<{
112
113
  path: z.ZodString;
114
+ project: z.ZodString;
113
115
  title: z.ZodOptional<z.ZodString>;
114
116
  content: z.ZodOptional<z.ZodString>;
115
117
  content_file: z.ZodOptional<z.ZodString>;
@@ -125,10 +127,16 @@ export declare const UpdateDocArgsSchema: z.ZodObject<{
125
127
  }, z.core.$strip>;
126
128
  export declare const GetDocArgsSchema: z.ZodObject<{
127
129
  path: z.ZodString;
130
+ project: z.ZodString;
131
+ }, z.core.$strip>;
132
+ export declare const DeleteDocArgsSchema: z.ZodObject<{
133
+ path: z.ZodString;
134
+ project: z.ZodString;
128
135
  }, z.core.$strip>;
129
136
  export declare const MoveDocArgsSchema: z.ZodObject<{
130
137
  path: z.ZodString;
131
138
  new_path: z.ZodString;
139
+ project: z.ZodString;
132
140
  }, z.core.$strip>;
133
141
  export declare const CheckFreshnessArgsSchema: z.ZodObject<{
134
142
  path: z.ZodOptional<z.ZodString>;
@@ -140,13 +148,16 @@ export declare const SearchDocsArgsSchema: z.ZodObject<{
140
148
  }, z.core.$strip>;
141
149
  export declare const RefreshFileSnapshotArgsSchema: z.ZodObject<{
142
150
  paths: z.ZodArray<z.ZodString>;
151
+ project: z.ZodString;
143
152
  file_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
144
153
  }, z.core.$strip>;
145
154
  export declare const GetDocMetadataArgsSchema: z.ZodObject<{
146
155
  path: z.ZodString;
156
+ project: z.ZodString;
147
157
  }, z.core.$strip>;
148
158
  export declare const PatchDocArgsSchema: z.ZodObject<{
149
159
  path: z.ZodString;
160
+ project: z.ZodString;
150
161
  search: z.ZodOptional<z.ZodString>;
151
162
  search_file: z.ZodOptional<z.ZodString>;
152
163
  replacement: z.ZodOptional<z.ZodString>;
@@ -157,6 +168,7 @@ export declare const CheckLinksArgsSchema: z.ZodObject<{
157
168
  }, z.core.$strip>;
158
169
  export declare const GitDiffDocArgsSchema: z.ZodObject<{
159
170
  paths: z.ZodArray<z.ZodString>;
171
+ project: z.ZodString;
160
172
  file_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
161
173
  }, z.core.$strip>;
162
174
  export declare const SetProjectArgsSchema: z.ZodObject<{
@@ -164,17 +176,17 @@ export declare const SetProjectArgsSchema: z.ZodObject<{
164
176
  }, z.core.$strip>;
165
177
  export declare const ListDocVersionsArgsSchema: z.ZodObject<{
166
178
  path: z.ZodString;
167
- project: z.ZodOptional<z.ZodString>;
179
+ project: z.ZodString;
168
180
  }, z.core.$strip>;
169
181
  export declare const GetDocVersionArgsSchema: z.ZodObject<{
170
182
  path: z.ZodString;
171
183
  version: z.ZodNumber;
172
- project: z.ZodOptional<z.ZodString>;
184
+ project: z.ZodString;
173
185
  }, z.core.$strip>;
174
186
  export declare const RestoreDocVersionArgsSchema: z.ZodObject<{
175
187
  path: z.ZodString;
176
188
  version: z.ZodNumber;
177
- project: z.ZodOptional<z.ZodString>;
189
+ project: z.ZodString;
178
190
  }, z.core.$strip>;
179
191
  export declare const ExportBundleSchema: z.ZodObject<{
180
192
  id: z.ZodString;
@@ -45,7 +45,17 @@ export const FreshnessReportSchema = z.object({
45
45
  broken: z.array(z.string()).default([]),
46
46
  total: z.number(),
47
47
  });
48
+ /**
49
+ * Every tool that targets a specific documentation entry takes this. It is mandatory
50
+ * and never defaulted: resolving a doc path across projects is what let operations
51
+ * silently hit the wrong project.
52
+ */
53
+ const requiredProject = (action) => z
54
+ .string()
55
+ .min(1)
56
+ .describe(`Project the documentation entry belongs to. Mandatory — ${action} is never resolved across projects, and never falls back to the current project.`);
48
57
  export const CreateDocArgsSchema = z.object({
58
+ project: requiredProject("creation").describe("Project to create the documentation entry in. Mandatory — never defaulted to the current project. The project is created automatically if it does not exist yet."),
49
59
  title: z.string().describe("Title of the documentation entry"),
50
60
  content: z.string().optional().describe("Documentation content in markdown"),
51
61
  content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
@@ -58,6 +68,7 @@ export const CreateDocArgsSchema = z.object({
58
68
  });
59
69
  export const UpdateDocArgsSchema = z.object({
60
70
  path: z.string().describe("Path of the documentation entry to update, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
71
+ project: requiredProject("the update"),
61
72
  title: z.string().optional().describe("Title of the documentation entry"),
62
73
  content: z.string().optional().describe("New content (replaces existing)"),
63
74
  content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
@@ -67,14 +78,25 @@ export const UpdateDocArgsSchema = z.object({
67
78
  });
68
79
  export const GetDocArgsSchema = z.object({
69
80
  path: z.string().describe("Path of the documentation entry, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
81
+ project: requiredProject("the lookup"),
82
+ });
83
+ export const DeleteDocArgsSchema = z.object({
84
+ path: z.string().describe("Path of the documentation entry to delete, e.g. 'backend/auth/authentication'"),
85
+ project: requiredProject("deletion"),
70
86
  });
71
87
  export const MoveDocArgsSchema = z.object({
72
88
  path: z.string().describe("Current path of the documentation entry, e.g. 'backend/auth/authentication'"),
73
89
  new_path: z.string().describe("New path for the documentation entry, e.g. 'backend/v2/authentication'. Changes folder and/or name."),
90
+ project: requiredProject("the move"),
74
91
  });
75
- export const CheckFreshnessArgsSchema = z.object({
92
+ export const CheckFreshnessArgsSchema = z
93
+ .object({
76
94
  path: z.string().optional().describe("Path of a single documentation entry to check, e.g. 'backend/auth/authentication'. If omitted, checks all docs in the project."),
77
- project: z.string().optional().describe("Project to scan (defaults to current project). Only used when path is omitted."),
95
+ project: z.string().min(1).optional().describe("Project to work in. Mandatory when path is provided; when path is omitted it selects the project to scan (all projects if omitted too)."),
96
+ })
97
+ .refine((d) => !d.path || !!d.project, {
98
+ message: "project is required when path is provided — freshness of a single entry is never resolved across projects",
99
+ path: ["project"],
78
100
  });
79
101
  export const SearchDocsArgsSchema = z.object({
80
102
  query: z.string().describe("Search query in natural language"),
@@ -82,13 +104,16 @@ export const SearchDocsArgsSchema = z.object({
82
104
  });
83
105
  export const RefreshFileSnapshotArgsSchema = z.object({
84
106
  paths: z.array(z.string()).min(1).describe("Paths of documentation entries to refresh, e.g. ['backend/auth/authentication', 'setup']"),
107
+ project: requiredProject("the refresh").describe("Project holding every entry listed in paths. Mandatory — snapshots are never refreshed across projects. Call once per project if the entries are spread across several."),
85
108
  file_paths: z.array(z.string()).optional().describe("Specific tracked files to refresh, as absolute paths (they are matched against the stored paths, relative or absolute). Refreshes all tracked files if omitted."),
86
109
  });
87
110
  export const GetDocMetadataArgsSchema = z.object({
88
111
  path: z.string().describe("Path of the documentation entry, e.g. 'backend/auth/authentication'"),
112
+ project: requiredProject("the lookup"),
89
113
  });
90
114
  export const PatchDocArgsSchema = z.object({
91
115
  path: z.string().describe("Path of the documentation entry to patch, e.g. 'backend/auth/authentication'"),
116
+ project: requiredProject("the patch"),
92
117
  search: z.string().optional().describe("Exact text to find in the current content"),
93
118
  search_file: z.string().optional().describe("Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft."),
94
119
  replacement: z.string().optional().describe("Text to replace it with"),
@@ -103,6 +128,7 @@ export const CheckLinksArgsSchema = z.object({
103
128
  });
104
129
  export const GitDiffDocArgsSchema = z.object({
105
130
  paths: z.array(z.string()).min(1).describe("Paths of documentation entries to diff, e.g. ['backend/auth/authentication']"),
131
+ project: requiredProject("the diff").describe("Project holding every entry listed in paths. Mandatory — entries are never resolved across projects. Call once per project if they are spread across several."),
106
132
  file_paths: z.array(z.string()).optional().describe("Specific tracked files to diff, as absolute paths (matched against the stored paths, relative or absolute). All tracked files if omitted."),
107
133
  });
108
134
  export const SetProjectArgsSchema = z.object({
@@ -110,17 +136,17 @@ export const SetProjectArgsSchema = z.object({
110
136
  });
111
137
  export const ListDocVersionsArgsSchema = z.object({
112
138
  path: z.string(),
113
- project: z.string().optional(),
139
+ project: requiredProject("history lookup"),
114
140
  });
115
141
  export const GetDocVersionArgsSchema = z.object({
116
142
  path: z.string(),
117
143
  version: z.number().int().positive(),
118
- project: z.string().optional(),
144
+ project: requiredProject("history lookup"),
119
145
  });
120
146
  export const RestoreDocVersionArgsSchema = z.object({
121
147
  path: z.string(),
122
148
  version: z.number().int().positive(),
123
- project: z.string().optional(),
149
+ project: requiredProject("the restore"),
124
150
  });
125
151
  export const ExportBundleSchema = z.object({
126
152
  id: z.string(),
@@ -134,7 +160,10 @@ export const ExportBundleSchema = z.object({
134
160
  export const PrepareExportBundleArgsSchema = z.object({
135
161
  name: z.string().describe("Bundle name (becomes the identifier via slugify)"),
136
162
  zip_filename: z.string().describe("ZIP filename to download, e.g. 'my-bundle.zip'"),
137
- docs: z.array(z.string()).min(1).describe("Doc references: 'project:doc-id' or 'doc-id' for current project"),
163
+ docs: z
164
+ .array(z.string().regex(/^[^:]+:.+$/, "each doc reference must be fully qualified as 'project:doc-id'"))
165
+ .min(1)
166
+ .describe("Doc references, each fully qualified as 'project:doc-id'. The project prefix is mandatory — a bare 'doc-id' is rejected so a bundle can never pick up the wrong project's entry."),
138
167
  description: z.string().optional().describe("Short description of the bundle"),
139
168
  });
140
169
  export const DeleteExportBundleArgsSchema = z.object({
@@ -0,0 +1,24 @@
1
+ import type { StorageEngine } from "./storage-engine.js";
2
+ import type { DocEntry } from "../../shared/schemas/doc.schema.js";
3
+ /**
4
+ * Doc-targeted tools resolve their entry inside one explicitly named project.
5
+ * Cross-project resolution (storageEngine.resolveDoc) is deliberately not used
6
+ * here: it picked the first project holding the path, which silently sent reads
7
+ * and writes to the wrong project when the same path exists in several.
8
+ */
9
+ export type ScopeError = {
10
+ errorText: string;
11
+ };
12
+ export declare function isScopeError(value: unknown): value is ScopeError;
13
+ /** Wraps a scope error into the MCP tool result shape. */
14
+ export declare function scopeErrorResult(error: ScopeError): {
15
+ isError: true;
16
+ content: {
17
+ type: string;
18
+ text: string;
19
+ }[];
20
+ };
21
+ export declare function assertProject(storageEngine: StorageEngine, project: string): ScopeError | null;
22
+ export declare function resolveDocInProject(storageEngine: StorageEngine, docPath: string, project: string, action?: string): Promise<{
23
+ doc: DocEntry;
24
+ } | ScopeError>;
@@ -0,0 +1,28 @@
1
+ export function isScopeError(value) {
2
+ return typeof value === "object" && value !== null && "errorText" in value;
3
+ }
4
+ /** Wraps a scope error into the MCP tool result shape. */
5
+ export function scopeErrorResult(error) {
6
+ return { isError: true, content: [{ type: "text", text: error.errorText }] };
7
+ }
8
+ export function assertProject(storageEngine, project) {
9
+ const knownProjects = storageEngine.getConfig().projects;
10
+ if (!knownProjects.includes(project)) {
11
+ return {
12
+ errorText: `Project "${project}" does not exist. Known projects: ${knownProjects.join(", ")}.`,
13
+ };
14
+ }
15
+ return null;
16
+ }
17
+ export async function resolveDocInProject(storageEngine, docPath, project, action = "operation") {
18
+ const projectError = assertProject(storageEngine, project);
19
+ if (projectError)
20
+ return projectError;
21
+ const doc = await storageEngine.getDoc(docPath, project);
22
+ if (!doc) {
23
+ return {
24
+ errorText: `Documentation entry "${docPath}" not found in project "${project}" — no ${action} performed. Check the project with list_projects or get_doc_tree before retrying.`,
25
+ };
26
+ }
27
+ return { doc };
28
+ }
@@ -44,10 +44,6 @@ export declare class StorageEngine {
44
44
  saveDoc(doc: DocEntry, project?: string, changeType?: "create" | "update" | "patch"): Promise<void>;
45
45
  getDoc(id: string, project?: string): Promise<DocEntry | null>;
46
46
  listDocs(project?: string): Promise<DocSummary[]>;
47
- resolveDoc(id: string): Promise<{
48
- doc: DocEntry;
49
- project: string;
50
- } | null>;
51
47
  deleteDoc(id: string, project?: string): Promise<boolean>;
52
48
  moveDoc(oldId: string, newId: string, project?: string): Promise<boolean>;
53
49
  getBundles(): ExportBundle[];
@@ -215,15 +215,10 @@ export class StorageEngine {
215
215
  }
216
216
  return summaries;
217
217
  }
218
- async resolveDoc(id) {
219
- const config = this.getConfig();
220
- for (const project of config.projects) {
221
- const doc = await this.getDoc(id, project);
222
- if (doc)
223
- return { doc, project };
224
- }
225
- return null;
226
- }
218
+ // resolveDoc() (first project whose tree contained the path) was removed on purpose:
219
+ // it silently sent reads and writes to the wrong project when the same doc path
220
+ // existed in several. Doc-targeted callers must name their project and go through
221
+ // getDoc(id, project) — see core/project-scope.ts.
227
222
  async deleteDoc(id, project) {
228
223
  const config = this.getConfig();
229
224
  const proj = project ?? config.currentProject;
@@ -347,6 +347,9 @@ app.post("/api/doc", async (c) => {
347
347
  if (!body.title || !body.content) {
348
348
  return c.json({ error: "title and content are required" }, 400);
349
349
  }
350
+ if (!body.project) {
351
+ return c.json({ error: "project is required" }, 400);
352
+ }
350
353
  const result = await ipcRequest("CREATE_DOC", {
351
354
  title: body.title,
352
355
  content: body.content,
@@ -364,7 +367,10 @@ app.post("/api/doc", async (c) => {
364
367
  });
365
368
  app.get("/api/doc/*", async (c) => {
366
369
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
367
- const result = await ipcRequest("GET_DOC", { id });
370
+ const project = c.req.query("project");
371
+ if (!project)
372
+ return c.json({ error: "project query param is required" }, 400);
373
+ const result = await ipcRequest("GET_DOC", { id, project });
368
374
  if (result.error)
369
375
  return c.json({ error: result.error }, 400);
370
376
  return c.json(result.data);
@@ -372,11 +378,29 @@ app.get("/api/doc/*", async (c) => {
372
378
  app.put("/api/doc/*", async (c) => {
373
379
  try {
374
380
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
381
+ const project = c.req.query("project");
382
+ if (!project)
383
+ return c.json({ error: "project query param is required" }, 400);
375
384
  const body = await c.req.json();
376
385
  if (body.content === undefined) {
377
386
  return c.json({ error: "content is required" }, 400);
378
387
  }
379
- const result = await ipcRequest("UPDATE_DOC", { id, content: body.content, title: body.title }, 15000);
388
+ const result = await ipcRequest("UPDATE_DOC", { id, project, content: body.content, title: body.title }, 15000);
389
+ if (result.error)
390
+ return c.json({ error: result.error }, 500);
391
+ return c.json(result.data ?? { success: true });
392
+ }
393
+ catch (err) {
394
+ return c.json({ error: err.message }, 500);
395
+ }
396
+ });
397
+ app.delete("/api/doc/*", async (c) => {
398
+ try {
399
+ const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
400
+ const project = c.req.query("project");
401
+ if (!project)
402
+ return c.json({ error: "project query param is required" }, 400);
403
+ const result = await ipcRequest("DELETE_DOC", { id, project }, 15000);
380
404
  if (result.error)
381
405
  return c.json({ error: result.error }, 500);
382
406
  return c.json(result.data ?? { success: true });
@@ -387,21 +411,30 @@ app.put("/api/doc/*", async (c) => {
387
411
  });
388
412
  app.get("/api/freshness/*", async (c) => {
389
413
  const id = decodeURIComponent(c.req.path.slice("/api/freshness/".length));
390
- const result = await ipcRequest("CHECK_FRESHNESS", { id });
414
+ const project = c.req.query("project");
415
+ if (!project)
416
+ return c.json({ error: "project query param is required" }, 400);
417
+ const result = await ipcRequest("CHECK_FRESHNESS", { id, project });
391
418
  if (result.error)
392
419
  return c.json({ error: result.error }, 400);
393
420
  return c.json(result.data);
394
421
  });
395
422
  app.get("/api/git-diff/*", async (c) => {
396
423
  const id = decodeURIComponent(c.req.path.slice("/api/git-diff/".length));
397
- const result = await ipcRequest("GIT_DIFF", { id }, 15000);
424
+ const project = c.req.query("project");
425
+ if (!project)
426
+ return c.json({ error: "project query param is required" }, 400);
427
+ const result = await ipcRequest("GIT_DIFF", { id, project }, 15000);
398
428
  if (result.error)
399
429
  return c.json({ error: result.error }, 400);
400
430
  return c.json(result.data);
401
431
  });
402
432
  app.get("/api/history/*", async (c) => {
403
433
  const id = decodeURIComponent(c.req.path.slice("/api/history/".length));
404
- const result = await ipcRequest("GET_DOC_HISTORY", { id });
434
+ const project = c.req.query("project");
435
+ if (!project)
436
+ return c.json({ error: "project query param is required" }, 400);
437
+ const result = await ipcRequest("GET_DOC_HISTORY", { id, project });
405
438
  if (result.error)
406
439
  return c.json({ error: result.error }, 400);
407
440
  return c.json(result.data);
@@ -411,7 +444,10 @@ app.get("/api/history-version/*", async (c) => {
411
444
  const lastSlash = rest.lastIndexOf("/");
412
445
  const id = rest.slice(0, lastSlash);
413
446
  const version = parseInt(rest.slice(lastSlash + 1), 10);
414
- const result = await ipcRequest("GET_DOC_VERSION", { id, version });
447
+ const project = c.req.query("project");
448
+ if (!project)
449
+ return c.json({ error: "project query param is required" }, 400);
450
+ const result = await ipcRequest("GET_DOC_VERSION", { id, version, project });
415
451
  if (result.error)
416
452
  return c.json({ error: result.error }, 400);
417
453
  return c.json(result.data);
@@ -429,8 +465,11 @@ app.get("/api/file-section/*", async (c) => {
429
465
  const docId = c.req.query("docId");
430
466
  if (!docId)
431
467
  return c.json({ error: "docId query param required" }, 400);
468
+ const project = c.req.query("project");
469
+ if (!project)
470
+ return c.json({ error: "project query param is required" }, 400);
432
471
  const filePath = decodeURIComponent(c.req.path.slice("/api/file-section/".length));
433
- const result = await ipcRequest("GET_FILE_SECTION", { docId, filePath }, 8000);
472
+ const result = await ipcRequest("GET_FILE_SECTION", { docId, project, filePath }, 8000);
434
473
  if (result.error)
435
474
  return c.json({ error: result.error }, 400);
436
475
  return c.json(result.data);