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
+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")