drawio-mcp-server 1.3.0 → 1.5.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Let's do some Vibe Diagramming with the most wide-spread diagramming tool called Draw.io (Diagrams.net).
4
4
 
5
- [![Build project](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml)
5
+ [![Discord channel](https://shields.io/static/v1?logo=discord&message=draw.io%20mcp&label=chat&color=5865F2&logoColor=white)](https://discord.gg/dM4PWdf42q) [![Build project](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml)
6
6
 
7
7
  ## Introduction
8
8
 
@@ -39,6 +39,81 @@ To use the Draw.io MCP server, you'll need:
39
39
 
40
40
  Note: The Draw.io desktop app or web version must be accessible to the system where the MCP server runs.
41
41
 
42
+ ## Configuration
43
+
44
+ ### WebSocket Port
45
+
46
+ The server listens on port 3333 by default for WebSocket connections from the browser extension. You can customize this port using the `--extension-port` or `-p` flag.
47
+
48
+ **Default behavior** (port 3333):
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "drawio": {
53
+ "command": "npx",
54
+ "args": ["-y", "drawio-mcp-server"]
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ **Custom port** (e.g., port 8080):
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "drawio": {
65
+ "command": "npx",
66
+ "args": ["-y", "drawio-mcp-server", "--extension-port", "8080"]
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Note**: When using a custom port, ensure the browser extension is configured to connect to the same port.
73
+
74
+ ### HTTP Transport Port
75
+
76
+ The server can expose a streamable HTTP MCP transport on port 3000. Change this using the `--http-port` flag:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "drawio": {
82
+ "command": "npx",
83
+ "args": ["-y", "drawio-mcp-server", "--transport", "http", "--http-port", "4000"]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Transport Selection
90
+
91
+ By default only the stdio transport starts. Limit or combine transports with the `--transport` flag:
92
+
93
+ - `--transport stdio` – start only stdio (CLI-friendly)
94
+ - `--transport http` – start only the HTTP transport (for remote clients)
95
+ - `--transport stdio,http` – start both transports
96
+
97
+ ### Running the streamable HTTP transport
98
+
99
+ Use the streamable HTTP transport when you need to reach the MCP server over the network (for example from a remote agent runtime). The Draw.io browser extension is still required, and you must opt in to the HTTP transport.
100
+
101
+ 1. Start the server with HTTP enabled (optionally alongside stdio):
102
+
103
+ ```sh
104
+ npx -y drawio-mcp-server --transport http --http-port 3000
105
+ # or both: npx -y drawio-mcp-server --transport stdio,http --http-port 4000
106
+ ```
107
+
108
+ 2. Verify the health endpoint:
109
+
110
+ ```sh
111
+ curl http://localhost:3000/health
112
+ # { "status": "ok" }
113
+ ```
114
+
115
+ 3. Point your MCP client to the `/mcp` endpoint (`http://localhost:3000/mcp` by default). CORS is enabled for all origins so you can call it from a browser-based client as well.
116
+
42
117
  ## Installation
43
118
 
44
119
  ### Connecting with Claude Desktop
@@ -86,6 +161,19 @@ Note: The Draw.io desktop app or web version must be accessible to the system wh
86
161
  ```
87
162
  </details>
88
163
 
164
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "drawio": {
170
+ "command": "npx",
171
+ "args": ["-y", "drawio-mcp-server", "--extension-port", "8080"]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
89
177
  4. Restart Claude Desktop
90
178
 
91
179
  ### Connecting with oterm
@@ -130,6 +218,19 @@ The configuration is usually in: ~/.local/share/oterm/config.json
130
218
  ```
131
219
  </details>
132
220
 
221
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
222
+
223
+ ```json
224
+ {
225
+ "mcpServers": {
226
+ "drawio": {
227
+ "command": "npx",
228
+ "args": ["-y", "drawio-mcp-server", "--extension-port", "8080"]
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
133
234
  ### Connect with Zed
134
235
 
135
236
  1. Open the Zed Preview application.
@@ -178,6 +279,79 @@ The configuration is usually in: ~/.local/share/oterm/config.json
178
279
  ```
179
280
  </details>
180
281
 
282
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
283
+
284
+ ```json
285
+ {
286
+ /// The name of your MCP server
287
+ "drawio": {
288
+ "command": {
289
+ /// The path to the executable
290
+ "path": "npx",
291
+ /// The arguments to pass to the executable
292
+ "args": ["-y","drawio-mcp-server","--extension-port","8080"],
293
+ /// The environment variables to set for the executable
294
+ "env": {}
295
+ }
296
+ }
297
+ }
298
+ ```
299
+
300
+ ### Connecting with Codex
301
+
302
+ Edit the configuration usually located in: ~/.codex/config.toml
303
+
304
+ <details>
305
+ <summary>Using <code>npm</code></summary>
306
+
307
+ ```toml
308
+ [mcp_servers.drawio]
309
+ command = "npx"
310
+ args = [
311
+ "-y",
312
+ "drawio-mcp-server"
313
+ ]
314
+ ```
315
+ </details>
316
+
317
+ <details>
318
+ <summary>Using <code>pnpm</code></summary>
319
+
320
+ ```toml
321
+ [mcp_servers.drawio]
322
+ command = "pnpm"
323
+ args = [
324
+ "dlx",
325
+ "drawio-mcp-server"
326
+ ]
327
+ ```
328
+ </details>
329
+
330
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
331
+
332
+ <details>
333
+ <summary>Using <code>npm</code></summary>
334
+
335
+ ```toml
336
+ [mcp_servers.drawio]
337
+ command = "npx"
338
+ args = [
339
+ "-y",
340
+ "drawio-mcp-server",
341
+ "--extension-port",
342
+ "8080"
343
+ ]
344
+ ```
345
+ </details>
346
+
347
+ To connect to a locally running MCP with Streamable HTTP transport:
348
+
349
+ ```toml
350
+ [mcp_servers.drawio]
351
+ url = "http://localhost:3000/mcp"
352
+ ```
353
+
354
+
181
355
  ### Browser Extension Setup
182
356
 
183
357
  In order to control the Draw.io diagram, you need to install dedicated Browser Extension.
@@ -198,6 +372,8 @@ In order to control the Draw.io diagram, you need to install dedicated Browser E
198
372
  </p>
199
373
  3. Ensure it is connected, the Extension icon should indicate green signal overlay <img alt="Extension connected" src="https://raw.githubusercontent.com/lgazo/drawio-mcp-extension/refs/heads/main/public/icon/logo_connected_32.png" />
200
374
 
375
+ **Important**: If you configured the MCP server to use a custom port (not 3333), you must configure the browser extension to use the same port. See the extension documentation for port configuration instructions.
376
+
201
377
 
202
378
  ## Sponsoring
203
379
 
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Default configuration values
3
+ */
4
+ const DEFAULT_CONFIG = {
5
+ extensionPort: 3333,
6
+ httpPort: 3000,
7
+ transports: ["stdio"],
8
+ };
9
+ /**
10
+ * Valid port range
11
+ */
12
+ const PORT_RANGE = {
13
+ min: 1,
14
+ max: 65535,
15
+ };
16
+ /**
17
+ * Parse extension port value from string - pure function
18
+ */
19
+ export const parseExtensionPortValue = (value) => {
20
+ if (!value) {
21
+ return new Error("--extension-port flag requires a port number");
22
+ }
23
+ const port = parseInt(value, 10);
24
+ if (isNaN(port)) {
25
+ return new Error(`Invalid port number "${value}". Port must be a number`);
26
+ }
27
+ if (port < PORT_RANGE.min || port > PORT_RANGE.max) {
28
+ return new Error(`Invalid port number "${value}". Port must be between ${PORT_RANGE.min} and ${PORT_RANGE.max}`);
29
+ }
30
+ return port;
31
+ };
32
+ /**
33
+ * Parse http port value from string - pure function
34
+ */
35
+ export const parseHttpPortValue = (value) => {
36
+ if (!value) {
37
+ return new Error("--http-port flag requires a port number");
38
+ }
39
+ const port = parseInt(value, 10);
40
+ if (isNaN(port)) {
41
+ return new Error(`Invalid port number "${value}". Port must be a number`);
42
+ }
43
+ if (port < PORT_RANGE.min || port > PORT_RANGE.max) {
44
+ return new Error(`Invalid port number "${value}". Port must be between ${PORT_RANGE.min} and ${PORT_RANGE.max}`);
45
+ }
46
+ return port;
47
+ };
48
+ export const parseTransports = (values) => {
49
+ if (!values || values.length === 0) {
50
+ return DEFAULT_CONFIG.transports;
51
+ }
52
+ const normalized = values
53
+ .flatMap((value) => value.split(","))
54
+ .map((value) => value.trim().toLowerCase())
55
+ .filter((value) => value.length > 0);
56
+ if (normalized.length === 0) {
57
+ return new Error("At least one transport must be specified");
58
+ }
59
+ const validTransports = [];
60
+ for (const value of normalized) {
61
+ if (value === "stdio" || value === "http") {
62
+ validTransports.push(value);
63
+ }
64
+ else {
65
+ return new Error(`Invalid transport "${value}". Supported transports: stdio, http`);
66
+ }
67
+ }
68
+ // Remove duplicates while preserving order
69
+ return Array.from(new Set(validTransports));
70
+ };
71
+ /**
72
+ * Find argument value by flag name - pure function
73
+ */
74
+ export const findArgValue = (args, ...flags) => {
75
+ const index = args.findIndex((arg) => flags.includes(arg));
76
+ return index !== -1 ? args[index + 1] : undefined;
77
+ };
78
+ /**
79
+ * Check if any flag exists in arguments - pure function
80
+ */
81
+ export const hasFlag = (args, ...flags) => {
82
+ return args.some((arg) => flags.includes(arg));
83
+ };
84
+ /**
85
+ * Check if help was requested - pure function
86
+ */
87
+ export const shouldShowHelp = (args) => {
88
+ return hasFlag(args, "--help", "-h");
89
+ };
90
+ /**
91
+ * Parse command line arguments into configuration object
92
+ * Pure function - no side effects, deterministic output
93
+ */
94
+ export const parseConfig = (args) => {
95
+ // Walk arguments so repeated flags allow "last wins" semantics
96
+ let portValue;
97
+ let httpPortValue;
98
+ let parsedHttpPort;
99
+ let transportValues;
100
+ for (let i = 0; i < args.length; i += 1) {
101
+ const arg = args[i];
102
+ if (arg === "--extension-port" || arg === "-p") {
103
+ const nextValue = args[i + 1];
104
+ if (nextValue === undefined) {
105
+ return new Error("--extension-port flag requires a port number");
106
+ }
107
+ portValue = nextValue;
108
+ i += 1; // Skip the value we just consumed
109
+ }
110
+ else if (arg === "--http-port") {
111
+ const nextValue = args[i + 1];
112
+ if (nextValue === undefined) {
113
+ return new Error("--http-port flag requires a port number");
114
+ }
115
+ httpPortValue = nextValue;
116
+ i += 1;
117
+ }
118
+ else if (arg === "--transport") {
119
+ const nextValue = args[i + 1];
120
+ if (nextValue === undefined) {
121
+ return new Error("--transport flag requires a transport name");
122
+ }
123
+ transportValues = [nextValue];
124
+ i += 1;
125
+ }
126
+ }
127
+ if (httpPortValue !== undefined) {
128
+ const httpPort = parseHttpPortValue(httpPortValue);
129
+ if (httpPort instanceof Error) {
130
+ return httpPort;
131
+ }
132
+ parsedHttpPort = httpPort;
133
+ }
134
+ if (portValue !== undefined) {
135
+ const extensionPort = parseExtensionPortValue(portValue);
136
+ if (extensionPort instanceof Error) {
137
+ return extensionPort;
138
+ }
139
+ const transports = parseTransports(transportValues);
140
+ if (transports instanceof Error) {
141
+ return transports;
142
+ }
143
+ return {
144
+ ...DEFAULT_CONFIG,
145
+ extensionPort,
146
+ httpPort: parsedHttpPort !== undefined ? parsedHttpPort : DEFAULT_CONFIG.httpPort,
147
+ transports,
148
+ };
149
+ }
150
+ if (httpPortValue !== undefined) {
151
+ const transports = parseTransports(transportValues);
152
+ if (transports instanceof Error) {
153
+ return transports;
154
+ }
155
+ return {
156
+ ...DEFAULT_CONFIG,
157
+ httpPort: parsedHttpPort,
158
+ transports,
159
+ };
160
+ }
161
+ const transports = parseTransports(transportValues);
162
+ if (transports instanceof Error) {
163
+ return transports;
164
+ }
165
+ // Return default configuration
166
+ return {
167
+ ...DEFAULT_CONFIG,
168
+ transports,
169
+ };
170
+ };
171
+ /**
172
+ * Build configuration from process.argv
173
+ * This is the main entry point for configuration
174
+ * Returns Error for invalid config, or ServerConfig
175
+ */
176
+ export const buildConfig = () => {
177
+ const args = process.argv.slice(2);
178
+ return parseConfig(args);
179
+ };
@@ -0,0 +1,251 @@
1
+ import { parseExtensionPortValue, parseHttpPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, parseTransports, } from "./config.js";
2
+ describe("parseExtensionPortValue", () => {
3
+ test("valid port returns number", () => {
4
+ expect(parseExtensionPortValue("8080")).toBe(8080);
5
+ });
6
+ test("undefined input returns Error", () => {
7
+ expect(parseExtensionPortValue(undefined)).toBeInstanceOf(Error);
8
+ });
9
+ test("non-numeric string returns Error", () => {
10
+ expect(parseExtensionPortValue("abc")).toBeInstanceOf(Error);
11
+ });
12
+ test("out of range (too low) returns Error", () => {
13
+ expect(parseExtensionPortValue("0")).toBeInstanceOf(Error);
14
+ });
15
+ test("out of range (too high) returns Error", () => {
16
+ expect(parseExtensionPortValue("70000")).toBeInstanceOf(Error);
17
+ });
18
+ test("port 1 is valid", () => {
19
+ expect(parseExtensionPortValue("1")).toBe(1);
20
+ });
21
+ test("port 65535 is valid", () => {
22
+ expect(parseExtensionPortValue("65535")).toBe(65535);
23
+ });
24
+ });
25
+ describe("parseHttpPortValue", () => {
26
+ test("valid port returns number", () => {
27
+ expect(parseHttpPortValue("3000")).toBe(3000);
28
+ });
29
+ test("undefined input returns Error", () => {
30
+ expect(parseHttpPortValue(undefined)).toBeInstanceOf(Error);
31
+ });
32
+ test("non-numeric string returns Error", () => {
33
+ expect(parseHttpPortValue("abc")).toBeInstanceOf(Error);
34
+ });
35
+ test("out of range returns Error", () => {
36
+ expect(parseHttpPortValue("70000")).toBeInstanceOf(Error);
37
+ });
38
+ });
39
+ describe("parseTransports", () => {
40
+ test("returns default when undefined", () => {
41
+ expect(parseTransports(undefined)).toEqual(["stdio"]);
42
+ });
43
+ test("parses single transport", () => {
44
+ expect(parseTransports(["stdio"])).toEqual(["stdio"]);
45
+ });
46
+ test("parses comma separated list", () => {
47
+ expect(parseTransports(["stdio,http"])).toEqual(["stdio", "http"]);
48
+ });
49
+ test("deduplicates transports", () => {
50
+ expect(parseTransports(["stdio", "stdio"])).toEqual(["stdio"]);
51
+ });
52
+ test("rejects empty string", () => {
53
+ const result = parseTransports([""]);
54
+ expect(result).toBeInstanceOf(Error);
55
+ });
56
+ test("rejects unknown transport", () => {
57
+ const result = parseTransports(["foo"]);
58
+ expect(result).toBeInstanceOf(Error);
59
+ });
60
+ });
61
+ describe("findArgValue", () => {
62
+ test("finds value after flag", () => {
63
+ expect(findArgValue(["--port", "8080", "--help"], "--port")).toBe("8080");
64
+ });
65
+ test("returns undefined when flag not found", () => {
66
+ expect(findArgValue(["--help"], "--port")).toBeUndefined();
67
+ });
68
+ test("returns undefined when flag is last argument", () => {
69
+ expect(findArgValue(["--port"], "--port")).toBeUndefined();
70
+ });
71
+ test("finds value with short flag", () => {
72
+ expect(findArgValue(["-p", "8080"], "-p")).toBe("8080");
73
+ });
74
+ test("works with readonly array", () => {
75
+ const args = ["--port", "8080"];
76
+ expect(findArgValue(args, "--port")).toBe("8080");
77
+ });
78
+ });
79
+ describe("hasFlag", () => {
80
+ test("returns true when flag exists", () => {
81
+ expect(hasFlag(["--help", "--port", "8080"], "--help")).toBe(true);
82
+ });
83
+ test("returns false when flag does not exist", () => {
84
+ expect(hasFlag(["--port", "8080"], "--help")).toBe(false);
85
+ });
86
+ test("returns true with short flag", () => {
87
+ expect(hasFlag(["-h"], "-h")).toBe(true);
88
+ });
89
+ test("works with multiple flags", () => {
90
+ expect(hasFlag(["-h"], "-h", "--help")).toBe(true);
91
+ });
92
+ test("works with readonly array", () => {
93
+ const args = ["--help"];
94
+ expect(hasFlag(args, "--help")).toBe(true);
95
+ });
96
+ });
97
+ describe("shouldShowHelp", () => {
98
+ test("returns true for --help", () => {
99
+ expect(shouldShowHelp(["--help"])).toBe(true);
100
+ });
101
+ test("returns true for -h", () => {
102
+ expect(shouldShowHelp(["-h"])).toBe(true);
103
+ });
104
+ test("returns false for no help flag", () => {
105
+ expect(shouldShowHelp(["--extension-port", "8080"])).toBe(false);
106
+ });
107
+ test("returns false for empty args", () => {
108
+ expect(shouldShowHelp([])).toBe(false);
109
+ });
110
+ });
111
+ describe("parseConfig", () => {
112
+ test("no args returns default config", () => {
113
+ expect(parseConfig([])).toEqual({
114
+ extensionPort: 3333,
115
+ httpPort: 3000,
116
+ transports: ["stdio"],
117
+ });
118
+ });
119
+ test("--extension-port flag sets custom port", () => {
120
+ expect(parseConfig(["--extension-port", "8080"])).toEqual({
121
+ extensionPort: 8080,
122
+ httpPort: 3000,
123
+ transports: ["stdio"],
124
+ });
125
+ });
126
+ test("-p flag sets custom port", () => {
127
+ expect(parseConfig(["-p", "8080"])).toEqual({
128
+ extensionPort: 8080,
129
+ httpPort: 3000,
130
+ transports: ["stdio"],
131
+ });
132
+ });
133
+ test("--http-port flag sets custom port", () => {
134
+ expect(parseConfig(["--http-port", "4242"])).toEqual({
135
+ extensionPort: 3333,
136
+ httpPort: 4242,
137
+ transports: ["stdio"],
138
+ });
139
+ });
140
+ test("both ports can be configured", () => {
141
+ expect(parseConfig(["--extension-port", "8080", "--http-port", "4242"])).toEqual({
142
+ extensionPort: 8080,
143
+ httpPort: 4242,
144
+ transports: ["stdio"],
145
+ });
146
+ });
147
+ test("help flag is ignored in config parsing", () => {
148
+ expect(parseConfig(["--help"])).toEqual({
149
+ extensionPort: 3333,
150
+ httpPort: 3000,
151
+ transports: ["stdio"],
152
+ });
153
+ });
154
+ test("invalid port returns Error", () => {
155
+ const result = parseConfig(["--extension-port", "abc"]);
156
+ expect(result).toBeInstanceOf(Error);
157
+ expect(result.message).toContain("Invalid port number");
158
+ });
159
+ test("missing port value returns Error", () => {
160
+ const result = parseConfig(["--extension-port"]);
161
+ expect(result).toBeInstanceOf(Error);
162
+ expect(result.message).toContain("--extension-port flag requires a port number");
163
+ });
164
+ test("missing http port value returns Error", () => {
165
+ const result = parseConfig(["--http-port"]);
166
+ expect(result).toBeInstanceOf(Error);
167
+ expect(result.message).toContain("--http-port flag requires a port number");
168
+ });
169
+ test("out of range port returns Error", () => {
170
+ const result = parseConfig(["--extension-port", "70000"]);
171
+ expect(result).toBeInstanceOf(Error);
172
+ expect(result.message).toContain("Invalid port number");
173
+ });
174
+ test("multiple --extension-port flags uses last one", () => {
175
+ expect(parseConfig(["--extension-port", "8080", "--extension-port", "9090"])).toEqual({
176
+ extensionPort: 9090,
177
+ httpPort: 3000,
178
+ transports: ["stdio"],
179
+ });
180
+ });
181
+ test("short and long form both work, last wins", () => {
182
+ expect(parseConfig(["--extension-port", "8080", "-p", "9090"])).toEqual({
183
+ extensionPort: 9090,
184
+ httpPort: 3000,
185
+ transports: ["stdio"],
186
+ });
187
+ });
188
+ test("last http-port flag wins", () => {
189
+ expect(parseConfig(["--http-port", "4000", "--http-port", "5000"])).toEqual({
190
+ extensionPort: 3333,
191
+ httpPort: 5000,
192
+ transports: ["stdio"],
193
+ });
194
+ });
195
+ test("sets single transport", () => {
196
+ expect(parseConfig(["--transport", "stdio"])).toEqual({
197
+ extensionPort: 3333,
198
+ httpPort: 3000,
199
+ transports: ["stdio"],
200
+ });
201
+ });
202
+ test("sets multiple transports", () => {
203
+ expect(parseConfig(["--transport", "stdio,http"])).toEqual({
204
+ extensionPort: 3333,
205
+ httpPort: 3000,
206
+ transports: ["stdio", "http"],
207
+ });
208
+ });
209
+ test("rejects unknown transport", () => {
210
+ const result = parseConfig(["--transport", "foo"]);
211
+ expect(result).toBeInstanceOf(Error);
212
+ });
213
+ });
214
+ describe("buildConfig", () => {
215
+ const originalArgv = process.argv;
216
+ afterEach(() => {
217
+ process.argv = originalArgv;
218
+ });
219
+ test("uses default config with empty args", () => {
220
+ process.argv = ["node", "script.js"];
221
+ const result = buildConfig();
222
+ expect(result).toEqual({
223
+ extensionPort: 3333,
224
+ httpPort: 3000,
225
+ transports: ["stdio"],
226
+ });
227
+ });
228
+ test("parses custom port from argv", () => {
229
+ process.argv = ["node", "script.js", "--extension-port", "8080"];
230
+ const result = buildConfig();
231
+ expect(result).toEqual({
232
+ extensionPort: 8080,
233
+ httpPort: 3000,
234
+ transports: ["stdio"],
235
+ });
236
+ });
237
+ test("parses custom http port from argv", () => {
238
+ process.argv = ["node", "script.js", "--http-port", "4242"];
239
+ const result = buildConfig();
240
+ expect(result).toEqual({
241
+ extensionPort: 3333,
242
+ httpPort: 4242,
243
+ transports: ["stdio"],
244
+ });
245
+ });
246
+ test("returns Error for invalid config", () => {
247
+ process.argv = ["node", "script.js", "--extension-port", "abc"];
248
+ const result = buildConfig();
249
+ expect(result).toBeInstanceOf(Error);
250
+ });
251
+ });
package/build/index.js CHANGED
@@ -1,17 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
5
+ import { serve } from "@hono/node-server";
4
6
  import { z } from "zod";
7
+ import { Hono } from "hono";
8
+ import { cors } from "hono/cors";
5
9
  import EventEmitter from "node:events";
6
10
  import { createServer } from "node:net";
7
- import uWS from "uWebSockets.js";
11
+ import { WebSocket, WebSocketServer } from "ws";
12
+ import { buildConfig, shouldShowHelp } from "./config.js";
8
13
  import { bus_reply_stream, bus_request_stream, } from "./types.js";
9
14
  import { create_bus } from "./emitter_bus.js";
10
15
  import { default_tool } from "./tool.js";
11
16
  import { nanoid_id_generator } from "./nanoid_id_generator.js";
12
17
  import { create_logger as create_console_logger } from "./mcp_console_logger.js";
13
18
  import { create_logger as create_server_logger, validLogLevels, } from "./mcp_server_logger.js";
14
- const PORT = 3333;
19
+ /**
20
+ * Display help message and exit
21
+ */
22
+ function showHelp() {
23
+ console.log(`
24
+ Draw.io MCP Server
25
+
26
+ Usage: drawio-mcp-server [options]
27
+
28
+ Options:
29
+ --extension-port, -p <number> WebSocket server port for browser extension (default: 3333)
30
+ --help, -h Show this help message
31
+
32
+ Examples:
33
+ drawio-mcp-server # Use default extension port 3333
34
+ drawio-mcp-server --extension-port 8080 # Use custom extension port 8080
35
+ drawio-mcp-server -p 8080 # Short form
36
+ `);
37
+ process.exit(0);
38
+ }
39
+ // No PORT constant needed - using dynamic config
15
40
  async function checkPortAvailable(port) {
16
41
  return new Promise((resolve) => {
17
42
  const server = createServer();
@@ -22,58 +47,63 @@ async function checkPortAvailable(port) {
22
47
  });
23
48
  }
24
49
  const emitter = new EventEmitter();
25
- const conns = [];
50
+ const conns = new Set();
26
51
  const bus_to_ws_forwarder_listener = (event) => {
27
- log.debug(`[bridge] received; forwarding message to #${conns.length} clients`, event);
28
- for (let i = 0; i < conns.length; i++) {
52
+ log.debug(`[bridge] received; forwarding message to #${conns.size} clients`, event);
53
+ for (const ws of [...conns]) {
54
+ if (ws.readyState !== WebSocket.OPEN) {
55
+ conns.delete(ws);
56
+ continue;
57
+ }
29
58
  try {
30
- conns[i].send(JSON.stringify(event));
59
+ ws.send(JSON.stringify(event));
31
60
  }
32
61
  catch (e) {
33
- log.debug(`[bridge] error forwarding request at conn = ${i}`);
62
+ log.debug("[bridge] error forwarding request", e);
63
+ conns.delete(ws);
34
64
  }
35
65
  }
36
66
  };
37
67
  emitter.on(bus_request_stream, bus_to_ws_forwarder_listener);
38
- const ws_handler = {
39
- maxPayloadLength: 128 * 1024,
40
- open: (ws) => {
41
- log.debug(`[ws_handler] A WebSocket client #${conns.length} connected, presumably MCP Extension!`);
42
- conns.push(ws);
43
- },
44
- message: (ws, message, isBinary) => {
45
- // ws.send(message, isBinary);
46
- const decoder = new TextDecoder();
47
- const str = decoder.decode(message);
48
- const json = JSON.parse(str);
49
- log.debug(`[ws] received from Extension`, json);
50
- // const event_name = message.__event;
51
- emitter.emit(bus_reply_stream, json);
52
- },
53
- close: (ws, code, message) => {
54
- log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
55
- //todo remove conn
56
- },
57
- };
58
- async function start_websocket_server() {
59
- const isPortAvailable = await checkPortAvailable(PORT);
68
+ async function start_websocket_server(extensionPort) {
69
+ log.debug(`Draw.io MCP Server starting (WebSocket extension port: ${extensionPort})`);
70
+ const isPortAvailable = await checkPortAvailable(extensionPort);
60
71
  if (!isPortAvailable) {
61
- console.error(`[start_websocket_server] Error: Port ${PORT} is already in use. Please stop the process using this port and try again.`);
72
+ console.error(`[start_websocket_server] Error: Port ${extensionPort} is already in use. Please stop the process using this port and try again.`);
62
73
  process.exit(1);
63
74
  }
64
- const app = uWS
65
- .App()
66
- .ws("/*", ws_handler)
67
- .listen(PORT, (token) => {
68
- if (token) {
69
- log.debug(`[start_websocket_server] Listening to port ${PORT}`);
70
- }
71
- else {
72
- console.error(`[start_websocket_server] Error: Failed to listen on port ${PORT}`);
73
- process.exit(1);
74
- }
75
+ const server = new WebSocketServer({ port: extensionPort });
76
+ server.on("connection", (ws) => {
77
+ log.debug(`[ws_handler] A WebSocket client #${conns.size} connected, presumably MCP Extension!`);
78
+ conns.add(ws);
79
+ ws.on("message", (data) => {
80
+ const str = typeof data === "string" ? data : data.toString();
81
+ try {
82
+ const json = JSON.parse(str);
83
+ log.debug(`[ws] received from Extension`, json);
84
+ emitter.emit(bus_reply_stream, json);
85
+ }
86
+ catch (error) {
87
+ log.debug(`[ws] failed to parse message`, error);
88
+ }
89
+ });
90
+ ws.on("close", (code) => {
91
+ conns.delete(ws);
92
+ log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
93
+ });
94
+ ws.on("error", (error) => {
95
+ log.debug(`[ws_handler] WebSocket client error`, error);
96
+ conns.delete(ws);
97
+ });
98
+ });
99
+ server.on("listening", () => {
100
+ log.debug(`[start_websocket_server] Listening to port ${extensionPort}`);
101
+ });
102
+ server.on("error", (error) => {
103
+ console.error(`[start_websocket_server] Error: Failed to listen on port ${extensionPort}`, error);
104
+ process.exit(1);
75
105
  });
76
- return app;
106
+ return server;
77
107
  }
78
108
  const logger_type = process.env.LOGGER_TYPE;
79
109
  let capabilities = {
@@ -92,7 +122,7 @@ if (logger_type === "mcp_server") {
92
122
  // Create server instance
93
123
  const server = new McpServer({
94
124
  name: "drawio-mcp-server",
95
- version: "1.2.1",
125
+ version: "1.4.0",
96
126
  }, {
97
127
  capabilities,
98
128
  });
@@ -282,7 +312,8 @@ const Attributes = z.lazy(() => z
282
312
  ]))
283
313
  .refine((arr) => arr.length === 0 || typeof arr[0] === "string", {
284
314
  message: "If not empty, the first element must be a string operator",
285
- }));
315
+ })
316
+ .default([]));
286
317
  const TOOL_list_paged_model = "list-paged-model";
287
318
  server.tool(TOOL_list_paged_model, "Retrieves a paginated view of all cells (vertices and edges) in the current Draw.io diagram. This tool provides access to the complete model data with essential fields only, sanitized to remove circular dependencies and excessive data. It allows to filter based on multiple criteria and attribute boolean logic. Useful for programmatic inspection of diagram structure without overwhelming response sizes.", {
288
319
  page: z
@@ -301,21 +332,67 @@ server.tool(TOOL_list_paged_model, "Retrieves a paginated view of all cells (ver
301
332
  .enum(["edge", "vertex", "object", "layer", "group"])
302
333
  .optional()
303
334
  .describe("Filter by cell type: 'edge' for connection lines, 'vertex' for vertices/shapes, 'object' for any cell type, 'layer' for layer cells, 'group' for grouped cells"),
304
- attributes: Attributes.optional()
305
- .describe('Boolean logic array expressions for filtering cell attributes. Format: ["and" | "or", ...expressions] or ["equal", key, value]. Matches against cell attributes and parsed style properties.')
306
- .default([]),
335
+ attributes: Attributes.optional().describe('Boolean logic array expressions for filtering cell attributes. Format: ["and" | "or", ...expressions] or ["equal", key, value]. Matches against cell attributes and parsed style properties.'),
307
336
  })
308
337
  .optional()
309
338
  .describe("Optional filter criteria to apply to cells before pagination")
310
339
  .default({}),
311
340
  }, default_tool(TOOL_list_paged_model, context));
312
- async function main() {
313
- log.debug("Draw.io MCP Server starting");
314
- await start_websocket_server();
315
- log.debug("Draw.io MCP Server WebSocket started");
341
+ async function start_stdio_transport() {
316
342
  const transport = new StdioServerTransport();
317
343
  await server.connect(transport);
318
- log.debug("Draw.io MCP Server running on stdio");
344
+ log.debug(`Draw.io MCP Server STDIO transport active`);
345
+ }
346
+ async function start_streamable_http_transport(http_port) {
347
+ // Create a stateless transport (no options = no session management)
348
+ const transport = new WebStandardStreamableHTTPServerTransport();
349
+ // Create the Hono app
350
+ const app = new Hono();
351
+ // Enable CORS for all origins
352
+ app.use("*", cors({
353
+ origin: "*",
354
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
355
+ allowHeaders: [
356
+ "Content-Type",
357
+ "mcp-session-id",
358
+ "Last-Event-ID",
359
+ "mcp-protocol-version",
360
+ ],
361
+ exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
362
+ }));
363
+ app.get("/health", (c) => c.json({ status: server.isConnected() ? "ok" : "mcp not ready" }));
364
+ app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
365
+ await server.connect(transport);
366
+ serve({
367
+ fetch: app.fetch,
368
+ port: http_port,
369
+ });
370
+ log.debug(`Draw.io MCP Server Streamable HTTP transport active`);
371
+ log.debug(`Health check: http://localhost:${http_port}/health`);
372
+ log.debug(`MCP endpoint: http://localhost:${http_port}/mcp`);
373
+ }
374
+ async function main() {
375
+ // Check if help was requested (before parsing config)
376
+ if (shouldShowHelp(process.argv.slice(2))) {
377
+ showHelp();
378
+ // never returns
379
+ }
380
+ // Build configuration from command line args
381
+ const configResult = buildConfig();
382
+ // Handle errors from configuration parsing
383
+ if (configResult instanceof Error) {
384
+ console.error(`Error: ${configResult.message}`);
385
+ process.exit(1);
386
+ }
387
+ const config = configResult;
388
+ await start_websocket_server(config.extensionPort);
389
+ if (config.transports.indexOf("stdio") > -1) {
390
+ await start_stdio_transport();
391
+ }
392
+ if (config.transports.indexOf("http") > -1) {
393
+ start_streamable_http_transport(config.httpPort);
394
+ }
395
+ log.debug(`Draw.io MCP Server running on ${config.transports}`);
319
396
  }
320
397
  main().catch((error) => {
321
398
  log.debug("Fatal error in main():", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawio-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Provides Draw.io services to MCP Clients",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -27,22 +27,25 @@
27
27
  "author": "Ladislav Gazo",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "1.17.5",
31
- "nanoid": "5.1.5",
32
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
33
- "zod": "3.24.3"
30
+ "@hono/node-server": "1.19.7",
31
+ "@modelcontextprotocol/sdk": "1.25.1",
32
+ "hono": "4.11.1",
33
+ "nanoid": "5.1.6",
34
+ "ws": "8.18.3",
35
+ "zod": "4.2.1"
34
36
  },
35
37
  "devDependencies": {
36
- "@jest/globals": "30.1.2",
38
+ "@jest/globals": "30.2.0",
37
39
  "@types/jest": "30.0.0",
38
- "@types/node": "24.3.1",
39
- "globals": "16.3.0",
40
- "jest": "30.1.3",
41
- "jest-environment-node": "30.1.2",
42
- "prettier": "3.5.3",
43
- "rimraf": "6.0.1",
44
- "ts-jest": "29.4.1",
45
- "typescript": "5.9.2"
40
+ "@types/node": "25.0.3",
41
+ "@types/ws": "^8.18.1",
42
+ "globals": "16.5.0",
43
+ "jest": "30.2.0",
44
+ "jest-environment-node": "30.2.0",
45
+ "prettier": "3.7.4",
46
+ "rimraf": "6.1.2",
47
+ "ts-jest": "29.4.6",
48
+ "typescript": "5.9.3"
46
49
  },
47
50
  "scripts": {
48
51
  "build": "tsc",