dominus-cli 0.5.3 → 0.5.5
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/index.js +228 -46
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +228 -45
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/Properties.lua +9 -2
- package/plugin/src/init.server.lua +5 -1
package/dist/mcp.js
CHANGED
|
@@ -41,7 +41,13 @@ var CliMsg = {
|
|
|
41
41
|
EXECUTE_PLAY_TEST: "cli:test:play",
|
|
42
42
|
BUILD_UI: "cli:build:ui",
|
|
43
43
|
GET_REFLECTION: "cli:get:reflection",
|
|
44
|
-
SERIALIZE_UI: "cli:serialize:ui"
|
|
44
|
+
SERIALIZE_UI: "cli:serialize:ui",
|
|
45
|
+
MOVE_INSTANCE: "cli:move:instance",
|
|
46
|
+
UNDO: "cli:undo",
|
|
47
|
+
REDO: "cli:redo",
|
|
48
|
+
BULK_SET_PROPERTIES: "cli:bulk:set:properties",
|
|
49
|
+
FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
|
|
50
|
+
};
|
|
45
51
|
function createMessage(type, payload) {
|
|
46
52
|
return { id: nanoid(), type, payload, ts: Date.now() };
|
|
47
53
|
}
|
|
@@ -703,10 +709,19 @@ async function startMcpServer() {
|
|
|
703
709
|
}
|
|
704
710
|
const mcp = new McpServer({
|
|
705
711
|
name: "dominus",
|
|
706
|
-
version: "0.
|
|
712
|
+
version: "0.5.5"
|
|
707
713
|
}, {
|
|
708
714
|
instructions: INTERNAL_MEMORY
|
|
709
715
|
});
|
|
716
|
+
function luaStr(s) {
|
|
717
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
|
|
718
|
+
}
|
|
719
|
+
function resolvePath(dotPath) {
|
|
720
|
+
const parts = dotPath.split(".");
|
|
721
|
+
let r = "game";
|
|
722
|
+
for (const p of parts) r += `[${luaStr(p)}]`;
|
|
723
|
+
return r;
|
|
724
|
+
}
|
|
710
725
|
mcp.tool(
|
|
711
726
|
"read_script",
|
|
712
727
|
"Read the source code of a script in Roblox Studio",
|
|
@@ -758,11 +773,14 @@ async function startMcpServer() {
|
|
|
758
773
|
);
|
|
759
774
|
mcp.tool(
|
|
760
775
|
"get_properties",
|
|
761
|
-
"Get all properties of
|
|
762
|
-
{
|
|
763
|
-
|
|
776
|
+
"Get all properties of a single instance in Roblox Studio. Returns property name, value, type, and category. For inspecting UI trees, prefer serialize_ui (structured tree) or get_descendants_properties (flat list with compact filtering).",
|
|
777
|
+
{
|
|
778
|
+
path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
|
|
779
|
+
compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
|
|
780
|
+
},
|
|
781
|
+
async ({ path: path2, compact }) => {
|
|
764
782
|
assertConnected();
|
|
765
|
-
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2 });
|
|
783
|
+
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2, compact });
|
|
766
784
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
767
785
|
}
|
|
768
786
|
);
|
|
@@ -978,16 +996,10 @@ async function startMcpServer() {
|
|
|
978
996
|
},
|
|
979
997
|
async (params) => {
|
|
980
998
|
assertConnected();
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
for (const p of parts) resolve += `["${p}"]`;
|
|
984
|
-
const lines = [`local orig = ${resolve}`, `local clone = orig:Clone()`];
|
|
985
|
-
if (params.newName) lines.push(`clone.Name = "${params.newName}"`);
|
|
999
|
+
const lines = [`local orig = ${resolvePath(params.path)}`, `local clone = orig:Clone()`];
|
|
1000
|
+
if (params.newName) lines.push(`clone.Name = ${luaStr(params.newName)}`);
|
|
986
1001
|
if (params.newParent) {
|
|
987
|
-
|
|
988
|
-
let pr = "game";
|
|
989
|
-
for (const p of pp) pr += `["${p}"]`;
|
|
990
|
-
lines.push(`clone.Parent = ${pr}`);
|
|
1002
|
+
lines.push(`clone.Parent = ${resolvePath(params.newParent)}`);
|
|
991
1003
|
} else {
|
|
992
1004
|
lines.push(`clone.Parent = orig.Parent`);
|
|
993
1005
|
}
|
|
@@ -1003,7 +1015,7 @@ async function startMcpServer() {
|
|
|
1003
1015
|
);
|
|
1004
1016
|
mcp.tool(
|
|
1005
1017
|
"group_instances",
|
|
1006
|
-
"Group multiple instances into a Model or Folder",
|
|
1018
|
+
"Group multiple instances into a Model or Folder. All listed instances become children of the new group.",
|
|
1007
1019
|
{
|
|
1008
1020
|
paths: z.array(z.string()).describe("Array of instance paths to group"),
|
|
1009
1021
|
groupName: z.string().describe("Name for the group"),
|
|
@@ -1014,20 +1026,11 @@ async function startMcpServer() {
|
|
|
1014
1026
|
assertConnected();
|
|
1015
1027
|
const gc = params.groupClass ?? "Model";
|
|
1016
1028
|
const parent = params.parent ?? "Workspace";
|
|
1017
|
-
const parentParts = parent.split(".");
|
|
1018
|
-
let parentResolve = "game";
|
|
1019
|
-
for (const p of parentParts) parentResolve += `["${p}"]`;
|
|
1020
|
-
const resolves = params.paths.map((path2) => {
|
|
1021
|
-
const parts = path2.split(".");
|
|
1022
|
-
let r = "game";
|
|
1023
|
-
for (const part of parts) r += `["${part}"]`;
|
|
1024
|
-
return r;
|
|
1025
|
-
});
|
|
1026
1029
|
const code = [
|
|
1027
|
-
`local group = Instance.new(
|
|
1028
|
-
`group.Name =
|
|
1029
|
-
`group.Parent = ${
|
|
1030
|
-
...
|
|
1030
|
+
`local group = Instance.new(${luaStr(gc)})`,
|
|
1031
|
+
`group.Name = ${luaStr(params.groupName)}`,
|
|
1032
|
+
`group.Parent = ${resolvePath(parent)}`,
|
|
1033
|
+
...params.paths.map((p) => `${resolvePath(p)}.Parent = group`),
|
|
1031
1034
|
`print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
|
|
1032
1035
|
].join("\n");
|
|
1033
1036
|
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
|
|
@@ -1036,7 +1039,7 @@ async function startMcpServer() {
|
|
|
1036
1039
|
);
|
|
1037
1040
|
mcp.tool(
|
|
1038
1041
|
"build_multiple",
|
|
1039
|
-
"Create many parts/instances in one batch. Send an array of specs \u2014 all created in a single round-trip. Ideal for building trees, houses, vehicles.",
|
|
1042
|
+
"Create many parts/instances in one batch. Send an array of specs \u2014 all created in a single round-trip. Ideal for building trees, houses, vehicles. Supports hex colors, rgb() colors, and BrickColor names.",
|
|
1040
1043
|
{
|
|
1041
1044
|
parts: z.array(z.object({
|
|
1042
1045
|
name: z.string(),
|
|
@@ -1044,10 +1047,11 @@ async function startMcpServer() {
|
|
|
1044
1047
|
parent: z.string().optional(),
|
|
1045
1048
|
position: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
|
|
1046
1049
|
size: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
|
|
1047
|
-
color: z.string().optional().describe('Hex "#RRGGBB" or BrickColor name'),
|
|
1048
|
-
material: z.string().optional(),
|
|
1050
|
+
color: z.string().optional().describe('Hex "#RRGGBB", "rgb(r,g,b)", or BrickColor name'),
|
|
1051
|
+
material: z.string().optional().describe("Enum.Material name: Plastic, Wood, Neon, Glass, etc."),
|
|
1049
1052
|
transparency: z.number().optional(),
|
|
1050
1053
|
anchored: z.boolean().optional(),
|
|
1054
|
+
canCollide: z.boolean().optional(),
|
|
1051
1055
|
shape: z.enum(["Block", "Ball", "Cylinder", "Wedge"]).optional()
|
|
1052
1056
|
})).describe("Array of part specifications"),
|
|
1053
1057
|
groupName: z.string().optional().describe("Group all parts into a Model with this name"),
|
|
@@ -1058,35 +1062,36 @@ async function startMcpServer() {
|
|
|
1058
1062
|
const parts = params.parts;
|
|
1059
1063
|
const groupName = params.groupName;
|
|
1060
1064
|
const groupParent = params.groupParent ?? "Workspace";
|
|
1061
|
-
function resolveP(path2) {
|
|
1062
|
-
const segs = path2.split(".");
|
|
1063
|
-
let r = "game";
|
|
1064
|
-
for (const s of segs) r += `["${s}"]`;
|
|
1065
|
-
return r;
|
|
1066
|
-
}
|
|
1067
1065
|
const lines = [];
|
|
1068
1066
|
if (groupName) {
|
|
1069
1067
|
lines.push(`local _group = Instance.new("Model")`);
|
|
1070
|
-
lines.push(`_group.Name =
|
|
1071
|
-
lines.push(`_group.Parent = ${
|
|
1068
|
+
lines.push(`_group.Name = ${luaStr(groupName)}`);
|
|
1069
|
+
lines.push(`_group.Parent = ${resolvePath(groupParent)}`);
|
|
1072
1070
|
}
|
|
1073
1071
|
for (let i = 0; i < parts.length; i++) {
|
|
1074
1072
|
const p = parts[i];
|
|
1075
1073
|
const v = `_p${i}`;
|
|
1076
1074
|
const cn = p.className ?? (p.shape === "Wedge" ? "WedgePart" : "Part");
|
|
1077
|
-
lines.push(`local ${v} = Instance.new(
|
|
1078
|
-
lines.push(`${v}.Name =
|
|
1075
|
+
lines.push(`local ${v} = Instance.new(${luaStr(cn)})`);
|
|
1076
|
+
lines.push(`${v}.Name = ${luaStr(p.name)}`);
|
|
1079
1077
|
if (p.position) lines.push(`${v}.Position = Vector3.new(${p.position.x}, ${p.position.y}, ${p.position.z})`);
|
|
1080
1078
|
if (p.size) lines.push(`${v}.Size = Vector3.new(${p.size.x}, ${p.size.y}, ${p.size.z})`);
|
|
1081
1079
|
if (p.color) {
|
|
1082
|
-
if (p.color.startsWith("#"))
|
|
1083
|
-
|
|
1080
|
+
if (p.color.startsWith("#")) {
|
|
1081
|
+
lines.push(`${v}.Color = Color3.fromHex(${luaStr(p.color)})`);
|
|
1082
|
+
} else if (p.color.startsWith("rgb(")) {
|
|
1083
|
+
const m = p.color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
|
1084
|
+
if (m) lines.push(`${v}.Color = Color3.fromRGB(${m[1]}, ${m[2]}, ${m[3]})`);
|
|
1085
|
+
} else {
|
|
1086
|
+
lines.push(`${v}.BrickColor = BrickColor.new(${luaStr(p.color)})`);
|
|
1087
|
+
}
|
|
1084
1088
|
}
|
|
1085
1089
|
if (p.material) lines.push(`${v}.Material = Enum.Material.${p.material}`);
|
|
1086
1090
|
if (p.transparency !== void 0) lines.push(`${v}.Transparency = ${p.transparency}`);
|
|
1087
1091
|
lines.push(`${v}.Anchored = ${p.anchored ?? true}`);
|
|
1092
|
+
if (p.canCollide !== void 0) lines.push(`${v}.CanCollide = ${p.canCollide}`);
|
|
1088
1093
|
if (p.shape && p.shape !== "Block" && cn === "Part") lines.push(`${v}.Shape = Enum.PartType.${p.shape}`);
|
|
1089
|
-
lines.push(`${v}.Parent = ${groupName ? "_group" :
|
|
1094
|
+
lines.push(`${v}.Parent = ${groupName ? "_group" : resolvePath(p.parent ?? "Workspace")}`);
|
|
1090
1095
|
}
|
|
1091
1096
|
lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
|
|
1092
1097
|
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
|
|
@@ -1113,6 +1118,184 @@ async function startMcpServer() {
|
|
|
1113
1118
|
return { content: [{ type: "text", text: JSON.stringify(res.payload.tree, null, 2) }] };
|
|
1114
1119
|
}
|
|
1115
1120
|
);
|
|
1121
|
+
mcp.tool(
|
|
1122
|
+
"move_instance",
|
|
1123
|
+
"Move (reparent) an instance to a new parent in Roblox Studio. Preserves the instance and all descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
|
|
1124
|
+
{
|
|
1125
|
+
path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
|
|
1126
|
+
newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
|
|
1127
|
+
},
|
|
1128
|
+
async ({ path: path2, newParent }) => {
|
|
1129
|
+
assertConnected();
|
|
1130
|
+
const res = await bridge.sendRequest(
|
|
1131
|
+
CliMsg.MOVE_INSTANCE,
|
|
1132
|
+
{ path: path2, newParent }
|
|
1133
|
+
);
|
|
1134
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1135
|
+
}
|
|
1136
|
+
);
|
|
1137
|
+
mcp.tool(
|
|
1138
|
+
"bulk_set_properties",
|
|
1139
|
+
"Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Two modes:\n1. Explicit: provide targets array of {path, properties} objects\n2. Filter: provide root + className + properties to apply to all matches",
|
|
1140
|
+
{
|
|
1141
|
+
targets: z.array(z.object({
|
|
1142
|
+
path: z.string(),
|
|
1143
|
+
properties: z.record(z.unknown())
|
|
1144
|
+
})).optional().describe("Array of {path, properties} for explicit mode"),
|
|
1145
|
+
root: z.string().optional().describe("Root path to search under (filter mode)"),
|
|
1146
|
+
className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
|
|
1147
|
+
properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
|
|
1148
|
+
},
|
|
1149
|
+
async (params) => {
|
|
1150
|
+
assertConnected();
|
|
1151
|
+
const res = await bridge.sendRequest(
|
|
1152
|
+
CliMsg.BULK_SET_PROPERTIES,
|
|
1153
|
+
{
|
|
1154
|
+
targets: params.targets,
|
|
1155
|
+
root: params.root,
|
|
1156
|
+
className: params.className,
|
|
1157
|
+
properties: params.properties
|
|
1158
|
+
},
|
|
1159
|
+
1e3 * 60
|
|
1160
|
+
);
|
|
1161
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1162
|
+
}
|
|
1163
|
+
);
|
|
1164
|
+
mcp.tool(
|
|
1165
|
+
"find_replace_scripts",
|
|
1166
|
+
"Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua patterns. Use dryRun=true to preview changes without applying.",
|
|
1167
|
+
{
|
|
1168
|
+
find: z.string().describe("Text or Lua pattern to search for"),
|
|
1169
|
+
replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
|
|
1170
|
+
root: z.string().optional().describe("Root path to limit search scope (optional)"),
|
|
1171
|
+
usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
|
|
1172
|
+
dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
|
|
1173
|
+
},
|
|
1174
|
+
async (params) => {
|
|
1175
|
+
assertConnected();
|
|
1176
|
+
const res = await bridge.sendRequest(
|
|
1177
|
+
CliMsg.FIND_REPLACE_SCRIPTS,
|
|
1178
|
+
{
|
|
1179
|
+
find: params.find,
|
|
1180
|
+
replace: params.replace,
|
|
1181
|
+
root: params.root,
|
|
1182
|
+
usePattern: params.usePattern,
|
|
1183
|
+
dryRun: params.dryRun
|
|
1184
|
+
},
|
|
1185
|
+
1e3 * 60 * 5
|
|
1186
|
+
);
|
|
1187
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
mcp.tool(
|
|
1191
|
+
"undo",
|
|
1192
|
+
"Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
|
|
1193
|
+
{},
|
|
1194
|
+
async () => {
|
|
1195
|
+
assertConnected();
|
|
1196
|
+
const res = await bridge.sendRequest(CliMsg.UNDO, {});
|
|
1197
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
mcp.tool(
|
|
1201
|
+
"redo",
|
|
1202
|
+
"Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
|
|
1203
|
+
{},
|
|
1204
|
+
async () => {
|
|
1205
|
+
assertConnected();
|
|
1206
|
+
const res = await bridge.sendRequest(CliMsg.REDO, {});
|
|
1207
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
mcp.tool(
|
|
1211
|
+
"move_instance",
|
|
1212
|
+
"Move (reparent) an instance to a new parent in Roblox Studio. Preserves the instance and all descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
|
|
1213
|
+
{
|
|
1214
|
+
path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
|
|
1215
|
+
newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
|
|
1216
|
+
},
|
|
1217
|
+
async ({ path: path2, newParent }) => {
|
|
1218
|
+
assertConnected();
|
|
1219
|
+
const res = await bridge.sendRequest(
|
|
1220
|
+
CliMsg.MOVE_INSTANCE,
|
|
1221
|
+
{ path: path2, newParent }
|
|
1222
|
+
);
|
|
1223
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
mcp.tool(
|
|
1227
|
+
"bulk_set_properties",
|
|
1228
|
+
"Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Two modes:\n1. Explicit: provide targets array of {path, properties} objects\n2. Filter: provide root + className + properties to apply to all matches",
|
|
1229
|
+
{
|
|
1230
|
+
targets: z.array(z.object({
|
|
1231
|
+
path: z.string(),
|
|
1232
|
+
properties: z.record(z.unknown())
|
|
1233
|
+
})).optional().describe("Array of {path, properties} for explicit mode"),
|
|
1234
|
+
root: z.string().optional().describe("Root path to search under (filter mode)"),
|
|
1235
|
+
className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
|
|
1236
|
+
properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
|
|
1237
|
+
},
|
|
1238
|
+
async (params) => {
|
|
1239
|
+
assertConnected();
|
|
1240
|
+
const res = await bridge.sendRequest(
|
|
1241
|
+
CliMsg.BULK_SET_PROPERTIES,
|
|
1242
|
+
{
|
|
1243
|
+
targets: params.targets,
|
|
1244
|
+
root: params.root,
|
|
1245
|
+
className: params.className,
|
|
1246
|
+
properties: params.properties
|
|
1247
|
+
},
|
|
1248
|
+
1e3 * 60
|
|
1249
|
+
);
|
|
1250
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
mcp.tool(
|
|
1254
|
+
"find_replace_scripts",
|
|
1255
|
+
"Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua patterns. Use dryRun=true to preview changes without applying.",
|
|
1256
|
+
{
|
|
1257
|
+
find: z.string().describe("Text or Lua pattern to search for"),
|
|
1258
|
+
replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
|
|
1259
|
+
root: z.string().optional().describe("Root path to limit search scope (optional)"),
|
|
1260
|
+
usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
|
|
1261
|
+
dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
|
|
1262
|
+
},
|
|
1263
|
+
async (params) => {
|
|
1264
|
+
assertConnected();
|
|
1265
|
+
const res = await bridge.sendRequest(
|
|
1266
|
+
CliMsg.FIND_REPLACE_SCRIPTS,
|
|
1267
|
+
{
|
|
1268
|
+
find: params.find,
|
|
1269
|
+
replace: params.replace,
|
|
1270
|
+
root: params.root,
|
|
1271
|
+
usePattern: params.usePattern,
|
|
1272
|
+
dryRun: params.dryRun
|
|
1273
|
+
},
|
|
1274
|
+
1e3 * 60 * 5
|
|
1275
|
+
);
|
|
1276
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1277
|
+
}
|
|
1278
|
+
);
|
|
1279
|
+
mcp.tool(
|
|
1280
|
+
"undo",
|
|
1281
|
+
"Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
|
|
1282
|
+
{},
|
|
1283
|
+
async () => {
|
|
1284
|
+
assertConnected();
|
|
1285
|
+
const res = await bridge.sendRequest(CliMsg.UNDO, {});
|
|
1286
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
mcp.tool(
|
|
1290
|
+
"redo",
|
|
1291
|
+
"Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
|
|
1292
|
+
{},
|
|
1293
|
+
async () => {
|
|
1294
|
+
assertConnected();
|
|
1295
|
+
const res = await bridge.sendRequest(CliMsg.REDO, {});
|
|
1296
|
+
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1116
1299
|
mcp.tool(
|
|
1117
1300
|
"recall_memory",
|
|
1118
1301
|
"Search persistent memory for facts about the current Roblox project",
|