dominus-cli 2.3.0 → 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
@@ -117,6 +117,11 @@ var CliMsg = {
117
117
  V2_CREATE_WELDS: "studio:v2:create_welds",
118
118
  V2_QUERY_PARTS: "studio:v2:query_parts",
119
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",
120
125
  V2_DELETE: "studio:v2:delete",
121
126
  V2_BUILD_UI: "studio:v2:build_ui",
122
127
  V2_SNAPSHOT_UI: "studio:v2:snapshot_ui",
@@ -572,6 +577,8 @@ For broad tasks with independent Studio branches, prefer run_parallel_task when
572
577
  Building workflow:
573
578
  - Use studio_create_parts for multi-part structures instead of many generic create operations.
574
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.
575
582
  - Use studio_transform_instances for Models and BaseParts so model layouts move through PVInstance pivots.
576
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.
577
584
  - Use studio_set_selection when surfacing completed objects to the Studio user.
@@ -580,7 +587,7 @@ Safety rules:
580
587
  - Never invent instance IDs or Roblox API members.
581
588
  - Do not retry a failed write unchanged. Inspect the error and current state first.
582
589
  - 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.
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.
584
591
  - UI fidelity work must snapshot and inspect the result after building; verify fonts, UDim2 values, colors, strokes, and hierarchy.
585
592
  - Dominus intentionally has no arbitrary Luau execution tool.`;
586
593
 
@@ -1447,6 +1454,260 @@ function registerDocsTools(server) {
1447
1454
  }
1448
1455
  );
1449
1456
  }
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
+ }
1450
1711
  var propertiesSchema2 = z.record(z.unknown()).refine(
1451
1712
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1452
1713
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
@@ -1476,7 +1737,9 @@ var mutationOperationSchema = z.discriminatedUnion("op", [
1476
1737
  name: z.string().min(1).max(100),
1477
1738
  offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
1478
1739
  properties: propertiesSchema2.optional()
1479
- })
1740
+ }),
1741
+ attributeUpdateSchema.extend({ op: z.literal("setAttributes") }),
1742
+ tagUpdateSchema.extend({ op: z.literal("updateTags") })
1480
1743
  ]);
1481
1744
  var workerProposalSchema = z.object({
1482
1745
  workerId: z.string().min(1).max(64),
@@ -1704,7 +1967,7 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1704
1967
  systemPrompt: [
1705
1968
  "You are a read-only Dominus worker. Inspect only the Studio evidence in the request.",
1706
1969
  "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.",
1970
+ "Only propose setProperties, setAttributes, updateTags, create, clone, or move operations using exact instance refs copied from evidence.",
1708
1971
  "Never invent instanceId values. Never propose delete, script edits, or arbitrary execution."
1709
1972
  ].join(" "),
1710
1973
  messages: [
@@ -1742,6 +2005,18 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1742
2005
  parent: { instanceId: "uuid", pathSegments: ["..."] },
1743
2006
  name: "CloneName",
1744
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: []
1745
2020
  }
1746
2021
  ],
1747
2022
  risks: ["string"],
@@ -1978,11 +2253,12 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1978
2253
  });
1979
2254
  const missingCreates = expectedCreates.filter((path5) => !paths.has(path5));
1980
2255
  const inspectOperations = operations.filter(
1981
- (operation) => operation.op !== "create" && operation.op !== "clone"
2256
+ (operation) => operation.op === "setProperties" || operation.op === "move"
1982
2257
  );
1983
2258
  const inspectFailures = [];
1984
2259
  const propertyMismatches = [];
1985
2260
  const moveMismatches = [];
2261
+ const metadataMismatches = [];
1986
2262
  for (let offset = 0; offset < inspectOperations.length; offset += 20) {
1987
2263
  const batch = inspectOperations.slice(offset, offset + 20);
1988
2264
  const targets = batch.map((operation) => operation.target);
@@ -2027,8 +2303,77 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
2027
2303
  }
2028
2304
  });
2029
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
+ }
2030
2375
  return {
2031
- 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,
2032
2377
  treeNodeCount: tree.nodeCount,
2033
2378
  treeTruncated: tree.truncated,
2034
2379
  expectedCreates: expectedCreates.length,
@@ -2036,6 +2381,7 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
2036
2381
  missingCreates,
2037
2382
  propertyMismatches,
2038
2383
  moveMismatches,
2384
+ metadataMismatches,
2039
2385
  inspectFailures
2040
2386
  };
2041
2387
  }
@@ -2044,6 +2390,7 @@ function findRepairOperations(operations, verification, originalRoot) {
2044
2390
  const missingCreates = new Set(verification.missingCreates);
2045
2391
  const mismatchedTargets = new Set(verification.propertyMismatches.map((item) => item.target));
2046
2392
  const mismatchedMoves = new Set(verification.moveMismatches.map((item) => item.target));
2393
+ const mismatchedMetadata = new Set(verification.metadataMismatches.map((item) => item.target));
2047
2394
  return operations.filter((operation) => {
2048
2395
  if (operation.op === "create") {
2049
2396
  const parentPath = resolveRefPath(operation.parent, knownPaths);
@@ -2051,6 +2398,9 @@ function findRepairOperations(operations, verification, originalRoot) {
2051
2398
  }
2052
2399
  if (operation.op === "clone") return false;
2053
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
+ }
2054
2404
  return mismatchedMoves.has(refLabel(operation.target));
2055
2405
  });
2056
2406
  }
@@ -2253,11 +2603,15 @@ var cloneOperation = z.object({
2253
2603
  offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
2254
2604
  properties: propertiesSchema3.optional()
2255
2605
  });
2606
+ var setAttributesOperation = attributeUpdateSchema.extend({ op: z.literal("setAttributes") });
2607
+ var updateTagsOperation = tagUpdateSchema.extend({ op: z.literal("updateTags") });
2256
2608
  var mutationOperation = z.discriminatedUnion("op", [
2257
2609
  setPropertiesOperation,
2258
2610
  createOperation,
2259
2611
  cloneOperation,
2260
- moveOperation
2612
+ moveOperation,
2613
+ setAttributesOperation,
2614
+ updateTagsOperation
2261
2615
  ]);
2262
2616
  var uiNodeSchema = z.lazy(
2263
2617
  () => z.object({
@@ -2367,7 +2721,7 @@ function registerStudioTools(server, bridge) {
2367
2721
  "studio_apply",
2368
2722
  {
2369
2723
  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.",
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.",
2371
2725
  inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
2372
2726
  outputSchema: toolOutputSchema,
2373
2727
  annotations: {
@@ -2493,7 +2847,7 @@ function registerStudioTools(server, bridge) {
2493
2847
  "studio_get_reflection",
2494
2848
  {
2495
2849
  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.",
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.",
2497
2851
  inputSchema: { className: z.string().min(1).max(100) },
2498
2852
  outputSchema: toolOutputSchema,
2499
2853
  annotations: {
@@ -2508,7 +2862,7 @@ function registerStudioTools(server, bridge) {
2508
2862
  }
2509
2863
 
2510
2864
  // src/mcp/server.ts
2511
- var VERSION = "2.3.0";
2865
+ var VERSION = "2.4.0";
2512
2866
  function createDominusMcpServer(bridge) {
2513
2867
  const server = new McpServer(
2514
2868
  {
@@ -2522,6 +2876,7 @@ function createDominusMcpServer(bridge) {
2522
2876
  registerConnectionTools(server, bridge);
2523
2877
  registerStudioTools(server, bridge);
2524
2878
  registerBuildingTools(server, bridge);
2879
+ registerMetadataTools(server, bridge);
2525
2880
  registerParallelTaskTool(server, bridge);
2526
2881
  registerDocsTools(server);
2527
2882
  registerDominusResources(server, bridge);