feat(mcp-server): drawing guide hints and the user's own drawing rules (#980)

* feat(mcp-server): add source-reading and clear-canvas hints to the drawing guide

Carry the missing web-prompt sentences into the MCP drawing guide: draw from
a document, image or web page by reading it yourself first, compare image
replications with screenshot_diagram, keep replies short after a successful
draw, compose artistic requests from standard shapes, and clear the canvas by
sending only the two root cells. Replace the "If the diagram is large" bullet
with guidance on choosing create_new_diagram vs edit_diagram.

The server INSTRUCTIONS mention reading files yourself and opening .drawio
files with load_diagram; the create_new_diagram description documents the
clear-canvas call, and its handler reports "Canvas cleared" when the prepared
XML holds only the root cells. Add tests for both and the matching README
lines in the mcp-server and claude-plugin packages.

* feat(mcp-server): append the user's instructions.md to the drawing guide

Read <DRAWIO_DATA_DIR>/instructions.md (default ~/.next-ai-drawio) on
every call and append its first 5000 characters to the drawing guide
under a "## Custom Instructions" heading, for start_session,
get_drawing_guide and the diagram-workflow prompt. start_session now
tells the model where the file lives. Document the feature in both
READMEs and cover it with unit and server-wiring tests.

* fix(mcp-server): review fixes for drawing guide text and custom instructions file

* fix(mcp-server): Codex review fixes for the guide and custom instructions

- Report "Canvas cleared" only for a one-page document; several empty
  pages get the page summary
