drawio-mcp-server 2.0.4 → 2.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.
Files changed (71) hide show
  1. package/README.md +29 -3
  2. package/build/assets/downloader.js +16 -17
  3. package/build/config.js +253 -5
  4. package/build/config.test.js +449 -1
  5. package/build/emitter_bus.js +3 -0
  6. package/build/emitter_bus.test.js +7 -0
  7. package/build/index.capabilities.test.js +45 -0
  8. package/build/index.js +416 -82
  9. package/build/install-desktop-plugin.js +56 -0
  10. package/build/install-desktop-plugin.test.js +66 -0
  11. package/build/multi-transport.test.js +117 -0
  12. package/build/plugin/mcp-plugin.js +2180 -1105
  13. package/build/prefetch-assets.js +6 -5
  14. package/build/real-environment/document-targeting.test.js +124 -0
  15. package/build/real-environment/export-diagram.test.js +1 -1
  16. package/build/real-environment/harness.js +118 -31
  17. package/build/real-environment/import-mermaid.test.js +81 -0
  18. package/build/real-environment/pages-and-concurrency.test.js +448 -0
  19. package/build/real-environment/shapes.test.js +58 -0
  20. package/build/real-environment/tools.js +51 -6
  21. package/build/register-tool.js +31 -0
  22. package/build/request_queue.js +29 -0
  23. package/build/request_queue.test.js +98 -0
  24. package/build/stdio-transport-purity.test.js +88 -0
  25. package/build/strip-schema.js +61 -0
  26. package/build/tls/expiry.js +14 -0
  27. package/build/tls/expiry.test.js +54 -0
  28. package/build/tls/generate.js +123 -0
  29. package/build/tls/generate.test.js +115 -0
  30. package/build/tls/index.js +80 -0
  31. package/build/tls/index.test.js +141 -0
  32. package/build/tls/install-hint.js +45 -0
  33. package/build/tls/install-hint.test.js +32 -0
  34. package/build/tls/load.js +18 -0
  35. package/build/tls/load.test.js +40 -0
  36. package/build/tls/paths.js +30 -0
  37. package/build/tls/paths.test.js +53 -0
  38. package/build/tls/san.js +27 -0
  39. package/build/tls/san.test.js +72 -0
  40. package/build/tool-registry.test.js +823 -0
  41. package/build/tool.js +74 -15
  42. package/build/tool.test.js +250 -7
  43. package/build/tools/add-cell-of-shape.js +4 -2
  44. package/build/tools/add-edge.js +3 -1
  45. package/build/tools/add-rectangle.js +4 -2
  46. package/build/tools/copy-page.js +13 -0
  47. package/build/tools/create-layer.js +4 -2
  48. package/build/tools/create-page.js +8 -0
  49. package/build/tools/delete-cell-by-id.js +4 -2
  50. package/build/tools/edit-cell.js +3 -1
  51. package/build/tools/edit-edge.js +3 -1
  52. package/build/tools/export-diagram.js +7 -3
  53. package/build/tools/get-active-layer.js +4 -1
  54. package/build/tools/get-current-page.js +5 -0
  55. package/build/tools/get-selected-cell.js +4 -1
  56. package/build/tools/import-diagram.js +12 -2
  57. package/build/tools/import-mermaid.js +31 -0
  58. package/build/tools/index.js +14 -0
  59. package/build/tools/list-documents.js +17 -0
  60. package/build/tools/list-layers.js +4 -1
  61. package/build/tools/list-paged-model.js +4 -3
  62. package/build/tools/list-pages.js +5 -0
  63. package/build/tools/move-cell-to-layer.js +3 -1
  64. package/build/tools/rename-page.js +10 -0
  65. package/build/tools/set-active-layer.js +4 -2
  66. package/build/tools/set-cell-data.js +3 -1
  67. package/build/tools/set-cell-parent.js +3 -1
  68. package/build/tools/set-cell-shape.js +3 -1
  69. package/build/tools/shared.js +35 -0
  70. package/build/tools/shared.test.js +29 -0
  71. package/package.json +12 -4
