drawio-mcp-server 2.0.3 → 2.1.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.
Files changed (71) hide show
  1. package/README.md +29 -3
  2. package/build/assets/downloader.js +16 -17
  3. package/build/config.js +253 -5
  4. package/build/config.test.js +449 -1
  5. package/build/emitter_bus.js +3 -0
  6. package/build/emitter_bus.test.js +7 -0
  7. package/build/index.capabilities.test.js +45 -0
  8. package/build/index.js +496 -106
  9. package/build/install-desktop-plugin.js +56 -0
  10. package/build/install-desktop-plugin.test.js +66 -0
  11. package/build/multi-transport.test.js +277 -0
  12. package/build/plugin/mcp-plugin.js +2180 -1105
  13. package/build/prefetch-assets.js +6 -5
  14. package/build/real-environment/document-targeting.test.js +124 -0
  15. package/build/real-environment/export-diagram.test.js +473 -0
  16. package/build/real-environment/harness.js +120 -32
  17. package/build/real-environment/import-mermaid.test.js +81 -0
  18. package/build/real-environment/pages-and-concurrency.test.js +448 -0
  19. package/build/real-environment/shapes.test.js +58 -0
  20. package/build/real-environment/tools.js +51 -6
  21. package/build/register-tool.js +31 -0
  22. package/build/request_queue.js +29 -0
  23. package/build/request_queue.test.js +98 -0
  24. package/build/stdio-transport-purity.test.js +88 -0
  25. package/build/strip-schema.js +61 -0
  26. package/build/tls/expiry.js +14 -0
  27. package/build/tls/expiry.test.js +54 -0
  28. package/build/tls/generate.js +123 -0
  29. package/build/tls/generate.test.js +115 -0
  30. package/build/tls/index.js +80 -0
  31. package/build/tls/index.test.js +141 -0
  32. package/build/tls/install-hint.js +45 -0
  33. package/build/tls/install-hint.test.js +32 -0
  34. package/build/tls/load.js +18 -0
  35. package/build/tls/load.test.js +40 -0
  36. package/build/tls/paths.js +30 -0
  37. package/build/tls/paths.test.js +53 -0
  38. package/build/tls/san.js +27 -0
  39. package/build/tls/san.test.js +72 -0
  40. package/build/tool-registry.test.js +823 -0
  41. package/build/tool.js +74 -15
  42. package/build/tool.test.js +250 -7
  43. package/build/tools/add-cell-of-shape.js +4 -2
  44. package/build/tools/add-edge.js +3 -1
  45. package/build/tools/add-rectangle.js +4 -2
  46. package/build/tools/copy-page.js +13 -0
  47. package/build/tools/create-layer.js +4 -2
  48. package/build/tools/create-page.js +8 -0
  49. package/build/tools/delete-cell-by-id.js +4 -2
  50. package/build/tools/edit-cell.js +3 -1
  51. package/build/tools/edit-edge.js +3 -1
  52. package/build/tools/export-diagram.js +7 -3
  53. package/build/tools/get-active-layer.js +4 -1
  54. package/build/tools/get-current-page.js +5 -0
  55. package/build/tools/get-selected-cell.js +4 -1
  56. package/build/tools/import-diagram.js +12 -2
  57. package/build/tools/import-mermaid.js +31 -0
  58. package/build/tools/index.js +14 -0
  59. package/build/tools/list-documents.js +17 -0
  60. package/build/tools/list-layers.js +4 -1
  61. package/build/tools/list-paged-model.js +4 -3
  62. package/build/tools/list-pages.js +5 -0
  63. package/build/tools/move-cell-to-layer.js +3 -1
  64. package/build/tools/rename-page.js +10 -0
  65. package/build/tools/set-active-layer.js +4 -2
  66. package/build/tools/set-cell-data.js +3 -1
  67. package/build/tools/set-cell-parent.js +3 -1
  68. package/build/tools/set-cell-shape.js +3 -1
  69. package/build/tools/shared.js +35 -0
  70. package/build/tools/shared.test.js +29 -0
  71. package/package.json +22 -14
