dodraw-mcp-server 0.1.4 → 0.1.6

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.
@@ -140,7 +140,7 @@ exports.toolDefinitions = [
140
140
  },
141
141
  {
142
142
  name: "add_directional_node",
143
- description: "Add a new node relative to an existing node and connect them. PRIMARY method for creating diagrams.",
143
+ description: "Add a new node relative to an existing node and connect them. PRIMARY method for creating diagrams, including Class and Database diagrams. Supports 'columns' for DB tables.",
144
144
  inputSchema: {
145
145
  type: "object",
146
146
  properties: {
@@ -154,14 +154,28 @@ exports.toolDefinitions = [
154
154
  color: { type: "string" },
155
155
  edgeLabel: { type: "string", description: "Optional label for the connecting edge" },
156
156
  attributes: { type: "array", items: { type: "string" }, description: "Optional list of attributes for Class nodes" },
157
- methods: { type: "array", items: { type: "string" }, description: "Optional list of methods for Class nodes" }
157
+ methods: { type: "array", items: { type: "string" }, description: "Optional list of methods for Class nodes" },
158
+ columns: {
159
+ type: "array",
160
+ description: "Optional list of columns for Table nodes",
161
+ items: {
162
+ type: "object",
163
+ properties: {
164
+ name: { type: "string" },
165
+ type: { type: "string" },
166
+ isPk: { type: "boolean" },
167
+ isFk: { type: "boolean" }
168
+ },
169
+ required: ["name", "type"]
170
+ }
171
+ }
158
172
  },
159
173
  required: ["filePath", "sourceNodeId", "direction", "label"]
160
174
  }
161
175
  },
162
176
  {
163
177
  name: "create_uml_class",
164
- description: "Create a UML Class node with methods and attributes",
178
+ description: "Create a UML Class node. Use this for the FIRST class. For connected classes, use 'add_directional_node' to ensure correct relative positioning.",
165
179
  inputSchema: {
166
180
  type: "object",
167
181
  properties: {
@@ -178,7 +192,7 @@ exports.toolDefinitions = [
178
192
  },
179
193
  {
180
194
  name: "create_db_table",
181
- description: "Create a Database Entity (ERD Table)",
195
+ description: "Create a Database Entity (ERD Table). Supports absolute positioning (x,y) OR relative positioning (referenceNodeId, direction). Can also auto-connect.",
182
196
  inputSchema: {
183
197
  type: "object",
184
198
  properties: {
@@ -199,7 +213,19 @@ exports.toolDefinitions = [
199
213
  },
200
214
  x: { type: "number" },
201
215
  y: { type: "number" },
202
- id: { type: "string", description: "Optional explicit ID" }
216
+ id: { type: "string", description: "Optional explicit ID" },
217
+ referenceNodeId: { type: "string", description: "Optional. ID of an existing node to place this new table relative to." },
218
+ direction: { type: "string", enum: ["UP", "DOWN", "LEFT", "RIGHT"], description: "Optional. Direction to place relative to referenceNodeId." },
219
+ connection: {
220
+ type: "object",
221
+ description: "Optional. If provided, creates an edge connecting the reference node to this new table.",
222
+ properties: {
223
+ type: { type: "string", enum: ["one-to-one", "one-to-many", "many-to-many"], description: "Relationship type" },
224
+ label: { type: "string" },
225
+ direction: { type: "string", enum: ["source-to-target", "target-to-source", "bi-directional"], description: "Edge direction flow" }
226
+ },
227
+ required: ["type"]
228
+ }
203
229
  },
204
230
  required: ["filePath", "tableName", "columns"]
205
231
  }
@@ -402,6 +428,14 @@ async function handleToolCall(name, args) {
402
428
  attributes: args.attributes || [],
403
429
  methods: args.methods || []
404
430
  } : undefined,
431
+ dbTableData: args.columns ? {
432
+ columns: args.columns.map((c) => ({
433
+ name: c.name,
434
+ type: c.type,
435
+ isPk: !!c.isPk,
436
+ isFk: !!c.isFk
437
+ }))
438
+ } : undefined,
405
439
  textAlignVertical: 'center',
406
440
  textAlignHorizontal: 'center'
407
441
  };
@@ -528,11 +562,19 @@ async function handleToolCall(name, args) {
528
562
  const methodCount = (args.methods || []).length;
529
563
  const estimatedHeight = 0.8 + (attrCount + methodCount) * 0.4;
530
564
  const height = Math.max(2, estimatedHeight);
565
+ // Auto-placement logic if X/Y not provided
566
+ let x = args.x !== undefined ? Number(args.x) : 0;
567
+ let z = args.y !== undefined ? Number(args.y) : 0;
568
+ if (args.x === undefined && args.y === undefined && state.nodes.length > 0) {
569
+ const lastNode = state.nodes[state.nodes.length - 1];
570
+ x = lastNode.x + 5;
571
+ z = lastNode.z;
572
+ }
531
573
  const newNode = {
532
574
  id: newNodeId,
533
- x: args.x ? Number(args.x) : 0,
575
+ x: x,
534
576
  y: 0,
535
- z: args.y ? Number(args.y) : 0,
577
+ z: z,
536
578
  width: width,
537
579
  height: height,
538
580
  label: args.className,
@@ -556,11 +598,74 @@ async function handleToolCall(name, args) {
556
598
  const estimatedHeight = 0.8 + cols.length * 0.4;
557
599
  const height = Math.max(2, estimatedHeight);
558
600
  const width = 3;
601
+ // Auto-placement logic if X/Y not provided
602
+ let x = args.x !== undefined ? Number(args.x) : 0;
603
+ let z = args.y !== undefined ? Number(args.y) : 0;
604
+ // Relative Placement Logic
605
+ if (args.referenceNodeId && args.direction) {
606
+ const sourceNode = state.nodes.find(n => n.id === args.referenceNodeId);
607
+ if (!sourceNode)
608
+ throw new Error(`Reference node ${args.referenceNodeId} not found`);
609
+ const INITIAL_SPACING = 2.0;
610
+ let dirX = 0;
611
+ let dirZ = 0;
612
+ const srcW = Number(sourceNode.width) || 2;
613
+ const srcH = Number(sourceNode.height) || 1.5;
614
+ switch (args.direction) {
615
+ case "RIGHT":
616
+ dirX = 1;
617
+ break;
618
+ case "LEFT":
619
+ dirX = -1;
620
+ break;
621
+ case "DOWN":
622
+ dirZ = 1;
623
+ break;
624
+ case "UP":
625
+ dirZ = -1;
626
+ break;
627
+ }
628
+ x = sourceNode.x + (dirX * (srcW + INITIAL_SPACING));
629
+ z = sourceNode.z + (dirZ * (srcH + INITIAL_SPACING));
630
+ // Collision Avoidance reuse
631
+ const COLLISION_SPACING = 0.5;
632
+ const shiftStepX = srcW + COLLISION_SPACING;
633
+ const shiftStepZ = srcH + COLLISION_SPACING;
634
+ const newW = width;
635
+ const newH = height;
636
+ const checkCollision = (cx, cz) => {
637
+ return state.nodes.some(n => {
638
+ if (n.layerId !== sourceNode.layerId)
639
+ return false;
640
+ const dx = Math.abs(n.x - cx);
641
+ const dz = Math.abs(n.z - cz);
642
+ const otherW = Number(n.width) || 2;
643
+ const otherH = Number(n.height) || 1.5;
644
+ const combinedHalfWidth = (otherW + newW) / 2;
645
+ const combinedHalfHeight = (otherH + newH) / 2;
646
+ return dx < combinedHalfWidth && dz < combinedHalfHeight;
647
+ });
648
+ };
649
+ let iterations = 0;
650
+ while (checkCollision(x, z) && iterations < 50) {
651
+ iterations++;
652
+ if (dirX !== 0)
653
+ z += shiftStepZ;
654
+ else
655
+ x += shiftStepX;
656
+ }
657
+ }
658
+ else if (args.x === undefined && args.y === undefined && state.nodes.length > 0) {
659
+ // Fallback to "next to last" if no explicit relative info
660
+ const lastNode = state.nodes[state.nodes.length - 1];
661
+ x = lastNode.x + 5;
662
+ z = lastNode.z;
663
+ }
559
664
  const newNode = {
560
665
  id: newNodeId,
561
- x: args.x ? Number(args.x) : 0,
666
+ x: x,
562
667
  y: 0,
563
- z: args.y ? Number(args.y) : 0,
668
+ z: z,
564
669
  width: width,
565
670
  height: height,
566
671
  label: args.tableName,
@@ -577,6 +682,71 @@ async function handleToolCall(name, args) {
577
682
  }
578
683
  };
579
684
  state.nodes.push(newNode);
685
+ // Auto-Connect logic
686
+ if (args.connection && args.referenceNodeId) {
687
+ let termStart = 'none';
688
+ let termEnd = 'none';
689
+ // Simplified mapping for DB
690
+ if (args.connection.type === 'one-to-many') {
691
+ // Source (Reference) -> Target (New)
692
+ // If Reference is "Users" (1) and New is "Orders" (N)
693
+ // termStart = one, termEnd = many
694
+ termStart = 'crows-one';
695
+ termEnd = 'crows-many';
696
+ }
697
+ else if (args.connection.type === 'one-to-one') {
698
+ termStart = 'crows-one';
699
+ termEnd = 'crows-one';
700
+ }
701
+ else if (args.connection.type === 'many-to-many') {
702
+ termStart = 'crows-many';
703
+ termEnd = 'crows-many';
704
+ }
705
+ // Points? Simple defaults based on direction or just center-ish
706
+ // Let's use the logic from add_directional_node for cleaner ports if possible
707
+ // But simple 0-3 logic:
708
+ let sp = 0;
709
+ let tp = 0;
710
+ switch (args.direction) {
711
+ case "RIGHT":
712
+ sp = 1;
713
+ tp = 3;
714
+ break;
715
+ case "LEFT":
716
+ sp = 3;
717
+ tp = 1;
718
+ break;
719
+ case "DOWN":
720
+ sp = 2;
721
+ tp = 0;
722
+ break;
723
+ case "UP":
724
+ sp = 0;
725
+ tp = 2;
726
+ break;
727
+ default:
728
+ sp = 2;
729
+ tp = 0;
730
+ break;
731
+ }
732
+ const newEdge = {
733
+ id: (0, crypto_1.randomUUID)(),
734
+ sourceId: args.referenceNodeId,
735
+ targetId: newNodeId,
736
+ sourcePointIndex: sp,
737
+ targetPointIndex: tp,
738
+ label: args.connection.label,
739
+ style: 'line',
740
+ routingType: 'orthogonal',
741
+ color: '#000000',
742
+ thickness: 0.01,
743
+ fontSize: 20,
744
+ borderStyle: 'solid',
745
+ terminationStart: termStart,
746
+ terminationEnd: termEnd
747
+ };
748
+ state.edges.push(newEdge);
749
+ }
580
750
  await (0, fileHandler_1.saveDiagramFile)(args.filePath, state);
581
751
  return { content: [{ type: "text", text: `Created DB Table '${args.tableName}'` }] };
582
752
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dodraw-mcp-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for DoDraw",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -140,7 +140,7 @@ export const toolDefinitions: Tool[] = [
140
140
  },
141
141
  {
142
142
  name: "add_directional_node",
143
- description: "Add a new node relative to an existing node and connect them. PRIMARY method for creating diagrams.",
143
+ description: "Add a new node relative to an existing node and connect them. PRIMARY method for creating diagrams, including Class and Database diagrams. Supports 'columns' for DB tables.",
144
144
  inputSchema: {
145
145
  type: "object",
146
146
  properties: {
@@ -154,14 +154,28 @@ export const toolDefinitions: Tool[] = [
154
154
  color: { type: "string" },
155
155
  edgeLabel: { type: "string", description: "Optional label for the connecting edge" },
156
156
  attributes: { type: "array", items: { type: "string" }, description: "Optional list of attributes for Class nodes" },
157
- methods: { type: "array", items: { type: "string" }, description: "Optional list of methods for Class nodes" }
157
+ methods: { type: "array", items: { type: "string" }, description: "Optional list of methods for Class nodes" },
158
+ columns: {
159
+ type: "array",
160
+ description: "Optional list of columns for Table nodes",
161
+ items: {
162
+ type: "object",
163
+ properties: {
164
+ name: { type: "string" },
165
+ type: { type: "string" },
166
+ isPk: { type: "boolean" },
167
+ isFk: { type: "boolean" }
168
+ },
169
+ required: ["name", "type"]
170
+ }
171
+ }
158
172
  },
159
173
  required: ["filePath", "sourceNodeId", "direction", "label"]
160
174
  }
161
175
  },
162
176
  {
163
177
  name: "create_uml_class",
164
- description: "Create a UML Class node with methods and attributes",
178
+ description: "Create a UML Class node. Use this for the FIRST class. For connected classes, use 'add_directional_node' to ensure correct relative positioning.",
165
179
  inputSchema: {
166
180
  type: "object",
167
181
  properties: {
@@ -178,7 +192,8 @@ export const toolDefinitions: Tool[] = [
178
192
  },
179
193
  {
180
194
  name: "create_db_table",
181
- description: "Create a Database Entity (ERD Table)",
195
+
196
+ description: "Create a Database Entity (ERD Table). Supports absolute positioning (x,y) OR relative positioning (referenceNodeId, direction). Can also auto-connect.",
182
197
  inputSchema: {
183
198
  type: "object",
184
199
  properties: {
@@ -199,7 +214,19 @@ export const toolDefinitions: Tool[] = [
199
214
  },
200
215
  x: { type: "number" },
201
216
  y: { type: "number" },
202
- id: { type: "string", description: "Optional explicit ID" }
217
+ id: { type: "string", description: "Optional explicit ID" },
218
+ referenceNodeId: { type: "string", description: "Optional. ID of an existing node to place this new table relative to." },
219
+ direction: { type: "string", enum: ["UP", "DOWN", "LEFT", "RIGHT"], description: "Optional. Direction to place relative to referenceNodeId." },
220
+ connection: {
221
+ type: "object",
222
+ description: "Optional. If provided, creates an edge connecting the reference node to this new table.",
223
+ properties: {
224
+ type: { type: "string", enum: ["one-to-one", "one-to-many", "many-to-many"], description: "Relationship type" },
225
+ label: { type: "string" },
226
+ direction: { type: "string", enum: ["source-to-target", "target-to-source", "bi-directional"], description: "Edge direction flow" }
227
+ },
228
+ required: ["type"]
229
+ }
203
230
  },
204
231
  required: ["filePath", "tableName", "columns"]
205
232
  }
@@ -417,6 +444,14 @@ export async function handleToolCall(name: string, args: any): Promise<any> {
417
444
  attributes: args.attributes || [],
418
445
  methods: args.methods || []
419
446
  } : undefined,
447
+ dbTableData: args.columns ? {
448
+ columns: args.columns.map((c: any) => ({
449
+ name: c.name,
450
+ type: c.type,
451
+ isPk: !!c.isPk,
452
+ isFk: !!c.isFk
453
+ }))
454
+ } : undefined,
420
455
  textAlignVertical: 'center',
421
456
  textAlignHorizontal: 'center'
422
457
  };
@@ -544,11 +579,21 @@ export async function handleToolCall(name: string, args: any): Promise<any> {
544
579
  const estimatedHeight = 0.8 + (attrCount + methodCount) * 0.4;
545
580
  const height = Math.max(2, estimatedHeight);
546
581
 
582
+ // Auto-placement logic if X/Y not provided
583
+ let x = args.x !== undefined ? Number(args.x) : 0;
584
+ let z = args.y !== undefined ? Number(args.y) : 0;
585
+
586
+ if (args.x === undefined && args.y === undefined && state.nodes.length > 0) {
587
+ const lastNode = state.nodes[state.nodes.length - 1];
588
+ x = lastNode.x + 5;
589
+ z = lastNode.z;
590
+ }
591
+
547
592
  const newNode: NodeData = {
548
593
  id: newNodeId,
549
- x: args.x ? Number(args.x) : 0,
594
+ x: x,
550
595
  y: 0,
551
- z: args.y ? Number(args.y) : 0,
596
+ z: z,
552
597
  width: width,
553
598
  height: height,
554
599
  label: args.className,
@@ -573,13 +618,72 @@ export async function handleToolCall(name: string, args: any): Promise<any> {
573
618
  // Header + items
574
619
  const estimatedHeight = 0.8 + cols.length * 0.4;
575
620
  const height = Math.max(2, estimatedHeight);
621
+
576
622
  const width = 3;
577
623
 
624
+ // Auto-placement logic if X/Y not provided
625
+ let x = args.x !== undefined ? Number(args.x) : 0;
626
+ let z = args.y !== undefined ? Number(args.y) : 0;
627
+
628
+ // Relative Placement Logic
629
+ if (args.referenceNodeId && args.direction) {
630
+ const sourceNode = state.nodes.find(n => n.id === args.referenceNodeId);
631
+ if (!sourceNode) throw new Error(`Reference node ${args.referenceNodeId} not found`);
632
+
633
+ const INITIAL_SPACING = 2.0;
634
+ let dirX = 0;
635
+ let dirZ = 0;
636
+ const srcW = Number(sourceNode.width) || 2;
637
+ const srcH = Number(sourceNode.height) || 1.5;
638
+
639
+ switch (args.direction) {
640
+ case "RIGHT": dirX = 1; break;
641
+ case "LEFT": dirX = -1; break;
642
+ case "DOWN": dirZ = 1; break;
643
+ case "UP": dirZ = -1; break;
644
+ }
645
+
646
+ x = sourceNode.x + (dirX * (srcW + INITIAL_SPACING));
647
+ z = sourceNode.z + (dirZ * (srcH + INITIAL_SPACING));
648
+
649
+ // Collision Avoidance reuse
650
+ const COLLISION_SPACING = 0.5;
651
+ const shiftStepX = srcW + COLLISION_SPACING;
652
+ const shiftStepZ = srcH + COLLISION_SPACING;
653
+ const newW = width;
654
+ const newH = height;
655
+
656
+ const checkCollision = (cx: number, cz: number) => {
657
+ return state.nodes.some(n => {
658
+ if (n.layerId !== sourceNode.layerId) return false;
659
+ const dx = Math.abs(n.x - cx);
660
+ const dz = Math.abs(n.z - cz);
661
+ const otherW = Number(n.width) || 2;
662
+ const otherH = Number(n.height) || 1.5;
663
+ const combinedHalfWidth = (otherW + newW) / 2;
664
+ const combinedHalfHeight = (otherH + newH) / 2;
665
+ return dx < combinedHalfWidth && dz < combinedHalfHeight;
666
+ });
667
+ };
668
+
669
+ let iterations = 0;
670
+ while (checkCollision(x, z) && iterations < 50) {
671
+ iterations++;
672
+ if (dirX !== 0) z += shiftStepZ;
673
+ else x += shiftStepX;
674
+ }
675
+ } else if (args.x === undefined && args.y === undefined && state.nodes.length > 0) {
676
+ // Fallback to "next to last" if no explicit relative info
677
+ const lastNode = state.nodes[state.nodes.length - 1];
678
+ x = lastNode.x + 5;
679
+ z = lastNode.z;
680
+ }
681
+
578
682
  const newNode: NodeData = {
579
683
  id: newNodeId,
580
- x: args.x ? Number(args.x) : 0,
684
+ x: x,
581
685
  y: 0,
582
- z: args.y ? Number(args.y) : 0,
686
+ z: z,
583
687
  width: width,
584
688
  height: height,
585
689
  label: args.tableName,
@@ -597,6 +701,57 @@ export async function handleToolCall(name: string, args: any): Promise<any> {
597
701
  };
598
702
 
599
703
  state.nodes.push(newNode);
704
+
705
+ // Auto-Connect logic
706
+ if (args.connection && args.referenceNodeId) {
707
+ let termStart = 'none';
708
+ let termEnd = 'none';
709
+ // Simplified mapping for DB
710
+ if (args.connection.type === 'one-to-many') {
711
+ // Source (Reference) -> Target (New)
712
+ // If Reference is "Users" (1) and New is "Orders" (N)
713
+ // termStart = one, termEnd = many
714
+ termStart = 'crows-one';
715
+ termEnd = 'crows-many';
716
+ } else if (args.connection.type === 'one-to-one') {
717
+ termStart = 'crows-one';
718
+ termEnd = 'crows-one';
719
+ } else if (args.connection.type === 'many-to-many') {
720
+ termStart = 'crows-many';
721
+ termEnd = 'crows-many';
722
+ }
723
+
724
+ // Points? Simple defaults based on direction or just center-ish
725
+ // Let's use the logic from add_directional_node for cleaner ports if possible
726
+ // But simple 0-3 logic:
727
+ let sp = 0;
728
+ let tp = 0;
729
+ switch(args.direction) {
730
+ case "RIGHT": sp=1; tp=3; break;
731
+ case "LEFT": sp=3; tp=1; break;
732
+ case "DOWN": sp=2; tp=0; break;
733
+ case "UP": sp=0; tp=2; break;
734
+ default: sp=2; tp=0; break;
735
+ }
736
+
737
+ const newEdge: EdgeData = {
738
+ id: randomUUID(),
739
+ sourceId: args.referenceNodeId,
740
+ targetId: newNodeId,
741
+ sourcePointIndex: sp,
742
+ targetPointIndex: tp,
743
+ label: args.connection.label,
744
+ style: 'line',
745
+ routingType: 'orthogonal',
746
+ color: '#000000',
747
+ thickness: 0.01,
748
+ fontSize: 20,
749
+ borderStyle: 'solid',
750
+ terminationStart: termStart as any,
751
+ terminationEnd: termEnd as any
752
+ };
753
+ state.edges.push(newEdge);
754
+ }
600
755
  await saveDiagramFile(args.filePath, state);
601
756
  return { content: [{ type: "text", text: `Created DB Table '${args.tableName}'` }] };
602
757
  }