dodraw-mcp-server 0.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 (37) hide show
  1. package/README.md +63 -0
  2. package/dist/src/index.js +45 -0
  3. package/dist/src/tools/diagramTools.js +426 -0
  4. package/dist/src/types.js +2 -0
  5. package/dist/src/utils/fileHandler.js +53 -0
  6. package/dist/test/test-auto-placement.js +83 -0
  7. package/dist/test/test-client.js +100 -0
  8. package/dist/test/test-collision.js +84 -0
  9. package/dist/test/test-constraints.js +88 -0
  10. package/dist/test/test-debug.js +41 -0
  11. package/dist/test/test-directional.js +90 -0
  12. package/dist/test/test-layer-support.js +69 -0
  13. package/dist/test/test-refined-spacing.js +58 -0
  14. package/dist/test/verify_types.js +29 -0
  15. package/package.json +24 -0
  16. package/src/index.ts +54 -0
  17. package/src/tools/diagramTools.ts +440 -0
  18. package/src/types.ts +78 -0
  19. package/src/utils/fileHandler.ts +51 -0
  20. package/test/test-auto-placement.ts +88 -0
  21. package/test/test-client.ts +116 -0
  22. package/test/test-collision.ts +93 -0
  23. package/test/test-constraints.ts +95 -0
  24. package/test/test-debug.ts +40 -0
  25. package/test/test-directional.ts +95 -0
  26. package/test/test-layer-support.ts +77 -0
  27. package/test/test-refined-spacing.ts +62 -0
  28. package/test/verify_types.ts +28 -0
  29. package/test_output/test_autoplacement.3duml +0 -0
  30. package/test_output/test_collision.3duml +0 -0
  31. package/test_output/test_constraints.3duml +0 -0
  32. package/test_output/test_debug.3duml +0 -0
  33. package/test_output/test_diagram.3duml +0 -0
  34. package/test_output/test_directional.3duml +0 -0
  35. package/test_output/test_layers.3duml +0 -0
  36. package/test_output/test_refined_spacing.3duml +0 -0
  37. package/tsconfig.json +13 -0
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+
2
+ # DoDraw MCP Server
3
+
4
+ An MCP server implementation for DoDraw (formerly 3DUML), providing tools to programmatically generate and manipulate 3D UML diagrams.
5
+
6
+ ## Distribution & Usage
7
+
8
+ There are two primary ways to distribute and use this server:
9
+
10
+ ### 1. Via NPM (Recommended)
11
+
12
+ This method allows users to run the server immediately without cloning the repository.
13
+
14
+ #### For Use (Running with `npx`)
15
+
16
+ Users can run the server directly using `npx`:
17
+
18
+ ```bash
19
+ npx -y dodraw-mcp-server
20
+ ```
21
+
22
+ *(Note: Replace `dodraw-mcp-server` with your actual package name if scoped, e.g., `@your-org/dodraw-mcp-server`)*
23
+
24
+ #### For Distribution (Publishing to NPM)
25
+
26
+ To make the server available via `npx`, follow these steps:
27
+
28
+ 1. **Login to NPM:**
29
+ ```bash
30
+ npm login
31
+ ```
32
+
33
+ 2. **Verify Package Details:**
34
+ Ensure `package.json` has the correct `name`, `version`, and `bin` entry.
35
+ ```json
36
+ "bin": {
37
+ "dodraw-mcp-server": "dist/src/index.js"
38
+ }
39
+ ```
40
+
41
+ 3. **Build the Project:**
42
+ Ensure the `dist` folder is up-to-date.
43
+ ```bash
44
+ npm run build
45
+ ```
46
+
47
+ 4. **Publish:**
48
+ ```bash
49
+ npm publish --access public
50
+ ```
51
+
52
+ ### 2. Via Source Code
53
+
54
+ 1. Clone the repository.
55
+ 2. Install dependencies: `npm install`.
56
+ 3. Build: `npm run build`.
57
+ 4. Configure your MCP Client (e.g., Claude Desktop) to point to the built file:
58
+ ```json
59
+ "dodraw": {
60
+ "command": "node",
61
+ "args": ["/path/to/repo/mcp-server/dist/src/index.js"]
62
+ }
63
+ ```
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const diagramTools_js_1 = require("./tools/diagramTools.js");
8
+ const server = new index_js_1.Server({
9
+ name: "dodraw-mcp-server",
10
+ version: "0.1.0",
11
+ }, {
12
+ capabilities: {
13
+ tools: {},
14
+ },
15
+ });
16
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
17
+ return {
18
+ tools: diagramTools_js_1.toolDefinitions,
19
+ };
20
+ });
21
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
22
+ try {
23
+ return await (0, diagramTools_js_1.handleToolCall)(request.params.name, request.params.arguments);
24
+ }
25
+ catch (error) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: `Error: ${error.message}`,
31
+ },
32
+ ],
33
+ isError: true,
34
+ };
35
+ }
36
+ });
37
+ async function main() {
38
+ const transport = new stdio_js_1.StdioServerTransport();
39
+ await server.connect(transport);
40
+ console.error("DoDraw MCP Server running on stdio");
41
+ }
42
+ main().catch((error) => {
43
+ console.error("Fatal error in main():", error);
44
+ process.exit(1);
45
+ });
@@ -0,0 +1,426 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolDefinitions = void 0;
4
+ exports.handleToolCall = handleToolCall;
5
+ const fileHandler_1 = require("../utils/fileHandler");
6
+ const crypto_1 = require("crypto");
7
+ exports.toolDefinitions = [
8
+ {
9
+ name: "create_new_diagram",
10
+ description: "Create a new empty DoDraw diagram file",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ filePath: { type: "string", description: "Path to the .3duml file to create. Must include .3duml extension." }
15
+ },
16
+ required: ["filePath"]
17
+ }
18
+ },
19
+ {
20
+ name: "read_diagram_structure",
21
+ description: "Read the structure (nodes, edges, layers) of a diagram",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ filePath: { type: "string" }
26
+ },
27
+ required: ["filePath"]
28
+ }
29
+ },
30
+ {
31
+ name: "add_node",
32
+ description: "Add a new node to the diagram",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ filePath: { type: "string" },
37
+ label: { type: "string" },
38
+ shape: { type: "string", enum: ["rectangle", "rounded", "ellipse", "diamond", "actor", "cylinder", "note", "cloud", "class", "text", "table"] },
39
+ x: { type: "number", description: "Optional. defaults to auto-placement" },
40
+ y: { type: "number", description: "Optional. defaults to 0 or same as last node" },
41
+ z: { type: "number" },
42
+ width: { type: "number", description: "Default: 2. Min: 0.1, Max: 50" },
43
+ height: { type: "number", description: "Default: 1.5. Min: 0.1, Max: 50" },
44
+ layerId: { type: "string", description: "Optional layer ID, defaults to active layer" },
45
+ color: { type: "string" }
46
+ },
47
+ required: ["filePath", "label"]
48
+ }
49
+ },
50
+ {
51
+ name: "add_edge",
52
+ description: "Add a new edge connecting two nodes",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ filePath: { type: "string" },
57
+ sourceId: { type: "string" },
58
+ targetId: { type: "string" },
59
+ sourcePointIndex: { type: "number", minimum: 0, maximum: 3, description: "0:Top, 1:Right, 2:Bottom, 3:Left" },
60
+ targetPointIndex: { type: "number", minimum: 0, maximum: 3 },
61
+ label: { type: "string" },
62
+ color: { type: "string", description: "Edge color (hex)" },
63
+ thickness: { type: "number", description: "Default: 0.01. Min: 0.005, Max: 0.5" }
64
+ },
65
+ required: ["filePath", "sourceId", "targetId", "sourcePointIndex", "targetPointIndex"]
66
+ }
67
+ },
68
+ {
69
+ name: "update_node",
70
+ description: "Update properties of an existing node",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ filePath: { type: "string" },
75
+ nodeId: { type: "string" },
76
+ label: { type: "string" },
77
+ x: { type: "number" },
78
+ y: { type: "number" },
79
+ width: { type: "number" },
80
+ height: { type: "number" },
81
+ backgroundColor: { type: "string" }
82
+ },
83
+ required: ["filePath", "nodeId"]
84
+ }
85
+ },
86
+ {
87
+ name: "add_layer",
88
+ description: "Add a new layer to the diagram",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ filePath: { type: "string" },
93
+ name: { type: "string" },
94
+ position: {
95
+ type: "object",
96
+ description: "Optional. If omitted, stacks 2 units below bottom-most layer (matching app behavior).",
97
+ properties: {
98
+ x: { type: "number" },
99
+ y: { type: "number" },
100
+ z: { type: "number" }
101
+ }
102
+ },
103
+ rotation: {
104
+ type: "object",
105
+ description: "Optional. Rotation in degrees.",
106
+ properties: {
107
+ x: { type: "number" },
108
+ y: { type: "number" },
109
+ z: { type: "number" }
110
+ }
111
+ },
112
+ visible: { type: "boolean" }
113
+ },
114
+ required: ["filePath", "name"]
115
+ }
116
+ },
117
+ {
118
+ name: "add_directional_node",
119
+ description: "Add a new node relative to an existing node and connect them. PRIMARY method for creating diagrams.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ filePath: { type: "string" },
124
+ sourceNodeId: { type: "string" },
125
+ direction: { type: "string", enum: ["UP", "DOWN", "LEFT", "RIGHT"], description: "Direction to place the new node relative to source." },
126
+ label: { type: "string" },
127
+ shape: { type: "string", enum: ["rectangle", "rounded", "ellipse", "diamond", "actor", "cylinder", "note", "cloud", "class", "text", "table"] },
128
+ width: { type: "number", description: "Default: 2. Min: 0.1, Max: 50" },
129
+ height: { type: "number", description: "Default: 1.5. Min: 0.1, Max: 50" },
130
+ color: { type: "string" },
131
+ edgeLabel: { type: "string", description: "Optional label for the connecting edge" }
132
+ },
133
+ required: ["filePath", "sourceNodeId", "direction", "label"]
134
+ }
135
+ }
136
+ ];
137
+ async function handleToolCall(name, args) {
138
+ switch (name) {
139
+ case "create_new_diagram": {
140
+ // ... (existing code)
141
+ const defaultLayerId = (0, crypto_1.randomUUID)();
142
+ const initialState = {
143
+ nodes: [],
144
+ edges: [],
145
+ layers: [{
146
+ id: defaultLayerId,
147
+ name: "Layer 1",
148
+ transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
149
+ visible: true
150
+ }],
151
+ activeLayerId: defaultLayerId,
152
+ gridSnappingEnabled: true,
153
+ gridSize: 0.5
154
+ };
155
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, initialState);
156
+ return { content: [{ type: "text", text: `Created new diagram at ${args.filePath}` }] };
157
+ }
158
+ case "read_diagram_structure": {
159
+ // ... (existing code)
160
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: JSON.stringify({
165
+ nodeCount: state.nodes.length,
166
+ edgeCount: state.edges.length,
167
+ layerCount: state.layers.length,
168
+ nodes: state.nodes.map(n => ({ id: n.id, label: n.label, shape: n.shape, x: n.x, y: n.y, z: n.z, layerId: n.layerId })),
169
+ edges: state.edges.map(e => ({ source: e.sourceId, target: e.targetId })),
170
+ layers: state.layers.map(l => ({ id: l.id, name: l.name, position: l.transform.position }))
171
+ }, null, 2)
172
+ }]
173
+ };
174
+ }
175
+ case "add_node": {
176
+ // ... (existing code)
177
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
178
+ // Auto-placement Logic
179
+ let x = 0;
180
+ let z = 0;
181
+ if (args.x !== undefined) {
182
+ x = Number(args.x);
183
+ }
184
+ if (args.y !== undefined) {
185
+ z = Number(args.y);
186
+ }
187
+ // If x was NOT provided, calculate it based on previous nodes
188
+ if (args.x === undefined) {
189
+ const targetLayerId = args.layerId || state.activeLayerId;
190
+ const layerNodes = state.nodes.filter(n => n.layerId === targetLayerId);
191
+ if (layerNodes.length > 0) {
192
+ const lastNode = layerNodes[layerNodes.length - 1];
193
+ // Place 2 units to the right of the last node
194
+ // Assuming standard width is ~2, so gap of 2 makes center-to-center ~4
195
+ x = lastNode.x + 5;
196
+ // If z (input y) is also not provided, align with last node
197
+ if (args.y === undefined) {
198
+ z = lastNode.z;
199
+ }
200
+ }
201
+ }
202
+ const newNode = {
203
+ id: (0, crypto_1.randomUUID)(),
204
+ x: x,
205
+ y: 0.05,
206
+ z: z,
207
+ width: Math.max(0.1, Math.min(50, args.width ? Number(args.width) : 2)),
208
+ height: Math.max(0.1, Math.min(50, args.height ? Number(args.height) : 1.5)),
209
+ label: args.label,
210
+ shape: args.shape || "rectangle",
211
+ layerId: args.layerId || state.activeLayerId,
212
+ backgroundColor: args.color,
213
+ textAlignVertical: 'center',
214
+ textAlignHorizontal: 'center'
215
+ };
216
+ console.error(`DEBUG: Adding node. Args: x=${args.x}, y=${args.y}. Computed: x=${newNode.x}, z=${newNode.z}`);
217
+ state.nodes.push(newNode);
218
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
219
+ return { content: [{ type: "text", text: `Added node ${newNode.id} at (${newNode.x}, ${newNode.z})` }] };
220
+ }
221
+ case "add_directional_node": {
222
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
223
+ const sourceNode = state.nodes.find(n => n.id === args.sourceNodeId);
224
+ if (!sourceNode) {
225
+ throw new Error(`Source node with ID ${args.sourceNodeId} not found.`);
226
+ }
227
+ const newNodeId = (0, crypto_1.randomUUID)();
228
+ // 1. Calculate base position with tighter spacing (matching App interaction)
229
+ const INITIAL_SPACING = 2.0;
230
+ let dirX = 0;
231
+ let dirZ = 0;
232
+ // sourceNode dimensions (safe defaults)
233
+ const srcW = Number(sourceNode.width) || 2;
234
+ const srcH = Number(sourceNode.height) || 1.5;
235
+ // Define Connection Points (Standard 0-3 based on direction)
236
+ let sourcePoint = 0;
237
+ let targetPoint = 0;
238
+ switch (args.direction) {
239
+ case "RIGHT":
240
+ dirX = 1;
241
+ sourcePoint = 1;
242
+ targetPoint = 3;
243
+ break;
244
+ case "LEFT":
245
+ dirX = -1;
246
+ sourcePoint = 3;
247
+ targetPoint = 1;
248
+ break;
249
+ case "DOWN":
250
+ dirZ = 1;
251
+ sourcePoint = 2;
252
+ targetPoint = 0;
253
+ break;
254
+ case "UP":
255
+ dirZ = -1;
256
+ sourcePoint = 0;
257
+ targetPoint = 2;
258
+ break;
259
+ default:
260
+ throw new Error(`Invalid direction: ${args.direction}`);
261
+ }
262
+ let newX = sourceNode.x + (dirX * (srcW + INITIAL_SPACING));
263
+ let newZ = sourceNode.z + (dirZ * (srcH + INITIAL_SPACING));
264
+ // 2. Collision Avoidance (Shift if overlapping)
265
+ const COLLISION_SPACING = 0.5;
266
+ const shiftStepX = srcW + COLLISION_SPACING;
267
+ const shiftStepZ = srcH + COLLISION_SPACING;
268
+ let iterations = 0;
269
+ const maxIterations = 50;
270
+ // New node dimensions (using input or default to source size for collision check approx)
271
+ const newW = args.width ? Number(args.width) : srcW;
272
+ const newH = args.height ? Number(args.height) : srcH;
273
+ const checkCollision = (cx, cz) => {
274
+ return state.nodes.some(n => {
275
+ if (n.layerId !== sourceNode.layerId)
276
+ return false;
277
+ const dx = Math.abs(n.x - cx);
278
+ const dz = Math.abs(n.z - cz); // z is y in 2D logic
279
+ const otherW = Number(n.width) || 2;
280
+ const otherH = Number(n.height) || 1.5;
281
+ const combinedHalfWidth = (otherW + newW) / 2;
282
+ const combinedHalfHeight = (otherH + newH) / 2;
283
+ // Box overlap check
284
+ return dx < combinedHalfWidth && dz < combinedHalfHeight;
285
+ });
286
+ };
287
+ while (checkCollision(newX, newZ) && iterations < maxIterations) {
288
+ iterations++;
289
+ if (dirX !== 0) {
290
+ // Moving Horizontally -> Shift Down
291
+ newZ += shiftStepZ;
292
+ }
293
+ else {
294
+ // Moving Vertically -> Shift Right
295
+ newX += shiftStepX;
296
+ }
297
+ }
298
+ const newNode = {
299
+ id: newNodeId,
300
+ x: newX,
301
+ y: sourceNode.y, // Same height/layer-level
302
+ z: newZ,
303
+ width: Math.max(0.1, Math.min(50, args.width ? Number(args.width) : 2)),
304
+ height: Math.max(0.1, Math.min(50, args.height ? Number(args.height) : 1.5)),
305
+ label: args.label,
306
+ shape: args.shape || "rectangle",
307
+ layerId: sourceNode.layerId, // Inherit layer
308
+ backgroundColor: args.color || sourceNode.backgroundColor, // Inherit or new
309
+ textAlignVertical: 'center',
310
+ textAlignHorizontal: 'center'
311
+ };
312
+ const newEdge = {
313
+ id: (0, crypto_1.randomUUID)(),
314
+ sourceId: sourceNode.id,
315
+ targetId: newNodeId,
316
+ sourcePointIndex: sourcePoint,
317
+ targetPointIndex: targetPoint,
318
+ label: args.edgeLabel,
319
+ style: 'line',
320
+ routingType: 'straight',
321
+ color: '#000000',
322
+ thickness: 0.01,
323
+ fontSize: 20,
324
+ borderStyle: 'solid'
325
+ };
326
+ state.nodes.push(newNode);
327
+ state.edges.push(newEdge);
328
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
329
+ return { content: [{ type: "text", text: `Added node '${args.label}' to the ${args.direction} of '${sourceNode.label}' and connected them.` }] };
330
+ }
331
+ case "add_edge": {
332
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
333
+ // Verify nodes exist
334
+ const sourceExists = state.nodes.find(n => n.id === args.sourceId);
335
+ const targetExists = state.nodes.find(n => n.id === args.targetId);
336
+ if (!sourceExists)
337
+ throw new Error(`Source node ${args.sourceId} not found`);
338
+ if (!targetExists)
339
+ throw new Error(`Target node ${args.targetId} not found`);
340
+ const newEdge = {
341
+ id: (0, crypto_1.randomUUID)(),
342
+ sourceId: args.sourceId,
343
+ targetId: args.targetId,
344
+ sourcePointIndex: Number(args.sourcePointIndex),
345
+ targetPointIndex: Number(args.targetPointIndex),
346
+ label: args.label,
347
+ style: 'line', // default
348
+ routingType: 'straight', // default
349
+ color: args.color || '#000000',
350
+ thickness: Math.max(0.005, Math.min(0.5, args.thickness ? Number(args.thickness) : 0.01)),
351
+ fontSize: 20,
352
+ borderStyle: 'solid'
353
+ };
354
+ state.edges.push(newEdge);
355
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
356
+ return { content: [{ type: "text", text: `Added edge ${newEdge.id}` }] };
357
+ }
358
+ case "update_node": {
359
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
360
+ const nodeIndex = state.nodes.findIndex(n => n.id === args.nodeId);
361
+ if (nodeIndex === -1) {
362
+ throw new Error(`Node ${args.nodeId} not found`);
363
+ }
364
+ if (args.label)
365
+ state.nodes[nodeIndex].label = args.label;
366
+ if (args.x !== undefined)
367
+ state.nodes[nodeIndex].x = Number(args.x);
368
+ if (args.y !== undefined)
369
+ state.nodes[nodeIndex].z = Number(args.y); // Map input y to z
370
+ if (args.width !== undefined)
371
+ state.nodes[nodeIndex].width = Number(args.width);
372
+ if (args.height !== undefined)
373
+ state.nodes[nodeIndex].height = Number(args.height);
374
+ if (args.backgroundColor)
375
+ state.nodes[nodeIndex].backgroundColor = args.backgroundColor;
376
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
377
+ return { content: [{ type: "text", text: `Updated node ${args.nodeId}` }] };
378
+ }
379
+ case "add_layer": {
380
+ const state = await (0, fileHandler_1.readDiagramFile)(args.filePath);
381
+ const newLayerId = (0, crypto_1.randomUUID)();
382
+ // Auto-positioning logic
383
+ let position = { x: 0, y: 0, z: 0 };
384
+ if (args.position) {
385
+ position = {
386
+ x: args.position.x ? Number(args.position.x) : 0,
387
+ y: args.position.y ? Number(args.position.y) : 0,
388
+ z: args.position.z ? Number(args.position.z) : 0
389
+ };
390
+ }
391
+ else {
392
+ // Find min Y layer (stacking downwards like the app)
393
+ let minY = 0;
394
+ for (const l of state.layers) {
395
+ if (l.transform?.position?.y < minY) {
396
+ minY = l.transform.position.y;
397
+ }
398
+ }
399
+ // App uses ~1.67, we use 2 for clean numbers
400
+ // If there are no layers or only Layer 1 at 0, next is -2
401
+ if (state.layers.length > 0) {
402
+ position.y = minY - 2;
403
+ }
404
+ }
405
+ const newLayer = {
406
+ id: newLayerId,
407
+ name: args.name,
408
+ transform: {
409
+ position: position,
410
+ rotation: {
411
+ x: args.rotation?.x ? Number(args.rotation.x) : 0,
412
+ y: args.rotation?.y ? Number(args.rotation.y) : 0,
413
+ z: args.rotation?.z ? Number(args.rotation.z) : 0
414
+ },
415
+ scale: { x: 1, y: 1, z: 1 }
416
+ },
417
+ visible: args.visible !== false
418
+ };
419
+ state.layers.push(newLayer);
420
+ await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
421
+ return { content: [{ type: "text", text: `Added layer '${args.name}' (ID: ${newLayerId}) at Y=${position.y}` }] };
422
+ }
423
+ default:
424
+ throw new Error(`Unknown tool: ${name}`);
425
+ }
426
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readDiagramFile = readDiagramFile;
7
+ exports.saveDiagramFile = saveDiagramFile;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const jszip_1 = __importDefault(require("jszip"));
10
+ async function readDiagramFile(filePath) {
11
+ try {
12
+ const fileContent = await promises_1.default.readFile(filePath);
13
+ const zip = await jszip_1.default.loadAsync(fileContent);
14
+ const diagramFile = zip.file("diagram.json");
15
+ if (!diagramFile) {
16
+ throw new Error("Invalid .DoDraw file: diagram.json missing");
17
+ }
18
+ const diagramJson = await diagramFile.async("string");
19
+ return JSON.parse(diagramJson);
20
+ }
21
+ catch (error) {
22
+ if (error.code === 'ENOENT') {
23
+ throw new Error(`File not found: ${filePath}`);
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ async function saveDiagramFile(filePath, state) {
29
+ let zip;
30
+ // Try to load existing zip to preserve other files (e.g. images)
31
+ try {
32
+ const fileContent = await promises_1.default.readFile(filePath);
33
+ zip = await jszip_1.default.loadAsync(fileContent);
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT') {
37
+ // Create new zip if file doesn't exist
38
+ zip = new jszip_1.default();
39
+ }
40
+ else {
41
+ throw error;
42
+ }
43
+ }
44
+ // Update diagram.json
45
+ zip.file("diagram.json", JSON.stringify(state, null, 2));
46
+ // Write back to file
47
+ const content = await zip.generateAsync({
48
+ type: "nodebuffer",
49
+ mimeType: "application/zip",
50
+ compression: "DEFLATE"
51
+ });
52
+ await promises_1.default.writeFile(filePath, content);
53
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
7
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
8
+ const path_1 = __importDefault(require("path"));
9
+ const process_1 = __importDefault(require("process"));
10
+ async function main() {
11
+ console.log("Connecting to server...");
12
+ const transport = new stdio_js_1.StdioClientTransport({
13
+ command: "node",
14
+ args: [path_1.default.join(process_1.default.cwd(), "dist", "src", "index.js")],
15
+ });
16
+ const client = new index_js_1.Client({ name: "dodraw-test-client", version: "1.0.0" }, { capabilities: {} });
17
+ await client.connect(transport);
18
+ console.log("Connected.");
19
+ try {
20
+ const filePath = path_1.default.join(process_1.default.cwd(), "test_output", "test_autoplacement.3duml");
21
+ await client.callTool({ name: "create_new_diagram", arguments: { filePath } });
22
+ // 1. Add First Node (auto) -> Should be 0,0
23
+ console.log("Adding Node A (auto)...");
24
+ await client.callTool({
25
+ name: "add_node",
26
+ arguments: { filePath, label: "Node A" }
27
+ });
28
+ // 2. Add Second Node (auto) -> Should be 5,0
29
+ console.log("Adding Node B (auto)...");
30
+ await client.callTool({
31
+ name: "add_node",
32
+ arguments: { filePath, label: "Node B" }
33
+ });
34
+ // 3. Add Third Node (explicit) -> 10, 10
35
+ console.log("Adding Node C (10, 10)...");
36
+ await client.callTool({
37
+ name: "add_node",
38
+ arguments: { filePath, label: "Node C", x: 10, y: 10 }
39
+ });
40
+ // 4. Add Fourth Node (auto) -> Should be 15, 10 (aligns with C)
41
+ console.log("Adding Node D (auto)...");
42
+ await client.callTool({
43
+ name: "add_node",
44
+ arguments: { filePath, label: "Node D" }
45
+ });
46
+ // Read and Verify
47
+ const result = await client.callTool({ name: "read_diagram_structure", arguments: { filePath } });
48
+ const structure = JSON.parse(result.content[0].text);
49
+ console.log("Verification Results:");
50
+ structure.nodes.forEach((n) => {
51
+ console.log(`Node ${n.label}: x=${n.x}, z=${n.y} (internal z)`); // n.y in JSON is z in source
52
+ });
53
+ // Assertions
54
+ const nodeA = structure.nodes.find((n) => n.label === "Node A");
55
+ const nodeB = structure.nodes.find((n) => n.label === "Node B");
56
+ const nodeC = structure.nodes.find((n) => n.label === "Node C");
57
+ const nodeD = structure.nodes.find((n) => n.label === "Node D");
58
+ if (nodeA.x !== 0)
59
+ throw new Error("Node A should be at x=0");
60
+ if (nodeB.x !== 5)
61
+ throw new Error("Node B should be at x=5");
62
+ if (nodeC.x !== 10)
63
+ throw new Error("Node C should be at x=10");
64
+ if (nodeD.x !== 15)
65
+ throw new Error("Node D should be at x=15");
66
+ if (nodeD.y !== 10)
67
+ throw new Error("Node D should align with Node C (z=10)"); // internal y=z
68
+ console.log("SUCCESS: Auto-placement logic verified.");
69
+ }
70
+ catch (error) {
71
+ console.error("Test failed Details:");
72
+ console.error(" Message:", error.message);
73
+ console.error(" Stack:", error.stack);
74
+ if (error.code)
75
+ console.error(" Code:", error.code);
76
+ if (error.data)
77
+ console.error(" Data:", JSON.stringify(error.data, null, 2));
78
+ }
79
+ finally {
80
+ await client.close();
81
+ }
82
+ }
83
+ main();