Skip to content

Commit 3a65bca

Browse files
author
Sébastien MORAND
committed
feat: add handDrawn look option for sketch-style rendering
Mermaid v11 supports a 'look' setting that switches diagrams to a sketchy, Excalidraw-style rendering via rough.js. This exposes it as an optional parameter on the MCP tool so users can request that style from their AI assistant without having to embed a frontmatter config block. - Add 'look' field to the Zod schema (enum: classic, handDrawn; default classic) - Pass it through renderMermaid into mermaidConfig - Propagate it to the mermaid.ink payload for svg_url / png_url outputs - Update tool snapshot fixture and add unit tests for createMermaidInkUrl Backwards compatible: default 'classic' preserves current behavior.
1 parent aef3807 commit 3a65bca

6 files changed

Lines changed: 73 additions & 3 deletions

File tree

__tests__/tools/mermaid.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"description": "Theme for the diagram (optional). Default is 'default'.",
1616
"default": "default"
1717
},
18+
"look": {
19+
"type": "string",
20+
"enum": ["classic", "handDrawn"],
21+
"description": "Visual style for the diagram (optional). 'classic' is the default Mermaid look; 'handDrawn' produces a sketchy, Excalidraw-like rendering (Mermaid v11+).",
22+
"default": "classic"
23+
},
1824
"backgroundColor": {
1925
"type": "string",
2026
"description": "Background color for the diagram (optional). Default is 'white'.",

__tests__/utils/mermaidUrl.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { inflateSync } from "node:zlib";
2+
import { describe, expect, it } from "vitest";
3+
import { createMermaidInkUrl } from "../../src/utils/mermaidUrl";
4+
5+
function decodePayload(url: string): {
6+
code: string;
7+
mermaid: { theme: string; look: string };
8+
} {
9+
const encoded = url.split("pako:")[1];
10+
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
11+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
12+
const buffer = Buffer.from(padded, "base64");
13+
return JSON.parse(inflateSync(buffer).toString("utf-8"));
14+
}
15+
16+
describe("createMermaidInkUrl", () => {
17+
const sample = "graph TD;A-->B;";
18+
19+
it("defaults to classic look when not specified", () => {
20+
const url = createMermaidInkUrl(sample, "img");
21+
expect(url.startsWith("https://mermaid.ink/img/pako:")).toBe(true);
22+
const payload = decodePayload(url);
23+
expect(payload.mermaid.theme).toBe("default");
24+
expect(payload.mermaid.look).toBe("classic");
25+
expect(payload.code).toBe(sample);
26+
});
27+
28+
it("propagates handDrawn look into the mermaid.ink payload", () => {
29+
const url = createMermaidInkUrl(sample, "svg", "forest", "handDrawn");
30+
expect(url.startsWith("https://mermaid.ink/svg/pako:")).toBe(true);
31+
const payload = decodePayload(url);
32+
expect(payload.mermaid.theme).toBe("forest");
33+
expect(payload.mermaid.look).toBe("handDrawn");
34+
});
35+
36+
it("uses the requested variant in the URL path", () => {
37+
const svgUrl = createMermaidInkUrl(sample, "svg");
38+
const imgUrl = createMermaidInkUrl(sample, "img");
39+
expect(svgUrl).toMatch(/^https:\/\/mermaid\.ink\/svg\//);
40+
expect(imgUrl).toMatch(/^https:\/\/mermaid\.ink\/img\//);
41+
});
42+
});

src/server.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,23 @@ function setupToolHandlers(server: McpServer): void {
5959
);
6060
}
6161

62-
const { mermaid, theme, backgroundColor, outputType = "base64" } = args;
62+
const {
63+
mermaid,
64+
theme,
65+
look,
66+
backgroundColor,
67+
outputType = "base64",
68+
} = args;
6369
Logger.info(
64-
`Rendering diagram (outputType=${outputType}, theme=${theme ?? "default"})`,
70+
`Rendering diagram (outputType=${outputType}, theme=${theme ?? "default"}, look=${look ?? "classic"})`,
6571
);
6672
const { id, svg, screenshot } = await withRetry(
6773
() =>
6874
renderMermaid(
6975
mermaid as string,
7076
theme as string,
7177
backgroundColor as string,
78+
look as "classic" | "handDrawn" | undefined,
7279
),
7380
{ maxAttempts: 3, delayMs: 500 },
7481
);
@@ -99,6 +106,7 @@ function setupToolHandlers(server: McpServer): void {
99106
mermaid as string,
100107
variant,
101108
(theme as string) || "default",
109+
(look as "classic" | "handDrawn" | undefined) || "classic",
102110
);
103111
return {
104112
content: [

src/tools/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ C-->D;.`)
1515
.describe("Theme for the diagram (optional). Default is 'default'.")
1616
.optional()
1717
.default("default"),
18+
look: z
19+
.enum(["classic", "handDrawn"])
20+
.describe(
21+
"Visual style for the diagram (optional). 'classic' is the default Mermaid look; 'handDrawn' produces a sketchy, Excalidraw-like rendering (Mermaid v11+).",
22+
)
23+
.optional()
24+
.default("classic"),
1825
backgroundColor: z
1926
.string()
2027
.describe(

src/utils/mermaidUrl.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ export function createMermaidInkUrl(
2121
mermaid: string,
2222
variant: "svg" | "img",
2323
theme = "default",
24+
look: "classic" | "handDrawn" = "classic",
2425
): string {
25-
const payload = JSON.stringify({ code: mermaid, mermaid: { theme } });
26+
const payload = JSON.stringify({
27+
code: mermaid,
28+
mermaid: { theme, look },
29+
});
2630
const encoded = encodeMermaidToBase64Url(payload);
2731
return `https://mermaid.ink/${variant}/pako:${encoded}`;
2832
}

src/utils/render.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export async function renderMermaid(
2020
mermaid: string,
2121
theme = "default",
2222
backgroundColor = "white",
23+
look: "classic" | "handDrawn" = "classic",
2324
): Promise<RenderResult> {
2425
if (!renderer) renderer = createMermaidRenderer();
2526
const cssContent = `svg { background: ${backgroundColor}; }`;
@@ -33,6 +34,8 @@ export async function renderMermaid(
3334
mermaidConfig: {
3435
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
3536
theme: theme as any,
37+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
38+
look: look as any,
3639
},
3740
});
3841
const r0 = r[0] as PromiseSettledResult<RenderResult>;

0 commit comments

Comments
 (0)