drawio-mcp-server 1.0.2 → 1.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
CHANGED
|
@@ -130,12 +130,60 @@ The configuration is usually in: ~/.local/share/oterm/config.json
|
|
|
130
130
|
```
|
|
131
131
|
</details>
|
|
132
132
|
|
|
133
|
+
### Connect with Zed
|
|
134
|
+
|
|
135
|
+
1. Open the Zed Preview application.
|
|
136
|
+
1. Click the Assistant (✨) icon in the bottom right corner.
|
|
137
|
+
1. Click Settings in the top right panel of the Assistant.
|
|
138
|
+
1. In the Context Servers section, click + Add Context Server.
|
|
139
|
+
1. Configure with the following:
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary>Using <code>npm</code></summary>
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
/// The name of your MCP server
|
|
147
|
+
"drawio": {
|
|
148
|
+
"command": {
|
|
149
|
+
/// The path to the executable
|
|
150
|
+
"path": "npx",
|
|
151
|
+
/// The arguments to pass to the executable
|
|
152
|
+
"args": ["-y","drawio-mcp-server"],
|
|
153
|
+
/// The environment variables to set for the executable
|
|
154
|
+
"env": {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>Using <code>pnpm</code></summary>
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
/// The name of your MCP server
|
|
167
|
+
"drawio": {
|
|
168
|
+
"command": {
|
|
169
|
+
/// The path to the executable
|
|
170
|
+
"path": "pnpm",
|
|
171
|
+
/// The arguments to pass to the executable
|
|
172
|
+
"args": ["dlx","drawio-mcp-server"],
|
|
173
|
+
/// The environment variables to set for the executable
|
|
174
|
+
"env": {}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
</details>
|
|
180
|
+
|
|
133
181
|
### Browser Extension Setup
|
|
134
182
|
|
|
135
183
|
In order to control the Draw.io diagram, you need to install dedicated Browser Extension.
|
|
136
184
|
|
|
137
185
|
1. Open [Draw.io in your browser](https://app.diagrams.net/)
|
|
138
|
-
2. Install the Draw.io MCP Browser Extension from a web store
|
|
186
|
+
2. Install the Draw.io MCP Browser Extension from a web store or [use other means](https://github.com/lgazo/drawio-mcp-extension)
|
|
139
187
|
<p>
|
|
140
188
|
<a href="https://chrome.google.com/webstore/detail/drawio-mcp-extension/okdbbjbbccdhhfaefmcmekalmmdjjide">
|
|
141
189
|
<picture>
|
|
@@ -208,6 +256,22 @@ The Draw.io MCP server provides the following tools for programmatic diagram int
|
|
|
208
256
|
|
|
209
257
|
## Related Resources
|
|
210
258
|
|
|
259
|
+
[Troubleshooting](./TROUBLESHOOTING.md)
|
|
260
|
+
|
|
261
|
+
[Prompt examples](./docs/examples/index.md)
|
|
262
|
+
|
|
263
|
+
[Contributing](./CONTRIBUTING.md)
|
|
264
|
+
|
|
211
265
|
[Architecture](./ARCHITECTURE.md)
|
|
212
266
|
|
|
213
267
|
[Development](./DEVELOPMENT.md)
|
|
268
|
+
|
|
269
|
+
## Star History
|
|
270
|
+
|
|
271
|
+
<a href="https://star-history.com/#lgazo/drawio-mcp-server&Date">
|
|
272
|
+
<picture>
|
|
273
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lgazo/drawio-mcp-server&type=Date&theme=dark" />
|
|
274
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=lgazo/drawio-mcp-server&type=Date" />
|
|
275
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=lgazo/drawio-mcp-server&type=Date" />
|
|
276
|
+
</picture>
|
|
277
|
+
</a>
|
package/build/index.js
CHANGED
|
@@ -3,17 +3,28 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import EventEmitter from "node:events";
|
|
6
|
+
import { createServer } from "node:net";
|
|
6
7
|
import uWS from "uWebSockets.js";
|
|
7
8
|
import { bus_reply_stream, bus_request_stream, } from "./types.js";
|
|
8
9
|
import { create_bus } from "./emitter_bus.js";
|
|
9
10
|
import { default_tool } from "./tool.js";
|
|
10
11
|
import { nanoid_id_generator } from "./nanoid_id_generator.js";
|
|
11
12
|
import { create_logger } from "./mcp_console_logger.js";
|
|
13
|
+
const PORT = 3333;
|
|
12
14
|
const log = create_logger();
|
|
15
|
+
async function checkPortAvailable(port) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const server = createServer();
|
|
18
|
+
server.listen(port, () => {
|
|
19
|
+
server.close(() => resolve(true));
|
|
20
|
+
});
|
|
21
|
+
server.on("error", () => resolve(false));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
13
24
|
const emitter = new EventEmitter();
|
|
14
25
|
const conns = [];
|
|
15
26
|
const bus_to_ws_forwarder_listener = (event) => {
|
|
16
|
-
log.debug(`[bridge] received;
|
|
27
|
+
log.debug(`[bridge] received; forwarding message to #${conns.length} clients`, event);
|
|
17
28
|
for (let i = 0; i < conns.length; i++) {
|
|
18
29
|
try {
|
|
19
30
|
conns[i].send(JSON.stringify(event));
|
|
@@ -25,8 +36,9 @@ const bus_to_ws_forwarder_listener = (event) => {
|
|
|
25
36
|
};
|
|
26
37
|
emitter.on(bus_request_stream, bus_to_ws_forwarder_listener);
|
|
27
38
|
const ws_handler = {
|
|
39
|
+
maxPayloadLength: 128 * 1024,
|
|
28
40
|
open: (ws) => {
|
|
29
|
-
log.debug(
|
|
41
|
+
log.debug(`[ws_handler] A WebSocket client #${conns.length} connected, presumably MCP Extension!`);
|
|
30
42
|
conns.push(ws);
|
|
31
43
|
},
|
|
32
44
|
message: (ws, message, isBinary) => {
|
|
@@ -39,22 +51,34 @@ const ws_handler = {
|
|
|
39
51
|
emitter.emit(bus_reply_stream, json);
|
|
40
52
|
},
|
|
41
53
|
close: (ws, code, message) => {
|
|
42
|
-
log.debug(
|
|
54
|
+
log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
|
|
43
55
|
//todo remove conn
|
|
44
56
|
},
|
|
45
57
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
log.debug("Listening to port 3333");
|
|
58
|
+
async function start_websocket_server() {
|
|
59
|
+
const isPortAvailable = await checkPortAvailable(PORT);
|
|
60
|
+
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.`);
|
|
62
|
+
process.exit(1);
|
|
52
63
|
}
|
|
53
|
-
|
|
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
|
+
});
|
|
76
|
+
return app;
|
|
77
|
+
}
|
|
54
78
|
// Create server instance
|
|
55
79
|
const server = new McpServer({
|
|
56
80
|
name: "drawio-mcp-server",
|
|
57
|
-
version: "1.0.
|
|
81
|
+
version: "1.0.3",
|
|
58
82
|
capabilities: {
|
|
59
83
|
resources: {},
|
|
60
84
|
tools: {},
|
|
@@ -174,7 +198,36 @@ server.tool(TOOL_add_cell_of_shape, "This tool allows you to add new vertex cell
|
|
|
174
198
|
.optional()
|
|
175
199
|
.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;`"),
|
|
176
200
|
}, default_tool(TOOL_add_cell_of_shape, context));
|
|
201
|
+
const TOOL_list_paged_model = "list-paged-model";
|
|
202
|
+
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.", {
|
|
203
|
+
page: z
|
|
204
|
+
.number()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe("Zero-based page number for pagination. Page 0 returns the first batch of cells, page 1 returns the next batch, etc. Default is 0.")
|
|
207
|
+
.default(0),
|
|
208
|
+
page_size: z
|
|
209
|
+
.number()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("Maximum number of cells to return in a single page. Controls response size and performance. Must be between 1 and 1000. Default is 50.")
|
|
212
|
+
.default(50),
|
|
213
|
+
filter: z
|
|
214
|
+
.object({
|
|
215
|
+
cell_type: z
|
|
216
|
+
.enum(["edge", "vertex", "object", "layer", "group"])
|
|
217
|
+
.optional()
|
|
218
|
+
.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"),
|
|
219
|
+
attributes: z
|
|
220
|
+
.array(z.any())
|
|
221
|
+
.optional()
|
|
222
|
+
.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.'),
|
|
223
|
+
})
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("Optional filter criteria to apply to cells before pagination"),
|
|
226
|
+
}, default_tool(TOOL_list_paged_model, context));
|
|
177
227
|
async function main() {
|
|
228
|
+
log.debug("Draw.io MCP Server starting");
|
|
229
|
+
await start_websocket_server();
|
|
230
|
+
log.debug("Draw.io MCP Server WebSocket started");
|
|
178
231
|
const transport = new StdioServerTransport();
|
|
179
232
|
await server.connect(transport);
|
|
180
233
|
log.debug("Draw.io MCP Server running on stdio");
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export function create_logger() {
|
|
2
2
|
return {
|
|
3
|
-
log: (level, message, data) => {
|
|
4
|
-
return
|
|
3
|
+
log: (level, message, ...data) => {
|
|
4
|
+
return data.length > 0
|
|
5
|
+
? console.error(`${level?.toUpperCase()}: ${message}`, ...data)
|
|
6
|
+
: console.error(`${level?.toUpperCase()}: ${message}`);
|
|
5
7
|
},
|
|
6
|
-
debug: (message, data) => {
|
|
7
|
-
return
|
|
8
|
+
debug: (message, ...data) => {
|
|
9
|
+
return data.length > 0
|
|
10
|
+
? console.error(`DEBUG: ${message}`, ...data)
|
|
11
|
+
: console.error(`DEBUG: ${message}`);
|
|
8
12
|
},
|
|
9
13
|
};
|
|
10
14
|
}
|
|
@@ -29,7 +29,7 @@ describe("create_logger", () => {
|
|
|
29
29
|
const testLevel = "info";
|
|
30
30
|
logger.log(testLevel, testMessage, testData);
|
|
31
31
|
expect(mockConsoleError).toHaveBeenCalledTimes(1);
|
|
32
|
-
expect(mockConsoleError).toHaveBeenCalledWith(testMessage
|
|
32
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`${testLevel.toUpperCase()}: ${testMessage}`, testData);
|
|
33
33
|
});
|
|
34
34
|
it("debug method should call console.error with message and data", () => {
|
|
35
35
|
const logger = create_logger();
|
|
@@ -37,15 +37,27 @@ describe("create_logger", () => {
|
|
|
37
37
|
const testData = { debug: true };
|
|
38
38
|
logger.debug(testMessage, testData);
|
|
39
39
|
expect(mockConsoleError).toHaveBeenCalledTimes(1);
|
|
40
|
-
expect(mockConsoleError).toHaveBeenCalledWith(testMessage
|
|
40
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`, testData);
|
|
41
41
|
});
|
|
42
|
-
it("should handle
|
|
42
|
+
it("should handle no additional data parameters", () => {
|
|
43
43
|
const logger = create_logger();
|
|
44
44
|
const testMessage = "message without data";
|
|
45
45
|
logger.log("warn", testMessage);
|
|
46
|
-
expect(mockConsoleError).toHaveBeenCalledWith(testMessage
|
|
46
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`WARN: ${testMessage}`);
|
|
47
47
|
mockConsoleError.mockClear();
|
|
48
48
|
logger.debug(testMessage);
|
|
49
|
-
expect(mockConsoleError).toHaveBeenCalledWith(testMessage
|
|
49
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`);
|
|
50
|
+
});
|
|
51
|
+
it("should handle multiple data parameters", () => {
|
|
52
|
+
const logger = create_logger();
|
|
53
|
+
const testMessage = "message with multiple data";
|
|
54
|
+
const data1 = { key1: "value1" };
|
|
55
|
+
const data2 = { key2: "value2" };
|
|
56
|
+
const data3 = "string data";
|
|
57
|
+
logger.log("error", testMessage, data1, data2, data3);
|
|
58
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`ERROR: ${testMessage}`, data1, data2, data3);
|
|
59
|
+
mockConsoleError.mockClear();
|
|
60
|
+
logger.debug(testMessage, data1, data2, data3);
|
|
61
|
+
expect(mockConsoleError).toHaveBeenCalledWith(`DEBUG: ${testMessage}`, data1, data2, data3);
|
|
50
62
|
});
|
|
51
63
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export function create_logger() {
|
|
2
2
|
return {
|
|
3
|
-
log: (level, message, data) => {
|
|
4
|
-
return console.log(message, data);
|
|
3
|
+
log: (level, message, ...data) => {
|
|
4
|
+
return console.log(message, ...data);
|
|
5
5
|
},
|
|
6
|
-
debug: (message, data) => {
|
|
7
|
-
return console.debug(message, data);
|
|
6
|
+
debug: (message, ...data) => {
|
|
7
|
+
return console.debug(message, ...data);
|
|
8
8
|
},
|
|
9
9
|
};
|
|
10
10
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { create_logger } from "./standard_console_logger.js";
|
|
3
|
+
describe("create_logger", () => {
|
|
4
|
+
let originalConsoleLog;
|
|
5
|
+
let originalConsoleDebug;
|
|
6
|
+
let mockConsoleLog;
|
|
7
|
+
let mockConsoleDebug;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Save original console methods
|
|
10
|
+
originalConsoleLog = console.log;
|
|
11
|
+
originalConsoleDebug = console.debug;
|
|
12
|
+
// Create mocks
|
|
13
|
+
mockConsoleLog = jest.fn();
|
|
14
|
+
mockConsoleDebug = jest.fn();
|
|
15
|
+
console.log = mockConsoleLog;
|
|
16
|
+
console.debug = mockConsoleDebug;
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Restore original console methods
|
|
20
|
+
console.log = originalConsoleLog;
|
|
21
|
+
console.debug = originalConsoleDebug;
|
|
22
|
+
// Clear all mocks
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
it("should return a Logger object with log and debug methods", () => {
|
|
26
|
+
const logger = create_logger();
|
|
27
|
+
expect(logger).toBeDefined();
|
|
28
|
+
expect(typeof logger.log).toBe("function");
|
|
29
|
+
expect(typeof logger.debug).toBe("function");
|
|
30
|
+
});
|
|
31
|
+
it("log method should call console.log with message and data", () => {
|
|
32
|
+
const logger = create_logger();
|
|
33
|
+
const testMessage = "test message";
|
|
34
|
+
const testData = { key: "value" };
|
|
35
|
+
const testLevel = "info";
|
|
36
|
+
logger.log(testLevel, testMessage, testData);
|
|
37
|
+
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage, testData);
|
|
39
|
+
});
|
|
40
|
+
it("debug method should call console.debug with message and data", () => {
|
|
41
|
+
const logger = create_logger();
|
|
42
|
+
const testMessage = "debug message";
|
|
43
|
+
const testData = { debug: true };
|
|
44
|
+
logger.debug(testMessage, testData);
|
|
45
|
+
expect(mockConsoleDebug).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(mockConsoleDebug).toHaveBeenCalledWith(testMessage, testData);
|
|
47
|
+
});
|
|
48
|
+
it("should handle no additional data parameters", () => {
|
|
49
|
+
const logger = create_logger();
|
|
50
|
+
const testMessage = "message without data";
|
|
51
|
+
logger.log("warn", testMessage);
|
|
52
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage);
|
|
53
|
+
mockConsoleLog.mockClear();
|
|
54
|
+
logger.debug(testMessage);
|
|
55
|
+
expect(mockConsoleDebug).toHaveBeenCalledWith(testMessage);
|
|
56
|
+
});
|
|
57
|
+
it("should ignore level parameter in log method", () => {
|
|
58
|
+
const logger = create_logger();
|
|
59
|
+
const testMessage = "test message";
|
|
60
|
+
const testData = { key: "value" };
|
|
61
|
+
// Test with different levels - should all behave the same
|
|
62
|
+
logger.log("error", testMessage, testData);
|
|
63
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage, testData);
|
|
64
|
+
mockConsoleLog.mockClear();
|
|
65
|
+
logger.log("warn", testMessage, testData);
|
|
66
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage, testData);
|
|
67
|
+
mockConsoleLog.mockClear();
|
|
68
|
+
logger.log("info", testMessage, testData);
|
|
69
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage, testData);
|
|
70
|
+
});
|
|
71
|
+
it("should handle multiple data parameters", () => {
|
|
72
|
+
const logger = create_logger();
|
|
73
|
+
const testMessage = "message with multiple data";
|
|
74
|
+
const data1 = { key1: "value1" };
|
|
75
|
+
const data2 = { key2: "value2" };
|
|
76
|
+
const data3 = "string data";
|
|
77
|
+
logger.log("info", testMessage, data1, data2, data3);
|
|
78
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(testMessage, data1, data2, data3);
|
|
79
|
+
mockConsoleLog.mockClear();
|
|
80
|
+
logger.debug(testMessage, data1, data2, data3);
|
|
81
|
+
expect(mockConsoleDebug).toHaveBeenCalledWith(testMessage, data1, data2, data3);
|
|
82
|
+
});
|
|
83
|
+
});
|