@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/README.md CHANGED
@@ -37,6 +37,21 @@ Global install exposes:
37
37
 
38
38
  ## MCP Configuration
39
39
 
40
+ ### Quick setup
41
+
42
+ From the repo you want to document, generate a ready-to-fill `.mcp.json` in the current directory:
43
+
44
+ ```bash
45
+ npx @imenam/mcp-documentor --claude-setup-mcp
46
+ ```
47
+
48
+ It creates the `mcp-documentor` entry with the usual environment variables as empty placeholders —
49
+ an empty variable is simply inactive, so the generated config runs as-is and you only fill in what
50
+ you need. An existing `.mcp.json` is merged, other servers are preserved, and an existing
51
+ `mcp-documentor` entry is only replaced when `--force` is passed.
52
+
53
+ ### Manual setup
54
+
40
55
  Add to your MCP client config (e.g. Cursor `mcp.json`):
41
56
 
42
57
  ```json
@@ -60,7 +75,9 @@ Add to your MCP client config (e.g. Cursor `mcp.json`):
60
75
  | `OPENAI_API_KEY` | No | Enables semantic search via OpenAI embeddings (`search_docs`) |
61
76
  | `MCP_DOCUMENTOR_OPENAI_MODEL` | No | Embedding model (default: `text-embedding-3-small`) |
62
77
  | `PROXY_URL` | No | URL of the proxy server to register and launch the GUI |
78
+ | `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
79
  | `MCP_DATA_DIR` | No | Overrides the default data directory (see Data storage) |
80
+ | `MCP_LOG_DIR` | No | Overrides the log directory of both the server and the GUI worker (default: `<package>/.docs-data/logs`). Must be set in the MCP client config — the package `.env` is loaded after the logger starts |
64
81
  | `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
82
  | `MCP_DOCUMENTOR_CODE_ROOTS` | No | JSON map of per-project code-root overrides, e.g. `{"my-proj":"/srv/my-proj"}` |
66
83
  | `READ_ONLY` | No | When set to `1`/`true`/`yes`, disables all create/update/delete tools and GUI write actions (read-only mode) |
@@ -73,25 +90,31 @@ Optional `.env` at the package root (directory containing `package.json` discove
73
90
 
74
91
  ## Available Tools
75
92
 
93
+ Every tool that targets a specific documentation entry takes a **mandatory `project`**. It is never
94
+ guessed and never falls back to the active project: resolving a doc path across projects is what
95
+ made operations silently hit the wrong project when the same path existed in several. Tools that
96
+ work on a whole project (`list_docs`, `get_doc_tree`, `check_links`, `check_freshness` without a
97
+ `path`, `reindex_embeddings`) keep `project` optional, since omitting it means "all projects".
98
+
76
99
  | Tool | Description |
77
100
  |---|---|
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 |
101
+ | `create_doc` | Create a new documentation entry linked to source files (`project` required) |
102
+ | `get_doc` | Retrieve a documentation entry with full content (`project` required) |
103
+ | `get_doc_metadata` | Get metadata and freshness status without loading content (`project` required) |
81
104
  | `list_docs` | List docs with staleness indicators (optionally filter by project) |
82
105
  | `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 |
106
+ | `update_doc` | Fully replace content and/or metadata (`project` required; prefer `patch_doc` for small edits) |
107
+ | `patch_doc` | Search-and-replace a fragment of a doc’s content (`project` required) |
108
+ | `move_doc` | Change a doc’s path (folder/name) within its project (`project` required) |
109
+ | `delete_doc` | Permanently delete a documentation entry (`project` required) |
87
110
  | `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 |
111
+ | `check_freshness` | Staleness for one doc (`project` required) or a full project scan |
112
+ | `refresh_file_snapshot` | Mark tracked files as up-to-date without editing the doc (`project` required) |
113
+ | `git_diff_doc` | Git diff for tracked files vs snapshot baseline (`project` required) |
91
114
  | `check_links` | Validate `related_docs`, `doc://` links, and `#heading` anchors |
92
115
  | `list_projects` | List all documentation projects |
93
116
  | `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 |
117
+ | `upload_image` | Upload an image from a local file path to a project's assets (`project` required); returns the markdown link to insert |
95
118
  | `sync_status` | Show branch, ahead/behind counts, conflicted files, and uncommitted changes |
96
119
  | `pull_docs` | Pull updates from the remote (defensive fast-forward; reindexes new docs) |
97
120
  | `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,5 @@
