@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.2

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 (48) hide show
  1. package/dist/index.js +9401 -9047
  2. package/package.json +2 -2
  3. package/studio-plugin/MCPPlugin.rbxmx +400 -97
  4. package/studio-plugin/.Carbon.rbxm.lock +0 -0
  5. package/studio-plugin/Carbon.rbxm +0 -0
  6. package/studio-plugin/INSTALLATION.md +0 -170
  7. package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
  8. package/studio-plugin/default.project.json +0 -19
  9. package/studio-plugin/dev.project.json +0 -23
  10. package/studio-plugin/include/LibMP.lua +0 -156378
  11. package/studio-plugin/inspector-icon.png +0 -0
  12. package/studio-plugin/package-lock.json +0 -706
  13. package/studio-plugin/package.json +0 -19
  14. package/studio-plugin/plugin.json +0 -10
  15. package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
  16. package/studio-plugin/src/modules/ClientBroker.ts +0 -450
  17. package/studio-plugin/src/modules/Communication.ts +0 -601
  18. package/studio-plugin/src/modules/EvalBridges.ts +0 -255
  19. package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
  20. package/studio-plugin/src/modules/LuauExec.ts +0 -403
  21. package/studio-plugin/src/modules/Recording.ts +0 -28
  22. package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
  23. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
  24. package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
  25. package/studio-plugin/src/modules/State.ts +0 -39
  26. package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
  27. package/studio-plugin/src/modules/UI.ts +0 -597
  28. package/studio-plugin/src/modules/Utils.ts +0 -527
  29. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
  30. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
  33. package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
  34. package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
  35. package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
  36. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
  37. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
  38. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
  39. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
  40. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
  41. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
  42. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
  43. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
  44. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
  45. package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
  46. package/studio-plugin/src/server/index.server.ts +0 -135
  47. package/studio-plugin/src/types/index.d.ts +0 -57
  48. package/studio-plugin/tsconfig.json +0 -20
