diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
index ce75d158..8f987f34 100644
--- a/app/api/chat/route.ts
+++ b/app/api/chat/route.ts
@@ -8,7 +8,6 @@ import {
stepCountIs,
streamText,
} from "ai"
-import fs from "fs/promises"
import { jsonrepair } from "jsonrepair"
import path from "path"
import { z } from "zod"
@@ -50,6 +49,11 @@ import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
import { getSystemPrompt } from "@/lib/system-prompts"
import { getUserIdFromRequest } from "@/lib/user-id"
import { hasCells } from "@/packages/mcp-server/src/pages.ts"
+import {
+ getShapeLibrary,
+ SHAPE_LIBRARY_LIST,
+} from "@/packages/mcp-server/src/shape-library.ts"
+import { SWIMLANE_EXAMPLE } from "@/packages/mcp-server/src/xml-examples.ts"
// No explicit cap: a reasoning model can spend minutes planning before it emits
// the tool call, so take whatever the host allows. Vercel's own default is 300s,
@@ -609,21 +613,7 @@ VALIDATION RULES (XML will be rejected if violated):
6. Escape special chars in values: < > & "
Example (generate ONLY this - no wrapper tags):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+${SWIMLANE_EXAMPLE}
Notes:
- For AWS diagrams, use **AWS 2025 icons**.
@@ -701,14 +691,7 @@ Example: If previous output ended with '
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+${SWIMLANE_EXAMPLE}
\`\`\`
### append_diagram Details
@@ -318,12 +307,7 @@ If cell_id not found, check "Current diagram XML" for correct IDs. Use display_d
### Two edges between same nodes (CORRECT - no overlap):
\`\`\`xml
-
-
-
-
-
-
+${TWO_EDGES_EXAMPLE}
\`\`\`
### Edge with single waypoint (simple detour):
@@ -342,14 +326,7 @@ If cell_id not found, check "Current diagram XML" for correct IDs. Use display_d
**WRONG:** Direct diagonal line crosses over Develop
**CORRECT:** Route around the OUTSIDE (go right first, then up)
\`\`\`xml
-
-
-
-
-
-
-
-
+${WAYPOINT_EXAMPLE}
\`\`\`
This routes the edge to the RIGHT of all shapes (x=750), then enters Main from the right side.
diff --git a/next.config.ts b/next.config.ts
index 466596a4..366b1351 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -10,9 +10,11 @@ const nextConfig: NextConfig = {
env: {
APP_VERSION: packageJson.version,
},
- // Include instrumentation.ts in standalone build for Langfuse telemetry
+ // Include instrumentation.ts in standalone build for Langfuse telemetry,
+ // and the shape library docs get_shape_library reads at run time
outputFileTracingIncludes: {
"*": ["./instrumentation.ts"],
+ "/api/chat": ["./docs/shape-libraries/*.md"],
},
}
diff --git a/packages/mcp-server/src/drawing-guide.ts b/packages/mcp-server/src/drawing-guide.ts
index b44b5f79..4265a64e 100644
--- a/packages/mcp-server/src/drawing-guide.ts
+++ b/packages/mcp-server/src/drawing-guide.ts
@@ -7,6 +7,13 @@
* update this file too.
*/
+import {
+ indent,
+ SWIMLANE_EXAMPLE,
+ TWO_EDGES_EXAMPLE,
+ WAYPOINT_EXAMPLE,
+} from "./xml-examples.ts"
+
export const DRAWING_GUIDE = `# Draw.io drawing guide
## Workflow
@@ -41,21 +48,7 @@ Rules (XML that breaks them is rejected):
Containers and swimlanes: children use the container id as parent and coordinates relative to the container.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+${indent(SWIMLANE_EXAMPLE)}
## Layout
- Keep every element of a page within x 0 to 800 and y 0 to 600, so the whole diagram fits one view without a page break.
@@ -80,23 +73,11 @@ Before sending XML, check:
Two edges between the same nodes:
-
-
-
-
-
-
+${indent(TWO_EDGES_EXAMPLE)}
Waypoints go inside in the edge geometry. Example: Hotfix (right, bottom) connects to Main (center, top) while Develop (center, middle) is in between, so the edge goes right to x=750 first, then up, and enters Main from the right:
-
-
-
-
-
-
-
-
+${indent(WAYPOINT_EXAMPLE)}
## Styles
- Shapes: rounded=1, fillColor=#hex, strokeColor=#hex, whiteSpace=wrap;html=1;
diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts
index aa58e170..34693158 100644
--- a/packages/mcp-server/src/index.ts
+++ b/packages/mcp-server/src/index.ts
@@ -62,7 +62,7 @@ import {
wrapCellsInModel,
} from "./pages.ts"
import { Autosaver, defaultDataDir } from "./persistence.ts"
-import { getShapeLibrary, SHAPE_LIBRARY_GROUPS } from "./shape-library.ts"
+import { getShapeLibrary, SHAPE_LIBRARY_LIST } from "./shape-library.ts"
import { validateAndFixXml } from "./xml-validation.ts"
// DOMParser/XMLSerializer globals for the XML helpers (Node has neither)
@@ -224,9 +224,7 @@ server.registerTool(
description:
"Get the style syntax and shape names of a draw.io icon library. Call this BEFORE drawing with " +
"cloud, network or other icon shapes, and use the exact names it returns; never guess them.\n\n" +
- `Libraries:\n${Object.entries(SHAPE_LIBRARY_GROUPS)
- .map(([group, names]) => `- ${group}: ${names.join(", ")}`)
- .join("\n")}`,
+ `Libraries:\n${SHAPE_LIBRARY_LIST}`,
inputSchema: {
library: z
.string()
diff --git a/packages/mcp-server/src/shape-library.ts b/packages/mcp-server/src/shape-library.ts
index 11dff479..dc1d935c 100644
--- a/packages/mcp-server/src/shape-library.ts
+++ b/packages/mcp-server/src/shape-library.ts
@@ -25,18 +25,29 @@ export const SHAPE_LIBRARY_GROUPS: Record = {
Icons: ["webicons"],
}
+/** The libraries by group, one "- Group: a, b" line each, for tool descriptions */
+export const SHAPE_LIBRARY_LIST = Object.entries(SHAPE_LIBRARY_GROUPS)
+ .map(([group, names]) => `- ${group}: ${names.join(", ")}`)
+ .join("\n")
+
const LIBRARIES = new Set(Object.values(SHAPE_LIBRARY_GROUPS).flat())
-const here = dirname(fileURLToPath(import.meta.url))
-// The build copies the docs to dist/shape-libraries; running from src (tsx)
-// reads them from the repository instead.
-const LIBRARY_DIRS = [
- join(here, "shape-libraries"),
- resolve(here, "../../../docs/shape-libraries"),
-]
+/**
+ * The build copies the docs to dist/shape-libraries; running from src (tsx)
+ * reads them from the repository instead. The web app passes its own folder.
+ */
+function libraryDirs(dir?: string): string[] {
+ if (dir) return [dir]
+ const here = dirname(fileURLToPath(import.meta.url))
+ return [
+ join(here, "shape-libraries"),
+ resolve(here, "../../../docs/shape-libraries"),
+ ]
+}
export async function getShapeLibrary(
name: string,
+ dir?: string,
): Promise<{ ok: true; text: string } | { ok: false; error: string }> {
const library = name.trim().toLowerCase()
if (!LIBRARIES.has(library)) {
@@ -45,11 +56,11 @@ export async function getShapeLibrary(
error: `Library "${name}" not found. Available: ${Array.from(LIBRARIES).join(", ")}`,
}
}
- for (const dir of LIBRARY_DIRS) {
+ for (const folder of libraryDirs(dir)) {
try {
return {
ok: true,
- text: await readFile(join(dir, `${library}.md`), "utf-8"),
+ text: await readFile(join(folder, `${library}.md`), "utf-8"),
}
} catch {
// Try the next location
diff --git a/packages/mcp-server/src/xml-examples.ts b/packages/mcp-server/src/xml-examples.ts
new file mode 100644
index 00000000..f1758693
--- /dev/null
+++ b/packages/mcp-server/src/xml-examples.ts
@@ -0,0 +1,43 @@
+/**
+ * XML examples shared by the MCP drawing guide and the web app's system
+ * prompt and tool descriptions, so both teach the model the same thing.
+ */
+
+export const SWIMLANE_EXAMPLE = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+export const TWO_EDGES_EXAMPLE = `
+
+
+
+
+`
+
+export const WAYPOINT_EXAMPLE = `
+
+
+
+
+
+
+`
+
+/** Indent every line, for an indented code block in Markdown */
+export const indent = (text: string, prefix = " ") =>
+ text
+ .split("\n")
+ .map((line) => prefix + line)
+ .join("\n")