1
+ export interface SetupMcpOptions {
2
+ cwd: string;
3
+ force: boolean;
4
+ }
5
+ export declare function runSetupMcp({ cwd, force }: SetupMcpOptions): number;
@@ -0,0 +1,94 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const SERVER_NAME = "mcp-documentor";
4
+ const PACKAGE_NAME = "@imenam/mcp-documentor";
5
+ /**
6
+ * Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
7
+ * pas remplie (PROXY_URL vide = GUI désactivée, clé vide = pas d'embeddings),
8
+ * donc le fichier généré est utilisable tel quel, jamais à moitié cassé.
9
+ */
10
+ function buildServerEntry() {
11
+ return {
12
+ command: "npx",
13
+ args: ["-y", PACKAGE_NAME],
14
+ env: {
15
+ PROXY_URL: "",
16
+ APP_GROUP: "",
17
+ APP_PATH: "",
18
+ MCP_DATA_DIR: "",
19
+ MCP_LOG_DIR: "",
20
+ MCP_DOCUMENTOR_CODE_ROOT: "",
21
+ MCP_DOCUMENTOR_OPENAI_API_KEY: "",
22
+ MCP_DOCUMENTOR_OPENAI_MODEL: "text-embedding-3-small",
23
+ MCP_DOCUMENTOR_SYNC_REMOTE: "",
24
+ MCP_DOCUMENTOR_SYNC_BRANCH: "",
25
+ MCP_DOCUMENTOR_SYNC_TOKEN: "",
26
+ },
27
+ };
28
+ }
29
+ const GENERATED_VARS = [
30
+ ["PROXY_URL", "URL du proxy pour exposer la GUI"],
31
+ ["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
32
+ ["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
33
+ ["MCP_DATA_DIR", "répertoire de données (défaut : celui du package)"],
34
+ ["MCP_LOG_DIR", "répertoire des logs"],
35
+ ["MCP_DOCUMENTOR_CODE_ROOT", "racine du code pour des chemins trackés portables"],
36
+ ["MCP_DOCUMENTOR_OPENAI_API_KEY", "clé OpenAI (active search_docs)"],
37
+ ["MCP_DOCUMENTOR_OPENAI_MODEL", "modèle d'embedding"],
38
+ ["MCP_DOCUMENTOR_SYNC_REMOTE", "URL HTTPS du repo privé de sync GitHub"],
39
+ ["MCP_DOCUMENTOR_SYNC_BRANCH", "branche de sync (défaut : main)"],
40
+ ["MCP_DOCUMENTOR_SYNC_TOKEN", "PAT GitHub fine-grained pour la sync"],
41
+ ];
42
+ const OPTIONAL_VARS = [
43
+ ["MCP_DOCUMENTOR_CODE_ROOTS", 'code roots par projet, ex: {"my-proj":"/srv/my-proj"}'],
44
+ ["READ_ONLY", "1/true pour désactiver toutes les écritures"],
45
+ ["SYNC_PULL_ON_START", "false pour désactiver le pull au démarrage"],
46
+ ];
47
+ export function runSetupMcp({ cwd, force }) {
48
+ const target = path.join(cwd, ".mcp.json");
49
+ let config = {};
50
+ const exists = fs.existsSync(target);
51
+ if (exists) {
52
+ const raw = fs.readFileSync(target, "utf-8").trim();
53
+ if (raw !== "") {
54
+ try {
55
+ config = JSON.parse(raw);
56
+ }
57
+ catch (err) {
58
+ console.error(`Erreur : ${target} contient du JSON invalide (${err.message}).`);
59
+ console.error("Corrigez le fichier ou supprimez-le, puis relancez la commande.");
60
+ return 1;
61
+ }
62
+ }
63
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
64
+ console.error(`Erreur : ${target} ne contient pas un objet JSON.`);
65
+ return 1;
66
+ }
67
+ }
68
+ if (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers)) {
69
+ config.mcpServers = {};
70
+ }
71
+ const alreadyPresent = Object.prototype.hasOwnProperty.call(config.mcpServers, SERVER_NAME);
72
+ if (alreadyPresent && !force) {
73
+ console.error(`Le serveur "${SERVER_NAME}" est déjà configuré dans ${target}.`);
74
+ console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
75
+ return 1;
76
+ }
77
+ config.mcpServers[SERVER_NAME] = buildServerEntry();
78
+ fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
79
+ const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
80
+ console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
81
+ console.log("");
82
+ console.log('À compléter dans le champ "env" :');
83
+ for (const [name, description] of GENERATED_VARS) {
84
+ console.log(` ${name.padEnd(30)} ${description}`);
85
+ }
86
+ console.log("");
87
+ console.log("Variables optionnelles à ajouter au besoin :");
88
+ for (const [name, description] of OPTIONAL_VARS) {
89
+ console.log(` ${name.padEnd(30)} ${description}`);
90
+ }
91
+ console.log("");
92
+ console.log("Une variable laissée vide est simplement inactive.");
93
+ return 0;
94
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Répertoire des logs : MCP_LOG_DIR s'il est renseigné, sinon <racine>/.docs-data/logs.
3
+ * Lu depuis process.env uniquement (le .env du package est chargé après l'init du
4
+ * logger), donc la variable doit venir de la config du client MCP.
5
+ */
6
+ export declare function resolveLogDir(rootDir: string): string;
@@ -0,0 +1,12 @@
1
+ import path from "path";
2
+ /**
3
+ * Répertoire des logs : MCP_LOG_DIR s'il est renseigné, sinon <racine>/.docs-data/logs.
4
+ * Lu depuis process.env uniquement (le .env du package est chargé après l'init du
5
+ * logger), donc la variable doit venir de la config du client MCP.
6
+ */
7
+ export function resolveLogDir(rootDir) {
8
+ const configured = process.env.MCP_LOG_DIR?.trim();
9
+ if (configured)
10
+ return path.resolve(configured);
11
+ return path.join(rootDir, ".docs-data", "logs");
12
+ }
@@ -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;
@@ -12,6 +12,7 @@ import { promisify } from "util";
12
12
  const execFileAsync = promisify(execFile);
13
13
  import { fileURLToPath } from "url";
14
14
  import { createLogger, setupLogging } from "@imenam/mcp-gui-interface";
15
+ import { resolveLogDir } from "./core/log-dir.js";
15
16
  import matter from "gray-matter";
16
17
  import JSZip from "jszip";
17
18
  import puppeteer from "puppeteer-core";
@@ -31,7 +32,7 @@ const dataDir = process.env.MCP_DATA_DIR?.trim() || path.join(rootDir, ".docs-da
31
32
  const staticRoot = path.join(rootDir, "gui/dist");
32
33
  const absoluteStaticRoot = path.resolve(staticRoot);
33
34
  const configuredAppPath = process.env.APP_PATH || "/documentor-gui";
34
- setupLogging({ processLabel: "mcp-documentor:GUI", logDir: path.join(rootDir, ".docs-data", "logs") });
35
+ setupLogging({ processLabel: "mcp-documentor:GUI", logDir: resolveLogDir(rootDir) });
35
36
  const logger = createLogger("mcp-documentor:GUI");
36
37
  process.on("uncaughtException", (err) => {
37
38
  logger.error(`Uncaught Exception: ${err.message}`);
@@ -347,6 +348,9 @@ app.post("/api/doc", async (c) => {
347
348
  if (!body.title || !body.content) {
348
349
  return c.json({ error: "title and content are required" }, 400);
349
350
  }
351
+ if (!body.project) {
352
+ return c.json({ error: "project is required" }, 400);
353
+ }
350
354
  const result = await ipcRequest("CREATE_DOC", {
351
355
  title: body.title,
352
356
  content: body.content,
@@ -364,7 +368,10 @@ app.post("/api/doc", async (c) => {
364
368
  });
365
369
  app.get("/api/doc/*", async (c) => {
366
370
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
367
- const result = await ipcRequest("GET_DOC", { id });
371
+ const project = c.req.query("project");
372
+ if (!project)
373
+ return c.json({ error: "project query param is required" }, 400);
374
+ const result = await ipcRequest("GET_DOC", { id, project });
368
375
  if (result.error)
369
376
  return c.json({ error: result.error }, 400);
370
377
  return c.json(result.data);
@@ -372,11 +379,29 @@ app.get("/api/doc/*", async (c) => {
372
379
  app.put("/api/doc/*", async (c) => {
373
380
  try {
374
381
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
382
+ const project = c.req.query("project");
383
+ if (!project)
384
+ return c.json({ error: "project query param is required" }, 400);
375
385
  const body = await c.req.json();
376
386
  if (body.content === undefined) {
377
387
  return c.json({ error: "content is required" }, 400);
378
388
  }
379
- const result = await ipcRequest("UPDATE_DOC", { id, content: body.content, title: body.title }, 15000);
389
+ const result = await ipcRequest("UPDATE_DOC", { id, project, content: body.content, title: body.title }, 15000);
390
+ if (result.error)
391
+ return c.json({ error: result.error }, 500);
392
+ return c.json(result.data ?? { success: true });
393
+ }
394
+ catch (err) {
395
+ return c.json({ error: err.message }, 500);
396
+ }
397
+ });
398
+ app.delete("/api/doc/*", async (c) => {
399
+ try {
400
+ const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
401
+ const project = c.req.query("project");
402
+ if (!project)
403
+ return c.json({ error: "project query param is required" }, 400);
404
+ const result = await ipcRequest("DELETE_DOC", { id, project }, 15000);
380
405
  if (result.error)
381
406
  return c.json({ error: result.error }, 500);
382
407
  return c.json(result.data ?? { success: true });
@@ -387,21 +412,30 @@ app.put("/api/doc/*", async (c) => {
387
412
  });
388
413
  app.get("/api/freshness/*", async (c) => {
389
414
  const id = decodeURIComponent(c.req.path.slice("/api/freshness/".length));
390
- const result = await ipcRequest("CHECK_FRESHNESS", { id });
415
+ const project = c.req.query("project");
416
+ if (!project)
417
+ return c.json({ error: "project query param is required" }, 400);
418
+ const result = await ipcRequest("CHECK_FRESHNESS", { id, project });
391
419
  if (result.error)
392
420
  return c.json({ error: result.error }, 400);
393
421
  return c.json(result.data);
394
422
  });
395
423
  app.get("/api/git-diff/*", async (c) => {
396
424
  const id = decodeURIComponent(c.req.path.slice("/api/git-diff/".length));
397
- const result = await ipcRequest("GIT_DIFF", { id }, 15000);
425
+ const project = c.req.query("project");
426
+ if (!project)
427
+ return c.json({ error: "project query param is required" }, 400);
428
+ const result = await ipcRequest("GIT_DIFF", { id, project }, 15000);
398
429
  if (result.error)
399
430
  return c.json({ error: result.error }, 400);
400
431
  return c.json(result.data);
401
432
  });
402
433
  app.get("/api/history/*", async (c) => {
403
434
  const id = decodeURIComponent(c.req.path.slice("/api/history/".length));
404
- const result = await ipcRequest("GET_DOC_HISTORY", { id });
435
+ const project = c.req.query("project");
436
+ if (!project)
437
+ return c.json({ error: "project query param is required" }, 400);
438
+ const result = await ipcRequest("GET_DOC_HISTORY", { id, project });
405
439
  if (result.error)
406
440
  return c.json({ error: result.error }, 400);
407
441
  return c.json(result.data);
@@ -411,7 +445,10 @@ app.get("/api/history-version/*", async (c) => {
411
445
  const lastSlash = rest.lastIndexOf("/");
412
446
  const id = rest.slice(0, lastSlash);
413
447
  const version = parseInt(rest.slice(lastSlash + 1), 10);
414
- const result = await ipcRequest("GET_DOC_VERSION", { id, version });
448
+ const project = c.req.query("project");
449
+ if (!project)
450
+ return c.json({ error: "project query param is required" }, 400);
451
+ const result = await ipcRequest("GET_DOC_VERSION", { id, version, project });
415
452
  if (result.error)
416
453
  return c.json({ error: result.error }, 400);
417
454
  return c.json(result.data);
@@ -429,8 +466,11 @@ app.get("/api/file-section/*", async (c) => {
429
466
  const docId = c.req.query("docId");
430
467
  if (!docId)
431
468
  return c.json({ error: "docId query param required" }, 400);
469
+ const project = c.req.query("project");
470
+ if (!project)
471
+ return c.json({ error: "project query param is required" }, 400);
432
472
  const filePath = decodeURIComponent(c.req.path.slice("/api/file-section/".length));
433
- const result = await ipcRequest("GET_FILE_SECTION", { docId, filePath }, 8000);
473
+ const result = await ipcRequest("GET_FILE_SECTION", { docId, project, filePath }, 8000);
434
474
  if (result.error)
435
475
  return c.json({ error: result.error }, 400);
436
476
  return c.json(result.data);