drawio-mcp-server 1.4.0 → 1.6.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 +158 -26
- package/build/config.js +92 -1
- package/build/config.test.js +121 -6
- package/build/index.js +110 -48
- package/package.json +17 -14
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
|
|
1
2
|
# Draw.io MCP server
|
|
2
3
|
|
|
3
4
|
Let's do some Vibe Diagramming with the most wide-spread diagramming tool called Draw.io (Diagrams.net).
|
|
4
5
|
|
|
5
|
-
[](https://discord.gg/dM4PWdf42q) [](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml)
|
|
6
|
+
[](https://discord.gg/dM4PWdf42q) [](https://github.com/lgazo/drawio-mcp-server/actions/workflows/ci.yml) [](https://mseep.ai/app/5fc2b7fe-8ceb-4683-97bd-6d31e07b5888)
|
|
6
7
|
|
|
7
8
|
## Introduction
|
|
8
9
|
|
|
@@ -71,6 +72,49 @@ The server listens on port 3333 by default for WebSocket connections from the br
|
|
|
71
72
|
|
|
72
73
|
**Note**: When using a custom port, ensure the browser extension is configured to connect to the same port.
|
|
73
74
|
|
|
75
|
+
### HTTP Transport Port
|
|
76
|
+
|
|
77
|
+
The server can expose a streamable HTTP MCP transport on port 3000. Change this using the `--http-port` flag:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"drawio": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "drawio-mcp-server", "--transport", "http", "--http-port", "4000"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Transport Selection
|
|
91
|
+
|
|
92
|
+
By default only the stdio transport starts. Limit or combine transports with the `--transport` flag:
|
|
93
|
+
|
|
94
|
+
- `--transport stdio` – start only stdio (CLI-friendly)
|
|
95
|
+
- `--transport http` – start only the HTTP transport (for remote clients)
|
|
96
|
+
- `--transport stdio,http` – start both transports
|
|
97
|
+
|
|
98
|
+
### Running the streamable HTTP transport
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
1. Start the server with HTTP enabled (optionally alongside stdio):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
npx -y drawio-mcp-server --transport http --http-port 3000
|
|
106
|
+
# or both: npx -y drawio-mcp-server --transport stdio,http --http-port 4000
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
2. Verify the health endpoint:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
curl http://localhost:3000/health
|
|
113
|
+
# { "status": "ok" }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
74
118
|
## Installation
|
|
75
119
|
|
|
76
120
|
### Connecting with Claude Desktop
|
|
@@ -203,14 +247,15 @@ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to
|
|
|
203
247
|
{
|
|
204
248
|
/// The name of your MCP server
|
|
205
249
|
"drawio": {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
250
|
+
/// The path to the executable
|
|
251
|
+
"command": "npx",
|
|
252
|
+
/// The arguments to pass to the executable
|
|
253
|
+
"args": [
|
|
254
|
+
"-y",
|
|
255
|
+
"drawio-mcp-server"
|
|
256
|
+
],
|
|
257
|
+
/// The environment variables to set for the executable
|
|
258
|
+
"env": {}
|
|
214
259
|
}
|
|
215
260
|
}
|
|
216
261
|
```
|
|
@@ -223,14 +268,15 @@ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to
|
|
|
223
268
|
{
|
|
224
269
|
/// The name of your MCP server
|
|
225
270
|
"drawio": {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
/// The path to the executable
|
|
272
|
+
"command": "pnpm",
|
|
273
|
+
/// The arguments to pass to the executable
|
|
274
|
+
"args": [
|
|
275
|
+
"dlx",
|
|
276
|
+
"drawio-mcp-server"
|
|
277
|
+
],
|
|
278
|
+
/// The environment variables to set for the executable
|
|
279
|
+
"env": {}
|
|
234
280
|
}
|
|
235
281
|
}
|
|
236
282
|
```
|
|
@@ -240,20 +286,74 @@ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to
|
|
|
240
286
|
|
|
241
287
|
```json
|
|
242
288
|
{
|
|
243
|
-
/// The name of your MCP server
|
|
244
289
|
"drawio": {
|
|
245
|
-
"command":
|
|
246
|
-
|
|
247
|
-
"
|
|
248
|
-
|
|
249
|
-
"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
290
|
+
"command": "npx",
|
|
291
|
+
"args": [
|
|
292
|
+
"-y",
|
|
293
|
+
"drawio-mcp-server",
|
|
294
|
+
"--extension-port",
|
|
295
|
+
"8080"
|
|
296
|
+
],
|
|
297
|
+
"env": {}
|
|
253
298
|
}
|
|
254
299
|
}
|
|
255
300
|
```
|
|
256
301
|
|
|
302
|
+
### Connecting with Codex
|
|
303
|
+
|
|
304
|
+
Edit the configuration usually located in: ~/.codex/config.toml
|
|
305
|
+
|
|
306
|
+
<details>
|
|
307
|
+
<summary>Using <code>npm</code></summary>
|
|
308
|
+
|
|
309
|
+
```toml
|
|
310
|
+
[mcp_servers.drawio]
|
|
311
|
+
command = "npx"
|
|
312
|
+
args = [
|
|
313
|
+
"-y",
|
|
314
|
+
"drawio-mcp-server"
|
|
315
|
+
]
|
|
316
|
+
```
|
|
317
|
+
</details>
|
|
318
|
+
|
|
319
|
+
<details>
|
|
320
|
+
<summary>Using <code>pnpm</code></summary>
|
|
321
|
+
|
|
322
|
+
```toml
|
|
323
|
+
[mcp_servers.drawio]
|
|
324
|
+
command = "pnpm"
|
|
325
|
+
args = [
|
|
326
|
+
"dlx",
|
|
327
|
+
"drawio-mcp-server"
|
|
328
|
+
]
|
|
329
|
+
```
|
|
330
|
+
</details>
|
|
331
|
+
|
|
332
|
+
To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
|
|
333
|
+
|
|
334
|
+
<details>
|
|
335
|
+
<summary>Using <code>npm</code></summary>
|
|
336
|
+
|
|
337
|
+
```toml
|
|
338
|
+
[mcp_servers.drawio]
|
|
339
|
+
command = "npx"
|
|
340
|
+
args = [
|
|
341
|
+
"-y",
|
|
342
|
+
"drawio-mcp-server",
|
|
343
|
+
"--extension-port",
|
|
344
|
+
"8080"
|
|
345
|
+
]
|
|
346
|
+
```
|
|
347
|
+
</details>
|
|
348
|
+
|
|
349
|
+
To connect to a locally running MCP with Streamable HTTP transport:
|
|
350
|
+
|
|
351
|
+
```toml
|
|
352
|
+
[mcp_servers.drawio]
|
|
353
|
+
url = "http://localhost:3000/mcp"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
|
|
257
357
|
### Browser Extension Setup
|
|
258
358
|
|
|
259
359
|
In order to control the Draw.io diagram, you need to install dedicated Browser Extension.
|
|
@@ -384,6 +484,34 @@ The Draw.io MCP server provides the following tools for programmatic diagram int
|
|
|
384
484
|
- `source_id`, `target_id`: Optional IDs of new source/target cells
|
|
385
485
|
- `style`: Optional replacement style string
|
|
386
486
|
|
|
487
|
+
### Layer Management Tools
|
|
488
|
+
- **`list-layers`**
|
|
489
|
+
Lists all available layers in the diagram with their IDs and names
|
|
490
|
+
*Returns*: Array of layer objects with properties (ID, name, visibility, locked status)
|
|
491
|
+
|
|
492
|
+
- **`set-active-layer`**
|
|
493
|
+
Sets the active layer for creating new elements. All subsequent element creation will happen in this layer
|
|
494
|
+
*Parameters*:
|
|
495
|
+
- `layer_id`: ID of the layer to set as active
|
|
496
|
+
*Returns*: Information about the newly active layer
|
|
497
|
+
|
|
498
|
+
- **`move-cell-to-layer`**
|
|
499
|
+
Moves a cell from its current layer to a target layer
|
|
500
|
+
*Parameters*:
|
|
501
|
+
- `cell_id`: ID of the cell to move
|
|
502
|
+
- `target_layer_id`: ID of the target layer where the cell will be moved
|
|
503
|
+
*Returns*: Confirmation of the move operation
|
|
504
|
+
|
|
505
|
+
- **`get-active-layer`**
|
|
506
|
+
Gets the currently active layer information
|
|
507
|
+
*Returns*: Information about the current active layer (ID and name)
|
|
508
|
+
|
|
509
|
+
- **`create-layer`**
|
|
510
|
+
Creates a new layer in the diagram
|
|
511
|
+
*Parameters*:
|
|
512
|
+
- `name`: Name for the new layer
|
|
513
|
+
*Returns*: Information about the newly created layer
|
|
514
|
+
|
|
387
515
|
## Related Resources
|
|
388
516
|
|
|
389
517
|
[Troubleshooting](./TROUBLESHOOTING.md)
|
|
@@ -405,3 +533,7 @@ The Draw.io MCP server provides the following tools for programmatic diagram int
|
|
|
405
533
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=lgazo/drawio-mcp-server&type=Date" />
|
|
406
534
|
</picture>
|
|
407
535
|
</a>
|
|
536
|
+
|
|
537
|
+
## Assessments
|
|
538
|
+
|
|
539
|
+
[](https://mseep.ai/app/lgazo-drawio-mcp-server)
|
package/build/config.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
5
|
extensionPort: 3333,
|
|
6
|
+
httpPort: 3000,
|
|
7
|
+
transports: ["stdio"],
|
|
6
8
|
};
|
|
7
9
|
/**
|
|
8
10
|
* Valid port range
|
|
@@ -27,6 +29,45 @@ export const parseExtensionPortValue = (value) => {
|
|
|
27
29
|
}
|
|
28
30
|
return port;
|
|
29
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
|
+
};
|
|
30
71
|
/**
|
|
31
72
|
* Find argument value by flag name - pure function
|
|
32
73
|
*/
|
|
@@ -53,6 +94,9 @@ export const shouldShowHelp = (args) => {
|
|
|
53
94
|
export const parseConfig = (args) => {
|
|
54
95
|
// Walk arguments so repeated flags allow "last wins" semantics
|
|
55
96
|
let portValue;
|
|
97
|
+
let httpPortValue;
|
|
98
|
+
let parsedHttpPort;
|
|
99
|
+
let transportValues;
|
|
56
100
|
for (let i = 0; i < args.length; i += 1) {
|
|
57
101
|
const arg = args[i];
|
|
58
102
|
if (arg === "--extension-port" || arg === "-p") {
|
|
@@ -63,19 +107,66 @@ export const parseConfig = (args) => {
|
|
|
63
107
|
portValue = nextValue;
|
|
64
108
|
i += 1; // Skip the value we just consumed
|
|
65
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;
|
|
66
133
|
}
|
|
67
134
|
if (portValue !== undefined) {
|
|
68
135
|
const extensionPort = parseExtensionPortValue(portValue);
|
|
69
136
|
if (extensionPort instanceof Error) {
|
|
70
137
|
return extensionPort;
|
|
71
138
|
}
|
|
139
|
+
const transports = parseTransports(transportValues);
|
|
140
|
+
if (transports instanceof Error) {
|
|
141
|
+
return transports;
|
|
142
|
+
}
|
|
72
143
|
return {
|
|
73
144
|
...DEFAULT_CONFIG,
|
|
74
145
|
extensionPort,
|
|
146
|
+
httpPort: parsedHttpPort !== undefined ? parsedHttpPort : DEFAULT_CONFIG.httpPort,
|
|
147
|
+
transports,
|
|
75
148
|
};
|
|
76
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
|
+
}
|
|
77
165
|
// Return default configuration
|
|
78
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
...DEFAULT_CONFIG,
|
|
168
|
+
transports,
|
|
169
|
+
};
|
|
79
170
|
};
|
|
80
171
|
/**
|
|
81
172
|
* Build configuration from process.argv
|
package/build/config.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseExtensionPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, } from "./config.js";
|
|
1
|
+
import { parseExtensionPortValue, parseHttpPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, parseTransports, } from "./config.js";
|
|
2
2
|
describe("parseExtensionPortValue", () => {
|
|
3
3
|
test("valid port returns number", () => {
|
|
4
4
|
expect(parseExtensionPortValue("8080")).toBe(8080);
|
|
@@ -22,6 +22,42 @@ describe("parseExtensionPortValue", () => {
|
|
|
22
22
|
expect(parseExtensionPortValue("65535")).toBe(65535);
|
|
23
23
|
});
|
|
24
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
|
+
});
|
|
25
61
|
describe("findArgValue", () => {
|
|
26
62
|
test("finds value after flag", () => {
|
|
27
63
|
expect(findArgValue(["--port", "8080", "--help"], "--port")).toBe("8080");
|
|
@@ -74,18 +110,46 @@ describe("shouldShowHelp", () => {
|
|
|
74
110
|
});
|
|
75
111
|
describe("parseConfig", () => {
|
|
76
112
|
test("no args returns default config", () => {
|
|
77
|
-
expect(parseConfig([])).toEqual({
|
|
113
|
+
expect(parseConfig([])).toEqual({
|
|
114
|
+
extensionPort: 3333,
|
|
115
|
+
httpPort: 3000,
|
|
116
|
+
transports: ["stdio"],
|
|
117
|
+
});
|
|
78
118
|
});
|
|
79
119
|
test("--extension-port flag sets custom port", () => {
|
|
80
120
|
expect(parseConfig(["--extension-port", "8080"])).toEqual({
|
|
81
121
|
extensionPort: 8080,
|
|
122
|
+
httpPort: 3000,
|
|
123
|
+
transports: ["stdio"],
|
|
82
124
|
});
|
|
83
125
|
});
|
|
84
126
|
test("-p flag sets custom port", () => {
|
|
85
|
-
expect(parseConfig(["-p", "8080"])).toEqual({
|
|
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
|
+
});
|
|
86
146
|
});
|
|
87
147
|
test("help flag is ignored in config parsing", () => {
|
|
88
|
-
expect(parseConfig(["--help"])).toEqual({
|
|
148
|
+
expect(parseConfig(["--help"])).toEqual({
|
|
149
|
+
extensionPort: 3333,
|
|
150
|
+
httpPort: 3000,
|
|
151
|
+
transports: ["stdio"],
|
|
152
|
+
});
|
|
89
153
|
});
|
|
90
154
|
test("invalid port returns Error", () => {
|
|
91
155
|
const result = parseConfig(["--extension-port", "abc"]);
|
|
@@ -97,6 +161,11 @@ describe("parseConfig", () => {
|
|
|
97
161
|
expect(result).toBeInstanceOf(Error);
|
|
98
162
|
expect(result.message).toContain("--extension-port flag requires a port number");
|
|
99
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
|
+
});
|
|
100
169
|
test("out of range port returns Error", () => {
|
|
101
170
|
const result = parseConfig(["--extension-port", "70000"]);
|
|
102
171
|
expect(result).toBeInstanceOf(Error);
|
|
@@ -105,13 +174,42 @@ describe("parseConfig", () => {
|
|
|
105
174
|
test("multiple --extension-port flags uses last one", () => {
|
|
106
175
|
expect(parseConfig(["--extension-port", "8080", "--extension-port", "9090"])).toEqual({
|
|
107
176
|
extensionPort: 9090,
|
|
177
|
+
httpPort: 3000,
|
|
178
|
+
transports: ["stdio"],
|
|
108
179
|
});
|
|
109
180
|
});
|
|
110
181
|
test("short and long form both work, last wins", () => {
|
|
111
182
|
expect(parseConfig(["--extension-port", "8080", "-p", "9090"])).toEqual({
|
|
112
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"],
|
|
113
200
|
});
|
|
114
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
|
+
});
|
|
115
213
|
});
|
|
116
214
|
describe("buildConfig", () => {
|
|
117
215
|
const originalArgv = process.argv;
|
|
@@ -121,12 +219,29 @@ describe("buildConfig", () => {
|
|
|
121
219
|
test("uses default config with empty args", () => {
|
|
122
220
|
process.argv = ["node", "script.js"];
|
|
123
221
|
const result = buildConfig();
|
|
124
|
-
expect(result).toEqual({
|
|
222
|
+
expect(result).toEqual({
|
|
223
|
+
extensionPort: 3333,
|
|
224
|
+
httpPort: 3000,
|
|
225
|
+
transports: ["stdio"],
|
|
226
|
+
});
|
|
125
227
|
});
|
|
126
228
|
test("parses custom port from argv", () => {
|
|
127
229
|
process.argv = ["node", "script.js", "--extension-port", "8080"];
|
|
128
230
|
const result = buildConfig();
|
|
129
|
-
expect(result).toEqual({
|
|
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
|
+
});
|
|
130
245
|
});
|
|
131
246
|
test("returns Error for invalid config", () => {
|
|
132
247
|
process.argv = ["node", "script.js", "--extension-port", "abc"];
|
package/build/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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
|
|
11
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
8
12
|
import { buildConfig, shouldShowHelp } from "./config.js";
|
|
9
13
|
import { bus_reply_stream, bus_request_stream, } from "./types.js";
|
|
10
14
|
import { create_bus } from "./emitter_bus.js";
|
|
@@ -43,58 +47,63 @@ async function checkPortAvailable(port) {
|
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
const emitter = new EventEmitter();
|
|
46
|
-
const conns =
|
|
50
|
+
const conns = new Set();
|
|
47
51
|
const bus_to_ws_forwarder_listener = (event) => {
|
|
48
|
-
log.debug(`[bridge] received; forwarding message to #${conns.
|
|
49
|
-
for (
|
|
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
|
+
}
|
|
50
58
|
try {
|
|
51
|
-
|
|
59
|
+
ws.send(JSON.stringify(event));
|
|
52
60
|
}
|
|
53
61
|
catch (e) {
|
|
54
|
-
log.debug(
|
|
62
|
+
log.debug("[bridge] error forwarding request", e);
|
|
63
|
+
conns.delete(ws);
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
};
|
|
58
67
|
emitter.on(bus_request_stream, bus_to_ws_forwarder_listener);
|
|
59
|
-
const ws_handler = {
|
|
60
|
-
maxPayloadLength: 128 * 1024,
|
|
61
|
-
open: (ws) => {
|
|
62
|
-
log.debug(`[ws_handler] A WebSocket client #${conns.length} connected, presumably MCP Extension!`);
|
|
63
|
-
conns.push(ws);
|
|
64
|
-
},
|
|
65
|
-
message: (ws, message, isBinary) => {
|
|
66
|
-
// ws.send(message, isBinary);
|
|
67
|
-
const decoder = new TextDecoder();
|
|
68
|
-
const str = decoder.decode(message);
|
|
69
|
-
const json = JSON.parse(str);
|
|
70
|
-
log.debug(`[ws] received from Extension`, json);
|
|
71
|
-
// const event_name = message.__event;
|
|
72
|
-
emitter.emit(bus_reply_stream, json);
|
|
73
|
-
},
|
|
74
|
-
close: (ws, code, message) => {
|
|
75
|
-
log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
|
|
76
|
-
//todo remove conn
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
68
|
async function start_websocket_server(extensionPort) {
|
|
69
|
+
log.debug(`Draw.io MCP Server starting (WebSocket extension port: ${extensionPort})`);
|
|
80
70
|
const isPortAvailable = await checkPortAvailable(extensionPort);
|
|
81
71
|
if (!isPortAvailable) {
|
|
82
72
|
console.error(`[start_websocket_server] Error: Port ${extensionPort} is already in use. Please stop the process using this port and try again.`);
|
|
83
73
|
process.exit(1);
|
|
84
74
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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}`);
|
|
96
101
|
});
|
|
97
|
-
|
|
102
|
+
server.on("error", (error) => {
|
|
103
|
+
console.error(`[start_websocket_server] Error: Failed to listen on port ${extensionPort}`, error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
return server;
|
|
98
107
|
}
|
|
99
108
|
const logger_type = process.env.LOGGER_TYPE;
|
|
100
109
|
let capabilities = {
|
|
@@ -113,7 +122,7 @@ if (logger_type === "mcp_server") {
|
|
|
113
122
|
// Create server instance
|
|
114
123
|
const server = new McpServer({
|
|
115
124
|
name: "drawio-mcp-server",
|
|
116
|
-
version: "1.
|
|
125
|
+
version: "1.6.0",
|
|
117
126
|
}, {
|
|
118
127
|
capabilities,
|
|
119
128
|
});
|
|
@@ -303,7 +312,8 @@ const Attributes = z.lazy(() => z
|
|
|
303
312
|
]))
|
|
304
313
|
.refine((arr) => arr.length === 0 || typeof arr[0] === "string", {
|
|
305
314
|
message: "If not empty, the first element must be a string operator",
|
|
306
|
-
})
|
|
315
|
+
})
|
|
316
|
+
.default([]));
|
|
307
317
|
const TOOL_list_paged_model = "list-paged-model";
|
|
308
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.", {
|
|
309
319
|
page: z
|
|
@@ -322,14 +332,64 @@ server.tool(TOOL_list_paged_model, "Retrieves a paginated view of all cells (ver
|
|
|
322
332
|
.enum(["edge", "vertex", "object", "layer", "group"])
|
|
323
333
|
.optional()
|
|
324
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"),
|
|
325
|
-
attributes: Attributes.optional()
|
|
326
|
-
.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.')
|
|
327
|
-
.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.'),
|
|
328
336
|
})
|
|
329
337
|
.optional()
|
|
330
338
|
.describe("Optional filter criteria to apply to cells before pagination")
|
|
331
339
|
.default({}),
|
|
332
340
|
}, default_tool(TOOL_list_paged_model, context));
|
|
341
|
+
const TOOL_list_layers = "list-layers";
|
|
342
|
+
server.tool(TOOL_list_layers, "Lists all available layers in the diagram with their IDs and names.", {}, default_tool(TOOL_list_layers, context));
|
|
343
|
+
const TOOL_set_active_layer = "set-active-layer";
|
|
344
|
+
server.tool(TOOL_set_active_layer, "Sets the active layer for creating new elements. All subsequent element creation will happen in this layer.", {
|
|
345
|
+
layer_id: z.string().describe("ID of the layer to set as active"),
|
|
346
|
+
}, default_tool(TOOL_set_active_layer, context));
|
|
347
|
+
const TOOL_move_cell_to_layer = "move-cell-to-layer";
|
|
348
|
+
server.tool(TOOL_move_cell_to_layer, "Moves a cell from its current layer to a target layer.", {
|
|
349
|
+
cell_id: z.string().describe("ID of the cell to move"),
|
|
350
|
+
target_layer_id: z
|
|
351
|
+
.string()
|
|
352
|
+
.describe("ID of the target layer where the cell will be moved"),
|
|
353
|
+
}, default_tool(TOOL_move_cell_to_layer, context));
|
|
354
|
+
const TOOL_get_active_layer = "get-active-layer";
|
|
355
|
+
server.tool(TOOL_get_active_layer, "Gets the currently active layer information.", {}, default_tool(TOOL_get_active_layer, context));
|
|
356
|
+
const TOOL_create_layer = "create-layer";
|
|
357
|
+
server.tool(TOOL_create_layer, "Creates a new layer in the diagram.", {
|
|
358
|
+
name: z.string().describe("Name for the new layer"),
|
|
359
|
+
}, default_tool(TOOL_create_layer, context));
|
|
360
|
+
async function start_stdio_transport() {
|
|
361
|
+
const transport = new StdioServerTransport();
|
|
362
|
+
await server.connect(transport);
|
|
363
|
+
log.debug(`Draw.io MCP Server STDIO transport active`);
|
|
364
|
+
}
|
|
365
|
+
async function start_streamable_http_transport(http_port) {
|
|
366
|
+
// Create a stateless transport (no options = no session management)
|
|
367
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
368
|
+
// Create the Hono app
|
|
369
|
+
const app = new Hono();
|
|
370
|
+
// Enable CORS for all origins
|
|
371
|
+
app.use("*", cors({
|
|
372
|
+
origin: "*",
|
|
373
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
374
|
+
allowHeaders: [
|
|
375
|
+
"Content-Type",
|
|
376
|
+
"mcp-session-id",
|
|
377
|
+
"Last-Event-ID",
|
|
378
|
+
"mcp-protocol-version",
|
|
379
|
+
],
|
|
380
|
+
exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
|
|
381
|
+
}));
|
|
382
|
+
app.get("/health", (c) => c.json({ status: server.isConnected() ? "ok" : "mcp not ready" }));
|
|
383
|
+
app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
|
|
384
|
+
await server.connect(transport);
|
|
385
|
+
serve({
|
|
386
|
+
fetch: app.fetch,
|
|
387
|
+
port: http_port,
|
|
388
|
+
});
|
|
389
|
+
log.debug(`Draw.io MCP Server Streamable HTTP transport active`);
|
|
390
|
+
log.debug(`Health check: http://localhost:${http_port}/health`);
|
|
391
|
+
log.debug(`MCP endpoint: http://localhost:${http_port}/mcp`);
|
|
392
|
+
}
|
|
333
393
|
async function main() {
|
|
334
394
|
// Check if help was requested (before parsing config)
|
|
335
395
|
if (shouldShowHelp(process.argv.slice(2))) {
|
|
@@ -344,12 +404,14 @@ async function main() {
|
|
|
344
404
|
process.exit(1);
|
|
345
405
|
}
|
|
346
406
|
const config = configResult;
|
|
347
|
-
log.debug(`Draw.io MCP Server starting (WebSocket extension port: ${config.extensionPort})`);
|
|
348
407
|
await start_websocket_server(config.extensionPort);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
408
|
+
if (config.transports.indexOf("stdio") > -1) {
|
|
409
|
+
await start_stdio_transport();
|
|
410
|
+
}
|
|
411
|
+
if (config.transports.indexOf("http") > -1) {
|
|
412
|
+
start_streamable_http_transport(config.httpPort);
|
|
413
|
+
}
|
|
414
|
+
log.debug(`Draw.io MCP Server running on ${config.transports}`);
|
|
353
415
|
}
|
|
354
416
|
main().catch((error) => {
|
|
355
417
|
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
|
+
"version": "1.6.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
|
-
"@
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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.
|
|
38
|
+
"@jest/globals": "30.2.0",
|
|
37
39
|
"@types/jest": "30.0.0",
|
|
38
|
-
"@types/node": "
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"jest
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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",
|