Skip to content

Commit 5bef9f4

Browse files
aodecipherveelenga
andauthored
fix: propagate mermaid-cli stderr in rendering errors (#83)
* fix: propagate mermaid-cli stderr in rendering errors When mermaid-cli fails, execFileAsync throws with `.stderr` containing the actual parse error details (e.g., "Parse error on line 3..."), but only the generic `error.message` ("Command failed: npx ...") was surfaced to the MCP response. This made it impossible for Claude to self-correct Mermaid syntax errors. Extract stderr from the caught error and include it in the thrown Error message so that detailed diagnostics reach the MCP client. * fix: improve catch handler, add tests, fix formatting --------- Co-authored-by: Vitalii Elenhaupt <velenhaupt@gmail.com>
1 parent 0971454 commit 5bef9f4

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

src/handlers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,13 @@ export async function renderDiagram(options: RenderOptions, liveFilePath: string
7474
await copyFile(outputFile, liveFilePath);
7575
mcpLogger.info(`Diagram rendered successfully: ${previewId}`);
7676
} catch (error) {
77-
mcpLogger.error(`Diagram rendering failed: ${previewId}`, {
78-
error: error instanceof Error ? error.message : String(error),
79-
});
80-
throw error;
77+
const stderr =
78+
error instanceof Error && "stderr" in error && (error as Error & { stderr: string }).stderr
79+
? `\n${(error as Error & { stderr: string }).stderr}`
80+
: "";
81+
const message = error instanceof Error ? error.message : String(error);
82+
mcpLogger.error(`Diagram rendering failed: ${previewId}`, { error: message });
83+
throw new Error(`${message}${stderr}`);
8184
}
8285
}
8386

test/handlers.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { handleMermaidPreview, handleMermaidSave } from "../src/handlers.js";
33
import { getPreviewDir, getDiagramFilePath } from "../src/file-utils.js";
44
import { mkdir, readdir, unlink, access, mkdtemp } from "fs/promises";
55
import { join } from "path";
6+
import { execFile } from "child_process";
67
import { tmpdir } from "os";
78

89
// Mock execFile to avoid actually running mmdc and create fake output files
@@ -172,6 +173,52 @@ describe("handleMermaidPreview", () => {
172173

173174
expect(result.content[0].text).toContain("Live preview is only available for SVG");
174175
});
176+
177+
it("should include stderr details in error when rendering fails", async () => {
178+
const mockExecFile = vi.mocked(execFile);
179+
const originalImpl = mockExecFile.getMockImplementation()!;
180+
181+
try {
182+
mockExecFile.mockImplementation((_file: string, _args: any, callback: any) => {
183+
const error: any = new Error("Command failed: npx mmdc");
184+
error.stderr = "Parse error on line 3: invalid syntax near 'graph'";
185+
callback(error, { stdout: "", stderr: error.stderr });
186+
});
187+
188+
const result = await handleMermaidPreview({
189+
diagram: "invalid diagram syntax",
190+
preview_id: testPreviewId,
191+
});
192+
193+
expect(result.isError).toBe(true);
194+
expect(result.content[0].text).toContain("Parse error on line 3");
195+
expect(result.content[0].text).toContain("Command failed");
196+
} finally {
197+
mockExecFile.mockImplementation(originalImpl);
198+
}
199+
});
200+
201+
it("should show original error message when stderr is empty", async () => {
202+
const mockExecFile = vi.mocked(execFile);
203+
const originalImpl = mockExecFile.getMockImplementation()!;
204+
205+
try {
206+
mockExecFile.mockImplementation((_file: string, _args: any, callback: any) => {
207+
const error = new Error("Command failed: npx mmdc");
208+
callback(error, { stdout: "", stderr: "" });
209+
});
210+
211+
const result = await handleMermaidPreview({
212+
diagram: "invalid diagram syntax",
213+
preview_id: testPreviewId,
214+
});
215+
216+
expect(result.isError).toBe(true);
217+
expect(result.content[0].text).toContain("Command failed");
218+
} finally {
219+
mockExecFile.mockImplementation(originalImpl);
220+
}
221+
});
175222
});
176223

177224
describe("handleMermaidSave", () => {

0 commit comments

Comments
 (0)