@@ -1,58 +1,109 @@
1
1
  import { chromium, } from "@playwright/test";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { spawnCaddy } from "drawio-mcp-dev-proxy";
5
+ import { createServer } from "node:net";
4
6
  import { ensureAssets } from "../assets/index.js";
5
- import { getHttpFeatureConfig } from "../config.js";
7
+ import { defaultConfig, getHttpFeatureConfig, } from "../config.js";
6
8
  import { createDrawioMcpApp } from "../index.js";
7
9
  import { MemoryLogger } from "./logger.js";
8
10
  import { createArtifactRunDir } from "./screenshot.js";
11
+ async function getFreePort() {
12
+ return new Promise((resolve, reject) => {
13
+ const srv = createServer();
14
+ srv.once("error", reject);
15
+ srv.listen(0, "127.0.0.1", () => {
16
+ const addr = srv.address();
17
+ if (addr && typeof addr === "object") {
18
+ const port = addr.port;
19
+ srv.close(() => resolve(port));
20
+ }
21
+ else {
22
+ srv.close(() => reject(new Error("could not resolve free port")));
23
+ }
24
+ });
25
+ });
26
+ }
9
27
  export async function createRealEnvironmentContext() {
10
28
  const logger = new MemoryLogger();
11
29
  const browserMessages = [];
12
30
  const artifactRunDir = await createArtifactRunDir();
13
- await ensureAssets({}, () => undefined);
14
- const app = createDrawioMcpApp({ log: logger });
31
+ await ensureAssets({}, logger);
32
+ const app = createDrawioMcpApp({ config: defaultConfig(), log: logger });
15
33
  const wsServer = await app.startWebSocketServer(0);
16
34
  const wsPort = Number(wsServer.address().port);
17
- const config = {
18
- extensionPort: wsPort,
19
- httpPort: 0,
20
- transports: ["http"],
21
- editorEnabled: true,
22
- };
23
- const startedHttp = await app.startHttpServer(0, config, getHttpFeatureConfig(config));
24
- const httpPort = startedHttp.port;
35
+ const useHttps = process.env.HARNESS_HTTPS === "1";
36
+ let caddy = null;
37
+ let proxyPort = null;
38
+ let webSocketUrl;
39
+ let upstreamHttpPort;
40
+ if (useHttps) {
41
+ proxyPort = await getFreePort();
42
+ const httpUpstream = await getFreePort();
43
+ caddy = await spawnCaddy({
44
+ proxyPort,
45
+ httpUpstream,
46
+ wsUpstream: wsPort,
47
+ });
48
+ webSocketUrl = `wss://localhost:${proxyPort}/ws`;
49
+ const config = {
50
+ extensionPort: wsPort,
51
+ httpPort: httpUpstream,
52
+ transports: ["http"],
53
+ editorEnabled: true,
54
+ webSocketUrl,
55
+ logger: "console",
56
+ tlsEnabled: false,
57
+ tlsAuto: false,
58
+ };
59
+ await app.startHttpServer(httpUpstream, config, getHttpFeatureConfig(config));
60
+ upstreamHttpPort = httpUpstream;
61
+ }
62
+ else {
63
+ const config = {
64
+ extensionPort: wsPort,
65
+ httpPort: 0,
66
+ transports: ["http"],
67
+ editorEnabled: true,
68
+ logger: "console",
69
+ tlsEnabled: false,
70
+ tlsAuto: false,
71
+ };
72
+ const startedHttp = await app.startHttpServer(0, config, getHttpFeatureConfig(config));
73
+ upstreamHttpPort = startedHttp.port;
74
+ }
75
+ const httpPort = useHttps ? proxyPort : upstreamHttpPort;
76
+ const baseUrl = useHttps
77
+ ? `https://localhost:${proxyPort}/`
78
+ : `http://localhost:${upstreamHttpPort}/`;
25
79
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
26
80
  const client = new Client({
27
81
  name: "real-environment-test",
28
82
  version: "1.0.0",
29
83
  });
84
+ const testServer = app.createMcpServer();
30
85
  await Promise.all([
31
- app.server.connect(serverTransport),
86
+ testServer.connect(serverTransport),
32
87
  client.connect(clientTransport),
33
88
  ]);
34
89
  const browser = await chromium.launch({ headless: true });
35
- const page = await browser.newPage();
36
- page.on("console", (message) => {
37
- browserMessages.push({
38
- type: message.type(),
39
- text: message.text(),
40
- });
90
+ const browserContext = await browser.newContext({
91
+ ignoreHTTPSErrors: useHttps,
41
92
  });
42
- await page.addInitScript((port) => {
43
- const store = {
44
- websocketPort: port,
45
- };
46
- window.__DRAWIO_MCP_TEST_HOOKS__ = true;
47
- window.localStorage.setItem("drawio-mcp-plugin-config", JSON.stringify(store));
48
- window.localStorage.setItem("drawio-mcp-config", JSON.stringify(store));
49
- }, wsPort);
50
- await page.goto(`http://localhost:${httpPort}/`, {
51
- waitUntil: "domcontentloaded",
93
+ const page = await openConnectedDrawioPage({
94
+ browserContext,
95
+ baseUrl,
96
+ webSocketUrl,
97
+ httpPort,
98
+ wsPort,
99
+ browserMessages,
52
100
  });
53
- await waitForPluginReady(page);
101
+ const proxy = caddy
102
+ ? { stop: () => caddy.stop(), proxyPort: caddy.proxyPort }
103
+ : null;
54
104
  return {
55
105
  browser,
106
+ browserContext,
56
107
  page,
57
108
  client,
58
109
  app,
@@ -61,12 +112,48 @@ export async function createRealEnvironmentContext() {
61
112
  artifactRunDir,
62
113
  httpPort,
63
114
  wsPort,
115
+ baseUrl,
116
+ proxy,
64
117
  };
65
118
  }
119
+ export async function openConnectedDrawioPage({ browser, browserContext, baseUrl, webSocketUrl, httpPort, wsPort, browserMessages, }) {
120
+ const page = browserContext
121
+ ? await browserContext.newPage()
122
+ : await browser.newPage();
123
+ const editorUrl = baseUrl ?? `http://localhost:${httpPort}/`;
124
+ if (browserMessages) {
125
+ page.on("console", (message) => {
126
+ browserMessages.push({
127
+ type: message.type(),
128
+ text: message.text(),
129
+ });
130
+ });
131
+ }
132
+ await page.addInitScript((args) => {
133
+ const store = {
134
+ websocketPort: args.port,
135
+ };
136
+ if (args.websocketUrl) {
137
+ store.websocketUrl = args.websocketUrl;
138
+ }
139
+ window.__DRAWIO_MCP_TEST_HOOKS__ = true;
140
+ window.localStorage.setItem("drawio-mcp-plugin-config", JSON.stringify(store));
141
+ window.localStorage.setItem("drawio-mcp-config", JSON.stringify(store));
142
+ }, { port: wsPort, websocketUrl: webSocketUrl });
143
+ await page.goto(editorUrl, { waitUntil: "domcontentloaded" });
144
+ await waitForPluginReady(page);
145
+ return page;
146
+ }
66
147
  export async function disposeRealEnvironmentContext(context) {
67
- await context.client.close();
68
- await context.browser.close();
69
- await context.app.close();
148
+ if (!context)
149
+ return;
150
+ await context.client?.close();
151
+ await context.browserContext?.close();
152
+ await context.browser?.close();
153
+ await context.app?.close();
154
+ if (context.proxy) {
155
+ await context.proxy.stop();
156
+ }
70
157
  }
71
158
  export async function waitForPluginReady(page) {
72
159
  await page.waitForFunction(() => {
@@ -159,6 +246,7 @@ export async function resetDiagram(context) {
159
246
  await context.client.callTool({
160
247
  name: "import-diagram",
161
248
  arguments: {
249
+ target_page: { index: 0 },
162
250
  data: emptyDiagram,
163
251
  format: "xml",
164
252
  mode: "replace",
@@ -0,0 +1,81 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ const FLOWCHART = "graph TD\nA[Start] --> B[Stop]";
6
+ describe("real environment/import-mermaid", () => {
7
+ let context;
8
+ beforeAll(async () => {
9
+ context = await createRealEnvironmentContext();
10
+ }, 180000);
11
+ afterAll(async () => {
12
+ await disposeRealEnvironmentContext(context);
13
+ });
14
+ it("native mode converts a flowchart to native cells on the target page", async () => {
15
+ await resetDiagram(context);
16
+ context.browserMessages.length = 0;
17
+ const logCountBefore = context.logger.entries.length;
18
+ const { payload } = await callToolJson(context, "import-mermaid", {
19
+ target_page: { index: 0 },
20
+ mermaid_source: FLOWCHART,
21
+ mode: "native",
22
+ insert_mode: "add",
23
+ });
24
+ expect(payload.success).toBe(true);
25
+ expect(payload.result?.mode).toBe("native");
26
+ expect(typeof payload.result?.xml).toBe("string");
27
+ expect(payload.result?.xml ?? "").toContain("mxGraphModel");
28
+ await context.page.waitForFunction(() => {
29
+ const ui = window.ui;
30
+ const model = ui?.editor?.graph?.getModel?.();
31
+ const cells = Object.values(model?.cells ?? {});
32
+ return cells.some((cell) => cell?.vertex === true);
33
+ });
34
+ await expectNoBrowserErrors(context, "import-mermaid:native");
35
+ await expectNoServerErrors(context, "import-mermaid:native", logCountBefore);
36
+ }, 180000);
37
+ it("embed mode produces XML carrying the mermaidData attribute", async () => {
38
+ await resetDiagram(context);
39
+ const { payload } = await callToolJson(context, "import-mermaid", {
40
+ target_page: { index: 0 },
41
+ mermaid_source: FLOWCHART,
42
+ mode: "embed",
43
+ insert_mode: "replace",
44
+ });
45
+ expect(payload.success).toBe(true);
46
+ expect(payload.result?.mode).toBe("embed");
47
+ expect(payload.result?.xml ?? "").toContain("mermaidData");
48
+ expect(payload.result?.xml ?? "").toContain("shape=image");
49
+ }, 180000);
50
+ it("requires target_page for replace and add modes", async () => {
51
+ await resetDiagram(context);
52
+ const result = await context.client.callTool({
53
+ name: "import-mermaid",
54
+ arguments: {
55
+ mermaid_source: FLOWCHART,
56
+ mode: "native",
57
+ insert_mode: "add",
58
+ },
59
+ });
60
+ expect(result.isError).toBe(true);
61
+ const content = result.content;
62
+ expect(String(content[0]?.text ?? "")).toContain("`target_page` is required");
63
+ }, 180000);
64
+ it("allows new-page imports without target_page", async () => {
65
+ await resetDiagram(context);
66
+ const { payload: beforePagesPayload } = await callToolJson(context, "list-pages", {});
67
+ expect(beforePagesPayload.success).toBe(true);
68
+ const { payload } = await callToolJson(context, "import-mermaid", {
69
+ mermaid_source: FLOWCHART,
70
+ mode: "native",
71
+ insert_mode: "new-page",
72
+ });
73
+ expect(payload.success).toBe(true);
74
+ expect(payload.result?.mode).toBe("native");
75
+ expect(payload.result?.xml ?? "").toContain("mxGraphModel");
76
+ const { payload: afterPagesPayload } = await callToolJson(context, "list-pages", {});
77
+ expect(afterPagesPayload.success).toBe(true);
78
+ expect(afterPagesPayload.result).toHaveLength(beforePagesPayload.result.length + 1);
79
+ expect(afterPagesPayload.result.at(-1)?.is_current).toBe(true);
80
+ }, 180000);
81
+ });