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