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/CHANGELOG.md +42 -1
- package/README.md +18 -3
- package/dist/bridge-daemon.js +9 -0
- package/dist/bridge-daemon.js.map +1 -1
- package/dist/install-plugin.js +84 -5
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +470 -32
- package/dist/mcp.js.map +1 -1
- package/package.json +6 -2
- package/plugin/Dominus.rbxmx +572 -8
- package/plugin/src/CommandRouter.lua +16 -0
- package/plugin/src/Metadata.lua +411 -0
- package/plugin/src/Mutation.lua +15 -0
- package/plugin/src/ValueCodec.lua +116 -0
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
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
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
|
|
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.
|
|
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);
|