drawio-mcp-server 1.8.0 → 2.0.4
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 +9 -5
- package/build/emitter_bus.js +2 -3
- package/build/index.js +243 -341
- package/build/multi-transport.test.js +160 -0
- package/build/plugin/mcp-plugin.js +853 -60
- 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/export-diagram.test.js +473 -0
- package/build/real-environment/harness.js +176 -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 +35 -33
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
|
+
import { createDrawioMcpApp } from "./index.js";
|
|
5
|
+
import { MemoryLogger } from "./real-environment/logger.js";
|
|
6
|
+
describe("multi-transport support", () => {
|
|
7
|
+
let app;
|
|
8
|
+
let logger;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
logger = new MemoryLogger();
|
|
11
|
+
app = createDrawioMcpApp({ log: logger });
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await app.close();
|
|
15
|
+
});
|
|
16
|
+
it("createMcpServer returns distinct instances", () => {
|
|
17
|
+
const server1 = app.createMcpServer();
|
|
18
|
+
const server2 = app.createMcpServer();
|
|
19
|
+
expect(server1).not.toBe(server2);
|
|
20
|
+
});
|
|
21
|
+
it("each McpServer instance has tools registered", async () => {
|
|
22
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
23
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
24
|
+
const server1 = app.createMcpServer();
|
|
25
|
+
const server2 = app.createMcpServer();
|
|
26
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
27
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
28
|
+
await Promise.all([
|
|
29
|
+
server1.connect(st1),
|
|
30
|
+
client1.connect(ct1),
|
|
31
|
+
server2.connect(st2),
|
|
32
|
+
client2.connect(ct2),
|
|
33
|
+
]);
|
|
34
|
+
const tools1 = await client1.listTools();
|
|
35
|
+
const tools2 = await client2.listTools();
|
|
36
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
37
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
38
|
+
const names1 = tools1.tools.map((t) => t.name).sort();
|
|
39
|
+
const names2 = tools2.tools.map((t) => t.name).sort();
|
|
40
|
+
expect(names1).toEqual(names2);
|
|
41
|
+
await client1.close();
|
|
42
|
+
await client2.close();
|
|
43
|
+
});
|
|
44
|
+
it("two InMemoryTransport connections work simultaneously without 'Already connected' error", async () => {
|
|
45
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
46
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
47
|
+
const server1 = app.createMcpServer();
|
|
48
|
+
const server2 = app.createMcpServer();
|
|
49
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
50
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
51
|
+
// This is the exact scenario that was failing before the fix:
|
|
52
|
+
// connecting two transports should not throw.
|
|
53
|
+
await expect(Promise.all([
|
|
54
|
+
server1.connect(st1),
|
|
55
|
+
client1.connect(ct1),
|
|
56
|
+
server2.connect(st2),
|
|
57
|
+
client2.connect(ct2),
|
|
58
|
+
])).resolves.not.toThrow();
|
|
59
|
+
// Both clients can independently list tools
|
|
60
|
+
const [tools1, tools2] = await Promise.all([
|
|
61
|
+
client1.listTools(),
|
|
62
|
+
client2.listTools(),
|
|
63
|
+
]);
|
|
64
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
65
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
66
|
+
await client1.close();
|
|
67
|
+
await client2.close();
|
|
68
|
+
});
|
|
69
|
+
it("close() shuts down all created McpServer instances", async () => {
|
|
70
|
+
const [ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
71
|
+
const [ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
72
|
+
const server1 = app.createMcpServer();
|
|
73
|
+
const server2 = app.createMcpServer();
|
|
74
|
+
const client1 = new Client({ name: "test-client-1", version: "1.0.0" });
|
|
75
|
+
const client2 = new Client({ name: "test-client-2", version: "1.0.0" });
|
|
76
|
+
await Promise.all([
|
|
77
|
+
server1.connect(st1),
|
|
78
|
+
client1.connect(ct1),
|
|
79
|
+
server2.connect(st2),
|
|
80
|
+
client2.connect(ct2),
|
|
81
|
+
]);
|
|
82
|
+
// close() should succeed without errors even with multiple servers
|
|
83
|
+
await expect(app.close()).resolves.not.toThrow();
|
|
84
|
+
// After close, listing tools should fail because the servers are shut down
|
|
85
|
+
await expect(client1.listTools()).rejects.toThrow();
|
|
86
|
+
await expect(client2.listTools()).rejects.toThrow();
|
|
87
|
+
});
|
|
88
|
+
it("connecting the same McpServer instance to two transports still throws", async () => {
|
|
89
|
+
const [_ct1, st1] = InMemoryTransport.createLinkedPair();
|
|
90
|
+
const [_ct2, st2] = InMemoryTransport.createLinkedPair();
|
|
91
|
+
const server = app.createMcpServer();
|
|
92
|
+
await server.connect(st1);
|
|
93
|
+
// The SDK constraint hasn't changed — a single Protocol instance
|
|
94
|
+
// still rejects a second connect().
|
|
95
|
+
await expect(server.connect(st2)).rejects.toThrow(/already connected/i);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("HTTP transport (stateless per-request)", () => {
|
|
99
|
+
let app;
|
|
100
|
+
let logger;
|
|
101
|
+
let httpServer;
|
|
102
|
+
let port;
|
|
103
|
+
const config = {
|
|
104
|
+
extensionPort: 0,
|
|
105
|
+
httpPort: 0,
|
|
106
|
+
transports: ["http"],
|
|
107
|
+
editorEnabled: false,
|
|
108
|
+
};
|
|
109
|
+
const features = {
|
|
110
|
+
enableMcp: true,
|
|
111
|
+
enableEditor: false,
|
|
112
|
+
enableHealth: false,
|
|
113
|
+
enableConfig: false,
|
|
114
|
+
};
|
|
115
|
+
beforeEach(async () => {
|
|
116
|
+
logger = new MemoryLogger();
|
|
117
|
+
app = createDrawioMcpApp({ log: logger });
|
|
118
|
+
const started = await app.startHttpServer(0, config, features);
|
|
119
|
+
httpServer = started.server;
|
|
120
|
+
port = started.port;
|
|
121
|
+
});
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
await app.close();
|
|
124
|
+
});
|
|
125
|
+
it("handles a single HTTP client request", async () => {
|
|
126
|
+
const client = new Client({ name: "http-test-1", version: "1.0.0" });
|
|
127
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
128
|
+
await client.connect(transport);
|
|
129
|
+
const tools = await client.listTools();
|
|
130
|
+
expect(tools.tools.length).toBeGreaterThan(0);
|
|
131
|
+
await client.close();
|
|
132
|
+
});
|
|
133
|
+
it("handles multiple sequential HTTP client requests without reuse error", async () => {
|
|
134
|
+
// First request
|
|
135
|
+
const client1 = new Client({ name: "http-test-seq-1", version: "1.0.0" });
|
|
136
|
+
const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
137
|
+
await client1.connect(transport1);
|
|
138
|
+
const tools1 = await client1.listTools();
|
|
139
|
+
expect(tools1.tools.length).toBeGreaterThan(0);
|
|
140
|
+
await client1.close();
|
|
141
|
+
// Second request — this was the exact scenario that triggered the
|
|
142
|
+
// "Stateless transport cannot be reused across requests" error.
|
|
143
|
+
const client2 = new Client({ name: "http-test-seq-2", version: "1.0.0" });
|
|
144
|
+
const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
145
|
+
await client2.connect(transport2);
|
|
146
|
+
const tools2 = await client2.listTools();
|
|
147
|
+
expect(tools2.tools.length).toBeGreaterThan(0);
|
|
148
|
+
await client2.close();
|
|
149
|
+
});
|
|
150
|
+
it("close() succeeds after HTTP requests (per-request servers are cleaned up)", async () => {
|
|
151
|
+
const client = new Client({ name: "http-cleanup", version: "1.0.0" });
|
|
152
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
153
|
+
await client.connect(transport);
|
|
154
|
+
await client.listTools();
|
|
155
|
+
await client.close();
|
|
156
|
+
// close() should not hang or throw — the per-request McpServer
|
|
157
|
+
// was already disposed and removed from the tracking set.
|
|
158
|
+
await expect(app.close()).resolves.not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
});
|