mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
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:
@@ -27,11 +27,13 @@ claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||
- **Real-time Preview**: Diagrams appear and update in your browser as Claude creates them
|
||||
- **Drawing Rules and Shape Libraries**: Claude gets the web app's layout and style rules and the icon docs for AWS, Azure, GCP, Kubernetes and more
|
||||
- **Self-check**: Claude can take a screenshot of the rendered diagram and fix what looks wrong
|
||||
- **Draw from Your Files**: ask Claude to draw from a document, image or web page; it reads the source with its own tools and draws. Existing .drawio files open with load_diagram
|
||||
- **Version History**: Restore one of the last 20 versions from the **History** button, shown as thumbnails
|
||||
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
||||
- **Edit Support**: Modify existing diagrams with natural language instructions, including your own edits in the browser
|
||||
- **Export**: Save diagrams as `.drawio`, `.png`, `.svg`, or `.drawio.svg` files
|
||||
- **Auto-save**: Each diagram is saved to `~/.next-ai-drawio/`, so `claude --resume` can pick it up again
|
||||
- **Custom Instructions**: keep your own drawing rules in `~/.next-ai-drawio/instructions.md` (for example "Always draw in minimal style"); they are appended to the drawing guide on every call
|
||||
- **Self-contained**: Embedded server, no external dependencies required
|
||||
|
||||
## Use Case Examples
|
||||
@@ -71,6 +73,18 @@ Create a sequence diagram showing OAuth 2.0 authorization code flow
|
||||
between user, client app, auth server, and resource server
|
||||
```
|
||||
|
||||
### 6. Draw From a Document
|
||||
|
||||
```
|
||||
Read docs/architecture.md and draw the system as a diagram
|
||||
```
|
||||
|
||||
### 7. Recreate a Sketch
|
||||
|
||||
```
|
||||
Recreate the whiteboard photo at ~/Desktop/sketch.jpg as a clean draw.io diagram
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
@@ -103,7 +117,7 @@ Claude Code <--stdio--> MCP Server <--http--> Browser (draw.io)
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||
| `DRAWIO_BASE_URL` | `https://embed.diagrams.net` | Base URL for draw.io (for self-hosted deployments) |
|
||||
| `DRAWIO_DATA_DIR` | `~/.next-ai-drawio` | Folder for auto-saved diagrams; `off` turns auto-save off |
|
||||
| `DRAWIO_DATA_DIR` | `~/.next-ai-drawio` | Folder for auto-saved diagrams and your `instructions.md`; `off` turns auto-save off (`instructions.md` is then read from the default folder) |
|
||||
| `DEBUG` | unset | Set to `true` to log debug messages |
|
||||
|
||||
## Links
|
||||
|
||||
@@ -99,6 +99,8 @@ Use the standard MCP configuration with:
|
||||
1. Restart your MCP client after updating config
|
||||
2. Ask the AI to create a diagram:
|
||||
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||
|
||||
> "Read docs/architecture.md and draw the system as a diagram"
|
||||
3. The diagram appears in your browser in real-time!
|
||||
|
||||
## Features
|
||||
@@ -106,12 +108,14 @@ Use the standard MCP configuration with:
|
||||
- **Real-time Preview**: Diagrams appear and update in your browser as the AI creates them
|
||||
- **Drawing Rules**: The AI gets the same layout, edge and style rules as the web app, plus the shape library docs (AWS, Azure, GCP, Kubernetes, Cisco and more), so it uses real icon names instead of guessing
|
||||
- **Self-check**: The AI can take a screenshot of the rendered diagram and fix overlapping shapes or edges that cross shapes
|
||||
- **Draw from Your Files**: ask the AI to draw from a document, image or web page; it reads the source with the host's own tools and draws. Existing .drawio files open with load_diagram
|
||||
- **Edit Support**: Modify existing diagrams with natural language instructions. If any change in an edit fails, nothing is written and the AI gets the reason and the current page XML
|
||||
- **Your Edits Are Kept**: Changes you make in the browser are read before the AI edits again. If the AI overwrites a change you were still making, your version is saved in History
|
||||
- **Version History**: Click **History** at the top right of the preview page to restore one of the last 20 versions, shown as thumbnails
|
||||
- **Download and Export**: Save as `.drawio`, `.png`, `.svg`, or `.drawio.svg` (an SVG with the diagram embedded, which draw.io can open and edit again), from the **Download** button or through `export_diagram`
|
||||
- **Multi-page**: List, add, rename, and delete pages, and edit any page
|
||||
- **Auto-save**: Each session's diagram is saved to `~/.next-ai-drawio/<session-id>.drawio`, so it survives a restart of the MCP client
|
||||
- **Custom Instructions**: keep your own drawing rules in `~/.next-ai-drawio/instructions.md` (for example "Always draw in minimal style"); they are appended to the drawing guide on every call
|
||||
- **Themes and Dark Mode**: Pick a draw.io theme under **Extras > Theme**; the page follows the system dark mode
|
||||
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from `embed.diagrams.net` by default, configurable via `DRAWIO_BASE_URL`)
|
||||
|
||||
@@ -139,6 +143,12 @@ After every change, the diagram is saved as a normal `.drawio` file in `~/.next-
|
||||
|
||||
The newest 50 files are kept. Set `DRAWIO_DATA_DIR` to use another folder, or to `off` to turn auto-save off.
|
||||
|
||||
## Custom Instructions
|
||||
|
||||
To give the AI your own drawing rules, write them in `~/.next-ai-drawio/instructions.md` as Markdown, for example "Always draw in minimal style" or "Label every edge". The file is appended to the drawing guide under a `## Custom Instructions` heading each time the guide is returned (`start_session`, `get_drawing_guide` and the `diagram-workflow` prompt), so edits apply without restarting your MCP client. Only the first 5000 characters are used.
|
||||
|
||||
`DRAWIO_DATA_DIR` changes the folder the file is read from; with `DRAWIO_DATA_DIR=off` the default folder is still used, since the file is only read. The text reaches the model as it is, so keep it to drawing rules you trust.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
@@ -168,7 +178,7 @@ The newest 50 files are kept. Set `DRAWIO_DATA_DIR` to use another folder, or to
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||
| `DRAWIO_BASE_URL` | `https://embed.diagrams.net` | Base URL for the draw.io embed. Set this to use a self-hosted draw.io instance for private deployments. |
|
||||
| `DRAWIO_DATA_DIR` | `~/.next-ai-drawio` | Folder for the auto-saved `.drawio` files. Set to `off` to turn auto-save off. |
|
||||
| `DRAWIO_DATA_DIR` | `~/.next-ai-drawio` | Folder for the auto-saved `.drawio` files and your `instructions.md`. Set to `off` to turn auto-save off (`instructions.md` is then read from the default folder). |
|
||||
| `DEBUG` | unset | Set to `true` to log debug messages to stderr. |
|
||||
|
||||
### Private Deployment (Self-hosted draw.io)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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\\"/>"}]}
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Tests for the user's instructions.md appended to the drawing guide
|
||||
* (src/custom-instructions.ts).
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
|
||||
import { homedir, tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
import {
|
||||
CUSTOM_INSTRUCTIONS_FILE,
|
||||
customInstructionsDir,
|
||||
guideWithCustomInstructions,
|
||||
readCustomInstructions,
|
||||
} from "../src/custom-instructions.ts"
|
||||
import { DRAWING_GUIDE } from "../src/drawing-guide.ts"
|
||||
|
||||
const GUIDE = "# Drawing guide\n\nKeep edges orthogonal."
|
||||
const tempDir = () => mkdtempSync(join(tmpdir(), "mcp-instructions-"))
|
||||
const writeRules = (dir: string, text: string) =>
|
||||
writeFileSync(join(dir, CUSTOM_INSTRUCTIONS_FILE), text)
|
||||
|
||||
describe("readCustomInstructions", () => {
|
||||
it("gives an empty string when there is no file", () => {
|
||||
const dir = tempDir()
|
||||
expect(readCustomInstructions(dir)).toBe("")
|
||||
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(GUIDE)
|
||||
})
|
||||
|
||||
it("appends the file's content under its own heading", () => {
|
||||
const dir = tempDir()
|
||||
writeRules(dir, "\nAlways draw in minimal style.\n\n")
|
||||
expect(readCustomInstructions(dir)).toBe(
|
||||
"Always draw in minimal style.",
|
||||
)
|
||||
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(
|
||||
`${GUIDE}\n\n## Custom Instructions\nAlways draw in minimal style.`,
|
||||
)
|
||||
})
|
||||
|
||||
it("cuts the content at 5000 characters", () => {
|
||||
const dir = tempDir()
|
||||
writeRules(dir, "x".repeat(6000))
|
||||
expect(readCustomInstructions(dir)).toBe("x".repeat(5000))
|
||||
})
|
||||
|
||||
it("gives an empty string when the file cannot be read", () => {
|
||||
const dir = tempDir()
|
||||
// A folder in place of the file: readFileSync fails with EISDIR
|
||||
mkdirSync(join(dir, CUSTOM_INSTRUCTIONS_FILE))
|
||||
expect(readCustomInstructions(dir)).toBe("")
|
||||
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(GUIDE)
|
||||
})
|
||||
|
||||
it("reads the file again on every call", () => {
|
||||
const dir = tempDir()
|
||||
writeRules(dir, "First rule.")
|
||||
expect(readCustomInstructions(dir)).toBe("First rule.")
|
||||
writeRules(dir, "Second rule.")
|
||||
expect(readCustomInstructions(dir)).toBe("Second rule.")
|
||||
})
|
||||
})
|
||||
|
||||
describe("customInstructionsDir", () => {
|
||||
const original = process.env.DRAWIO_DATA_DIR
|
||||
afterEach(() => {
|
||||
if (original === undefined) delete process.env.DRAWIO_DATA_DIR
|
||||
else process.env.DRAWIO_DATA_DIR = original
|
||||
})
|
||||
|
||||
it("honours DRAWIO_DATA_DIR", () => {
|
||||
const dir = tempDir()
|
||||
writeRules(dir, "Use blue fill #dae8fc.")
|
||||
process.env.DRAWIO_DATA_DIR = dir
|
||||
expect(customInstructionsDir()).toBe(dir)
|
||||
expect(guideWithCustomInstructions(GUIDE)).toBe(
|
||||
`${GUIDE}\n\n## Custom Instructions\nUse blue fill #dae8fc.`,
|
||||
)
|
||||
})
|
||||
|
||||
it("expands ~, which JSON configs pass on as it is", () => {
|
||||
process.env.DRAWIO_DATA_DIR = "~/drawio-saves"
|
||||
expect(customInstructionsDir()).toBe(join(homedir(), "drawio-saves"))
|
||||
})
|
||||
|
||||
it("falls back to the home folder when DRAWIO_DATA_DIR is off or unset", () => {
|
||||
const home = join(homedir(), ".next-ai-drawio")
|
||||
process.env.DRAWIO_DATA_DIR = "off"
|
||||
expect(customInstructionsDir()).toBe(home)
|
||||
delete process.env.DRAWIO_DATA_DIR
|
||||
expect(customInstructionsDir()).toBe(home)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DRAWING_GUIDE", () => {
|
||||
it("stays within its own budget; instructions.md adds up to 5000 more", () => {
|
||||
expect(DRAWING_GUIDE.length).toBeLessThanOrEqual(15000)
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,8 @@
|
||||
*/
|
||||
|
||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
@@ -47,6 +49,11 @@ const EXPECTED_TOOLS = [
|
||||
// Claude Code truncates tool descriptions and server instructions here
|
||||
const MAX_DESCRIPTION = 2048
|
||||
|
||||
// The server reads <DRAWIO_DATA_DIR>/instructions.md into the guide; a
|
||||
// developer's real ~/.next-ai-drawio/instructions.md must not leak in
|
||||
const CUSTOM_RULE = "Always use blue fill #dae8fc."
|
||||
let dataDir: string
|
||||
|
||||
let proc: ChildProcessWithoutNullStreams
|
||||
let stdoutBuf = ""
|
||||
const pending = new Map<
|
||||
@@ -72,8 +79,11 @@ function send(method: string, params: unknown, isNotification = false) {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
dataDir = mkdtempSync(path.join(tmpdir(), "mcp-wiring-"))
|
||||
writeFileSync(path.join(dataDir, "instructions.md"), CUSTOM_RULE)
|
||||
proc = spawn(tsxBin, [entry], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: { ...process.env, DRAWIO_DATA_DIR: dataDir },
|
||||
}) as ChildProcessWithoutNullStreams
|
||||
|
||||
proc.stdout.on("data", (chunk: Buffer) => {
|
||||
@@ -111,6 +121,7 @@ beforeAll(async () => {
|
||||
|
||||
afterAll(() => {
|
||||
proc?.kill("SIGTERM")
|
||||
rmSync(dataDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("MCP server wiring", () => {
|
||||
@@ -185,8 +196,21 @@ describe("MCP server wiring", () => {
|
||||
})
|
||||
const text: string = resp.result.content[0].text
|
||||
expect(text).toContain("Edge routing rules")
|
||||
expect(text).toContain("do not describe the diagram")
|
||||
expect(text).toContain("clear the canvas")
|
||||
expect(text.length).toBeLessThanOrEqual(15000)
|
||||
})
|
||||
|
||||
it("appends the user's instructions.md to the drawing guide", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "get_drawing_guide",
|
||||
arguments: {},
|
||||
})
|
||||
const text: string = resp.result.content[0].text
|
||||
expect(
|
||||
text.endsWith(`\n\n## Custom Instructions\n${CUSTOM_RULE}`),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("load_diagram dual-source arguments", () => {
|
||||
|
||||
@@ -164,6 +164,15 @@ describe("prepareNewDiagram", () => {
|
||||
)
|
||||
expect(out.ok).toBe(true)
|
||||
})
|
||||
|
||||
it("accepts the root cells alone as a blank page", () => {
|
||||
const out = prepareNewDiagram(
|
||||
`<mxCell id="0"/><mxCell id="1" parent="0"/>`,
|
||||
)
|
||||
expect(out.ok).toBe(true)
|
||||
if (!out.ok) return
|
||||
expect(hasCells(out.xml)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("labels that look like attributes", () => {
|
||||
|
||||
Reference in New Issue
Block a user