- Build the instructions.md path with path.join (Windows separators)
- Keep the 15000-character guide budget in the wiring test
- Plugin README: DRAWIO_DATA_DIR also holds instructions.md
This commit is contained in:
Dayuan Jiang
2026-10-11 20:04:59 +09:00
committed by GitHub
parent 3d1a020b35
commit c774a39c33
8 changed files with 252 additions and 8 deletions
@@ -0,0 +1,58 @@
/**
* The user's own drawing rules, kept in a Markdown file and appended to the
* drawing guide, like the web app's custom system message
* (app/api/chat/route.ts). The file is read on every call, so edits apply
* without restarting the MCP host.
*/
import { readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import { log } from "./logger.ts"
import { expandHome } from "./persistence.ts"
export const CUSTOM_INSTRUCTIONS_FILE = "instructions.md"
// Same cap as the web app's custom system message
const MAX_CHARS = 5000
/**
* DRAWIO_DATA_DIR, default ~/.next-ai-drawio. "off" only turns auto-save
* off; this file is merely read, so the default folder is used then.
*/
export function customInstructionsDir(): string {
const dir = process.env.DRAWIO_DATA_DIR
return dir && dir !== "off"
? expandHome(dir)
: join(homedir(), ".next-ai-drawio")
}
/** Full path of the instructions file. */
export function customInstructionsPath(dir = customInstructionsDir()): string {
return join(dir, CUSTOM_INSTRUCTIONS_FILE)
}
let warned = false
/** The file's content, trimmed and capped; "" when there is none. */
export function readCustomInstructions(dir = customInstructionsDir()): string {
const path = customInstructionsPath(dir)
try {
return readFileSync(path, "utf-8").trim().slice(0, MAX_CHARS)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !warned) {
warned = true
log.warn(`Could not read the custom instructions ${path}: ${error}`)
}
return ""
}
}
/** The guide with the user's rules appended under "## Custom Instructions". */
export function guideWithCustomInstructions(
guide: string,
dir?: string,
): string {
const text = readCustomInstructions(dir)
return text ? `${guide}\n\n## Custom Instructions\n${text}` : guide
}
+6 -2
View File
@@ -23,7 +23,11 @@ export const DRAWING_GUIDE = `# Draw.io drawing guide
- 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).
- After drawing or heavily editing a complex diagram, call screenshot_diagram once to see the result, and fix overlapping shapes and edges that cross shapes.
- When replicating a diagram from an image, match its style and layout closely: straight or curved lines, rounded or square shapes.
- Drawing from a source: to draw from a document (PDF, Markdown, code, data), an image or screenshot, or a web page, first read it yourself (the attachment the user shared, or your own file-reading or web-fetch tools), then plan the layout and draw. This server never receives attachments; it only receives the XML you send. Extract the entities and relationships the diagram needs; do not copy the text into boxes.
- When replicating a diagram from an image, match its style and layout closely: straight or curved lines, rounded or square shapes, colors and relative positions. Call screenshot_diagram afterwards and compare with the original.
- After a successful create_new_diagram or edit_diagram call, do not describe the diagram; the user sees it in the preview. One short sentence at most.
- For artistic requests (a cat, a logo, a scene), compose the picture from standard shapes and connectors while keeping it clear.
- To clear the canvas to one blank page, call create_new_diagram with only the two root cells <mxCell id="0"/><mxCell id="1" parent="0"/>; the previous diagram stays in History.
- 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
@@ -100,7 +104,7 @@ When the user asks for a minimal, plain, black-and-white or unstyled diagram, us
- 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.
- Use create_new_diagram for a new diagram, a major restructuring or an empty canvas. Use edit_diagram for small changes: adding or removing a few elements, labels, colors or positions. Never redraw a large diagram for a small change.
{"operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"New Label\\" style=\\"rounded=1;\\" x=\\"100\\" y=\\"100\\" w=\\"120\\" h=\\"60\\"/>"}]}
{"page_name": "CNN", "operations": [{"operation": "add", "cell_id": "conv-1", "new_xml": "<mxCell id=\\"conv-1\\" value=\\"Conv\\" x=\\"40\\" y=\\"40\\" w=\\"120\\" h=\\"60\\"/>"}]}
+30 -4
View File
@@ -24,6 +24,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import open from "open"
import { z } from "zod"
import { expandCompactCells, foldCells } from "./compact-cells.ts"
import {
customInstructionsPath,
guideWithCustomInstructions,
} from "./custom-instructions.ts"
import type { DiagramOperation } from "./diagram-operations.ts"
import { installDomPolyfill } from "./dom.ts"
import { DRAWING_GUIDE } from "./drawing-guide.ts"
@@ -132,6 +136,8 @@ Start with start_session: it opens the preview and its result contains the drawi
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.
To draw from a file, image or web page, read it yourself first; the server only receives XML. Open existing .drawio files with load_diagram.
After drawing a complex diagram, call screenshot_diagram once to see it, and fix overlapping shapes or edges that cross shapes.
Tools:
@@ -216,6 +222,10 @@ function describeSelector(s: PageSelector): string {
return "first page"
}
// The drawing guide plus the user's own rules from instructions.md, read
// on every call so edits apply without restarting the host
const guideText = () => guideWithCustomInstructions(DRAWING_GUIDE)
// The same guide as start_session, for hosts that show prompts to the user
server.registerPrompt(
"diagram-workflow",
@@ -226,7 +236,7 @@ server.registerPrompt(
messages: [
{
role: "user",
content: { type: "text", text: DRAWING_GUIDE },
content: { type: "text", text: guideText() },
},
],
}),
@@ -243,7 +253,7 @@ server.registerTool(
inputSchema: {},
annotations: { readOnlyHint: true, openWorldHint: false },
},
async () => ({ content: [{ type: "text", text: DRAWING_GUIDE }] }),
async () => ({ content: [{ type: "text", text: guideText() }] }),
)
// Tool: get_shape_library
@@ -308,6 +318,7 @@ registerWriteTool(
const saveNote = savePath
? `\n\nAuto-save: after every change the diagram is saved to ${savePath}. To continue it in a later conversation, call start_session, then load_diagram with this path.`
: ""
const rulesNote = `\n\nYour own drawing rules: write them in ${customInstructionsPath()} (Markdown, up to 5000 characters, read on every call).`
log.info(`Started session ${sessionId}, browser at ${browserUrl}`)
@@ -315,7 +326,7 @@ registerWriteTool(
content: [
{
type: "text",
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.${saveNote}\n\n${DRAWING_GUIDE}`,
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.${saveNote}${rulesNote}\n\n${guideText()}`,
},
],
}
@@ -346,7 +357,9 @@ Accepted xml:
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"/>.
Rules: cells are siblings (never nested), ids are unique per page and start from "2", parent only for shapes inside a container, no XML comments, and shapes stay within x 0 to 800 and y 0 to 600. A style used by several cells is defined once with <mxStyle name="..." value="..."/> before the cells and used by name (see the drawing guide); html=1 and whiteSpace=wrap are added automatically.`,
Rules: cells are siblings (never nested), ids are unique per page and start from "2", parent only for shapes inside a container, no XML comments, and shapes stay within x 0 to 800 and y 0 to 600. A style used by several cells is defined once with <mxStyle name="..." value="..."/> before the cells and used by name (see the drawing guide); html=1 and whiteSpace=wrap are added automatically.
To clear the canvas to one blank page, send only the two root cells <mxCell id="0"/><mxCell id="1" parent="0"/>; the previous diagram stays in History.`,
inputSchema: {
xml: z
.string()
@@ -429,6 +442,19 @@ Rules: cells are siblings (never nested), ids are unique per page and start from
log.info(`Diagram content set successfully (${pageSummary})`)
// Only the root cells: the model asked for an empty canvas
// (several empty pages get the page summary below)
if (!hasCells(xml) && pages.length <= 1) {
return {
content: [
{
type: "text",
text: "Canvas cleared: one blank page. The previous diagram is in History.",
},
],
}
}
return {
content: [
{