dominus-cli 2.3.0 → 2.4.1

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
@@ -74,6 +74,7 @@ var ControllerMsg = {
74
74
  WELCOME: "controller:welcome",
75
75
  RELAY: "controller:relay",
76
76
  TARGET_STATE: "controller:target_state",
77
+ GET_TARGET_STATE: "controller:get_target_state",
77
78
  SET_ACTIVE_CONNECTION: "controller:set_active_connection"
78
79
  };
79
80
  var CliMsg = {
@@ -117,6 +118,11 @@ var CliMsg = {
117
118
  V2_CREATE_WELDS: "studio:v2:create_welds",
118
119
  V2_QUERY_PARTS: "studio:v2:query_parts",
119
120
  V2_SET_SELECTION: "studio:v2:set_selection",
121
+ V2_GET_METADATA: "studio:v2:get_metadata",
122
+ V2_SET_ATTRIBUTES: "studio:v2:set_attributes",
123
+ V2_UPDATE_TAGS: "studio:v2:update_tags",
124
+ V2_LIST_CALLABLE_METHODS: "studio:v2:list_callable_methods",
125
+ V2_INVOKE_METHODS: "studio:v2:invoke_methods",
120
126
  V2_DELETE: "studio:v2:delete",
121
127
  V2_BUILD_UI: "studio:v2:build_ui",
122
128
  V2_SNAPSHOT_UI: "studio:v2:snapshot_ui",
@@ -175,16 +181,45 @@ var DominusClient = class extends EventEmitter {
175
181
  port;
176
182
  token;
177
183
  connectTimeoutMs;
184
+ autoReconnect;
185
+ reconnectDelayMs;
186
+ maxReconnectDelayMs;
178
187
  connections = /* @__PURE__ */ new Map();
179
188
  activeConnectionId = null;
189
+ connectPromise = null;
190
+ reconnectTimer = null;
191
+ nextReconnectDelayMs;
192
+ hasAuthenticated = false;
193
+ stopping = false;
180
194
  pendingRequests = /* @__PURE__ */ new Map();
181
195
  constructor(port = 18088, options = {}) {
182
196
  super();
183
197
  this.port = port;
184
198
  this.token = options.token ?? loadOrCreateBridgeToken();
185
199
  this.connectTimeoutMs = options.connectTimeoutMs ?? 8e3;
200
+ this.autoReconnect = options.autoReconnect ?? true;
201
+ this.reconnectDelayMs = options.reconnectDelayMs ?? 250;
202
+ this.maxReconnectDelayMs = options.maxReconnectDelayMs ?? 5e3;
203
+ this.nextReconnectDelayMs = this.reconnectDelayMs;
186
204
  }
187
205
  connect() {
206
+ if (this.ws?.readyState === WebSocket.OPEN) return Promise.resolve();
207
+ if (this.connectPromise) return this.connectPromise;
208
+ this.stopping = false;
209
+ this.clearReconnectTimer();
210
+ const attempt = this.openConnection();
211
+ this.connectPromise = attempt;
212
+ void attempt.then(
213
+ () => {
214
+ if (this.connectPromise === attempt) this.connectPromise = null;
215
+ },
216
+ () => {
217
+ if (this.connectPromise === attempt) this.connectPromise = null;
218
+ }
219
+ );
220
+ return attempt;
221
+ }
222
+ openConnection() {
188
223
  return new Promise((resolve, reject) => {
189
224
  const ws = new WebSocket(`ws://127.0.0.1:${this.port}`, {
190
225
  perMessageDeflate: false,
@@ -216,6 +251,8 @@ var DominusClient = class extends EventEmitter {
216
251
  settled = true;
217
252
  clearTimeout(timer);
218
253
  this.applyWelcome(msg.payload);
254
+ this.hasAuthenticated = true;
255
+ this.nextReconnectDelayMs = this.reconnectDelayMs;
219
256
  this.emit("server:started", { port: this.port, relay: true });
220
257
  resolve();
221
258
  return;
@@ -235,15 +272,21 @@ var DominusClient = class extends EventEmitter {
235
272
  });
236
273
  ws.on("close", () => {
237
274
  clearTimeout(timer);
238
- this.ws = null;
239
- this.connections.clear();
240
- this.activeConnectionId = null;
275
+ const wasCurrentSocket = this.ws === ws;
276
+ if (wasCurrentSocket) {
277
+ this.ws = null;
278
+ this.connections.clear();
279
+ this.activeConnectionId = null;
280
+ }
241
281
  if (!settled) {
242
282
  settled = true;
243
283
  reject(new Error("Dominus bridge closed before authentication completed"));
244
284
  }
245
- this.rejectPending(new Error("Dominus relay connection lost"));
246
- this.emit("studio:disconnected");
285
+ if (wasCurrentSocket) {
286
+ this.rejectPending(new Error("Dominus relay connection lost"));
287
+ this.emit("studio:disconnected");
288
+ this.scheduleReconnect();
289
+ }
247
290
  });
248
291
  ws.on("error", (err) => {
249
292
  if (!settled) {
@@ -255,6 +298,26 @@ var DominusClient = class extends EventEmitter {
255
298
  });
256
299
  });
257
300
  }
301
+ scheduleReconnect() {
302
+ if (!this.autoReconnect || !this.hasAuthenticated || this.stopping || this.reconnectTimer || this.ws?.readyState === WebSocket.OPEN) {
303
+ return;
304
+ }
305
+ const delayMs = this.nextReconnectDelayMs;
306
+ this.nextReconnectDelayMs = Math.min(delayMs * 2, this.maxReconnectDelayMs);
307
+ const timer = setTimeout(() => {
308
+ if (this.reconnectTimer !== timer) return;
309
+ this.reconnectTimer = null;
310
+ if (this.stopping) return;
311
+ void this.connect().catch(() => this.scheduleReconnect());
312
+ }, delayMs);
313
+ timer.unref?.();
314
+ this.reconnectTimer = timer;
315
+ }
316
+ clearReconnectTimer() {
317
+ if (!this.reconnectTimer) return;
318
+ clearTimeout(this.reconnectTimer);
319
+ this.reconnectTimer = null;
320
+ }
258
321
  applyWelcome(payload) {
259
322
  const welcome = payload;
260
323
  this.replaceConnections(welcome.connections ?? []);
@@ -407,10 +470,28 @@ var DominusClient = class extends EventEmitter {
407
470
  active: connection.connectionId === this.activeConnectionId
408
471
  }));
409
472
  }
473
+ async refreshConnections() {
474
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) await this.connect();
475
+ const result = await this.sendControlRequest(ControllerMsg.GET_TARGET_STATE, {});
476
+ this.replaceConnections(result.connections ?? []);
477
+ if (this.activeConnectionId && !this.connections.has(this.activeConnectionId)) {
478
+ this.activeConnectionId = null;
479
+ }
480
+ if (!this.activeConnectionId && result.defaultActiveConnectionId && this.connections.has(result.defaultActiveConnectionId)) {
481
+ this.activeConnectionId = result.defaultActiveConnectionId;
482
+ }
483
+ if (!this.activeConnectionId && this.connections.size === 1) {
484
+ this.activeConnectionId = this.connections.keys().next().value ?? null;
485
+ }
486
+ return this.listConnections();
487
+ }
410
488
  getActiveConnectionId() {
411
489
  return this.activeConnectionId;
412
490
  }
413
491
  async setActiveConnectionId(connectionId) {
492
+ if (connectionId && !this.connections.has(connectionId)) {
493
+ await this.refreshConnections();
494
+ }
414
495
  if (connectionId && !this.connections.has(connectionId)) {
415
496
  throw new Error(`Studio connection ${connectionId} is not connected`);
416
497
  }
@@ -444,6 +525,9 @@ var DominusClient = class extends EventEmitter {
444
525
  return this.port;
445
526
  }
446
527
  stop() {
528
+ this.stopping = true;
529
+ this.hasAuthenticated = false;
530
+ this.clearReconnectTimer();
447
531
  this.rejectPending(new Error("Dominus client is shutting down"));
448
532
  return new Promise((resolve) => {
449
533
  if (!this.ws) {
@@ -572,6 +656,8 @@ For broad tasks with independent Studio branches, prefer run_parallel_task when
572
656
  Building workflow:
573
657
  - Use studio_create_parts for multi-part structures instead of many generic create operations.
574
658
  - Use studio_clone_instances when an existing instance or model is the intended template. Clone rather than manually recreating descendants and properties.
659
+ - 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.
660
+ - 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.
575
661
  - Use studio_transform_instances for Models and BaseParts so model layouts move through PVInstance pivots.
576
662
  - 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.
577
663
  - Use studio_set_selection when surfacing completed objects to the Studio user.
@@ -580,7 +666,7 @@ Safety rules:
580
666
  - Never invent instance IDs or Roblox API members.
581
667
  - Do not retry a failed write unchanged. Inspect the error and current state first.
582
668
  - Deletion is a separate destructive tool and requires explicit user intent.
583
- - studio_apply is transactional and supports setProperties, create, clone, and move operations. Keep related edits together, but stay within its documented limits.
669
+ - studio_apply is transactional and supports setProperties, setAttributes, updateTags, create, clone, and move operations. Keep related edits together, but stay within its documented limits.
584
670
  - UI fidelity work must snapshot and inspect the result after building; verify fonts, UDim2 values, colors, strokes, and hierarchy.
585
671
  - Dominus intentionally has no arbitrary Luau execution tool.`;
586
672
 
@@ -594,23 +680,26 @@ function registerDominusResources(server, bridge) {
594
680
  description: "Current authenticated Roblox Studio sessions and selected target.",
595
681
  mimeType: "application/json"
596
682
  },
597
- async (uri) => ({
598
- contents: [
599
- {
600
- uri: uri.href,
601
- mimeType: "application/json",
602
- text: JSON.stringify(
603
- {
604
- protocolVersion: 2,
605
- activeConnectionId: bridge.getActiveConnectionId(),
606
- connections: bridge.listConnections()
607
- },
608
- null,
609
- 2
610
- )
611
- }
612
- ]
613
- })
683
+ async (uri) => {
684
+ const connections = bridge.refreshConnections ? await bridge.refreshConnections() : bridge.listConnections();
685
+ return {
686
+ contents: [
687
+ {
688
+ uri: uri.href,
689
+ mimeType: "application/json",
690
+ text: JSON.stringify(
691
+ {
692
+ protocolVersion: 2,
693
+ activeConnectionId: bridge.getActiveConnectionId(),
694
+ connections
695
+ },
696
+ null,
697
+ 2
698
+ )
699
+ }
700
+ ]
701
+ };
702
+ }
614
703
  );
615
704
  server.registerPrompt(
616
705
  "dominus-workflow",
@@ -937,11 +1026,12 @@ function registerConnectionTools(server, bridge) {
937
1026
  },
938
1027
  async () => {
939
1028
  try {
1029
+ const connections = bridge.refreshConnections ? await bridge.refreshConnections() : bridge.listConnections();
940
1030
  return success({
941
1031
  protocolVersion: 2,
942
1032
  port: bridge.getPort(),
943
1033
  activeConnectionId: bridge.getActiveConnectionId(),
944
- connections: bridge.listConnections()
1034
+ connections
945
1035
  });
946
1036
  } catch (err) {
947
1037
  return failure(err);
@@ -1447,6 +1537,260 @@ function registerDocsTools(server) {
1447
1537
  }
1448
1538
  );
1449
1539
  }
1540
+ var finiteNumber2 = z.number().finite();
1541
+ var vector2Value = z.object({ x: finiteNumber2, y: finiteNumber2 }).strict();
1542
+ var vector3Value = z.object({ x: finiteNumber2, y: finiteNumber2, z: finiteNumber2 }).strict();
1543
+ var color3Object = z.object({ r: finiteNumber2, g: finiteNumber2, b: finiteNumber2 }).strict();
1544
+ var color3Value = z.union([
1545
+ z.string().min(1).max(100),
1546
+ color3Object,
1547
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])
1548
+ ]);
1549
+ var cframeValue = z.array(finiteNumber2).refine((value) => value.length === 3 || value.length === 12, "CFrame requires 3 or 12 numbers");
1550
+ var numberSequenceKeypoint = z.union([
1551
+ z.tuple([finiteNumber2, finiteNumber2]),
1552
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])
1553
+ ]);
1554
+ var colorSequenceKeypoint = z.tuple([finiteNumber2, color3Value]);
1555
+ var typedString = z.object({ type: z.literal("string"), value: z.string() }).strict();
1556
+ var typedBoolean = z.object({ type: z.literal("boolean"), value: z.boolean() }).strict();
1557
+ var typedNumber = z.object({ type: z.literal("number"), value: finiteNumber2 }).strict();
1558
+ var typedUDim = z.object({
1559
+ type: z.literal("UDim"),
1560
+ value: z.union([
1561
+ z.object({ scale: finiteNumber2, offset: finiteNumber2 }).strict(),
1562
+ z.tuple([finiteNumber2, finiteNumber2])
1563
+ ])
1564
+ }).strict();
1565
+ var typedUDim2 = z.object({
1566
+ type: z.literal("UDim2"),
1567
+ value: z.union([
1568
+ z.object({
1569
+ xScale: finiteNumber2,
1570
+ xOffset: finiteNumber2,
1571
+ yScale: finiteNumber2,
1572
+ yOffset: finiteNumber2
1573
+ }).strict(),
1574
+ z.tuple([finiteNumber2, finiteNumber2, finiteNumber2, finiteNumber2])
1575
+ ])
1576
+ }).strict();
1577
+ var typedBrickColor = z.object({ type: z.literal("BrickColor"), value: z.string().min(1).max(100) }).strict();
1578
+ var typedColor3 = z.object({ type: z.literal("Color3"), value: color3Value }).strict();
1579
+ var typedVector2 = z.object({
1580
+ type: z.literal("Vector2"),
1581
+ value: z.union([vector2Value, z.tuple([finiteNumber2, finiteNumber2])])
1582
+ }).strict();
1583
+ var typedVector3 = z.object({
1584
+ type: z.literal("Vector3"),
1585
+ value: z.union([vector3Value, z.tuple([finiteNumber2, finiteNumber2, finiteNumber2])])
1586
+ }).strict();
1587
+ var typedCFrame = z.object({ type: z.literal("CFrame"), value: cframeValue }).strict();
1588
+ var typedNumberSequence = z.object({
1589
+ type: z.literal("NumberSequence"),
1590
+ value: z.union([finiteNumber2, z.array(numberSequenceKeypoint).min(1).max(100)])
1591
+ }).strict();
1592
+ var typedColorSequence = z.object({
1593
+ type: z.literal("ColorSequence"),
1594
+ value: z.union([color3Value, z.array(colorSequenceKeypoint).min(1).max(100)])
1595
+ }).strict();
1596
+ var typedNumberRange = z.object({
1597
+ type: z.literal("NumberRange"),
1598
+ value: z.union([finiteNumber2, z.tuple([finiteNumber2, finiteNumber2])])
1599
+ }).strict();
1600
+ var typedRect = z.object({
1601
+ type: z.literal("Rect"),
1602
+ value: z.tuple([finiteNumber2, finiteNumber2, finiteNumber2, finiteNumber2])
1603
+ }).strict();
1604
+ var typedFont = z.object({
1605
+ type: z.literal("Font"),
1606
+ value: z.union([
1607
+ z.object({
1608
+ family: z.string().min(1).max(500),
1609
+ weight: z.string().min(1).max(100).optional(),
1610
+ style: z.string().min(1).max(100).optional()
1611
+ }).strict(),
1612
+ z.object({
1613
+ Family: z.string().min(1).max(500),
1614
+ Weight: z.string().min(1).max(100).optional(),
1615
+ Style: z.string().min(1).max(100).optional()
1616
+ }).strict()
1617
+ ])
1618
+ }).strict();
1619
+ var attributeValueSchema = z.discriminatedUnion("type", [
1620
+ typedString,
1621
+ typedBoolean,
1622
+ typedNumber,
1623
+ typedUDim,
1624
+ typedUDim2,
1625
+ typedBrickColor,
1626
+ typedColor3,
1627
+ typedVector2,
1628
+ typedVector3,
1629
+ typedCFrame,
1630
+ typedNumberSequence,
1631
+ typedColorSequence,
1632
+ typedNumberRange,
1633
+ typedRect,
1634
+ typedFont
1635
+ ]);
1636
+ var methodArgumentSchema = z.discriminatedUnion("type", [
1637
+ z.object({ type: z.literal("nil") }).strict(),
1638
+ typedString,
1639
+ typedBoolean,
1640
+ typedNumber,
1641
+ typedUDim,
1642
+ typedUDim2,
1643
+ typedBrickColor,
1644
+ typedColor3,
1645
+ typedVector2,
1646
+ typedVector3,
1647
+ typedCFrame,
1648
+ typedNumberSequence,
1649
+ typedColorSequence,
1650
+ typedNumberRange,
1651
+ typedRect,
1652
+ typedFont,
1653
+ z.object({ type: z.literal("Instance"), value: instanceRefSchema }).strict()
1654
+ ]);
1655
+ var callableMethodSchema = z.enum([
1656
+ "GetAttribute",
1657
+ "GetAttributes",
1658
+ "GetTags",
1659
+ "HasTag",
1660
+ "GetFullName",
1661
+ "IsA",
1662
+ "IsAncestorOf",
1663
+ "IsDescendantOf",
1664
+ "FindFirstChild",
1665
+ "FindFirstChildOfClass",
1666
+ "FindFirstChildWhichIsA",
1667
+ "FindFirstAncestor",
1668
+ "FindFirstAncestorOfClass",
1669
+ "FindFirstAncestorWhichIsA",
1670
+ "GetChildren",
1671
+ "GetDescendants",
1672
+ "GetPivot",
1673
+ "GetScale",
1674
+ "GetBoundingBox",
1675
+ "AddTag",
1676
+ "RemoveTag",
1677
+ "SetAttribute",
1678
+ "PivotTo",
1679
+ "ScaleTo"
1680
+ ]);
1681
+ var attributeUpdateSchema = z.object({
1682
+ target: instanceRefSchema,
1683
+ set: z.array(
1684
+ z.object({
1685
+ name: z.string().min(1).max(100),
1686
+ value: attributeValueSchema
1687
+ })
1688
+ ).max(100).optional().default([]),
1689
+ remove: z.array(z.string().min(1).max(100)).max(100).optional().default([])
1690
+ });
1691
+ var tagUpdateSchema = z.object({
1692
+ target: instanceRefSchema,
1693
+ add: z.array(z.string().min(1).max(100)).max(100).optional().default([]),
1694
+ remove: z.array(z.string().min(1).max(100)).max(100).optional().default([])
1695
+ });
1696
+ function registerMetadataTools(server, bridge) {
1697
+ server.registerTool(
1698
+ "studio_get_metadata",
1699
+ {
1700
+ title: "Get Roblox Tags and Attributes",
1701
+ 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.",
1702
+ inputSchema: {
1703
+ targets: z.array(instanceRefSchema).min(1).max(100)
1704
+ },
1705
+ outputSchema: toolOutputSchema,
1706
+ annotations: {
1707
+ readOnlyHint: true,
1708
+ destructiveHint: false,
1709
+ idempotentHint: true,
1710
+ openWorldHint: false
1711
+ }
1712
+ },
1713
+ ({ targets }) => callStudio(bridge, CliMsg.V2_GET_METADATA, { targets })
1714
+ );
1715
+ server.registerTool(
1716
+ "studio_set_attributes",
1717
+ {
1718
+ title: "Set Roblox Attributes",
1719
+ 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.",
1720
+ inputSchema: {
1721
+ operations: z.array(attributeUpdateSchema).min(1).max(100)
1722
+ },
1723
+ outputSchema: toolOutputSchema,
1724
+ annotations: {
1725
+ readOnlyHint: false,
1726
+ destructiveHint: false,
1727
+ idempotentHint: true,
1728
+ openWorldHint: false
1729
+ }
1730
+ },
1731
+ ({ operations }) => callStudio(bridge, CliMsg.V2_SET_ATTRIBUTES, { operations }, 6e4)
1732
+ );
1733
+ server.registerTool(
1734
+ "studio_update_tags",
1735
+ {
1736
+ title: "Update Roblox Tags",
1737
+ 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.",
1738
+ inputSchema: {
1739
+ operations: z.array(tagUpdateSchema).min(1).max(100)
1740
+ },
1741
+ outputSchema: toolOutputSchema,
1742
+ annotations: {
1743
+ readOnlyHint: false,
1744
+ destructiveHint: false,
1745
+ idempotentHint: true,
1746
+ openWorldHint: false
1747
+ }
1748
+ },
1749
+ ({ operations }) => callStudio(bridge, CliMsg.V2_UPDATE_TAGS, { operations }, 6e4)
1750
+ );
1751
+ server.registerTool(
1752
+ "studio_list_callable_methods",
1753
+ {
1754
+ title: "List Safe Instance Methods",
1755
+ 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.",
1756
+ inputSchema: {
1757
+ target: instanceRefSchema
1758
+ },
1759
+ outputSchema: toolOutputSchema,
1760
+ annotations: {
1761
+ readOnlyHint: true,
1762
+ destructiveHint: false,
1763
+ idempotentHint: true,
1764
+ openWorldHint: false
1765
+ }
1766
+ },
1767
+ ({ target }) => callStudio(bridge, CliMsg.V2_LIST_CALLABLE_METHODS, { target })
1768
+ );
1769
+ server.registerTool(
1770
+ "studio_invoke_methods",
1771
+ {
1772
+ title: "Invoke Safe Instance Methods",
1773
+ 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.",
1774
+ inputSchema: {
1775
+ calls: z.array(
1776
+ z.object({
1777
+ target: instanceRefSchema,
1778
+ method: callableMethodSchema,
1779
+ arguments: z.array(methodArgumentSchema).max(10).optional().default([])
1780
+ })
1781
+ ).min(1).max(50)
1782
+ },
1783
+ outputSchema: toolOutputSchema,
1784
+ annotations: {
1785
+ readOnlyHint: false,
1786
+ destructiveHint: false,
1787
+ idempotentHint: false,
1788
+ openWorldHint: false
1789
+ }
1790
+ },
1791
+ ({ calls }) => callStudio(bridge, CliMsg.V2_INVOKE_METHODS, { calls }, 6e4)
1792
+ );
1793
+ }
1450
1794
  var propertiesSchema2 = z.record(z.unknown()).refine(
1451
1795
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1452
1796
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
@@ -1476,7 +1820,9 @@ var mutationOperationSchema = z.discriminatedUnion("op", [
1476
1820
  name: z.string().min(1).max(100),
1477
1821
  offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
1478
1822
  properties: propertiesSchema2.optional()
1479
- })
1823
+ }),
1824
+ attributeUpdateSchema.extend({ op: z.literal("setAttributes") }),
1825
+ tagUpdateSchema.extend({ op: z.literal("updateTags") })
1480
1826
  ]);
1481
1827
  var workerProposalSchema = z.object({
1482
1828
  workerId: z.string().min(1).max(64),
@@ -1704,7 +2050,7 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1704
2050
  systemPrompt: [
1705
2051
  "You are a read-only Dominus worker. Inspect only the Studio evidence in the request.",
1706
2052
  "You cannot write to Studio. Return one strict JSON WorkerProposal and no Markdown.",
1707
- "Only propose setProperties, create, clone, or move operations using exact instance refs copied from evidence.",
2053
+ "Only propose setProperties, setAttributes, updateTags, create, clone, or move operations using exact instance refs copied from evidence.",
1708
2054
  "Never invent instanceId values. Never propose delete, script edits, or arbitrary execution."
1709
2055
  ].join(" "),
1710
2056
  messages: [
@@ -1742,6 +2088,18 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1742
2088
  parent: { instanceId: "uuid", pathSegments: ["..."] },
1743
2089
  name: "CloneName",
1744
2090
  offset: { x: 0, y: 0, z: 0 }
2091
+ },
2092
+ {
2093
+ op: "setAttributes",
2094
+ target: { instanceId: "uuid", pathSegments: ["..."] },
2095
+ set: [{ name: "PlotId", value: { type: "number", value: 1 } }],
2096
+ remove: []
2097
+ },
2098
+ {
2099
+ op: "updateTags",
2100
+ target: { instanceId: "uuid", pathSegments: ["..."] },
2101
+ add: ["Plot"],
2102
+ remove: []
1745
2103
  }
1746
2104
  ],
1747
2105
  risks: ["string"],
@@ -1978,11 +2336,12 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1978
2336
  });
1979
2337
  const missingCreates = expectedCreates.filter((path5) => !paths.has(path5));
1980
2338
  const inspectOperations = operations.filter(
1981
- (operation) => operation.op !== "create" && operation.op !== "clone"
2339
+ (operation) => operation.op === "setProperties" || operation.op === "move"
1982
2340
  );
1983
2341
  const inspectFailures = [];
1984
2342
  const propertyMismatches = [];
1985
2343
  const moveMismatches = [];
2344
+ const metadataMismatches = [];
1986
2345
  for (let offset = 0; offset < inspectOperations.length; offset += 20) {
1987
2346
  const batch = inspectOperations.slice(offset, offset + 20);
1988
2347
  const targets = batch.map((operation) => operation.target);
@@ -2027,8 +2386,77 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
2027
2386
  }
2028
2387
  });
2029
2388
  }
2389
+ const metadataOperations = operations.filter(
2390
+ (operation) => operation.op === "setAttributes" || operation.op === "updateTags"
2391
+ );
2392
+ for (let offset = 0; offset < metadataOperations.length; offset += 100) {
2393
+ const batch = metadataOperations.slice(offset, offset + 100);
2394
+ const metadata = await requestStudio(bridge, CliMsg.V2_GET_METADATA, {
2395
+ targets: batch.map((operation) => operation.target)
2396
+ });
2397
+ const results = Array.isArray(metadata.results) ? metadata.results : [];
2398
+ batch.forEach((operation, index) => {
2399
+ const result = results[index];
2400
+ const targetLabel = refLabel(operation.target);
2401
+ if (!result) {
2402
+ inspectFailures.push(`${targetLabel}: missing metadata inspection result`);
2403
+ return;
2404
+ }
2405
+ if (operation.op === "setAttributes") {
2406
+ const attributes = Array.isArray(result.attributes) ? result.attributes : [];
2407
+ const actual = new Map(attributes.map((entry) => [String(entry.name), entry.value]));
2408
+ for (const item of operation.set) {
2409
+ const actualValue = actual.get(item.name);
2410
+ if (!actual.has(item.name) || !valuesEquivalent(item.value, actualValue)) {
2411
+ metadataMismatches.push({
2412
+ target: targetLabel,
2413
+ kind: "attribute",
2414
+ name: item.name,
2415
+ expected: item.value,
2416
+ actual: actualValue
2417
+ });
2418
+ }
2419
+ }
2420
+ for (const name of operation.remove) {
2421
+ if (actual.has(name)) {
2422
+ metadataMismatches.push({
2423
+ target: targetLabel,
2424
+ kind: "attribute",
2425
+ name,
2426
+ expected: "absent",
2427
+ actual: actual.get(name)
2428
+ });
2429
+ }
2430
+ }
2431
+ } else {
2432
+ const tags = new Set(Array.isArray(result.tags) ? result.tags.map(String) : []);
2433
+ for (const name of operation.add) {
2434
+ if (!tags.has(name)) {
2435
+ metadataMismatches.push({
2436
+ target: targetLabel,
2437
+ kind: "tag",
2438
+ name,
2439
+ expected: true,
2440
+ actual: false
2441
+ });
2442
+ }
2443
+ }
2444
+ for (const name of operation.remove) {
2445
+ if (tags.has(name)) {
2446
+ metadataMismatches.push({
2447
+ target: targetLabel,
2448
+ kind: "tag",
2449
+ name,
2450
+ expected: false,
2451
+ actual: true
2452
+ });
2453
+ }
2454
+ }
2455
+ }
2456
+ });
2457
+ }
2030
2458
  return {
2031
- passed: !tree.truncated && missingCreates.length === 0 && propertyMismatches.length === 0 && moveMismatches.length === 0 && inspectFailures.length === 0,
2459
+ passed: !tree.truncated && missingCreates.length === 0 && propertyMismatches.length === 0 && moveMismatches.length === 0 && metadataMismatches.length === 0 && inspectFailures.length === 0,
2032
2460
  treeNodeCount: tree.nodeCount,
2033
2461
  treeTruncated: tree.truncated,
2034
2462
  expectedCreates: expectedCreates.length,
@@ -2036,6 +2464,7 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
2036
2464
  missingCreates,
2037
2465
  propertyMismatches,
2038
2466
  moveMismatches,
2467
+ metadataMismatches,
2039
2468
  inspectFailures
2040
2469
  };
2041
2470
  }
@@ -2044,6 +2473,7 @@ function findRepairOperations(operations, verification, originalRoot) {
2044
2473
  const missingCreates = new Set(verification.missingCreates);
2045
2474
  const mismatchedTargets = new Set(verification.propertyMismatches.map((item) => item.target));
2046
2475
  const mismatchedMoves = new Set(verification.moveMismatches.map((item) => item.target));
2476
+ const mismatchedMetadata = new Set(verification.metadataMismatches.map((item) => item.target));
2047
2477
  return operations.filter((operation) => {
2048
2478
  if (operation.op === "create") {
2049
2479
  const parentPath = resolveRefPath(operation.parent, knownPaths);
@@ -2051,6 +2481,9 @@ function findRepairOperations(operations, verification, originalRoot) {
2051
2481
  }
2052
2482
  if (operation.op === "clone") return false;
2053
2483
  if (operation.op === "setProperties") return mismatchedTargets.has(refLabel(operation.target));
2484
+ if (operation.op === "setAttributes" || operation.op === "updateTags") {
2485
+ return mismatchedMetadata.has(refLabel(operation.target));
2486
+ }
2054
2487
  return mismatchedMoves.has(refLabel(operation.target));
2055
2488
  });
2056
2489
  }
@@ -2253,11 +2686,15 @@ var cloneOperation = z.object({
2253
2686
  offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
2254
2687
  properties: propertiesSchema3.optional()
2255
2688
  });
2689
+ var setAttributesOperation = attributeUpdateSchema.extend({ op: z.literal("setAttributes") });
2690
+ var updateTagsOperation = tagUpdateSchema.extend({ op: z.literal("updateTags") });
2256
2691
  var mutationOperation = z.discriminatedUnion("op", [
2257
2692
  setPropertiesOperation,
2258
2693
  createOperation,
2259
2694
  cloneOperation,
2260
- moveOperation
2695
+ moveOperation,
2696
+ setAttributesOperation,
2697
+ updateTagsOperation
2261
2698
  ]);
2262
2699
  var uiNodeSchema = z.lazy(
2263
2700
  () => z.object({
@@ -2367,7 +2804,7 @@ function registerStudioTools(server, bridge) {
2367
2804
  "studio_apply",
2368
2805
  {
2369
2806
  title: "Apply Atomic Studio Mutations",
2370
- description: "Atomically set properties, create instances, deep-clone instances, and move instances. Any failed operation cancels and undoes the entire batch.",
2807
+ 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.",
2371
2808
  inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
2372
2809
  outputSchema: toolOutputSchema,
2373
2810
  annotations: {
@@ -2493,7 +2930,7 @@ function registerStudioTools(server, bridge) {
2493
2930
  "studio_get_reflection",
2494
2931
  {
2495
2932
  title: "Read Live Roblox Reflection",
2496
- description: "Read current ReflectionService fields for a class in the connected Studio, including Type, Display category, and permission metadata.",
2933
+ 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.",
2497
2934
  inputSchema: { className: z.string().min(1).max(100) },
2498
2935
  outputSchema: toolOutputSchema,
2499
2936
  annotations: {
@@ -2508,7 +2945,7 @@ function registerStudioTools(server, bridge) {
2508
2945
  }
2509
2946
 
2510
2947
  // src/mcp/server.ts
2511
- var VERSION = "2.3.0";
2948
+ var VERSION = "2.4.0";
2512
2949
  function createDominusMcpServer(bridge) {
2513
2950
  const server = new McpServer(
2514
2951
  {
@@ -2522,6 +2959,7 @@ function createDominusMcpServer(bridge) {
2522
2959
  registerConnectionTools(server, bridge);
2523
2960
  registerStudioTools(server, bridge);
2524
2961
  registerBuildingTools(server, bridge);
2962
+ registerMetadataTools(server, bridge);
2525
2963
  registerParallelTaskTool(server, bridge);
2526
2964
  registerDocsTools(server);
2527
2965
  registerDominusResources(server, bridge);