drawio-mcp-server 1.7.0 → 1.8.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.
@@ -0,0 +1,96 @@
1
+ import { createWriteStream, existsSync, mkdirSync, createReadStream, rmSync, } from "node:fs";
2
+ import { pipeline } from "node:stream/promises";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Extract } from "unzipper";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const DRAWIO_GITHUB_API = "https://api.github.com/repos/jgraph/drawio/releases/latest";
8
+ export async function getLatestWarUrl() {
9
+ const response = await fetch(DRAWIO_GITHUB_API);
10
+ if (!response.ok) {
11
+ throw new Error(`Failed to get draw.io release info: ${response.status}`);
12
+ }
13
+ const data = await response.json();
14
+ const warAsset = data.assets?.find((asset) => asset.name === "draw.war");
15
+ if (!warAsset) {
16
+ throw new Error("Could not find draw.war in latest release");
17
+ }
18
+ return warAsset.browser_download_url;
19
+ }
20
+ export async function downloadFile(url, destPath) {
21
+ const response = await fetch(url);
22
+ if (!response.ok) {
23
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
24
+ }
25
+ if (!response.body) {
26
+ throw new Error("No response body");
27
+ }
28
+ const destDir = dirname(destPath);
29
+ if (!existsSync(destDir)) {
30
+ mkdirSync(destDir, { recursive: true });
31
+ }
32
+ await pipeline(response.body, createWriteStream(destPath));
33
+ }
34
+ export async function extractWar(warPath, extractDir) {
35
+ if (!existsSync(extractDir)) {
36
+ mkdirSync(extractDir, { recursive: true });
37
+ }
38
+ return new Promise((resolve, reject) => {
39
+ const extract = Extract({ path: extractDir });
40
+ const stream = createReadStream(warPath);
41
+ stream.pipe(extract);
42
+ extract.on("close", resolve);
43
+ extract.on("error", reject);
44
+ });
45
+ }
46
+ export function cleanupExtractedFiles(extractDir) {
47
+ const webappDir = join(extractDir, "webapp");
48
+ const pathsToRemove = [
49
+ join(webappDir, "WEB-INF"),
50
+ join(webappDir, "META-INF"),
51
+ ];
52
+ for (const path of pathsToRemove) {
53
+ if (existsSync(path)) {
54
+ try {
55
+ rmSync(path, { recursive: true, force: true });
56
+ console.log(`Removed: ${path}`);
57
+ }
58
+ catch (err) {
59
+ console.warn(`Failed to remove ${path}:`, err);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ export async function downloadAndExtractAssets(targetDir, onProgress) {
65
+ const progress = onProgress || console.log;
66
+ progress("Fetching draw.io release info...");
67
+ const warUrl = await getLatestWarUrl();
68
+ const warPath = join(targetDir, "draw.war");
69
+ progress(`Downloading draw.war from ${warUrl}...`);
70
+ await downloadFile(warUrl, warPath);
71
+ progress("Download complete.");
72
+ const webappDir = join(targetDir, "webapp");
73
+ progress("Extracting archive...");
74
+ await extractWar(warPath, webappDir);
75
+ progress("Extraction complete.");
76
+ progress("Cleaning up unnecessary files...");
77
+ cleanupExtractedFiles(targetDir);
78
+ // Remove the WAR file
79
+ try {
80
+ rmSync(warPath, { force: true });
81
+ }
82
+ catch (err) {
83
+ console.warn("Failed to remove WAR file:", err);
84
+ }
85
+ progress("Assets ready!");
86
+ }
87
+ export async function ensureAssets(config, onProgress) {
88
+ const { getCacheDir, getAssetRoot, assetsExist } = await import("./manager.js");
89
+ const cacheDir = getCacheDir(config.assetPath);
90
+ const assetRoot = getAssetRoot(config);
91
+ if (!assetsExist(config)) {
92
+ console.log(`Assets not found in ${assetRoot}. Downloading...`);
93
+ await downloadAndExtractAssets(cacheDir, onProgress);
94
+ }
95
+ return { assetRoot, isLocal: true };
96
+ }
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_CACHE_NAME, getCacheDir, getAssetRoot, assetsExist, getLocalPluginPath, isUsingLocalAssets, } from "./manager.js";
2
+ export { getLatestWarUrl, downloadFile, extractWar, cleanupExtractedFiles, downloadAndExtractAssets, ensureAssets, } from "./downloader.js";
@@ -0,0 +1,31 @@
1
+ import cachedir from "cachedir";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync } from "node:fs";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export const DEFAULT_CACHE_NAME = "drawio-mcp-server";
7
+ export function getCacheDir(customPath) {
8
+ if (customPath) {
9
+ return customPath;
10
+ }
11
+ const dir = cachedir(DEFAULT_CACHE_NAME);
12
+ if (!dir) {
13
+ throw new Error("Could not determine cache directory. Use --asset-path to specify one.");
14
+ }
15
+ return dir;
16
+ }
17
+ export function getAssetRoot(config) {
18
+ const cacheDir = getCacheDir(config.assetPath);
19
+ return join(cacheDir, "webapp");
20
+ }
21
+ export function assetsExist(config) {
22
+ const assetRoot = getAssetRoot(config);
23
+ const indexPath = join(assetRoot, "index.html");
24
+ return existsSync(indexPath);
25
+ }
26
+ export function getLocalPluginPath() {
27
+ return join(__dirname, "..", "..", "build", "plugin", "mcp-plugin.js");
28
+ }
29
+ export function isUsingLocalAssets(config) {
30
+ return assetsExist(config);
31
+ }
package/build/config.js CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Application configuration interface
3
+ */
1
4
  /**
2
5
  * Default configuration values
3
6
  */
@@ -5,6 +8,7 @@ const DEFAULT_CONFIG = {
5
8
  extensionPort: 3333,
6
9
  httpPort: 3000,
7
10
  transports: ["stdio"],
11
+ editorEnabled: false,
8
12
  };
9
13
  /**
10
14
  * Valid port range
@@ -97,6 +101,8 @@ export const parseConfig = (args) => {
97
101
  let httpPortValue;
98
102
  let parsedHttpPort;
99
103
  let transportValues;
104
+ let editorEnabled = false;
105
+ let assetPath;
100
106
  for (let i = 0; i < args.length; i += 1) {
101
107
  const arg = args[i];
102
108
  if (arg === "--extension-port" || arg === "-p") {
@@ -123,6 +129,31 @@ export const parseConfig = (args) => {
123
129
  transportValues = [nextValue];
124
130
  i += 1;
125
131
  }
132
+ else if (arg === "--editor" || arg === "-e") {
133
+ const nextValue = args[i + 1];
134
+ if (nextValue !== undefined &&
135
+ (nextValue === "false" || nextValue === "true")) {
136
+ editorEnabled = nextValue === "true";
137
+ i += 1;
138
+ }
139
+ else {
140
+ editorEnabled = true;
141
+ }
142
+ }
143
+ else if (arg === "--editor=true") {
144
+ editorEnabled = true;
145
+ }
146
+ else if (arg === "--editor=false") {
147
+ editorEnabled = false;
148
+ }
149
+ else if (arg === "--asset-path") {
150
+ const nextValue = args[i + 1];
151
+ if (nextValue === undefined) {
152
+ return new Error("--asset-path flag requires a path");
153
+ }
154
+ assetPath = nextValue;
155
+ i += 1;
156
+ }
126
157
  }
127
158
  if (httpPortValue !== undefined) {
128
159
  const httpPort = parseHttpPortValue(httpPortValue);
@@ -145,6 +176,8 @@ export const parseConfig = (args) => {
145
176
  extensionPort,
146
177
  httpPort: parsedHttpPort !== undefined ? parsedHttpPort : DEFAULT_CONFIG.httpPort,
147
178
  transports,
179
+ editorEnabled,
180
+ assetPath,
148
181
  };
149
182
  }
150
183
  if (httpPortValue !== undefined) {
@@ -156,6 +189,8 @@ export const parseConfig = (args) => {
156
189
  ...DEFAULT_CONFIG,
157
190
  httpPort: parsedHttpPort,
158
191
  transports,
192
+ editorEnabled,
193
+ assetPath,
159
194
  };
160
195
  }
161
196
  const transports = parseTransports(transportValues);
@@ -166,6 +201,8 @@ export const parseConfig = (args) => {
166
201
  return {
167
202
  ...DEFAULT_CONFIG,
168
203
  transports,
204
+ editorEnabled,
205
+ assetPath,
169
206
  };
170
207
  };
171
208
  /**
@@ -177,3 +214,11 @@ export const buildConfig = () => {
177
214
  const args = process.argv.slice(2);
178
215
  return parseConfig(args);
179
216
  };
217
+ export function getHttpFeatureConfig(config) {
218
+ return {
219
+ enableMcp: config.transports.includes("http"),
220
+ enableEditor: config.editorEnabled,
221
+ enableHealth: true,
222
+ enableConfig: true,
223
+ };
224
+ }
@@ -114,6 +114,7 @@ describe("parseConfig", () => {
114
114
  extensionPort: 3333,
115
115
  httpPort: 3000,
116
116
  transports: ["stdio"],
117
+ editorEnabled: false,
117
118
  });
118
119
  });
119
120
  test("--extension-port flag sets custom port", () => {
@@ -121,6 +122,7 @@ describe("parseConfig", () => {
121
122
  extensionPort: 8080,
122
123
  httpPort: 3000,
123
124
  transports: ["stdio"],
125
+ editorEnabled: false,
124
126
  });
125
127
  });
126
128
  test("-p flag sets custom port", () => {
@@ -128,6 +130,7 @@ describe("parseConfig", () => {
128
130
  extensionPort: 8080,
129
131
  httpPort: 3000,
130
132
  transports: ["stdio"],
133
+ editorEnabled: false,
131
134
  });
132
135
  });
133
136
  test("--http-port flag sets custom port", () => {
@@ -135,6 +138,7 @@ describe("parseConfig", () => {
135
138
  extensionPort: 3333,
136
139
  httpPort: 4242,
137
140
  transports: ["stdio"],
141
+ editorEnabled: false,
138
142
  });
139
143
  });
140
144
  test("both ports can be configured", () => {
@@ -142,6 +146,7 @@ describe("parseConfig", () => {
142
146
  extensionPort: 8080,
143
147
  httpPort: 4242,
144
148
  transports: ["stdio"],
149
+ editorEnabled: false,
145
150
  });
146
151
  });
147
152
  test("help flag is ignored in config parsing", () => {
@@ -149,6 +154,7 @@ describe("parseConfig", () => {
149
154
  extensionPort: 3333,
150
155
  httpPort: 3000,
151
156
  transports: ["stdio"],
157
+ editorEnabled: false,
152
158
  });
153
159
  });
154
160
  test("invalid port returns Error", () => {
@@ -176,6 +182,7 @@ describe("parseConfig", () => {
176
182
  extensionPort: 9090,
177
183
  httpPort: 3000,
178
184
  transports: ["stdio"],
185
+ editorEnabled: false,
179
186
  });
180
187
  });
181
188
  test("short and long form both work, last wins", () => {
@@ -183,6 +190,7 @@ describe("parseConfig", () => {
183
190
  extensionPort: 9090,
184
191
  httpPort: 3000,
185
192
  transports: ["stdio"],
193
+ editorEnabled: false,
186
194
  });
187
195
  });
188
196
  test("last http-port flag wins", () => {
@@ -190,6 +198,7 @@ describe("parseConfig", () => {
190
198
  extensionPort: 3333,
191
199
  httpPort: 5000,
192
200
  transports: ["stdio"],
201
+ editorEnabled: false,
193
202
  });
194
203
  });
195
204
  test("sets single transport", () => {
@@ -197,6 +206,7 @@ describe("parseConfig", () => {
197
206
  extensionPort: 3333,
198
207
  httpPort: 3000,
199
208
  transports: ["stdio"],
209
+ editorEnabled: false,
200
210
  });
201
211
  });
202
212
  test("sets multiple transports", () => {
@@ -204,12 +214,53 @@ describe("parseConfig", () => {
204
214
  extensionPort: 3333,
205
215
  httpPort: 3000,
206
216
  transports: ["stdio", "http"],
217
+ editorEnabled: false,
207
218
  });
208
219
  });
209
220
  test("rejects unknown transport", () => {
210
221
  const result = parseConfig(["--transport", "foo"]);
211
222
  expect(result).toBeInstanceOf(Error);
212
223
  });
224
+ test("--editor flag enables editor", () => {
225
+ expect(parseConfig(["--editor"])).toEqual({
226
+ extensionPort: 3333,
227
+ httpPort: 3000,
228
+ transports: ["stdio"],
229
+ editorEnabled: true,
230
+ });
231
+ });
232
+ test("-e flag enables editor", () => {
233
+ expect(parseConfig(["-e"])).toEqual({
234
+ extensionPort: 3333,
235
+ httpPort: 3000,
236
+ transports: ["stdio"],
237
+ editorEnabled: true,
238
+ });
239
+ });
240
+ test("--editor false disables editor explicitly", () => {
241
+ expect(parseConfig(["--editor", "false"])).toEqual({
242
+ extensionPort: 3333,
243
+ httpPort: 3000,
244
+ transports: ["stdio"],
245
+ editorEnabled: false,
246
+ });
247
+ });
248
+ test("--editor=true enables editor", () => {
249
+ expect(parseConfig(["--editor=true"])).toEqual({
250
+ extensionPort: 3333,
251
+ httpPort: 3000,
252
+ transports: ["stdio"],
253
+ editorEnabled: true,
254
+ });
255
+ });
256
+ test("--editor=false disables editor explicitly", () => {
257
+ expect(parseConfig(["--editor=false"])).toEqual({
258
+ extensionPort: 3333,
259
+ httpPort: 3000,
260
+ transports: ["stdio"],
261
+ editorEnabled: false,
262
+ });
263
+ });
213
264
  });
214
265
  describe("buildConfig", () => {
215
266
  const originalArgv = process.argv;
@@ -223,6 +274,7 @@ describe("buildConfig", () => {
223
274
  extensionPort: 3333,
224
275
  httpPort: 3000,
225
276
  transports: ["stdio"],
277
+ editorEnabled: false,
226
278
  });
227
279
  });
228
280
  test("parses custom port from argv", () => {
@@ -232,6 +284,7 @@ describe("buildConfig", () => {
232
284
  extensionPort: 8080,
233
285
  httpPort: 3000,
234
286
  transports: ["stdio"],
287
+ editorEnabled: false,
235
288
  });
236
289
  });
237
290
  test("parses custom http port from argv", () => {
@@ -241,6 +294,7 @@ describe("buildConfig", () => {
241
294
  extensionPort: 3333,
242
295
  httpPort: 4242,
243
296
  transports: ["stdio"],
297
+ editorEnabled: false,
244
298
  });
245
299
  });
246
300
  test("returns Error for invalid config", () => {
package/build/index.js CHANGED
@@ -8,15 +8,18 @@ import { Hono } from "hono";
8
8
  import { cors } from "hono/cors";
9
9
  import EventEmitter from "node:events";
10
10
  import { createServer } from "node:net";
11
+ import { join } from "node:path";
12
+ import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
11
13
  import { WebSocket, WebSocketServer } from "ws";
12
- import { buildConfig, shouldShowHelp } from "./config.js";
14
+ import { buildConfig, shouldShowHelp, getHttpFeatureConfig, } from "./config.js";
13
15
  import { bus_reply_stream, bus_request_stream, } from "./types.js";
14
16
  import { create_bus } from "./emitter_bus.js";
15
17
  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
- const VERSION = "1.7.0";
21
+ import { getLocalPluginPath, isUsingLocalAssets, getAssetRoot, ensureAssets, } from "./assets/index.js";
22
+ const VERSION = "1.8.0";
20
23
  /**
21
24
  * Display help message and exit
22
25
  */
@@ -28,12 +31,19 @@ Usage: drawio-mcp-server [options]
28
31
 
29
32
  Options:
30
33
  --extension-port, -p <number> WebSocket server port for browser extension (default: 3333)
34
+ --editor, -e Enable draw.io editor endpoint
35
+ --http-port HTTP server port for Streamable HTTP transport (default: 3000)
36
+ --transport Transport type: stdio, http (default: stdio)
37
+ --asset-path <path> Custom path for downloaded assets
31
38
  --help, -h Show this help message
32
39
 
33
40
  Examples:
34
41
  drawio-mcp-server # Use default extension port 3333
35
42
  drawio-mcp-server --extension-port 8080 # Use custom extension port 8080
36
43
  drawio-mcp-server -p 8080 # Short form
44
+ drawio-mcp-server --editor # Enable draw.io editor endpoint
45
+ drawio-mcp-server -e --http # Enable editor and HTTP transport
46
+ drawio-mcp-server --editor --asset-path /data/assets # Use custom asset path
37
47
  `);
38
48
  process.exit(0);
39
49
  }
@@ -375,12 +385,7 @@ async function start_stdio_transport() {
375
385
  await server.connect(transport);
376
386
  log.debug(`Draw.io MCP Server STDIO transport active`);
377
387
  }
378
- async function start_streamable_http_transport(http_port) {
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
388
+ function setupCors(app) {
384
389
  app.use("*", cors({
385
390
  origin: "*",
386
391
  allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
@@ -392,16 +397,125 @@ async function start_streamable_http_transport(http_port) {
392
397
  ],
393
398
  exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
394
399
  }));
400
+ }
401
+ function registerHealthRoute(app) {
395
402
  app.get("/health", (c) => c.json({ status: server.isConnected() ? "ok" : "mcp not ready" }));
403
+ }
404
+ function registerConfigRoute(app, config) {
405
+ app.get("/api/config", (c) => c.json({
406
+ websocketPort: config.extensionPort,
407
+ serverUrl: `http://localhost:${config.httpPort}`,
408
+ }));
409
+ }
410
+ function registerEditorRoutes(app, config) {
411
+ const assetConfig = {
412
+ assetPath: config.assetPath,
413
+ };
414
+ const isLocal = isUsingLocalAssets(assetConfig);
415
+ const assetRoot = isLocal ? getAssetRoot(assetConfig) : null;
416
+ const localPluginPath = getLocalPluginPath();
417
+ app.get("/js/mcp-plugin.js", (c) => {
418
+ if (existsSync(localPluginPath)) {
419
+ const content = readFileSync(localPluginPath);
420
+ c.header("Content-Type", "application/javascript");
421
+ return c.body(content);
422
+ }
423
+ return c.text("Plugin not found", 404);
424
+ });
425
+ app.use("/*", async (c, next) => {
426
+ let path = c.req.path;
427
+ if (path === "" || path === "/") {
428
+ path = "index.html";
429
+ }
430
+ if (!isLocal || !assetRoot) {
431
+ await next();
432
+ return;
433
+ }
434
+ let filePath = join(assetRoot, path);
435
+ if (existsSync(filePath)) {
436
+ const fileStats = statSync(filePath);
437
+ if (fileStats.isDirectory()) {
438
+ const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
439
+ const parentDir = join(assetRoot, normalizedPath);
440
+ if (existsSync(parentDir) && statSync(parentDir).isDirectory()) {
441
+ const entries = readdirSync(parentDir);
442
+ const firstFile = entries.find((e) => !statSync(join(parentDir, e)).isDirectory());
443
+ if (firstFile) {
444
+ filePath = join(parentDir, firstFile);
445
+ }
446
+ else {
447
+ await next();
448
+ return;
449
+ }
450
+ }
451
+ else {
452
+ await next();
453
+ return;
454
+ }
455
+ }
456
+ let content = readFileSync(filePath);
457
+ const ext = filePath.split(".").pop()?.toLowerCase();
458
+ if (ext === "html") {
459
+ const contentStr = content.toString("utf-8");
460
+ if (contentStr.includes("</body>") &&
461
+ !contentStr.includes("mcp-plugin.js")) {
462
+ const pluginScript = `<script src="/js/mcp-plugin.js"></script>`;
463
+ content = Buffer.from(contentStr.replace("</body>", `${pluginScript}</body>`));
464
+ }
465
+ }
466
+ const contentTypes = {
467
+ html: "text/html",
468
+ css: "text/css",
469
+ js: "application/javascript",
470
+ json: "application/json",
471
+ svg: "image/svg+xml",
472
+ png: "image/png",
473
+ ico: "image/x-icon",
474
+ map: "application/json",
475
+ };
476
+ c.header("Content-Type", contentTypes[ext || ""] || "text/plain");
477
+ return c.body(content);
478
+ }
479
+ await next();
480
+ });
481
+ app.get("/", (c) => {
482
+ return c.redirect("/index.html?offline=1&local=1");
483
+ });
484
+ log.debug(`Draw.io editor enabled at: http://localhost:${config.httpPort}/`);
485
+ }
486
+ function registerMcpRoute(app) {
487
+ const transport = new WebStandardStreamableHTTPServerTransport();
396
488
  app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
397
- await server.connect(transport);
489
+ return transport;
490
+ }
491
+ function createHttpApp(config, features) {
492
+ const app = new Hono();
493
+ setupCors(app);
494
+ if (features.enableHealth)
495
+ registerHealthRoute(app);
496
+ if (features.enableConfig)
497
+ registerConfigRoute(app, config);
498
+ const mcpTransport = features.enableMcp ? registerMcpRoute(app) : undefined;
499
+ if (features.enableEditor)
500
+ registerEditorRoutes(app, config);
501
+ return { app, mcpTransport };
502
+ }
503
+ async function startHttpServer(httpPort, config, features) {
504
+ const { app, mcpTransport } = createHttpApp(config, features);
505
+ if (mcpTransport) {
506
+ await server.connect(mcpTransport);
507
+ }
398
508
  serve({
399
509
  fetch: app.fetch,
400
- port: http_port,
510
+ port: httpPort,
401
511
  });
402
- log.debug(`Draw.io MCP Server Streamable HTTP transport active`);
403
- log.debug(`Health check: http://localhost:${http_port}/health`);
404
- log.debug(`MCP endpoint: http://localhost:${http_port}/mcp`);
512
+ log.debug(`Draw.io MCP Server HTTP active on port ${httpPort}`);
513
+ if (features.enableMcp) {
514
+ log.debug(`MCP endpoint: http://localhost:${httpPort}/mcp`);
515
+ }
516
+ if (features.enableEditor) {
517
+ log.debug(`Editor: http://localhost:${httpPort}/`);
518
+ }
405
519
  }
406
520
  async function main() {
407
521
  // Check if help was requested (before parsing config)
@@ -417,13 +531,21 @@ async function main() {
417
531
  process.exit(1);
418
532
  }
419
533
  const config = configResult;
534
+ const features = getHttpFeatureConfig(config);
535
+ // Initialize assets if needed
536
+ if (features.enableEditor) {
537
+ console.log("Initializing draw.io assets...");
538
+ const assetConfig = {
539
+ assetPath: config.assetPath,
540
+ };
541
+ await ensureAssets(assetConfig, (msg) => console.log(msg));
542
+ console.log("Assets ready!");
543
+ }
420
544
  await start_websocket_server(config.extensionPort);
421
545
  if (config.transports.indexOf("stdio") > -1) {
422
546
  await start_stdio_transport();
423
547
  }
424
- if (config.transports.indexOf("http") > -1) {
425
- start_streamable_http_transport(config.httpPort);
426
- }
548
+ startHttpServer(config.httpPort, config, features);
427
549
  log.debug(`Draw.io MCP Server running on ${config.transports}`);
428
550
  }
429
551
  main().catch((error) => {