drawio-mcp-server 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const BIN = join(__dirname, "..", "..", "build", "index.js");
9
+ // Sandbox every adapter's defaultPaths() resolution so a spawned `install`
10
+ // process can never resolve to the real developer's home directory.
11
+ // homedir() reads HOME on POSIX and USERPROFILE/HOMEDRIVE+HOMEPATH on
12
+ // Windows; claude-desktop's resolver reads APPDATA directly on win32.
13
+ function isolateEnv(homeDir) {
14
+ const env = { ...process.env };
15
+ env.HOME = homeDir;
16
+ env.USERPROFILE = homeDir;
17
+ env.APPDATA = join(homeDir, "AppData", "Roaming");
18
+ env.LOCALAPPDATA = join(homeDir, "AppData", "Local");
19
+ env.HOMEDRIVE = "C:"; // benign default; ignored on POSIX
20
+ env.HOMEPATH = homeDir;
21
+ delete env.XDG_CONFIG_HOME;
22
+ delete env.XDG_DATA_HOME;
23
+ delete env.XDG_STATE_HOME;
24
+ delete env.XDG_CACHE_HOME;
25
+ return env;
26
+ }
27
+ describe("install subcommand — end to end", () => {
28
+ let dir;
29
+ let home;
30
+ beforeEach(() => {
31
+ dir = mkdtempSync(join(tmpdir(), "drawio-e2e-"));
32
+ home = mkdtempSync(join(tmpdir(), "drawio-home-"));
33
+ });
34
+ afterEach(() => {
35
+ rmSync(dir, { recursive: true, force: true });
36
+ rmSync(home, { recursive: true, force: true });
37
+ });
38
+ function run(args) {
39
+ return spawnSync("node", [BIN, "install", ...args], {
40
+ encoding: "utf8",
41
+ env: isolateEnv(home),
42
+ cwd: home,
43
+ });
44
+ }
45
+ it("codex --print emits TOML on stdout, no file written", () => {
46
+ const target = join(dir, "codex.toml");
47
+ const r = run(["codex", "--print", "--config-path", target]);
48
+ expect(r.status).toBe(0);
49
+ expect(r.stdout).toContain("[mcp_servers.drawio]");
50
+ expect(existsSync(target)).toBe(false);
51
+ });
52
+ it("zed --print emits JSON", () => {
53
+ const target = join(dir, "zed.json");
54
+ const r = run(["zed", "--print", "--config-path", target]);
55
+ expect(r.status).toBe(0);
56
+ expect(r.stdout).toContain('"context_servers"');
57
+ });
58
+ it("codex writes to target when --yes passed", () => {
59
+ const target = join(dir, "codex.toml");
60
+ const r = run(["codex", "--yes", "--config-path", target]);
61
+ expect(r.status).toBe(0);
62
+ expect(existsSync(target)).toBe(true);
63
+ expect(readFileSync(target, "utf8")).toContain("[mcp_servers.drawio]");
64
+ });
65
+ it("codex refuses to overwrite non-empty target without --yes and returns exit 4", () => {
66
+ const target = join(dir, "codex.toml");
67
+ const first = run(["codex", "--yes", "--config-path", target]);
68
+ expect(first.status).toBe(0);
69
+ const modified = readFileSync(target, "utf8").replace("drawio-mcp-server", "drawio-mcp-server-old");
70
+ writeFileSync(target, modified);
71
+ const second = run(["codex", "--config-path", target]);
72
+ expect(second.status).toBe(4);
73
+ expect(second.stderr).toContain("Re-run with --yes");
74
+ });
75
+ it("all with per-host config paths writes to each, and never touches unoverridden adapters' real paths", () => {
76
+ const codexPath = join(dir, "codex.toml");
77
+ const zedPath = join(dir, "zed.json");
78
+ const r = run([
79
+ "all",
80
+ "--yes",
81
+ "--config-path",
82
+ `codex=${codexPath}`,
83
+ "--config-path",
84
+ `zed=${zedPath}`,
85
+ ]);
86
+ expect(r.status).toBe(0);
87
+ expect(existsSync(codexPath)).toBe(true);
88
+ expect(existsSync(zedPath)).toBe(true);
89
+ // opencode, claude-desktop, and claude-code got no --config-path
90
+ // override. With HOME sandboxed to a fresh temp dir, their
91
+ // defaultPaths() resolve to non-existent files, so detect() reports
92
+ // "absent" and applyAll() skips them entirely — proving isolation
93
+ // rather than just asserting the explicit overrides worked.
94
+ expect(existsSync(join(home, ".claude.json"))).toBe(false);
95
+ expect(existsSync(join(home, ".config", "Claude", "claude_desktop_config.json"))).toBe(false);
96
+ expect(existsSync(join(home, ".config", "opencode", "opencode.json"))).toBe(false);
97
+ });
98
+ it("returns exit 1 on unknown host", () => {
99
+ const r = run(["bogus"]);
100
+ expect(r.status).toBe(1);
101
+ expect(r.stderr).toContain("unknown host");
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -138,6 +138,7 @@ describe("HTTP transport (stateless per-request)", () => {
138
138
  await client.connect(transport);
139
139
  const tools = await client.listTools();
140
140
  expect(tools.tools.length).toBeGreaterThan(0);
141
+ expect(tools.tools.map((tool) => tool.name)).toEqual(expect.arrayContaining(["set-document-title", "save-document"]));
141
142
  await client.close();
142
143
  });
143
144
  it("handles multiple sequential HTTP client requests without reuse error", async () => {
@@ -1909,6 +1909,7 @@ var DrawMcp = (() => {
1909
1909
  );
1910
1910
  codec.decode(graphModelElement, model);
1911
1911
  }
1912
+ sync_live_current_page_state(ui2);
1912
1913
  return {
1913
1914
  success: true,
1914
1915
  message: `Diagram replaced successfully${filename ? ` from ${filename}` : ""}`,
@@ -2072,6 +2073,7 @@ var DrawMcp = (() => {
2072
2073
  } finally {
2073
2074
  model.endUpdate();
2074
2075
  }
2076
+ sync_live_current_page_state(ui2);
2075
2077
  return {
2076
2078
  success: true,
2077
2079
  message: `Diagram imported successfully (new page created)${filename ? `: ${filename}` : ""}`,
@@ -2481,6 +2483,78 @@ var DrawMcp = (() => {
2481
2483
  }
2482
2484
  }
2483
2485
 
2486
+ // src/tools/save-document/index.ts
2487
+ function normalize_optional_string2(value) {
2488
+ if (value === void 0 || value === null || value === "") {
2489
+ return null;
2490
+ }
2491
+ return String(value);
2492
+ }
2493
+ var save_document = (ui2) => {
2494
+ const saveAction = ui2?.actions?.get?.("save");
2495
+ if (!saveAction || typeof saveAction.funct !== "function") {
2496
+ throw new Error("The Draw.io save action is not available");
2497
+ }
2498
+ if (typeof saveAction.isEnabled === "function" && saveAction.isEnabled() !== true) {
2499
+ throw new Error("The Draw.io save action is currently disabled");
2500
+ }
2501
+ saveAction.funct();
2502
+ const file = ui2?.getCurrentFile?.();
2503
+ return {
2504
+ triggered: true,
2505
+ title: normalize_optional_string2(file?.getTitle?.()),
2506
+ mode: normalize_optional_string2(file?.getMode?.())
2507
+ };
2508
+ };
2509
+
2510
+ // src/tools/set-document-title/index.ts
2511
+ var DRAWIO_FILE_SUFFIX = /(\.drawio\.(?:svg|png)|\.drawio|\.xml|\.svg|\.png)$/i;
2512
+ function normalize_optional_string3(value) {
2513
+ if (value === void 0 || value === null || value === "") {
2514
+ return null;
2515
+ }
2516
+ return String(value);
2517
+ }
2518
+ function title_with_preserved_suffix(currentTitle, requestedTitle) {
2519
+ const title = normalize_optional_string3(requestedTitle)?.trim() ?? "";
2520
+ if (!title) {
2521
+ throw new Error("`title` must not be empty");
2522
+ }
2523
+ const currentSuffix = currentTitle?.match(DRAWIO_FILE_SUFFIX)?.[1] ?? "";
2524
+ if (!currentSuffix || DRAWIO_FILE_SUFFIX.test(title)) {
2525
+ return title;
2526
+ }
2527
+ return `${title}${currentSuffix}`;
2528
+ }
2529
+ function tool_error(error) {
2530
+ return error instanceof Error ? error : new Error(String(error));
2531
+ }
2532
+ var set_document_title = (ui2, options) => {
2533
+ const file = ui2?.getCurrentFile?.();
2534
+ if (!file) {
2535
+ throw new Error("No active Draw.io file is available");
2536
+ }
2537
+ if (typeof file.rename !== "function") {
2538
+ throw new Error("Document renaming is not supported by this storage mode");
2539
+ }
2540
+ const previousTitle = normalize_optional_string3(file.getTitle?.());
2541
+ const nextTitle = title_with_preserved_suffix(previousTitle, options.title);
2542
+ return new Promise(
2543
+ (resolve, reject) => {
2544
+ file.rename?.(
2545
+ nextTitle,
2546
+ () => {
2547
+ resolve({
2548
+ previous_title: previousTitle,
2549
+ title: normalize_optional_string3(file.getTitle?.()) ?? nextTitle
2550
+ });
2551
+ },
2552
+ (error) => reject(tool_error(error))
2553
+ );
2554
+ }
2555
+ );
2556
+ };
2557
+
2484
2558
  // src/tool-registry.ts
2485
2559
  var VISIBLE_PAGE_EXECUTION = {
2486
2560
  mode: "visible-page"
@@ -2788,6 +2862,16 @@ var DrawMcp = (() => {
2788
2862
  name: "rename-page",
2789
2863
  params: /* @__PURE__ */ new Set(["page", "name"]),
2790
2864
  handler: rename_page
2865
+ },
2866
+ {
2867
+ name: "set-document-title",
2868
+ params: /* @__PURE__ */ new Set(["title"]),
2869
+ handler: set_document_title
2870
+ },
2871
+ {
2872
+ name: "save-document",
2873
+ params: /* @__PURE__ */ new Set([]),
2874
+ handler: save_document
2791
2875
  }
2792
2876
  ];
2793
2877
  var toolDefinitions = rawToolDefinitions.map(
@@ -79,4 +79,111 @@ describe("real environment/import export", () => {
79
79
  await expectNoBrowserErrors(context, "import-export");
80
80
  await expectNoServerErrors(context, "import-export", logCountBefore);
81
81
  }, 180000);
82
+ it("keeps ui.currentPage.root in sync with graph.model.root after replace import (regression for #56)", async () => {
83
+ await resetDiagram(context);
84
+ context.browserMessages.length = 0;
85
+ const logCountBefore = context.logger.entries.length;
86
+ const importXml = '<mxGraphModel dx="0" dy="0" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="regression-1" value="Regression cell" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>';
87
+ const { payload: importResult } = await callToolJson(context, "import-diagram", {
88
+ data: importXml,
89
+ format: "xml",
90
+ mode: "replace",
91
+ filename: "regression.drawio",
92
+ });
93
+ expectToolSuccess(importResult);
94
+ await context.page.waitForFunction(() => {
95
+ const maybeWindow = window;
96
+ const graph = maybeWindow.ui?.editor?.graph;
97
+ const model = graph?.getModel?.();
98
+ const cells = Object.values(model?.cells ?? {});
99
+ return cells.some((cell) => cell?.value === "Regression cell");
100
+ });
101
+ const pageState = await context.page.evaluate(() => {
102
+ const maybeWindow = window;
103
+ const ui = maybeWindow.ui;
104
+ const page = ui?.currentPage;
105
+ const model = ui?.editor?.graph?.getModel?.();
106
+ const modelRoot = model?.getRoot?.() ?? model?.root ?? null;
107
+ return {
108
+ pageRootMatchesModelRoot: page?.root === modelRoot,
109
+ graphModelNodeIsNull: page?.graphModelNode == null,
110
+ pageRootHasImportedCell: (() => {
111
+ if (!page?.root || typeof page.root.getChildAt !== "function")
112
+ return null;
113
+ const rootChild = page.root.getChildAt(0);
114
+ if (!rootChild || typeof rootChild.getChildAt !== "function")
115
+ return null;
116
+ for (let i = 0; i < (rootChild.getChildCount?.() ?? 0); i++) {
117
+ const child = rootChild.getChildAt(i);
118
+ if (child?.value === "Regression cell")
119
+ return true;
120
+ }
121
+ return false;
122
+ })(),
123
+ };
124
+ });
125
+ expect(pageState.pageRootMatchesModelRoot).toBe(true);
126
+ expect(pageState.graphModelNodeIsNull).toBe(true);
127
+ expect(pageState.pageRootHasImportedCell).toBe(true);
128
+ // The page-root identity asserted above is a proxy for correctness. What a
129
+ // user actually loses when currentPage goes stale is the *serialized* file:
130
+ // drawio serializes through the Page abstraction, so a stale page root ends
131
+ // up in the saved bytes while the live graph (holding the import) does not.
132
+ // Assert the import survives serialization, not just the in-memory model.
133
+ const serializedFileData = await context.page.evaluate(() => {
134
+ const maybeWindow = window;
135
+ const ui = maybeWindow.ui;
136
+ try {
137
+ if (typeof ui?.getXmlFileData === "function" &&
138
+ typeof maybeWindow.mxUtils?.getXml === "function") {
139
+ return String(maybeWindow.mxUtils.getXml(ui.getXmlFileData(true, false, true)));
140
+ }
141
+ }
142
+ catch {
143
+ /* fall through to getFileData */
144
+ }
145
+ try {
146
+ if (typeof ui?.getFileData === "function") {
147
+ return String(ui.getFileData(true, null, null, null, true, null, null, null, null, true));
148
+ }
149
+ }
150
+ catch {
151
+ /* unsupported build */
152
+ }
153
+ return null;
154
+ });
155
+ if (serializedFileData !== null) {
156
+ expect(serializedFileData).toContain("Regression cell");
157
+ }
158
+ const survivesPageRoundTrip = await context.page.evaluate(async () => {
159
+ const maybeWindow = window;
160
+ const ui = maybeWindow.ui;
161
+ if (!ui?.insertPage || !ui?.selectPage || !ui?.currentPage) {
162
+ return { skipped: true };
163
+ }
164
+ const originalPage = ui.currentPage;
165
+ const scratch = ui.insertPage();
166
+ ui.selectPage(scratch);
167
+ await new Promise((r) => setTimeout(r, 20));
168
+ ui.selectPage(originalPage);
169
+ await new Promise((r) => setTimeout(r, 20));
170
+ const model = ui.editor.graph.getModel();
171
+ const cells = Object.values(model?.cells ?? {});
172
+ const stillPresent = cells.some((cell) => cell?.value === "Regression cell");
173
+ if (typeof ui.removePage === "function") {
174
+ try {
175
+ ui.removePage(scratch);
176
+ }
177
+ catch {
178
+ /* best-effort cleanup */
179
+ }
180
+ }
181
+ return { skipped: false, stillPresent };
182
+ });
183
+ if (!survivesPageRoundTrip.skipped) {
184
+ expect(survivesPageRoundTrip.stillPresent).toBe(true);
185
+ }
186
+ await expectNoBrowserErrors(context, "import-export");
187
+ await expectNoServerErrors(context, "import-export", logCountBefore);
188
+ }, 180000);
82
189
  });
@@ -134,6 +134,34 @@ describe("graceful shutdown releases WebSocket port", () => {
134
134
  const free = await isPortFree(extensionPort, HOST);
135
135
  expect(free).toBe(true);
136
136
  }, 20000);
137
+ it("keeps the HTTP port free in stdio-only mode", async () => {
138
+ const extensionPort = await getFreePort();
139
+ const httpPort = await getFreePort();
140
+ proc = spawnServer(extensionPort, httpPort);
141
+ const portTaken = await waitForPortHeld(extensionPort, HOST, 5000);
142
+ expect(portTaken).toBe(true);
143
+ // Give any (wrongly) eager HTTP listener time to bind before asserting.
144
+ await new Promise((r) => setTimeout(r, 1000));
145
+ expect(await isPortFree(httpPort, HOST)).toBe(true);
146
+ }, 20000);
147
+ it("exits fatally when the HTTP port cannot be bound in http transport", async () => {
148
+ const extensionPort = await getFreePort();
149
+ // Occupy the HTTP port so the server's bind attempt fails.
150
+ const blocker = createServer();
151
+ await new Promise((resolve) => {
152
+ blocker.listen({ port: 0, host: HOST }, () => resolve());
153
+ });
154
+ const takenPort = blocker.address().port;
155
+ proc = spawnServer(extensionPort, takenPort, "http");
156
+ let stderr = "";
157
+ proc.stderr.on("data", (chunk) => {
158
+ stderr += chunk.toString();
159
+ });
160
+ const exitCode = await waitForExit(proc, 10000);
161
+ expect(exitCode).toBe(1);
162
+ expect(stderr).toContain("Failed to bind HTTP server");
163
+ blocker.close();
164
+ }, 20000);
137
165
  it("releases extension port when stdio host closes stdin pipe", async () => {
138
166
  const extensionPort = await getFreePort();
139
167
  const httpPort = await getFreePort();
@@ -20,6 +20,8 @@ describe("shared tool registry", () => {
20
20
  "create-page",
21
21
  "copy-page",
22
22
  "rename-page",
23
+ "set-document-title",
24
+ "save-document",
23
25
  "import-mermaid",
24
26
  ]));
25
27
  });
@@ -38,6 +40,9 @@ describe("shared tool registry", () => {
38
40
  expect(registry.get("list-paged-model")?.params.has("target_document")).toBe(true);
39
41
  expect(registry.get("rename-page")?.params.has("page")).toBe(true);
40
42
  expect(registry.get("rename-page")?.params.has("target_document")).toBe(true);
43
+ expect(registry.get("set-document-title")?.params.has("title")).toBe(true);
44
+ expect(registry.get("set-document-title")?.params.has("target_document")).toBe(true);
45
+ expect(registry.get("save-document")?.params.has("target_document")).toBe(true);
41
46
  expect(registry.get("copy-page")?.params.has("page")).toBe(true);
42
47
  expect(registry.get("copy-page")?.params.has("target_document")).toBe(true);
43
48
  expect(registry.get("import-mermaid")?.params.has("target_page")).toBe(true);
@@ -45,6 +50,58 @@ describe("shared tool registry", () => {
45
50
  expect(registry.get("get-shape-by-name")?.params.has("target_page")).toBe(false);
46
51
  expect(registry.get("get-shape-by-name")?.params.has("target_document")).toBe(true);
47
52
  });
53
+ it("renames the current document while preserving its file extension", async () => {
54
+ await activateDocument();
55
+ const toolDefinitions = await loadToolDefinitions();
56
+ const registry = new Map(toolDefinitions.map((definition) => [definition.name, definition]));
57
+ const handler = registry.get("set-document-title")?.handler;
58
+ expect(handler).toBeDefined();
59
+ let title = "Untitled Diagram.drawio";
60
+ const rename = jest.fn((nextTitle, success, _error) => {
61
+ title = nextTitle;
62
+ success();
63
+ });
64
+ const ui = {
65
+ getCurrentFile: jest.fn(() => ({
66
+ getTitle: () => title,
67
+ rename,
68
+ })),
69
+ };
70
+ await expect(handler?.(ui, {
71
+ target_document: { id: "doc-1" },
72
+ title: "Architecture",
73
+ })).resolves.toEqual({
74
+ previous_title: "Untitled Diagram.drawio",
75
+ title: "Architecture.drawio",
76
+ });
77
+ expect(rename).toHaveBeenCalledWith("Architecture.drawio", expect.any(Function), expect.any(Function));
78
+ });
79
+ it("triggers the editor's existing save action", async () => {
80
+ await activateDocument();
81
+ const toolDefinitions = await loadToolDefinitions();
82
+ const registry = new Map(toolDefinitions.map((definition) => [definition.name, definition]));
83
+ const handler = registry.get("save-document")?.handler;
84
+ expect(handler).toBeDefined();
85
+ const save = jest.fn();
86
+ const ui = {
87
+ actions: {
88
+ get: jest.fn((name) => name === "save" ? { funct: save } : null),
89
+ },
90
+ getCurrentFile: jest.fn(() => ({
91
+ getTitle: () => "Architecture.drawio",
92
+ getMode: () => "device",
93
+ })),
94
+ };
95
+ expect(handler?.(ui, {
96
+ target_document: { id: "doc-1" },
97
+ })).toEqual({
98
+ triggered: true,
99
+ title: "Architecture.drawio",
100
+ mode: "device",
101
+ });
102
+ expect(ui.actions.get).toHaveBeenCalledWith("save");
103
+ expect(save).toHaveBeenCalledTimes(1);
104
+ });
48
105
  it("classifies background-safe and UI-bound page tools in the shared registry", async () => {
49
106
  const toolDefinitions = await loadToolDefinitions();
50
107
  const registry = new Map(toolDefinitions.map((definition) => [definition.name, definition]));
@@ -22,10 +22,12 @@ import { registerListPagesTool } from "./list-pages.js";
22
22
  import { registerListPagedModelTool } from "./list-paged-model.js";
23
23
  import { registerMoveCellToLayerTool } from "./move-cell-to-layer.js";
24
24
  import { registerRenamePageTool } from "./rename-page.js";
25
+ import { registerSaveDocumentTool } from "./save-document.js";
25
26
  import { registerSetActiveLayerTool } from "./set-active-layer.js";
26
27
  import { registerSetCellDataTool } from "./set-cell-data.js";
27
28
  import { registerSetCellParentTool } from "./set-cell-parent.js";
28
29
  import { registerSetCellShapeTool } from "./set-cell-shape.js";
30
+ import { registerSetDocumentTitleTool } from "./set-document-title.js";
29
31
  const registrars = [
30
32
  registerGetSelectedCellTool,
31
33
  registerAddRectangleTool,
@@ -55,6 +57,8 @@ const registrars = [
55
57
  registerCreatePageTool,
56
58
  registerCopyPageTool,
57
59
  registerRenamePageTool,
60
+ registerSetDocumentTitleTool,
61
+ registerSaveDocumentTool,
58
62
  ];
59
63
  export function registerTools(...args) {
60
64
  for (const register of registrars) {
@@ -0,0 +1,5 @@
1
+ import { default_tool } from "../tool.js";
2
+ export const TOOL_save_document = "save-document";
3
+ export const registerSaveDocumentTool = (server, context) => {
4
+ server.tool(TOOL_save_document, "Triggers Draw.io's existing File > Save action for the current document. The active storage provider may display an authentication, conflict, or Save As prompt that requires user interaction.", {}, default_tool(TOOL_save_document, context, { queue: true }));
5
+ };
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { default_tool } from "../tool.js";
3
+ export const TOOL_set_document_title = "set-document-title";
4
+ export const registerSetDocumentTitleTool = (server, context) => {
5
+ server.tool(TOOL_set_document_title, "Renames the current Draw.io document through its active storage provider while preserving the existing file extension when one is present.", {
6
+ title: z
7
+ .string()
8
+ .trim()
9
+ .min(1)
10
+ .describe("New document title, with or without the existing extension"),
11
+ }, default_tool(TOOL_set_document_title, context, { queue: true }));
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawio-mcp-server",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Provides Draw.io services to MCP Clients",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -31,11 +31,11 @@
31
31
  "url": "git+https://github.com/lgazo/drawio-mcp-server.git"
32
32
  },
33
33
  "dependencies": {
34
- "@hono/node-server": "1.19.13",
35
- "@modelcontextprotocol/sdk": "1.29.0",
34
+ "@hono/node-server": "1.19.17",
35
+ "@modelcontextprotocol/sdk": "1.30.0",
36
36
  "cachedir": "2.4.0",
37
- "hono": "4.12.25",
38
- "nanoid": "5.1.6",
37
+ "hono": "4.13.5",
38
+ "nanoid": "5.1.16",
39
39
  "node-forge": "1.4.0",
40
40
  "unzipper": "0.12.3",
41
41
  "ws": "8.21.0",
@@ -45,22 +45,26 @@
45
45
  "@biomejs/biome": "2.4.13",
46
46
  "@jest/globals": "30.2.0",
47
47
  "@playwright/test": "1.59.1",
48
+ "@types/diff": "8.0.0",
48
49
  "@types/jest": "30.0.0",
49
50
  "@types/node": "25.0.3",
50
51
  "@types/node-forge": "1.3.11",
51
52
  "@types/unzipper": "0.10.11",
52
53
  "@types/ws": "8.18.1",
53
54
  "concurrently": "9.1.2",
55
+ "diff": "8.0.4",
54
56
  "esbuild": "0.25.12",
55
57
  "globals": "16.5.0",
56
58
  "jest": "30.2.0",
57
59
  "jest-environment-node": "30.2.0",
60
+ "jsonc-parser": "3.3.1",
58
61
  "prettier": "3.7.4",
59
62
  "rimraf": "6.1.3",
63
+ "smol-toml": "1.8.0",
60
64
  "ts-jest": "29.4.9",
61
65
  "typescript": "5.9.3",
62
66
  "drawio-mcp-dev-proxy": "1.0.0",
63
- "drawio-mcp-plugin": "2.2.0"
67
+ "drawio-mcp-plugin": "2.3.0"
64
68
  },
65
69
  "scripts": {
66
70
  "vendor:compat": "rimraf src/vendored/compat && mkdir -p src/vendored/compat && cp ../drawio-mcp-compat/src/index.ts src/vendored/compat/index.ts",