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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
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";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
10
|
+
import { createDrawioMcpApp } from "./index.js";
|
|
11
|
+
import { MemoryLogger } from "./real-environment/logger.js";
|
|
12
|
+
import { defaultConfig } from "./config.js";
|
|
13
|
+
describe("multi-transport support", () => {
|
|
14
|
+
let app;
|
|
15
|
+
let logger;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
logger = new MemoryLogger();
|
|
18
|
+
app = createDrawioMcpApp({ log: logger });
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await app.close();
|
|
22
|
+
});
|
|
23
|
+
it("createMcpServer returns distinct instances", () => {
|
|
24
|
+
const server1 = app.createMcpServer();
|
|
25
|
+
const server2 = app.createMcpServer();
|
|
26
|
+
expect(server1).not.toBe(server2);
|
|
27
|
+
});
|
|
28
|
+
it("each McpServer instance has tools registered", async () => {
|
|
29
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
30
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
31
|
+
const server1 = app.createMcpServer();
|
|
32
|
+
const server2 = app.createMcpServer();
|
|
33
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
34
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
35
|
+
await Promise.all([
|
|
36
|
+
server1.connect(st1),
|
|
37
|
+
client1.connect(ct1),
|
|
38
|
+
server2.connect(st2),
|
|
39
|
+
client2.connect(ct2),
|
|
40
|
+
]);
|
|
41
|
+
const tools1 = await client1.listTools();
|
|
42
|
+
const tools2 = await client2.listTools();
|
|
43
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
44
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
45
|
+
const names1 = tools1.tools.map((t) => t.name).sort();
|
|
46
|
+
const names2 = tools2.tools.map((t) => t.name).sort();
|
|
47
|
+
expect(names1).toEqual(names2);
|
|
48
|
+
await client1.close();
|
|
49
|
+
await client2.close();
|
|
50
|
+
});
|
|
51
|
+
it("two InMemoryTransport connections work simultaneously without 'Already connected' error", async () => {
|
|
52
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
53
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
54
|
+
const server1 = app.createMcpServer();
|
|
55
|
+
const server2 = app.createMcpServer();
|
|
56
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
57
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
58
|
+
// This is the exact scenario that was failing before the fix:
|
|
59
|
+
// connecting two transports should not throw.
|
|
60
|
+
await expect(Promise.all([
|
|
61
|
+
server1.connect(st1),
|
|
62
|
+
client1.connect(ct1),
|
|
63
|
+
server2.connect(st2),
|
|
64
|
+
client2.connect(ct2),
|
|
65
|
+
])).resolves.not.toThrow();
|
|
66
|
+
// Both clients can independently list tools
|
|
67
|
+
const [tools1, tools2] = await Promise.all([
|
|
68
|
+
client1.listTools(),
|
|
69
|
+
client2.listTools(),
|
|
70
|
+
]);
|
|
71
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
72
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
73
|
+
await client1.close();
|
|
74
|
+
await client2.close();
|
|
75
|
+
});
|
|
76
|
+
it("close() shuts down all created McpServer instances", async () => {
|
|
77
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
78
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
79
|
+
const server1 = app.createMcpServer();
|
|
80
|
+
const server2 = app.createMcpServer();
|
|
81
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
82
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
83
|
+
await Promise.all([
|
|
84
|
+
server1.connect(st1),
|
|
85
|
+
client1.connect(ct1),
|
|
86
|
+
server2.connect(st2),
|
|
87
|
+
client2.connect(ct2),
|
|
88
|
+
]);
|
|
89
|
+
// close() should succeed without errors even with multiple servers
|
|
90
|
+
await expect(app.close()).resolves.not.toThrow();
|
|
91
|
+
// After close, listing tools should fail because the servers are shut down
|
|
92
|
+
await expect(client1.listTools()).rejects.toThrow();
|
|
93
|
+
await expect(client2.listTools()).rejects.toThrow();
|
|
94
|
+
});
|
|
95
|
+
it("connecting the same McpServer instance to two transports still throws", async () => {
|
|
96
|
+
const [_ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
97
|
+
const [_ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
98
|
+
const server = app.createMcpServer();
|
|
99
|
+
await server.connect(st1);
|
|
100
|
+
// The SDK constraint hasn't changed — a single Protocol instance
|
|
101
|
+
// still rejects a second connect().
|
|
102
|
+
await expect(server.connect(st2)).rejects.toThrow(/already connected/i);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("HTTP transport (stateless per-request)", () => {
|
|
106
|
+
let app;
|
|
107
|
+
let logger;
|
|
108
|
+
let httpServer;
|
|
109
|
+
let port;
|
|
110
|
+
const config = {
|
|
111
|
+
extensionPort: 0,
|
|
112
|
+
httpPort: 0,
|
|
113
|
+
transports: ["http"],
|
|
114
|
+
editorEnabled: false,
|
|
115
|
+
logger: "console",
|
|
116
|
+
tlsEnabled: false,
|
|
117
|
+
tlsAuto: false,
|
|
118
|
+
};
|
|
119
|
+
const features = {
|
|
120
|
+
enableMcp: true,
|
|
121
|
+
enableEditor: false,
|
|
122
|
+
enableHealth: false,
|
|
123
|
+
enableConfig: false,
|
|
124
|
+
};
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
logger = new MemoryLogger();
|
|
127
|
+
app = createDrawioMcpApp({ log: logger });
|
|
128
|
+
const started = await app.startHttpServer(0, config, features);
|
|
129
|
+
httpServer = started.server;
|
|
130
|
+
port = started.port;
|
|
131
|
+
});
|
|
132
|
+
afterEach(async () => {
|
|
133
|
+
await app.close();
|
|
134
|
+
});
|
|
135
|
+
it("handles a single HTTP client request", async () => {
|
|
136
|
+
const client = new Client({ name: "http-test-1", version: "1.0.0" });
|
|
137
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
138
|
+
await client.connect(transport);
|
|
139
|
+
const tools = await client.listTools();
|
|
140
|
+
expect(tools.tools.length).toBeGreaterThan(0);
|
|
141
|
+
await client.close();
|
|
142
|
+
});
|
|
143
|
+
it("handles multiple sequential HTTP client requests without reuse error", async () => {
|
|
144
|
+
// First request
|
|
145
|
+
const client1 = new Client({ name: "http-test-seq-1", version: "1.0.0" });
|
|
146
|
+
const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
147
|
+
await client1.connect(transport1);
|
|
148
|
+
const tools1 = await client1.listTools();
|
|
149
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
150
|
+
await client1.close();
|
|
151
|
+
// Second request — this was the exact scenario that triggered the
|
|
152
|
+
// "Stateless transport cannot be reused across requests" error.
|
|
153
|
+
const client2 = new Client({ name: "http-test-seq-2", version: "1.0.0" });
|
|
154
|
+
const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
155
|
+
await client2.connect(transport2);
|
|
156
|
+
const tools2 = await client2.listTools();
|
|
157
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
158
|
+
await client2.close();
|
|
159
|
+
});
|
|
160
|
+
it("close() succeeds after HTTP requests (per-request servers are cleaned up)", async () => {
|
|
161
|
+
const client = new Client({ name: "http-cleanup", version: "1.0.0" });
|
|
162
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
163
|
+
await client.connect(transport);
|
|
164
|
+
await client.listTools();
|
|
165
|
+
await client.close();
|
|
166
|
+
// close() should not hang or throw — the per-request McpServer
|
|
167
|
+
// was already disposed and removed from the tracking set.
|
|
168
|
+
await expect(app.close()).resolves.not.toThrow();
|
|
169
|
+
});
|
|
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
|
+
});
|