mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e16ea564a |
@@ -12,13 +12,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
@@ -26,14 +26,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
# Push to AWS ECR for App Runner auto-deploy
|
||||
- name: Configure AWS credentials
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
@@ -50,10 +50,10 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload unsigned artifacts for signing
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-unsigned
|
||||
with:
|
||||
name: windows-unsigned
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Sign with SignPath
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
uses: signpath/github-action-submit-signing-request@v3
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: '880a211d-2cd3-4e7b-8d04-3d1f8eb39df5'
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
output-artifact-directory: release-signed
|
||||
|
||||
- name: Upload signed artifacts to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: release-signed/*.exe
|
||||
env:
|
||||
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
working-directory: packages/mcp-server
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
|
||||
@@ -11,12 +11,12 @@ jobs:
|
||||
name: Lint & Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -52,19 +52,19 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
CI: true
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.shard }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
if: github.event_name != 'issue_comment'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
only-issue-labels: "wait for confrim"
|
||||
days-before-issue-stale: 15
|
||||
|
||||
Generated
+1168
-1718
File diff suppressed because it is too large
Load Diff
+18
-18
@@ -45,11 +45,11 @@
|
||||
"@aihubmix/ai-sdk-provider": "^2.2.1",
|
||||
"@aws-sdk/client-dynamodb": "^3.957.0",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@extractus/article-extractor": "^8.0.18",
|
||||
"@extractus/article-extractor": "^9.0.0",
|
||||
"@formatjs/intl-localematcher": "^0.9.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"@langfuse/client": "^5.0.0",
|
||||
"@langfuse/otel": "^5.0.0",
|
||||
"@langfuse/tracing": "^5.0.0",
|
||||
"@next/third-parties": "^16.0.6",
|
||||
"@opennextjs/cloudflare": "^1.17.1",
|
||||
"@openrouter/ai-sdk-provider": "^2.10.0",
|
||||
@@ -74,13 +74,13 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"jsonrepair": "^3.13.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"motion": "^12.23.25",
|
||||
"nanoid": "^5.0.0",
|
||||
"lucide-react": "^1.0.0",
|
||||
"motion": "^14.0.0",
|
||||
"nanoid": "^6.0.0",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.0.7",
|
||||
"ollama-ai-provider-v2": "^3.6.0",
|
||||
"pako": "^2.1.0",
|
||||
"pako": "^3.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^19.1.2",
|
||||
"react-dom": "^19.1.2",
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@biomejs/biome": "2.5.15",
|
||||
"@biomejs/biome": "2.5.7",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
@@ -119,27 +119,27 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/pako": "^3.0.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"concurrently": "^9.2.1",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"@vitest/coverage-v8": "^5.0.0",
|
||||
"concurrently": "^10.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^41.10.6",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "9.39.5",
|
||||
"eslint": "10.12.0",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"jsdom": "^30.0.0",
|
||||
"lint-staged": "^17.0.0",
|
||||
"shx": "^0.4.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
"typescript": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.16",
|
||||
"vitest": "^5.0.0",
|
||||
"wait-on": "^9.0.3",
|
||||
"wrangler": "^4.60.0"
|
||||
},
|
||||
|
||||
@@ -123,7 +123,7 @@ Use the standard MCP configuration with:
|
||||
| `get_drawing_guide` | Return the drawing rules again, for example after a long conversation was compacted |
|
||||
| `get_shape_library` | Return the shapes and icon styles of a library such as `aws4`, `azure2`, or `kubernetes` |
|
||||
| `create_new_diagram` | Create a new diagram from XML; a plain list of `mxCell` elements is enough |
|
||||
| `load_diagram` | Load a `.drawio` file into the session, from a `path` on disk or from its `xml` content (handles compressed files) |
|
||||
| `load_diagram` | Load a `.drawio` file from disk into the session (handles compressed files) |
|
||||
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells); all or nothing |
|
||||
| `get_diagram` | Get the current diagram XML, including your edits in the browser |
|
||||
| `screenshot_diagram` | Return a PNG of a page so the AI can check the rendered diagram |
|
||||
|
||||
Generated
+527
-217
File diff suppressed because it is too large
Load Diff
@@ -42,16 +42,16 @@
|
||||
"@modelcontextprotocol/sdk": "^1.31.0",
|
||||
"linkedom": "^0.18.0",
|
||||
"open": "^11.0.0",
|
||||
"pako": "^2.2.0",
|
||||
"pako": "^3.0.0",
|
||||
"saxes": "^6.0.0",
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pako": "^2.0.4",
|
||||
"@types/pako": "^3.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.8"
|
||||
"typescript": "^7.0.0",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
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, or takes the file's content as its 'xml' argument when you already have it in hand). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
|
||||
- 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).
|
||||
|
||||
@@ -455,54 +455,21 @@ registerWriteTool(
|
||||
{
|
||||
title: "Load .drawio file",
|
||||
description:
|
||||
"Load a .drawio diagram into the current session, REPLACING the entire diagram (all pages). " +
|
||||
"Provide ONE of two mutually exclusive sources: 'path' (the server reads the file from disk — you do NOT need to read the file yourself or pass its XML through create_new_diagram) " +
|
||||
"or 'xml' (the raw file content you already have — from another tool, a repository read, or an API response — so no temporary file needs to be written first). " +
|
||||
"Both accept plain XML and draw.io's compressed save format.\n\n" +
|
||||
"After loading from 'path' (or from compressed 'xml'), call get_diagram before edit_diagram — you haven't seen the file's cell IDs yet. " +
|
||||
"Plain-XML 'xml' content you supplied yourself is already known and can be edited immediately.",
|
||||
"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. " +
|
||||
"Handles both plain-XML and draw.io's compressed save format.\n\n" +
|
||||
"After loading, call get_diagram before edit_diagram — you haven't seen the file's cell IDs or structure yet.",
|
||||
inputSchema: {
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"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. Mutually exclusive with 'xml'.",
|
||||
),
|
||||
xml: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Raw .drawio file content: a plain <mxfile>/<mxGraphModel>, or draw.io's compressed save format. Use when the content is already in hand (another tool's output, a repository read, an API response). Mutually exclusive with 'path'.",
|
||||
"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, xml: inlineXml }) => {
|
||||
async ({ path }) => {
|
||||
try {
|
||||
// Argument validation comes before the session check: a bad
|
||||
// argument is a caller error and should be reported as such.
|
||||
if (path !== undefined && inlineXml !== undefined) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Provide either 'path' or 'xml', not both.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
if (path === undefined && inlineXml === undefined) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Provide either 'path' (a .drawio file to read) or 'xml' (the file's content).",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
if (!currentSession) {
|
||||
return {
|
||||
content: [
|
||||
@@ -515,37 +482,29 @@ registerWriteTool(
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly one of path/xml is present (validated above).
|
||||
let content = ""
|
||||
let sourceLabel = ""
|
||||
if (inlineXml !== undefined) {
|
||||
content = inlineXml
|
||||
sourceLabel = "inline XML"
|
||||
} else if (path !== undefined) {
|
||||
const fs = await import("node:fs/promises")
|
||||
const nodePath = await import("node:path")
|
||||
const absolutePath = nodePath.resolve(expandHome(path))
|
||||
const fs = await import("node:fs/promises")
|
||||
const nodePath = await import("node:path")
|
||||
const absolutePath = nodePath.resolve(expandHome(path))
|
||||
|
||||
try {
|
||||
// A pipe or device could be read forever, and the other
|
||||
// write tools wait for this one
|
||||
if (!(await fs.stat(absolutePath)).isFile()) {
|
||||
throw new Error("not a regular file")
|
||||
}
|
||||
content = await fs.readFile(absolutePath, "utf-8")
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
let content: string
|
||||
try {
|
||||
// A pipe or device could be read forever, and the other
|
||||
// write tools wait for this one
|
||||
if (!(await fs.stat(absolutePath)).isFile()) {
|
||||
throw new Error("not a regular file")
|
||||
}
|
||||
content = await fs.readFile(absolutePath, "utf-8")
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
sourceLabel = absolutePath
|
||||
}
|
||||
|
||||
const loaded = parseDrawioFileContent(content)
|
||||
@@ -558,7 +517,7 @@ registerWriteTool(
|
||||
const xml = loaded.xml
|
||||
|
||||
log.info(
|
||||
`Loading diagram from ${sourceLabel} (${xml.length} chars)`,
|
||||
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
|
||||
)
|
||||
|
||||
// Save the user's current state before replacing (same flow as
|
||||
@@ -578,17 +537,10 @@ registerWriteTool(
|
||||
currentSession.xml = xml
|
||||
currentSession.version++
|
||||
setState(currentSession.id, xml)
|
||||
// Edit-gate semantics by source:
|
||||
// - 'path': the model only supplied a path, so it doesn't know
|
||||
// the file's cell IDs — keep the gate (one get_diagram first).
|
||||
// - plain 'xml': the model supplied the exact content, same
|
||||
// rationale as create_new_diagram — record it as seen.
|
||||
// - compressed 'xml': the session now holds the decompressed
|
||||
// form, which the model cannot derive from the compressed
|
||||
// input — keep the gate.
|
||||
const markSeen =
|
||||
inlineXml !== undefined && !loaded.hadCompressedPages
|
||||
currentSession.lastSeenXml = markSeen ? xml : ""
|
||||
// Deliberately NOT marking the loaded XML as seen: the model only
|
||||
// supplied a path, so it doesn't know the file's cell IDs. The
|
||||
// edit gate will require one get_diagram before edits.
|
||||
currentSession.lastSeenXml = ""
|
||||
|
||||
addHistory(currentSession.id, xml, "")
|
||||
|
||||
@@ -599,17 +551,13 @@ registerWriteTool(
|
||||
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
|
||||
: "no pages parsed"
|
||||
|
||||
log.info(`Diagram loaded (${pageSummary})`)
|
||||
|
||||
const gateHint = markSeen
|
||||
? ""
|
||||
: "\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet."
|
||||
log.info(`Diagram loaded from file (${pageSummary})`)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Diagram loaded from ${sourceLabel}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}${gateHint}`,
|
||||
text: `Diagram loaded from ${absolutePath}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -624,6 +572,7 @@ registerWriteTool(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Tool: edit_diagram
|
||||
registerWriteTool(
|
||||
"edit_diagram",
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { getXmlSyntaxError } from "./xml-syntax.ts"
|
||||
|
||||
export type LoadResult =
|
||||
| { ok: true; xml: string; hadCompressedPages: boolean }
|
||||
| { ok: true; xml: string }
|
||||
| { ok: false; error: string }
|
||||
|
||||
/**
|
||||
@@ -101,9 +101,5 @@ export function parseDrawioFileContent(content: string): LoadResult {
|
||||
decompressedAny = true
|
||||
}
|
||||
// Nothing changed — keep the file's own serialisation.
|
||||
return {
|
||||
ok: true,
|
||||
xml: decompressedAny ? serializeMxfile(doc) : trimmed,
|
||||
hadCompressedPages: decompressedAny,
|
||||
}
|
||||
return { ok: true, xml: decompressedAny ? serializeMxfile(doc) : trimmed }
|
||||
}
|
||||
|
||||
@@ -54,11 +54,7 @@ describe("decompressPageContent", () => {
|
||||
describe("parseDrawioFileContent", () => {
|
||||
it("passes a plain-XML mxfile through unchanged", () => {
|
||||
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
||||
expect(r).toEqual({
|
||||
ok: true,
|
||||
xml: PLAIN_MXFILE,
|
||||
hadCompressedPages: false,
|
||||
})
|
||||
expect(r).toEqual({ ok: true, xml: PLAIN_MXFILE })
|
||||
})
|
||||
|
||||
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
|
||||
@@ -107,11 +103,7 @@ describe("parseDrawioFileContent", () => {
|
||||
it("keeps empty pages as-is", () => {
|
||||
const withEmpty = `<mxfile><diagram id="a" name="Page-1">${MODEL_XML}</diagram><diagram id="b" name="Empty"></diagram></mxfile>`
|
||||
const r = parseDrawioFileContent(withEmpty)
|
||||
expect(r).toEqual({
|
||||
ok: true,
|
||||
xml: withEmpty,
|
||||
hadCompressedPages: false,
|
||||
})
|
||||
expect(r).toEqual({ ok: true, xml: withEmpty })
|
||||
})
|
||||
|
||||
it("rejects empty files", () => {
|
||||
@@ -132,30 +124,3 @@ describe("parseDrawioFileContent", () => {
|
||||
if (!r.ok) expect(r.error).toContain('"Broken"')
|
||||
})
|
||||
})
|
||||
|
||||
describe("hadCompressedPages", () => {
|
||||
it("is false for a plain-XML mxfile", () => {
|
||||
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) expect(r.hadCompressedPages).toBe(false)
|
||||
})
|
||||
|
||||
it("is false for a bare mxGraphModel", () => {
|
||||
const r = parseDrawioFileContent(MODEL_XML)
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) expect(r.hadCompressedPages).toBe(false)
|
||||
})
|
||||
|
||||
it("is true for a fully compressed mxfile", () => {
|
||||
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) expect(r.hadCompressedPages).toBe(true)
|
||||
})
|
||||
|
||||
it("is true for a mixed plain/compressed file", () => {
|
||||
const mixed = `<mxfile><diagram id="a" name="Plain">${MODEL_XML}</diagram><diagram id="b" name="Squeezed">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
|
||||
const r = parseDrawioFileContent(mixed)
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) expect(r.hadCompressedPages).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -188,51 +188,3 @@ describe("MCP server wiring", () => {
|
||||
expect(text.length).toBeLessThanOrEqual(15000)
|
||||
})
|
||||
})
|
||||
|
||||
describe("load_diagram dual-source arguments", () => {
|
||||
it("advertises both optional 'path' and 'xml' sources", async () => {
|
||||
const resp = await send("tools/list", {})
|
||||
const load = resp.result.tools.find(
|
||||
(t: { name: string }) => t.name === "load_diagram",
|
||||
)
|
||||
const props = load?.inputSchema?.properties ?? {}
|
||||
expect(props.path).toBeTruthy()
|
||||
expect(props.xml).toBeTruthy()
|
||||
const required: string[] = load?.inputSchema?.required ?? []
|
||||
expect(required).not.toContain("path")
|
||||
expect(required).not.toContain("xml")
|
||||
})
|
||||
|
||||
it("rejects passing both 'path' and 'xml'", async () => {
|
||||
// Argument validation fires before the session check: no session
|
||||
// exists in this harness, so a both-args call must report the
|
||||
// mutual-exclusion error, not "No active session".
|
||||
const resp = await send("tools/call", {
|
||||
name: "load_diagram",
|
||||
arguments: { path: "/tmp/x.drawio", xml: "<mxfile/>" },
|
||||
})
|
||||
expect(resp.error, JSON.stringify(resp.error)).toBeUndefined()
|
||||
expect(resp.result?.isError).toBe(true)
|
||||
expect(resp.result?.content?.[0]?.text).toContain("not both")
|
||||
})
|
||||
|
||||
it("rejects passing neither 'path' nor 'xml'", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "load_diagram",
|
||||
arguments: {},
|
||||
})
|
||||
expect(resp.result?.isError).toBe(true)
|
||||
expect(resp.result?.content?.[0]?.text).toContain("either 'path'")
|
||||
})
|
||||
|
||||
it("accepts 'xml' alone as a source (fails only on the missing session)", async () => {
|
||||
const resp = await send("tools/call", {
|
||||
name: "load_diagram",
|
||||
arguments: {
|
||||
xml: '<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>',
|
||||
},
|
||||
})
|
||||
expect(resp.result?.isError).toBe(true)
|
||||
expect(resp.result?.content?.[0]?.text).toContain("No active session")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user