refactor(web): share prompt examples and the shape library with the MCP server

- The three XML examples (swimlanes, two edges, waypoints) that the web
  system prompt, the display_diagram description and the MCP drawing
  guide each had a copy of now live in packages/mcp-server/src/xml-examples.ts
- The shape library group list and reader come from the MCP
  shape-library.ts; getShapeLibrary takes the folder, and the web route
  passes docs/shape-libraries under the working directory as before.
  Only the 30 known library names are read. Error texts differ slightly
- next.config.ts traces docs/shape-libraries/*.md for /api/chat, since
  the read now happens in another module
- Every prompt is byte-for-byte unchanged: the web system prompt for five
  model ids with and without minimal style, and the MCP drawing guide,
  compared before and after; the library list and swimlane example match
  the old tool description text exactly
This commit is contained in:
dayuan.jiang
2026-10-04 12:58:24 +09:00
parent bf4e95dcfb
commit d32eb25523
7 changed files with 99 additions and 137 deletions
+12 -62
View File
@@ -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):
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
${SWIMLANE_EXAMPLE}
Notes:
- For AWS diagrams, use **AWS 2025 icons**.
@@ -701,14 +691,7 @@ Example: If previous output ended with '<mxCell id="x" style="rounded=1', contin
description: `Get draw.io shape/icon library documentation with style syntax and shape names.
Available libraries:
- Cloud: aws4, azure2, gcp2, alibaba_cloud, openstack, salesforce
- Networking: cisco19, network, kubernetes, vvd, rack
- Business: bpmn, lean_mapping
- General: flowchart, basic, arrows2, infographic, sitemap
- UI/Mockups: android, material_design
- Enterprise: citrix, sap, mscae, atlassian
- Engineering: fluidpower, electrical, pid, cabinets, floorplan
- Icons: webicons
${SHAPE_LIBRARY_LIST}
Call this tool to get shape names and usage syntax for a specific library.`,
inputSchema: z.object({
@@ -719,45 +702,12 @@ Call this tool to get shape names and usage syntax for a specific library.`,
),
}),
execute: async ({ library }) => {
// Sanitize input - prevent path traversal attacks
const sanitizedLibrary = library
.toLowerCase()
.replace(/[^a-z0-9_-]/g, "")
if (sanitizedLibrary !== library.toLowerCase()) {
return `Invalid library name "${library}". Use only letters, numbers, underscores, and hyphens.`
}
const baseDir = path.join(
process.cwd(),
"docs/shape-libraries",
// Only known library names reach the file system
const result = await getShapeLibrary(
library,
path.join(process.cwd(), "docs/shape-libraries"),
)
const filePath = path.join(
baseDir,
`${sanitizedLibrary}.md`,
)
// Verify path stays within expected directory
const resolvedPath = path.resolve(filePath)
if (!resolvedPath.startsWith(path.resolve(baseDir))) {
return `Invalid library path.`
}
try {
const content = await fs.readFile(filePath, "utf-8")
return content
} catch (error) {
if (
(error as NodeJS.ErrnoException).code === "ENOENT"
) {
return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, material_design, lean_mapping, openstack, rack`
}
console.error(
`[get_shape_library] Error loading "${library}":`,
error,
)
return `Error loading library "${library}". Please try again.`
}
return result.ok ? result.text : result.error
},
},
},
+9 -32
View File
@@ -1,11 +1,14 @@
/**
* System prompts for different AI models
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
*
* Token counting utilities are in a separate file (token-counter.ts) to avoid
* WebAssembly issues with Next.js server-side rendering.
*/
import {
SWIMLANE_EXAMPLE,
TWO_EDGES_EXAMPLE,
WAYPOINT_EXAMPLE,
} from "@/packages/mcp-server/src/xml-examples.ts"
// Default system prompt (~1900 tokens) - works with all models
export const DEFAULT_SYSTEM_PROMPT = `
You are an expert diagram creation assistant specializing in draw.io XML generation.
@@ -241,21 +244,7 @@ const EXTENDED_ADDITIONS = `
**Example with swimlanes and edges** (generate ONLY this - no wrapper tags):
\`\`\`xml
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
${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
<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
${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
<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="750" y="80"/>
<mxPoint x="750" y="150"/>
</Array>
</mxGeometry>
</mxCell>
${WAYPOINT_EXAMPLE}
\`\`\`
This routes the edge to the RIGHT of all shapes (x=750), then enters Main from the right side.
+3 -1
View File
@@ -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"],
},
}
+10 -29
View File
@@ -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.
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
${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:
<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
${indent(TWO_EDGES_EXAMPLE)}
Waypoints go inside <Array as="points"> 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:
<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="750" y="80"/>
<mxPoint x="750" y="150"/>
</Array>
</mxGeometry>
</mxCell>
${indent(WAYPOINT_EXAMPLE)}
## Styles
- Shapes: rounded=1, fillColor=#hex, strokeColor=#hex, whiteSpace=wrap;html=1;
+2 -4
View File
@@ -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()
+20 -9
View File
@@ -25,18 +25,29 @@ export const SHAPE_LIBRARY_GROUPS: Record<string, string[]> = {
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
+43
View File
@@ -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 = `<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`
export const TWO_EDGES_EXAMPLE = `<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`
export const WAYPOINT_EXAMPLE = `<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="750" y="80"/>
<mxPoint x="750" y="150"/>
</Array>
</mxGeometry>
</mxCell>`
/** 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")