dominus-cli 2.2.1 → 2.4.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/dist/mcp.js CHANGED
@@ -109,6 +109,19 @@ var CliMsg = {
109
109
  V2_READ_SCRIPT: "studio:v2:read_script",
110
110
  V2_UPDATE_SCRIPT: "studio:v2:update_script",
111
111
  V2_APPLY: "studio:v2:apply",
112
+ V2_CREATE_PARTS: "studio:v2:create_parts",
113
+ V2_CLONE_INSTANCES: "studio:v2:clone_instances",
114
+ V2_GROUP_INSTANCES: "studio:v2:group_instances",
115
+ V2_UNGROUP_INSTANCES: "studio:v2:ungroup_instances",
116
+ V2_TRANSFORM_INSTANCES: "studio:v2:transform_instances",
117
+ V2_CREATE_WELDS: "studio:v2:create_welds",
118
+ V2_QUERY_PARTS: "studio:v2:query_parts",
119
+ V2_SET_SELECTION: "studio:v2:set_selection",
120
+ V2_GET_METADATA: "studio:v2:get_metadata",
121
+ V2_SET_ATTRIBUTES: "studio:v2:set_attributes",
122
+ V2_UPDATE_TAGS: "studio:v2:update_tags",
123
+ V2_LIST_CALLABLE_METHODS: "studio:v2:list_callable_methods",
124
+ V2_INVOKE_METHODS: "studio:v2:invoke_methods",
112
125
  V2_DELETE: "studio:v2:delete",
113
126
  V2_BUILD_UI: "studio:v2:build_ui",
114
127
  V2_SNAPSHOT_UI: "studio:v2:snapshot_ui",
@@ -561,11 +574,20 @@ Required workflow for Studio changes:
561
574
 
562
575
  For broad tasks with independent Studio branches, prefer run_parallel_task when the MCP client supports Sampling. Dominus partitions immediate child scopes, runs proposal-only workers concurrently, rejects unknown refs and overlapping writes, applies only the coordinator-approved atomic plan, and reads the result back. Use the normal workflow when Sampling is unavailable or the task is a simple edit.
563
576
 
577
+ Building workflow:
578
+ - Use studio_create_parts for multi-part structures instead of many generic create operations.
579
+ - Use studio_clone_instances when an existing instance or model is the intended template. Clone rather than manually recreating descendants and properties.
580
+ - Use studio_get_metadata, studio_set_attributes, and studio_update_tags for saved Instance metadata. Do not create runtime bootstrap scripts or attribute mirrors when direct tags or attributes satisfy the task.
581
+ - Use studio_list_callable_methods before studio_invoke_methods when a task needs an Instance method not covered by a dedicated tool. Dedicated tools remain preferred for common writes.
582
+ - Use studio_transform_instances for Models and BaseParts so model layouts move through PVInstance pivots.
583
+ - Use studio_group_instances and studio_ungroup_instances to organize hierarchy, studio_create_welds for rigid assemblies, and studio_query_parts to inspect crowded 3D regions before editing.
584
+ - Use studio_set_selection when surfacing completed objects to the Studio user.
585
+
564
586
  Safety rules:
565
587
  - Never invent instance IDs or Roblox API members.
566
588
  - Do not retry a failed write unchanged. Inspect the error and current state first.
567
589
  - Deletion is a separate destructive tool and requires explicit user intent.
568
- - studio_apply is transactional. Keep related edits together, but stay within its documented limits.
590
+ - studio_apply is transactional and supports setProperties, setAttributes, updateTags, create, clone, and move operations. Keep related edits together, but stay within its documented limits.
569
591
  - UI fidelity work must snapshot and inspect the result after building; verify fonts, UDim2 values, colors, strokes, and hierarchy.
570
592
  - Dominus intentionally has no arbitrary Luau execution tool.`;
571
593
 
@@ -632,6 +654,8 @@ function failure(error, data) {
632
654
  data
633
655
  });
634
656
  }
657
+
658
+ // src/mcp/tools/shared.ts
635
659
  var instanceRefSchema = z.object({
636
660
  instanceId: z.string().uuid().optional(),
637
661
  pathSegments: z.array(z.string().min(1).max(256)).min(1).max(100).optional()
@@ -664,7 +688,245 @@ function ref(value) {
664
688
  return value;
665
689
  }
666
690
 
667
- // src/mcp/tools/connections.ts
691
+ // src/mcp/tools/building.ts
692
+ var finiteNumber = z.number().finite();
693
+ var vector3Schema = z.object({ x: finiteNumber, y: finiteNumber, z: finiteNumber }).strict();
694
+ var propertiesSchema = z.record(z.unknown()).refine(
695
+ (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
696
+ "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
697
+ );
698
+ var partSchema = z.object({
699
+ name: z.string().min(1).max(100),
700
+ className: z.enum([
701
+ "Part",
702
+ "WedgePart",
703
+ "CornerWedgePart",
704
+ "TrussPart",
705
+ "MeshPart",
706
+ "SpawnLocation",
707
+ "Seat",
708
+ "VehicleSeat"
709
+ ]).optional().default("Part"),
710
+ shape: z.enum(["Block", "Ball", "Cylinder", "Wedge", "CornerWedge"]).optional(),
711
+ position: vector3Schema.optional(),
712
+ orientation: vector3Schema.optional(),
713
+ size: vector3Schema.optional(),
714
+ color: z.string().min(1).max(100).optional(),
715
+ material: z.string().min(1).max(100).optional(),
716
+ transparency: z.number().min(0).max(1).optional(),
717
+ anchored: z.boolean().optional().default(true),
718
+ canCollide: z.boolean().optional(),
719
+ properties: propertiesSchema.optional()
720
+ });
721
+ var cloneRequestSchema = z.object({
722
+ target: instanceRefSchema,
723
+ parent: instanceRefSchema.optional(),
724
+ name: z.string().min(1).max(100).optional(),
725
+ count: z.number().int().min(1).max(50).optional().default(1),
726
+ offset: vector3Schema.optional(),
727
+ stepOffset: vector3Schema.optional(),
728
+ properties: propertiesSchema.optional()
729
+ });
730
+ var cframeSchema = z.array(finiteNumber).refine((value) => value.length === 3 || value.length === 12, "CFrame requires 3 or 12 numbers");
731
+ var transformOperationSchema = z.object({
732
+ target: instanceRefSchema,
733
+ mode: z.enum(["absolute", "relative"]).optional().default("absolute"),
734
+ cframe: cframeSchema.optional(),
735
+ position: vector3Schema.optional(),
736
+ orientation: vector3Schema.optional(),
737
+ offset: vector3Schema.optional(),
738
+ rotationOffset: vector3Schema.optional(),
739
+ space: z.enum(["world", "local"]).optional().default("world")
740
+ }).refine(
741
+ (value) => value.mode === "relative" ? Boolean(value.offset || value.rotationOffset) : Boolean(value.cframe || value.position || value.orientation),
742
+ "Absolute transforms require cframe, position, or orientation; relative transforms require offset or rotationOffset"
743
+ );
744
+ function registerBuildingTools(server, bridge) {
745
+ server.registerTool(
746
+ "studio_create_parts",
747
+ {
748
+ title: "Build Roblox Parts",
749
+ description: "Create up to 200 typed BaseParts in one undoable batch, optionally grouped under a new Model or Folder. Supports positions, orientations, sizes, colors, materials, shapes, and additional writable properties.",
750
+ inputSchema: {
751
+ parent: instanceRefSchema.optional(),
752
+ groupName: z.string().min(1).max(100).optional(),
753
+ groupClass: z.enum(["Model", "Folder"]).optional().default("Model"),
754
+ parts: z.array(partSchema).min(1).max(200)
755
+ },
756
+ outputSchema: toolOutputSchema,
757
+ annotations: {
758
+ readOnlyHint: false,
759
+ destructiveHint: false,
760
+ idempotentHint: false,
761
+ openWorldHint: false
762
+ }
763
+ },
764
+ ({ parent, groupName, groupClass, parts }) => callStudio(bridge, CliMsg.V2_CREATE_PARTS, { parent, groupName, groupClass, parts }, 12e4)
765
+ );
766
+ server.registerTool(
767
+ "studio_clone_instances",
768
+ {
769
+ title: "Clone Roblox Instances",
770
+ description: "Deep-clone instances and descendants with Instance:Clone(), optionally rename, reparent, override root properties, and create repeated spatial copies using offsets. The entire request is undoable and rolls back on failure.",
771
+ inputSchema: { requests: z.array(cloneRequestSchema).min(1).max(100) },
772
+ outputSchema: toolOutputSchema,
773
+ annotations: {
774
+ readOnlyHint: false,
775
+ destructiveHint: false,
776
+ idempotentHint: false,
777
+ openWorldHint: false
778
+ }
779
+ },
780
+ ({ requests }) => callStudio(bridge, CliMsg.V2_CLONE_INSTANCES, { requests }, 12e4)
781
+ );
782
+ server.registerTool(
783
+ "studio_group_instances",
784
+ {
785
+ title: "Group Roblox Instances",
786
+ description: "Create a Model or Folder and move up to 100 independent instances into it in one undoable operation.",
787
+ inputSchema: {
788
+ targets: z.array(instanceRefSchema).min(1).max(100),
789
+ name: z.string().min(1).max(100),
790
+ className: z.enum(["Model", "Folder"]).optional().default("Model"),
791
+ parent: instanceRefSchema.optional()
792
+ },
793
+ outputSchema: toolOutputSchema,
794
+ annotations: {
795
+ readOnlyHint: false,
796
+ destructiveHint: false,
797
+ idempotentHint: false,
798
+ openWorldHint: false
799
+ }
800
+ },
801
+ ({ targets, name, className, parent }) => callStudio(bridge, CliMsg.V2_GROUP_INSTANCES, { targets, name, className, parent }, 6e4)
802
+ );
803
+ server.registerTool(
804
+ "studio_ungroup_instances",
805
+ {
806
+ title: "Ungroup Roblox Instances",
807
+ description: "Move every child out of up to 20 Models or Folders, then remove the empty containers in one undoable operation.",
808
+ inputSchema: {
809
+ targets: z.array(instanceRefSchema).min(1).max(20),
810
+ parent: instanceRefSchema.optional(),
811
+ confirm: z.literal(true)
812
+ },
813
+ outputSchema: toolOutputSchema,
814
+ annotations: {
815
+ readOnlyHint: false,
816
+ destructiveHint: true,
817
+ idempotentHint: false,
818
+ openWorldHint: false
819
+ }
820
+ },
821
+ ({ targets, parent, confirm }) => callStudio(bridge, CliMsg.V2_UNGROUP_INSTANCES, { targets, parent, confirm }, 6e4)
822
+ );
823
+ server.registerTool(
824
+ "studio_transform_instances",
825
+ {
826
+ title: "Transform Roblox Models and Parts",
827
+ description: "Move or rotate up to 100 Models and BaseParts through PVInstance:PivotTo. Supports exact absolute CFrames and world/local relative offsets without breaking model layouts.",
828
+ inputSchema: { operations: z.array(transformOperationSchema).min(1).max(100) },
829
+ outputSchema: toolOutputSchema,
830
+ annotations: {
831
+ readOnlyHint: false,
832
+ destructiveHint: false,
833
+ idempotentHint: false,
834
+ openWorldHint: false
835
+ }
836
+ },
837
+ ({ operations }) => callStudio(bridge, CliMsg.V2_TRANSFORM_INSTANCES, { operations }, 6e4)
838
+ );
839
+ server.registerTool(
840
+ "studio_create_welds",
841
+ {
842
+ title: "Create Roblox Welds",
843
+ description: "Create up to 200 WeldConstraints between explicit BasePart refs in one undoable operation.",
844
+ inputSchema: {
845
+ welds: z.array(
846
+ z.object({
847
+ part0: instanceRefSchema,
848
+ part1: instanceRefSchema,
849
+ name: z.string().min(1).max(100).optional(),
850
+ parent: instanceRefSchema.optional()
851
+ })
852
+ ).min(1).max(200)
853
+ },
854
+ outputSchema: toolOutputSchema,
855
+ annotations: {
856
+ readOnlyHint: false,
857
+ destructiveHint: false,
858
+ idempotentHint: false,
859
+ openWorldHint: false
860
+ }
861
+ },
862
+ ({ welds }) => callStudio(bridge, CliMsg.V2_CREATE_WELDS, { welds }, 6e4)
863
+ );
864
+ server.registerTool(
865
+ "studio_query_parts",
866
+ {
867
+ title: "Query Parts in 3D Space",
868
+ description: "Find BaseParts intersecting an oriented box or radius using WorldRoot spatial queries. Returns stable refs, positions, and sizes for inspection or later mutations.",
869
+ inputSchema: {
870
+ root: instanceRefSchema.optional(),
871
+ mode: z.enum(["box", "radius"]).optional().default("box"),
872
+ position: vector3Schema,
873
+ orientation: vector3Schema.optional(),
874
+ size: vector3Schema.optional(),
875
+ radius: z.number().finite().positive().optional(),
876
+ maxParts: z.number().int().min(1).max(500).optional().default(200),
877
+ filter: z.array(instanceRefSchema).max(100).optional(),
878
+ filterType: z.enum(["Include", "Exclude"]).optional().default("Exclude"),
879
+ respectCanCollide: z.boolean().optional().default(false)
880
+ },
881
+ outputSchema: toolOutputSchema,
882
+ annotations: {
883
+ readOnlyHint: true,
884
+ destructiveHint: false,
885
+ idempotentHint: true,
886
+ openWorldHint: false
887
+ }
888
+ },
889
+ ({
890
+ root,
891
+ mode,
892
+ position,
893
+ orientation,
894
+ size,
895
+ radius,
896
+ maxParts,
897
+ filter,
898
+ filterType,
899
+ respectCanCollide
900
+ }) => callStudio(bridge, CliMsg.V2_QUERY_PARTS, {
901
+ root,
902
+ mode,
903
+ position,
904
+ orientation,
905
+ size,
906
+ radius,
907
+ maxParts,
908
+ filter,
909
+ filterType,
910
+ respectCanCollide
911
+ })
912
+ );
913
+ server.registerTool(
914
+ "studio_set_selection",
915
+ {
916
+ title: "Set Roblox Studio Selection",
917
+ description: "Replace the current Studio selection with up to 200 stable instance refs. Pass an empty array to clear selection.",
918
+ inputSchema: { targets: z.array(instanceRefSchema).max(200) },
919
+ outputSchema: toolOutputSchema,
920
+ annotations: {
921
+ readOnlyHint: false,
922
+ destructiveHint: false,
923
+ idempotentHint: true,
924
+ openWorldHint: false
925
+ }
926
+ },
927
+ ({ targets }) => callStudio(bridge, CliMsg.V2_SET_SELECTION, { targets })
928
+ );
929
+ }
668
930
  function registerConnectionTools(server, bridge) {
669
931
  server.registerTool(
670
932
  "dominus_status",
@@ -1192,7 +1454,261 @@ function registerDocsTools(server) {
1192
1454
  }
1193
1455
  );
1194
1456
  }
1195
- var propertiesSchema = z.record(z.unknown()).refine(
1457
+ var finiteNumber2 = z.number().finite();
1458
+ var vector2Value = z.object({ x: finiteNumber2, y: finiteNumber2 }).strict();
1459
+ var vector3Value = z.object({ x: finiteNumber2, y: finiteNumber2, z: finiteNumber2 }).strict();
1460
+ var color3Object = z.object({ r: finiteNumber2, g: finiteNumber2, b: finiteNumber2 }).strict();
1461
+ var color3Value = z.union([
1462
+ z.string().min(1).max(100),
1463
+ color3Object,
1464
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])
1465
+ ]);
1466
+ var cframeValue = z.array(finiteNumber2).refine((value) => value.length === 3 || value.length === 12, "CFrame requires 3 or 12 numbers");
1467
+ var numberSequenceKeypoint = z.union([
1468
+ z.tuple([finiteNumber2, finiteNumber2]),
1469
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])
1470
+ ]);
1471
+ var colorSequenceKeypoint = z.tuple([finiteNumber2, color3Value]);
1472
+ var typedString = z.object({ type: z.literal("string"), value: z.string() }).strict();
1473
+ var typedBoolean = z.object({ type: z.literal("boolean"), value: z.boolean() }).strict();
1474
+ var typedNumber = z.object({ type: z.literal("number"), value: finiteNumber2 }).strict();
1475
+ var typedUDim = z.object({
1476
+ type: z.literal("UDim"),
1477
+ value: z.union([
1478
+ z.object({ scale: finiteNumber2, offset: finiteNumber2 }).strict(),
1479
+ z.tuple([finiteNumber2, finiteNumber2])
1480
+ ])
1481
+ }).strict();
1482
+ var typedUDim2 = z.object({
1483
+ type: z.literal("UDim2"),
1484
+ value: z.union([
1485
+ z.object({
1486
+ xScale: finiteNumber2,
1487
+ xOffset: finiteNumber2,
1488
+ yScale: finiteNumber2,
1489
+ yOffset: finiteNumber2
1490
+ }).strict(),
1491
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2, finiteNumber2])
1492
+ ])
1493
+ }).strict();
1494
+ var typedBrickColor = z.object({ type: z.literal("BrickColor"), value: z.string().min(1).max(100) }).strict();
1495
+ var typedColor3 = z.object({ type: z.literal("Color3"), value: color3Value }).strict();
1496
+ var typedVector2 = z.object({
1497
+ type: z.literal("Vector2"),
1498
+ value: z.union([vector2Value, z.tuple([finiteNumber2, finiteNumber2])])
1499
+ }).strict();
1500
+ var typedVector3 = z.object({
1501
+ type: z.literal("Vector3"),
1502
+ value: z.union([vector3Value, z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])])
1503
+ }).strict();
1504
+ var typedCFrame = z.object({ type: z.literal("CFrame"), value: cframeValue }).strict();
1505
+ var typedNumberSequence = z.object({
1506
+ type: z.literal("NumberSequence"),
1507
+ value: z.union([finiteNumber2, z.array(numberSequenceKeypoint).min(1).max(100)])
1508
+ }).strict();
1509
+ var typedColorSequence = z.object({
1510
+ type: z.literal("ColorSequence"),
1511
+ value: z.union([color3Value, z.array(colorSequenceKeypoint).min(1).max(100)])
1512
+ }).strict();
1513
+ var typedNumberRange = z.object({
1514
+ type: z.literal("NumberRange"),
1515
+ value: z.union([finiteNumber2, z.tuple([finiteNumber2, finiteNumber2])])
1516
+ }).strict();
1517
+ var typedRect = z.object({
1518
+ type: z.literal("Rect"),
1519
+ value: z.tuple([finiteNumber2, finiteNumber2, finiteNumber2, finiteNumber2])
1520
+ }).strict();
1521
+ var typedFont = z.object({
1522
+ type: z.literal("Font"),
1523
+ value: z.union([
1524
+ z.object({
1525
+ family: z.string().min(1).max(500),
1526
+ weight: z.string().min(1).max(100).optional(),
1527
+ style: z.string().min(1).max(100).optional()
1528
+ }).strict(),
1529
+ z.object({
1530
+ Family: z.string().min(1).max(500),
1531
+ Weight: z.string().min(1).max(100).optional(),
1532
+ Style: z.string().min(1).max(100).optional()
1533
+ }).strict()
1534
+ ])
1535
+ }).strict();
1536
+ var attributeValueSchema = z.discriminatedUnion("type", [
1537
+ typedString,
1538
+ typedBoolean,
1539
+ typedNumber,
1540
+ typedUDim,
1541
+ typedUDim2,
1542
+ typedBrickColor,
1543
+ typedColor3,
1544
+ typedVector2,
1545
+ typedVector3,
1546
+ typedCFrame,
1547
+ typedNumberSequence,
1548
+ typedColorSequence,
1549
+ typedNumberRange,
1550
+ typedRect,
1551
+ typedFont
1552
+ ]);
1553
+ var methodArgumentSchema = z.discriminatedUnion("type", [
1554
+ z.object({ type: z.literal("nil") }).strict(),
1555
+ typedString,
1556
+ typedBoolean,
1557
+ typedNumber,
1558
+ typedUDim,
1559
+ typedUDim2,
1560
+ typedBrickColor,
1561
+ typedColor3,
1562
+ typedVector2,
1563
+ typedVector3,
1564
+ typedCFrame,
1565
+ typedNumberSequence,
1566
+ typedColorSequence,
1567
+ typedNumberRange,
1568
+ typedRect,
1569
+ typedFont,
1570
+ z.object({ type: z.literal("Instance"), value: instanceRefSchema }).strict()
1571
+ ]);
1572
+ var callableMethodSchema = z.enum([
1573
+ "GetAttribute",
1574
+ "GetAttributes",
1575
+ "GetTags",
1576
+ "HasTag",
1577
+ "GetFullName",
1578
+ "IsA",
1579
+ "IsAncestorOf",
1580
+ "IsDescendantOf",
1581
+ "FindFirstChild",
1582
+ "FindFirstChildOfClass",
1583
+ "FindFirstChildWhichIsA",
1584
+ "FindFirstAncestor",
1585
+ "FindFirstAncestorOfClass",
1586
+ "FindFirstAncestorWhichIsA",
1587
+ "GetChildren",
1588
+ "GetDescendants",
1589
+ "GetPivot",
1590
+ "GetScale",
1591
+ "GetBoundingBox",
1592
+ "AddTag",
1593
+ "RemoveTag",
1594
+ "SetAttribute",
1595
+ "PivotTo",
1596
+ "ScaleTo"
1597
+ ]);
1598
+ var attributeUpdateSchema = z.object({
1599
+ target: instanceRefSchema,
1600
+ set: z.array(
1601
+ z.object({
1602
+ name: z.string().min(1).max(100),
1603
+ value: attributeValueSchema
1604
+ })
1605
+ ).max(100).optional().default([]),
1606
+ remove: z.array(z.string().min(1).max(100)).max(100).optional().default([])
1607
+ });
1608
+ var tagUpdateSchema = z.object({
1609
+ target: instanceRefSchema,
1610
+ add: z.array(z.string().min(1).max(100)).max(100).optional().default([]),
1611
+ remove: z.array(z.string().min(1).max(100)).max(100).optional().default([])
1612
+ });
1613
+ function registerMetadataTools(server, bridge) {
1614
+ server.registerTool(
1615
+ "studio_get_metadata",
1616
+ {
1617
+ title: "Get Roblox Tags and Attributes",
1618
+ description: "Read persistent Instance tags and typed attributes for up to 100 instances. Use this instead of runtime bootstrap scripts or attribute mirrors when inspecting saved Studio metadata.",
1619
+ inputSchema: {
1620
+ targets: z.array(instanceRefSchema).min(1).max(100)
1621
+ },
1622
+ outputSchema: toolOutputSchema,
1623
+ annotations: {
1624
+ readOnlyHint: true,
1625
+ destructiveHint: false,
1626
+ idempotentHint: true,
1627
+ openWorldHint: false
1628
+ }
1629
+ },
1630
+ ({ targets }) => callStudio(bridge, CliMsg.V2_GET_METADATA, { targets })
1631
+ );
1632
+ server.registerTool(
1633
+ "studio_set_attributes",
1634
+ {
1635
+ title: "Set Roblox Attributes",
1636
+ description: "Set or remove persistent typed Instance attributes in one undoable batch. Values are explicitly typed and saved with the place; no Play-mode bootstrap is required.",
1637
+ inputSchema: {
1638
+ operations: z.array(attributeUpdateSchema).min(1).max(100)
1639
+ },
1640
+ outputSchema: toolOutputSchema,
1641
+ annotations: {
1642
+ readOnlyHint: false,
1643
+ destructiveHint: false,
1644
+ idempotentHint: true,
1645
+ openWorldHint: false
1646
+ }
1647
+ },
1648
+ ({ operations }) => callStudio(bridge, CliMsg.V2_SET_ATTRIBUTES, { operations }, 6e4)
1649
+ );
1650
+ server.registerTool(
1651
+ "studio_update_tags",
1652
+ {
1653
+ title: "Update Roblox Tags",
1654
+ description: "Add or remove persistent Instance tags in one undoable batch using Instance:AddTag and RemoveTag. Tags are saved directly in Studio and replicate normally.",
1655
+ inputSchema: {
1656
+ operations: z.array(tagUpdateSchema).min(1).max(100)
1657
+ },
1658
+ outputSchema: toolOutputSchema,
1659
+ annotations: {
1660
+ readOnlyHint: false,
1661
+ destructiveHint: false,
1662
+ idempotentHint: true,
1663
+ openWorldHint: false
1664
+ }
1665
+ },
1666
+ ({ operations }) => callStudio(bridge, CliMsg.V2_UPDATE_TAGS, { operations }, 6e4)
1667
+ );
1668
+ server.registerTool(
1669
+ "studio_list_callable_methods",
1670
+ {
1671
+ title: "List Safe Instance Methods",
1672
+ description: "List the guarded Instance methods Dominus can invoke on a specific target, including required classes, argument types, and whether each method reads or writes Studio state.",
1673
+ inputSchema: {
1674
+ target: instanceRefSchema
1675
+ },
1676
+ outputSchema: toolOutputSchema,
1677
+ annotations: {
1678
+ readOnlyHint: true,
1679
+ destructiveHint: false,
1680
+ idempotentHint: true,
1681
+ openWorldHint: false
1682
+ }
1683
+ },
1684
+ ({ target }) => callStudio(bridge, CliMsg.V2_LIST_CALLABLE_METHODS, { target })
1685
+ );
1686
+ server.registerTool(
1687
+ "studio_invoke_methods",
1688
+ {
1689
+ title: "Invoke Safe Instance Methods",
1690
+ description: "Invoke a bounded allowlist of typed Instance methods. Supports hierarchy queries, metadata methods, GetPivot/GetBoundingBox/GetScale, and undoable AddTag/RemoveTag/SetAttribute/PivotTo/ScaleTo writes. Arbitrary, destructive, yielding, network, and signal methods are blocked in the Studio plugin.",
1691
+ inputSchema: {
1692
+ calls: z.array(
1693
+ z.object({
1694
+ target: instanceRefSchema,
1695
+ method: callableMethodSchema,
1696
+ arguments: z.array(methodArgumentSchema).max(10).optional().default([])
1697
+ })
1698
+ ).min(1).max(50)
1699
+ },
1700
+ outputSchema: toolOutputSchema,
1701
+ annotations: {
1702
+ readOnlyHint: false,
1703
+ destructiveHint: false,
1704
+ idempotentHint: false,
1705
+ openWorldHint: false
1706
+ }
1707
+ },
1708
+ ({ calls }) => callStudio(bridge, CliMsg.V2_INVOKE_METHODS, { calls }, 6e4)
1709
+ );
1710
+ }
1711
+ var propertiesSchema2 = z.record(z.unknown()).refine(
1196
1712
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1197
1713
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
1198
1714
  );
@@ -1200,20 +1716,30 @@ var mutationOperationSchema = z.discriminatedUnion("op", [
1200
1716
  z.object({
1201
1717
  op: z.literal("setProperties"),
1202
1718
  target: instanceRefSchema,
1203
- properties: propertiesSchema
1719
+ properties: propertiesSchema2
1204
1720
  }),
1205
1721
  z.object({
1206
1722
  op: z.literal("create"),
1207
1723
  parent: instanceRefSchema,
1208
1724
  className: z.string().min(1).max(100),
1209
1725
  name: z.string().min(1).max(256),
1210
- properties: propertiesSchema.optional()
1726
+ properties: propertiesSchema2.optional()
1211
1727
  }),
1212
1728
  z.object({
1213
1729
  op: z.literal("move"),
1214
1730
  target: instanceRefSchema,
1215
1731
  parent: instanceRefSchema
1216
- })
1732
+ }),
1733
+ z.object({
1734
+ op: z.literal("clone"),
1735
+ target: instanceRefSchema,
1736
+ parent: instanceRefSchema,
1737
+ name: z.string().min(1).max(100),
1738
+ offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
1739
+ properties: propertiesSchema2.optional()
1740
+ }),
1741
+ attributeUpdateSchema.extend({ op: z.literal("setAttributes") }),
1742
+ tagUpdateSchema.extend({ op: z.literal("updateTags") })
1217
1743
  ]);
1218
1744
  var workerProposalSchema = z.object({
1219
1745
  workerId: z.string().min(1).max(64),
@@ -1441,7 +1967,7 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1441
1967
  systemPrompt: [
1442
1968
  "You are a read-only Dominus worker. Inspect only the Studio evidence in the request.",
1443
1969
  "You cannot write to Studio. Return one strict JSON WorkerProposal and no Markdown.",
1444
- "Only propose setProperties, create, or move operations using exact instance refs copied from evidence.",
1970
+ "Only propose setProperties, setAttributes, updateTags, create, clone, or move operations using exact instance refs copied from evidence.",
1445
1971
  "Never invent instanceId values. Never propose delete, script edits, or arbitrary execution."
1446
1972
  ].join(" "),
1447
1973
  messages: [
@@ -1472,6 +1998,25 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1472
1998
  op: "move",
1473
1999
  target: { instanceId: "uuid", pathSegments: ["..."] },
1474
2000
  parent: { instanceId: "uuid", pathSegments: ["..."] }
2001
+ },
2002
+ {
2003
+ op: "clone",
2004
+ target: { instanceId: "uuid", pathSegments: ["..."] },
2005
+ parent: { instanceId: "uuid", pathSegments: ["..."] },
2006
+ name: "CloneName",
2007
+ offset: { x: 0, y: 0, z: 0 }
2008
+ },
2009
+ {
2010
+ op: "setAttributes",
2011
+ target: { instanceId: "uuid", pathSegments: ["..."] },
2012
+ set: [{ name: "PlotId", value: { type: "number", value: 1 } }],
2013
+ remove: []
2014
+ },
2015
+ {
2016
+ op: "updateTags",
2017
+ target: { instanceId: "uuid", pathSegments: ["..."] },
2018
+ add: ["Plot"],
2019
+ remove: []
1475
2020
  }
1476
2021
  ],
1477
2022
  risks: ["string"],
@@ -1556,7 +2101,7 @@ function validateWorkerResults(results) {
1556
2101
  function validateOperations(assignment, operations) {
1557
2102
  const errors = [];
1558
2103
  for (const [index, operation] of operations.entries()) {
1559
- const refs = operation.op === "create" ? [operation.parent] : operation.op === "move" ? [operation.target, operation.parent] : [operation.target];
2104
+ const refs = operation.op === "create" ? [operation.parent] : operation.op === "move" || operation.op === "clone" ? [operation.target, operation.parent] : [operation.target];
1560
2105
  for (const ref2 of refs) {
1561
2106
  if (!isKnownParallelRef(ref2, assignment.knownRefKeys)) {
1562
2107
  errors.push(`Operation ${index} references an instance absent from worker evidence`);
@@ -1702,15 +2247,18 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1702
2247
  );
1703
2248
  const knownPaths = collectInstancePaths(originalRoot);
1704
2249
  const expectedCreates = operations.flatMap((operation) => {
1705
- if (operation.op !== "create") return [];
2250
+ if (operation.op !== "create" && operation.op !== "clone") return [];
1706
2251
  const parentPath = resolveRefPath(operation.parent, knownPaths);
1707
2252
  return parentPath ? [pathKey([...parentPath, operation.name])] : [];
1708
2253
  });
1709
2254
  const missingCreates = expectedCreates.filter((path5) => !paths.has(path5));
1710
- const inspectOperations = operations.filter((operation) => operation.op !== "create");
2255
+ const inspectOperations = operations.filter(
2256
+ (operation) => operation.op === "setProperties" || operation.op === "move"
2257
+ );
1711
2258
  const inspectFailures = [];
1712
2259
  const propertyMismatches = [];
1713
2260
  const moveMismatches = [];
2261
+ const metadataMismatches = [];
1714
2262
  for (let offset = 0; offset < inspectOperations.length; offset += 20) {
1715
2263
  const batch = inspectOperations.slice(offset, offset + 20);
1716
2264
  const targets = batch.map((operation) => operation.target);
@@ -1755,8 +2303,77 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1755
2303
  }
1756
2304
  });
1757
2305
  }
2306
+ const metadataOperations = operations.filter(
2307
+ (operation) => operation.op === "setAttributes" || operation.op === "updateTags"
2308
+ );
2309
+ for (let offset = 0; offset < metadataOperations.length; offset += 100) {
2310
+ const batch = metadataOperations.slice(offset, offset + 100);
2311
+ const metadata = await requestStudio(bridge, CliMsg.V2_GET_METADATA, {
2312
+ targets: batch.map((operation) => operation.target)
2313
+ });
2314
+ const results = Array.isArray(metadata.results) ? metadata.results : [];
2315
+ batch.forEach((operation, index) => {
2316
+ const result = results[index];
2317
+ const targetLabel = refLabel(operation.target);
2318
+ if (!result) {
2319
+ inspectFailures.push(`${targetLabel}: missing metadata inspection result`);
2320
+ return;
2321
+ }
2322
+ if (operation.op === "setAttributes") {
2323
+ const attributes = Array.isArray(result.attributes) ? result.attributes : [];
2324
+ const actual = new Map(attributes.map((entry) => [String(entry.name), entry.value]));
2325
+ for (const item of operation.set) {
2326
+ const actualValue = actual.get(item.name);
2327
+ if (!actual.has(item.name) || !valuesEquivalent(item.value, actualValue)) {
2328
+ metadataMismatches.push({
2329
+ target: targetLabel,
2330
+ kind: "attribute",
2331
+ name: item.name,
2332
+ expected: item.value,
2333
+ actual: actualValue
2334
+ });
2335
+ }
2336
+ }
2337
+ for (const name of operation.remove) {
2338
+ if (actual.has(name)) {
2339
+ metadataMismatches.push({
2340
+ target: targetLabel,
2341
+ kind: "attribute",
2342
+ name,
2343
+ expected: "absent",
2344
+ actual: actual.get(name)
2345
+ });
2346
+ }
2347
+ }
2348
+ } else {
2349
+ const tags = new Set(Array.isArray(result.tags) ? result.tags.map(String) : []);
2350
+ for (const name of operation.add) {
2351
+ if (!tags.has(name)) {
2352
+ metadataMismatches.push({
2353
+ target: targetLabel,
2354
+ kind: "tag",
2355
+ name,
2356
+ expected: true,
2357
+ actual: false
2358
+ });
2359
+ }
2360
+ }
2361
+ for (const name of operation.remove) {
2362
+ if (tags.has(name)) {
2363
+ metadataMismatches.push({
2364
+ target: targetLabel,
2365
+ kind: "tag",
2366
+ name,
2367
+ expected: false,
2368
+ actual: true
2369
+ });
2370
+ }
2371
+ }
2372
+ }
2373
+ });
2374
+ }
1758
2375
  return {
1759
- passed: !tree.truncated && missingCreates.length === 0 && propertyMismatches.length === 0 && moveMismatches.length === 0 && inspectFailures.length === 0,
2376
+ passed: !tree.truncated && missingCreates.length === 0 && propertyMismatches.length === 0 && moveMismatches.length === 0 && metadataMismatches.length === 0 && inspectFailures.length === 0,
1760
2377
  treeNodeCount: tree.nodeCount,
1761
2378
  treeTruncated: tree.truncated,
1762
2379
  expectedCreates: expectedCreates.length,
@@ -1764,6 +2381,7 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1764
2381
  missingCreates,
1765
2382
  propertyMismatches,
1766
2383
  moveMismatches,
2384
+ metadataMismatches,
1767
2385
  inspectFailures
1768
2386
  };
1769
2387
  }
@@ -1772,12 +2390,17 @@ function findRepairOperations(operations, verification, originalRoot) {
1772
2390
  const missingCreates = new Set(verification.missingCreates);
1773
2391
  const mismatchedTargets = new Set(verification.propertyMismatches.map((item) => item.target));
1774
2392
  const mismatchedMoves = new Set(verification.moveMismatches.map((item) => item.target));
2393
+ const mismatchedMetadata = new Set(verification.metadataMismatches.map((item) => item.target));
1775
2394
  return operations.filter((operation) => {
1776
2395
  if (operation.op === "create") {
1777
2396
  const parentPath = resolveRefPath(operation.parent, knownPaths);
1778
2397
  return Boolean(parentPath && missingCreates.has(pathKey([...parentPath, operation.name])));
1779
2398
  }
2399
+ if (operation.op === "clone") return false;
1780
2400
  if (operation.op === "setProperties") return mismatchedTargets.has(refLabel(operation.target));
2401
+ if (operation.op === "setAttributes" || operation.op === "updateTags") {
2402
+ return mismatchedMetadata.has(refLabel(operation.target));
2403
+ }
1781
2404
  return mismatchedMoves.has(refLabel(operation.target));
1782
2405
  });
1783
2406
  }
@@ -1846,7 +2469,7 @@ function isParallelRefWithinScopes(ref2, scopes) {
1846
2469
  return scopes.some((scope) => Boolean(ref2.instanceId && scope.instanceId === ref2.instanceId));
1847
2470
  }
1848
2471
  function operationTarget(operation) {
1849
- if (operation.op === "create") {
2472
+ if (operation.op === "create" || operation.op === "clone") {
1850
2473
  const parentPath = operation.parent.pathSegments;
1851
2474
  return {
1852
2475
  key: `${refKey(operation.parent)}/create:${operation.name}`,
@@ -1951,37 +2574,50 @@ function emit(server, type, data) {
1951
2574
  function toErrorMessage(error) {
1952
2575
  return error instanceof Error ? error.message : String(error);
1953
2576
  }
1954
- var propertiesSchema2 = z.record(z.unknown()).refine(
2577
+ var propertiesSchema3 = z.record(z.unknown()).refine(
1955
2578
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1956
2579
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
1957
2580
  );
1958
2581
  var setPropertiesOperation = z.object({
1959
2582
  op: z.literal("setProperties"),
1960
2583
  target: instanceRefSchema,
1961
- properties: propertiesSchema2
2584
+ properties: propertiesSchema3
1962
2585
  });
1963
2586
  var createOperation = z.object({
1964
2587
  op: z.literal("create"),
1965
2588
  parent: instanceRefSchema,
1966
2589
  className: z.string().min(1).max(100),
1967
2590
  name: z.string().min(1).max(256),
1968
- properties: propertiesSchema2.optional()
2591
+ properties: propertiesSchema3.optional()
1969
2592
  });
1970
2593
  var moveOperation = z.object({
1971
2594
  op: z.literal("move"),
1972
2595
  target: instanceRefSchema,
1973
2596
  parent: instanceRefSchema
1974
2597
  });
2598
+ var cloneOperation = z.object({
2599
+ op: z.literal("clone"),
2600
+ target: instanceRefSchema,
2601
+ parent: instanceRefSchema.optional(),
2602
+ name: z.string().min(1).max(100).optional(),
2603
+ offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
2604
+ properties: propertiesSchema3.optional()
2605
+ });
2606
+ var setAttributesOperation = attributeUpdateSchema.extend({ op: z.literal("setAttributes") });
2607
+ var updateTagsOperation = tagUpdateSchema.extend({ op: z.literal("updateTags") });
1975
2608
  var mutationOperation = z.discriminatedUnion("op", [
1976
2609
  setPropertiesOperation,
1977
2610
  createOperation,
1978
- moveOperation
2611
+ cloneOperation,
2612
+ moveOperation,
2613
+ setAttributesOperation,
2614
+ updateTagsOperation
1979
2615
  ]);
1980
2616
  var uiNodeSchema = z.lazy(
1981
2617
  () => z.object({
1982
2618
  ClassName: z.string().min(1).max(100),
1983
2619
  Name: z.string().min(1).max(256).optional(),
1984
- properties: propertiesSchema2.optional(),
2620
+ properties: propertiesSchema3.optional(),
1985
2621
  Children: z.array(uiNodeSchema).max(1e3).optional()
1986
2622
  })
1987
2623
  );
@@ -2085,7 +2721,7 @@ function registerStudioTools(server, bridge) {
2085
2721
  "studio_apply",
2086
2722
  {
2087
2723
  title: "Apply Atomic Studio Mutations",
2088
- description: "Atomically set properties, create instances, and move instances. Any failed operation cancels and undoes the entire batch.",
2724
+ description: "Atomically set properties or typed attributes, update tags, create instances, deep-clone instances, and move instances. Any failed operation cancels and undoes the entire batch.",
2089
2725
  inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
2090
2726
  outputSchema: toolOutputSchema,
2091
2727
  annotations: {
@@ -2211,7 +2847,7 @@ function registerStudioTools(server, bridge) {
2211
2847
  "studio_get_reflection",
2212
2848
  {
2213
2849
  title: "Read Live Roblox Reflection",
2214
- description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata.",
2850
+ description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata. Reflection discovers APIs but does not grant call permission; use studio_list_callable_methods for that.",
2215
2851
  inputSchema: { className: z.string().min(1).max(100) },
2216
2852
  outputSchema: toolOutputSchema,
2217
2853
  annotations: {
@@ -2226,7 +2862,7 @@ function registerStudioTools(server, bridge) {
2226
2862
  }
2227
2863
 
2228
2864
  // src/mcp/server.ts
2229
- var VERSION = "2.2.1";
2865
+ var VERSION = "2.4.0";
2230
2866
  function createDominusMcpServer(bridge) {
2231
2867
  const server = new McpServer(
2232
2868
  {
@@ -2239,6 +2875,8 @@ function createDominusMcpServer(bridge) {
2239
2875
  );
2240
2876
  registerConnectionTools(server, bridge);
2241
2877
  registerStudioTools(server, bridge);
2878
+ registerBuildingTools(server, bridge);
2879
+ registerMetadataTools(server, bridge);
2242
2880
  registerParallelTaskTool(server, bridge);
2243
2881
  registerDocsTools(server);
2244
2882
  registerDominusResources(server, bridge);