drawio-mcp-server 1.2.1 → 1.4.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,38 @@ 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
+
42
74
  ## Installation
43
75
 
44
76
  ### Connecting with Claude Desktop
@@ -86,6 +118,19 @@ Note: The Draw.io desktop app or web version must be accessible to the system wh
86
118
  ```
87
119
  </details>
88
120
 
121
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "drawio": {
127
+ "command": "npx",
128
+ "args": ["-y", "drawio-mcp-server", "--extension-port", "8080"]
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
89
134
  4. Restart Claude Desktop
90
135
 
91
136
  ### Connecting with oterm
@@ -130,6 +175,19 @@ The configuration is usually in: ~/.local/share/oterm/config.json
130
175
  ```
131
176
  </details>
132
177
 
178
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
179
+
180
+ ```json
181
+ {
182
+ "mcpServers": {
183
+ "drawio": {
184
+ "command": "npx",
185
+ "args": ["-y", "drawio-mcp-server", "--extension-port", "8080"]
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
133
191
  ### Connect with Zed
134
192
 
135
193
  1. Open the Zed Preview application.
@@ -178,6 +236,24 @@ The configuration is usually in: ~/.local/share/oterm/config.json
178
236
  ```
179
237
  </details>
180
238
 
239
+ To use a custom extension port (e.g., 8080), add `"--extension-port", "8080"` to the args array:
240
+
241
+ ```json
242
+ {
243
+ /// The name of your MCP server
244
+ "drawio": {
245
+ "command": {
246
+ /// The path to the executable
247
+ "path": "npx",
248
+ /// The arguments to pass to the executable
249
+ "args": ["-y","drawio-mcp-server","--extension-port","8080"],
250
+ /// The environment variables to set for the executable
251
+ "env": {}
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
181
257
  ### Browser Extension Setup
182
258
 
183
259
  In order to control the Draw.io diagram, you need to install dedicated Browser Extension.
@@ -198,6 +274,8 @@ In order to control the Draw.io diagram, you need to install dedicated Browser E
198
274
  </p>
199
275
  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
276
 
277
+ **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.
278
+
201
279
 
202
280
  ## Sponsoring
203
281
 
@@ -279,6 +357,33 @@ The Draw.io MCP server provides the following tools for programmatic diagram int
279
357
  - `text`: Optional text content
280
358
  - `style`: Optional additional style properties
281
359
 
360
+ - **`set-cell-shape`**
361
+ Applies a library shape's style to an existing cell
362
+ *Parameters*:
363
+ - `cell_id`: ID of the cell whose appearance should change
364
+ - `shape_name`: Name of the library shape whose style should be applied
365
+
366
+ - **`set-cell-data`**
367
+ Stores or updates a custom attribute on a cell
368
+ *Parameters*:
369
+ - `cell_id`: ID of the cell to update
370
+ - `key`: Attribute name to set
371
+ - `value`: Attribute value (stored as a string internally)
372
+
373
+ - **`edit-cell`**
374
+ Updates an existing vertex/shape cell in place by ID
375
+ *Parameters*:
376
+ - `cell_id`: ID of the cell whose properties should change (required)
377
+ - `text`, `x`, `y`, `width`, `height`, `style`: Optional fields to update on the cell; omitted properties stay as-is
378
+
379
+ - **`edit-edge`**
380
+ Updates an existing edge connection between cells by ID
381
+ *Parameters*:
382
+ - `cell_id`: ID of the edge cell to update (required)
383
+ - `text`: Optional edge label text
384
+ - `source_id`, `target_id`: Optional IDs of new source/target cells
385
+ - `style`: Optional replacement style string
386
+
282
387
  ## Related Resources
283
388
 
284
389
  [Troubleshooting](./TROUBLESHOOTING.md)
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Default configuration values
3
+ */
4
+ const DEFAULT_CONFIG = {
5
+ extensionPort: 3333,
6
+ };
7
+ /**
8
+ * Valid port range
9
+ */
10
+ const PORT_RANGE = {
11
+ min: 1,
12
+ max: 65535,
13
+ };
14
+ /**
15
+ * Parse extension port value from string - pure function
16
+ */
17
+ export const parseExtensionPortValue = (value) => {
18
+ if (!value) {
19
+ return new Error("--extension-port flag requires a port number");
20
+ }
21
+ const port = parseInt(value, 10);
22
+ if (isNaN(port)) {
23
+ return new Error(`Invalid port number "${value}". Port must be a number`);
24
+ }
25
+ if (port < PORT_RANGE.min || port > PORT_RANGE.max) {
26
+ return new Error(`Invalid port number "${value}". Port must be between ${PORT_RANGE.min} and ${PORT_RANGE.max}`);
27
+ }
28
+ return port;
29
+ };
30
+ /**
31
+ * Find argument value by flag name - pure function
32
+ */
33
+ export const findArgValue = (args, ...flags) => {
34
+ const index = args.findIndex((arg) => flags.includes(arg));
35
+ return index !== -1 ? args[index + 1] : undefined;
36
+ };
37
+ /**
38
+ * Check if any flag exists in arguments - pure function
39
+ */
40
+ export const hasFlag = (args, ...flags) => {
41
+ return args.some((arg) => flags.includes(arg));
42
+ };
43
+ /**
44
+ * Check if help was requested - pure function
45
+ */
46
+ export const shouldShowHelp = (args) => {
47
+ return hasFlag(args, "--help", "-h");
48
+ };
49
+ /**
50
+ * Parse command line arguments into configuration object
51
+ * Pure function - no side effects, deterministic output
52
+ */
53
+ export const parseConfig = (args) => {
54
+ // Walk arguments so repeated flags allow "last wins" semantics
55
+ let portValue;
56
+ for (let i = 0; i < args.length; i += 1) {
57
+ const arg = args[i];
58
+ if (arg === "--extension-port" || arg === "-p") {
59
+ const nextValue = args[i + 1];
60
+ if (nextValue === undefined) {
61
+ return new Error("--extension-port flag requires a port number");
62
+ }
63
+ portValue = nextValue;
64
+ i += 1; // Skip the value we just consumed
65
+ }
66
+ }
67
+ if (portValue !== undefined) {
68
+ const extensionPort = parseExtensionPortValue(portValue);
69
+ if (extensionPort instanceof Error) {
70
+ return extensionPort;
71
+ }
72
+ return {
73
+ ...DEFAULT_CONFIG,
74
+ extensionPort,
75
+ };
76
+ }
77
+ // Return default configuration
78
+ return DEFAULT_CONFIG;
79
+ };
80
+ /**
81
+ * Build configuration from process.argv
82
+ * This is the main entry point for configuration
83
+ * Returns Error for invalid config, or ServerConfig
84
+ */
85
+ export const buildConfig = () => {
86
+ const args = process.argv.slice(2);
87
+ return parseConfig(args);
88
+ };
@@ -0,0 +1,136 @@
1
+ import { parseExtensionPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, } 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("findArgValue", () => {
26
+ test("finds value after flag", () => {
27
+ expect(findArgValue(["--port", "8080", "--help"], "--port")).toBe("8080");
28
+ });
29
+ test("returns undefined when flag not found", () => {
30
+ expect(findArgValue(["--help"], "--port")).toBeUndefined();
31
+ });
32
+ test("returns undefined when flag is last argument", () => {
33
+ expect(findArgValue(["--port"], "--port")).toBeUndefined();
34
+ });
35
+ test("finds value with short flag", () => {
36
+ expect(findArgValue(["-p", "8080"], "-p")).toBe("8080");
37
+ });
38
+ test("works with readonly array", () => {
39
+ const args = ["--port", "8080"];
40
+ expect(findArgValue(args, "--port")).toBe("8080");
41
+ });
42
+ });
43
+ describe("hasFlag", () => {
44
+ test("returns true when flag exists", () => {
45
+ expect(hasFlag(["--help", "--port", "8080"], "--help")).toBe(true);
46
+ });
47
+ test("returns false when flag does not exist", () => {
48
+ expect(hasFlag(["--port", "8080"], "--help")).toBe(false);
49
+ });
50
+ test("returns true with short flag", () => {
51
+ expect(hasFlag(["-h"], "-h")).toBe(true);
52
+ });
53
+ test("works with multiple flags", () => {
54
+ expect(hasFlag(["-h"], "-h", "--help")).toBe(true);
55
+ });
56
+ test("works with readonly array", () => {
57
+ const args = ["--help"];
58
+ expect(hasFlag(args, "--help")).toBe(true);
59
+ });
60
+ });
61
+ describe("shouldShowHelp", () => {
62
+ test("returns true for --help", () => {
63
+ expect(shouldShowHelp(["--help"])).toBe(true);
64
+ });
65
+ test("returns true for -h", () => {
66
+ expect(shouldShowHelp(["-h"])).toBe(true);
67
+ });
68
+ test("returns false for no help flag", () => {
69
+ expect(shouldShowHelp(["--extension-port", "8080"])).toBe(false);
70
+ });
71
+ test("returns false for empty args", () => {
72
+ expect(shouldShowHelp([])).toBe(false);
73
+ });
74
+ });
75
+ describe("parseConfig", () => {
76
+ test("no args returns default config", () => {
77
+ expect(parseConfig([])).toEqual({ extensionPort: 3333 });
78
+ });
79
+ test("--extension-port flag sets custom port", () => {
80
+ expect(parseConfig(["--extension-port", "8080"])).toEqual({
81
+ extensionPort: 8080,
82
+ });
83
+ });
84
+ test("-p flag sets custom port", () => {
85
+ expect(parseConfig(["-p", "8080"])).toEqual({ extensionPort: 8080 });
86
+ });
87
+ test("help flag is ignored in config parsing", () => {
88
+ expect(parseConfig(["--help"])).toEqual({ extensionPort: 3333 });
89
+ });
90
+ test("invalid port returns Error", () => {
91
+ const result = parseConfig(["--extension-port", "abc"]);
92
+ expect(result).toBeInstanceOf(Error);
93
+ expect(result.message).toContain("Invalid port number");
94
+ });
95
+ test("missing port value returns Error", () => {
96
+ const result = parseConfig(["--extension-port"]);
97
+ expect(result).toBeInstanceOf(Error);
98
+ expect(result.message).toContain("--extension-port flag requires a port number");
99
+ });
100
+ test("out of range port returns Error", () => {
101
+ const result = parseConfig(["--extension-port", "70000"]);
102
+ expect(result).toBeInstanceOf(Error);
103
+ expect(result.message).toContain("Invalid port number");
104
+ });
105
+ test("multiple --extension-port flags uses last one", () => {
106
+ expect(parseConfig(["--extension-port", "8080", "--extension-port", "9090"])).toEqual({
107
+ extensionPort: 9090,
108
+ });
109
+ });
110
+ test("short and long form both work, last wins", () => {
111
+ expect(parseConfig(["--extension-port", "8080", "-p", "9090"])).toEqual({
112
+ extensionPort: 9090,
113
+ });
114
+ });
115
+ });
116
+ describe("buildConfig", () => {
117
+ const originalArgv = process.argv;
118
+ afterEach(() => {
119
+ process.argv = originalArgv;
120
+ });
121
+ test("uses default config with empty args", () => {
122
+ process.argv = ["node", "script.js"];
123
+ const result = buildConfig();
124
+ expect(result).toEqual({ extensionPort: 3333 });
125
+ });
126
+ test("parses custom port from argv", () => {
127
+ process.argv = ["node", "script.js", "--extension-port", "8080"];
128
+ const result = buildConfig();
129
+ expect(result).toEqual({ extensionPort: 8080 });
130
+ });
131
+ test("returns Error for invalid config", () => {
132
+ process.argv = ["node", "script.js", "--extension-port", "abc"];
133
+ const result = buildConfig();
134
+ expect(result).toBeInstanceOf(Error);
135
+ });
136
+ });
package/build/index.js CHANGED
@@ -5,13 +5,34 @@ import { z } from "zod";
5
5
  import EventEmitter from "node:events";
6
6
  import { createServer } from "node:net";
7
7
  import uWS from "uWebSockets.js";
8
+ import { buildConfig, shouldShowHelp } from "./config.js";
8
9
  import { bus_reply_stream, bus_request_stream, } from "./types.js";
9
10
  import { create_bus } from "./emitter_bus.js";
10
11
  import { default_tool } from "./tool.js";
11
12
  import { nanoid_id_generator } from "./nanoid_id_generator.js";
12
13
  import { create_logger as create_console_logger } from "./mcp_console_logger.js";
13
14
  import { create_logger as create_server_logger, validLogLevels, } from "./mcp_server_logger.js";
14
- const PORT = 3333;
15
+ /**
16
+ * Display help message and exit
17
+ */
18
+ function showHelp() {
19
+ console.log(`
20
+ Draw.io MCP Server
21
+
22
+ Usage: drawio-mcp-server [options]
23
+
24
+ Options:
25
+ --extension-port, -p <number> WebSocket server port for browser extension (default: 3333)
26
+ --help, -h Show this help message
27
+
28
+ Examples:
29
+ drawio-mcp-server # Use default extension port 3333
30
+ drawio-mcp-server --extension-port 8080 # Use custom extension port 8080
31
+ drawio-mcp-server -p 8080 # Short form
32
+ `);
33
+ process.exit(0);
34
+ }
35
+ // No PORT constant needed - using dynamic config
15
36
  async function checkPortAvailable(port) {
16
37
  return new Promise((resolve) => {
17
38
  const server = createServer();
@@ -55,21 +76,21 @@ const ws_handler = {
55
76
  //todo remove conn
56
77
  },
57
78
  };
58
- async function start_websocket_server() {
59
- const isPortAvailable = await checkPortAvailable(PORT);
79
+ async function start_websocket_server(extensionPort) {
80
+ const isPortAvailable = await checkPortAvailable(extensionPort);
60
81
  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.`);
