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.
- package/README.md +29 -3
- package/build/assets/downloader.js +16 -17
- package/build/config.js +253 -5
- package/build/config.test.js +449 -1
- package/build/emitter_bus.js +3 -0
- package/build/emitter_bus.test.js +7 -0
- package/build/index.capabilities.test.js +45 -0
- package/build/index.js +496 -106
- package/build/install-desktop-plugin.js +56 -0
- package/build/install-desktop-plugin.test.js +66 -0
- package/build/multi-transport.test.js +277 -0
- package/build/plugin/mcp-plugin.js +2180 -1105
- package/build/prefetch-assets.js +6 -5
- package/build/real-environment/document-targeting.test.js +124 -0
- package/build/real-environment/export-diagram.test.js +473 -0
- package/build/real-environment/harness.js +120 -32
- package/build/real-environment/import-mermaid.test.js +81 -0
- package/build/real-environment/pages-and-concurrency.test.js +448 -0
- package/build/real-environment/shapes.test.js +58 -0
- package/build/real-environment/tools.js +51 -6
- package/build/register-tool.js +31 -0
- package/build/request_queue.js +29 -0
- package/build/request_queue.test.js +98 -0
- package/build/stdio-transport-purity.test.js +88 -0
- package/build/strip-schema.js +61 -0
- package/build/tls/expiry.js +14 -0
- package/build/tls/expiry.test.js +54 -0
- package/build/tls/generate.js +123 -0
- package/build/tls/generate.test.js +115 -0
- package/build/tls/index.js +80 -0
- package/build/tls/index.test.js +141 -0
- package/build/tls/install-hint.js +45 -0
- package/build/tls/install-hint.test.js +32 -0
- package/build/tls/load.js +18 -0
- package/build/tls/load.test.js +40 -0
- package/build/tls/paths.js +30 -0
- package/build/tls/paths.test.js +53 -0
- package/build/tls/san.js +27 -0
- package/build/tls/san.test.js +72 -0
- package/build/tool-registry.test.js +823 -0
- package/build/tool.js +74 -15
- package/build/tool.test.js +250 -7
- package/build/tools/add-cell-of-shape.js +4 -2
- package/build/tools/add-edge.js +3 -1
- package/build/tools/add-rectangle.js +4 -2
- package/build/tools/copy-page.js +13 -0
- package/build/tools/create-layer.js +4 -2
- package/build/tools/create-page.js +8 -0
- package/build/tools/delete-cell-by-id.js +4 -2
- package/build/tools/edit-cell.js +3 -1
- package/build/tools/edit-edge.js +3 -1
- package/build/tools/export-diagram.js +7 -3
- package/build/tools/get-active-layer.js +4 -1
- package/build/tools/get-current-page.js +5 -0
- package/build/tools/get-selected-cell.js +4 -1
- package/build/tools/import-diagram.js +12 -2
- package/build/tools/import-mermaid.js +31 -0
- package/build/tools/index.js +14 -0
- package/build/tools/list-documents.js +17 -0
- package/build/tools/list-layers.js +4 -1
- package/build/tools/list-paged-model.js +4 -3
- package/build/tools/list-pages.js +5 -0
- package/build/tools/move-cell-to-layer.js +3 -1
- package/build/tools/rename-page.js +10 -0
- package/build/tools/set-active-layer.js +4 -2
- package/build/tools/set-cell-data.js +3 -1
- package/build/tools/set-cell-parent.js +3 -1
- package/build/tools/set-cell-shape.js +3 -1
- package/build/tools/shared.js +35 -0
- package/build/tools/shared.test.js +29 -0
- 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({},
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
86
|
+
testServer.connect(serverTransport),
|
|
32
87
|
client.connect(clientTransport),
|
|
33
88
|
]);
|
|
34
89
|
const browser = await chromium.launch({ headless: true });
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
browserMessages.push({
|
|
38
|
-
type: message.type(),
|
|
39
|
-
text: message.text(),
|
|
40
|
-
});
|
|
90
|
+
const browserContext = await browser.newContext({
|
|
91
|
+
ignoreHTTPSErrors: useHttps,
|
|
41
92
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
await context.
|
|
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
|
+
});
|