@@ -1,117 +0,0 @@
1
- import { HttpService, ServerStorage } from "@rbxts/services";
2
-
3
- const LEGACY_SETTING_KEY_PREFIX = "MCP_SERVER_URL_";
4
- const SETTING_KEY_PREFIX = "MCP_LAST_SUCCESSFUL_SERVER_URL_";
5
- const GLOBAL_SETTING_KEY = "MCP_LAST_SUCCESSFUL_SERVER_URL_GLOBAL_V1";
6
-
7
- let pluginRef: Plugin | undefined;
8
-
9
- function init(p: Plugin): void {
10
- pluginRef = p;
11
- }
12
-
13
- function normalizeServerUrl(serverUrl: string | undefined): string {
14
- let normalized = (serverUrl ?? "").gsub("^%s+", "")[0].gsub("%s+$", "")[0];
15
- if (normalized === "") return "";
16
-
17
- if (normalized.match("^%a[%w+.-]*://")[0] === undefined) {
18
- normalized = `http://${normalized}`;
19
- }
20
-
21
- while (
22
- normalized.size() > 0 &&
23
- normalized.sub(-1) === "/" &&
24
- normalized.match("^%a[%w+.-]*://$")[0] === undefined
25
- ) {
26
- normalized = normalized.sub(1, -2);
27
- }
28
-
29
- return normalized;
30
- }
31
-
32
- function extractPort(serverUrl: string): number | undefined {
33
- const [portStr] = serverUrl.match(":(%d+)$");
34
- if (portStr === undefined) return undefined;
35
- return tonumber(portStr);
36
- }
37
-
38
- function addUnique(values: string[], value: string): void {
39
- if (!values.includes(value)) {
40
- values.push(value);
41
- }
42
- }
43
-
44
- function computeInstanceIds(options?: { createAnonymous?: boolean }): string[] {
45
- const ids: string[] = [];
46
- if (game.PlaceId !== 0) {
47
- addUnique(ids, `place:${tostring(game.PlaceId)}`);
48
- }
49
- const existing = ServerStorage.GetAttribute("__MCPPlaceId");
50
- if (typeIs(existing, "string") && existing !== "") {
51
- addUnique(ids, `anon:${existing as string}`);
52
- } else if (game.PlaceId === 0 && options?.createAnonymous === true) {
53
- const fresh = HttpService.GenerateGUID(false);
54
- pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
55
- addUnique(ids, `anon:${fresh}`);
56
- }
57
- return ids;
58
- }
59
-
60
- function settingKey(instanceId: string): string {
61
- return SETTING_KEY_PREFIX + instanceId;
62
- }
63
-
64
- function legacySettingKey(instanceId: string): string {
65
- return LEGACY_SETTING_KEY_PREFIX + instanceId;
66
- }
67
-
68
- function readSettingString(key: string): string | undefined {
69
- if (!pluginRef) return undefined;
70
- const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
71
- if (!ok || !typeIs(value, "string")) return undefined;
72
-
73
- const normalized = normalizeServerUrl(value as string);
74
- return normalized !== "" ? normalized : undefined;
75
- }
76
-
77
- function writeSettingString(key: string, serverUrl: string): void {
78
- if (!pluginRef) return;
79
- pcall(() => pluginRef!.SetSetting(key, serverUrl));
80
- }
81
-
82
- function rememberServerUrl(serverUrl: string): void {
83
- const normalized = normalizeServerUrl(serverUrl);
84
- if (!pluginRef || normalized === "") return;
85
- writeSettingString(GLOBAL_SETTING_KEY, normalized);
86
- for (const instanceId of computeInstanceIds({ createAnonymous: true })) {
87
- writeSettingString(settingKey(instanceId), normalized);
88
- writeSettingString(legacySettingKey(instanceId), normalized);
89
- }
90
- }
91
-
92
- function readServerUrl(): string | undefined {
93
- if (!pluginRef) return undefined;
94
- // Reading settings should not mint a place identity. Client play DMs have
95
- // their own ServerStorage; creating an id there makes a misleading anon id
96
- // that never matches the edit/server bridge identity.
97
- for (const instanceId of computeInstanceIds()) {
98
- const remembered = readSettingString(settingKey(instanceId));
99
- if (remembered !== undefined) return remembered;
100
- }
101
- const globalRemembered = readSettingString(GLOBAL_SETTING_KEY);
102
- if (globalRemembered !== undefined) return globalRemembered;
103
- for (const instanceId of computeInstanceIds()) {
104
- const legacyRemembered = readSettingString(legacySettingKey(instanceId));
105
- if (legacyRemembered !== undefined) return legacyRemembered;
106
- }
107
-
108
- return undefined;
109
- }
110
-
111
- export = {
112
- init,
113
- normalizeServerUrl,
114
- extractPort,
115
- rememberServerUrl,
116
- readServerUrl,
117
- };
@@ -1,39 +0,0 @@
1
- import { Connection } from "../types";
2
-
3
- const CURRENT_VERSION = "__VERSION__";
4
- const PLUGIN_VARIANT = "__PLUGIN_VARIANT__";
5
- const BASE_PORT = 58741;
6
-
7
- function createConnection(port: number): Connection {
8
- return {
9
- port,
10
- serverUrl: `http://localhost:${port}`,
11
- isActive: false,
12
- pollInterval: 0.5,
13
- lastPoll: 0,
14
- consecutiveFailures: 0,
15
- maxFailuresBeforeError: 50,
16
- lastSuccessfulConnection: 0,
17
- currentRetryDelay: 0.5,
18
- maxRetryDelay: 5,
19
- retryBackoffMultiplier: 1.2,
20
- lastHttpOk: false,
21
- lastMcpOk: false,
22
- mcpWaitStartTime: undefined,
23
- isPolling: false,
24
- heartbeatConnection: undefined,
25
- };
26
- }
27
-
28
- const connection = createConnection(BASE_PORT);
29
-
30
- function getActiveConnection(): Connection {
31
- return connection;
32
- }
33
-
34
- export = {
35
- CURRENT_VERSION,
36
- PLUGIN_VARIANT,
37
- BASE_PORT,
38
- getActiveConnection,
39
- };
@@ -1,267 +0,0 @@
1
- // Cross-DM stop_playtest signaling via plugin:SetSetting, scoped by
2
- // per-instance setting key so the same Studio process can host playtests
3
- // for multiple places without one place's stop_playtest yanking another's.
4
- // During publish-after-connect, both "anon:<uuid>" and "place:<PlaceId>"
5
- // can refer to the same Studio place, so stop requests are mirrored across
6
- // both keys while the monitor waits for a matching result on either key.
7
- //
8
- // `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
9
- // shared across every DataModel the plugin runs in (edit DMs, play-server
10
- // DMs, play-client DMs). For each connected place we use a dedicated key
11
- // "MCP_STOP_PLAY_<instanceId>" as a tiny request/result mailbox:
12
- //
13
- // * The edit DM's handler writes a tokenized stop request into its own key
14
- // (computed from its placeId / ServerStorage anon UUID).
15
- // * Each play-server DM's monitor loop polls the key matching its own
16
- // instanceId at 1Hz. On a fresh token, it calls StudioTestService:EndTest
17
- // and writes a matching result token. Play-server DMs for other places
18
- // never touch this key.
19
- // * The edit DM waits up to ~8s for its result token, confirming a matching
20
- // play-server actually consumed the request.
21
- //
22
- // Earlier versions used a single shared boolean flag, which let any
23
- // play-server DM in the same Studio process consume any place's stop
24
- // request — silently yanking teammates' playtests. The per-key scoping
25
- // below is the fix.
26
-
27
- import { HttpService, RunService, ServerStorage } from "@rbxts/services";
28
-
29
- const StudioTestService = game.GetService("StudioTestService");
30
-
31
- const SETTING_KEY_PREFIX = "MCP_STOP_PLAY_";
32
- // Keep this conservative. plugin:GetSetting is backed by Studio's plugin
33
- // settings store, and this monitor runs during every play session, including
34
- // manually-started Play. The official reference implementation polls at 1s.
35
- const POLL_INTERVAL_SEC = 1;
36
- // Total time we wait for the matching play-server DM to consume the
37
- // signal. Must cover: monitor detection (<= POLL_INTERVAL_SEC) +
38
- // StudioTestService:EndTest teardown (several seconds on heavier places).
39
- // 8s is intentionally shorter than the MCP request timeout but long enough
40
- // for the 1s monitor cadence plus ordinary Studio teardown latency.
41
- const WAIT_FOR_CONSUMPTION_TIMEOUT_SEC = 8.0;
42
- const WAIT_POLL_SEC = 0.1;
43
- const REQUEST_TTL_SEC = 12.0;
44
-
45
- let pluginRef: Plugin | undefined;
46
- let endTestIssued = false;
47
-
48
- interface StopPayload {
49
- kind?: string;
50
- id?: string;
51
- requestedAt?: number;
52
- consumedAt?: number;
53
- ok?: boolean;
54
- error?: string;
55
- }
56
-
57
- interface StopRequestResult {
58
- ok: boolean;
59
- requestId?: string;
60
- }
61
-
62
- interface StopConsumptionResult {
63
- ok: boolean;
64
- consumed: boolean;
65
- error?: string;
66
- }
67
-
68
- function init(p: Plugin): void {
69
- pluginRef = p;
70
- }
71
-
72
- // Mirror of Communication.computeInstanceId(). Duplicated here because
73
- // StopPlayMonitor runs in both edit and play-server DMs, and both must
74
- // agree on the place identifier (published places: placeId; unpublished:
75
- // UUID on ServerStorage's __MCPPlaceId attribute, travels with the .rbxl
76
- // into the play DM).
77
- function addUnique(values: string[], value: string): void {
78
- if (!values.includes(value)) {
79
- values.push(value);
80
- }
81
- }
82
-
83
- function computeInstanceIds(): string[] {
84
- const ids: string[] = [];
85
- if (game.PlaceId !== 0) {
86
- addUnique(ids, `place:${tostring(game.PlaceId)}`);
87
- }
88
- const existing = ServerStorage.GetAttribute("__MCPPlaceId");
89
- if (typeIs(existing, "string") && existing !== "") {
90
- addUnique(ids, `anon:${existing as string}`);
91
- } else if (game.PlaceId === 0) {
92
- const fresh = HttpService.GenerateGUID(false);
93
- pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
94
- addUnique(ids, `anon:${fresh}`);
95
- }
96
- return ids;
97
- }
98
-
99
- function settingKey(instanceId: string): string {
100
- return SETTING_KEY_PREFIX + instanceId;
101
- }
102
-
103
- function settingKeys(): string[] {
104
- return computeInstanceIds().map((instanceId) => settingKey(instanceId));
105
- }
106
-
107
- function readSetting(key: string): unknown {
108
- if (!pluginRef) return undefined;
109
- const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
110
- return ok ? value : undefined;
111
- }
112
-
113
- function writeSetting(key: string, value: unknown): boolean {
114
- if (!pluginRef) return false;
115
- const [ok] = pcall(() => pluginRef!.SetSetting(key, value));
116
- return ok;
117
- }
118
-
119
- function decodePayload(value: unknown): StopPayload | undefined {
120
- let decoded = value;
121
- if (typeIs(value, "string")) {
122
- const [ok, result] = pcall(() => HttpService.JSONDecode(value as string));
123
- if (!ok) return undefined;
124
- decoded = result;
125
- }
126
- if (!typeIs(decoded, "table")) return undefined;
127
- const payload = decoded as StopPayload;
128
- if (!typeIs(payload.kind, "string") || !typeIs(payload.id, "string")) {
129
- return undefined;
130
- }
131
- return payload;
132
- }
133
-
134
- function writePayload(key: string, payload: StopPayload): boolean {
135
- const [encodedOk, encoded] = pcall(() => HttpService.JSONEncode(payload));
136
- if (!encodedOk || !typeIs(encoded, "string")) return false;
137
- return writeSetting(key, encoded);
138
- }
139
-
140
- function writeResult(key: string, request: StopPayload, ok: boolean, errText?: string): void {
141
- writePayload(key, {
142
- kind: "result",
143
- id: request.id,
144
- requestedAt: request.requestedAt,
145
- consumedAt: tick(),
146
- ok,
147
- error: errText,
148
- });
149
- }
150
-
151
- function handleStopRequest(key: string, request: StopPayload): void {
152
- if (request.kind !== "request" || !typeIs(request.id, "string")) return;
153
- if (!typeIs(request.requestedAt, "number")) {
154
- writeSetting(key, false);
155
- return;
156
- }
157
-
158
- const age = tick() - request.requestedAt;
159
- if (age < -5 || age > REQUEST_TTL_SEC) {
160
- writeSetting(key, false);
161
- return;
162
- }
163
-
164
- if (endTestIssued) {
165
- writeResult(
166
- key,
167
- request,
168
- false,
169
- "StudioTestService:EndTest was already issued for this play session, but the runtime DataModel is still alive.",
170
- );
171
- return;
172
- }
173
-
174
- if (!RunService.IsRunning() || !RunService.IsServer()) {
175
- writeResult(key, request, false, "StopPlayMonitor is not running in the server DataModel.");
176
- return;
177
- }
178
-
179
- endTestIssued = true;
180
- const [endOk, endErr] = pcall(() => StudioTestService.EndTest("stopped_by_mcp"));
181
- writeResult(key, request, endOk, endOk ? undefined : tostring(endErr));
182
- if (!endOk) {
183
- endTestIssued = false;
184
- }
185
- }
186
-
187
- function startMonitor(): void {
188
- if (!pluginRef) {
189
- warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping");
190
- return;
191
- }
192
- task.spawn(() => {
193
- while (true) {
194
- for (const myKey of settingKeys()) {
195
- const value = readSetting(myKey);
196
- if (value === true) {
197
- // Legacy boolean requests are ambiguous and may be stale from
198
- // a prior crashed session. New stop requests use token payloads.
199
- writeSetting(myKey, false);
200
- } else {
201
- const payload = decodePayload(value);
202
- if (payload) {
203
- handleStopRequest(myKey, payload);
204
- }
205
- }
206
- }
207
- task.wait(POLL_INTERVAL_SEC);
208
- }
209
- });
210
- }
211
-
212
- function requestStop(): StopRequestResult {
213
- if (!pluginRef) return { ok: false };
214
- const requestId = HttpService.GenerateGUID(false);
215
- const payload: StopPayload = {
216
- kind: "request",
217
- id: requestId,
218
- requestedAt: tick(),
219
- };
220
- let ok = false;
221
- for (const myKey of settingKeys()) {
222
- ok = writePayload(myKey, payload) || ok;
223
- }
224
- return { ok, requestId: ok ? requestId : undefined };
225
- }
226
-
227
- function waitForConsumption(requestId: string): StopConsumptionResult {
228
- if (!pluginRef) return { ok: false, consumed: false, error: "Plugin reference is not initialized." };
229
- const start = tick();
230
- while (tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC) {
231
- for (const myKey of settingKeys()) {
232
- const payload = decodePayload(readSetting(myKey));
233
- if (payload && payload.kind === "result" && payload.id === requestId) {
234
- return {
235
- ok: payload.ok === true,
236
- consumed: true,
237
- error: payload.error,
238
- };
239
- }
240
- }
241
- task.wait(WAIT_POLL_SEC);
242
- }
243
- return {
244
- ok: false,
245
- consumed: false,
246
- error: "Timed out waiting for the play-server DataModel to acknowledge stop_playtest.",
247
- };
248
- }
249
-
250
- function clearPending(requestId?: string): void {
251
- if (!pluginRef) return;
252
- for (const myKey of settingKeys()) {
253
- if (requestId !== undefined) {
254
- const payload = decodePayload(readSetting(myKey));
255
- if (payload && payload.id !== requestId) continue;
256
- }
257
- writeSetting(myKey, false);
258
- }
259
- }
260
-
261
- export = {
262
- init,
263
- startMonitor,
264
- requestStop,
265
- waitForConsumption,
266
- clearPending,
267
- };