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
@@ -0,0 +1,440 @@
1
+
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import { readDiagramFile, saveDiagramFile } from "../utils/fileHandler";
4
+ import { DiagramState, NodeData, EdgeData } from "../types";
5
+ import { randomUUID } from "crypto";
6
+
7
+ export const toolDefinitions: Tool[] = [
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
+
138
+ export async function handleToolCall(name: string, args: any): Promise<any> {
139
+ switch (name) {
140
+ case "create_new_diagram": {
141
+ // ... (existing code)
142
+ const defaultLayerId = randomUUID();
143
+ const initialState: DiagramState = {
144
+ nodes: [],
145
+ edges: [],
146
+ layers: [{
147
+ id: defaultLayerId,
148
+ name: "Layer 1",
149
+ transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
150
+ visible: true
151
+ }],
152
+ activeLayerId: defaultLayerId,
153
+ gridSnappingEnabled: true,
154
+ gridSize: 0.5
155
+ };
156
+ await saveDiagramFile(args.filePath, initialState);
157
+ return { content: [{ type: "text", text: `Created new diagram at ${args.filePath}` }] };
158
+ }
159
+ case "read_diagram_structure": {
160
+ // ... (existing code)
161
+ const state = await readDiagramFile(args.filePath);
162
+ return {
163
+ content: [{
164
+ type: "text",
165
+ text: JSON.stringify({
166
+ nodeCount: state.nodes.length,
167
+ edgeCount: state.edges.length,
168
+ layerCount: state.layers.length,
169
+ 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 })),
170
+ edges: state.edges.map(e => ({ source: e.sourceId, target: e.targetId })),
171
+ layers: state.layers.map(l => ({ id: l.id, name: l.name, position: l.transform.position }))
172
+ }, null, 2)
173
+ }]
174
+ };
175
+ }
176
+ case "add_node": {
177
+ // ... (existing code)
178
+ const state = await readDiagramFile(args.filePath);
179
+
180
+ // Auto-placement Logic
181
+ let x = 0;
182
+ let z = 0;
183
+
184
+ if (args.x !== undefined) {
185
+ x = Number(args.x);
186
+ }
187
+ if (args.y !== undefined) {
188
+ z = Number(args.y);
189
+ }
190
+
191
+ // If x was NOT provided, calculate it based on previous nodes
192
+ if (args.x === undefined) {
193
+ const targetLayerId = args.layerId || state.activeLayerId;
194
+ const layerNodes = state.nodes.filter(n => n.layerId === targetLayerId);
195
+ if (layerNodes.length > 0) {
196
+ const lastNode = layerNodes[layerNodes.length - 1];
197
+ // Place 2 units to the right of the last node
198
+ // Assuming standard width is ~2, so gap of 2 makes center-to-center ~4
199
+ x = lastNode.x + 5;
200
+ // If z (input y) is also not provided, align with last node
201
+ if (args.y === undefined) {
202
+ z = lastNode.z;
203
+ }
204
+ }
205
+ }
206
+
207
+ const newNode: NodeData = {
208
+ id: randomUUID(),
209
+ x: x,
210
+ y: 0.05,
211
+ z: z,
212
+ width: Math.max(0.1, Math.min(50, args.width ? Number(args.width) : 2)),
213
+ height: Math.max(0.1, Math.min(50, args.height ? Number(args.height) : 1.5)),
214
+ label: args.label,
215
+ shape: args.shape || "rectangle",
216
+ layerId: args.layerId || state.activeLayerId,
217
+ backgroundColor: args.color,
218
+ textAlignVertical: 'center',
219
+ textAlignHorizontal: 'center'
220
+ };
221
+
222
+ console.error(`DEBUG: Adding node. Args: x=${args.x}, y=${args.y}. Computed: x=${newNode.x}, z=${newNode.z}`);
223
+ state.nodes.push(newNode);
224
+ await saveDiagramFile(args.filePath, state);
225
+ return { content: [{ type: "text", text: `Added node ${newNode.id} at (${newNode.x}, ${newNode.z})` }] };
226
+ }
227
+ case "add_directional_node": {
228
+ const state = await readDiagramFile(args.filePath);
229
+ const sourceNode = state.nodes.find(n => n.id === args.sourceNodeId);
230
+
231
+ if (!sourceNode) {
232
+ throw new Error(`Source node with ID ${args.sourceNodeId} not found.`);
233
+ }
234
+
235
+ const newNodeId = randomUUID();
236
+
237
+ // 1. Calculate base position with tighter spacing (matching App interaction)
238
+ const INITIAL_SPACING = 2.0;
239
+ let dirX = 0;
240
+ let dirZ = 0;
241
+
242
+ // sourceNode dimensions (safe defaults)
243
+ const srcW = Number(sourceNode.width) || 2;
244
+ const srcH = Number(sourceNode.height) || 1.5;
245
+
246
+ // Define Connection Points (Standard 0-3 based on direction)
247
+ let sourcePoint = 0;
248
+ let targetPoint = 0;
249
+
250
+ switch (args.direction) {
251
+ case "RIGHT":
252
+ dirX = 1;
253
+ sourcePoint = 1; targetPoint = 3;
254
+ break;
255
+ case "LEFT":
256
+ dirX = -1;
257
+ sourcePoint = 3; targetPoint = 1;
258
+ break;
259
+ case "DOWN":
260
+ dirZ = 1;
261
+ sourcePoint = 2; targetPoint = 0;
262
+ break;
263
+ case "UP":
264
+ dirZ = -1;
265
+ sourcePoint = 0; targetPoint = 2;
266
+ break;
267
+ default:
268
+ throw new Error(`Invalid direction: ${args.direction}`);
269
+ }
270
+
271
+ let newX = sourceNode.x + (dirX * (srcW + INITIAL_SPACING));
272
+ let newZ = sourceNode.z + (dirZ * (srcH + INITIAL_SPACING));
273
+
274
+ // 2. Collision Avoidance (Shift if overlapping)
275
+ const COLLISION_SPACING = 0.5;
276
+ const shiftStepX = srcW + COLLISION_SPACING;
277
+ const shiftStepZ = srcH + COLLISION_SPACING;
278
+
279
+ let iterations = 0;
280
+ const maxIterations = 50;
281
+
282
+ // New node dimensions (using input or default to source size for collision check approx)
283
+ const newW = args.width ? Number(args.width) : srcW;
284
+ const newH = args.height ? Number(args.height) : srcH;
285
+
286
+ const checkCollision = (cx: number, cz: number) => {
287
+ return state.nodes.some(n => {
288
+ if (n.layerId !== sourceNode.layerId) return false;
289
+ const dx = Math.abs(n.x - cx);
290
+ const dz = Math.abs(n.z - cz); // z is y in 2D logic
291
+ const otherW = Number(n.width) || 2;
292
+ const otherH = Number(n.height) || 1.5;
293
+
294
+ const combinedHalfWidth = (otherW + newW) / 2;
295
+ const combinedHalfHeight = (otherH + newH) / 2;
296
+
297
+ // Box overlap check
298
+ return dx < combinedHalfWidth && dz < combinedHalfHeight;
299
+ });
300
+ };
301
+
302
+ while (checkCollision(newX, newZ) && iterations < maxIterations) {
303
+ iterations++;
304
+ if (dirX !== 0) {
305
+ // Moving Horizontally -> Shift Down
306
+ newZ += shiftStepZ;
307
+ } else {
308
+ // Moving Vertically -> Shift Right
309
+ newX += shiftStepX;
310
+ }
311
+ }
312
+
313
+ const newNode: NodeData = {
314
+ id: newNodeId,
315
+ x: newX,
316
+ y: sourceNode.y, // Same height/layer-level
317
+ z: newZ,
318
+ width: Math.max(0.1, Math.min(50, args.width ? Number(args.width) : 2)),
319
+ height: Math.max(0.1, Math.min(50, args.height ? Number(args.height) : 1.5)),
320
+ label: args.label,
321
+ shape: args.shape || "rectangle",
322
+ layerId: sourceNode.layerId, // Inherit layer
323
+ backgroundColor: args.color || sourceNode.backgroundColor, // Inherit or new
324
+ textAlignVertical: 'center',
325
+ textAlignHorizontal: 'center'
326
+ };
327
+
328
+ const newEdge: EdgeData = {
329
+ id: randomUUID(),
330
+ sourceId: sourceNode.id,
331
+ targetId: newNodeId,
332
+ sourcePointIndex: sourcePoint,
333
+ targetPointIndex: targetPoint,
334
+ label: args.edgeLabel,
335
+ style: 'line',
336
+ routingType: 'straight',
337
+ color: '#000000',
338
+ thickness: 0.01,
339
+ fontSize: 20,
340
+ borderStyle: 'solid'
341
+ };
342
+
343
+ state.nodes.push(newNode);
344
+ state.edges.push(newEdge);
345
+
346
+ await saveDiagramFile(args.filePath, state);
347
+ return { content: [{ type: "text", text: `Added node '${args.label}' to the ${args.direction} of '${sourceNode.label}' and connected them.` }] };
348
+ }
349
+ case "add_edge": {
350
+ const state = await readDiagramFile(args.filePath);
351
+ // Verify nodes exist
352
+ const sourceExists = state.nodes.find(n => n.id === args.sourceId);
353
+ const targetExists = state.nodes.find(n => n.id === args.targetId);
354
+ if (!sourceExists) throw new Error(`Source node ${args.sourceId} not found`);
355
+ if (!targetExists) throw new Error(`Target node ${args.targetId} not found`);
356
+
357
+ const newEdge: EdgeData = {
358
+ id: randomUUID(),
359
+ sourceId: args.sourceId,
360
+ targetId: args.targetId,
361
+ sourcePointIndex: Number(args.sourcePointIndex),
362
+ targetPointIndex: Number(args.targetPointIndex),
363
+ label: args.label,
364
+ style: 'line', // default
365
+ routingType: 'straight', // default
366
+ color: args.color || '#000000',
367
+ thickness: Math.max(0.005, Math.min(0.5, args.thickness ? Number(args.thickness) : 0.01)),
368
+ fontSize: 20,
369
+ borderStyle: 'solid'
370
+ };
371
+ state.edges.push(newEdge);
372
+ await saveDiagramFile(args.filePath, state);
373
+ return { content: [{ type: "text", text: `Added edge ${newEdge.id}` }] };
374
+ }
375
+ case "update_node": {
376
+ const state = await readDiagramFile(args.filePath);
377
+ const nodeIndex = state.nodes.findIndex(n => n.id === args.nodeId);
378
+ if (nodeIndex === -1) {
379
+ throw new Error(`Node ${args.nodeId} not found`);
380
+ }
381
+ if (args.label) state.nodes[nodeIndex].label = args.label;
382
+ if (args.x !== undefined) state.nodes[nodeIndex].x = Number(args.x);
383
+ if (args.y !== undefined) state.nodes[nodeIndex].z = Number(args.y); // Map input y to z
384
+ if (args.width !== undefined) state.nodes[nodeIndex].width = Number(args.width);
385
+ if (args.height !== undefined) state.nodes[nodeIndex].height = Number(args.height);
386
+ if (args.backgroundColor) state.nodes[nodeIndex].backgroundColor = args.backgroundColor;
387
+
388
+ await saveDiagramFile(args.filePath, state);
389
+ return { content: [{ type: "text", text: `Updated node ${args.nodeId}` }] };
390
+ }
391
+ case "add_layer": {
392
+ const state = await readDiagramFile(args.filePath);
393
+ const newLayerId = randomUUID();
394
+
395
+ // Auto-positioning logic
396
+ let position = { x: 0, y: 0, z: 0 };
397
+ if (args.position) {
398
+ position = {
399
+ x: args.position.x ? Number(args.position.x) : 0,
400
+ y: args.position.y ? Number(args.position.y) : 0,
401
+ z: args.position.z ? Number(args.position.z) : 0
402
+ };
403
+ } else {
404
+ // Find min Y layer (stacking downwards like the app)
405
+ let minY = 0;
406
+ for (const l of state.layers) {
407
+ if (l.transform?.position?.y < minY) {
408
+ minY = l.transform.position.y;
409
+ }
410
+ }
411
+ // App uses ~1.67, we use 2 for clean numbers
412
+ // If there are no layers or only Layer 1 at 0, next is -2
413
+ if (state.layers.length > 0) {
414
+ position.y = minY - 2;
415
+ }
416
+ }
417
+
418
+ const newLayer: any = {
419
+ id: newLayerId,
420
+ name: args.name,
421
+ transform: {
422
+ position: position,
423
+ rotation: {
424
+ x: args.rotation?.x ? Number(args.rotation.x) : 0,
425
+ y: args.rotation?.y ? Number(args.rotation.y) : 0,
426
+ z: args.rotation?.z ? Number(args.rotation.z) : 0
427
+ },
428
+ scale: { x: 1, y: 1, z: 1 }
429
+ },
430
+ visible: args.visible !== false
431
+ };
432
+ state.layers.push(newLayer);
433
+
434
+ await saveDiagramFile(args.filePath, state);
435
+ return { content: [{ type: "text", text: `Added layer '${args.name}' (ID: ${newLayerId}) at Y=${position.y}` }] };
436
+ }
437
+ default:
438
+ throw new Error(`Unknown tool: ${name}`);
439
+ }
440
+ }
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+
2
+ export interface NodeData {
3
+ id: string;
4
+ x: number;
5
+ y: number;
6
+ z: number;
7
+ width: number;
8
+ height: number;
9
+ layerId: string;
10
+ label: string;
11
+ shape?: 'rectangle' | 'rounded' | 'ellipse' | 'diamond' | 'actor' | 'cylinder' | 'note' | 'cloud' | 'class' | 'text' | 'table';
12
+ fontSize?: number;
13
+ borderColor?: string;
14
+ borderWidth?: number;
15
+ borderStyle?: 'solid' | 'dashed' | 'dotted';
16
+ backgroundColor?: string;
17
+ opacity?: number;
18
+ textAlignVertical?: 'top' | 'center' | 'bottom';
19
+ textAlignHorizontal?: 'left' | 'center' | 'right';
20
+ parentId?: string;
21
+ imageUrl?: string;
22
+ isBold?: boolean;
23
+ isItalic?: boolean;
24
+ isUnderline?: boolean;
25
+ isStrikethrough?: boolean;
26
+ isVerticalText?: boolean;
27
+ description?: string;
28
+ url?: string;
29
+ tableData?: {
30
+ columns: number;
31
+ rows: number;
32
+ hasRowHeader: boolean;
33
+ hasColumnHeader: boolean;
34
+ cells?: Record<string, string>;
35
+ };
36
+ }
37
+
38
+ export interface EdgeData {
39
+ id: string;
40
+ sourceId: string;
41
+ targetId: string;
42
+ sourcePointIndex: number;
43
+ targetPointIndex: number;
44
+ color?: string;
45
+ label?: string;
46
+ style?: 'line' | 'arrow-source' | 'arrow-target' | 'arrow-both';
47
+ fontSize?: number;
48
+ thickness?: number;
49
+ borderStyle?: 'solid' | 'dashed' | 'dotted';
50
+ description?: string;
51
+ routingType?: 'straight' | 'orthogonal';
52
+ controlPoints?: { x: number, y: number, z: number }[];
53
+ isBold?: boolean;
54
+ isItalic?: boolean;
55
+ isUnderline?: boolean;
56
+ isStrikethrough?: boolean;
57
+ }
58
+
59
+ export interface LayerData {
60
+ id: string;
61
+ name: string;
62
+ transform: {
63
+ position: { x: number; y: number; z: number };
64
+ rotation: { x: number; y: number; z: number };
65
+ scale: { x: number; y: number; z: number };
66
+ };
67
+ visible?: boolean;
68
+ }
69
+
70
+ export interface DiagramState {
71
+ nodes: NodeData[];
72
+ edges: EdgeData[];
73
+ layers: LayerData[];
74
+ activeLayerId: string;
75
+ gridSnappingEnabled: boolean;
76
+ gridSize: number;
77
+ [key: string]: any; // Allow other properties from Store state
78
+ }
@@ -0,0 +1,51 @@
1
+
2
+ import fs from 'fs/promises';
3
+ import JSZip from 'jszip';
4
+ import { DiagramState } from '../types';
5
+
6
+ export async function readDiagramFile(filePath: string): Promise<DiagramState> {
7
+ try {
8
+ const fileContent = await fs.readFile(filePath);
9
+ const zip = await JSZip.loadAsync(fileContent);
10
+ const diagramFile = zip.file("diagram.json");
11
+ if (!diagramFile) {
12
+ throw new Error("Invalid .DoDraw file: diagram.json missing");
13
+ }
14
+ const diagramJson = await diagramFile.async("string");
15
+ return JSON.parse(diagramJson) as DiagramState;
16
+ } catch (error: any) {
17
+ if (error.code === 'ENOENT') {
18
+ throw new Error(`File not found: ${filePath}`);
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ export async function saveDiagramFile(filePath: string, state: DiagramState): Promise<void> {
25
+ let zip: JSZip;
26
+
27
+ // Try to load existing zip to preserve other files (e.g. images)
28
+ try {
29
+ const fileContent = await fs.readFile(filePath);
30
+ zip = await JSZip.loadAsync(fileContent);
31
+ } catch (error: any) {
32
+ if (error.code === 'ENOENT') {
33
+ // Create new zip if file doesn't exist
34
+ zip = new JSZip();
35
+ } else {
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ // Update diagram.json
41
+ zip.file("diagram.json", JSON.stringify(state, null, 2));
42
+
43
+ // Write back to file
44
+ const content = await zip.generateAsync({
45
+ type: "nodebuffer",
46
+ mimeType: "application/zip",
47
+ compression: "DEFLATE"
48
+ });
49
+
50
+ await fs.writeFile(filePath, content);
51
+ }
@@ -0,0 +1,88 @@
1
+
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import path from "path";
5
+ import process from "process";
6
+
7
+ async function main() {
8
+ console.log("Connecting to server...");
9
+ const transport = new StdioClientTransport({
10
+ command: "node",
11
+ args: [path.join(process.cwd(), "dist", "src", "index.js")],
12
+ });
13
+
14
+ const client = new Client(
15
+ { name: "dodraw-test-client", version: "1.0.0" },
16
+ { capabilities: {} }
17
+ );
18
+
19
+ await client.connect(transport);
20
+ console.log("Connected.");
21
+
22
+ try {
23
+ const filePath = path.join(process.cwd(), "test_output", "test_autoplacement.3duml");
24
+ await client.callTool({ name: "create_new_diagram", arguments: { filePath } });
25
+
26
+ // 1. Add First Node (auto) -> Should be 0,0
27
+ console.log("Adding Node A (auto)...");
28
+ await client.callTool({
29
+ name: "add_node",
30
+ arguments: { filePath, label: "Node A" }
31
+ });
32
+
33
+ // 2. Add Second Node (auto) -> Should be 5,0
34
+ console.log("Adding Node B (auto)...");
35
+ await client.callTool({
36
+ name: "add_node",
37
+ arguments: { filePath, label: "Node B" }
38
+ });
39
+
40
+ // 3. Add Third Node (explicit) -> 10, 10
41
+ console.log("Adding Node C (10, 10)...");
42
+ await client.callTool({
43
+ name: "add_node",
44
+ arguments: { filePath, label: "Node C", x: 10, y: 10 }
45
+ });
46
+
47
+ // 4. Add Fourth Node (auto) -> Should be 15, 10 (aligns with C)
48
+ console.log("Adding Node D (auto)...");
49
+ await client.callTool({
50
+ name: "add_node",
51
+ arguments: { filePath, label: "Node D" }
52
+ });
53
+
54
+ // Read and Verify
55
+ const result = await client.callTool({ name: "read_diagram_structure", arguments: { filePath } }) as any;
56
+ const structure = JSON.parse(result.content[0].text);
57
+
58
+ console.log("Verification Results:");
59
+ structure.nodes.forEach((n: any) => {
60
+ console.log(`Node ${n.label}: x=${n.x}, z=${n.y} (internal z)`); // n.y in JSON is z in source
61
+ });
62
+
63
+ // Assertions
64
+ const nodeA = structure.nodes.find((n: any) => n.label === "Node A");
65
+ const nodeB = structure.nodes.find((n: any) => n.label === "Node B");
66
+ const nodeC = structure.nodes.find((n: any) => n.label === "Node C");
67
+ const nodeD = structure.nodes.find((n: any) => n.label === "Node D");
68
+
69
+ if (nodeA.x !== 0) throw new Error("Node A should be at x=0");
70
+ if (nodeB.x !== 5) throw new Error("Node B should be at x=5");
71
+ if (nodeC.x !== 10) throw new Error("Node C should be at x=10");
72
+ if (nodeD.x !== 15) throw new Error("Node D should be at x=15");
73
+ if (nodeD.y !== 10) throw new Error("Node D should align with Node C (z=10)"); // internal y=z
74
+
75
+ console.log("SUCCESS: Auto-placement logic verified.");
76
+
77
+ } catch (error: any) {
78
+ console.error("Test failed Details:");
79
+ console.error(" Message:", error.message);
80
+ console.error(" Stack:", error.stack);
81
+ if (error.code) console.error(" Code:", error.code);
82
+ if (error.data) console.error(" Data:", JSON.stringify(error.data, null, 2));
83
+ } finally {
84
+ await client.close();
85
+ }
86
+ }
87
+
88
+ main();