drawio-mcp-server 1.7.0 → 2.0.3
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 +96 -425
- package/build/assets/downloader.js +96 -0
- package/build/assets/index.js +2 -0
- package/build/assets/manager.js +31 -0
- package/build/config.js +45 -0
- package/build/config.test.js +54 -0
- package/build/emitter_bus.js +2 -3
- package/build/index.js +305 -337
- package/build/plugin/mcp-plugin.js +2618 -0
- package/build/prefetch-assets.js +11 -0
- package/build/real-environment/add-cell-of-shape.test.js +51 -0
- package/build/real-environment/add-edge.test.js +86 -0
- package/build/real-environment/assertions.js +18 -0
- package/build/real-environment/delete-cell-by-id.test.js +45 -0
- package/build/real-environment/edge-editing.test.js +70 -0
- package/build/real-environment/edit-cell.test.js +64 -0
- package/build/real-environment/harness.js +175 -0
- package/build/real-environment/import-export.test.js +82 -0
- package/build/real-environment/layers-and-selection.test.js +70 -0
- package/build/real-environment/logger.js +25 -0
- package/build/real-environment/screenshot.js +46 -0
- package/build/real-environment/set-cell-parent.test.js +64 -0
- package/build/real-environment/shapes.test.js +153 -0
- package/build/real-environment/test-helpers.js +10 -0
- package/build/real-environment/tools.js +22 -0
- package/build/real-environment/types.js +1 -0
- package/build/tool.js +46 -0
- package/build/tools/add-cell-of-shape.js +42 -0
- package/build/tools/add-edge.js +33 -0
- package/build/tools/add-rectangle.js +41 -0
- package/build/tools/create-layer.js +8 -0
- package/build/tools/delete-cell-by-id.js +10 -0
- package/build/tools/edit-cell.js +28 -0
- package/build/tools/edit-edge.js +30 -0
- package/build/tools/export-diagram.js +96 -0
- package/build/tools/get-active-layer.js +5 -0
- package/build/tools/get-selected-cell.js +5 -0
- package/build/tools/get-shape-by-name.js +10 -0
- package/build/tools/get-shape-categories.js +5 -0
- package/build/tools/get-shapes-in-category.js +10 -0
- package/build/tools/import-diagram.js +22 -0
- package/build/tools/index.js +49 -0
- package/build/tools/list-layers.js +5 -0
- package/build/tools/list-paged-model.js +41 -0
- package/build/tools/move-cell-to-layer.js +11 -0
- package/build/tools/set-active-layer.js +8 -0
- package/build/tools/set-cell-data.js +14 -0
- package/build/tools/set-cell-parent.js +9 -0
- package/build/tools/set-cell-shape.js +13 -0
- package/build/tools/shared.js +7 -0
- package/build/tools/types.js +1 -0
- package/package.json +29 -22
package/build/index.js
CHANGED
|
@@ -3,20 +3,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
|
-
import { z } from "zod";
|
|
7
6
|
import { Hono } from "hono";
|
|
8
7
|
import { cors } from "hono/cors";
|
|
9
8
|
import EventEmitter from "node:events";
|
|
10
9
|
import { createServer } from "node:net";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { readFileSync, existsSync, statSync, readdirSync, realpathSync, } from "node:fs";
|
|
11
13
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12
|
-
|
|
14
|
+
const VERSION = process.env.npm_package_version ?? "2.0.0";
|
|
15
|
+
import { buildConfig, shouldShowHelp, getHttpFeatureConfig, } from "./config.js";
|
|
13
16
|
import { bus_reply_stream, bus_request_stream, } from "./types.js";
|
|
14
17
|
import { create_bus } from "./emitter_bus.js";
|
|
15
|
-
import { default_tool } from "./tool.js";
|
|
16
18
|
import { nanoid_id_generator } from "./nanoid_id_generator.js";
|
|
17
19
|
import { create_logger as create_console_logger } from "./mcp_console_logger.js";
|
|
18
20
|
import { create_logger as create_server_logger, validLogLevels, } from "./mcp_server_logger.js";
|
|
19
|
-
|
|
21
|
+
import { getLocalPluginPath, isUsingLocalAssets, getAssetRoot, ensureAssets, } from "./assets/index.js";
|
|
22
|
+
import { registerTools } from "./tools/index.js";
|
|
23
|
+
const fatalLog = create_console_logger();
|
|
20
24
|
/**
|
|
21
25
|
* Display help message and exit
|
|
22
26
|
*/
|
|
@@ -28,12 +32,19 @@ Usage: drawio-mcp-server [options]
|
|
|
28
32
|
|
|
29
33
|
Options:
|
|
30
34
|
--extension-port, -p <number> WebSocket server port for browser extension (default: 3333)
|
|
35
|
+
--editor, -e Enable draw.io editor endpoint
|
|
36
|
+
--http-port HTTP server port for Streamable HTTP transport (default: 3000)
|
|
37
|
+
--transport Transport type: stdio, http (default: stdio)
|
|
38
|
+
--asset-path <path> Custom path for downloaded assets
|
|
31
39
|
--help, -h Show this help message
|
|
32
40
|
|
|
33
41
|
Examples:
|
|
34
42
|
drawio-mcp-server # Use default extension port 3333
|
|
35
43
|
drawio-mcp-server --extension-port 8080 # Use custom extension port 8080
|
|
36
44
|
drawio-mcp-server -p 8080 # Short form
|
|
45
|
+
drawio-mcp-server --editor # Enable draw.io editor endpoint
|
|
46
|
+
drawio-mcp-server -e --http # Enable editor and HTTP transport
|
|
47
|
+
drawio-mcp-server --editor --asset-path /data/assets # Use custom asset path
|
|
37
48
|
`);
|
|
38
49
|
process.exit(0);
|
|
39
50
|
}
|
|
@@ -47,65 +58,6 @@ async function checkPortAvailable(port) {
|
|
|
47
58
|
server.on("error", () => resolve(false));
|
|
48
59
|
});
|
|
49
60
|
}
|
|
50
|
-
const emitter = new EventEmitter();
|
|
51
|
-
const conns = new Set();
|
|
52
|
-
const bus_to_ws_forwarder_listener = (event) => {
|
|
53
|
-
log.debug(`[bridge] received; forwarding message to #${conns.size} clients`, event);
|
|
54
|
-
for (const ws of [...conns]) {
|
|
55
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
56
|
-
conns.delete(ws);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
ws.send(JSON.stringify(event));
|
|
61
|
-
}
|
|
62
|
-
catch (e) {
|
|
63
|
-
log.debug("[bridge] error forwarding request", e);
|
|
64
|
-
conns.delete(ws);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
emitter.on(bus_request_stream, bus_to_ws_forwarder_listener);
|
|
69
|
-
async function start_websocket_server(extensionPort) {
|
|
70
|
-
log.debug(`Draw.io MCP Server (${VERSION}) starting (WebSocket extension port: ${extensionPort})`);
|
|
71
|
-
const isPortAvailable = await checkPortAvailable(extensionPort);
|
|
72
|
-
if (!isPortAvailable) {
|
|
73
|
-
console.error(`[start_websocket_server] Error: Port ${extensionPort} is already in use. Please stop the process using this port and try again.`);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
const server = new WebSocketServer({ port: extensionPort });
|
|
77
|
-
server.on("connection", (ws) => {
|
|
78
|
-
log.debug(`[ws_handler] A WebSocket client #${conns.size} connected, presumably MCP Extension!`);
|
|
79
|
-
conns.add(ws);
|
|
80
|
-
ws.on("message", (data) => {
|
|
81
|
-
const str = typeof data === "string" ? data : data.toString();
|
|
82
|
-
try {
|
|
83
|
-
const json = JSON.parse(str);
|
|
84
|
-
log.debug(`[ws] received from Extension`, json);
|
|
85
|
-
emitter.emit(bus_reply_stream, json);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
log.debug(`[ws] failed to parse message`, error);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
ws.on("close", (code) => {
|
|
92
|
-
conns.delete(ws);
|
|
93
|
-
log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
|
|
94
|
-
});
|
|
95
|
-
ws.on("error", (error) => {
|
|
96
|
-
log.debug(`[ws_handler] WebSocket client error`, error);
|
|
97
|
-
conns.delete(ws);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
server.on("listening", () => {
|
|
101
|
-
log.debug(`[start_websocket_server] Listening to port ${extensionPort}`);
|
|
102
|
-
});
|
|
103
|
-
server.on("error", (error) => {
|
|
104
|
-
console.error(`[start_websocket_server] Error: Failed to listen on port ${extensionPort}`, error);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
});
|
|
107
|
-
return server;
|
|
108
|
-
}
|
|
109
61
|
const logger_type = process.env.LOGGER_TYPE;
|
|
110
62
|
let capabilities = {
|
|
111
63
|
resources: {},
|
|
@@ -120,267 +72,12 @@ if (logger_type === "mcp_server") {
|
|
|
120
72
|
},
|
|
121
73
|
};
|
|
122
74
|
}
|
|
123
|
-
|
|
124
|
-
const server = new McpServer({
|
|
125
|
-
name: "drawio-mcp-server",
|
|
126
|
-
version: VERSION,
|
|
127
|
-
}, {
|
|
128
|
-
capabilities,
|
|
129
|
-
});
|
|
130
|
-
const log = logger_type === "mcp_server"
|
|
131
|
-
? create_server_logger(server)
|
|
132
|
-
: create_console_logger();
|
|
133
|
-
const bus = create_bus(log)(emitter);
|
|
134
|
-
const id_generator = nanoid_id_generator();
|
|
135
|
-
const context = {
|
|
136
|
-
bus,
|
|
137
|
-
id_generator,
|
|
138
|
-
log,
|
|
139
|
-
};
|
|
140
|
-
const TOOL_get_selected_cell = "get-selected-cell";
|
|
141
|
-
server.tool(TOOL_get_selected_cell, "This tool allows you to retrieve selected cell (whether vertex or edge) on the current page of a Draw.io diagram. The response is a JSON containing attributes of the cell.", {}, default_tool(TOOL_get_selected_cell, context));
|
|
142
|
-
const TOOL_add_rectangle = "add-rectangle";
|
|
143
|
-
server.tool(TOOL_add_rectangle, "This tool allows you to add new Rectangle vertex cell (object) on the current page of a Draw.io diagram. It accepts multiple optional input parameter.", {
|
|
144
|
-
x: z
|
|
145
|
-
.number()
|
|
146
|
-
.optional()
|
|
147
|
-
.describe("X-axis position of the Rectangle vertex cell")
|
|
148
|
-
.default(100),
|
|
149
|
-
y: z
|
|
150
|
-
.number()
|
|
151
|
-
.optional()
|
|
152
|
-
.describe("Y-axis position of the Rectangle vertex cell")
|
|
153
|
-
.default(100),
|
|
154
|
-
width: z
|
|
155
|
-
.number()
|
|
156
|
-
.optional()
|
|
157
|
-
.describe("Width of the Rectangle vertex cell")
|
|
158
|
-
.default(200),
|
|
159
|
-
height: z
|
|
160
|
-
.number()
|
|
161
|
-
.optional()
|
|
162
|
-
.describe("Height of the Rectangle vertex cell")
|
|
163
|
-
.default(100),
|
|
164
|
-
text: z
|
|
165
|
-
.string()
|
|
166
|
-
.optional()
|
|
167
|
-
.describe("Text content placed inside of the Rectangle vertex cell")
|
|
168
|
-
.default("New Cell"),
|
|
169
|
-
style: z
|
|
170
|
-
.string()
|
|
171
|
-
.optional()
|
|
172
|
-
.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;`")
|
|
173
|
-
.default("whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;"),
|
|
174
|
-
}, default_tool(TOOL_add_rectangle, context));
|
|
175
|
-
const TOOL_add_edge = "add-edge";
|
|
176
|
-
server.tool(TOOL_add_edge, "This tool creates an edge, sometimes called also a relation, between two vertexes (cells).", {
|
|
177
|
-
source_id: z
|
|
178
|
-
.string()
|
|
179
|
-
.describe("Source ID of a cell. It is represented by `id` attribute."),
|
|
180
|
-
target_id: z
|
|
181
|
-
.string()
|
|
182
|
-
.describe("Target ID of a cell. It is represented by `id` attribute."),
|
|
183
|
-
text: z
|
|
184
|
-
.string()
|
|
185
|
-
.optional()
|
|
186
|
-
.describe("Text content placed over the edge cell"),
|
|
187
|
-
style: z
|
|
188
|
-
.string()
|
|
189
|
-
.optional()
|
|
190
|
-
.describe("Semi-colon separated list of Draw.io visual styles, in the form of `key=value`. Example: `edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;`")
|
|
191
|
-
.default("edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;"),
|
|
192
|
-
}, default_tool(TOOL_add_edge, context));
|
|
193
|
-
const TOOL_delete_cell_by_id = "delete-cell-by-id";
|
|
194
|
-
server.tool(TOOL_delete_cell_by_id, "Deletes a cell, whether it is a vertex or edge.", {
|
|
195
|
-
cell_id: z
|
|
196
|
-
.string()
|
|
197
|
-
.describe("The ID of a cell to delete. The cell can be either vertex or edge. The ID is located in `id` attribute."),
|
|
198
|
-
}, default_tool(TOOL_delete_cell_by_id, context));
|
|
199
|
-
const TOOL_get_shape_categories = "get-shape-categories";
|
|
200
|
-
server.tool(TOOL_get_shape_categories, "Retrieves available shape categories from the diagram's library. Library is split into multiple categories.", {}, default_tool(TOOL_get_shape_categories, context));
|
|
201
|
-
const TOOL_get_shapes_in_category = "get-shapes-in-category";
|
|
202
|
-
server.tool(TOOL_get_shapes_in_category, "Retrieve all shapes in the provided category from the diagram's library. A shape primarily contains `style` based on which you can create new vertex cells.", {
|
|
203
|
-
category_id: z
|
|
204
|
-
.string()
|
|
205
|
-
.describe("Identifier (ID / key) of the category from which all the shapes should be retrieved."),
|
|
206
|
-
}, default_tool(TOOL_get_shapes_in_category, context));
|
|
207
|
-
const TOOL_get_shape_by_name = "get-shape-by-name";
|
|
208
|
-
server.tool(TOOL_get_shape_by_name, "Retrieve a specific shape by its name from all available shapes in the diagram's library. It returns the shape and also the category it belongs.", {
|
|
209
|
-
shape_name: z
|
|
210
|
-
.string()
|
|
211
|
-
.describe("Name of the shape to retrieve from the shape library of the current diagram."),
|
|
212
|
-
}, default_tool(TOOL_get_shape_by_name, context));
|
|
213
|
-
const TOOL_add_cell_of_shape = "add-cell-of-shape";
|
|
214
|
-
server.tool(TOOL_add_cell_of_shape, "This tool allows you to add new vertex cell (object) on the current page of a Draw.io diagram by its shape name. It accepts multiple optional input parameter.", {
|
|
215
|
-
shape_name: z
|
|
216
|
-
.string()
|
|
217
|
-
.describe("Name of the shape to retrieved from the shape library of the current diagram."),
|
|
218
|
-
x: z
|
|
219
|
-
.number()
|
|
220
|
-
.optional()
|
|
221
|
-
.describe("X-axis position of the vertex cell of the shape")
|
|
222
|
-
.default(100),
|
|
223
|
-
y: z
|
|
224
|
-
.number()
|
|
225
|
-
.optional()
|
|
226
|
-
.describe("Y-axis position of the vertex cell of the shape")
|
|
227
|
-
.default(100),
|
|
228
|
-
width: z
|
|
229
|
-
.number()
|
|
230
|
-
.optional()
|
|
231
|
-
.describe("Width of the vertex cell of the shape")
|
|
232
|
-
.default(200),
|
|
233
|
-
height: z
|
|
234
|
-
.number()
|
|
235
|
-
.optional()
|
|
236
|
-
.describe("Height of the vertex cell of the shape")
|
|
237
|
-
.default(100),
|
|
238
|
-
text: z
|
|
239
|
-
.string()
|
|
240
|
-
.optional()
|
|
241
|
-
.describe("Text content placed inside of the vertex cell of the shape"),
|
|
242
|
-
style: z
|
|
243
|
-
.string()
|
|
244
|
-
.optional()
|
|
245
|
-
.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;`"),
|
|
246
|
-
}, default_tool(TOOL_add_cell_of_shape, context));
|
|
247
|
-
const TOOL_set_cell_shape = "set-cell-shape";
|
|
248
|
-
server.tool(TOOL_set_cell_shape, "Updates the visual style of an existing vertex cell to match a library shape by name.", {
|
|
249
|
-
cell_id: z
|
|
250
|
-
.string()
|
|
251
|
-
.describe("Identifier (`id` attribute) of the cell whose shape should change."),
|
|
252
|
-
shape_name: z
|
|
253
|
-
.string()
|
|
254
|
-
.describe("Name of the library shape whose style should be applied to the existing cell."),
|
|
255
|
-
}, default_tool(TOOL_set_cell_shape, context));
|
|
256
|
-
const TOOL_set_cell_data = "set-cell-data";
|
|
257
|
-
server.tool(TOOL_set_cell_data, "Sets or updates a custom attribute on an existing cell.", {
|
|
258
|
-
cell_id: z
|
|
259
|
-
.string()
|
|
260
|
-
.describe("Identifier (`id` attribute) of the cell to update with custom data."),
|
|
261
|
-
key: z.string().describe("Name of the attribute to set on the cell."),
|
|
262
|
-
value: z
|
|
263
|
-
.union([z.string(), z.number(), z.boolean()])
|
|
264
|
-
.describe("Value to store for the attribute. Non-string values are stringified before storage."),
|
|
265
|
-
}, default_tool(TOOL_set_cell_data, context));
|
|
266
|
-
const TOOL_edit_cell = "edit-cell";
|
|
267
|
-
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.", {
|
|
268
|
-
cell_id: z
|
|
269
|
-
.string()
|
|
270
|
-
.describe("Identifier (`id` attribute) of the cell to update. Applies to vertex/shape cells."),
|
|
271
|
-
text: z
|
|
272
|
-
.string()
|
|
273
|
-
.optional()
|
|
274
|
-
.describe("Replace the cell's text/label content."),
|
|
275
|
-
x: z
|
|
276
|
-
.number()
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("Set a new X-axis position for the cell."),
|
|
279
|
-
y: z
|
|
280
|
-
.number()
|
|
281
|
-
.optional()
|
|
282
|
-
.describe("Set a new Y-axis position for the cell."),
|
|
283
|
-
width: z.number().optional().describe("Set a new width for the cell."),
|
|
284
|
-
height: z.number().optional().describe("Set a new height for the cell."),
|
|
285
|
-
style: z
|
|
286
|
-
.string()
|
|
287
|
-
.optional()
|
|
288
|
-
.describe("Replace the cell's style string (semi-colon separated `key=value` pairs)."),
|
|
289
|
-
}, default_tool(TOOL_edit_cell, context));
|
|
290
|
-
const TOOL_edit_edge = "edit-edge";
|
|
291
|
-
server.tool(TOOL_edit_edge, "Update properties of an existing edge by its ID. Only provided fields are modified; unspecified properties remain unchanged.", {
|
|
292
|
-
cell_id: z
|
|
293
|
-
.string()
|
|
294
|
-
.describe("Identifier (`id` attribute) of the edge cell to update. The ID must reference an edge."),
|
|
295
|
-
text: z.string().optional().describe("Replace the edge's label text."),
|
|
296
|
-
source_id: z
|
|
297
|
-
.string()
|
|
298
|
-
.optional()
|
|
299
|
-
.describe("Reassign the edge's source terminal to a different cell ID."),
|
|
300
|
-
target_id: z
|
|
301
|
-
.string()
|
|
302
|
-
.optional()
|
|
303
|
-
.describe("Reassign the edge's target terminal to a different cell ID."),
|
|
304
|
-
style: z
|
|
305
|
-
.string()
|
|
306
|
-
.optional()
|
|
307
|
-
.describe("Replace the edge's style string (semi-colon separated `key=value` pairs)."),
|
|
308
|
-
}, default_tool(TOOL_edit_edge, context));
|
|
309
|
-
const Attributes = z.lazy(() => z
|
|
310
|
-
.array(z.union([
|
|
311
|
-
z.string(),
|
|
312
|
-
Attributes, // recursion: nested arrays
|
|
313
|
-
]))
|
|
314
|
-
.refine((arr) => arr.length === 0 || typeof arr[0] === "string", {
|
|
315
|
-
message: "If not empty, the first element must be a string operator",
|
|
316
|
-
})
|
|
317
|
-
.default([]));
|
|
318
|
-
const TOOL_list_paged_model = "list-paged-model";
|
|
319
|
-
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.", {
|
|
320
|
-
page: z
|
|
321
|
-
.number()
|
|
322
|
-
.optional()
|
|
323
|
-
.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.")
|
|
324
|
-
.default(0),
|
|
325
|
-
page_size: z
|
|
326
|
-
.number()
|
|
327
|
-
.optional()
|
|
328
|
-
.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.")
|
|
329
|
-
.default(50),
|
|
330
|
-
filter: z
|
|
331
|
-
.object({
|
|
332
|
-
cell_type: z
|
|
333
|
-
.enum(["edge", "vertex", "object", "layer", "group"])
|
|
334
|
-
.optional()
|
|
335
|
-
.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"),
|
|
336
|
-
parent_ids: z
|
|
337
|
-
.array(z.string())
|
|
338
|
-
.optional()
|
|
339
|
-
.describe("Filter cells to only those whose parent is one of the specified parent IDs."),
|
|
340
|
-
layer_ids: z
|
|
341
|
-
.array(z.string())
|
|
342
|
-
.optional()
|
|
343
|
-
.describe("Filter cells to only those whose parent is one of the specified layer IDs. Alias for parent_ids."),
|
|
344
|
-
ids: z
|
|
345
|
-
.array(z.string())
|
|
346
|
-
.optional()
|
|
347
|
-
.describe("Filter cells to only those whose ID is one of the specified IDs."),
|
|
348
|
-
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.'),
|
|
349
|
-
})
|
|
350
|
-
.optional()
|
|
351
|
-
.describe("Optional filter criteria to apply to cells before pagination")
|
|
352
|
-
.default({}),
|
|
353
|
-
}, default_tool(TOOL_list_paged_model, context));
|
|
354
|
-
const TOOL_list_layers = "list-layers";
|
|
355
|
-
server.tool(TOOL_list_layers, "Lists all available layers in the diagram with their IDs and names.", {}, default_tool(TOOL_list_layers, context));
|
|
356
|
-
const TOOL_set_active_layer = "set-active-layer";
|
|
357
|
-
server.tool(TOOL_set_active_layer, "Sets the active layer for creating new elements. All subsequent element creation will happen in this layer.", {
|
|
358
|
-
layer_id: z.string().describe("ID of the layer to set as active"),
|
|
359
|
-
}, default_tool(TOOL_set_active_layer, context));
|
|
360
|
-
const TOOL_move_cell_to_layer = "move-cell-to-layer";
|
|
361
|
-
server.tool(TOOL_move_cell_to_layer, "Moves a cell from its current layer to a target layer.", {
|
|
362
|
-
cell_id: z.string().describe("ID of the cell to move"),
|
|
363
|
-
target_layer_id: z
|
|
364
|
-
.string()
|
|
365
|
-
.describe("ID of the target layer where the cell will be moved"),
|
|
366
|
-
}, default_tool(TOOL_move_cell_to_layer, context));
|
|
367
|
-
const TOOL_get_active_layer = "get-active-layer";
|
|
368
|
-
server.tool(TOOL_get_active_layer, "Gets the currently active layer information.", {}, default_tool(TOOL_get_active_layer, context));
|
|
369
|
-
const TOOL_create_layer = "create-layer";
|
|
370
|
-
server.tool(TOOL_create_layer, "Creates a new layer in the diagram.", {
|
|
371
|
-
name: z.string().describe("Name for the new layer"),
|
|
372
|
-
}, default_tool(TOOL_create_layer, context));
|
|
373
|
-
async function start_stdio_transport() {
|
|
75
|
+
async function start_stdio_transport(server, log) {
|
|
374
76
|
const transport = new StdioServerTransport();
|
|
375
77
|
await server.connect(transport);
|
|
376
78
|
log.debug(`Draw.io MCP Server STDIO transport active`);
|
|
377
79
|
}
|
|
378
|
-
|
|
379
|
-
// Create a stateless transport (no options = no session management)
|
|
380
|
-
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
381
|
-
// Create the Hono app
|
|
382
|
-
const app = new Hono();
|
|
383
|
-
// Enable CORS for all origins
|
|
80
|
+
function setupCors(app) {
|
|
384
81
|
app.use("*", cors({
|
|
385
82
|
origin: "*",
|
|
386
83
|
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
@@ -392,16 +89,272 @@ async function start_streamable_http_transport(http_port) {
|
|
|
392
89
|
],
|
|
393
90
|
exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
|
|
394
91
|
}));
|
|
395
|
-
|
|
92
|
+
}
|
|
93
|
+
function registerHealthRoute(app) {
|
|
94
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
95
|
+
}
|
|
96
|
+
function registerConfigRoute(app, config) {
|
|
97
|
+
app.get("/api/config", (c) => c.json({
|
|
98
|
+
websocketPort: config.extensionPort,
|
|
99
|
+
serverUrl: `http://localhost:${config.httpPort}`,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function registerEditorRoutes(app, config, log) {
|
|
103
|
+
const assetConfig = {
|
|
104
|
+
assetPath: config.assetPath,
|
|
105
|
+
};
|
|
106
|
+
const isLocal = isUsingLocalAssets(assetConfig);
|
|
107
|
+
const assetRoot = isLocal ? getAssetRoot(assetConfig) : null;
|
|
108
|
+
const localPluginPath = getLocalPluginPath();
|
|
109
|
+
app.get("/js/mcp-plugin.js", (c) => {
|
|
110
|
+
if (existsSync(localPluginPath)) {
|
|
111
|
+
const content = readFileSync(localPluginPath);
|
|
112
|
+
c.header("Content-Type", "application/javascript");
|
|
113
|
+
return c.body(content);
|
|
114
|
+
}
|
|
115
|
+
return c.text("Plugin not found", 404);
|
|
116
|
+
});
|
|
117
|
+
app.use("/*", async (c, next) => {
|
|
118
|
+
let path = c.req.path;
|
|
119
|
+
if (path === "" || path === "/") {
|
|
120
|
+
path = "index.html";
|
|
121
|
+
}
|
|
122
|
+
if (!isLocal || !assetRoot) {
|
|
123
|
+
await next();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let filePath = join(assetRoot, path);
|
|
127
|
+
if (existsSync(filePath)) {
|
|
128
|
+
const fileStats = statSync(filePath);
|
|
129
|
+
if (fileStats.isDirectory()) {
|
|
130
|
+
const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
131
|
+
const parentDir = join(assetRoot, normalizedPath);
|
|
132
|
+
if (existsSync(parentDir) && statSync(parentDir).isDirectory()) {
|
|
133
|
+
const entries = readdirSync(parentDir);
|
|
134
|
+
const firstFile = entries.find((e) => !statSync(join(parentDir, e)).isDirectory());
|
|
135
|
+
if (firstFile) {
|
|
136
|
+
filePath = join(parentDir, firstFile);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await next();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await next();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let content = readFileSync(filePath);
|
|
149
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
150
|
+
if (ext === "html") {
|
|
151
|
+
const contentStr = content.toString("utf-8");
|
|
152
|
+
if (contentStr.includes("</body>") &&
|
|
153
|
+
!contentStr.includes("mcp-plugin.js")) {
|
|
154
|
+
const pluginScript = `<script src="/js/mcp-plugin.js"></script>`;
|
|
155
|
+
content = Buffer.from(contentStr.replace("</body>", `${pluginScript}</body>`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const contentTypes = {
|
|
159
|
+
html: "text/html",
|
|
160
|
+
css: "text/css",
|
|
161
|
+
js: "application/javascript",
|
|
162
|
+
json: "application/json",
|
|
163
|
+
svg: "image/svg+xml",
|
|
164
|
+
png: "image/png",
|
|
165
|
+
ico: "image/x-icon",
|
|
166
|
+
map: "application/json",
|
|
167
|
+
};
|
|
168
|
+
c.header("Content-Type", contentTypes[ext || ""] || "text/plain");
|
|
169
|
+
return c.body(content);
|
|
170
|
+
}
|
|
171
|
+
await next();
|
|
172
|
+
});
|
|
173
|
+
app.get("/", (c) => {
|
|
174
|
+
return c.redirect("/index.html?offline=1&local=1");
|
|
175
|
+
});
|
|
176
|
+
log.debug(`Draw.io editor enabled at: http://localhost:${config.httpPort}/`);
|
|
177
|
+
}
|
|
178
|
+
function registerMcpRoute(app) {
|
|
179
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
396
180
|
app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
|
|
397
|
-
|
|
398
|
-
|
|
181
|
+
return transport;
|
|
182
|
+
}
|
|
183
|
+
function createHttpApp(log, config, features) {
|
|
184
|
+
const app = new Hono();
|
|
185
|
+
setupCors(app);
|
|
186
|
+
if (features.enableHealth)
|
|
187
|
+
registerHealthRoute(app);
|
|
188
|
+
if (features.enableConfig)
|
|
189
|
+
registerConfigRoute(app, config);
|
|
190
|
+
const mcpTransport = features.enableMcp ? registerMcpRoute(app) : undefined;
|
|
191
|
+
if (features.enableEditor)
|
|
192
|
+
registerEditorRoutes(app, config, log);
|
|
193
|
+
return { app, mcpTransport };
|
|
194
|
+
}
|
|
195
|
+
async function startHttpServer(server, log, httpPort, config, features) {
|
|
196
|
+
const { app, mcpTransport } = createHttpApp(log, config, features);
|
|
197
|
+
if (mcpTransport) {
|
|
198
|
+
await server.connect(mcpTransport);
|
|
199
|
+
}
|
|
200
|
+
const httpServer = serve({
|
|
399
201
|
fetch: app.fetch,
|
|
400
|
-
port:
|
|
202
|
+
port: httpPort,
|
|
203
|
+
});
|
|
204
|
+
const listeningPort = httpPort === 0
|
|
205
|
+
? (httpServer.address()?.port ?? httpPort)
|
|
206
|
+
: httpPort;
|
|
207
|
+
log.debug(`Draw.io MCP Server HTTP active on port ${listeningPort}`);
|
|
208
|
+
if (features.enableMcp) {
|
|
209
|
+
log.debug(`MCP endpoint: http://localhost:${listeningPort}/mcp`);
|
|
210
|
+
}
|
|
211
|
+
if (features.enableEditor) {
|
|
212
|
+
log.debug(`Editor: http://localhost:${listeningPort}/`);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
server: httpServer,
|
|
216
|
+
port: listeningPort,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export function createDrawioMcpApp(overrides) {
|
|
220
|
+
const server = new McpServer({
|
|
221
|
+
name: "drawio-mcp-server",
|
|
222
|
+
version: VERSION,
|
|
223
|
+
}, {
|
|
224
|
+
capabilities,
|
|
401
225
|
});
|
|
402
|
-
log
|
|
403
|
-
|
|
404
|
-
|
|
226
|
+
const log = overrides?.log ??
|
|
227
|
+
(logger_type === "mcp_server"
|
|
228
|
+
? create_server_logger(server)
|
|
229
|
+
: create_console_logger());
|
|
230
|
+
const emitter = new EventEmitter();
|
|
231
|
+
const conns = new Set();
|
|
232
|
+
const bus = create_bus(log)(emitter);
|
|
233
|
+
const id_generator = nanoid_id_generator();
|
|
234
|
+
const context = {
|
|
235
|
+
bus,
|
|
236
|
+
id_generator,
|
|
237
|
+
log,
|
|
238
|
+
};
|
|
239
|
+
registerTools(server, context);
|
|
240
|
+
const bus_to_ws_forwarder_listener = (event) => {
|
|
241
|
+
log.debug(`[bridge] received; forwarding message to #${conns.size} clients`, event);
|
|
242
|
+
for (const ws of [...conns]) {
|
|
243
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
244
|
+
conns.delete(ws);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
ws.send(JSON.stringify(event));
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
log.debug("[bridge] error forwarding request", e);
|
|
252
|
+
conns.delete(ws);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
emitter.on(bus_request_stream, bus_to_ws_forwarder_listener);
|
|
257
|
+
let wsServer;
|
|
258
|
+
let httpServer;
|
|
259
|
+
async function startWebSocketServer(extensionPort = 3333) {
|
|
260
|
+
log.debug(`Draw.io MCP Server (${VERSION}) starting (WebSocket extension port: ${extensionPort})`);
|
|
261
|
+
if (extensionPort !== 0) {
|
|
262
|
+
const isPortAvailable = await checkPortAvailable(extensionPort);
|
|
263
|
+
if (!isPortAvailable) {
|
|
264
|
+
throw new Error(`[start_websocket_server] Error: Port ${extensionPort} is already in use. Please stop the process using this port and try again.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
wsServer = new WebSocketServer({ port: extensionPort });
|
|
268
|
+
wsServer.on("connection", (ws) => {
|
|
269
|
+
log.debug(`[ws_handler] A WebSocket client #${conns.size} connected, presumably MCP Extension!`);
|
|
270
|
+
conns.add(ws);
|
|
271
|
+
ws.on("message", (data) => {
|
|
272
|
+
const str = typeof data === "string" ? data : data.toString();
|
|
273
|
+
try {
|
|
274
|
+
const json = JSON.parse(str);
|
|
275
|
+
log.debug(`[ws] received from Extension`, json);
|
|
276
|
+
emitter.emit(bus_reply_stream, json);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
log.debug(`[ws] failed to parse message`, error);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
ws.on("close", (code) => {
|
|
283
|
+
conns.delete(ws);
|
|
284
|
+
log.debug(`[ws_handler] WebSocket client closed with code ${code}`);
|
|
285
|
+
});
|
|
286
|
+
ws.on("error", (error) => {
|
|
287
|
+
log.debug(`[ws_handler] WebSocket client error`, error);
|
|
288
|
+
conns.delete(ws);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
const onListening = () => {
|
|
293
|
+
wsServer?.off("error", onError);
|
|
294
|
+
const address = wsServer?.address();
|
|
295
|
+
log.debug(`[start_websocket_server] Listening to port ${address?.port ?? extensionPort}`);
|
|
296
|
+
resolve();
|
|
297
|
+
};
|
|
298
|
+
const onError = (error) => {
|
|
299
|
+
wsServer?.off("listening", onListening);
|
|
300
|
+
reject(error);
|
|
301
|
+
};
|
|
302
|
+
wsServer?.once("listening", onListening);
|
|
303
|
+
wsServer?.once("error", onError);
|
|
304
|
+
});
|
|
305
|
+
return wsServer;
|
|
306
|
+
}
|
|
307
|
+
async function close() {
|
|
308
|
+
emitter.off(bus_request_stream, bus_to_ws_forwarder_listener);
|
|
309
|
+
for (const ws of [...conns]) {
|
|
310
|
+
try {
|
|
311
|
+
ws.close();
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// ignore
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
conns.clear();
|
|
318
|
+
await server.close();
|
|
319
|
+
if (wsServer) {
|
|
320
|
+
await new Promise((resolve, reject) => {
|
|
321
|
+
wsServer?.close((error) => {
|
|
322
|
+
if (error) {
|
|
323
|
+
reject(error);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
resolve();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
wsServer = undefined;
|
|
330
|
+
}
|
|
331
|
+
if (httpServer) {
|
|
332
|
+
await new Promise((resolve, reject) => {
|
|
333
|
+
httpServer?.close((error) => {
|
|
334
|
+
if (error) {
|
|
335
|
+
reject(error);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
resolve();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
httpServer = undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
server,
|
|
346
|
+
log,
|
|
347
|
+
context,
|
|
348
|
+
emitter,
|
|
349
|
+
close,
|
|
350
|
+
startWebSocketServer,
|
|
351
|
+
startStdioTransport: () => start_stdio_transport(server, log),
|
|
352
|
+
startHttpServer: async (httpPort, config, features) => {
|
|
353
|
+
const started = await startHttpServer(server, log, httpPort, config, features);
|
|
354
|
+
httpServer = started.server;
|
|
355
|
+
return started;
|
|
356
|
+
},
|
|
357
|
+
};
|
|
405
358
|
}
|
|
406
359
|
async function main() {
|
|
407
360
|
// Check if help was requested (before parsing config)
|
|
@@ -417,16 +370,31 @@ async function main() {
|
|
|
417
370
|
process.exit(1);
|
|
418
371
|
}
|
|
419
372
|
const config = configResult;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
373
|
+
const features = getHttpFeatureConfig(config);
|
|
374
|
+
// Initialize assets if needed
|
|
375
|
+
if (features.enableEditor) {
|
|
376
|
+
console.log("Initializing draw.io assets...");
|
|
377
|
+
const assetConfig = {
|
|
378
|
+
assetPath: config.assetPath,
|
|
379
|
+
};
|
|
380
|
+
await ensureAssets(assetConfig, (msg) => console.log(msg));
|
|
381
|
+
console.log("Assets ready!");
|
|
423
382
|
}
|
|
424
|
-
|
|
425
|
-
|
|
383
|
+
const app = createDrawioMcpApp();
|
|
384
|
+
await app.startWebSocketServer(config.extensionPort);
|
|
385
|
+
if (config.transports.indexOf("stdio") > -1) {
|
|
386
|
+
await app.startStdioTransport();
|
|
426
387
|
}
|
|
427
|
-
|
|
388
|
+
await app.startHttpServer(config.httpPort, config, features);
|
|
389
|
+
app.log.debug(`Draw.io MCP Server running on ${config.transports}`);
|
|
390
|
+
}
|
|
391
|
+
const isMainModule = process.argv[1]
|
|
392
|
+
? realpathSync(fileURLToPath(import.meta.url)) ===
|
|
393
|
+
realpathSync(process.argv[1])
|
|
394
|
+
: false;
|
|
395
|
+
if (isMainModule) {
|
|
396
|
+
main().catch((error) => {
|
|
397
|
+
fatalLog.debug("Fatal error in main():", error);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
});
|
|
428
400
|
}
|
|
429
|
-
main().catch((error) => {
|
|
430
|
-
log.debug("Fatal error in main():", error);
|
|
431
|
-
process.exit(1);
|
|
432
|
-
});
|