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.
- package/README.md +63 -0
- package/dist/src/index.js +45 -0
- package/dist/src/tools/diagramTools.js +426 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/fileHandler.js +53 -0
- package/dist/test/test-auto-placement.js +83 -0
- package/dist/test/test-client.js +100 -0
- package/dist/test/test-collision.js +84 -0
- package/dist/test/test-constraints.js +88 -0
- package/dist/test/test-debug.js +41 -0
- package/dist/test/test-directional.js +90 -0
- package/dist/test/test-layer-support.js +69 -0
- package/dist/test/test-refined-spacing.js +58 -0
- package/dist/test/verify_types.js +29 -0
- package/package.json +24 -0
- package/src/index.ts +54 -0
- package/src/tools/diagramTools.ts +440 -0
- package/src/types.ts +78 -0
- package/src/utils/fileHandler.ts +51 -0
- package/test/test-auto-placement.ts +88 -0
- package/test/test-client.ts +116 -0
- package/test/test-collision.ts +93 -0
- package/test/test-constraints.ts +95 -0
- package/test/test-debug.ts +40 -0
- package/test/test-directional.ts +95 -0
- package/test/test-layer-support.ts +77 -0
- package/test/test-refined-spacing.ts +62 -0
- package/test/verify_types.ts +28 -0
- package/test_output/test_autoplacement.3duml +0 -0
- package/test_output/test_collision.3duml +0 -0
- package/test_output/test_constraints.3duml +0 -0
- package/test_output/test_debug.3duml +0 -0
- package/test_output/test_diagram.3duml +0 -0
- package/test_output/test_directional.3duml +0 -0
- package/test_output/test_layers.3duml +0 -0
- package/test_output/test_refined_spacing.3duml +0 -0
- 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,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();
|