Compare commits

..

1 Commits

Author SHA1 Message Date
dayuan.jiang
08be7ecea7 ci: auto-publish mcp-server to npm via OIDC trusted publishing
Publishes @next-ai-drawio/mcp-server when packages/mcp-server changes on
main and the package.json version isn't on npm yet. Uses npm trusted
publishing (OIDC) - no token secret, no OTP, works with the strictest
2FA setting.
2026-07-12 15:19:47 +09:00
21 changed files with 2114 additions and 2663 deletions

View File

@@ -33,11 +33,6 @@
"matchPackagePatterns": ["@ai-sdk/*", "ai", "next"], "matchPackagePatterns": ["@ai-sdk/*", "ai", "next"],
"groupName": "Core framework packages", "groupName": "Core framework packages",
"automerge": false "automerge": false
},
{
"matchPackageNames": ["@biomejs/biome"],
"groupName": "Biome",
"automerge": false
} }
], ],
"vulnerabilityAlerts": { "vulnerabilityAlerts": {

View File

@@ -58,8 +58,6 @@ jobs:
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
provenance: mode=max
sbom: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
@@ -91,3 +89,4 @@ jobs:
docker pull ghcr.io/${REPO_LOWER}:latest docker pull ghcr.io/${REPO_LOWER}:latest
docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest

View File

@@ -372,13 +372,12 @@ export async function POST(req: Request) {
break break
} }
// GLM, Qwen, Kimi, Qiniu, Novita, MiMo, Atlas Cloud - OpenAI compatible // GLM, Qwen, Kimi, Qiniu, Novita, MiMo - OpenAI compatible
case "glm": case "glm":
case "qwen": case "qwen":
case "kimi": case "kimi":
case "qiniu": case "qiniu":
case "novita": case "novita":
case "atlascloud":
case "mimo": { case "mimo": {
const baseURL = const baseURL =
baseUrl || baseUrl ||

View File

@@ -6,8 +6,7 @@
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false
"includes": ["**", "!public"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,

View File

@@ -158,7 +158,7 @@ export function ModelSelector({
}, []) }, [])
return ( return (
<div ref={wrapperRef} className="min-w-0 max-w-48"> <div ref={wrapperRef} className="inline-block">
<ModelSelectorRoot open={open} onOpenChange={setOpen}> <ModelSelectorRoot open={open} onOpenChange={setOpen}>
<ModelSelectorTrigger asChild> <ModelSelectorTrigger asChild>
<ButtonWithTooltip <ButtonWithTooltip
@@ -167,7 +167,7 @@ export function ModelSelector({
size="sm" size="sm"
disabled={disabled} disabled={disabled}
className={cn( className={cn(
"h-8 min-w-0 max-w-full shrink overflow-hidden gap-1.5 px-2 transition-[padding,background-color] duration-150 ease-in-out hover:bg-accent", "hover:bg-accent gap-1.5 h-8 px-2 transition-[padding,background-color] duration-150 ease-in-out",
!showLabel && "px-1.5 justify-center", !showLabel && "px-1.5 justify-center",
)} )}
// accessibility: expose label to screen readers // accessibility: expose label to screen readers
@@ -176,7 +176,7 @@ export function ModelSelector({
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> <Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
{/* show/hide visible label based on measured width */} {/* show/hide visible label based on measured width */}
{showLabel ? ( {showLabel ? (
<span className="min-w-0 truncate text-xs"> <span className="text-xs truncate">
{selectedModel {selectedModel
? selectedModel.modelId ? selectedModel.modelId
: dict.modelConfig.default} : dict.modelConfig.default}

View File

@@ -194,8 +194,3 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# Get your API key from: https://platform.xiaomimimo.com/ # Get your API key from: https://platform.xiaomimimo.com/
# MIMO_API_KEY=your_mimo_api_key # MIMO_API_KEY=your_mimo_api_key
# MIMO_BASE_URL=https://api.xiaomimimo.com/v1 # Optional, default. Token Plan users: https://token-plan-cn.xiaomimimo.com/v1 # MIMO_BASE_URL=https://api.xiaomimimo.com/v1 # Optional, default. Token Plan users: https://token-plan-cn.xiaomimimo.com/v1
# Atlas Cloud Configuration (Optional)
# Get your API key from: https://www.atlascloud.ai/console/api-keys
# ATLASCLOUD_API_KEY=your_atlascloud_api_key
# ATLASCLOUD_BASE_URL=https://api.atlascloud.ai/v1 # Optional, default. LLM chat endpoint; media generation uses a separate API.

View File

@@ -118,7 +118,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"minimax", "minimax",
"novita", "novita",
"mimo", "mimo",
"atlascloud",
] ]
// Bedrock provider options for Anthropic beta features // Bedrock provider options for Anthropic beta features
@@ -544,7 +543,6 @@ function buildProviderOptions(
case "kimi": case "kimi":
case "qiniu": case "qiniu":
case "novita": case "novita":
case "atlascloud":
case "mimo": { case "mimo": {
// These providers don't have reasoning configs in AI SDK yet // These providers don't have reasoning configs in AI SDK yet
// Gateway passes through to underlying providers which handle their own configs // Gateway passes through to underlying providers which handle their own configs
@@ -583,7 +581,6 @@ export const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
minimax: "MINIMAX_API_KEY", minimax: "MINIMAX_API_KEY",
novita: "NOVITA_API_KEY", novita: "NOVITA_API_KEY",
mimo: "MIMO_API_KEY", mimo: "MIMO_API_KEY",
atlascloud: "ATLASCLOUD_API_KEY",
} }
/** /**
@@ -1373,8 +1370,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "glm": case "glm":
case "qwen": case "qwen":
case "qiniu": case "qiniu":
case "novita": case "novita": {
case "atlascloud": {
const envVar = PROVIDER_ENV_VARS[provider] const envVar = PROVIDER_ENV_VARS[provider]
if (!envVar) { if (!envVar) {
throw new Error( throw new Error(
@@ -1418,7 +1414,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
default: default:
throw new Error( throw new Error(
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita, mimo, atlascloud`, `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita, mimo`,
) )
} }

View File

@@ -24,7 +24,6 @@ export type ProviderName =
| "minimax" | "minimax"
| "novita" | "novita"
| "mimo" | "mimo"
| "atlascloud"
// Individual model configuration // Individual model configuration
export interface ModelConfig { export interface ModelConfig {
@@ -117,7 +116,6 @@ export const PROVIDER_LOGO_MAP: Record<string, string> = {
minimax: "minimax", minimax: "minimax",
novita: "novita", novita: "novita",
mimo: "xiaomi", mimo: "xiaomi",
atlascloud: "openai",
} }
// Provider metadata // Provider metadata
@@ -208,10 +206,6 @@ export const PROVIDER_INFO: Record<
label: "MiMo (Xiaomi)", label: "MiMo (Xiaomi)",
defaultBaseUrl: "https://api.xiaomimimo.com/v1", defaultBaseUrl: "https://api.xiaomimimo.com/v1",
}, },
atlascloud: {
label: "Atlas Cloud",
defaultBaseUrl: "https://api.atlascloud.ai/v1",
},
} }
// Suggested models per provider for quick add // Suggested models per provider for quick add
@@ -450,7 +444,6 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
"deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash",
], ],
mimo: ["mimo-v2.5-pro", "mimo-v2.5"], mimo: ["mimo-v2.5-pro", "mimo-v2.5"],
atlascloud: ["qwen/qwen3.5-flash", "deepseek-ai/deepseek-v4-pro"],
} }
// Helper to generate UUID // Helper to generate UUID

3404
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@
"@opennextjs/cloudflare": "^1.17.1", "@opennextjs/cloudflare": "^1.17.1",
"@openrouter/ai-sdk-provider": "^2.0.0", "@openrouter/ai-sdk-provider": "^2.0.0",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.221.0", "@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
"@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/sdk-trace-node": "^2.2.0",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
@@ -109,7 +109,7 @@
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4", "@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.5.7", "@biomejs/biome": "2.4.13",
"@playwright/test": "^1.57.0", "@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
@@ -129,7 +129,7 @@
"electron": "^39.2.7", "electron": "^39.2.7",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"eslint": "9.39.5", "eslint": "9.39.4",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^27.4.0", "jsdom": "^27.4.0",

View File

@@ -116,14 +116,9 @@ Use the standard MCP configuration with:
|------|-------------| |------|-------------|
| `start_session` | Opens browser with real-time diagram preview | | `start_session` | Opens browser with real-time diagram preview |
| `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) | | `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) |
| `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) | | `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
| `get_diagram` | Get the current diagram XML | | `get_diagram` | Get the current diagram XML |
| `export_diagram` | Save diagram to a `.drawio`, `.png`, or `.svg` file | | `export_diagram` | Save diagram to a `.drawio` file |
| `list_pages` | List every page (tab) with id, name, index, and cell count |
| `add_page` | Append a new page without touching existing ones |
| `rename_page` | Rename a page |
| `delete_page` | Delete a page (refuses to delete the last one) |
## How It Works ## How It Works

View File

@@ -1,12 +1,12 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.2.3", "version": "0.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.2.3", "version": "0.2.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",
@@ -62,9 +62,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -79,9 +79,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -96,9 +96,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -113,9 +113,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -130,9 +130,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -147,9 +147,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -164,9 +164,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -181,9 +181,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -198,9 +198,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -215,9 +215,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -232,9 +232,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -249,9 +249,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -266,9 +266,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -283,9 +283,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -300,9 +300,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -317,9 +317,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -334,9 +334,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -351,9 +351,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -368,9 +368,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -385,9 +385,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -402,9 +402,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -419,9 +419,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -436,9 +436,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -453,9 +453,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -470,9 +470,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -487,9 +487,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -523,12 +523,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.30.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9 || ^2.0.5", "@hono/node-server": "^1.19.9",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"content-type": "^1.0.5", "content-type": "^1.0.5",
@@ -899,26 +899,26 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.13.3", "version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.1.0", "@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/spy": "4.1.10", "@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.10", "@vitest/utils": "4.1.8",
"chai": "^6.2.2", "chai": "^6.2.2",
"tinyrainbow": "^3.1.0" "tinyrainbow": "^3.1.0"
}, },
@@ -927,13 +927,13 @@
} }
}, },
"node_modules/@vitest/mocker": { "node_modules/@vitest/mocker": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "4.1.10", "@vitest/spy": "4.1.8",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.21" "magic-string": "^0.30.21"
}, },
@@ -954,9 +954,9 @@
} }
}, },
"node_modules/@vitest/pretty-format": { "node_modules/@vitest/pretty-format": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -967,13 +967,13 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "4.1.10", "@vitest/utils": "4.1.8",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
"funding": { "funding": {
@@ -981,14 +981,14 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "4.1.10", "@vitest/pretty-format": "4.1.8",
"@vitest/utils": "4.1.10", "@vitest/utils": "4.1.8",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
@@ -997,9 +997,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -1007,13 +1007,13 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "4.1.10", "@vitest/pretty-format": "4.1.8",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0" "tinyrainbow": "^3.1.0"
}, },
@@ -1102,17 +1102,10 @@
} }
}, },
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "2.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC", "license": "ISC"
"engines": {
"node": ">=20.19.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
}, },
"node_modules/bundle-name": { "node_modules/bundle-name": {
"version": "4.1.0", "version": "4.1.0",
@@ -1252,114 +1245,30 @@
} }
}, },
"node_modules/css-select": { "node_modules/css-select": {
"version": "7.0.0", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"boolbase": "^2.0.0", "boolbase": "^1.0.0",
"css-what": "^8.0.0", "css-what": "^6.1.0",
"domhandler": "^6.0.1", "domhandler": "^5.0.2",
"domutils": "^4.0.2", "domutils": "^3.0.1",
"nth-check": "^3.0.1" "nth-check": "^2.0.1"
},
"engines": {
"node": ">=20.19.0"
}, },
"funding": { "funding": {
"type": "github",
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/css-select/node_modules/dom-serializer": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
"integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
"license": "MIT",
"dependencies": {
"domelementtype": "^3.0.0",
"domhandler": "^6.0.0",
"entities": "^8.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"type": "github",
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/css-select/node_modules/domelementtype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
"integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/css-select/node_modules/domhandler": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
"integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^3.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"type": "github",
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/css-select/node_modules/domutils": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
"integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^3.0.0",
"domelementtype": "^3.0.0",
"domhandler": "^6.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"type": "github",
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/css-select/node_modules/entities": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/css-what": { "node_modules/css-what": {
"version": "8.0.0", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=20.19.0" "node": ">= 6"
}, },
"funding": { "funding": {
"type": "github",
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
@@ -1579,9 +1488,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.28.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -1592,32 +1501,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1", "@esbuild/aix-ppc64": "0.27.1",
"@esbuild/android-arm": "0.28.1", "@esbuild/android-arm": "0.27.1",
"@esbuild/android-arm64": "0.28.1", "@esbuild/android-arm64": "0.27.1",
"@esbuild/android-x64": "0.28.1", "@esbuild/android-x64": "0.27.1",
"@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-arm64": "0.27.1",
"@esbuild/darwin-x64": "0.28.1", "@esbuild/darwin-x64": "0.27.1",
"@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-arm64": "0.27.1",
"@esbuild/freebsd-x64": "0.28.1", "@esbuild/freebsd-x64": "0.27.1",
"@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm": "0.27.1",
"@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-arm64": "0.27.1",
"@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-ia32": "0.27.1",
"@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-loong64": "0.27.1",
"@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-mips64el": "0.27.1",
"@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-ppc64": "0.27.1",
"@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-riscv64": "0.27.1",
"@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-s390x": "0.27.1",
"@esbuild/linux-x64": "0.28.1", "@esbuild/linux-x64": "0.27.1",
"@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-arm64": "0.27.1",
"@esbuild/netbsd-x64": "0.28.1", "@esbuild/netbsd-x64": "0.27.1",
"@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-arm64": "0.27.1",
"@esbuild/openbsd-x64": "0.28.1", "@esbuild/openbsd-x64": "0.27.1",
"@esbuild/openharmony-arm64": "0.28.1", "@esbuild/openharmony-arm64": "0.27.1",
"@esbuild/sunos-x64": "0.28.1", "@esbuild/sunos-x64": "0.27.1",
"@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-arm64": "0.27.1",
"@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-ia32": "0.27.1",
"@esbuild/win32-x64": "0.28.1" "@esbuild/win32-x64": "0.27.1"
} }
}, },
"node_modules/escape-html": { "node_modules/escape-html": {
@@ -1877,6 +1786,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -1929,9 +1851,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "10.1.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"funding": [ "funding": [
"https://github.com/fb55/htmlparser2?sponsor=1", "https://github.com/fb55/htmlparser2?sponsor=1",
{ {
@@ -1943,14 +1865,14 @@
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.2.2", "domutils": "^3.2.1",
"entities": "^7.0.1" "entities": "^6.0.0"
} }
}, },
"node_modules/htmlparser2/node_modules/entities": { "node_modules/htmlparser2/node_modules/entities": {
"version": "7.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -2374,15 +2296,15 @@
} }
}, },
"node_modules/linkedom": { "node_modules/linkedom": {
"version": "0.18.13", "version": "0.18.12",
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz", "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
"integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"css-select": "^7.0.0", "css-select": "^5.1.0",
"cssom": "^0.5.0", "cssom": "^0.5.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"htmlparser2": "^10.1.0", "htmlparser2": "^10.0.0",
"uhyphen": "^0.2.0" "uhyphen": "^0.2.0"
}, },
"engines": { "engines": {
@@ -2497,18 +2419,14 @@
} }
}, },
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "3.0.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"boolbase": "^2.0.0" "boolbase": "^1.0.0"
},
"engines": {
"node": ">=20.19.0"
}, },
"funding": { "funding": {
"type": "github",
"url": "https://github.com/fb55/nth-check?sponsor=1" "url": "https://github.com/fb55/nth-check?sponsor=1"
} }
}, },
@@ -2754,6 +2672,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
@@ -3068,13 +2996,14 @@
"optional": true "optional": true
}, },
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.23.9", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.9.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "~0.28.0" "esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
}, },
"bin": { "bin": {
"tsx": "dist/cli.mjs" "tsx": "dist/cli.mjs"
@@ -3121,9 +3050,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.18.2", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -3224,19 +3153,19 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "4.1.10", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/expect": "4.1.10", "@vitest/expect": "4.1.8",
"@vitest/mocker": "4.1.10", "@vitest/mocker": "4.1.8",
"@vitest/pretty-format": "4.1.10", "@vitest/pretty-format": "4.1.8",
"@vitest/runner": "4.1.10", "@vitest/runner": "4.1.8",
"@vitest/snapshot": "4.1.10", "@vitest/snapshot": "4.1.8",
"@vitest/spy": "4.1.10", "@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.10", "@vitest/utils": "4.1.8",
"es-module-lexer": "^2.0.0", "es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0", "expect-type": "^1.3.0",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
@@ -3264,12 +3193,12 @@
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.10", "@vitest/browser-playwright": "4.1.8",
"@vitest/browser-preview": "4.1.10", "@vitest/browser-preview": "4.1.8",
"@vitest/browser-webdriverio": "4.1.10", "@vitest/browser-webdriverio": "4.1.8",
"@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-istanbul": "4.1.8",
"@vitest/coverage-v8": "4.1.10", "@vitest/coverage-v8": "4.1.8",
"@vitest/ui": "4.1.10", "@vitest/ui": "4.1.8",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*", "jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0" "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -3368,9 +3297,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.4.3", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.2.3", "version": "0.2.1",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -1,102 +0,0 @@
/**
* Workflow gate for edit_diagram.
*
* Instead of a wall-clock timeout (the old 30s rule rejected slow-but-correct
* clients, see #885), we compare content: `lastSeenXml` is the state-store
* XML the model last saw (get_diagram) or wrote itself (create_new_diagram /
* edit_diagram / page CRUD). The store only changes on server writes or
* browser pushes (user autosave, sync exports), so if the live store still
* matches `lastSeenXml`, nothing happened that the model hasn't seen — the
* edit is safe no matter how much time passed.
*
* "Matches" is structural, not byte-for-byte: draw.io re-serialises the
* document when it pushes state back (different attribute order, pretty-
* printed whitespace, regenerated diagram ids, viewport attributes like
* dx/dy/pageWidth on <mxGraphModel>, a different mxfile host). None of that
* is a user edit, so the fingerprint keeps only what a user can actually
* change: the set of pages, each page's name, and each page's cell tree
* (tags + sorted attributes + text). Byte equality is kept as a fast path.
*/
import { isMxGraphModel, normalizeToMxfile, parseMxfile } from "./pages.js"
export type EditGateResult =
| { ok: true }
| { ok: false; reason: "no-context" | "stale" }
/**
* Canonical serialisation of an element subtree: tag + attributes sorted by
* name + child elements in order + non-whitespace text. Whitespace-only text
* nodes (pretty-printing) are dropped.
*/
function canonicalizeElement(el: Element): string {
const attrs = Array.from(el.attributes)
.map((a) => `${a.name}=${JSON.stringify(a.value)}`)
.sort()
.join(" ")
let children = ""
for (const child of Array.from(el.childNodes)) {
if (child.nodeType === 1) {
children += canonicalizeElement(child as Element)
} else if (child.nodeType === 3 || child.nodeType === 4) {
const text = (child.textContent ?? "").trim()
if (text) children += JSON.stringify(text)
}
}
return `<${el.tagName} ${attrs}>${children}</${el.tagName}>`
}
/**
* Structural fingerprint of a diagram document: page names + each page's
* <root> subtree, ignoring everything draw.io rewrites on re-serialisation
* (mxfile/mxGraphModel attributes, diagram ids, formatting). A bare
* <mxGraphModel> fingerprints identically to its single-page mxfile wrapping.
* Unparseable input falls back to the trimmed raw string, degrading to the
* plain string comparison.
*
* `includeNames=false` drops page names from the fingerprint — used when the
* other side of a comparison is a bare <mxGraphModel>, which carries no page
* name at all (normalizeToMxfile would invent "Page-1", falsely mismatching
* any real page name).
*/
export function contentFingerprint(xml: string, includeNames = true): string {
const normalized = normalizeToMxfile(xml)
const doc = normalized ? parseMxfile(normalized) : null
if (!doc) return xml.trim()
const pages: string[] = []
doc.querySelectorAll("diagram").forEach((d) => {
const name = includeNames ? d.getAttribute("name") || "" : ""
const root = d.querySelector("root")
// No <root> means the page content is not plain XML (e.g. draw.io's
// compressed format) — fingerprint the raw text instead.
const body = root
? canonicalizeElement(root)
: (d.textContent || "").trim()
pages.push(`${name}=${body}`)
})
return pages.join("\n")
}
export function checkEditGate(
lastSeenXml: string,
liveXml: string,
): EditGateResult {
// Model never fetched or produced any diagram state in this session.
if (!lastSeenXml) return { ok: false, reason: "no-context" }
// Browser state moved since the model last looked (e.g. manual user
// edits): force a re-fetch so update/delete operations don't build on
// stale cell contents. An empty liveXml means the store has no entry to
// compare against, so there is nothing newer to have missed.
if (liveXml && liveXml !== lastSeenXml) {
// A bare <mxGraphModel> on either side carries no page name, so
// comparing names would mismatch against anything not called
// "Page-1". Compare cell trees only in that case.
const includeNames =
!isMxGraphModel(liveXml) && !isMxGraphModel(lastSeenXml)
if (
contentFingerprint(liveXml, includeNames) !==
contentFingerprint(lastSeenXml, includeNames)
)
return { ok: false, reason: "stale" }
}
return { ok: true }
}

View File

@@ -36,7 +36,6 @@ class XMLSerializerPolyfill {
} }
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill ;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
import { createRequire } from "node:module"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import open from "open" import open from "open"
@@ -45,7 +44,6 @@ import {
applyDiagramOperations, applyDiagramOperations,
type DiagramOperation, type DiagramOperation,
} from "./diagram-operations.js" } from "./diagram-operations.js"
import { checkEditGate } from "./edit-gate.js"
import { addHistory } from "./history.js" import { addHistory } from "./history.js"
import { import {
getState, getState,
@@ -56,7 +54,6 @@ import {
startHttpServer, startHttpServer,
waitForSync, waitForSync,
} from "./http-server.js" } from "./http-server.js"
import { parseDrawioFileContent } from "./load-diagram.js"
import { log } from "./logger.js" import { log } from "./logger.js"
import { import {
addPageToDoc, addPageToDoc,
@@ -82,25 +79,13 @@ let currentSession: {
id: string id: string
xml: string xml: string
version: number version: number
// The exact state-store XML the model last saw (get_diagram) or wrote lastGetDiagramTime: number // Track when get_diagram was last called (for enforcing workflow)
// itself (create/edit/page CRUD). The store only changes on server
// writes or browser pushes (user autosave / sync), so edit_diagram can
// detect unseen user edits by comparing the live store against this.
// Empty = no diagram context established yet.
lastSeenXml: string
} | null = null } | null = null
// Create MCP server. The version reported in the MCP handshake is read from // Create MCP server
// package.json so it can never drift from the published npm version again
// (it sat hardcoded at stale values for most of this package's history).
// Both src/ (tsx dev) and dist/ (published build) live one level below the
// package root, so the relative path works in either runtime.
const require = createRequire(import.meta.url)
const packageVersion: string = require("../package.json").version
const server = new McpServer({ const server = new McpServer({
name: "next-ai-drawio", name: "next-ai-drawio",
version: packageVersion, version: "0.3.0",
}) })
// Shared Zod schema fragment for page-targeting parameters. // Shared Zod schema fragment for page-targeting parameters.
@@ -173,21 +158,21 @@ server.prompt(
1. Call start_session to open the browser preview 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) 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 ## Working with Multiple Pages
- Use list_pages to discover existing pages (id, name, index) - 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 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 - 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 - 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) ## Adding Elements to an Existing Page
1. Call edit_diagram with your operations, optionally with a page selector 1. Use edit_diagram with "add" operation, optionally with a page selector
2. If you don't know the current cell IDs or structure, call get_diagram first 2. Provide a unique cell_id and complete mxCell XML
3. For add/update, provide the cell_id and complete mxCell XML 3. No need to call get_diagram first - the server fetches latest state automatically
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
## Modifying or Deleting Existing Elements
1. FIRST call get_diagram to see current cell IDs and page structure
2. THEN call edit_diagram with "update" or "delete" operations
3. For update, provide the cell_id and complete new mxCell XML
## Important Notes ## 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. - 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.
@@ -220,7 +205,7 @@ server.registerTool(
id: sessionId, id: sessionId,
xml: "", xml: "",
version: 0, version: 0,
lastSeenXml: "", lastGetDiagramTime: 0,
} }
// Open browser // Open browser
@@ -391,12 +376,10 @@ COMMON STYLES:
// Update session state // Update session state
currentSession.xml = xml currentSession.xml = xml
currentSession.version++ currentSession.version++
currentSession.lastGetDiagramTime = Date.now()
// Push to embedded server state. The model just authored this // Push to embedded server state
// exact XML, so record it as seen — edit_diagram may follow
// without a redundant get_diagram round-trip.
setState(currentSession.id, xml) setState(currentSession.id, xml)
currentSession.lastSeenXml = xml
// Save AI result (no SVG yet - will be captured by browser) // Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, xml, "") addHistory(currentSession.id, xml, "")
@@ -434,136 +417,19 @@ COMMON STYLES:
}, },
) )
// Tool: load_diagram
server.registerTool(
"load_diagram",
{
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. " +
"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()
.describe(
"Path to the .drawio file to load (e.g., ./diagram.drawio)",
),
},
},
async ({ path }) => {
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
const fs = await import("node:fs/promises")
const nodePath = await import("node:path")
const absolutePath = nodePath.resolve(path)
let content: string
try {
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,
}
}
const loaded = parseDrawioFileContent(content)
if (!loaded.ok) {
return {
content: [{ type: "text", text: `Error: ${loaded.error}` }],
isError: true,
}
}
const xml = loaded.xml
log.info(
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
)
// Save the user's current state before replacing (same flow as
// create_new_diagram).
const browserState = getState(currentSession.id)
if (browserState?.xml) {
currentSession.xml = browserState.xml
}
if (currentSession.xml) {
addHistory(
currentSession.id,
currentSession.xml,
browserState?.svg || "",
)
}
currentSession.xml = xml
currentSession.version++
setState(currentSession.id, 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, "")
const doc = parseMxfile(xml)
const pages = doc ? listPagesFromDoc(doc) : []
const pageSummary =
pages.length > 0
? `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 from file (${pageSummary})`)
return {
content: [
{
type: "text",
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.`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("load_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: edit_diagram // Tool: edit_diagram
server.registerTool( server.registerTool(
"edit_diagram", "edit_diagram",
{ {
description: description:
"Edit a specific page in the current diagram by ID-based operations (update/add/delete cells).\n\n" + "Edit a specific page in the current diagram by ID-based operations (update/add/delete cells).\n\n" +
"Freshness: the server remembers the last diagram state you have seen, and rejects this call " + "⚠️ REQUIRED: You MUST call get_diagram BEFORE this tool!\n" +
"only if the user edited the diagram in the browser since then. You do NOT need to call " + "This fetches the latest state from the browser including any manual user edits.\n" +
"get_diagram before every edit — if your view is stale, the call is rejected (with no side " + "Skipping get_diagram WILL cause user's changes to be LOST.\n\n" +
"effects) and the error tells you to call get_diagram once and retry.\n\n" + "Workflow:\n" +
"Call get_diagram first only when you don't know the current diagram content (cell IDs, " + "1. Call get_diagram to see current cell IDs, page structure, and active page\n" +
"structure) — e.g. the diagram wasn't created in this conversation, or you're unsure your " + "2. Use the returned XML to construct your edit operations\n" +
"memory of it is accurate.\n\n" + "3. Call edit_diagram with your operations and (optionally) a page selector\n\n" +
"Multi-page targeting:\n" + "Multi-page targeting:\n" +
"- page_id / page_name / page_index are optional; when all omitted, the FIRST page is targeted\n" + "- 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" + "- Use list_pages to discover what pages exist\n\n" +
@@ -616,6 +482,27 @@ server.registerTool(
} }
} }
// Enforce workflow: require get_diagram to be called first
const timeSinceGet = Date.now() - currentSession.lastGetDiagramTime
if (timeSinceGet > 30000) {
// 30 seconds
log.warn(
"edit_diagram called without recent get_diagram - rejecting to prevent data loss",
)
return {
content: [
{
type: "text",
text:
"Error: You must call get_diagram first before edit_diagram.\n\n" +
"This ensures you have the latest diagram state including any manual edits the user made in the browser. " +
"Please call get_diagram, then use that XML to construct your edit operations.",
},
],
isError: true,
}
}
// Fetch latest state from browser. Re-normalise to mxfile: the // Fetch latest state from browser. Re-normalise to mxfile: the
// embed/sync path can hand back a bare <mxGraphModel>, and adopting // embed/sync path can hand back a bare <mxGraphModel>, and adopting
// it verbatim would silently strip a multi-page document down to // it verbatim would silently strip a multi-page document down to
@@ -639,37 +526,6 @@ server.registerTool(
} }
} }
// Enforce workflow: the model must have seen the current diagram
// state. Content comparison instead of a wall-clock timeout —
// slow reasoning between get_diagram and edit_diagram is fine as
// long as nothing changed in the browser meanwhile (#885).
const gate = checkEditGate(
currentSession.lastSeenXml,
browserState?.xml ?? "",
)
if (!gate.ok) {
log.warn(
gate.reason === "stale"
? "edit_diagram called with unseen browser changes - rejecting to prevent data loss"
: "edit_diagram called without get_diagram - rejecting to prevent data loss",
)
return {
content: [
{
type: "text",
text:
gate.reason === "stale"
? "Error: The diagram changed in the browser since you last fetched it (e.g. manual user edits).\n\n" +
"Call get_diagram to see the latest state, then rebuild your edit operations on top of it."
: "Error: You must call get_diagram first before edit_diagram.\n\n" +
"This ensures you have the latest diagram state including any manual edits the user made in the browser. " +
"Please call get_diagram, then use that XML to construct your edit operations.",
},
],
isError: true,
}
}
const pageSelector = pickPageSelector({ const pageSelector = pickPageSelector({
page_id, page_id,
page_name, page_name,
@@ -745,10 +601,8 @@ server.registerTool(
currentSession.xml = result currentSession.xml = result
currentSession.version++ currentSession.version++
// Push to embedded server; the pushed XML is now the latest // Push to embedded server
// state the model has seen.
setState(currentSession.id, result) setState(currentSession.id, result)
currentSession.lastSeenXml = result
// Save AI result (no SVG yet - will be captured by browser) // Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, result, "") addHistory(currentSession.id, result, "")
@@ -787,9 +641,8 @@ server.registerTool(
{ {
description: description:
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " + "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) — " + "Call this BEFORE edit_diagram if you need to update or delete existing elements, " +
"e.g. before editing a diagram you didn't create in this conversation, or after edit_diagram " + "so you can see the current cell IDs, pages, and structure.\n\n" +
"was rejected because the user changed the diagram in the browser.\n\n" +
"Returns the full <mxfile> by default. If a page selector is provided, returns just that page's <mxGraphModel> embedded in a one-page <mxfile> wrapper.", "Returns the full <mxfile> by default. If a page selector is provided, returns just that page's <mxGraphModel> embedded in a one-page <mxfile> wrapper.",
inputSchema: { inputSchema: {
...pageSelectorSchema, ...pageSelectorSchema,
@@ -824,6 +677,9 @@ server.registerTool(
} }
} }
// Mark that get_diagram was called (for edit_diagram workflow check)
currentSession.lastGetDiagramTime = Date.now()
// Fetch latest state from browser, re-normalising to mxfile so a // Fetch latest state from browser, re-normalising to mxfile so a
// bare <mxGraphModel> pushed back by the embed/sync path doesn't // bare <mxGraphModel> pushed back by the embed/sync path doesn't
// strip page structure (see edit_diagram for the same guard). // strip page structure (see edit_diagram for the same guard).
@@ -844,11 +700,6 @@ server.registerTool(
} }
} }
// The model is now looking at the current state. Record the raw
// store value — the gate's fast path is plain string equality
// against the store, with a structural comparison as fallback.
currentSession.lastSeenXml = browserState?.xml || currentSession.xml
const pageSelector = pickPageSelector({ const pageSelector = pickPageSelector({
page_id, page_id,
page_name, page_name,
@@ -1219,11 +1070,11 @@ async function loadMxfileForMutation(): Promise<
addHistory(sessionRef.id, sessionRef.xml, browserState?.svg || "") addHistory(sessionRef.id, sessionRef.xml, browserState?.svg || "")
sessionRef.xml = newXml sessionRef.xml = newXml
sessionRef.version++ sessionRef.version++
// Page CRUD updates the structure that get_diagram would return,
// so refresh the workflow timestamp — subsequent edit_diagram
// calls don't need a redundant get_diagram round-trip.
sessionRef.lastGetDiagramTime = Date.now()
setState(sessionRef.id, newXml) setState(sessionRef.id, newXml)
// The model just wrote this exact state, so mark it as seen —
// subsequent edit_diagram calls don't need a redundant
// get_diagram round-trip.
sessionRef.lastSeenXml = newXml
addHistory(sessionRef.id, newXml, "") addHistory(sessionRef.id, newXml, "")
}, },
} }

View File

@@ -1,101 +0,0 @@
/**
* File-loading helpers for the load_diagram tool.
*
* A .drawio file is an <mxfile> whose <diagram> children hold each page's
* <mxGraphModel> either as plain XML or — draw.io's default save format —
* compressed: encodeURIComponent(xml) → raw deflate → base64 as the
* diagram's text content. The rest of the server assumes plain XML inside
* every <diagram>, so loading decompresses all pages up front.
*/
import { inflateRawSync } from "node:zlib"
import { DOMParser } from "linkedom"
import {
isMxFile,
isMxGraphModel,
normalizeToMxfile,
parseMxfile,
serializeMxfile,
} from "./pages.js"
export type LoadResult =
| { ok: true; xml: string }
| { ok: false; error: string }
/**
* Decode one compressed page body (base64 → raw deflate → URI-decode).
* Returns null if the text isn't in that format.
*/
export function decompressPageContent(compressed: string): string | null {
try {
const inflated = inflateRawSync(
Buffer.from(compressed.trim(), "base64"),
).toString("utf-8")
try {
return decodeURIComponent(inflated)
} catch {
// Not URI-encoded (older files) — the inflated text is the XML.
return inflated
}
} catch {
return null
}
}
/**
* Parse the content of a .drawio file into the canonical session shape:
* an <mxfile> whose every page holds plain <mxGraphModel> XML. Accepts a
* bare <mxGraphModel> (wrapped into a one-page mxfile) and decompresses
* any compressed pages.
*/
export function parseDrawioFileContent(content: string): LoadResult {
const trimmed = content.trim()
if (!trimmed) return { ok: false, error: "File is empty." }
if (isMxGraphModel(trimmed)) {
const normalized = normalizeToMxfile(trimmed)
return normalized
? { ok: true, xml: normalized }
: { ok: false, error: "Failed to parse <mxGraphModel> XML." }
}
if (!isMxFile(trimmed)) {
return {
ok: false,
error: "Not a draw.io file: expected an <mxfile> or <mxGraphModel> root element.",
}
}
const doc = parseMxfile(trimmed)
if (!doc) return { ok: false, error: "Failed to parse <mxfile> XML." }
let decompressedAny = false
for (const d of Array.from(doc.querySelectorAll("diagram"))) {
if (d.querySelector("mxGraphModel")) continue
const text = (d.textContent || "").trim()
if (!text) continue // an empty page is valid
const pageLabel =
d.getAttribute("name") || d.getAttribute("id") || "unnamed"
const xml = decompressPageContent(text)
if (!xml || !isMxGraphModel(xml)) {
return {
ok: false,
error: `Page "${pageLabel}" has content that is neither plain <mxGraphModel> XML nor draw.io's compressed format.`,
}
}
const inner = new DOMParser().parseFromString(xml, "text/xml")
if (
inner.querySelector("parsererror") ||
inner.documentElement?.tagName !== "mxGraphModel"
) {
return {
ok: false,
error: `Page "${pageLabel}" decompressed but its XML failed to parse.`,
}
}
d.textContent = ""
d.appendChild(
doc.importNode(inner.documentElement as unknown as Node, true),
)
decompressedAny = true
}
// Nothing changed — keep the file's own serialisation.
return { ok: true, xml: decompressedAny ? serializeMxfile(doc) : trimmed }
}

View File

@@ -1,132 +0,0 @@
/**
* Unit tests for the edit_diagram workflow gate (edit-gate.ts).
*
* The gate replaced the old 30-second wall-clock rule (#885): an edit is
* allowed when the model has seen the current browser state, no matter how
* long ago — and rejected when the browser state moved since. "Seen" is
* judged structurally, so draw.io's re-serialisation of the same content
* (attribute order, whitespace, viewport attributes, wrapper shape) never
* reads as a user edit.
*/
import { DOMParser } from "linkedom"
import { beforeAll, describe, expect, it } from "vitest"
beforeAll(() => {
;(globalThis as any).DOMParser = DOMParser
})
import { checkEditGate, contentFingerprint } from "../src/edit-gate.js"
const XML_A = `<mxfile host="app.diagrams.net"><diagram id="p1" name="Page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="box1" value="Hello" style="rounded=0;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`
// The same document as draw.io re-serialises it on autosave: different host,
// regenerated diagram id, viewport attributes on mxGraphModel, re-ordered
// cell attributes, pretty-printed whitespace.
const XML_A_RESERIALIZED = `<mxfile host="embed.diagrams.net">
<diagram id="regenerated-id" name="Page-1">
<mxGraphModel dx="1596" dy="743" grid="1" pageWidth="827" pageHeight="1169">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="box1" parent="1" style="rounded=0;" value="Hello" vertex="1">
<mxGeometry height="60" width="120" x="40" y="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>`
// A real user edit: box1 moved to a different position.
const XML_B = XML_A.replace('x="40" y="40"', 'x="300" y="200"')
// Bare mxGraphModel with identical page content to XML_A.
const XML_A_BARE = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="box1" value="Hello" style="rounded=0;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>`
describe("checkEditGate", () => {
it("rejects when no diagram context was ever established", () => {
expect(checkEditGate("", XML_A)).toEqual({
ok: false,
reason: "no-context",
})
})
it("allows when the browser state is exactly what the model saw", () => {
expect(checkEditGate(XML_A, XML_A)).toEqual({ ok: true })
})
it("allows when the browser state is a re-serialisation of the same content", () => {
expect(checkEditGate(XML_A, XML_A_RESERIALIZED)).toEqual({ ok: true })
})
it("rejects when a cell actually changed", () => {
expect(checkEditGate(XML_A, XML_B)).toEqual({
ok: false,
reason: "stale",
})
})
it("rejects a real edit even when wrapped in re-serialisation noise", () => {
const movedAndReserialized = XML_A_RESERIALIZED.replace(
'x="40" y="40"',
'x="300" y="200"',
)
expect(checkEditGate(XML_A, movedAndReserialized)).toEqual({
ok: false,
reason: "stale",
})
})
it("allows when the store has no live entry to compare against", () => {
expect(checkEditGate(XML_A, "")).toEqual({ ok: true })
})
// A bare <mxGraphModel> push carries no page name, so the gate must not
// compare the invented "Page-1" wrapper name against the real one.
it("allows a bare mxGraphModel push when the page has a custom name", () => {
const seenRenamed = XML_A.replace('name="Page-1"', 'name="Arch"')
expect(checkEditGate(seenRenamed, XML_A_BARE)).toEqual({ ok: true })
})
it("still rejects a bare mxGraphModel push whose cells changed", () => {
const seenRenamed = XML_A.replace('name="Page-1"', 'name="Arch"')
const bareMoved = XML_A_BARE.replace('x="40" y="40"', 'x="300" y="200"')
expect(checkEditGate(seenRenamed, bareMoved)).toEqual({
ok: false,
reason: "stale",
})
})
})
describe("contentFingerprint", () => {
it("is invariant under draw.io re-serialisation", () => {
expect(contentFingerprint(XML_A)).toBe(
contentFingerprint(XML_A_RESERIALIZED),
)
})
it("treats a bare mxGraphModel like its one-page mxfile wrapping", () => {
expect(contentFingerprint(XML_A_BARE)).toBe(contentFingerprint(XML_A))
})
it("changes when a cell attribute changes", () => {
expect(contentFingerprint(XML_A)).not.toBe(contentFingerprint(XML_B))
})
it("changes when a page is renamed", () => {
const renamed = XML_A.replace('name="Page-1"', 'name="Renamed"')
expect(contentFingerprint(XML_A)).not.toBe(contentFingerprint(renamed))
})
it("changes when a page is added", () => {
const twoPages = XML_A.replace(
"</mxfile>",
`<diagram id="p2" name="Page-2"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`,
)
expect(contentFingerprint(XML_A)).not.toBe(contentFingerprint(twoPages))
})
it("falls back to the raw string for unparseable input", () => {
expect(contentFingerprint("not xml at all")).toBe("not xml at all")
})
})

View File

@@ -1,126 +0,0 @@
/**
* Unit tests for load_diagram's file parsing (load-diagram.ts).
*
* A .drawio file stores each page's <mxGraphModel> either as plain XML or
* as draw.io's compressed default (encodeURIComponent → raw deflate →
* base64 text content). The loader must produce the canonical session
* shape: an <mxfile> whose every page is plain XML.
*/
import { deflateRawSync } from "node:zlib"
import { DOMParser } from "linkedom"
import { beforeAll, describe, expect, it } from "vitest"
// Install the DOM polyfills exactly as index.ts does at runtime.
beforeAll(() => {
;(globalThis as any).DOMParser = DOMParser
class XMLSerializerPolyfill {
serializeToString(node: any): string {
if (node.outerHTML !== undefined) return node.outerHTML
if (node.documentElement) return node.documentElement.outerHTML
return ""
}
}
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
})
import {
decompressPageContent,
parseDrawioFileContent,
} from "../src/load-diagram.js"
const MODEL_XML = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="box1" value="Hello" style="rounded=0;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>`
/** Compress a page body exactly the way draw.io does when saving. */
function drawioCompress(xml: string): string {
return deflateRawSync(
Buffer.from(encodeURIComponent(xml), "utf-8"),
).toString("base64")
}
const PLAIN_MXFILE = `<mxfile host="app.diagrams.net"><diagram id="p1" name="Page-1">${MODEL_XML}</diagram></mxfile>`
const COMPRESSED_MXFILE = `<mxfile host="app.diagrams.net" compressed="true"><diagram id="p1" name="Page-1">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
describe("decompressPageContent", () => {
it("round-trips draw.io's compressed format", () => {
expect(decompressPageContent(drawioCompress(MODEL_XML))).toBe(MODEL_XML)
})
it("handles non-URI-encoded legacy payloads", () => {
const legacy = deflateRawSync(Buffer.from(MODEL_XML, "utf-8")).toString(
"base64",
)
expect(decompressPageContent(legacy)).toBe(MODEL_XML)
})
it("returns null for garbage", () => {
expect(decompressPageContent("not base64 deflate")).toBeNull()
})
})
describe("parseDrawioFileContent", () => {
it("passes a plain-XML mxfile through unchanged", () => {
const r = parseDrawioFileContent(PLAIN_MXFILE)
expect(r).toEqual({ ok: true, xml: PLAIN_MXFILE })
})
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
const r = parseDrawioFileContent(MODEL_XML)
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.xml).toContain("<mxfile")
expect(r.xml).toContain('value="Hello"')
}
})
it("decompresses a compressed mxfile into plain XML pages", () => {
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.xml).toContain("<mxGraphModel")
expect(r.xml).toContain('value="Hello"')
// The compressed blob must be gone.
expect(r.xml).not.toContain(drawioCompress(MODEL_XML))
}
})
it("decompresses only the compressed pages of a mixed 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) {
const doc = new DOMParser().parseFromString(r.xml, "text/xml")
const diagrams = Array.from(
doc.querySelectorAll("diagram"),
) as Element[]
expect(diagrams).toHaveLength(2)
for (const d of diagrams) {
expect(d.querySelector("mxGraphModel")).not.toBeNull()
}
}
})
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 })
})
it("rejects empty files", () => {
const r = parseDrawioFileContent(" ")
expect(r.ok).toBe(false)
})
it("rejects non-drawio content", () => {
const r = parseDrawioFileContent("<svg><rect/></svg>")
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain("Not a draw.io file")
})
it("rejects a page whose content is neither XML nor compressed", () => {
const bad = `<mxfile><diagram id="a" name="Broken">!!! not a diagram !!!</diagram></mxfile>`
const r = parseDrawioFileContent(bad)
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain('"Broken"')
})
})

View File

@@ -31,7 +31,6 @@ const tsxBin = path.resolve(
const EXPECTED_TOOLS = [ const EXPECTED_TOOLS = [
"start_session", "start_session",
"create_new_diagram", "create_new_diagram",
"load_diagram",
"edit_diagram", "edit_diagram",
"get_diagram", "get_diagram",
"export_diagram", "export_diagram",

View File

@@ -206,16 +206,6 @@ vi.mock("@aihubmix/ai-sdk-provider", () => {
return { aihubmix: mockAihubmix, createAihubmix: mockCreateAihubmix } return { aihubmix: mockAihubmix, createAihubmix: mockCreateAihubmix }
}) })
vi.mock("@ai-sdk/openai", () => {
const mockModel = { modelId: "test-model" }
const mockChat = vi.fn(() => mockModel)
const mockProviderFn = vi.fn(() => mockModel) as any
mockProviderFn.chat = mockChat
const mockCreateOpenAI = vi.fn(() => mockProviderFn)
const mockOpenai = vi.fn(() => mockModel)
return { createOpenAI: mockCreateOpenAI, openai: mockOpenai }
})
describe("AIHubMix provider", () => { describe("AIHubMix provider", () => {
let createAihubmixMock: ReturnType<typeof vi.fn> let createAihubmixMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {} const savedEnv: Record<string, string | undefined> = {}
@@ -272,54 +262,6 @@ describe("AIHubMix provider", () => {
}) })
}) })
describe("Atlas Cloud provider", () => {
let createOpenAIMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}
beforeEach(async () => {
savedEnv.ATLASCLOUD_API_KEY = process.env.ATLASCLOUD_API_KEY
savedEnv.ATLASCLOUD_BASE_URL = process.env.ATLASCLOUD_BASE_URL
delete process.env.ATLASCLOUD_BASE_URL
const mod = await import("@ai-sdk/openai")
createOpenAIMock = mod.createOpenAI as ReturnType<typeof vi.fn>
createOpenAIMock.mockClear()
})
afterEach(() => {
process.env.ATLASCLOUD_API_KEY = savedEnv.ATLASCLOUD_API_KEY
process.env.ATLASCLOUD_BASE_URL = savedEnv.ATLASCLOUD_BASE_URL
})
it("uses Atlas Cloud default endpoint with ATLASCLOUD_API_KEY", () => {
process.env.ATLASCLOUD_API_KEY = "server-atlas-key"
getAIModel({
provider: "atlascloud",
modelId: "qwen/qwen3.5-flash",
})
expect(createOpenAIMock).toHaveBeenCalledWith({
apiKey: "server-atlas-key",
baseURL: "https://api.atlascloud.ai/v1",
})
})
it("uses custom Atlas Cloud base URL when provided", () => {
getAIModel({
provider: "atlascloud",
apiKey: "client-atlas-key",
baseUrl: "https://proxy.example.com/v1",
modelId: "deepseek-ai/deepseek-v4-pro",
})
expect(createOpenAIMock).toHaveBeenCalledWith({
apiKey: "client-atlas-key",
baseURL: "https://proxy.example.com/v1",
})
})
})
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => { describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
let createDeepSeekMock: ReturnType<typeof vi.fn> let createDeepSeekMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {} const savedEnv: Record<string, string | undefined> = {}

View File

@@ -39,22 +39,6 @@ describe("ServerModelsConfigSchema", () => {
expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow() expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow()
}) })
it("accepts Atlas Cloud provider names", () => {
const config: ServerModelsConfig = {
providers: [
{
name: "Atlas Cloud Server",
provider: "atlascloud",
models: ["qwen/qwen3.5-flash"],
apiKeyEnv: "ATLASCLOUD_API_KEY",
baseUrlEnv: "ATLASCLOUD_BASE_URL",
},
],
}
expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow()
})
it("rejects invalid provider names", () => { it("rejects invalid provider names", () => {
const invalidConfig = { const invalidConfig = {
providers: [ providers: [