@el4cteo/rbx-studio-mcp 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/bridge/rpc.js +243 -0
  4. package/dist/bridge/rpc.js.map +1 -0
  5. package/dist/bridge/server.js +281 -0
  6. package/dist/bridge/server.js.map +1 -0
  7. package/dist/index.js +104 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/apidump.js +269 -0
  10. package/dist/lib/apidump.js.map +1 -0
  11. package/dist/lib/errors.js +38 -0
  12. package/dist/lib/errors.js.map +1 -0
  13. package/dist/lib/format.js +191 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/pluginbuild.js +83 -0
  16. package/dist/lib/pluginbuild.js.map +1 -0
  17. package/dist/lib/png.js +84 -0
  18. package/dist/lib/png.js.map +1 -0
  19. package/dist/lib/protocol.js +22 -0
  20. package/dist/lib/protocol.js.map +1 -0
  21. package/dist/lib/tool.js +27 -0
  22. package/dist/lib/tool.js.map +1 -0
  23. package/dist/resources.js +70 -0
  24. package/dist/resources.js.map +1 -0
  25. package/dist/tools/api.js +78 -0
  26. package/dist/tools/api.js.map +1 -0
  27. package/dist/tools/character.js +94 -0
  28. package/dist/tools/character.js.map +1 -0
  29. package/dist/tools/debug.js +211 -0
  30. package/dist/tools/debug.js.map +1 -0
  31. package/dist/tools/device.js +74 -0
  32. package/dist/tools/device.js.map +1 -0
  33. package/dist/tools/discover.js +217 -0
  34. package/dist/tools/discover.js.map +1 -0
  35. package/dist/tools/exec.js +191 -0
  36. package/dist/tools/exec.js.map +1 -0
  37. package/dist/tools/input.js +96 -0
  38. package/dist/tools/input.js.map +1 -0
  39. package/dist/tools/instances.js +261 -0
  40. package/dist/tools/instances.js.map +1 -0
  41. package/dist/tools/perf.js +367 -0
  42. package/dist/tools/perf.js.map +1 -0
  43. package/dist/tools/playtest.js +153 -0
  44. package/dist/tools/playtest.js.map +1 -0
  45. package/dist/tools/screenshot.js +75 -0
  46. package/dist/tools/screenshot.js.map +1 -0
  47. package/dist/tools/scripts.js +316 -0
  48. package/dist/tools/scripts.js.map +1 -0
  49. package/dist/tools/session.js +152 -0
  50. package/dist/tools/session.js.map +1 -0
  51. package/dist/tools/world.js +281 -0
  52. package/dist/tools/world.js.map +1 -0
  53. package/package.json +62 -0
  54. package/plugin/default.project.json +6 -0
  55. package/plugin/src/Config.luau +59 -0
  56. package/plugin/src/Console.luau +657 -0
  57. package/plugin/src/Context.luau +35 -0
  58. package/plugin/src/Dispatch.luau +90 -0
  59. package/plugin/src/Editor.luau +142 -0
  60. package/plugin/src/Emulation.luau +151 -0
  61. package/plugin/src/LogBuffer.luau +277 -0
  62. package/plugin/src/Net.luau +102 -0
  63. package/plugin/src/Paths.luau +255 -0
  64. package/plugin/src/Phrase.luau +465 -0
  65. package/plugin/src/Png.luau +238 -0
  66. package/plugin/src/Scope.luau +78 -0
  67. package/plugin/src/ScriptEdit.luau +100 -0
  68. package/plugin/src/Serialize.luau +287 -0
  69. package/plugin/src/TextEdit.luau +296 -0
  70. package/plugin/src/Transport.luau +328 -0
  71. package/plugin/src/Undo.luau +72 -0
  72. package/plugin/src/Visuals.luau +710 -0
  73. package/plugin/src/handlers/Api.luau +242 -0
  74. package/plugin/src/handlers/Assets.luau +145 -0
  75. package/plugin/src/handlers/Capture.luau +187 -0
  76. package/plugin/src/handlers/Character.luau +361 -0
  77. package/plugin/src/handlers/Debug.luau +391 -0
  78. package/plugin/src/handlers/Device.luau +119 -0
  79. package/plugin/src/handlers/Discover.luau +289 -0
  80. package/plugin/src/handlers/Exec.luau +270 -0
  81. package/plugin/src/handlers/Geometry.luau +261 -0
  82. package/plugin/src/handlers/Input.luau +287 -0
  83. package/plugin/src/handlers/Instances.luau +389 -0
  84. package/plugin/src/handlers/Perf.luau +645 -0
  85. package/plugin/src/handlers/Playtest.luau +205 -0
  86. package/plugin/src/handlers/Scripts.luau +387 -0
  87. package/plugin/src/handlers/Session.luau +168 -0
  88. package/plugin/src/handlers/Viewport.luau +302 -0
  89. package/plugin/src/handlers/World.luau +176 -0
  90. package/plugin/src/init.server.luau +317 -0
  91. package/scripts/build-plugin.mjs +157 -0
  92. package/scripts/check-plugin.mjs +97 -0
  93. package/scripts/install-plugin.mjs +39 -0
  94. package/scripts/latency.mjs +201 -0
  95. package/scripts/locate-luau.mjs +51 -0
  96. package/scripts/sourcemap.mjs +58 -0
  97. package/scripts/test-plugin.mjs +82 -0
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+ import { json, text } from "../lib/format.js";
3
+ import { pluginStalenessWarning } from "../lib/pluginbuild.js";
4
+ import { defineTool } from "../lib/tool.js";
5
+ export function registerSessionTools(context) {
6
+ const { bridge } = context;
7
+ defineTool(context, {
8
+ name: "studio_status",
9
+ title: "Studio status",
10
+ description: "One-call snapshot of the connected Roblox Studio: place name and id, " +
11
+ "whether it is in edit / run / play mode, the current selection, which " +
12
+ "scripts are open in the editor, and how big the data model is.\n\n" +
13
+ "Call this FIRST in any Studio session, and again whenever a tool reports " +
14
+ "NO_STUDIO or TIMEOUT — it is the cheapest way to tell a disconnected " +
15
+ "plugin apart from a genuinely failing request. Also call it before and " +
16
+ "after `playtest`, because most tools behave differently in run mode.\n\n" +
17
+ "`openScripts` is what the user is actually working on: for each open tab " +
18
+ "it gives the script's path, the cursor line, any selected text, and which " +
19
+ "lines are on screen. Use it whenever a request is deictic — 'this " +
20
+ "function', 'the script I'm in', 'fix this' — instead of searching the " +
21
+ "place or asking which file they mean. Studio exposes no focused-tab API, " +
22
+ "so with several open, prefer the one holding a selection and otherwise " +
23
+ "ask.\n\n" +
24
+ "Returns JSON. Selection is capped at 50 entries and selected text at 400 " +
25
+ "characters; use `find` or `script_read` for more.",
26
+ inputSchema: {
27
+ studioId: z
28
+ .string()
29
+ .optional()
30
+ .describe("Target a specific Studio instance. Omit to use the active one " +
31
+ "(see list_studios / set_active_studio)."),
32
+ },
33
+ readOnly: true,
34
+ }, async ({ studioId }) => {
35
+ const status = await bridge.call("studio.status", {}, { studioId });
36
+ const targetId = studioId ?? bridge.activeId;
37
+ if (targetId)
38
+ bridge.notePlaceName(targetId, status.placeName, status.context);
39
+ // A stale plugin answers with older handlers and no other symptom, so the
40
+ // warning rides along with the one call agents are told to make first.
41
+ const session = bridge.list().find((entry) => entry.studioId === targetId);
42
+ const warning = pluginStalenessWarning(session?.buildId);
43
+ return json(status, warning ? `WARNING: ${warning}` : undefined);
44
+ });
45
+ defineTool(context, {
46
+ name: "list_studios",
47
+ title: "List connected Studios",
48
+ description: "Lists every Roblox Studio window currently connected to this server, " +
49
+ "with its studioId, place name, transport (sse or poll), when it " +
50
+ "connected, and which one is active.\n\n" +
51
+ "Call this whenever a tool reports AMBIGUOUS_STUDIO, and whenever the " +
52
+ "user refers to 'the other place' or 'my other window'. With a single " +
53
+ "Studio open every other tool targets it automatically, so you can skip " +
54
+ "it then.\n\n" +
55
+ "Nothing is targeted by default when several are connected: pick one with " +
56
+ "`set_active_studio`, or pass `studioId` to a single tool call to act on " +
57
+ "one place without changing the default.\n\n" +
58
+ "Each Studio is queried live, so `placeName` is the published name the " +
59
+ "user would recognise. A place never saved to Roblox has no published " +
60
+ "name and falls back to its data model name ('Place1').\n\n" +
61
+ "`context` matters more than it looks. Pressing Play adds a second entry " +
62
+ "for the playtest's server — same place, same name, same id as the editor " +
63
+ "session. Instances created or changed in a 'playtest' context are thrown " +
64
+ "away the moment the user stops, so building there looks like it worked " +
65
+ "and then vanishes. Target 'edit' unless the user specifically wants to " +
66
+ "inspect or affect the running game.",
67
+ inputSchema: {},
68
+ readOnly: true,
69
+ }, async () => {
70
+ const sessions = bridge.list();
71
+ if (sessions.length === 0) {
72
+ return text("No Studio instances are connected.\n" +
73
+ "Open Roblox Studio and enable the Studio MCP plugin (Plugins tab -> " +
74
+ "Studio MCP -> Connect), then call this again.");
75
+ }
76
+ // A defaulted target is not reported as active: tools refuse to use it
77
+ // while several are connected, so showing it as active would explain
78
+ // neither the AMBIGUOUS_STUDIO that follows nor how to clear it.
79
+ const active = bridge.activeIsChosen || sessions.length === 1 ? bridge.activeId : null;
80
+ // The identity captured at connect only knows the data model's name. Each
81
+ // Studio is asked live for the name the user would actually recognise,
82
+ // plus what it has open — which is usually how someone identifies a window
83
+ // ("the one with the intro script"). A Studio that does not answer still
84
+ // gets listed, from the identity it announced.
85
+ const detail = await Promise.all(sessions.map(async (session) => {
86
+ try {
87
+ const status = await bridge.call("studio.status", {}, { studioId: session.studioId, timeoutMs: 3_000 });
88
+ bridge.notePlaceName(session.studioId, status.placeName, status.context);
89
+ return {
90
+ placeName: status.placeName,
91
+ context: status.context,
92
+ openScripts: (status.openScripts ?? []).map((script) => script.path),
93
+ };
94
+ }
95
+ catch {
96
+ return { placeName: session.placeName, unreachable: true };
97
+ }
98
+ }));
99
+ return json({
100
+ studios: sessions.map((session, index) => ({
101
+ studioId: session.studioId,
102
+ ...detail[index],
103
+ placeId: session.placeId,
104
+ connectedAt: new Date(session.connectedAt).toISOString(),
105
+ transport: session.transport,
106
+ pluginVersion: session.pluginVersion,
107
+ buildId: session.buildId,
108
+ stale: pluginStalenessWarning(session.buildId) ?? false,
109
+ active: session.studioId === active,
110
+ })),
111
+ activeStudioId: active,
112
+ }, active === null && sessions.length > 1
113
+ ? "No place is selected. Ask the user which one they mean, then call " +
114
+ "set_active_studio."
115
+ : undefined);
116
+ });
117
+ defineTool(context, {
118
+ name: "set_active_studio",
119
+ title: "Set active Studio",
120
+ description: "Chooses which connected Studio window every other tool targets by " +
121
+ "default. Use it after list_studios when several places are open, and " +
122
+ "again whenever the user says to switch to another place.\n\n" +
123
+ "The choice persists until it is changed or that Studio disconnects. " +
124
+ "While several Studios are connected and none has been chosen, tools " +
125
+ "refuse with AMBIGUOUS_STUDIO rather than guessing.",
126
+ inputSchema: {
127
+ studioId: z
128
+ .string()
129
+ .min(1)
130
+ .describe("A studioId from list_studios."),
131
+ },
132
+ readOnly: false,
133
+ destructive: false,
134
+ idempotent: true,
135
+ }, async ({ studioId }) => {
136
+ bridge.setActive(studioId);
137
+ const session = bridge.list().find((s) => s.studioId === studioId);
138
+ // Confirmed with the session's context because two entries can share a
139
+ // place name, and picking the playtest one means work that disappears
140
+ // when the user presses stop.
141
+ const status = await bridge
142
+ .call("studio.status", {}, { studioId, timeoutMs: 3_000 })
143
+ .catch(() => null);
144
+ const where = status?.context && status.context !== "edit" ? ` (${status.context})` : "";
145
+ return text(`Active Studio is now "${status?.placeName ?? session?.placeName ?? studioId}"${where}.` +
146
+ (where
147
+ ? "\nThis is a running playtest, not the editor. Changes made here are " +
148
+ "discarded when the user stops it."
149
+ : ""));
150
+ });
151
+ }
152
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,281 @@
1
+ import { z } from "zod";
2
+ import { json, table, text, textOf } from "../lib/format.js";
3
+ import { defineTool } from "../lib/tool.js";
4
+ /** Roblox's toolbox search. Public, unauthenticated, and the same index Studio's own asset browser uses. */
5
+ const TOOLBOX_SEARCH = "https://apis.roblox.com/toolbox-service/v1/marketplace";
6
+ const TOOLBOX_DETAILS = "https://apis.roblox.com/toolbox-service/v1/items/details";
7
+ /** Toolbox category ids. Models is the only one that inserts as instances. */
8
+ const CATEGORIES = { model: 10, decal: 13, mesh: 40, audio: 3 };
9
+ /**
10
+ * Searches the Creator Store from the server rather than the plugin.
11
+ *
12
+ * Node already has internet access and these endpoints answer unauthenticated,
13
+ * while a plugin making outbound HTTP needs the user to approve each domain in
14
+ * Plugin Management. Searching here means it works the moment the server starts.
15
+ */
16
+ async function searchCreatorStore(keyword, category, limit) {
17
+ const categoryId = CATEGORIES[category] ?? 10;
18
+ const url = `${TOOLBOX_SEARCH}/${categoryId}?keyword=${encodeURIComponent(keyword)}` +
19
+ `&limit=${Math.min(limit, 30)}&sortType=Relevance`;
20
+ const found = await fetch(url, { signal: AbortSignal.timeout(15_000) });
21
+ if (!found.ok) {
22
+ throw new Error(`Creator Store search failed (${found.status}). Roblox may be rate-limiting.`);
23
+ }
24
+ const results = (await found.json());
25
+ const ids = (results.data ?? []).map((entry) => entry.id).slice(0, limit);
26
+ if (ids.length === 0)
27
+ return [];
28
+ // Search returns bare ids; everything worth showing — name, creator, whether
29
+ // it carries scripts — needs the second call.
30
+ const detailed = await fetch(`${TOOLBOX_DETAILS}?assetIds=${ids.join(",")}`, {
31
+ signal: AbortSignal.timeout(15_000),
32
+ });
33
+ if (!detailed.ok) {
34
+ throw new Error(`Could not read asset details (${detailed.status}).`);
35
+ }
36
+ const payload = (await detailed.json());
37
+ return payload.data ?? [];
38
+ }
39
+ export function registerWorldTools(context) {
40
+ const { bridge } = context;
41
+ defineTool(context, {
42
+ name: "geometry",
43
+ title: "Solid modelling",
44
+ description: "Cuts, joins and shatters parts with real constructive solid geometry.\n\n" +
45
+ "This is how to build a shape that is not a box without importing a " +
46
+ "mesh: `subtract` a door out of a wall, `union` several parts into one " +
47
+ "solid, `intersect` to keep only the overlap, `fragment` to shatter " +
48
+ "something into debris.\n\n" +
49
+ "`subtract` and `intersect` need the parts to actually overlap, and they " +
50
+ "fail differently when they do not. `intersect` returns nothing, which " +
51
+ "comes back as an error rather than a silent no-op. `subtract` returns " +
52
+ "the subject UNCHANGED — a full-size copy of it, reported as a created " +
53
+ "part — because cutting nothing out of something legitimately leaves it " +
54
+ "whole. So a subtract that succeeds is not proof that anything was cut: " +
55
+ "check the positions overlap with `inspect` first, or compare the " +
56
+ "result's size against the original.\n\n" +
57
+ "Results keep the original's material, colour and anchoring. Roblox " +
58
+ "returns bare grey MeshParts, so a brick wall with a hole cut in it " +
59
+ "would otherwise come back as a grey slab — correct geometry that looks " +
60
+ "like a mistake.\n\n" +
61
+ "The originals are consumed unless `keepOriginals` is set. The whole " +
62
+ "operation is one undo step.",
63
+ inputSchema: {
64
+ op: z
65
+ .enum(["union", "subtract", "intersect", "fragment"])
66
+ .describe("'union' merges, 'subtract' cuts `with` out of `path`, 'intersect' " +
67
+ "keeps only the overlap, 'fragment' shatters `path` into pieces."),
68
+ path: z.string().describe("The part being operated on — the one cut from, for subtract."),
69
+ with: z
70
+ .array(z.string())
71
+ .max(50)
72
+ .optional()
73
+ .describe("The other parts. Required for union, subtract and intersect."),
74
+ pieces: z
75
+ .number()
76
+ .int()
77
+ .min(2)
78
+ .max(100)
79
+ .default(8)
80
+ .describe("fragment only: roughly how many pieces to break into."),
81
+ name: z.string().optional().describe("Name for the result. Defaults to the original's."),
82
+ parent: z.string().optional().describe("Where to put the result. Defaults to the original's parent."),
83
+ keepOriginals: z
84
+ .boolean()
85
+ .default(false)
86
+ .describe("Leave the input parts in place instead of consuming them."),
87
+ collisionFidelity: z
88
+ .enum(["Default", "Hull", "Box", "PreciseConvexDecomposition"])
89
+ .default("Default")
90
+ .describe("How exactly the result collides. Precise is expensive — raise it " +
91
+ "only for a surface players walk on."),
92
+ splitApart: z
93
+ .boolean()
94
+ .default(false)
95
+ .describe("Return disconnected chunks as separate parts rather than one."),
96
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
97
+ },
98
+ destructive: true,
99
+ }, async (args) => {
100
+ const isFragment = args.op === "fragment";
101
+ const response = await bridge.call(isFragment ? "geometry.fragment" : "geometry.combine", {
102
+ op: args.op,
103
+ path: args.path,
104
+ with: args.with,
105
+ pieces: args.pieces,
106
+ name: args.name,
107
+ parent: args.parent,
108
+ keepOriginals: args.keepOriginals,
109
+ collisionFidelity: args.collisionFidelity,
110
+ splitApart: args.splitApart,
111
+ },
112
+ // Solid modelling on a complex mesh is genuinely slow.
113
+ { studioId: args.studioId, timeoutMs: 120_000 });
114
+ const notes = [];
115
+ if (response.removed && response.removed.length > 0) {
116
+ notes.push(`Consumed: ${response.removed.join(", ")}.`);
117
+ }
118
+ if (!response.undoable) {
119
+ notes.push("Studio would not open an undo recording, so this is not one Ctrl+Z.");
120
+ }
121
+ return json(response.created, notes.length > 0 ? notes.join(" ") : undefined);
122
+ });
123
+ defineTool(context, {
124
+ name: "assets",
125
+ title: "Creator Store",
126
+ description: "Searches Roblox's Creator Store and inserts models into the place.\n\n" +
127
+ "`search` looks through the same public index Studio's own asset browser " +
128
+ "uses and returns ids with names, creators, vote ratios and — the part " +
129
+ "that matters — whether the model contains scripts. `insert` puts one " +
130
+ "into the place by id.\n\n" +
131
+ "ALWAYS check `hasScripts` before inserting. Free models carrying " +
132
+ "scripts are the oldest hazard on the platform, and a model dropped into " +
133
+ "someone's game can run whatever it likes. The insert reports the script " +
134
+ "count again, and names them, so it can still be undone.\n\n" +
135
+ "Only public assets can be inserted. A private or deleted id fails with " +
136
+ "a message saying so rather than inserting nothing quietly.",
137
+ inputSchema: {
138
+ op: z.enum(["search", "insert"]).describe("'search' finds assets, 'insert' adds one to the place."),
139
+ keyword: z.string().optional().describe("search only: what to look for, e.g. \"medieval door\"."),
140
+ category: z
141
+ .enum(["model", "decal", "mesh", "audio"])
142
+ .default("model")
143
+ .describe("search only: what kind of asset. Only models insert as instances."),
144
+ limit: z.number().int().min(1).max(20).default(8).describe("search only: how many results."),
145
+ assetId: z.number().int().positive().optional().describe("insert only: the asset id to insert."),
146
+ parent: z.string().optional().describe("insert only: where to put it. Defaults to Workspace."),
147
+ position: z
148
+ .string()
149
+ .optional()
150
+ .describe('insert only: where to place it, e.g. "0, 10, 0". Defaults to wherever it was saved.'),
151
+ name: z.string().optional().describe("insert only: rename it on the way in."),
152
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
153
+ },
154
+ destructive: false,
155
+ }, async (args) => {
156
+ if (args.op === "search") {
157
+ if (!args.keyword)
158
+ return text("search needs a `keyword`.");
159
+ const found = await searchCreatorStore(args.keyword, args.category, args.limit);
160
+ if (found.length === 0) {
161
+ return text(`Nothing matched "${args.keyword}" in ${args.category}s.`);
162
+ }
163
+ const rows = found.map((entry) => ({
164
+ assetId: entry.asset?.id ?? 0,
165
+ name: entry.asset?.name ?? "?",
166
+ creator: entry.creator?.name ?? "?",
167
+ approval: entry.voting?.upVotePercent ? `${entry.voting.upVotePercent}%` : "—",
168
+ hasScripts: entry.asset?.hasScripts ? "YES" : "no",
169
+ triangles: entry.asset?.modelTechnicalDetails?.objectMeshSummary?.triangles ?? "—",
170
+ }));
171
+ const risky = rows.filter((row) => row.hasScripts === "YES");
172
+ return text(textOf(table(["assetId", "name", "creator", "approval", "hasScripts", "triangles"], rows)) +
173
+ (risky.length > 0
174
+ ? `\n\n${risky.length} of these contain scripts (${risky
175
+ .map((r) => r.name)
176
+ .join(", ")}). Inserting one runs whatever its author put in it — prefer a script-free model unless the scripts are the point.`
177
+ : "\n\nNone of these contain scripts."));
178
+ }
179
+ if (!args.assetId)
180
+ return text("insert needs an `assetId`. Use `op: \"search\"` to find one.");
181
+ const response = await bridge.call("assets.insert", {
182
+ assetId: args.assetId,
183
+ parent: args.parent,
184
+ position: args.position,
185
+ name: args.name,
186
+ },
187
+ // Downloading an asset goes out to Roblox and back.
188
+ { studioId: args.studioId, timeoutMs: 90_000 });
189
+ const notes = [];
190
+ if (response.scriptCount > 0) {
191
+ notes.push(`WARNING: this asset contains ${response.scriptCount} script(s): ` +
192
+ `${(response.scripts ?? []).join(", ")}. They will run on the next playtest. ` +
193
+ "Read them before playing, or delete them.");
194
+ }
195
+ if (!response.undoable)
196
+ notes.push("Not undoable as one step.");
197
+ return json(response.inserted, notes.length > 0 ? notes.join("\n\n") : undefined);
198
+ });
199
+ defineTool(context, {
200
+ name: "undo",
201
+ title: "Undo and redo",
202
+ description: "Steps Studio's undo history backwards or forwards.\n\n" +
203
+ "Every write this server makes is already wrapped in an undo recording, " +
204
+ "so this reverses your own work as cleanly as the user pressing Ctrl+Z " +
205
+ "— one tool call is one step. Use it when the user says an edit was " +
206
+ "wrong, instead of trying to reconstruct the previous state by hand, " +
207
+ "which is guesswork and usually incomplete.\n\n" +
208
+ "It reports how many steps actually applied, which is not always what " +
209
+ "was asked: the stack runs out, and an undo that did nothing otherwise " +
210
+ "looks exactly like one that worked.\n\n" +
211
+ "Studio's history covers the whole session, including the user's own " +
212
+ "edits — undoing more steps than you made will start reverting THEIR " +
213
+ "work. Undo only what you just did, and only when asked.",
214
+ inputSchema: {
215
+ action: z
216
+ .enum(["undo", "redo", "status"])
217
+ .default("status")
218
+ .describe("'status' reports what is available without changing anything."),
219
+ steps: z
220
+ .number()
221
+ .int()
222
+ .min(1)
223
+ .max(25)
224
+ .default(1)
225
+ .describe("How many steps to take. Keep it to what you did yourself."),
226
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
227
+ },
228
+ destructive: true,
229
+ }, async (args) => {
230
+ const response = await bridge.call("world.history", { action: args.action, steps: args.steps }, { studioId: args.studioId });
231
+ return json(response, response.note);
232
+ });
233
+ defineTool(context, {
234
+ name: "collision",
235
+ title: "Collision groups",
236
+ description: "Controls which parts physically collide with which.\n\n" +
237
+ "This is the right answer to 'these should pass through each other'. " +
238
+ "The alternative — turning CanCollide off — disables collision against " +
239
+ "everything, so a ghost that should pass through walls also falls " +
240
+ "through the floor.\n\n" +
241
+ "The order is: `create` a group, `assign` parts to it, then set what it " +
242
+ "is `collidable` with. A group with nothing assigned does nothing.\n\n" +
243
+ "Assigning a Model assigns every part inside it, which is almost always " +
244
+ "what is meant.",
245
+ inputSchema: {
246
+ action: z
247
+ .enum(["list", "create", "assign", "collidable"])
248
+ .default("list")
249
+ .describe("'list' shows existing groups and changes nothing."),
250
+ group: z.string().optional().describe("The group's name. Required for everything but list."),
251
+ paths: z
252
+ .array(z.string())
253
+ .max(100)
254
+ .optional()
255
+ .describe("assign only: parts or models to put in the group."),
256
+ with: z.string().optional().describe("collidable only: the other group."),
257
+ collidable: z
258
+ .boolean()
259
+ .default(true)
260
+ .describe("collidable only: whether the two groups collide. False makes them pass through."),
261
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
262
+ },
263
+ destructive: false,
264
+ }, async (args) => {
265
+ const response = await bridge.call("world.collision", {
266
+ action: args.action,
267
+ group: args.group,
268
+ paths: args.paths,
269
+ with: args.with,
270
+ collidable: args.collidable,
271
+ }, { studioId: args.studioId });
272
+ if (args.action === "list") {
273
+ const groups = response.groups ?? [];
274
+ if (groups.length === 0)
275
+ return text("No collision groups are registered.");
276
+ return text(textOf(table(["name", "mask"], groups)));
277
+ }
278
+ return json(response);
279
+ });
280
+ }
281
+ //# sourceMappingURL=world.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/tools/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAoB,MAAM,gBAAgB,CAAC;AAsC9D,4GAA4G;AAC5G,MAAM,cAAc,GAAG,wDAAwD,CAAC;AAChF,MAAM,eAAe,GAAG,0DAA0D,CAAC;AAEnF,8EAA8E;AAC9E,MAAM,UAAU,GAA2B,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAcxF;;;;;;GAMG;AACH,KAAK,UAAU,kBAAkB,CAC/B,OAAe,EACf,QAAgB,EAChB,KAAa;IAEb,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,GAAG,GACP,GAAG,cAAc,IAAI,UAAU,YAAY,kBAAkB,CAAC,OAAO,CAAC,EAAE;QACxE,UAAU,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,qBAAqB,CAAC;IAErD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxE,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,CAAC,MAAM,iCAAiC,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAqC,CAAC;IACzE,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC1E,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEhC,6EAA6E;IAC7E,8CAA8C;IAC9C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,eAAe,aAAa,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE;QAC3E,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;KACpC,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA+B,CAAC;IACtE,OAAO,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAoB;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAE3B,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,2EAA2E;YAC3E,qEAAqE;YACrE,wEAAwE;YACxE,qEAAqE;YACrE,4BAA4B;YAC5B,0EAA0E;YAC1E,wEAAwE;YACxE,wEAAwE;YACxE,wEAAwE;YACxE,yEAAyE;YACzE,yEAAyE;YACzE,mEAAmE;YACnE,yCAAyC;YACzC,qEAAqE;YACrE,qEAAqE;YACrE,yEAAyE;YACzE,qBAAqB;YACrB,sEAAsE;YACtE,6BAA6B;QAC/B,WAAW,EAAE;YACX,EAAE,EAAE,CAAC;iBACF,IAAI,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;iBACpD,QAAQ,CACP,oEAAoE;gBAClE,iEAAiE,CACpE;YACH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8DAA8D,CAAC;YACzF,IAAI,EAAE,CAAC;iBACJ,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,GAAG,CAAC,EAAE,CAAC;iBACP,QAAQ,EAAE;iBACV,QAAQ,CAAC,8DAA8D,CAAC;YAC3E,MAAM,EAAE,CAAC;iBACN,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,GAAG,CAAC;iBACR,OAAO,CAAC,CAAC,CAAC;iBACV,QAAQ,CAAC,uDAAuD,CAAC;YACpE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kDAAkD,CAAC;YACxF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6DAA6D,CAAC;YACrG,aAAa,EAAE,CAAC;iBACb,OAAO,EAAE;iBACT,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,2DAA2D,CAAC;YACxE,iBAAiB,EAAE,CAAC;iBACjB,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,4BAA4B,CAAC,CAAC;iBAC9D,OAAO,CAAC,SAAS,CAAC;iBAClB,QAAQ,CACP,mEAAmE;gBACjE,qCAAqC,CACxC;YACH,UAAU,EAAE,CAAC;iBACV,OAAO,EAAE;iBACT,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,+DAA+D,CAAC;YAC5E,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SACpF;QACD,WAAW,EAAE,IAAI;KAClB,EACD,KAAK,EAAE,IAAI,EAAuB,EAAE;QAClC,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,KAAK,UAAU,CAAC;QAC1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAChC,UAAU,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,kBAAkB,EACrD;YACE,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;YACzC,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B;QACD,uDAAuD;QACvD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,CAChD,CAAC;QAEF,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpD,KAAK,CAAC,IAAI,CAAC,aAAa,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,qEAAqE,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAChF,CAAC,CACF,CAAC;IAEF,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,eAAe;QACtB,WAAW,EACT,wEAAwE;YACxE,0EAA0E;YAC1E,wEAAwE;YACxE,uEAAuE;YACvE,2BAA2B;YAC3B,mEAAmE;YACnE,0EAA0E;YAC1E,0EAA0E;YAC1E,6DAA6D;YAC7D,yEAAyE;YACzE,4DAA4D;QAC9D,WAAW,EAAE;YACX,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,wDAAwD,CAAC;YACnG,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;YACjG,QAAQ,EAAE,CAAC;iBACR,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;iBACzC,OAAO,CAAC,OAAO,CAAC;iBAChB,QAAQ,CAAC,mEAAmE,CAAC;YAChF,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YAC5F,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;YAChG,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sDAAsD,CAAC;YAC9F,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,qFAAqF,CAAC;YAClG,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uCAAuC,CAAC;YAC7E,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SACpF;QACD,WAAW,EAAE,KAAK;KACnB,EACD,KAAK,EAAE,IAAI,EAAuB,EAAE;QAClC,IAAI,IAAI,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC,2BAA2B,CAAC,CAAC;YAC5D,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAChF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvB,OAAO,IAAI,CAAC,oBAAoB,IAAI,CAAC,OAAO,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;YACzE,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACjC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC;gBAC7B,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,IAAI,GAAG;gBAC9B,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,IAAI,GAAG;gBACnC,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,GAAG;gBAC9E,UAAU,EAAE,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;gBAClD,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,SAAS,IAAI,GAAG;aACnF,CAAC,CAAC,CAAC;YACJ,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC;YAC7D,OAAO,IAAI,CACT,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;gBACxF,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;oBACf,CAAC,CAAC,OAAO,KAAK,CAAC,MAAM,8BAA8B,KAAK;yBACnD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;yBAClB,IAAI,CAAC,IAAI,CAAC,oHAAoH;oBACnI,CAAC,CAAC,oCAAoC,CAAC,CAC5C,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,8DAA8D,CAAC,CAAC;QAC/F,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAChC,eAAe,EACf;YACE,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB;QACD,oDAAoD;QACpD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,CAC/C,CAAC;QAEF,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,QAAQ,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CACR,gCAAgC,QAAQ,CAAC,WAAW,cAAc;gBAChE,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,wCAAwC;gBAC9E,2CAA2C,CAC9C,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACpF,CAAC,CACF,CAAC;IAEF,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,eAAe;QACtB,WAAW,EACT,wDAAwD;YACxD,yEAAyE;YACzE,wEAAwE;YACxE,qEAAqE;YACrE,sEAAsE;YACtE,gDAAgD;YAChD,uEAAuE;YACvE,wEAAwE;YACxE,yCAAyC;YACzC,sEAAsE;YACtE,sEAAsE;YACtE,yDAAyD;QAC3D,WAAW,EAAE;YACX,MAAM,EAAE,CAAC;iBACN,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;iBAChC,OAAO,CAAC,QAAQ,CAAC;iBACjB,QAAQ,CAAC,+DAA+D,CAAC;YAC5E,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,EAAE,CAAC;iBACP,OAAO,CAAC,CAAC,CAAC;iBACV,QAAQ,CAAC,2DAA2D,CAAC;YACxE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SACpF;QACD,WAAW,EAAE,IAAI;KAClB,EACD,KAAK,EAAE,IAAI,EAAuB,EAAE;QAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAChC,eAAe,EACf,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAC1C,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAC5B,CAAC;QACF,OAAO,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CACF,CAAC;IAEF,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,kBAAkB;QACzB,WAAW,EACT,yDAAyD;YACzD,sEAAsE;YACtE,wEAAwE;YACxE,mEAAmE;YACnE,wBAAwB;YACxB,yEAAyE;YACzE,uEAAuE;YACvE,yEAAyE;YACzE,gBAAgB;QAClB,WAAW,EAAE;YACX,MAAM,EAAE,CAAC;iBACN,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;iBAChD,OAAO,CAAC,MAAM,CAAC;iBACf,QAAQ,CAAC,mDAAmD,CAAC;YAChE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qDAAqD,CAAC;YAC5F,KAAK,EAAE,CAAC;iBACL,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,GAAG,CAAC,GAAG,CAAC;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,mDAAmD,CAAC;YAChE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YACzE,UAAU,EAAE,CAAC;iBACV,OAAO,EAAE;iBACT,OAAO,CAAC,IAAI,CAAC;iBACb,QAAQ,CAAC,iFAAiF,CAAC;YAC9F,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SACpF;QACD,WAAW,EAAE,KAAK;KACnB,EACD,KAAK,EAAE,IAAI,EAAuB,EAAE;QAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAChC,iBAAiB,EACjB;YACE,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,EACD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAC5B,CAAC;QACF,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC;YACrC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,qCAAqC,CAAC,CAAC;YAC5E,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAmD,CAAC,CAAC,CAAC,CAAC;QACpG,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC,CACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@el4cteo/rbx-studio-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Free, open-source MCP server for Roblox Studio. Push-based SSE transport, editor-safe script edits, token-lean tools.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "rbx-studio-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "plugin",
13
+ "scripts",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.15"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "watch": "tsc --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "start": "node dist/index.js",
25
+ "build:plugin": "node scripts/check-plugin.mjs && node scripts/build-plugin.mjs",
26
+ "sourcemap": "node scripts/sourcemap.mjs",
27
+ "test": "node scripts/test-plugin.mjs",
28
+ "build:all": "npm run build && npm run build:plugin",
29
+ "install:plugin": "node scripts/install-plugin.mjs",
30
+ "check:plugin": "node scripts/check-plugin.mjs",
31
+ "prepack": "npm run build"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "roblox",
37
+ "roblox-studio",
38
+ "luau",
39
+ "claude",
40
+ "ai"
41
+ ],
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.22.0",
44
+ "zod": "^4.4.3"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^26.2.0",
48
+ "typescript": "^7.0.2"
49
+ },
50
+ "author": "EL4CTEO",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/EL4CTEO/roblox-studio-mcp.git"
54
+ },
55
+ "homepage": "https://github.com/EL4CTEO/roblox-studio-mcp#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/EL4CTEO/roblox-studio-mcp/issues"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "StudioMCP",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
@@ -0,0 +1,59 @@
1
+ --!strict
2
+ --[[
3
+ Connection settings for the Studio MCP plugin.
4
+
5
+ The port must match the one the Node server was started with (`--port`, or
6
+ the ROBLOX_STUDIO_MCP_PORT environment variable). It is stored as a plugin
7
+ setting so a user running on a non-default port only has to set it once.
8
+ ]]
9
+
10
+ local Config = {}
11
+
12
+ Config.PLUGIN_VERSION = "0.1.0"
13
+
14
+ -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
+ -- computes the same hash from its own copy of the sources and compares, so a
16
+ -- plugin left running from an older build is reported rather than silently
17
+ -- answering with stale handlers. Stays "dev" when the tree is loaded unbuilt.
18
+ Config.BUILD_ID = "dev"
19
+ Config.PROTOCOL_VERSION = 1
20
+ Config.DEFAULT_PORT = 44755
21
+
22
+ -- Header the bridge requires on every request. A web page cannot set it
23
+ -- cross-origin without a preflight the bridge never answers, which is what
24
+ -- keeps a malicious site from driving Studio through the loopback port.
25
+ Config.CLIENT_HEADER = "x-roblox-studio-mcp"
26
+
27
+ -- How long to wait before retrying after the stream drops. Studio closes SSE
28
+ -- connections at the 30 minute mark, so reconnects are routine, not exceptional.
29
+ Config.RECONNECT_DELAY = 1
30
+ Config.MAX_RECONNECT_DELAY = 30
31
+
32
+ -- How long a healthy long-poll session runs before standing aside so streaming
33
+ -- can be tried again. Long enough that a Studio which genuinely cannot stream
34
+ -- is not reconnecting constantly, short enough that a session downgraded by one
35
+ -- unlucky attempt does not spend the rest of its life slower than it should be.
36
+ Config.RESTREAM_INTERVAL = 60
37
+
38
+ local port: number? = nil
39
+
40
+ function Config.setPort(value: number)
41
+ port = value
42
+ end
43
+
44
+ function Config.getPort(): number
45
+ return port or Config.DEFAULT_PORT
46
+ end
47
+
48
+ function Config.baseUrl(): string
49
+ return string.format("http://127.0.0.1:%d", Config.getPort())
50
+ end
51
+
52
+ function Config.headers(): { [string]: string }
53
+ return {
54
+ [Config.CLIENT_HEADER] = tostring(Config.PROTOCOL_VERSION),
55
+ ["Content-Type"] = "application/json",
56
+ }
57
+ end
58
+
59
+ return Config