@@ -1,4 +1,4 @@
1
- import { parseExtensionPortValue, parseHttpPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, parseTransports, } from "./config.js";
1
+ import { parseExtensionPortValue, parseHttpPortValue, findArgValue, hasFlag, shouldShowHelp, parseConfig, buildConfig, parseTransports, parseWebSocketUrlValue, parseHostValue, parseLoggerValue, envToArgs, defaultConfig, } from "./config.js";
2
2
  describe("parseExtensionPortValue", () => {
3
3
  test("valid port returns number", () => {
4
4
  expect(parseExtensionPortValue("8080")).toBe(8080);
@@ -115,6 +115,9 @@ describe("parseConfig", () => {
115
115
  httpPort: 3000,
116
116
  transports: ["stdio"],
117
117
  editorEnabled: false,
118
+ logger: "console",
119
+ tlsEnabled: false,
120
+ tlsAuto: false,
118
121
  });
119
122
  });
120
123
  test("--extension-port flag sets custom port", () => {
@@ -123,6 +126,9 @@ describe("parseConfig", () => {
123
126
  httpPort: 3000,
124
127
  transports: ["stdio"],
125
128
  editorEnabled: false,
129
+ logger: "console",
130
+ tlsEnabled: false,
131
+ tlsAuto: false,
126
132
  });
127
133
  });
128
134
  test("-p flag sets custom port", () => {
@@ -131,6 +137,9 @@ describe("parseConfig", () => {
131
137
  httpPort: 3000,
132
138
  transports: ["stdio"],
133
139
  editorEnabled: false,
140
+ logger: "console",
141
+ tlsEnabled: false,
142
+ tlsAuto: false,
134
143
  });
135
144
  });
136
145
  test("--http-port flag sets custom port", () => {
@@ -139,6 +148,9 @@ describe("parseConfig", () => {
139
148
  httpPort: 4242,
140
149
  transports: ["stdio"],
141
150
  editorEnabled: false,
151
+ logger: "console",
152
+ tlsEnabled: false,
153
+ tlsAuto: false,
142
154
  });
143
155
  });
144
156
  test("both ports can be configured", () => {
@@ -147,6 +159,9 @@ describe("parseConfig", () => {
147
159
  httpPort: 4242,
148
160
  transports: ["stdio"],
149
161
  editorEnabled: false,
162
+ logger: "console",
163
+ tlsEnabled: false,
164
+ tlsAuto: false,
150
165
  });
151
166
  });
152
167
  test("help flag is ignored in config parsing", () => {
@@ -155,6 +170,9 @@ describe("parseConfig", () => {
155
170
  httpPort: 3000,
156
171
  transports: ["stdio"],
157
172
  editorEnabled: false,
173
+ logger: "console",
174
+ tlsEnabled: false,
175
+ tlsAuto: false,
158
176
  });
159
177
  });
160
178
  test("invalid port returns Error", () => {
@@ -183,6 +201,9 @@ describe("parseConfig", () => {
183
201
  httpPort: 3000,
184
202
  transports: ["stdio"],
185
203
  editorEnabled: false,
204
+ logger: "console",
205
+ tlsEnabled: false,
206
+ tlsAuto: false,
186
207
  });
187
208
  });
188
209
  test("short and long form both work, last wins", () => {
@@ -191,6 +212,9 @@ describe("parseConfig", () => {
191
212
  httpPort: 3000,
192
213
  transports: ["stdio"],
193
214
  editorEnabled: false,
215
+ logger: "console",
216
+ tlsEnabled: false,
217
+ tlsAuto: false,
194
218
  });
195
219
  });
196
220
  test("last http-port flag wins", () => {
@@ -199,6 +223,9 @@ describe("parseConfig", () => {
199
223
  httpPort: 5000,
200
224
  transports: ["stdio"],
201
225
  editorEnabled: false,
226
+ logger: "console",
227
+ tlsEnabled: false,
228
+ tlsAuto: false,
202
229
  });
203
230
  });
204
231
  test("sets single transport", () => {
@@ -207,6 +234,9 @@ describe("parseConfig", () => {
207
234
  httpPort: 3000,
208
235
  transports: ["stdio"],
209
236
  editorEnabled: false,
237
+ logger: "console",
238
+ tlsEnabled: false,
239
+ tlsAuto: false,
210
240
  });
211
241
  });
212
242
  test("sets multiple transports", () => {
@@ -215,6 +245,9 @@ describe("parseConfig", () => {
215
245
  httpPort: 3000,
216
246
  transports: ["stdio", "http"],
217
247
  editorEnabled: false,
248
+ logger: "console",
249
+ tlsEnabled: false,
250
+ tlsAuto: false,
218
251
  });
219
252
  });
220
253
  test("rejects unknown transport", () => {
@@ -227,6 +260,9 @@ describe("parseConfig", () => {
227
260
  httpPort: 3000,
228
261
  transports: ["stdio"],
229
262
  editorEnabled: true,
263
+ logger: "console",
264
+ tlsEnabled: false,
265
+ tlsAuto: false,
230
266
  });
231
267
  });
232
268
  test("-e flag enables editor", () => {
@@ -235,6 +271,9 @@ describe("parseConfig", () => {
235
271
  httpPort: 3000,
236
272
  transports: ["stdio"],
237
273
  editorEnabled: true,
274
+ logger: "console",
275
+ tlsEnabled: false,
276
+ tlsAuto: false,
238
277
  });
239
278
  });
240
279
  test("--editor false disables editor explicitly", () => {
@@ -243,6 +282,9 @@ describe("parseConfig", () => {
243
282
  httpPort: 3000,
244
283
  transports: ["stdio"],
245
284
  editorEnabled: false,
285
+ logger: "console",
286
+ tlsEnabled: false,
287
+ tlsAuto: false,
246
288
  });
247
289
  });
248
290
  test("--editor=true enables editor", () => {
@@ -251,6 +293,9 @@ describe("parseConfig", () => {
251
293
  httpPort: 3000,
252
294
  transports: ["stdio"],
253
295
  editorEnabled: true,
296
+ logger: "console",
297
+ tlsEnabled: false,
298
+ tlsAuto: false,
254
299
  });
255
300
  });
256
301
  test("--editor=false disables editor explicitly", () => {
@@ -259,13 +304,278 @@ describe("parseConfig", () => {
259
304
  httpPort: 3000,
260
305
  transports: ["stdio"],
261
306
  editorEnabled: false,
307
+ logger: "console",
308
+ tlsEnabled: false,
309
+ tlsAuto: false,
262
310
  });
263
311
  });
264
312
  });
