mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
feat(mcp-server): bring the web app's drawing knowledge to MCP
- Add a drawing guide adapted from the web system prompt (layout, edge routing, styles, minimal style, editing rules), returned by start_session, a new get_drawing_guide tool and the diagram-workflow prompt - Add get_shape_library with the 30 icon libraries; the build copies docs/shape-libraries into dist and CI checks the packed files - Accept bare mxCell lists in create_new_diagram and add_page; the server adds the wrapper and root cells - Send server instructions, shorten create_new_diagram's description to fit Claude Code's 2,048 character limit, and annotate every tool - Fix dead links and the totals in docs/shape-libraries/README.md
This commit is contained in:
@@ -62,6 +62,10 @@ jobs:
|
||||
if: steps.version.outputs.publish == 'true'
|
||||
run: npm test
|
||||
|
||||
- name: Build and check package contents
|
||||
if: steps.version.outputs.publish == 'true'
|
||||
run: npm run build && npm run check-package
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.version.outputs.publish == 'true'
|
||||
run: npm publish
|
||||
|
||||
@@ -38,6 +38,10 @@ jobs:
|
||||
- name: Run MCP server unit tests
|
||||
run: npm --prefix packages/mcp-server test
|
||||
|
||||
# Tests run from src/, so check the built npm package separately
|
||||
- name: Build MCP server and check package contents
|
||||
run: npm --prefix packages/mcp-server run build && npm --prefix packages/mcp-server run check-package
|
||||
|
||||
e2e:
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -11,7 +11,6 @@ Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||
| gcp2 | 297 | `mxgraph.gcp2` | Google Cloud Platform - Compute Engine, BigQuery, GKE, etc. | [gcp2.md](./gcp2.md) |
|
||||
| alibaba_cloud | 273 | `mxgraph.alibaba_cloud` | Alibaba Cloud - ECS, OSS, RDS, SLB, VPC, etc. | [alibaba_cloud.md](./alibaba_cloud.md) |
|
||||
| openstack | 18 | `mxgraph.openstack` | OpenStack cloud platform icons | [openstack.md](./openstack.md) |
|
||||
| digitalocean | 74 | `mxgraph.digitalocean` | DigitalOcean - Droplets, Spaces, Kubernetes, etc. | [digitalocean.md](./digitalocean.md) |
|
||||
| salesforce | 96 | `mxgraph.salesforce` | Salesforce platform icons | [salesforce.md](./salesforce.md) |
|
||||
|
||||
## Networking & Infrastructure
|
||||
@@ -20,7 +19,6 @@ Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||
|---------|--------|--------|-------------|------|
|
||||
| cisco19 | 232 | `mxgraph.cisco19` | Cisco network equipment - routers, switches, firewalls | [cisco19.md](./cisco19.md) |
|
||||
| network | 58 | `mxgraph.networks` | General network diagram symbols | [network.md](./network.md) |
|
||||
| arista | 45 | `mxgraph.arista` | Arista network switches and equipment | [arista.md](./arista.md) |
|
||||
| kubernetes | 40 | `mxgraph.kubernetes` | Kubernetes - pods, services, deployments, nodes | [kubernetes.md](./kubernetes.md) |
|
||||
| vvd | 93 | `mxgraph.vvd` | VMware Validated Design icons | [vvd.md](./vvd.md) |
|
||||
| rack | 11 | `mxgraph.rack` | Server rack and data center equipment | [rack.md](./rack.md) |
|
||||
@@ -30,7 +28,6 @@ Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||
| Library | Shapes | Prefix | Description | File |
|
||||
|---------|--------|--------|-------------|------|
|
||||
| bpmn | 39 | `mxgraph.bpmn` | Business Process Model and Notation - events, gateways, tasks | [bpmn.md](./bpmn.md) |
|
||||
| eip | 36 | `mxgraph.eip` | Enterprise Integration Patterns - messaging, routing | [eip.md](./eip.md) |
|
||||
| lean_mapping | 13 | `mxgraph.lean_mapping` | Lean/Value Stream Mapping symbols | [lean_mapping.md](./lean_mapping.md) |
|
||||
|
||||
## General Diagrams
|
||||
@@ -48,6 +45,7 @@ Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||
| Library | Shapes | Prefix | Description | File |
|
||||
|---------|--------|--------|-------------|------|
|
||||
| android | 17 | `mxgraph.android` | Android UI mockup components | [android.md](./android.md) |
|
||||
| material_design | 300 | `image=https://fonts.gstatic.com/...` | Google Material Icons (SVG images) | [material_design.md](./material_design.md) |
|
||||
|
||||
## Enterprise Software
|
||||
|
||||
@@ -73,6 +71,5 @@ Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||
| Library | Shapes | Prefix | Description | File |
|
||||
|---------|--------|--------|-------------|------|
|
||||
| webicons | 176 | `mxgraph.webicons` | Web/social media logos - GitHub, Twitter, AWS, etc. | [webicons.md](./webicons.md) |
|
||||
| un-ocha-icons | 242 | `mxgraph.un-ocha-icons` | UN OCHA humanitarian icons | [un-ocha-icons.md](./un-ocha-icons.md) |
|
||||
|
||||
**Total: 33 libraries, 4,281 shapes**
|
||||
**Total: 30 libraries, 4,184 shapes**
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.31.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
@@ -8,7 +8,8 @@
|
||||
"next-ai-drawio-mcp": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc && node scripts/copy-assets.mjs",
|
||||
"check-package": "node scripts/check-package.mjs",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Fail if the npm package would miss files the server reads at runtime.
|
||||
// Tests run from src/ (tsx) and cannot notice a broken dist/ copy step.
|
||||
// Run after `npm run build`.
|
||||
import { execSync } from "node:child_process"
|
||||
|
||||
const REQUIRED = ["dist/index.js", "dist/shape-libraries/aws4.md"]
|
||||
|
||||
const [pack] = JSON.parse(
|
||||
execSync("npm pack --dry-run --json", { encoding: "utf8" }),
|
||||
)
|
||||
const files = new Set(pack.files.map((f) => f.path))
|
||||
const missing = REQUIRED.filter((f) => !files.has(f))
|
||||
if (missing.length > 0) {
|
||||
console.error(`npm package is missing: ${missing.join(", ")}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`npm package OK (${files.size} files)`)
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copy non-TypeScript assets into dist/ after tsc, so they ship in the npm
|
||||
// package ("files": ["dist"]).
|
||||
import { cpSync, mkdirSync, readdirSync } from "node:fs"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const pkg = join(dirname(fileURLToPath(import.meta.url)), "..")
|
||||
|
||||
// Shape library docs live at the repository root, shared with the web app
|
||||
const libSrc = join(pkg, "../../docs/shape-libraries")
|
||||
const libDest = join(pkg, "dist/shape-libraries")
|
||||
mkdirSync(libDest, { recursive: true })
|
||||
for (const file of readdirSync(libSrc)) {
|
||||
if (file.endsWith(".md") && file !== "README.md") {
|
||||
cpSync(join(libSrc, file), join(libDest, file))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Drawing guide for the model, returned by start_session, get_drawing_guide
|
||||
* and the diagram-workflow prompt.
|
||||
*
|
||||
* Adapted from the web app's system prompt (lib/system-prompts.ts) and its
|
||||
* tool descriptions (app/api/chat/route.ts). When drawing rules change there,
|
||||
* update this file too.
|
||||
*/
|
||||
|
||||
export const DRAWING_GUIDE = `# Draw.io drawing guide
|
||||
|
||||
## Workflow
|
||||
- create_new_diagram draws a new diagram and REPLACES the whole document. add_page adds another tab. edit_diagram changes cells of an existing page. load_diagram opens a .drawio file (the server reads the file itself). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
|
||||
- Before drawing, describe your layout plan in 2-3 sentences, so shapes do not overlap and edges do not cross shapes.
|
||||
- Send XML only through tool calls, never in chat text. Never draw a box just to send the user a message.
|
||||
- Before using any icon library (AWS, Azure, GCP, Kubernetes, Cisco, BPMN, Material Design, web icons...), call get_shape_library and use the exact style names it returns. NEVER guess icon style names. For AWS, use the AWS 2025 icons (library aws4).
|
||||
- When replicating a diagram from an image, match its style and layout closely: straight or curved lines, rounded or square shapes.
|
||||
- The preview page has History (it saves a snapshot before every AI change and can restore any of the last 20 versions) and Download. You can make changes freely; nothing is lost.
|
||||
|
||||
## The XML you send
|
||||
Single page (create_new_diagram, add_page): send ONLY the mxCell elements. The server adds <mxfile>, <mxGraphModel>, <root> and the root cells id="0" and id="1".
|
||||
|
||||
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;entryX=0;entryY=0.5;endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
Several pages at once (create_new_diagram only): send a full <mxfile> with one <diagram id="..." name="..."> per page. Every page's <root> must start with <mxCell id="0"/><mxCell id="1" parent="0"/>.
|
||||
|
||||
Rules (XML that breaks them is rejected):
|
||||
1. All mxCell elements are siblings. NEVER nest an mxCell inside another mxCell.
|
||||
2. Ids are unique within a page and start from "2" ("0" and "1" are the root cells).
|
||||
3. parent="1" for top-level shapes, parent="<container id>" for shapes inside a container.
|
||||
4. Edge source and target must reference existing cell ids.
|
||||
5. Escape special characters in attribute values: < for <, > for >, & for &, " for ".
|
||||
6. NEVER include XML comments (<!-- -->). draw.io strips them.
|
||||
7. In tool arguments (JSON), every " inside the XML must be escaped as \\".
|
||||
|
||||
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>
|
||||
|
||||
## 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.
|
||||
- Containers (for example AWS cloud boxes) are at most 700 pixels wide and 550 pixels tall.
|
||||
- Start near x=40, y=40 and keep elements grouped closely.
|
||||
- For large diagrams, stack vertically or use a grid instead of spreading wide.
|
||||
|
||||
## Edge routing rules
|
||||
Rule 1: Never let two edges share a path. Two edges between the same nodes exit and enter at different points (exitY=0.3 for the first, exitY=0.7 for the second, not both 0.5).
|
||||
Rule 2: For bidirectional connections (A to B and B to A), use opposite sides: A exits right (exitX=1) into the left of B (entryX=0); B exits left (exitX=0) into the right of A (entryX=1).
|
||||
Rule 3: Always set exitX, exitY, entryX and entryY in the edge style, e.g. style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;".
|
||||
Rule 4: Route edges AROUND shapes in the way. Before drawing an edge, find every shape between source and target; if one is in the path, add waypoints. Route diagonal connections along the outside of the diagram, not through the middle. Keep 20-30px clearance from shapes. An edge must never cross another shape's box.
|
||||
Rule 5: Plan the layout first. Organize shapes into rows or columns following the flow, space them 150-200px apart so edges have room, and prefer one flow direction (left to right or top to bottom).
|
||||
Rule 6: Use 2-3 waypoints for L-shaped or U-shaped paths. Each change of direction needs a waypoint, and segments should be horizontal or vertical.
|
||||
Rule 7: Use natural connection points. Never connect at corners (both X and Y 0 or 1). Top-to-bottom flow: exitY=1 into entryY=0. Left-to-right flow: exitX=1 into entryX=0. Diagonal: the side closest to the target.
|
||||
|
||||
Before sending XML, check:
|
||||
1. Does any edge cross a shape that is not its source or target? Add waypoints.
|
||||
2. Do two edges share a path? Change their exit or entry points.
|
||||
3. Is any connection point at a corner? Use the middle of a side.
|
||||
4. Could moving shapes remove edge crossings? Revise the layout.
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
|
||||
## Styles
|
||||
- Shapes: rounded=1, fillColor=#hex, strokeColor=#hex, whiteSpace=wrap;html=1;
|
||||
- Edges: endArrow=classic, block, open or none; startArrow=none or classic; curved=1; edgeStyle=orthogonalEdgeStyle
|
||||
- Text: fontSize=14, fontStyle=1 (bold), align=center, left or right
|
||||
- Animated connectors: add flowAnimation=1 to the edge style.
|
||||
|
||||
## Minimal style
|
||||
When the user asks for a minimal, plain, black-and-white or unstyled diagram, use these rules instead of the styles above:
|
||||
- No fillColor, strokeColor, rounded, fontSize, fontStyle or hex colors.
|
||||
- Shapes use style "whiteSpace=wrap;html=1;", edges use "html=1;endArrow=classic;".
|
||||
- Containers that hold other shapes use "whiteSpace=wrap;html=1;fillColor=none;" so they do not cover their children.
|
||||
- Keep at least 50px between elements, and follow all edge routing rules strictly.
|
||||
|
||||
## Editing with edit_diagram
|
||||
- update replaces a cell: send the complete mxCell including mxGeometry, with the same id as cell_id.
|
||||
- add inserts a new cell with a new id. One cell per operation.
|
||||
- delete removes a cell. Its children and every edge connected to it are deleted too, so give only the container's id.
|
||||
- All-or-nothing: if any operation fails, nothing is applied. A rejected call includes the current XML of the page; rebuild your operations on it and retry.
|
||||
- If the diagram is large, change it with edit_diagram instead of redrawing it.
|
||||
|
||||
{"operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"New Label\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
||||
{"page_name": "CNN", "operations": [{"operation": "add", "cell_id": "conv-1", "new_xml": "<mxCell id=\\"conv-1\\" value=\\"Conv\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"40\\" y=\\"40\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
||||
{"page_index": 1, "operations": [{"operation": "delete", "cell_id": "5"}]}
|
||||
|
||||
Pages: list_pages shows every page's id, name and index. edit_diagram, get_diagram and export_diagram take an optional page_id, page_name or page_index; without one they use the first page.
|
||||
`
|
||||
+120
-107
@@ -26,6 +26,7 @@ import open from "open"
|
||||
import { z } from "zod"
|
||||
import type { DiagramOperation } from "./diagram-operations.js"
|
||||
import { installDomPolyfill } from "./dom.js"
|
||||
import { DRAWING_GUIDE } from "./drawing-guide.js"
|
||||
import { editDiagram, targetPageXml } from "./edit-diagram.js"
|
||||
import { checkEditGate } from "./edit-gate.js"
|
||||
import { addHistory } from "./history.js"
|
||||
@@ -51,7 +52,9 @@ import {
|
||||
projectPage,
|
||||
renamePageInDoc,
|
||||
serializeMxfile,
|
||||
wrapCellsInModel,
|
||||
} from "./pages.js"
|
||||
import { getShapeLibrary, SHAPE_LIBRARY_GROUPS } from "./shape-library.js"
|
||||
import { validateAndFixXml } from "./xml-validation.js"
|
||||
|
||||
// DOMParser/XMLSerializer globals for the XML helpers (Node has neither)
|
||||
@@ -83,10 +86,29 @@ let currentSession: {
|
||||
const require = createRequire(import.meta.url)
|
||||
const packageVersion: string = require("../package.json").version
|
||||
|
||||
const server = new McpServer({
|
||||
name: "next-ai-drawio",
|
||||
version: packageVersion,
|
||||
})
|
||||
// Hosts truncate instructions (Claude Code at 2,048 characters) and may show
|
||||
// only the first 512, so the essentials come first. The full rules are in
|
||||
// DRAWING_GUIDE, returned by start_session.
|
||||
const INSTRUCTIONS = `next-ai-drawio creates and edits draw.io diagrams and shows them live in a browser preview, where the user can also edit them by hand.
|
||||
|
||||
Start with start_session: it opens the preview and its result contains the drawing guide (layout, edge routing and style rules). Follow the guide when drawing; call get_drawing_guide if it is no longer in your context.
|
||||
|
||||
Before using cloud or icon shapes (AWS, Azure, GCP, Kubernetes, Cisco, BPMN...), call get_shape_library and use the exact style names it returns. Never guess icon style names.
|
||||
|
||||
Tools:
|
||||
- create_new_diagram: draw a new diagram, replacing the whole document. Send only the mxCell elements of one page (the server adds the wrapper and root cells), or a full <mxfile> for several pages.
|
||||
- edit_diagram: add, update or delete cells of an existing page by id. All-or-nothing; a rejected call includes the current XML so you can retry.
|
||||
- get_diagram: read the current XML, including the user's manual edits.
|
||||
- load_diagram, export_diagram: open or save .drawio files and export .png or .svg. Use absolute paths.
|
||||
- list_pages, add_page, rename_page, delete_page: manage pages (tabs).`
|
||||
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "next-ai-drawio",
|
||||
version: packageVersion,
|
||||
},
|
||||
{ instructions: INSTRUCTIONS },
|
||||
)
|
||||
|
||||
// Shared Zod schema fragment for page-targeting parameters.
|
||||
// Every multi-page-aware tool reuses these three optional fields so the LLM
|
||||
@@ -149,7 +171,7 @@ function describeSelector(s: PageSelector): string {
|
||||
return "first page"
|
||||
}
|
||||
|
||||
// Register prompt with workflow guidance
|
||||
// The same guide as start_session, for hosts that show prompts to the user
|
||||
server.registerPrompt(
|
||||
"diagram-workflow",
|
||||
{
|
||||
@@ -159,49 +181,67 @@ server.registerPrompt(
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `# Draw.io Diagram Workflow Guidelines
|
||||
|
||||
## Creating a New Diagram
|
||||
1. Call start_session to open the browser preview
|
||||
2. Use create_new_diagram with either a bare <mxGraphModel> (single page) or a full <mxfile> with one or more <diagram> children (multi-page)
|
||||
|
||||
## Opening an Existing .drawio File
|
||||
- Use load_diagram with the file path — the server reads and decompresses the file itself; don't read it and pass the XML through create_new_diagram
|
||||
- After loading, call get_diagram once before editing (you haven't seen the file's cell IDs yet)
|
||||
|
||||
## Working with Multiple Pages
|
||||
- Use list_pages to discover existing pages (id, name, index)
|
||||
- Use add_page to append a new page (without losing existing ones — unlike create_new_diagram which REPLACES everything)
|
||||
- Use rename_page / delete_page for management
|
||||
- edit_diagram, get_diagram, and export_diagram all accept optional page_id / page_name / page_index — when omitted they target the first page
|
||||
|
||||
## Editing a Page (add / update / delete cells)
|
||||
1. Call edit_diagram with your operations, optionally with a page selector
|
||||
2. If you don't know the current cell IDs or structure, call get_diagram first
|
||||
3. For add/update, provide the cell_id and complete mxCell XML
|
||||
4. No need to call get_diagram before every edit: the server rejects the edit (with no side effects) if the user changed the diagram in the browser since you last saw it, and tells you to call get_diagram once and retry
|
||||
|
||||
## Important Notes
|
||||
- create_new_diagram REPLACES the entire document, including ALL pages - only use for new diagrams. Use add_page to add a tab without losing existing content.
|
||||
- edit_diagram PRESERVES the user's manual changes: it is rejected with the current XML when the user edited the diagram since you last saw it
|
||||
- Always use unique cell_ids within a page (cell ids "0" and "1" are reserved root sentinels and can repeat across pages)`,
|
||||
},
|
||||
content: { type: "text", text: DRAWING_GUIDE },
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// Tool: get_drawing_guide
|
||||
server.registerTool(
|
||||
"get_drawing_guide",
|
||||
{
|
||||
title: "Get drawing guide",
|
||||
description:
|
||||
"Return the drawing guide: XML format, layout, edge routing, style and editing rules. " +
|
||||
"start_session already returns it; call this only if the guide is no longer in your context.",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: true, openWorldHint: false },
|
||||
},
|
||||
async () => ({ content: [{ type: "text", text: DRAWING_GUIDE }] }),
|
||||
)
|
||||
|
||||
// Tool: get_shape_library
|
||||
server.registerTool(
|
||||
"get_shape_library",
|
||||
{
|
||||
title: "Get shape library",
|
||||
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")}`,
|
||||
inputSchema: {
|
||||
library: z
|
||||
.string()
|
||||
.describe("Library name, e.g. aws4, kubernetes, flowchart"),
|
||||
},
|
||||
annotations: { readOnlyHint: true, openWorldHint: false },
|
||||
},
|
||||
async ({ library }) => {
|
||||
const found = await getShapeLibrary(library)
|
||||
return found.ok
|
||||
? { content: [{ type: "text", text: found.text }] }
|
||||
: {
|
||||
content: [{ type: "text", text: `Error: ${found.error}` }],
|
||||
isError: true,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: start_session
|
||||
server.registerTool(
|
||||
"start_session",
|
||||
{
|
||||
title: "Start session",
|
||||
description:
|
||||
"Start a new diagram session and open the browser for real-time preview. " +
|
||||
"Starts an embedded server and opens a browser window with draw.io. " +
|
||||
"The browser will show diagram updates as they happen.",
|
||||
"The browser will show diagram updates as they happen. " +
|
||||
"The result includes the drawing guide; follow it when drawing.",
|
||||
inputSchema: {},
|
||||
annotations: { destructiveHint: false, openWorldHint: false },
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
@@ -227,7 +267,7 @@ server.registerTool(
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`,
|
||||
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.\n\n${DRAWING_GUIDE}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -247,72 +287,26 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"create_new_diagram",
|
||||
{
|
||||
description: `Create a NEW diagram from XML. ONLY use this when creating a diagram from scratch.
|
||||
title: "Create new diagram",
|
||||
description: `Create a NEW diagram, REPLACING the whole document: every page and any unsaved user changes (the previous state stays in History). To add a tab use add_page; to change cells use edit_diagram.
|
||||
|
||||
⚠️ DESTRUCTIVE: This tool REPLACES the entire document, INCLUDING every existing page/tab and any unsaved user changes. To add a tab without losing existing content, use add_page instead. To modify cells on an existing page, use edit_diagram.
|
||||
Before using icon shapes (AWS, Azure, GCP, Kubernetes, Cisco...), call get_shape_library first. Follow the drawing guide returned by start_session (call get_drawing_guide if it is no longer in your context).
|
||||
|
||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
||||
Accepted xml:
|
||||
1) Only the mxCell elements of one page (recommended). The server adds <mxfile>, <mxGraphModel>, <root> and the root cells "0" and "1":
|
||||
<mxCell id="2" value="Shape" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell>
|
||||
2) A bare <mxGraphModel> with <root> (one page).
|
||||
3) A full <mxfile> with one or more <diagram> pages. Every page's <root> must start with <mxCell id="0"/><mxCell id="1" parent="0"/>.
|
||||
|
||||
When to use this tool:
|
||||
- Creating a new diagram from scratch (no existing diagram, or wanting to wipe and start over)
|
||||
- The user explicitly asks to "start over" or "create a new diagram"
|
||||
|
||||
When to use add_page instead:
|
||||
- The user wants ANOTHER tab/page alongside what's already there (e.g. "add a CNN diagram on a new page")
|
||||
|
||||
When to use edit_diagram instead:
|
||||
- ANY modifications to an existing page's cells (add/remove/move shapes, change labels, etc.)
|
||||
|
||||
ACCEPTED XML SHAPES:
|
||||
|
||||
1) Bare mxGraphModel (single-page, legacy):
|
||||
<mxGraphModel>
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="Shape" style="rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
The server auto-wraps this in <mxfile><diagram id="..." name="Page-1">...</diagram></mxfile>.
|
||||
|
||||
2) Full mxfile (one or more pages):
|
||||
<mxfile host="app.diagrams.net">
|
||||
<diagram id="page-1" name="Architecture">
|
||||
<mxGraphModel><root>...</root></mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="page-2" name="Sequence">
|
||||
<mxGraphModel><root>...</root></mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Each <diagram> becomes a tab in the embedded editor. Cell ids "0" and "1" are reserved root sentinels and MUST repeat in every page's <root>.
|
||||
|
||||
LAYOUT CONSTRAINTS (per page):
|
||||
- Keep all elements within x=0-800, y=0-600 (single page viewport)
|
||||
- Start from margins (x=40, y=40), keep elements grouped closely
|
||||
- Use unique IDs starting from "2" within each page (0 and 1 are reserved)
|
||||
- Set parent="1" for top-level shapes
|
||||
- Space shapes 150-200px apart for clear edge routing
|
||||
|
||||
EDGE ROUTING RULES:
|
||||
- Never let multiple edges share the same path - use different exitY/entryY values
|
||||
- For bidirectional connections (A↔B), use OPPOSITE sides
|
||||
- Always specify exitX, exitY, entryX, entryY explicitly in edge style
|
||||
- Route edges AROUND obstacles using waypoints (add 20-30px clearance)
|
||||
- Use natural connection points based on flow (not corners)
|
||||
|
||||
COMMON STYLES:
|
||||
- Shapes: rounded=1; fillColor=#hex; strokeColor=#hex
|
||||
- Edges: endArrow=classic; edgeStyle=orthogonalEdgeStyle; curved=1
|
||||
- Text: fontSize=14; fontStyle=1 (bold); align=center`,
|
||||
Rules: cells are siblings (never nested), ids are unique per page and start from "2", parent="1" for top-level shapes, no XML comments, and shapes stay within x 0 to 800 and y 0 to 600.`,
|
||||
inputSchema: {
|
||||
xml: z
|
||||
.string()
|
||||
.describe(
|
||||
"REQUIRED: Either a complete <mxGraphModel> (legacy single-page) or a full <mxfile> with one or more <diagram> children (multi-page).",
|
||||
"REQUIRED: the mxCell elements of one page, a bare <mxGraphModel>, or a full <mxfile> with one or more <diagram> pages.",
|
||||
),
|
||||
},
|
||||
annotations: { openWorldHint: false },
|
||||
},
|
||||
async ({ xml: inputXml }) => {
|
||||
try {
|
||||
@@ -328,8 +322,10 @@ COMMON STYLES:
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and auto-fix XML (works for both mxfile and mxGraphModel inputs).
|
||||
let xml = inputXml
|
||||
// Bare cells get the wrapper and root cells first: the strict
|
||||
// parser rejects several top-level elements. Then validate and
|
||||
// auto-fix (works for both mxfile and mxGraphModel inputs).
|
||||
let xml = wrapCellsInModel(inputXml)
|
||||
const { valid, error, fixed, fixes } = validateAndFixXml(xml)
|
||||
if (fixed) {
|
||||
xml = fixed
|
||||
@@ -357,7 +353,7 @@ COMMON STYLES:
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: XML must be either a <mxGraphModel> or an <mxfile> with one or more <diagram> children.",
|
||||
text: "Error: XML must be the mxCell elements of one page, a <mxGraphModel>, or an <mxfile> with one or more <diagram> children.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
@@ -432,6 +428,7 @@ COMMON STYLES:
|
||||
server.registerTool(
|
||||
"load_diagram",
|
||||
{
|
||||
title: "Load .drawio file",
|
||||
description:
|
||||
"Load a .drawio file from disk into the current session, REPLACING the entire diagram (all pages). " +
|
||||
"The server reads the file directly — you do NOT need to read the file yourself or pass its XML through create_new_diagram. " +
|
||||
@@ -444,6 +441,7 @@ server.registerTool(
|
||||
"Absolute path to the .drawio file to load (e.g. /Users/me/diagram.drawio or ~/diagram.drawio). Relative paths resolve against the MCP server's working directory, which is often not your project.",
|
||||
),
|
||||
},
|
||||
annotations: { openWorldHint: false },
|
||||
},
|
||||
async ({ path }) => {
|
||||
try {
|
||||
@@ -549,6 +547,7 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"edit_diagram",
|
||||
{
|
||||
title: "Edit diagram",
|
||||
description:
|
||||
"Edit a specific page in the current diagram by ID-based operations (update/add/delete cells).\n\n" +
|
||||
"All-or-nothing: if any operation fails, nothing is applied and every failure is listed.\n\n" +
|
||||
@@ -563,16 +562,13 @@ server.registerTool(
|
||||
"- page_id / page_name / page_index are optional; when all omitted, the FIRST page is targeted\n" +
|
||||
"- Use list_pages to discover what pages exist\n\n" +
|
||||
"Operations:\n" +
|
||||
"- add: Add a new cell. Provide cell_id (new unique id within the page) and new_xml.\n" +
|
||||
"- add: Add a new cell. Provide cell_id (new unique id within the page) and new_xml. One cell per operation.\n" +
|
||||
"- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
|
||||
"- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
|
||||
"For add/update, new_xml must be a complete mxCell element including mxGeometry.\n\n" +
|
||||
"- delete: Remove a cell by its id. Only cell_id is needed. Its children and connected edges are deleted too, so give only a container's id.\n\n" +
|
||||
"For add/update, new_xml must be a complete mxCell element including mxGeometry. No XML comments. " +
|
||||
'Every " inside new_xml must be escaped as \\" in the JSON.\n\n' +
|
||||
"Example - Add a rectangle on the default (first) page:\n" +
|
||||
'{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}\n\n' +
|
||||
"Example - Add a cell on a specific page by name:\n" +
|
||||
'{"page_name": "CNN", "operations": [{"operation": "add", "cell_id": "conv-1", "new_xml": "<mxCell id=\\"conv-1\\" ... />"}]}\n\n' +
|
||||
"Example - Update a cell on page index 1:\n" +
|
||||
'{"page_index": 1, "operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" .../>"}]}\n\n' +
|
||||
"Example - Delete a cell on the default page:\n" +
|
||||
'{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}',
|
||||
inputSchema: {
|
||||
@@ -585,7 +581,11 @@ server.registerTool(
|
||||
.describe(
|
||||
"Operation to perform: add, update, or delete",
|
||||
),
|
||||
cell_id: z.string().describe("The id of the mxCell"),
|
||||
cell_id: z
|
||||
.string()
|
||||
.describe(
|
||||
"The id of the mxCell. Must match the id attribute in new_xml.",
|
||||
),
|
||||
new_xml: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -596,6 +596,7 @@ server.registerTool(
|
||||
)
|
||||
.describe("Array of operations to apply"),
|
||||
},
|
||||
annotations: { openWorldHint: false },
|
||||
},
|
||||
async ({ operations, page_id, page_name, page_index }) => {
|
||||
try {
|
||||
@@ -744,6 +745,7 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"get_diagram",
|
||||
{
|
||||
title: "Get diagram",
|
||||
description:
|
||||
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
|
||||
"Call this when you don't know the current diagram content (cell IDs, pages, structure) — " +
|
||||
@@ -753,6 +755,7 @@ server.registerTool(
|
||||
inputSchema: {
|
||||
...pageSelectorSchema,
|
||||
},
|
||||
annotations: { readOnlyHint: true, openWorldHint: false },
|
||||
},
|
||||
async (input) => {
|
||||
// Defensive: when every field is optional an MCP client could in
|
||||
@@ -915,6 +918,7 @@ function exportViaBrowser(
|
||||
server.registerTool(
|
||||
"export_diagram",
|
||||
{
|
||||
title: "Export diagram",
|
||||
description:
|
||||
"Export the current diagram to a file. Supports .drawio (XML), .png, and .svg formats. " +
|
||||
"The format is auto-detected from the file extension, or can be specified explicitly.\n\n" +
|
||||
@@ -937,6 +941,7 @@ server.registerTool(
|
||||
"Export format. If omitted, detected from file extension. Defaults to drawio.",
|
||||
),
|
||||
},
|
||||
annotations: { openWorldHint: false },
|
||||
},
|
||||
async ({ path: rawPath, format, page_id, page_name, page_index }) => {
|
||||
const path = expandHome(rawPath)
|
||||
@@ -1222,9 +1227,11 @@ async function loadMxfileForMutation(): Promise<
|
||||
server.registerTool(
|
||||
"list_pages",
|
||||
{
|
||||
title: "List pages",
|
||||
description:
|
||||
"List every page (tab) in the current diagram. Returns each page's id, name, 0-based index, and cell count. Use this to discover what pages exist before targeting one with edit_diagram, get_diagram, export_diagram, rename_page, or delete_page.",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: true, openWorldHint: false },
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
@@ -1276,12 +1283,13 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"add_page",
|
||||
{
|
||||
title: "Add page",
|
||||
description:
|
||||
'Append a new page (tab) to the current diagram WITHOUT touching existing pages or unsaved user changes. Use this when the user wants "another diagram alongside" — e.g. "add a CNN page" — instead of create_new_diagram which wipes everything.\n\n' +
|
||||
"Inputs:\n" +
|
||||
"- name: optional display name for the tab (defaults to Page-N where N = existing-page-count + 1)\n" +
|
||||
"- id: optional explicit page id; if omitted the server generates a short alphanumeric id\n" +
|
||||
'- xml: optional starting <mxGraphModel> for the new page. If omitted, the page starts blank with the standard root sentinel cells ("0" and "1").\n\n' +
|
||||
'- xml: optional starting content: the mxCell elements of the page (root cells "0" and "1" are added), or a bare <mxGraphModel>. If omitted, the page starts blank.\n\n' +
|
||||
"Returns the new page's id, name, and index so the caller can immediately target it with edit_diagram.",
|
||||
inputSchema: {
|
||||
name: z
|
||||
@@ -1301,9 +1309,10 @@ server.registerTool(
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional starting <mxGraphModel> XML for the new page. Must include <root> with id="0" and id="1" cells. If omitted the page starts blank.',
|
||||
"Optional starting content: the mxCell elements of the page, or a bare <mxGraphModel>. If omitted the page starts blank.",
|
||||
),
|
||||
},
|
||||
annotations: { destructiveHint: false, openWorldHint: false },
|
||||
},
|
||||
async (input) => {
|
||||
// All three fields optional — coalesce so a no-args call doesn't
|
||||
@@ -1322,7 +1331,7 @@ server.registerTool(
|
||||
|
||||
// If caller provided XML, validate it before splicing it in so we
|
||||
// never get a half-broken mxfile written to the session.
|
||||
let cleanXml: string | undefined = xml
|
||||
let cleanXml: string | undefined = xml && wrapCellsInModel(xml)
|
||||
if (cleanXml) {
|
||||
const { valid, error, fixed, fixes } =
|
||||
validateAndFixXml(cleanXml)
|
||||
@@ -1384,6 +1393,7 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"rename_page",
|
||||
{
|
||||
title: "Rename page",
|
||||
description:
|
||||
"Rename an existing page (tab). At least one of page_id / page_name / page_index is required to identify which page to rename. The new_name becomes the visible tab label in the editor.",
|
||||
inputSchema: {
|
||||
@@ -1393,6 +1403,7 @@ server.registerTool(
|
||||
.min(1)
|
||||
.describe("The new display name for the page tab."),
|
||||
},
|
||||
annotations: { destructiveHint: false, openWorldHint: false },
|
||||
},
|
||||
async ({ new_name, page_id, page_name, page_index }) => {
|
||||
try {
|
||||
@@ -1464,11 +1475,13 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"delete_page",
|
||||
{
|
||||
title: "Delete page",
|
||||
description:
|
||||
"Delete a page (tab) from the current diagram. At least one of page_id / page_name / page_index is required. Refuses to delete the last remaining page — the editor needs at least one tab.",
|
||||
inputSchema: {
|
||||
...pageSelectorSchema,
|
||||
},
|
||||
annotations: { openWorldHint: false },
|
||||
},
|
||||
async (input) => {
|
||||
// All three fields are optional — coalesce so a no-args call returns
|
||||
|
||||
@@ -81,6 +81,37 @@ function stripXmlDeclaration(xml: string): string {
|
||||
return xml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "")
|
||||
}
|
||||
|
||||
const ROOT_CELLS = '<mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
|
||||
/**
|
||||
* Turn a list of bare cells (optionally inside <root>) into a one-page
|
||||
* <mxGraphModel>, adding the "0" and "1" root cells. The model then only
|
||||
* writes its own cells, as in the web app (wrapWithMxFile in lib/utils.ts).
|
||||
* Root cells the model wrote anyway are replaced, and trailing closing tags
|
||||
* some providers append are dropped. <mxfile>, <mxGraphModel> and anything
|
||||
* else are returned unchanged.
|
||||
*/
|
||||
export function wrapCellsInModel(xml: string): string {
|
||||
let content = stripXmlDeclaration(xml.trim())
|
||||
if (!/^<(mxCell|UserObject|object|root)[\s/>]/.test(content)) return xml
|
||||
|
||||
content = content.replace(/<\/?root>/g, "").trim()
|
||||
// End of the last cell, counting wrapped cells (</UserObject>, </object>)
|
||||
let end = -1
|
||||
for (const close of ["/>", "</mxCell>", "</UserObject>", "</object>"]) {
|
||||
const at = content.lastIndexOf(close)
|
||||
if (at !== -1) end = Math.max(end, at + close.length)
|
||||
}
|
||||
if (end !== -1 && /^(\s*<\/[^>]+>)*\s*$/.test(content.slice(end))) {
|
||||
content = content.slice(0, end)
|
||||
}
|
||||
content = content
|
||||
.replace(/<mxCell[^>]*\bid=["']0["'][^>]*(?:\/>|><\/mxCell>)/g, "")
|
||||
.replace(/<mxCell[^>]*\bid=["']1["'][^>]*(?:\/>|><\/mxCell>)/g, "")
|
||||
.trim()
|
||||
return `<mxGraphModel><root>${ROOT_CELLS}${content}</root></mxGraphModel>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a bare <mxGraphModel> XML string in <mxfile><diagram>...</diagram></mxfile>.
|
||||
* If the input is already an mxfile, returns it unchanged.
|
||||
@@ -255,7 +286,7 @@ export function addPageToDoc(
|
||||
}
|
||||
inner = trimmed
|
||||
} else {
|
||||
inner = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>`
|
||||
inner = `<mxGraphModel><root>${ROOT_CELLS}</root></mxGraphModel>`
|
||||
}
|
||||
|
||||
const snippet = `<wrapper><diagram id="${escapeAttr(id)}" name="${escapeAttr(name)}">${inner}</diagram></wrapper>`
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Shape and icon library docs (docs/shape-libraries/*.md), the same files
|
||||
* the web app's get_shape_library tool reads (app/api/chat/route.ts).
|
||||
*/
|
||||
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { dirname, join, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
export const SHAPE_LIBRARY_GROUPS: Record<string, string[]> = {
|
||||
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"],
|
||||
}
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
export async function getShapeLibrary(
|
||||
name: string,
|
||||
): Promise<{ ok: true; text: string } | { ok: false; error: string }> {
|
||||
const library = name.trim().toLowerCase()
|
||||
if (!LIBRARIES.has(library)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Library "${name}" not found. Available: ${Array.from(LIBRARIES).join(", ")}`,
|
||||
}
|
||||
}
|
||||
for (const dir of LIBRARY_DIRS) {
|
||||
try {
|
||||
return {
|
||||
ok: true,
|
||||
text: await readFile(join(dir, `${library}.md`), "utf-8"),
|
||||
}
|
||||
} catch {
|
||||
// Try the next location
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: `Library "${library}" is missing from this installation.`,
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,13 @@ const EXPECTED_TOOLS = [
|
||||
"add_page",
|
||||
"rename_page",
|
||||
"delete_page",
|
||||
"get_drawing_guide",
|
||||
"get_shape_library",
|
||||
]
|
||||
|
||||
// Claude Code truncates tool descriptions and server instructions here
|
||||
const MAX_DESCRIPTION = 2048
|
||||
|
||||
let proc: ChildProcessWithoutNullStreams
|
||||
let stdoutBuf = ""
|
||||
const pending = new Map<
|
||||
@@ -48,6 +53,7 @@ const pending = new Map<
|
||||
{ resolve: (m: any) => void; reject: (e: Error) => void; timeout: any }
|
||||
>()
|
||||
let nextId = 1
|
||||
let initResp: any
|
||||
|
||||
function send(method: string, params: unknown, isNotification = false) {
|
||||
const msg: Record<string, unknown> = { jsonrpc: "2.0", method, params }
|
||||
@@ -92,7 +98,7 @@ beforeAll(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const initResp = await send("initialize", {
|
||||
initResp = await send("initialize", {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "wiring-test", version: "0.0.0" },
|
||||
@@ -107,7 +113,7 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
describe("MCP server wiring", () => {
|
||||
it("registers all ten tools", async () => {
|
||||
it("registers all tools", async () => {
|
||||
const resp = await send("tools/list", {})
|
||||
expect(resp.error, JSON.stringify(resp.error)).toBeUndefined()
|
||||
const names: string[] = (resp.result?.tools ?? []).map(
|
||||
@@ -139,4 +145,45 @@ describe("MCP server wiring", () => {
|
||||
expect(props.id).toBeTruthy()
|
||||
expect(props.xml).toBeTruthy()
|
||||
})
|
||||
|
||||
it("keeps every description and the instructions within the host limit", async () => {
|
||||
const resp = await send("tools/list", {})
|
||||
for (const tool of resp.result.tools) {
|
||||
expect(
|
||||
tool.description.length,
|
||||
`${tool.name} description length`,
|
||||
).toBeLessThanOrEqual(MAX_DESCRIPTION)
|
||||
}
|
||||
const instructions: string = initResp.result.instructions
|
||||
expect(instructions).toContain("start_session")
|
||||
expect(instructions.length).toBeLessThanOrEqual(MAX_DESCRIPTION)
|
||||
})
|
||||
|
||||
it("serves a shape library without a session", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "get_shape_library",
|
||||
arguments: { library: "AWS4" },
|
||||
})
|
||||
expect(resp.result.isError).toBeFalsy()
|
||||
expect(resp.result.content[0].text).toContain("mxgraph.aws4")
|
||||
})
|
||||
|
||||
it("reports an unknown shape library with the available names", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "get_shape_library",
|
||||
arguments: { library: "../secrets" },
|
||||
})
|
||||
expect(resp.result.isError).toBe(true)
|
||||
expect(resp.result.content[0].text).toContain("kubernetes")
|
||||
})
|
||||
|
||||
it("serves the drawing guide without a session", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "get_drawing_guide",
|
||||
arguments: {},
|
||||
})
|
||||
const text: string = resp.result.content[0].text
|
||||
expect(text).toContain("Edge routing rules")
|
||||
expect(text.length).toBeLessThanOrEqual(15000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Tests for wrapCellsInModel: the model may send only the mxCell elements of
|
||||
* a page, like in the web app, and the server adds the wrapper and root cells.
|
||||
*/
|
||||
|
||||
import { beforeAll, describe, expect, it } from "vitest"
|
||||
import { installDomPolyfill } from "../src/dom.js"
|
||||
|
||||
beforeAll(() => {
|
||||
installDomPolyfill()
|
||||
})
|
||||
|
||||
import { wrapCellsInModel } from "../src/pages.js"
|
||||
import { validateAndFixXml } from "../src/xml-validation.js"
|
||||
|
||||
const A = `<mxCell id="2" value="A" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
||||
const B = `<mxCell id="3" value="B" vertex="1" parent="1"><mxGeometry x="200" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
||||
const ROOTS = `<mxCell id="0"/><mxCell id="1" parent="0"/>`
|
||||
|
||||
describe("wrapCellsInModel", () => {
|
||||
it("wraps sibling cells so they pass validation", () => {
|
||||
expect(validateAndFixXml(A + B).valid).toBe(false)
|
||||
const wrapped = wrapCellsInModel(A + B)
|
||||
expect(wrapped).toBe(
|
||||
`<mxGraphModel><root>${ROOTS}${A}${B}</root></mxGraphModel>`,
|
||||
)
|
||||
expect(validateAndFixXml(wrapped).valid).toBe(true)
|
||||
})
|
||||
|
||||
it("replaces root cells the model wrote itself", () => {
|
||||
const wrapped = wrapCellsInModel(
|
||||
`<mxCell id="0"></mxCell><mxCell id="1" parent="0"/>${A}`,
|
||||
)
|
||||
expect(wrapped.match(/id="0"/g)).toHaveLength(1)
|
||||
expect(wrapped.match(/id="1"/g)).toHaveLength(1)
|
||||
expect(wrapped).toContain(A)
|
||||
})
|
||||
|
||||
it("unwraps a <root> and drops trailing provider tags", () => {
|
||||
const wrapped = wrapCellsInModel(
|
||||
`<root>${A}</root></invoke></function_calls>`,
|
||||
)
|
||||
expect(wrapped).toBe(
|
||||
`<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`,
|
||||
)
|
||||
})
|
||||
|
||||
it("leaves <mxGraphModel> and <mxfile> input unchanged", () => {
|
||||
const model = `<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`
|
||||
const file = `<mxfile><diagram id="p" name="P">${model}</diagram></mxfile>`
|
||||
expect(wrapCellsInModel(model)).toBe(model)
|
||||
expect(wrapCellsInModel(file)).toBe(file)
|
||||
})
|
||||
|
||||
it("keeps a UserObject cell at the end", () => {
|
||||
const wrapped = `<UserObject id="u" label="U"><mxCell vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></UserObject>`
|
||||
expect(wrapCellsInModel(A + wrapped)).toContain(wrapped)
|
||||
expect(wrapCellsInModel(`${wrapped}</invoke>`)).toBe(
|
||||
`<mxGraphModel><root>${ROOTS}${wrapped}</root></mxGraphModel>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user