drawio-mcp-server 2.0.4 → 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 +416 -82
- package/build/install-desktop-plugin.js +56 -0
- package/build/install-desktop-plugin.test.js +66 -0
- package/build/multi-transport.test.js +117 -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 +1 -1
- package/build/real-environment/harness.js +118 -31
- 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 +12 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Resolve drawio-desktop's plugins directory for the current platform.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors Electron's `app.getPath("userData")` for the productName "draw.io":
|
|
11
|
+
* linux: $XDG_CONFIG_HOME/draw.io/plugins (defaults to ~/.config/draw.io/plugins)
|
|
12
|
+
* darwin: ~/Library/Application Support/draw.io/plugins
|
|
13
|
+
* win32: %APPDATA%/draw.io/plugins
|
|
14
|
+
*/
|
|
15
|
+
export function resolveDrawioPluginsDir(platform = process.platform, env = process.env) {
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
const appData = env.APPDATA;
|
|
18
|
+
if (!appData || appData.length === 0) {
|
|
19
|
+
throw new Error("APPDATA environment variable is not set; cannot locate drawio-desktop plugins directory.");
|
|
20
|
+
}
|
|
21
|
+
return join(appData, "draw.io", "plugins");
|
|
22
|
+
}
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
return join(homedir(), "Library", "Application Support", "draw.io", "plugins");
|
|
25
|
+
}
|
|
26
|
+
// linux + everything else: XDG
|
|
27
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
28
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
29
|
+
return join(base, "draw.io", "plugins");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Source path of the bundled mcp-plugin.js produced by the server build.
|
|
33
|
+
* Same resolution style as `assets/manager.ts:getLocalPluginPath`.
|
|
34
|
+
*/
|
|
35
|
+
function getBundledPluginPath() {
|
|
36
|
+
return join(__dirname, "..", "build", "plugin", "mcp-plugin.js");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Copy the bundled mcp-plugin.js into drawio-desktop's plugins directory.
|
|
40
|
+
*
|
|
41
|
+
* Always overwrites an existing file (plugin version tracks server version).
|
|
42
|
+
*
|
|
43
|
+
* @param opts.targetDir override the resolved plugins directory (used by tests)
|
|
44
|
+
*/
|
|
45
|
+
export async function installDesktopPlugin(opts = {}) {
|
|
46
|
+
const pluginsDir = opts.targetDir ?? resolveDrawioPluginsDir();
|
|
47
|
+
const source = getBundledPluginPath();
|
|
48
|
+
if (!existsSync(source)) {
|
|
49
|
+
throw new Error(`Bundled mcp-plugin.js not found at ${source}. Run "pnpm --filter drawio-mcp-server build" first.`);
|
|
50
|
+
}
|
|
51
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
52
|
+
const installedPath = join(pluginsDir, "mcp-plugin.js");
|
|
53
|
+
const overwrote = existsSync(installedPath);
|
|
54
|
+
await copyFile(source, installedPath);
|
|
55
|
+
return { pluginsDir, installedPath, overwrote };
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "@jest/globals";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, existsSync, } from "node:fs";
|
|
3
|
+
import { tmpdir, homedir } from "node:os";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveDrawioPluginsDir, installDesktopPlugin, } from "./install-desktop-plugin.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
describe("resolveDrawioPluginsDir", () => {
|
|
9
|
+
test("linux with XDG_CONFIG_HOME unset uses ~/.config", () => {
|
|
10
|
+
const result = resolveDrawioPluginsDir("linux", {});
|
|
11
|
+
expect(result).toBe(join(homedir(), ".config", "draw.io", "plugins"));
|
|
12
|
+
});
|
|
13
|
+
test("linux with XDG_CONFIG_HOME set uses it", () => {
|
|
14
|
+
const result = resolveDrawioPluginsDir("linux", {
|
|
15
|
+
XDG_CONFIG_HOME: "/custom/xdg",
|
|
16
|
+
});
|
|
17
|
+
expect(result).toBe("/custom/xdg/draw.io/plugins");
|
|
18
|
+
});
|
|
19
|
+
test("darwin uses Library/Application Support", () => {
|
|
20
|
+
const result = resolveDrawioPluginsDir("darwin", {});
|
|
21
|
+
expect(result).toBe(join(homedir(), "Library", "Application Support", "draw.io", "plugins"));
|
|
22
|
+
});
|
|
23
|
+
test("win32 uses APPDATA", () => {
|
|
24
|
+
const result = resolveDrawioPluginsDir("win32", {
|
|
25
|
+
APPDATA: "C:\\Users\\test\\AppData\\Roaming",
|
|
26
|
+
});
|
|
27
|
+
expect(result).toBe(join("C:\\Users\\test\\AppData\\Roaming", "draw.io", "plugins"));
|
|
28
|
+
});
|
|
29
|
+
test("win32 without APPDATA throws", () => {
|
|
30
|
+
expect(() => resolveDrawioPluginsDir("win32", {})).toThrow(/APPDATA/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("installDesktopPlugin", () => {
|
|
34
|
+
// The compiled tests live in build/, so __dirname is .../build, which makes
|
|
35
|
+
// getBundledPluginPath() resolve to .../build/../build/plugin/mcp-plugin.js
|
|
36
|
+
// i.e. .../build/plugin/mcp-plugin.js — same path the build script populates.
|
|
37
|
+
const expectedSource = join(__dirname, "..", "build", "plugin", "mcp-plugin.js");
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
if (!existsSync(expectedSource)) {
|
|
40
|
+
mkdirSync(dirname(expectedSource), { recursive: true });
|
|
41
|
+
writeFileSync(expectedSource, "// fixture mcp-plugin.js\n");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test("copies bundled plugin to target dir", async () => {
|
|
45
|
+
const targetDir = mkdtempSync(join(tmpdir(), "drawio-plugin-install-"));
|
|
46
|
+
const result = await installDesktopPlugin({ targetDir });
|
|
47
|
+
expect(result.pluginsDir).toBe(targetDir);
|
|
48
|
+
expect(result.installedPath).toBe(join(targetDir, "mcp-plugin.js"));
|
|
49
|
+
expect(result.overwrote).toBe(false);
|
|
50
|
+
expect(existsSync(result.installedPath)).toBe(true);
|
|
51
|
+
expect(readFileSync(result.installedPath, "utf8")).toBe(readFileSync(expectedSource, "utf8"));
|
|
52
|
+
});
|
|
53
|
+
test("second install overwrites and reports overwrote: true", async () => {
|
|
54
|
+
const targetDir = mkdtempSync(join(tmpdir(), "drawio-plugin-install-"));
|
|
55
|
+
await installDesktopPlugin({ targetDir });
|
|
56
|
+
const result = await installDesktopPlugin({ targetDir });
|
|
57
|
+
expect(result.overwrote).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
test("creates plugins dir if missing", async () => {
|
|
60
|
+
const parent = mkdtempSync(join(tmpdir(), "drawio-plugin-install-"));
|
|
61
|
+
const targetDir = join(parent, "nested", "plugins");
|
|
62
|
+
expect(existsSync(targetDir)).toBe(false);
|
|
63
|
+
const result = await installDesktopPlugin({ targetDir });
|
|
64
|
+
expect(existsSync(result.installedPath)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import * as https from "node:https";
|
|
3
|
+
import { Socket } from "node:net";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { connect as tlsConnect } from "node:tls";
|
|
1
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
8
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
9
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
10
|
import { createDrawioMcpApp } from "./index.js";
|
|
5
11
|
import { MemoryLogger } from "./real-environment/logger.js";
|
|
12
|
+
import { defaultConfig } from "./config.js";
|
|
6
13
|
describe("multi-transport support", () => {
|
|
7
14
|
let app;
|
|
8
15
|
let logger;
|
|
@@ -105,6 +112,9 @@ describe("HTTP transport (stateless per-request)", () => {
|
|
|
105
112
|
httpPort: 0,
|
|
106
113
|
transports: ["http"],
|
|
107
114
|
editorEnabled: false,
|
|
115
|
+
logger: "console",
|
|
116
|
+
tlsEnabled: false,
|
|
117
|
+
tlsAuto: false,
|
|
108
118
|
};
|
|
109
119
|
const features = {
|
|
110
120
|
enableMcp: true,
|
|
@@ -158,3 +168,110 @@ describe("HTTP transport (stateless per-request)", () => {
|
|
|
158
168
|
await expect(app.close()).resolves.not.toThrow();
|
|
159
169
|
});
|
|
160
170
|
});
|
|
171
|
+
describe("WebSocket TLS", () => {
|
|
172
|
+
let app;
|
|
173
|
+
let logger;
|
|
174
|
+
let wsPort;
|
|
175
|
+
let tlsDir;
|
|
176
|
+
beforeEach(async () => {
|
|
177
|
+
logger = new MemoryLogger();
|
|
178
|
+
tlsDir = mkdtempSync(join(tmpdir(), "tls-ws-"));
|
|
179
|
+
const baseCfg = defaultConfig();
|
|
180
|
+
app = createDrawioMcpApp({
|
|
181
|
+
log: logger,
|
|
182
|
+
config: { ...baseCfg, tlsEnabled: true, tlsAuto: true, tlsDir },
|
|
183
|
+
});
|
|
184
|
+
const wsServer = await app.startWebSocketServer(0);
|
|
185
|
+
wsPort = wsServer.address().port;
|
|
186
|
+
});
|
|
187
|
+
afterEach(async () => {
|
|
188
|
+
await app.close();
|
|
189
|
+
});
|
|
190
|
+
it("accepts TLS handshakes (wss)", async () => {
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
const socket = tlsConnect({ port: wsPort, host: "127.0.0.1", rejectUnauthorized: false }, () => {
|
|
193
|
+
expect(socket.authorized || !socket.authorized).toBe(true);
|
|
194
|
+
socket.end();
|
|
195
|
+
resolve();
|
|
196
|
+
});
|
|
197
|
+
socket.on("error", reject);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
it("rejects plain TCP traffic", async () => {
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
const s = new Socket();
|
|
203
|
+
s.connect(wsPort, "127.0.0.1", () => {
|
|
204
|
+
s.write("plain text\r\n");
|
|
205
|
+
});
|
|
206
|
+
// The server sends a TLS alert and destroys the socket.
|
|
207
|
+
// In Jest's ESM VM environment "close" can be slow to propagate,
|
|
208
|
+
// so we also resolve on "data" (the TLS alert bytes) and "end".
|
|
209
|
+
const done = () => {
|
|
210
|
+
s.destroy();
|
|
211
|
+
resolve();
|
|
212
|
+
};
|
|
213
|
+
s.on("error", done);
|
|
214
|
+
s.on("close", done);
|
|
215
|
+
s.on("data", done);
|
|
216
|
+
s.on("end", done);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe("HTTP transport — TLS (https)", () => {
|
|
221
|
+
let app;
|
|
222
|
+
let logger;
|
|
223
|
+
let httpServer;
|
|
224
|
+
let port;
|
|
225
|
+
let tlsDir;
|
|
226
|
+
beforeEach(async () => {
|
|
227
|
+
logger = new MemoryLogger();
|
|
228
|
+
tlsDir = mkdtempSync(join(tmpdir(), "tls-http-"));
|
|
229
|
+
const cfg = {
|
|
230
|
+
...defaultConfig(),
|
|
231
|
+
extensionPort: 0,
|
|
232
|
+
httpPort: 0,
|
|
233
|
+
transports: ["http"],
|
|
234
|
+
editorEnabled: false,
|
|
235
|
+
tlsEnabled: true,
|
|
236
|
+
tlsAuto: true,
|
|
237
|
+
tlsDir,
|
|
238
|
+
};
|
|
239
|
+
app = createDrawioMcpApp({ log: logger, config: cfg });
|
|
240
|
+
const features = {
|
|
241
|
+
enableMcp: true,
|
|
242
|
+
enableEditor: false,
|
|
243
|
+
enableHealth: true,
|
|
244
|
+
enableConfig: false,
|
|
245
|
+
};
|
|
246
|
+
const started = await app.startHttpServer(0, cfg, features);
|
|
247
|
+
httpServer = started.server;
|
|
248
|
+
port = started.port;
|
|
249
|
+
});
|
|
250
|
+
afterEach(async () => {
|
|
251
|
+
await app.close();
|
|
252
|
+
});
|
|
253
|
+
it("serves /health over HTTPS", async () => {
|
|
254
|
+
const body = await new Promise((resolve, reject) => {
|
|
255
|
+
const req = https.request({
|
|
256
|
+
hostname: "localhost",
|
|
257
|
+
port,
|
|
258
|
+
path: "/health",
|
|
259
|
+
method: "GET",
|
|
260
|
+
rejectUnauthorized: false,
|
|
261
|
+
}, (res) => {
|
|
262
|
+
expect(res.statusCode).toBe(200);
|
|
263
|
+
let data = "";
|
|
264
|
+
res.on("data", (chunk) => {
|
|
265
|
+
data += chunk;
|
|
266
|
+
});
|
|
267
|
+
res.on("end", () => resolve(data));
|
|
268
|
+
});
|
|
269
|
+
req.on("error", reject);
|
|
270
|
+
req.end();
|
|
271
|
+
});
|
|
272
|
+
expect(JSON.parse(body)).toEqual({ status: "ok" });
|
|
273
|
+
});
|
|
274
|
+
it("rejects plain HTTP", async () => {
|
|
275
|
+
await expect(fetch(`http://localhost:${port}/health`)).rejects.toThrow();
|
|
276
|
+
});
|
|
277
|
+
});
|