313
+ describe("parseConfig --host", () => {
314
+ test("--host sets host in config", () => {
315
+ const result = parseConfig(["--host", "127.0.0.1"]);
316
+ expect(result).not.toBeInstanceOf(Error);
317
+ expect(result.host).toBe("127.0.0.1");
318
+ });
319
+ test("--host with IPv6 sets host in config", () => {
320
+ const result = parseConfig(["--host", "::1"]);
321
+ expect(result).not.toBeInstanceOf(Error);
322
+ expect(result.host).toBe("::1");
323
+ });
324
+ test("--host with invalid value returns Error", () => {
325
+ const result = parseConfig(["--host", "localhost"]);
326
+ expect(result).toBeInstanceOf(Error);
327
+ });
328
+ test("omitting --host leaves host undefined", () => {
329
+ const result = parseConfig([]);
330
+ expect(result).not.toBeInstanceOf(Error);
331
+ expect(result.host).toBeUndefined();
332
+ });
333
+ test("--host without value returns Error", () => {
334
+ const result = parseConfig(["--host"]);
335
+ expect(result).toBeInstanceOf(Error);
336
+ expect(result.message).toContain("--host flag requires an IP address");
337
+ });
338
+ });
339
+ describe("parseWebSocketUrlValue", () => {
340
+ test("accepts ws:// URL", () => {
341
+ expect(parseWebSocketUrlValue("ws://example.com:3333")).toBe("ws://example.com:3333");
342
+ });
343
+ test("accepts wss:// URL", () => {
344
+ expect(parseWebSocketUrlValue("wss://example.com/ws")).toBe("wss://example.com/ws");
345
+ });
346
+ test("rejects http:// URL", () => {
347
+ const result = parseWebSocketUrlValue("http://example.com");
348
+ expect(result).toBeInstanceOf(Error);
349
+ });
350
+ test("rejects empty string", () => {
351
+ const result = parseWebSocketUrlValue("");
352
+ expect(result).toBeInstanceOf(Error);
353
+ });
354
+ test("rejects undefined", () => {
355
+ const result = parseWebSocketUrlValue(undefined);
356
+ expect(result).toBeInstanceOf(Error);
357
+ });
358
+ test("rejects garbage", () => {
359
+ const result = parseWebSocketUrlValue("not a url");
360
+ expect(result).toBeInstanceOf(Error);
361
+ });
362
+ });
363
+ describe("parseLoggerValue", () => {
364
+ test("accepts console", () => {
365
+ expect(parseLoggerValue("console")).toBe("console");
366
+ });
367
+ test("accepts mcp-server", () => {
368
+ expect(parseLoggerValue("mcp-server")).toBe("mcp-server");
369
+ });
370
+ test("normalises legacy mcp_server to mcp-server", () => {
371
+ expect(parseLoggerValue("mcp_server")).toBe("mcp-server");
372
+ });
373
+ test("rejects unknown value", () => {
374
+ expect(parseLoggerValue("foo")).toBeInstanceOf(Error);
375
+ });
376
+ test("rejects undefined", () => {
377
+ expect(parseLoggerValue(undefined)).toBeInstanceOf(Error);
378
+ });
379
+ test("rejects empty string", () => {
380
+ expect(parseLoggerValue("")).toBeInstanceOf(Error);
381
+ });
382
+ });
383
+ describe("parseConfig --logger", () => {
384
+ test("default logger is console", () => {
385
+ const result = parseConfig([]);
386
+ expect(result).not.toBeInstanceOf(Error);
387
+ expect(result.logger).toBe("console");
388
+ });
389
+ test("--logger console keeps default", () => {
390
+ const result = parseConfig(["--logger", "console"]);
391
+ expect(result.logger).toBe("console");
392
+ });
393
+ test("--logger mcp-server is accepted", () => {
394
+ const result = parseConfig(["--logger", "mcp-server"]);
395
+ expect(result.logger).toBe("mcp-server");
396
+ });
397
+ test("--logger mcp_server is normalised", () => {
398
+ const result = parseConfig(["--logger", "mcp_server"]);
399
+ expect(result.logger).toBe("mcp-server");
400
+ });
401
+ test("--logger without value returns Error", () => {
402
+ const result = parseConfig(["--logger"]);
403
+ expect(result).toBeInstanceOf(Error);
404
+ expect(result.message).toContain("--logger flag requires a mode");
405
+ });
406
+ test("--logger with invalid value returns Error", () => {
407
+ const result = parseConfig(["--logger", "syslog"]);
408
+ expect(result).toBeInstanceOf(Error);
409
+ expect(result.message).toContain('Invalid logger "syslog"');
410
+ });
411
+ test("last --logger wins", () => {
412
+ const result = parseConfig([
413
+ "--logger",
414
+ "mcp-server",
415
+ "--logger",
416
+ "console",
417
+ ]);
418
+ expect(result.logger).toBe("console");
419
+ });
420
+ });
421
+ describe("defaultConfig", () => {
422
+ test("returns config with default logger", () => {
423
+ const config = defaultConfig();
424
+ expect(config.logger).toBe("console");
425
+ expect(config.extensionPort).toBe(3333);
426
+ expect(config.httpPort).toBe(3000);
427
+ expect(config.transports).toEqual(["stdio"]);
428
+ expect(config.editorEnabled).toBe(false);
429
+ });
430
+ });
431
+ describe("parseHostValue", () => {
432
+ test("undefined input returns Error", () => {
433
+ expect(parseHostValue(undefined)).toBeInstanceOf(Error);
434
+ });
435
+ test("empty string returns Error", () => {
436
+ expect(parseHostValue("")).toBeInstanceOf(Error);
437
+ });
438
+ test("valid IPv4 loopback returns value", () => {
439
+ expect(parseHostValue("127.0.0.1")).toBe("127.0.0.1");
440
+ });
441
+ test("valid IPv4 wildcard returns value", () => {
442
+ expect(parseHostValue("0.0.0.0")).toBe("0.0.0.0");
443
+ });
444
+ test("valid IPv6 loopback returns value", () => {
445
+ expect(parseHostValue("::1")).toBe("::1");
446
+ });
447
+ test("valid IPv6 wildcard returns value", () => {
448
+ expect(parseHostValue("::")).toBe("::");
449
+ });
450
+ test("hostname string returns Error", () => {
451
+ expect(parseHostValue("localhost")).toBeInstanceOf(Error);
452
+ });
453
+ test("arbitrary string returns Error", () => {
454
+ expect(parseHostValue("not-an-ip")).toBeInstanceOf(Error);
455
+ });
456
+ });
457
+ describe("parseConfig --websocket-url", () => {
458
+ test("accepts wss:// override", () => {
459
+ const result = parseConfig(["--websocket-url", "wss://example.com/ws"]);
460
+ expect(result).not.toBeInstanceOf(Error);
461
+ expect(result.webSocketUrl).toBe("wss://example.com/ws");
462
+ });
463
+ test("rejects http:// scheme", () => {
464
+ const result = parseConfig(["--websocket-url", "http://example.com"]);
465
+ expect(result).toBeInstanceOf(Error);
466
+ });
467
+ test("missing value returns Error", () => {
468
+ const result = parseConfig(["--websocket-url"]);
469
+ expect(result).toBeInstanceOf(Error);
470
+ expect(result.message).toContain("--websocket-url flag requires a URL");
471
+ });
472
+ test("last --websocket-url wins", () => {
473
+ const result = parseConfig([
474
+ "--websocket-url",
475
+ "ws://first.example/ws",
476
+ "--websocket-url",
477
+ "wss://second.example/ws",
478
+ ]);
479
+ expect(result.webSocketUrl).toBe("wss://second.example/ws");
480
+ });
481
+ });
482
+ describe("envToArgs", () => {
483
+ test("empty env yields empty args", () => {
484
+ expect(envToArgs({})).toEqual([]);
485
+ });
486
+ test("maps DRAWIO_MCP_EXTENSION_PORT", () => {
487
+ expect(envToArgs({ DRAWIO_MCP_EXTENSION_PORT: "8080" })).toEqual([
488
+ "--extension-port",
489
+ "8080",
490
+ ]);
491
+ });
492
+ test("maps DRAWIO_MCP_HTTP_PORT", () => {
493
+ expect(envToArgs({ DRAWIO_MCP_HTTP_PORT: "4242" })).toEqual([
494
+ "--http-port",
495
+ "4242",
496
+ ]);
497
+ });
498
+ test("maps DRAWIO_MCP_TRANSPORT", () => {
499
+ expect(envToArgs({ DRAWIO_MCP_TRANSPORT: "stdio,http" })).toEqual([
500
+ "--transport",
501
+ "stdio,http",
502
+ ]);
503
+ });
504
+ test("maps DRAWIO_MCP_EDITOR true/false", () => {
505
+ expect(envToArgs({ DRAWIO_MCP_EDITOR: "true" })).toEqual([
506
+ "--editor",
507
+ "true",
508
+ ]);
509
+ expect(envToArgs({ DRAWIO_MCP_EDITOR: "false" })).toEqual([
510
+ "--editor",
511
+ "false",
512
+ ]);
513
+ });
514
+ test("ignores invalid DRAWIO_MCP_EDITOR value", () => {
515
+ expect(envToArgs({ DRAWIO_MCP_EDITOR: "yes" })).toEqual([]);
516
+ });
517
+ test("maps DRAWIO_MCP_ASSET_PATH", () => {
518
+ expect(envToArgs({ DRAWIO_MCP_ASSET_PATH: "/tmp/assets" })).toEqual([
519
+ "--asset-path",
520
+ "/tmp/assets",
521
+ ]);
522
+ });
523
+ test("maps DRAWIO_MCP_WEBSOCKET_URL", () => {
524
+ expect(envToArgs({ DRAWIO_MCP_WEBSOCKET_URL: "wss://example.com/ws" })).toEqual(["--websocket-url", "wss://example.com/ws"]);
525
+ });
526
+ test("ignores empty values", () => {
527
+ expect(envToArgs({
528
+ DRAWIO_MCP_EXTENSION_PORT: "",
529
+ DRAWIO_MCP_WEBSOCKET_URL: "",
530
+ })).toEqual([]);
531
+ });
532
+ test("maps multiple env vars together", () => {
533
+ expect(envToArgs({
534
+ DRAWIO_MCP_EXTENSION_PORT: "8080",
535
+ DRAWIO_MCP_WEBSOCKET_URL: "wss://example.com/ws",
536
+ DRAWIO_MCP_TRANSPORT: "http",
537
+ })).toEqual([
538
+ "--extension-port",
539
+ "8080",
540
+ "--transport",
541
+ "http",
542
+ "--websocket-url",
543
+ "wss://example.com/ws",
544
+ ]);
545
+ });
546
+ test("DRAWIO_MCP_HOST maps to --host", () => {
547
+ const result = envToArgs({ DRAWIO_MCP_HOST: "0.0.0.0" });
548
+ expect(result).toEqual(["--host", "0.0.0.0"]);
549
+ });
550
+ test("missing DRAWIO_MCP_HOST produces no args", () => {
551
+ const result = envToArgs({});
552
+ expect(result).toEqual([]);
553
+ });
554
+ test("DRAWIO_MCP_LOGGER maps to --logger", () => {
555
+ expect(envToArgs({ DRAWIO_MCP_LOGGER: "mcp-server" })).toEqual([
556
+ "--logger",
557
+ "mcp-server",
558
+ ]);
559
+ });
560
+ test("DRAWIO_MCP_LOGGER empty produces no args", () => {
561
+ expect(envToArgs({ DRAWIO_MCP_LOGGER: "" })).toEqual([]);
562
+ });
563
+ });
265
564
  describe("buildConfig", () => {
266
565
  const originalArgv = process.argv;
566
+ const originalEnv = process.env;
567
+ beforeEach(() => {
568
+ // Strip any host-set DRAWIO_MCP_* vars so they don't leak into tests
569
+ const clean = { ...originalEnv };
570
+ for (const key of Object.keys(clean)) {
571
+ if (key.startsWith("DRAWIO_MCP_"))
572
+ delete clean[key];
573
+ }
574
+ process.env = clean;
575
+ });
267
576
  afterEach(() => {
268
577
  process.argv = originalArgv;
578
+ process.env = originalEnv;
269
579
  });
270
580
  test("uses default config with empty args", () => {
271
581
  process.argv = ["node", "script.js"];
@@ -275,6 +585,9 @@ describe("buildConfig", () => {
275
585
  httpPort: 3000,
276
586
  transports: ["stdio"],
277
587
  editorEnabled: false,
588
+ logger: "console",
589
+ tlsEnabled: false,
590
+ tlsAuto: false,
278
591
  });
279
592
  });
280
593
  test("parses custom port from argv", () => {
@@ -285,6 +598,9 @@ describe("buildConfig", () => {
285
598
  httpPort: 3000,
286
599
  transports: ["stdio"],
287
600
  editorEnabled: false,
601
+ logger: "console",
602
+ tlsEnabled: false,
603
+ tlsAuto: false,
288
604
  });
289
605
  });
290
606
  test("parses custom http port from argv", () => {
@@ -295,6 +611,9 @@ describe("buildConfig", () => {
295
611
  httpPort: 4242,
296
612
  transports: ["stdio"],
297
613
  editorEnabled: false,
614
+ logger: "console",
615
+ tlsEnabled: false,
616
+ tlsAuto: false,
298
617
  });
299
618
  });
300
619
  test("returns Error for invalid config", () => {
@@ -302,4 +621,133 @@ describe("buildConfig", () => {
302
621
  const result = buildConfig();
303
622
  expect(result).toBeInstanceOf(Error);
304
623
  });
624
+ test("env var DRAWIO_MCP_WEBSOCKET_URL is honored", () => {
625
+ process.argv = ["node", "script.js"];
626
+ process.env = {
627
+ ...originalEnv,
628
+ DRAWIO_MCP_WEBSOCKET_URL: "wss://env.example/ws",
629
+ };
630
+ const result = buildConfig();
631
+ expect(result.webSocketUrl).toBe("wss://env.example/ws");
632
+ });
633
+ test("CLI flag overrides env var (last-wins)", () => {
634
+ process.argv = [
635
+ "node",
636
+ "script.js",
637
+ "--websocket-url",
638
+ "wss://flag.example/ws",
639
+ ];
640
+ process.env = {
641
+ ...originalEnv,
642
+ DRAWIO_MCP_WEBSOCKET_URL: "wss://env.example/ws",
643
+ };
644
+ const result = buildConfig();
645
+ expect(result.webSocketUrl).toBe("wss://flag.example/ws");
646
+ });
647
+ test("env var DRAWIO_MCP_EXTENSION_PORT is honored", () => {
648
+ process.argv = ["node", "script.js"];
649
+ process.env = { ...originalEnv, DRAWIO_MCP_EXTENSION_PORT: "9090" };
650
+ const result = buildConfig();
651
+ expect(result.extensionPort).toBe(9090);
652
+ });
653
+ test("CLI port overrides env var port", () => {
654
+ process.argv = ["node", "script.js", "--extension-port", "7777"];
655
+ process.env = { ...originalEnv, DRAWIO_MCP_EXTENSION_PORT: "9090" };
656
+ const result = buildConfig();
657
+ expect(result.extensionPort).toBe(7777);
658
+ });
659
+ test("env var DRAWIO_MCP_LOGGER is honored", () => {
660
+ process.argv = ["node", "script.js"];
661
+ process.env = { ...originalEnv, DRAWIO_MCP_LOGGER: "mcp-server" };
662
+ const result = buildConfig();
663
+ expect(result.logger).toBe("mcp-server");
664
+ });
665
+ test("CLI --logger overrides DRAWIO_MCP_LOGGER", () => {
666
+ process.argv = ["node", "script.js", "--logger", "console"];
667
+ process.env = { ...originalEnv, DRAWIO_MCP_LOGGER: "mcp-server" };
668
+ const result = buildConfig();
669
+ expect(result.logger).toBe("console");
670
+ });
671
+ });
672
+ describe("TLS configuration", () => {
673
+ it("--tls alone sets tlsEnabled with no mode", () => {
674
+ const cfg = parseConfig(["--tls"]);
675
+ expect(cfg).not.toBeInstanceOf(Error);
676
+ if (cfg instanceof Error)
677
+ return;
678
+ expect(cfg.tlsEnabled).toBe(true);
679
+ expect(cfg.tlsAuto).toBe(false);
680
+ expect(cfg.tlsCert).toBeUndefined();
681
+ expect(cfg.tlsKey).toBeUndefined();
682
+ });
683
+ it("--tls --tls-cert X --tls-key Y configures manual mode", () => {
684
+ const cfg = parseConfig([
685
+ "--tls",
686
+ "--tls-cert",
687
+ "/c.pem",
688
+ "--tls-key",
689
+ "/k.pem",
690
+ ]);
691
+ expect(cfg).not.toBeInstanceOf(Error);
692
+ if (cfg instanceof Error)
693
+ return;
694
+ expect(cfg.tlsEnabled).toBe(true);
695
+ expect(cfg.tlsCert).toBe("/c.pem");
696
+ expect(cfg.tlsKey).toBe("/k.pem");
697
+ expect(cfg.tlsAuto).toBe(false);
698
+ });
699
+ it("--tls --tls-auto configures auto mode", () => {
700
+ const cfg = parseConfig(["--tls", "--tls-auto"]);
701
+ expect(cfg).not.toBeInstanceOf(Error);
702
+ if (cfg instanceof Error)
703
+ return;
704
+ expect(cfg.tlsEnabled).toBe(true);
705
+ expect(cfg.tlsAuto).toBe(true);
706
+ });
707
+ it("--tls-dir captures override path", () => {
708
+ const cfg = parseConfig(["--tls", "--tls-auto", "--tls-dir", "/data/tls"]);
709
+ expect(cfg).not.toBeInstanceOf(Error);
710
+ if (cfg instanceof Error)
711
+ return;
712
+ expect(cfg.tlsDir).toBe("/data/tls");
713
+ });
714
+ it("--tls-cert without --tls-key is an error", () => {
715
+ expect(parseConfig(["--tls", "--tls-cert", "/c.pem"])).toBeInstanceOf(Error);
716
+ });
717
+ it("--tls-key without --tls-cert is an error", () => {
718
+ expect(parseConfig(["--tls", "--tls-key", "/k.pem"])).toBeInstanceOf(Error);
719
+ });
720
+ it("--tls-auto + --tls-cert is an error", () => {
721
+ expect(parseConfig([
722
+ "--tls",
723
+ "--tls-auto",
724
+ "--tls-cert",
725
+ "/c",
726
+ "--tls-key",
727
+ "/k",
728
+ ])).toBeInstanceOf(Error);
729
+ });
730
+ it("--tls-cert without --tls is an error", () => {
731
+ expect(parseConfig(["--tls-cert", "/c", "--tls-key", "/k"])).toBeInstanceOf(Error);
732
+ });
733
+ it("envToArgs maps DRAWIO_MCP_TLS=true to --tls", () => {
734
+ expect(envToArgs({ DRAWIO_MCP_TLS: "true" })).toEqual(["--tls"]);
735
+ });
736
+ it("envToArgs maps DRAWIO_MCP_TLS_AUTO=true to --tls-auto", () => {
737
+ expect(envToArgs({ DRAWIO_MCP_TLS_AUTO: "true" })).toEqual(["--tls-auto"]);
738
+ });
739
+ it("envToArgs maps cert/key/dir env vars to flags", () => {
740
+ expect(envToArgs({
741
+ DRAWIO_MCP_TLS_CERT: "/c.pem",
742
+ DRAWIO_MCP_TLS_KEY: "/k.pem",
743
+ DRAWIO_MCP_TLS_DIR: "/data/tls",
744
+ })).toEqual([
745
+ "--tls-cert",
746
+ "/c.pem",
747
+ "--tls-key",
748
+ "/k.pem",
749
+ "--tls-dir",
750
+ "/data/tls",
751
+ ]);
752
+ });
305
753
  });
@@ -15,6 +15,9 @@ export function create_bus(log) {
15
15
  }
16
16
  };
17
17
  emitter.on(bus_reply_stream, listener);
18
+ return () => {
19
+ emitter.off(bus_reply_stream, listener);
20
+ };
18
21
  },
