@imenam/mcp-feature-analyzer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +295 -0
  3. package/dist/shared/diagram-geometry.d.ts +45 -0
  4. package/dist/shared/diagram-geometry.js +120 -0
  5. package/dist/shared/diagram-layout.d.ts +98 -0
  6. package/dist/shared/diagram-layout.js +1156 -0
  7. package/dist/shared/diagram-quality.d.ts +30 -0
  8. package/dist/shared/diagram-quality.js +26 -0
  9. package/dist/shared/labels.d.ts +31 -0
  10. package/dist/shared/labels.js +53 -0
  11. package/dist/shared/mindmap-tree.d.ts +28 -0
  12. package/dist/shared/mindmap-tree.js +62 -0
  13. package/dist/shared/prompt.d.ts +17 -0
  14. package/dist/shared/prompt.js +97 -0
  15. package/dist/shared/review-state.d.ts +10 -0
  16. package/dist/shared/review-state.js +17 -0
  17. package/dist/shared/schemas/analysis.schema.d.ts +372 -0
  18. package/dist/shared/schemas/analysis.schema.js +152 -0
  19. package/dist/shared/schemas/api.schema.d.ts +92 -0
  20. package/dist/shared/schemas/api.schema.js +46 -0
  21. package/dist/shared/schemas/diff.schema.d.ts +91 -0
  22. package/dist/shared/schemas/diff.schema.js +37 -0
  23. package/dist/shared/schemas/ipc.schema.d.ts +29 -0
  24. package/dist/shared/schemas/ipc.schema.js +30 -0
  25. package/dist/shared/severity.d.ts +11 -0
  26. package/dist/shared/severity.js +17 -0
  27. package/dist/shared/text.d.ts +12 -0
  28. package/dist/shared/text.js +20 -0
  29. package/dist/src/cli/setup-mcp.d.ts +24 -0
  30. package/dist/src/cli/setup-mcp.js +145 -0
  31. package/dist/src/core/analysis-store.d.ts +95 -0
  32. package/dist/src/core/analysis-store.js +387 -0
  33. package/dist/src/core/data-dir.d.ts +5 -0
  34. package/dist/src/core/data-dir.js +11 -0
  35. package/dist/src/core/diff-parser.d.ts +22 -0
  36. package/dist/src/core/diff-parser.js +225 -0
  37. package/dist/src/core/errors.d.ts +16 -0
  38. package/dist/src/core/errors.js +28 -0
  39. package/dist/src/core/git.d.ts +35 -0
  40. package/dist/src/core/git.js +257 -0
  41. package/dist/src/core/ids.d.ts +16 -0
  42. package/dist/src/core/ids.js +41 -0
  43. package/dist/src/core/invariants.d.ts +24 -0
  44. package/dist/src/core/invariants.js +157 -0
  45. package/dist/src/core/locations.d.ts +30 -0
  46. package/dist/src/core/locations.js +66 -0
  47. package/dist/src/core/log-dir.d.ts +6 -0
  48. package/dist/src/core/log-dir.js +13 -0
  49. package/dist/src/core/migrate.d.ts +24 -0
  50. package/dist/src/core/migrate.js +53 -0
  51. package/dist/src/core/refresh.d.ts +30 -0
  52. package/dist/src/core/refresh.js +113 -0
  53. package/dist/src/core/request-guard.d.ts +32 -0
  54. package/dist/src/core/request-guard.js +72 -0
  55. package/dist/src/core/review-actions.d.ts +27 -0
  56. package/dist/src/core/review-actions.js +99 -0
  57. package/dist/src/gui-worker.d.ts +1 -0
  58. package/dist/src/gui-worker.js +401 -0
  59. package/dist/src/index.d.ts +2 -0
  60. package/dist/src/index.js +658 -0
  61. package/dist/src/tools/add-findings.d.ts +9 -0
  62. package/dist/src/tools/add-findings.js +106 -0
  63. package/dist/src/tools/create-analysis.d.ts +4 -0
  64. package/dist/src/tools/create-analysis.js +36 -0
  65. package/dist/src/tools/delete-analysis.d.ts +4 -0
  66. package/dist/src/tools/delete-analysis.js +11 -0
  67. package/dist/src/tools/delete-diagram.d.ts +4 -0
  68. package/dist/src/tools/delete-diagram.js +22 -0
  69. package/dist/src/tools/delete-findings.d.ts +7 -0
  70. package/dist/src/tools/delete-findings.js +27 -0
  71. package/dist/src/tools/format.d.ts +27 -0
  72. package/dist/src/tools/format.js +92 -0
  73. package/dist/src/tools/get-analysis.d.ts +4 -0
  74. package/dist/src/tools/get-analysis.js +87 -0
  75. package/dist/src/tools/get-diff.d.ts +4 -0
  76. package/dist/src/tools/get-diff.js +67 -0
  77. package/dist/src/tools/get-review-feedback.d.ts +4 -0
  78. package/dist/src/tools/get-review-feedback.js +71 -0
  79. package/dist/src/tools/inputs.d.ts +14 -0
  80. package/dist/src/tools/inputs.js +40 -0
  81. package/dist/src/tools/list-analyses.d.ts +8 -0
  82. package/dist/src/tools/list-analyses.js +71 -0
  83. package/dist/src/tools/refresh-analysis.d.ts +7 -0
  84. package/dist/src/tools/refresh-analysis.js +39 -0
  85. package/dist/src/tools/set-diagram.d.ts +16 -0
  86. package/dist/src/tools/set-diagram.js +207 -0
  87. package/dist/src/tools/types.d.ts +52 -0
  88. package/dist/src/tools/types.js +133 -0
  89. package/dist/src/tools/update-analysis.d.ts +4 -0
  90. package/dist/src/tools/update-analysis.js +121 -0
  91. package/dist/src/tools/update-finding.d.ts +8 -0
  92. package/dist/src/tools/update-finding.js +91 -0
  93. package/dist/src/version.d.ts +1 -0
  94. package/dist/src/version.js +2 -0
  95. package/gui/dist/assets/index-CU9pNYDp.js +72 -0
  96. package/gui/dist/assets/index-Du5eL78F.css +1 -0
  97. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  98. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  99. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  100. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  101. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  102. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  103. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  104. package/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  105. package/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  106. package/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  107. package/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  108. package/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  109. package/gui/dist/index.html +13 -0
  110. package/package.json +56 -0
  111. package/scripts/generate-version.js +20 -0
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env node
2
+ import dotenv from "dotenv";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
+ import { GuiLauncher, createLogger, reconnectGuiToolDefinition, setupLogging } from "@imenam/mcp-gui-interface";
10
+ import { APP_VERSION } from "./version.js";
11
+ import { resolveDataDir } from "./core/data-dir.js";
12
+ import { resolveLogDir } from "./core/log-dir.js";
13
+ import { AnalysisStore } from "./core/analysis-store.js";
14
+ import { AnalysisError, NotFoundError, RevisionConflictError } from "./core/errors.js";
15
+ import { DEFAULT_HEAD_REF } from "./core/git.js";
16
+ import { addNote, deleteNote, setFileReviewed, setFindingStatus, submitReview } from "./core/review-actions.js";
17
+ import { GUI_REQUEST_TYPES, IPCMessageSchema } from "../shared/schemas/ipc.schema.js";
18
+ import { AddNoteRequestSchema, AnalysisRequestSchema, DeleteNoteRequestSchema, SetFileReviewedRequestSchema, SetFindingStatusRequestSchema, SubmitReviewRequestSchema, } from "../shared/schemas/api.schema.js";
19
+ import { ANALYSIS_MODES, DIAGRAM_KINDS, FINDING_KINDS, NODE_SHAPES, NODE_STATUSES, SEVERITIES, } from "../shared/schemas/analysis.schema.js";
20
+ import { errorResult, textResult } from "./tools/types.js";
21
+ import { createAnalysis } from "./tools/create-analysis.js";
22
+ import { listAnalyses } from "./tools/list-analyses.js";
23
+ import { getAnalysis } from "./tools/get-analysis.js";
24
+ import { getDiff } from "./tools/get-diff.js";
25
+ import { refreshAnalysis } from "./tools/refresh-analysis.js";
26
+ import { updateAnalysis } from "./tools/update-analysis.js";
27
+ import { addFindings } from "./tools/add-findings.js";
28
+ import { updateFinding } from "./tools/update-finding.js";
29
+ import { deleteFindings } from "./tools/delete-findings.js";
30
+ import { DEFAULT_NODE_SHAPE, DEFAULT_NODE_STATUS, setDiagram } from "./tools/set-diagram.js";
31
+ import { deleteDiagram } from "./tools/delete-diagram.js";
32
+ import { getReviewFeedback } from "./tools/get-review-feedback.js";
33
+ import { deleteAnalysis } from "./tools/delete-analysis.js";
34
+ const __filename = fileURLToPath(import.meta.url);
35
+ const __dirname = path.dirname(__filename);
36
+ // Les flags CLI sont traités avant toute initialisation : ils s'exécutent dans le
37
+ // répertoire de l'utilisateur, pas dans un serveur MCP.
38
+ if (process.argv.slice(2).includes("--claude-setup-mcp")) {
39
+ const argv = process.argv.slice(2);
40
+ const { runSetupMcp, parseGuiExposure } = await import("./cli/setup-mcp.js");
41
+ const parsed = parseGuiExposure(argv);
42
+ if ("error" in parsed) {
43
+ console.error(`Erreur : ${parsed.error}`);
44
+ process.exit(1);
45
+ }
46
+ process.exit(runSetupMcp({ cwd: process.cwd(), force: argv.includes("--force"), exposure: parsed.exposure }));
47
+ }
48
+ function findProjectRoot(startDir) {
49
+ let current = startDir;
50
+ while (current !== path.dirname(current)) {
51
+ if (fs.existsSync(path.join(current, "package.json")))
52
+ return current;
53
+ current = path.dirname(current);
54
+ }
55
+ return startDir;
56
+ }
57
+ const rootDir = findProjectRoot(__dirname);
58
+ const packageEnvPath = path.join(rootDir, ".env");
59
+ setupLogging({ processLabel: "mcp-feature-analyzer", logDir: resolveLogDir(rootDir) });
60
+ const logger = createLogger("mcp-feature-analyzer");
61
+ // Rechargé à chaque appel, avec override : le .env local prime sur les
62
+ // variables héritées de la déclaration du MCP.
63
+ function getConfig() {
64
+ if (fs.existsSync(packageEnvPath))
65
+ dotenv.config({ path: packageEnvPath, override: true, quiet: true });
66
+ return { proxyUrl: process.env.PROXY_URL, appPort: process.env.APP_PORT, version: APP_VERSION };
67
+ }
68
+ getConfig();
69
+ const dataDir = resolveDataDir(rootDir);
70
+ const store = new AnalysisStore(dataDir);
71
+ /** Présentation du serveur transmise au client MCP : concepts, déroulé et règles. */
72
+ const INSTRUCTIONS = `mcp-feature-analyzer helps a human review a feature you developed. You write the analysis through these tools; the user reviews it in a web GUI and records feedback that you read back.
73
+
74
+ Concepts:
75
+ - An **analysis** is one feature under review. It freezes the git diff of a repository (branch mode: base...head; working_tree mode: uncommitted changes, index included, against HEAD) together with your overview, summary, findings and diagrams.
76
+ - A **project** groups the analyses of one product or feature; the GUI files analyses by project.
77
+ - The **overview** is your global analysis of the feature: its objective (what it lets users do), its approach (architecture choices, main flow) and the attention points the reviewer should check first. The **summary** lists the functional changes of the feature, one bullet per change, not one bullet per file.
78
+ - A **finding** is a point for the reviewer: an issue anchored on changed lines, or a requirement_gap for requested behaviour that is missing (location optional). Severity: ${SEVERITIES.join(", ")}.
79
+ - A **diagram** is a flow, layers or mindmap picture of the feature. You give nodes and links only; the GUI lays them out.
80
+ - The **review** belongs to the reviewer: files marked reviewed, findings ignored, notes, and a submitted decision with a prompt for you.
81
+
82
+ Workflow:
83
+ 1. list_analyses (with project) to find an analysis of the same feature: reuse it with update_analysis and refresh_analysis instead of creating a duplicate. Otherwise create_analysis with its project once the feature is developed.
84
+ 2. get_diff to read the frozen diff.
85
+ 3. update_analysis with the overview (objective, approach, attention_points) and the summary (the functional changes).
86
+ 4. add_findings for every problem, including requirement_gap findings for requested behaviour that is missing.
87
+ 5. set_diagram when a picture helps (impacted layers, process flow, concept tree).
88
+ 6. Tell the user to review the analysis in the GUI (the link is in the tool results).
89
+ 7. get_review_feedback to read the decision, the selected points and the prompt.
90
+ 8. Fix the code, then refresh_analysis to recompute the diff for a new review round.
91
+
92
+ Diagram rules:
93
+ - One subject per diagram, 5 to 12 nodes; split a bigger picture into several diagrams.
94
+ - Declare nodes in reading order (from the start of the flow, from top to bottom): the declaration order is the initial order of every rank or column.
95
+ - Zero crossings are expected. set_diagram reports link crossings and links through nodes; fix them before moving on.
96
+ - flow for a process with steps and decisions; layers for the impact perimeter across architecture layers; mindmap for a tree of concepts (one root, one parent per node).
97
+
98
+ Rules:
99
+ - Line numbers are always NEW-side numbers, as shown by get_diff.
100
+ - Nothing is ever written to the repository: suggestions are displayed to the reviewer only.
101
+ - The reviewer alone decides which findings are ignored and when the review is submitted; you cannot change either.`;
102
+ const server = new Server({ name: "mcp-feature-analyzer", version: APP_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
103
+ // --- Cycle de vie de la GUI -------------------------------------------------
104
+ /** Envoie un message IPC au GUI worker s'il est connecté. */
105
+ function broadcastToGUI(message) {
106
+ const child = guiLauncher.getProcess();
107
+ if (child?.connected) {
108
+ try {
109
+ child.send(message);
110
+ }
111
+ catch (err) {
112
+ logger.error(`Failed to send IPC message: ${err.message}`);
113
+ }
114
+ }
115
+ }
116
+ function reply(msg, type, data) {
117
+ broadcastToGUI({ type, correlationId: msg.correlationId, data, timestamp: new Date().toISOString() });
118
+ }
119
+ function replyError(msg, type, error, errorKind) {
120
+ broadcastToGUI({ type, correlationId: msg.correlationId, error, errorKind, timestamp: new Date().toISOString() });
121
+ }
122
+ /** Notifie la GUI qu'une analyse a changé côté agent, pour qu'elle se rafraîchisse. */
123
+ function notifyAnalysisChanged(analysisId) {
124
+ broadcastToGUI({ type: "ANALYSIS_UPDATED", data: { analysisId }, timestamp: new Date().toISOString() });
125
+ }
126
+ /** Envoie à la GUI la liste des analyses, après une création ou une suppression. */
127
+ function notifyAnalysesListChanged() {
128
+ broadcastToGUI({ type: "ANALYSES_UPDATED", data: { analyses: store.list().analyses }, timestamp: new Date().toISOString() });
129
+ }
130
+ /** Données IPC d'une requête de la GUI, validées par `schema` ; une donnée invalide est une erreur de la requête. */
131
+ function parseRequest(schema, data) {
132
+ const parsed = schema.safeParse(data);
133
+ if (!parsed.success) {
134
+ const issues = parsed.error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"} ${issue.message}`);
135
+ throw new AnalysisError(`Invalid request: ${issues.join("; ")}.`);
136
+ }
137
+ return parsed.data;
138
+ }
139
+ /** Nature d'une erreur levée en traitant une requête de la GUI. */
140
+ function errorKindOf(err) {
141
+ if (err instanceof RevisionConflictError)
142
+ return "conflict";
143
+ if (err instanceof NotFoundError)
144
+ return "not_found";
145
+ if (err instanceof AnalysisError)
146
+ return "invalid";
147
+ return "internal";
148
+ }
149
+ /** Modification faite depuis la GUI : la liste des analyses et l'analyse sont renvoyées à tous les clients. */
150
+ function afterUserMutation(analysisId) {
151
+ notifyAnalysisChanged(analysisId);
152
+ notifyAnalysesListChanged();
153
+ }
154
+ /** Traite une requête de la GUI et renvoie ses données de réponse ; lève une erreur métier sinon. */
155
+ function handleGuiRequest(msg) {
156
+ switch (msg.type) {
157
+ case "GET_CONFIG":
158
+ return { dataDir, version: APP_VERSION };
159
+ case "LIST_ANALYSES":
160
+ return store.list();
161
+ case "GET_ANALYSIS":
162
+ return { analysis: store.get(parseRequest(AnalysisRequestSchema, msg.data).analysisId) };
163
+ case "GET_DIFF":
164
+ return { diff: store.getSnapshot(parseRequest(AnalysisRequestSchema, msg.data).analysisId) };
165
+ case "SET_FILE_REVIEWED": {
166
+ const request = parseRequest(SetFileReviewedRequestSchema, msg.data);
167
+ const analysis = setFileReviewed(store, request.analysisId, request.body);
168
+ afterUserMutation(analysis.id);
169
+ return { analysis };
170
+ }
171
+ case "SET_FINDING_STATUS": {
172
+ const request = parseRequest(SetFindingStatusRequestSchema, msg.data);
173
+ const analysis = setFindingStatus(store, request.analysisId, request.findingId, request.body);
174
+ afterUserMutation(analysis.id);
175
+ return { analysis };
176
+ }
177
+ case "ADD_NOTE": {
178
+ const request = parseRequest(AddNoteRequestSchema, msg.data);
179
+ const analysis = addNote(store, request.analysisId, request.body);
180
+ afterUserMutation(analysis.id);
181
+ return { analysis };
182
+ }
183
+ case "DELETE_NOTE": {
184
+ const request = parseRequest(DeleteNoteRequestSchema, msg.data);
185
+ const analysis = deleteNote(store, request.analysisId, request.noteId, request.body);
186
+ afterUserMutation(analysis.id);
187
+ return { analysis };
188
+ }
189
+ case "SUBMIT_REVIEW": {
190
+ const request = parseRequest(SubmitReviewRequestSchema, msg.data);
191
+ const analysis = submitReview(store, request.analysisId, request.body);
192
+ afterUserMutation(analysis.id);
193
+ return { analysis };
194
+ }
195
+ default:
196
+ throw new Error(`GUI request ${msg.type} has no handler.`);
197
+ }
198
+ }
199
+ const GUI_REQUESTS = new Set(GUI_REQUEST_TYPES);
200
+ /**
201
+ * Messages du GUI worker. READY et ALREADY_RUNNING sont d'abord traités par
202
+ * GuiLauncher (statut, URL), puis transmis ici.
203
+ */
204
+ function handleIPCMessage(raw) {
205
+ const parsed = IPCMessageSchema.safeParse(raw);
206
+ if (!parsed.success) {
207
+ logger.warn(`Invalid IPC message ignored: ${parsed.error.message}`);
208
+ return;
209
+ }
210
+ const msg = parsed.data;
211
+ switch (msg.type) {
212
+ case "READY":
213
+ logger.info(`GUI ready at ${msg.data?.url}`);
214
+ return;
215
+ case "ALREADY_RUNNING":
216
+ logger.info(`GUI already served by another instance at ${msg.data?.url}`);
217
+ return;
218
+ case "GET_INITIAL_STATE":
219
+ broadcastToGUI({
220
+ type: "INITIAL_STATE",
221
+ data: { version: APP_VERSION, analyses: store.list().analyses },
222
+ timestamp: new Date().toISOString(),
223
+ });
224
+ return;
225
+ default:
226
+ if (GUI_REQUESTS.has(msg.type)) {
227
+ try {
228
+ reply(msg, `${msg.type}_RESPONSE`, handleGuiRequest(msg));
229
+ }
230
+ catch (err) {
231
+ const kind = errorKindOf(err);
232
+ if (kind === "internal")
233
+ logger.error(`GUI request ${msg.type} failed: ${err?.stack ?? err}`);
234
+ replyError(msg, `${msg.type}_RESPONSE`, err?.message ?? String(err), kind);
235
+ }
236
+ return;
237
+ }
238
+ logger.debug(`Unhandled IPC message type: ${msg.type}`);
239
+ }
240
+ }
241
+ const guiLauncher = new GuiLauncher({
242
+ guiPath: path.join(__dirname, "gui-worker.js"),
243
+ maxRestarts: 0,
244
+ onMessage: handleIPCMessage,
245
+ reloadEnv: getConfig,
246
+ });
247
+ /** La GUI a besoin soit d'un proxy, soit d'un port local (mode standalone). */
248
+ function guiIsConfigured() {
249
+ return !!(process.env.PROXY_URL?.trim() || process.env.APP_PORT?.trim());
250
+ }
251
+ function launchGUI() {
252
+ if (!guiIsConfigured()) {
253
+ logger.info("[MASTER] Ni PROXY_URL ni APP_PORT défini — GUI désactivée.");
254
+ return;
255
+ }
256
+ logger.info("Launching GUI worker...");
257
+ guiLauncher.start();
258
+ }
259
+ /**
260
+ * `reconnect_gui` : relance via GuiLauncher en mode proxy. En mode standalone,
261
+ * GuiLauncher ne sait pas relancer le worker : l'outil décrit l'état et la
262
+ * marche à suivre.
263
+ */
264
+ async function reconnectGui(args) {
265
+ getConfig();
266
+ const appPort = process.env.APP_PORT?.trim();
267
+ if (process.env.PROXY_URL?.trim() || !appPort) {
268
+ return { ...(await guiLauncher.handleReconnectTool({ force: args.force === true })) };
269
+ }
270
+ const child = guiLauncher.getProcess();
271
+ const alive = !!child && child.exitCode === null && !child.killed;
272
+ const url = guiLauncher.getUrl();
273
+ if (alive && guiLauncher.getStatus() === "ready") {
274
+ return textResult(`GUI is served in standalone mode at ${url}. Nothing to do.`);
275
+ }
276
+ return errorResult(`reconnect_gui relaunches the GUI through the proxy only. The GUI is in standalone mode (APP_PORT=${appPort}) and is not running (status: ${guiLauncher.getStatus()}). Free port ${appPort} or set another APP_PORT, then restart the MCP server. To use the proxy instead, set PROXY_URL and call reconnect_gui again.`);
277
+ }
278
+ // --- Déclaration des outils -------------------------------------------------
279
+ /** Lien vers l'analyse dans la GUI, ajouté aux résultats de mutation ; vide quand la GUI est désactivée. */
280
+ function guiHint(analysisId) {
281
+ const url = guiLauncher.getUrl();
282
+ if (!url)
283
+ return "";
284
+ return `\n\nOpen the review: ${url}#/${encodeURIComponent(analysisId)}`;
285
+ }
286
+ const ANALYSIS_ID_DOC = "Id of the analysis, as returned by create_analysis or list_analyses.";
287
+ const LINE_NUMBERS_DOC = "Line numbers are NEW-side numbers, as shown in the second column of get_diff.";
288
+ const LOCATION_PROPERTIES = {
289
+ path: {
290
+ type: "string",
291
+ description: "Path of a changed file of the analysis, relative to the repository root, as listed by get_diff.",
292
+ },
293
+ start_line: { type: "integer", minimum: 1, description: `First targeted line. ${LINE_NUMBERS_DOC}` },
294
+ end_line: { type: "integer", minimum: 1, description: "Last targeted line, inclusive (default: start_line)." },
295
+ };
296
+ const SEVERITY_DOC = `Severity: ${SEVERITIES.join(", ")} (from most to least serious).`;
297
+ const FINDING_KIND_DOC = `Kind (default "issue"): "issue" is a problem in the changed code and needs a location; ` +
298
+ `"requirement_gap" is requested behaviour that is missing and may have no location.`;
299
+ const SUGGESTION_DOC = "Replacement text proposed for the targeted lines, kept verbatim (indentation included); an empty string proposes deleting them. " +
300
+ "Displayed to the reviewer only: nothing is written to the repository. Needs a location.";
301
+ const FINDING_PROMPT_DOC = "Prompt to hand back to the agent for this finding, replacing the generated one (body, current lines, replacement) in the review feedback.";
302
+ const REQUEST_TEXT_DOC = "Initial request the feature answers (ticket, user prompt), shown to the reviewer.";
303
+ const REQUEST_SOURCE_DOC = "Where the initial request comes from, e.g. a ticket id or URL.";
304
+ const NODE_STATUS_DOC = `Status (default "${DEFAULT_NODE_STATUS}"): new = added by the feature; modified = changed by the feature; ` +
305
+ `impacted = unchanged code whose behaviour the feature affects; existing = unchanged context; ` +
306
+ `finding = where a finding sits; missing = requested but not implemented (drawn as an outline).`;
307
+ const NODE_SHAPE_DOC = `Shape (default "${DEFAULT_NODE_SHAPE}"): box = component or step; pill = start, end or actor; decision = a branch in a flow (drawn as a diamond).`;
308
+ const DIAGRAM_KIND_DOC = "flow = a process with steps and decisions, laid out left to right along the links, where a side branch without a join and the last node of a chain hang below the node before them; " +
309
+ "layers = the impact perimeter, one column per entry of `layers` (e.g. GUI, API, core, storage), each node in its layer; " +
310
+ "mindmap = a tree of concepts drawn radially around its root: exactly one root, one incoming link for every other node, no cycle (refused otherwise).";
311
+ const DIAGRAM_RULES_DOC = "Rules: one subject per diagram, 5 to 12 nodes; declare nodes in reading order, since the declaration order is the initial order of every rank or column; " +
312
+ "zero crossings expected. The result reports every link crossing and every link through a node, with advice: reorder the nodes, split the diagram or change its kind, then send it again.";
313
+ const PROJECT_DOC = "Project the analysis belongs to: the product or feature name, used as its folder in the GUI. " +
314
+ "Reuse the exact name of an existing project (see list_analyses) for another analysis of it.";
315
+ const TOOLS = [
316
+ {
317
+ name: "list_analyses",
318
+ description: "List the analyses grouped by project, with their refs, open findings by severity, reviewed files, diagram count and review state " +
319
+ "(not started, in progress, all files reviewed, submitted). Start here: when the project already has an analysis of the feature, " +
320
+ "continue it (update_analysis to rewrite the overview and summary, refresh_analysis after changing the code) instead of creating a duplicate; " +
321
+ "create a new one with create_analysis for another feature or another diff.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ project: { type: "string", description: "Only list the analyses of this project (exact name). An unknown name lists the existing projects." },
326
+ },
327
+ },
328
+ },
329
+ {
330
+ name: "create_analysis",
331
+ description: "Create an analysis once a feature is developed: computes and freezes the git diff of the repository for human review. " +
332
+ `In branch mode the diff is base...head (head defaults to ${DEFAULT_HEAD_REF}); in working_tree mode it is the uncommitted changes, index included, against HEAD. ` +
333
+ "Returns the id and the changed files; then read them with get_diff.",
334
+ inputSchema: {
335
+ type: "object",
336
+ properties: {
337
+ repo_path: { type: "string", description: "Absolute path of the root of the git repository." },
338
+ project: { type: "string", description: PROJECT_DOC },
339
+ title: { type: "string", description: "Short title of the feature, e.g. \"OAuth token refresh\"." },
340
+ mode: {
341
+ type: "string",
342
+ enum: [...ANALYSIS_MODES],
343
+ description: "branch: diff between two refs; working_tree: uncommitted changes against HEAD.",
344
+ },
345
+ base: {
346
+ type: "string",
347
+ description: "Branch mode only, required: the ref the feature branched from, e.g. \"master\". Not accepted in working_tree mode.",
348
+ },
349
+ head: {
350
+ type: "string",
351
+ description: `Branch mode only: the ref holding the feature (default ${DEFAULT_HEAD_REF}). Not accepted in working_tree mode.`,
352
+ },
353
+ request_text: { type: "string", description: REQUEST_TEXT_DOC },
354
+ request_source: { type: "string", description: `${REQUEST_SOURCE_DOC} Needs request_text.` },
355
+ },
356
+ required: ["repo_path", "project", "title", "mode"],
357
+ },
358
+ },
359
+ {
360
+ name: "get_analysis",
361
+ description: "Read a whole analysis: project, request, overview, summary, files (reviewed or not), findings with their status and location, reviewer notes, " +
362
+ "diagrams (flagged when their layout has crossings) and review state. Use it to check what is already recorded before adding more.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: { analysis_id: { type: "string", description: ANALYSIS_ID_DOC } },
366
+ required: ["analysis_id"],
367
+ },
368
+ },
369
+ {
370
+ name: "get_diff",
371
+ description: "Read the frozen diff of one file, or of every file when path is omitted, with the old and the new line number of each line. " +
372
+ "Use the NEW-side numbers (second column) for every finding location and diagram node line.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
377
+ path: { type: "string", description: "Changed file to show (default: all files)." },
378
+ },
379
+ required: ["analysis_id"],
380
+ },
381
+ },
382
+ {
383
+ name: "update_analysis",
384
+ description: "Change the title, the project, the overview, the summary or the initial request of an analysis. Right after reading the diff, " +
385
+ "write the overview (objective, approach, attention_points: your global analysis of the feature) and the summary " +
386
+ "(the functional changes of the feature, one bullet per change, not one bullet per file). Give at least one field; fields left out are kept.",
387
+ inputSchema: {
388
+ type: "object",
389
+ properties: {
390
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
391
+ title: { type: "string", description: "New title." },
392
+ project: { type: "string", description: `New project. ${PROJECT_DOC}` },
393
+ objective: {
394
+ type: ["string", "null"],
395
+ description: "Overview: what the feature lets users do, from a functional point of view. When the analysis has no overview yet, give objective and approach together to create it. " +
396
+ "null removes the whole overview (approach and attention points included).",
397
+ },
398
+ approach: {
399
+ type: "string",
400
+ description: "Overview: how the feature achieves it (architecture choices, main flow). Replaces the current approach.",
401
+ },
402
+ attention_points: {
403
+ type: "array",
404
+ items: { type: "string" },
405
+ description: "Overview: risks and points the reviewer should check first, one per string. Replaces the whole list; [] clears it. " +
406
+ "Defaults to an empty list when the overview is created without it.",
407
+ },
408
+ summary: {
409
+ type: "array",
410
+ items: { type: "string" },
411
+ description: "Functional changes of the feature, one bullet per change (e.g. \"Expired tokens are refreshed once before the request fails\"), not one bullet per file. " +
412
+ "Replaces the whole list; [] clears it.",
413
+ },
414
+ request_text: { type: ["string", "null"], description: `${REQUEST_TEXT_DOC} null removes the request.` },
415
+ request_source: { type: ["string", "null"], description: `${REQUEST_SOURCE_DOC} null clears it.` },
416
+ },
417
+ required: ["analysis_id"],
418
+ },
419
+ },
420
+ {
421
+ name: "add_findings",
422
+ description: "Record findings for the reviewer, as a batch. Each location is checked against the frozen diff and its lines are copied; " +
423
+ "if any finding is invalid, nothing is added and every problem is reported. " +
424
+ "Include requirement_gap findings for requested behaviour that is missing.",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
429
+ findings: {
430
+ type: "array",
431
+ minItems: 1,
432
+ items: {
433
+ type: "object",
434
+ properties: {
435
+ id: { type: "string", description: "Optional id (letters, digits, '_' or '-'); generated when omitted." },
436
+ severity: { type: "string", enum: [...SEVERITIES], description: SEVERITY_DOC },
437
+ kind: { type: "string", enum: [...FINDING_KINDS], description: FINDING_KIND_DOC },
438
+ title: { type: "string", description: "One-line title." },
439
+ body: { type: "string", description: "Explanation: what is wrong and why it matters." },
440
+ ...LOCATION_PROPERTIES,
441
+ suggestion: { type: "string", description: SUGGESTION_DOC },
442
+ prompt: { type: "string", description: FINDING_PROMPT_DOC },
443
+ },
444
+ required: ["severity", "title", "body"],
445
+ },
446
+ },
447
+ },
448
+ required: ["analysis_id", "findings"],
449
+ },
450
+ },
451
+ {
452
+ name: "update_finding",
453
+ description: "Change the fields of one finding: severity, kind, title, body, location, suggestion or prompt. " +
454
+ "A new location is checked against the diff and reopens an outdated finding; path null removes the location. " +
455
+ "Whether a finding is ignored is the reviewer's decision and cannot be changed here.",
456
+ inputSchema: {
457
+ type: "object",
458
+ properties: {
459
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
460
+ finding_id: { type: "string", description: "Id of the finding, as shown by get_analysis." },
461
+ severity: { type: "string", enum: [...SEVERITIES], description: SEVERITY_DOC },
462
+ kind: { type: "string", enum: [...FINDING_KINDS], description: "issue or requirement_gap." },
463
+ title: { type: "string", description: "New title." },
464
+ body: { type: "string", description: "New explanation." },
465
+ path: {
466
+ type: ["string", "null"],
467
+ description: `${LOCATION_PROPERTIES.path.description} Give it with start_line to move the finding; null removes the location (requirement_gap only).`,
468
+ },
469
+ start_line: LOCATION_PROPERTIES.start_line,
470
+ end_line: LOCATION_PROPERTIES.end_line,
471
+ suggestion: { type: ["string", "null"], description: `${SUGGESTION_DOC} null clears it.` },
472
+ prompt: { type: ["string", "null"], description: `${FINDING_PROMPT_DOC} null clears it.` },
473
+ },
474
+ required: ["analysis_id", "finding_id"],
475
+ },
476
+ },
477
+ {
478
+ name: "delete_findings",
479
+ description: "Delete findings by id, typically once refresh_analysis shows them solved. If any id is unknown, nothing is deleted.",
480
+ inputSchema: {
481
+ type: "object",
482
+ properties: {
483
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
484
+ finding_ids: { type: "array", minItems: 1, items: { type: "string" }, description: "Ids of the findings to delete." },
485
+ },
486
+ required: ["analysis_id", "finding_ids"],
487
+ },
488
+ },
489
+ {
490
+ name: "set_diagram",
491
+ description: "Create a diagram, or fully replace the one with diagram_id, when a picture helps the reviewer (impact perimeter, process flow, concept map). " +
492
+ "Give nodes and links only, with no coordinates or colours: the GUI lays the diagram out and colours nodes by status. " +
493
+ `${DIAGRAM_KIND_DOC} ${DIAGRAM_RULES_DOC}`,
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
498
+ diagram_id: { type: "string", description: "Id of the diagram to replace, or of the new one; generated when omitted." },
499
+ title: { type: "string", description: "Title of the diagram." },
500
+ kind: { type: "string", enum: [...DIAGRAM_KINDS], description: DIAGRAM_KIND_DOC },
501
+ layers: {
502
+ type: "array",
503
+ items: { type: "string" },
504
+ description: "Required when kind is layers, forbidden otherwise: column names from left to right.",
505
+ },
506
+ nodes: {
507
+ type: "array",
508
+ minItems: 1,
509
+ description: "Nodes in reading order (5 to 12): the declaration order is the initial order of every rank or column.",
510
+ items: {
511
+ type: "object",
512
+ properties: {
513
+ id: { type: "string", description: "Node id, unique in the diagram; links refer to it." },
514
+ label: { type: "string", description: "Text shown in the node." },
515
+ shape: { type: "string", enum: [...NODE_SHAPES], description: NODE_SHAPE_DOC },
516
+ status: { type: "string", enum: [...NODE_STATUSES], description: NODE_STATUS_DOC },
517
+ layer: { type: "string", description: "Required when kind is layers: one of `layers`. Forbidden otherwise." },
518
+ detail: { type: "string", description: "Longer description shown on hover." },
519
+ path: {
520
+ type: "string",
521
+ description: "Changed file the node stands for; clicking the node opens it in the review. Omit it for code outside the diff.",
522
+ },
523
+ line: { type: "integer", minimum: 1, description: `Line to open in that file. ${LINE_NUMBERS_DOC} Needs path.` },
524
+ },
525
+ required: ["id", "label"],
526
+ },
527
+ },
528
+ links: {
529
+ type: "array",
530
+ items: {
531
+ type: "object",
532
+ properties: {
533
+ from: { type: "string", description: "Id of the source node." },
534
+ to: { type: "string", description: "Id of the target node." },
535
+ label: { type: "string", description: "Text shown on the link." },
536
+ },
537
+ required: ["from", "to"],
538
+ },
539
+ description: "Directed links between nodes (default: none). In a mindmap, from parent to child.",
540
+ },
541
+ },
542
+ required: ["analysis_id", "title", "kind", "nodes"],
543
+ },
544
+ },
545
+ {
546
+ name: "delete_diagram",
547
+ description: "Delete a diagram from an analysis.",
548
+ inputSchema: {
549
+ type: "object",
550
+ properties: {
551
+ analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
552
+ diagram_id: { type: "string", description: "Id of the diagram, as shown by get_analysis." },
553
+ },
554
+ required: ["analysis_id", "diagram_id"],
555
+ },
556
+ },
557
+ {
558
+ name: "get_review_feedback",
559
+ description: "Read the reviewer's feedback once the review is submitted: decision, selected findings and notes, and the prompt to act on. " +
560
+ "Before submission it reports the review progress. Read-only. Always read the feedback before calling refresh_analysis, which discards it.",
561
+ inputSchema: {
562
+ type: "object",
563
+ properties: { analysis_id: { type: "string", description: ANALYSIS_ID_DOC } },
564
+ required: ["analysis_id"],
565
+ },
566
+ },
567
+ {
568
+ name: "refresh_analysis",
569
+ description: "Recompute the frozen diff with the same repository, mode and refs, after fixing the code. " +
570
+ "Files whose diff changed become unreviewed, findings whose lines changed become outdated, and the review restarts as pending: " +
571
+ "the previous feedback is discarded, so read it with get_review_feedback first.",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: { analysis_id: { type: "string", description: ANALYSIS_ID_DOC } },
575
+ required: ["analysis_id"],
576
+ },
577
+ },
578
+ {
579
+ name: "delete_analysis",
580
+ description: "Delete an analysis and its frozen diff. The repository is not touched. Also removes an analysis listed as unreadable.",
581
+ inputSchema: {
582
+ type: "object",
583
+ properties: { analysis_id: { type: "string", description: ANALYSIS_ID_DOC } },
584
+ required: ["analysis_id"],
585
+ },
586
+ },
587
+ reconnectGuiToolDefinition(),
588
+ ];
589
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
590
+ /** Notifie la GUI d'une mutation et ajoute le lien vers l'analyse au résultat. */
591
+ function afterMutation({ result, analysisId }) {
592
+ notifyAnalysisChanged(analysisId);
593
+ return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(analysisId) }] };
594
+ }
595
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
596
+ const { name, arguments: args } = request.params;
597
+ const params = (args ?? {});
598
+ try {
599
+ switch (name) {
600
+ case "list_analyses":
601
+ return listAnalyses(store, params);
602
+ case "get_analysis":
603
+ return getAnalysis(store, params);
604
+ case "get_diff":
605
+ return getDiff(store, params);
606
+ case "get_review_feedback":
607
+ return getReviewFeedback(store, params);
608
+ case "create_analysis": {
609
+ const mutation = await createAnalysis(store, params);
610
+ notifyAnalysesListChanged();
611
+ return afterMutation(mutation);
612
+ }
613
+ case "update_analysis":
614
+ return afterMutation(updateAnalysis(store, params));
615
+ case "add_findings":
616
+ return afterMutation(addFindings(store, params));
617
+ case "update_finding":
618
+ return afterMutation(updateFinding(store, params));
619
+ case "delete_findings":
620
+ return afterMutation(deleteFindings(store, params));
621
+ case "set_diagram":
622
+ return afterMutation(setDiagram(store, params));
623
+ case "delete_diagram":
624
+ return afterMutation(deleteDiagram(store, params));
625
+ case "refresh_analysis":
626
+ return afterMutation(await refreshAnalysis(store, params));
627
+ case "delete_analysis": {
628
+ const { result, analysisId } = deleteAnalysis(store, params);
629
+ notifyAnalysisChanged(analysisId);
630
+ notifyAnalysesListChanged();
631
+ return result;
632
+ }
633
+ case "reconnect_gui":
634
+ return await reconnectGui(params);
635
+ default:
636
+ throw new Error(`Tool not found: ${name}`);
637
+ }
638
+ }
639
+ catch (error) {
640
+ return errorResult(error.message);
641
+ }
642
+ });
643
+ async function main() {
644
+ const transport = new StdioServerTransport();
645
+ await server.connect(transport);
646
+ logger.info(`MCP Server connected and running on stdio (data dir: ${dataDir})`);
647
+ launchGUI();
648
+ }
649
+ main().catch((err) => {
650
+ logger.error(`Fatal error: ${err.message}`);
651
+ process.exit(1);
652
+ });
653
+ process.on("uncaughtException", (err) => {
654
+ logger.error(`Uncaught Exception in MASTER: ${err.message}`);
655
+ logger.error(err.stack || "No stack trace");
656
+ guiLauncher.getProcess()?.kill("SIGTERM");
657
+ process.exit(1);
658
+ });
@@ -0,0 +1,9 @@
1
+ import { type AnalysisStore } from "../core/analysis-store.js";
2
+ import { type Args, type MutationResult } from "./types.js";
3
+ export declare const FINDING_INPUT_FIELDS: readonly ["id", "severity", "kind", "title", "body", "path", "start_line", "end_line", "suggestion", "prompt"];
4
+ /**
5
+ * Ajoute un lot de constats. Tout le lot est validé (champs, identifiants,
6
+ * emplacements résolus sur le snapshot) avant l'écriture : un seul constat
7
+ * invalide et rien n'est ajouté.
8
+ */
9
+ export declare function addFindings(store: AnalysisStore, args: Args): MutationResult;