82
+ console.error(`[start_websocket_server] Error: Port ${extensionPort} is already in use. Please stop the process using this port and try again.`);
62
83
  process.exit(1);
63
84
  }
64
85
  const app = uWS
65
86
  .App()
66
87
  .ws("/*", ws_handler)
67
- .listen(PORT, (token) => {
88
+ .listen(extensionPort, (token) => {
68
89
  if (token) {
69
- log.debug(`[start_websocket_server] Listening to port ${PORT}`);
90
+ log.debug(`[start_websocket_server] Listening to port ${extensionPort}`);
70
91
  }
71
92
  else {
72
- console.error(`[start_websocket_server] Error: Failed to listen on port ${PORT}`);
93
+ console.error(`[start_websocket_server] Error: Failed to listen on port ${extensionPort}`);
73
94
  process.exit(1);
74
95
  }
75
96
  });
@@ -92,7 +113,7 @@ if (logger_type === "mcp_server") {
92
113
  // Create server instance
93
114
  const server = new McpServer({
94
115
  name: "drawio-mcp-server",
95
- version: "1.2.1",
116
+ version: "1.4.0",
96
117
  }, {
97
118
  capabilities,
98
119
  });
@@ -213,6 +234,68 @@ server.tool(TOOL_add_cell_of_shape, "This tool allows you to add new vertex cell
213
234
  .optional()
214
235
  .describe("Semi-colon separated list of Draw.io visual styles, in the form of `key=value`. Example: `whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;`"),
215
236
  }, default_tool(TOOL_add_cell_of_shape, context));
237
+ const TOOL_set_cell_shape = "set-cell-shape";
238
+ server.tool(TOOL_set_cell_shape, "Updates the visual style of an existing vertex cell to match a library shape by name.", {
239
+ cell_id: z
240
+ .string()
241
+ .describe("Identifier (`id` attribute) of the cell whose shape should change."),
242
+ shape_name: z
243
+ .string()
244
+ .describe("Name of the library shape whose style should be applied to the existing cell."),
245
+ }, default_tool(TOOL_set_cell_shape, context));
246
+ const TOOL_set_cell_data = "set-cell-data";
247
+ server.tool(TOOL_set_cell_data, "Sets or updates a custom attribute on an existing cell.", {
248
+ cell_id: z
249
+ .string()
250
+ .describe("Identifier (`id` attribute) of the cell to update with custom data."),
251
+ key: z.string().describe("Name of the attribute to set on the cell."),
252
+ value: z
253
+ .union([z.string(), z.number(), z.boolean()])
254
+ .describe("Value to store for the attribute. Non-string values are stringified before storage."),
255
+ }, default_tool(TOOL_set_cell_data, context));
256
+ const TOOL_edit_cell = "edit-cell";
257
+ server.tool(TOOL_edit_cell, "Update properties of an existing vertex/shape cell by its ID. Only provided fields are modified; unspecified properties remain unchanged.", {
258
+ cell_id: z
259
+ .string()
260
+ .describe("Identifier (`id` attribute) of the cell to update. Applies to vertex/shape cells."),
261
+ text: z
262
+ .string()
263
+ .optional()
264
+ .describe("Replace the cell's text/label content."),
265
+ x: z
266
+ .number()
267
+ .optional()
268
+ .describe("Set a new X-axis position for the cell."),
269
+ y: z
270
+ .number()
271
+ .optional()
272
+ .describe("Set a new Y-axis position for the cell."),
273
+ width: z.number().optional().describe("Set a new width for the cell."),
274
+ height: z.number().optional().describe("Set a new height for the cell."),
275
+ style: z
276
+ .string()
277
+ .optional()
278
+ .describe("Replace the cell's style string (semi-colon separated `key=value` pairs)."),
279
+ }, default_tool(TOOL_edit_cell, context));
280
+ const TOOL_edit_edge = "edit-edge";
281
+ server.tool(TOOL_edit_edge, "Update properties of an existing edge by its ID. Only provided fields are modified; unspecified properties remain unchanged.", {
282
+ cell_id: z
283
+ .string()
284
+ .describe("Identifier (`id` attribute) of the edge cell to update. The ID must reference an edge."),
285
+ text: z.string().optional().describe("Replace the edge's label text."),
286
+ source_id: z
287
+ .string()
288
+ .optional()
289
+ .describe("Reassign the edge's source terminal to a different cell ID."),
290
+ target_id: z
291
+ .string()
292
+ .optional()
293
+ .describe("Reassign the edge's target terminal to a different cell ID."),
294
+ style: z
295
+ .string()
296
+ .optional()
297
+ .describe("Replace the edge's style string (semi-colon separated `key=value` pairs)."),
298
+ }, default_tool(TOOL_edit_edge, context));
216
299
  const Attributes = z.lazy(() => z
217
300
  .array(z.union([
218
301
  z.string(),
@@ -248,9 +331,22 @@ server.tool(TOOL_list_paged_model, "Retrieves a paginated view of all cells (ver
248
331
  .default({}),
249
332
  }, default_tool(TOOL_list_paged_model, context));
250
333
  async function main() {
251
- log.debug("Draw.io MCP Server starting");
252
- await start_websocket_server();
253
- log.debug("Draw.io MCP Server WebSocket started");
334
+ // Check if help was requested (before parsing config)
335
+ if (shouldShowHelp(process.argv.slice(2))) {
336
+ showHelp();
337
+ // never returns
338
+ }
339
+ // Build configuration from command line args
340
+ const configResult = buildConfig();
341
+ // Handle errors from configuration parsing
342
+ if (configResult instanceof Error) {
343
+ console.error(`Error: ${configResult.message}`);
344
+ process.exit(1);
345
+ }
346
+ const config = configResult;
347
+ log.debug(`Draw.io MCP Server starting (WebSocket extension port: ${config.extensionPort})`);
348
+ await start_websocket_server(config.extensionPort);
349
+ log.debug(`Draw.io MCP Server WebSocket started on extension port ${config.extensionPort}`);
254
350
  const transport = new StdioServerTransport();
255
351
  await server.connect(transport);
256
352
  log.debug("Draw.io MCP Server running on stdio");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawio-mcp-server",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Provides Draw.io services to MCP Clients",
5
5
  "type": "module",
6
6
  "main": "index.js",