19
22
  };
20
23
  return bus;
@@ -62,4 +62,11 @@ describe("create_bus", () => {
62
62
  expect(mockReply1).toHaveBeenCalledWith(matchingEvent);
63
63
  expect(mockReply2).not.toHaveBeenCalled();
64
64
  });
65
+ it("should allow callers to unsubscribe pending reply handlers", () => {
66
+ const mockReply = jest.fn();
67
+ const unsubscribe = bus.on_reply_from_extension("event1", mockReply);
68
+ unsubscribe();
69
+ emitter.emit(bus_reply_stream, { __event: "event1", data: "late" });
70
+ expect(mockReply).not.toHaveBeenCalled();
71
+ });
65
72
  });
@@ -0,0 +1,45 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
+ import { createDrawioMcpApp } from "./index.js";
4
+ import { defaultConfig } from "./config.js";
5
+ import { MemoryLogger } from "./real-environment/logger.js";
6
+ describe("createDrawioMcpApp capabilities", () => {
7
+ let app;
8
+ afterEach(async () => {
9
+ if (app)
10
+ await app.close();
11
+ });
12
+ it("does not advertise logging capability when logger is console", async () => {
13
+ app = createDrawioMcpApp({
14
+ config: { ...defaultConfig(), logger: "console" },
15
+ log: new MemoryLogger(),
16
+ });
17
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
18
+ const server = app.createMcpServer();
19
+ const client = new Client({ name: "cap-test", version: "1.0.0" });
20
+ await Promise.all([
21
+ server.connect(serverTransport),
22
+ client.connect(clientTransport),
23
+ ]);
24
+ const capabilities = client.getServerCapabilities();
25
+ expect(capabilities?.logging).toBeUndefined();
26
+ await client.close();
27
+ });
28
+ it("advertises logging capability when logger is mcp-server", async () => {
29
+ app = createDrawioMcpApp({
30
+ config: { ...defaultConfig(), logger: "mcp-server" },
31
+ log: new MemoryLogger(),
32
+ });
33
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
34
+ const server = app.createMcpServer();
35
+ const client = new Client({ name: "cap-test", version: "1.0.0" });
36
+ await Promise.all([
37
+ server.connect(serverTransport),
38
+ client.connect(clientTransport),
39
+ ]);
40
+ const capabilities = client.getServerCapabilities();
41
+ expect(capabilities?.logging).toBeDefined();
42
+ expect(capabilities?.logging).toEqual(expect.objectContaining({ setLevels: true }));
43
+ await client.close();
44
+ });
45
+ });