@chrrxs/robloxstudio-mcp 2.23.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/index.js +1688 -4547
  2. package/package.json +7 -5
  3. package/studio-plugin/MCPPlugin.rbxmx +489 -1965
  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 -171527
  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 -632
  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 -469
  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/BuildHandlers.ts +0 -481
  32. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
  33. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
  34. package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
  35. package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
  36. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +0 -380
  37. package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
  38. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
  39. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -354
  40. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
  41. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -191
  42. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -874
  43. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
  44. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -530
  45. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
  46. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
  47. package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
  48. package/studio-plugin/src/server/index.server.ts +0 -135
  49. package/studio-plugin/src/types/index.d.ts +0 -57
  50. package/studio-plugin/tsconfig.json +0 -20
@@ -1,632 +0,0 @@
1
- import { HttpService, RunService, ServerStorage } from "@rbxts/services";
2
- import State from "./State";
3
- import Utils from "./Utils";
4
- import UI from "./UI";
5
- import { cleanupLegacyEditBridges } from "./EvalBridges";
6
- import QueryHandlers from "./handlers/QueryHandlers";
7
- import PropertyHandlers from "./handlers/PropertyHandlers";
8
- import InstanceHandlers from "./handlers/InstanceHandlers";
9
- import ScriptHandlers from "./handlers/ScriptHandlers";
10
- import MetadataHandlers from "./handlers/MetadataHandlers";
11
- import TestHandlers from "./handlers/TestHandlers";
12
- import BuildHandlers from "./handlers/BuildHandlers";
13
- import AssetHandlers from "./handlers/AssetHandlers";
14
- import CaptureHandlers from "./handlers/CaptureHandlers";
15
- import InputHandlers from "./handlers/InputHandlers";
16
- import LogHandlers from "./handlers/LogHandlers";
17
- import SerializationHandlers from "./handlers/SerializationHandlers";
18
- import MemoryHandlers from "./handlers/MemoryHandlers";
19
- import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
20
- import BreakpointHandlers from "./handlers/BreakpointHandlers";
21
- import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
22
- import MicroProfilerHandlers from "./handlers/MicroProfilerHandlers";
23
- import GenerateModelHandlers from "./handlers/GenerateModelHandlers";
24
- import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
25
- import ClientBroker from "./ClientBroker";
26
- import ServerUrlSettings from "./ServerUrlSettings";
27
- import HttpDiagnostics from "./HttpDiagnostics";
28
- import { Connection, RequestPayload, PollResponse, ReadyResponse } from "../types";
29
-
30
- // Per-plugin-load random GUID. Used as the /poll URL param so the server
31
- // can tell our polls apart from any other plugin's polls. Not user-facing —
32
- // MCP tools and the LLM operate on instanceId (the place identifier).
33
- const pluginSessionId = HttpService.GenerateGUID(false);
34
-
35
- // Place-level identifier shared by every plugin running in DataModels of
36
- // the same place file (edit DM + playtest server DM + playtest clients).
37
- // Format: "place:<PlaceId>" when published, "anon:<UUID>" for unpublished
38
- // places where the UUID lives on ServerStorage's __MCPPlaceId attribute
39
- // and travels with the .rbxl.
40
- const MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId";
41
-
42
- function computeInstanceId(): string {
43
- if (game.PlaceId !== 0) {
44
- return `place:${tostring(game.PlaceId)}`;
45
- }
46
- const existing = ServerStorage.GetAttribute(MCP_PLACE_ID_ATTRIBUTE);
47
- if (typeIs(existing, "string") && existing !== "") {
48
- return `anon:${existing as string}`;
49
- }
50
- const fresh = HttpService.GenerateGUID(false);
51
- pcall(() => ServerStorage.SetAttribute(MCP_PLACE_ID_ATTRIBUTE, fresh));
52
- return `anon:${fresh}`;
53
- }
54
-
55
- let assignedRole: string | undefined;
56
- let hasVersionMismatch = false;
57
- let lastVersionMismatchWarningKey: string | undefined;
58
- let lastReadyInstanceId: string | undefined;
59
- const readyFailureLogKeys = new Set<string>();
60
- let retryingDuplicateReady = false;
61
-
62
- // Cache the published place name from MarketplaceService:GetProductInfo so
63
- // /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
64
- // from game.Name (the DataModel name, often "Place1" in edit). We only fetch
65
- // once per plugin load; the published name doesn't change mid-session.
66
- let cachedPlaceName: string | undefined;
67
- let cachedPlaceNamePlaceId: number | undefined;
68
-
69
- function resolvePlaceName(): string {
70
- if (cachedPlaceName !== undefined && cachedPlaceNamePlaceId === game.PlaceId) return cachedPlaceName;
71
- cachedPlaceName = undefined;
72
- cachedPlaceNamePlaceId = game.PlaceId;
73
- if (game.PlaceId === 0) {
74
- cachedPlaceName = game.Name;
75
- return cachedPlaceName;
76
- }
77
- const MarketplaceService = game.GetService("MarketplaceService");
78
- const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
79
- if (ok && info !== undefined) {
80
- const name = (info as { Name?: string }).Name;
81
- if (typeIs(name, "string") && name !== "") {
82
- cachedPlaceName = name;
83
- return cachedPlaceName;
84
- }
85
- }
86
- // Don't cache failures — could be transient (offline, rate-limited).
87
- // Next /ready will retry. Return game.Name as fallback.
88
- return game.Name;
89
- }
90
-
91
- function detectRole(): string {
92
- if (!RunService.IsRunning()) return "edit";
93
- if (RunService.IsServer()) return "server";
94
- return "client";
95
- }
96
-
97
- const initialRole = detectRole();
98
-
99
- type Handler = (data: Record<string, unknown>) => unknown;
100
-
101
- const routeMap: Record<string, Handler> = {
102
-
103
- "/api/file-tree": QueryHandlers.getFileTree,
104
- "/api/search-files": QueryHandlers.searchFiles,
105
- "/api/place-info": QueryHandlers.getPlaceInfo,
106
- "/api/services": QueryHandlers.getServices,
107
- "/api/search-objects": QueryHandlers.searchObjects,
108
- "/api/instance-properties": QueryHandlers.getInstanceProperties,
109
- "/api/instance-children": QueryHandlers.getInstanceChildren,
110
- "/api/search-by-property": QueryHandlers.searchByProperty,
111
- "/api/class-info": QueryHandlers.getClassInfo,
112
- "/api/project-structure": QueryHandlers.getProjectStructure,
113
- "/api/grep-scripts": QueryHandlers.grepScripts,
114
- "/api/get-descendants": QueryHandlers.getDescendants,
115
- "/api/compare-instances": QueryHandlers.compareInstances,
116
-
117
- "/api/set-property": PropertyHandlers.setProperty,
118
- "/api/set-properties": PropertyHandlers.setProperties,
119
- "/api/mass-set-property": PropertyHandlers.massSetProperty,
120
- "/api/mass-get-property": PropertyHandlers.massGetProperty,
121
- "/api/create-object": InstanceHandlers.createObject,
122
- "/api/mass-create-objects": InstanceHandlers.massCreateObjects,
123
- // Back-compat alias: pre-2.7.0 servers split this endpoint when properties were present.
124
- "/api/mass-create-objects-with-properties": InstanceHandlers.massCreateObjects,
125
- "/api/delete-object": InstanceHandlers.deleteObject,
126
- "/api/smart-duplicate": InstanceHandlers.smartDuplicate,
127
- "/api/mass-duplicate": InstanceHandlers.massDuplicate,
128
- "/api/clone-object": InstanceHandlers.cloneObject,
129
-
130
- "/api/get-script-source": ScriptHandlers.getScriptSource,
131
- "/api/set-script-source": ScriptHandlers.setScriptSource,
132
- "/api/edit-script-lines": ScriptHandlers.editScriptLines,
133
- "/api/insert-script-lines": ScriptHandlers.insertScriptLines,
134
- "/api/delete-script-lines": ScriptHandlers.deleteScriptLines,
135
-
136
- "/api/set-attribute": MetadataHandlers.setAttribute,
137
- "/api/get-attributes": MetadataHandlers.getAttributes,
138
- "/api/delete-attribute": MetadataHandlers.deleteAttribute,
139
- "/api/get-tags": MetadataHandlers.getTags,
140
- "/api/add-tag": MetadataHandlers.addTag,
141
- "/api/remove-tag": MetadataHandlers.removeTag,
142
- "/api/get-tagged": MetadataHandlers.getTagged,
143
- "/api/get-selection": MetadataHandlers.getSelection,
144
- "/api/execute-luau": MetadataHandlers.executeLuau,
145
- "/api/eval-runtime": EvalRuntimeHandlers.evalRuntime,
146
- "/api/undo": MetadataHandlers.undo,
147
- "/api/redo": MetadataHandlers.redo,
148
- "/api/bulk-set-attributes": MetadataHandlers.bulkSetAttributes,
149
-
150
- "/api/start-playtest": TestHandlers.startPlaytest,
151
- "/api/stop-playtest": TestHandlers.stopPlaytest,
152
- "/api/multiplayer-test-start": TestHandlers.multiplayerTestStart,
153
- "/api/multiplayer-test-state": TestHandlers.multiplayerTestState,
154
- "/api/multiplayer-test-add-players": TestHandlers.multiplayerTestAddPlayers,
155
- "/api/multiplayer-test-leave-client": TestHandlers.multiplayerTestLeaveClient,
156
- "/api/multiplayer-test-end": TestHandlers.multiplayerTestEnd,
157
-
158
- "/api/export-build": BuildHandlers.exportBuild,
159
- "/api/import-build": BuildHandlers.importBuild,
160
- "/api/import-scene": BuildHandlers.importScene,
161
- "/api/search-materials": BuildHandlers.searchMaterials,
162
-
163
- "/api/insert-asset": AssetHandlers.insertAsset,
164
- "/api/preview-asset": AssetHandlers.previewAsset,
165
-
166
- "/api/capture-screenshot": CaptureHandlers.captureScreenshot,
167
- "/api/capture-begin": CaptureHandlers.captureBegin,
168
- "/api/capture-read": CaptureHandlers.captureRead,
169
- "/api/simulate-mouse-input": InputHandlers.simulateMouseInput,
170
- "/api/simulate-keyboard-input": InputHandlers.simulateKeyboardInput,
171
-
172
- "/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
173
-
174
- "/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
175
- "/api/breakpoints": BreakpointHandlers.breakpoints,
176
- "/api/capture-script-profiler": ScriptProfilerHandlers.captureScriptProfiler,
177
- "/api/capture-micro-profiler": MicroProfilerHandlers.captureMicroProfiler,
178
- "/api/generate-model": GenerateModelHandlers.generateModel,
179
-
180
- "/api/export-rbxm": SerializationHandlers.exportRbxm,
181
- "/api/import-rbxm": SerializationHandlers.importRbxm,
182
-
183
- "/api/get-memory-breakdown": MemoryHandlers.getMemoryBreakdown,
184
- "/api/get-scene-analysis": SceneAnalysisHandlers.getSceneAnalysis,
185
- };
186
-
187
- function processRequest(request: RequestPayload): unknown {
188
- const endpoint = request.endpoint;
189
- const data = request.data ?? {};
190
-
191
- const handler = routeMap[endpoint];
192
- if (handler) {
193
- return handler(data as Record<string, unknown>);
194
- } else {
195
- return { error: `Unknown endpoint: ${endpoint}` };
196
- }
197
- }
198
-
199
- function sendResponse(conn: Connection, requestId: string, responseData: unknown) {
200
- const responseUrl = `${conn.serverUrl}/response`;
201
- const [encodeOk, encoded] = pcall(() => HttpService.JSONEncode({ requestId, response: responseData }));
202
- const body = encodeOk
203
- ? encoded
204
- : HttpService.JSONEncode({
205
- requestId,
206
- error: `Plugin response serialization failed: ${tostring(encoded)}`,
207
- });
208
- if (!encodeOk) {
209
- warn(`[robloxstudio-mcp] Failed to serialize response ${requestId}: ${tostring(encoded)}`);
210
- }
211
-
212
- const [requestOk, requestResult] = pcall(() => HttpService.RequestAsync({
213
- Url: responseUrl,
214
- Method: "POST",
215
- Headers: { "Content-Type": "application/json" },
216
- Body: body,
217
- }));
218
- if (!requestOk || !requestResult.Success) {
219
- warn(`[robloxstudio-mcp] Failed to deliver response ${requestId}: ${HttpDiagnostics.formatRequestFailure(responseUrl, requestOk, requestResult)}`);
220
- }
221
- }
222
-
223
- function getConnectionStatus(): string {
224
- const conn = State.getActiveConnection();
225
- if (!conn.isActive) return "disconnected";
226
- if (conn.consecutiveFailures >= conn.maxFailuresBeforeError) return "error";
227
- if (conn.lastHttpOk) return "connected";
228
- return "connecting";
229
- }
230
-
231
- // Throttle for re-issuing /ready after the server reports knownInstance=false.
232
- // Without this, every poll during the brief window where the server has just
233
- // restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
234
- let lastReadyPostAt = 0;
235
-
236
- // game.Name and game.PlaceId can both settle after plugin load. PlaceId also
237
- // changes when an unpublished file is published while MCP is already active.
238
- // Re-fire /ready so the bridge can migrate anon:<uuid> to place:<PlaceId>.
239
- let nameChangeConn: RBXScriptConnection | undefined;
240
- let placeIdChangeConn: RBXScriptConnection | undefined;
241
- function ensureIdentityWatcher(conn: Connection): void {
242
- if (!nameChangeConn) {
243
- const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("Name"));
244
- if (okSig && signal) {
245
- nameChangeConn = signal.Connect(() => {
246
- // sendReady has its own 2s throttle, so rapid burst changes coalesce.
247
- sendReady(conn);
248
- });
249
- }
250
- }
251
- if (!placeIdChangeConn) {
252
- const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("PlaceId"));
253
- if (okSig && signal) {
254
- placeIdChangeConn = signal.Connect(() => {
255
- cachedPlaceName = undefined;
256
- cachedPlaceNamePlaceId = undefined;
257
- sendReady(conn);
258
- });
259
- }
260
- }
261
- }
262
-
263
- function sendReady(conn: Connection): void {
264
- const now = tick();
265
- // Normal identity refreshes stay conservatively throttled. Once a stale
266
- // predecessor causes 409, retry once per second so takeover follows the
267
- // server's short inactivity lease without another two seconds of jitter.
268
- const readyInterval = retryingDuplicateReady ? 1 : 2;
269
- if (now - lastReadyPostAt < readyInterval) return;
270
- lastReadyPostAt = now;
271
- const instanceId = computeInstanceId();
272
- task.spawn(() => {
273
- const [readyOk, readyResult] = pcall(() => {
274
- return HttpService.RequestAsync({
275
- Url: `${conn.serverUrl}/ready`,
276
- Method: "POST",
277
- Headers: { "Content-Type": "application/json" },
278
- Body: HttpService.JSONEncode({
279
- pluginSessionId,
280
- instanceId,
281
- role: detectRole(),
282
- placeId: game.PlaceId,
283
- placeName: resolvePlaceName(),
284
- dataModelName: game.Name,
285
- isRunning: RunService.IsRunning(),
286
- pluginVersion: State.CURRENT_VERSION,
287
- pluginVariant: State.PLUGIN_VARIANT,
288
- pluginReady: true,
289
- timestamp: tick(),
290
- }),
291
- });
292
- });
293
- const readyUrl = `${conn.serverUrl}/ready`;
294
- const readyRole = detectRole();
295
- const readyLogKey = `${conn.serverUrl}|${instanceId}|${readyRole}`;
296
- if (!readyOk) {
297
- const shouldLog = !readyFailureLogKeys.has(readyLogKey);
298
- readyFailureLogKeys.add(readyLogKey);
299
- if (shouldLog) {
300
- warn(`[robloxstudio-mcp] /ready failed for ${instanceId}/${readyRole}: ${HttpDiagnostics.formatRequestFailure(readyUrl, readyOk, readyResult)}`);
301
- }
302
- return;
303
- }
304
- if (!readyResult.Success) {
305
- const reason = HttpDiagnostics.formatRequestFailure(readyUrl, true, readyResult);
306
- const shouldLog = !readyFailureLogKeys.has(readyLogKey);
307
- readyFailureLogKeys.add(readyLogKey);
308
- // A predecessor can remain registered briefly when Studio exits before
309
- // its asynchronous /disconnect completes. Keep polling and retrying
310
- // /ready: the server will take us over once the predecessor has stopped
311
- // polling, while a genuinely active duplicate continues to hold routing.
312
- if (readyResult.StatusCode === 409) {
313
- retryingDuplicateReady = true;
314
- const ui = UI.getElements();
315
- ui.statusLabel.Text = "Waiting for previous instance";
316
- ui.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
317
- ui.detailStatusLabel.Text = reason;
318
- ui.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
319
- if (shouldLog) {
320
- warn(`[robloxstudio-mcp] /ready rejected for ${instanceId}/${readyRole}: ${reason}`);
321
- }
322
- return;
323
- }
324
- if (shouldLog) {
325
- warn(`[robloxstudio-mcp] /ready rejected for ${instanceId}/${readyRole}: ${reason}`);
326
- }
327
- return;
328
- }
329
- retryingDuplicateReady = false;
330
- const [parseOk, readyData] = pcall(
331
- () => HttpService.JSONDecode(readyResult.Body) as ReadyResponse,
332
- );
333
- if (parseOk && readyData.assignedRole) {
334
- assignedRole = readyData.assignedRole;
335
- }
336
- lastReadyInstanceId = parseOk && typeIs(readyData.instanceId, "string") && readyData.instanceId !== ""
337
- ? readyData.instanceId
338
- : instanceId;
339
- ServerUrlSettings.rememberServerUrl(conn.serverUrl);
340
- const connectedRole = assignedRole ?? detectRole();
341
- if (readyFailureLogKeys.has(readyLogKey)) {
342
- readyFailureLogKeys.delete(readyLogKey);
343
- print(`[robloxstudio-mcp] /ready connected for ${instanceId}/${connectedRole} via ${conn.serverUrl}`);
344
- }
345
- });
346
- }
347
-
348
- function pollForRequests() {
349
- const conn = State.getActiveConnection();
350
- if (!conn.isActive) return;
351
- if (conn.isPolling) return;
352
-
353
- conn.isPolling = true;
354
-
355
- const [success, result] = pcall(() => {
356
- return HttpService.RequestAsync({
357
- Url: `${conn.serverUrl}/poll?pluginSessionId=${pluginSessionId}`,
358
- Method: "GET",
359
- Headers: { "Content-Type": "application/json" },
360
- });
361
- });
362
-
363
- conn.isPolling = false;
364
-
365
- const ui = UI.getElements();
366
- UI.updateToolbarIcon();
367
-
368
- if (success && (result.Success || result.StatusCode === 503)) {
369
- conn.consecutiveFailures = 0;
370
- conn.currentRetryDelay = 0.5;
371
- conn.lastSuccessfulConnection = tick();
372
-
373
- const data = HttpService.JSONDecode(result.Body) as PollResponse;
374
- const mcpConnected = data.mcpConnected === true;
375
- conn.lastHttpOk = true;
376
- conn.lastMcpOk = mcpConnected;
377
- const serverVersion = data.serverVersion ?? "unknown";
378
- if (data.versionMismatch === true) {
379
- hasVersionMismatch = true;
380
- const warningKey = `${State.CURRENT_VERSION}:${serverVersion}`;
381
- if (lastVersionMismatchWarningKey !== warningKey) {
382
- lastVersionMismatchWarningKey = warningKey;
383
- warn(`[robloxstudio-mcp] Version mismatch: Studio plugin v${State.CURRENT_VERSION} / MCP v${serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`);
384
- }
385
- UI.showBanner("version-mismatch", `Plugin v${State.CURRENT_VERSION} / MCP v${serverVersion} mismatch`);
386
- } else if (hasVersionMismatch) {
387
- hasVersionMismatch = false;
388
- UI.hideBanner("version-mismatch");
389
- }
390
-
391
- // Server tells us when its in-memory instances map doesn't have us
392
- // (e.g. after an MCP process restart). Re-issue /ready immediately so
393
- // target=server/client-N start routing again. The throttle inside
394
- // sendReady() prevents duplicate registrations while the server
395
- // catches up.
396
- if (data.knownInstance === false) {
397
- sendReady(conn);
398
- }
399
-
400
- const el = ui;
401
- el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
402
- el.step1Label.Text = "HTTP server (OK)";
403
-
404
- if (mcpConnected && !el.statusLabel.Text.find("Connected")[0]) {
405
- el.statusLabel.Text = "Connected";
406
- el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94);
407
- el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
408
- el.statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
409
- el.statusText.Text = "ONLINE";
410
- el.detailStatusLabel.Text = "HTTP: OK MCP: OK";
411
- el.detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94);
412
- el.step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
413
- el.step2Label.Text = "MCP bridge (OK)";
414
- el.step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
415
- el.step3Label.Text = "Commands (OK)";
416
- conn.mcpWaitStartTime = undefined;
417
- el.troubleshootLabel.Visible = false;
418
- UI.stopPulseAnimation();
419
- } else if (!mcpConnected) {
420
- el.statusLabel.Text = "Waiting for MCP server";
421
- el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
422
- el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
423
- el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
424
- el.statusText.Text = "WAITING";
425
- el.detailStatusLabel.Text = "HTTP: OK MCP: ...";
426
- el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
427
- el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
428
- el.step2Label.Text = "MCP bridge (waiting...)";
429
- el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
430
- el.step3Label.Text = "Commands (waiting...)";
431
- if (conn.mcpWaitStartTime === undefined) {
432
- conn.mcpWaitStartTime = tick();
433
- }
434
- const elapsed = tick() - (conn.mcpWaitStartTime ?? tick());
435
- el.troubleshootLabel.Visible = elapsed > 8;
436
- UI.startPulseAnimation();
437
- }
438
-
439
- if (data.request && mcpConnected) {
440
- task.spawn(() => {
441
- const [ok, response] = pcall(() => processRequest(data.request!));
442
- if (ok) {
443
- sendResponse(conn, data.requestId!, response);
444
- } else {
445
- sendResponse(conn, data.requestId!, { error: tostring(response) });
446
- }
447
- });
448
- }
449
- } else if (conn.isActive) {
450
- conn.consecutiveFailures++;
451
-
452
- if (conn.consecutiveFailures > 1) {
453
- conn.currentRetryDelay = math.min(
454
- conn.currentRetryDelay * conn.retryBackoffMultiplier,
455
- conn.maxRetryDelay,
456
- );
457
- }
458
-
459
-
460
- const el = ui;
461
- if (conn.consecutiveFailures >= conn.maxFailuresBeforeError) {
462
- el.statusLabel.Text = "Server unavailable";
463
- el.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
464
- el.statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
465
- el.statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
466
- el.statusText.Text = "ERROR";
467
- el.detailStatusLabel.Text = "HTTP: X MCP: X";
468
- el.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
469
- el.step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
470
- el.step1Label.Text = "HTTP server (error)";
471
- el.step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
472
- el.step2Label.Text = "MCP bridge (error)";
473
- el.step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
474
- el.step3Label.Text = "Commands (error)";
475
- conn.mcpWaitStartTime = undefined;
476
- el.troubleshootLabel.Visible = false;
477
- UI.stopPulseAnimation();
478
- } else if (conn.consecutiveFailures > 5) {
479
- const waitTime = math.ceil(conn.currentRetryDelay);
480
- el.statusLabel.Text = `Retrying (${waitTime}s)`;
481
- el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
482
- el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
483
- el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
484
- el.statusText.Text = "RETRY";
485
- el.detailStatusLabel.Text = "HTTP: ... MCP: ...";
486
- el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
487
- el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
488
- el.step1Label.Text = "HTTP server (retrying...)";
489
- el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
490
- el.step2Label.Text = "MCP bridge (retrying...)";
491
- el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
492
- el.step3Label.Text = "Commands (retrying...)";
493
- conn.mcpWaitStartTime = undefined;
494
- el.troubleshootLabel.Visible = false;
495
- UI.startPulseAnimation();
496
- } else if (conn.consecutiveFailures > 1) {
497
- el.statusLabel.Text = `Connecting (attempt ${conn.consecutiveFailures})`;
498
- el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
499
- el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
500
- el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
501
- el.statusText.Text = "CONNECTING";
502
- el.detailStatusLabel.Text = "HTTP: ... MCP: ...";
503
- el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
504
- el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
505
- el.step1Label.Text = "HTTP server (connecting...)";
506
- el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
507
- el.step2Label.Text = "MCP bridge (connecting...)";
508
- el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
509
- el.step3Label.Text = "Commands (connecting...)";
510
- conn.mcpWaitStartTime = undefined;
511
- el.troubleshootLabel.Visible = false;
512
- UI.startPulseAnimation();
513
- }
514
- }
515
- }
516
-
517
-
518
- function activatePlugin() {
519
- const conn = State.getActiveConnection();
520
- const ui = UI.getElements();
521
-
522
- conn.isActive = true;
523
- conn.consecutiveFailures = 0;
524
- conn.currentRetryDelay = 0.5;
525
-
526
- const normalizedUrl = ServerUrlSettings.normalizeServerUrl(ui.urlInput.Text);
527
- conn.serverUrl = normalizedUrl !== "" ? normalizedUrl : conn.serverUrl;
528
- if (conn.serverUrl === "") conn.serverUrl = ClientBroker.DEFAULT_MCP_URL;
529
- ui.urlInput.Text = conn.serverUrl;
530
- const port = ServerUrlSettings.extractPort(conn.serverUrl);
531
- if (port !== undefined) conn.port = port;
532
- UI.updateUIState();
533
-
534
- if (!conn.heartbeatConnection) {
535
- conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
536
- const now = tick();
537
- if (initialRole === "server" && !RunService.IsRunning()) {
538
- ClientBroker.disconnectAllProxies();
539
- deactivatePlugin();
540
- return;
541
- }
542
- const currentInstanceId = computeInstanceId();
543
- if (lastReadyInstanceId !== undefined && currentInstanceId !== lastReadyInstanceId) {
544
- cachedPlaceName = undefined;
545
- cachedPlaceNamePlaceId = undefined;
546
- sendReady(conn);
547
- }
548
- const currentInterval = conn.consecutiveFailures > 5 ? conn.currentRetryDelay : conn.pollInterval;
549
- if (now - conn.lastPoll > currentInterval) {
550
- conn.lastPoll = now;
551
- pollForRequests();
552
- }
553
- });
554
- }
555
-
556
- // Initial /ready; pollForRequests will also re-fire ready if the server
557
- // later reports knownInstance=false (process restart, etc).
558
- sendReady(conn);
559
-
560
- // Remove legacy edit-mode eval bridge scripts from older plugin builds.
561
- // Current bridges are created only in running play DataModels.
562
- if (!RunService.IsRunning()) {
563
- task.spawn(cleanupLegacyEditBridges);
564
- }
565
-
566
- // Watch identity fields so stale name or anon instance ids are refreshed.
567
- ensureIdentityWatcher(conn);
568
- }
569
-
570
- function deactivatePlugin() {
571
- const conn = State.getActiveConnection();
572
- conn.isActive = false;
573
- conn.lastMcpOk = false;
574
-
575
- UI.updateUIState();
576
-
577
- pcall(() => {
578
- HttpService.RequestAsync({
579
- Url: `${conn.serverUrl}/disconnect`,
580
- Method: "POST",
581
- Headers: { "Content-Type": "application/json" },
582
- Body: HttpService.JSONEncode({ pluginSessionId, timestamp: tick() }),
583
- });
584
- });
585
-
586
- if (conn.heartbeatConnection) {
587
- conn.heartbeatConnection.Disconnect();
588
- conn.heartbeatConnection = undefined;
589
- }
590
-
591
- conn.consecutiveFailures = 0;
592
- conn.currentRetryDelay = 0.5;
593
- }
594
-
595
- function deactivateAll() {
596
- const conn = State.getActiveConnection();
597
- if (conn.isActive) {
598
- deactivatePlugin();
599
- }
600
- }
601
-
602
- function checkForUpdates() {
603
- task.spawn(() => {
604
- const [success, result] = pcall(() => {
605
- return HttpService.RequestAsync({
606
- Url: "https://registry.npmjs.org/@chrrxs/robloxstudio-mcp/latest",
607
- Method: "GET",
608
- Headers: { Accept: "application/json" },
609
- });
610
- });
611
-
612
- if (success && result.Success) {
613
- const [ok, data] = pcall(() => HttpService.JSONDecode(result.Body) as { version?: string });
614
- if (ok && data?.version) {
615
- const latestVersion = data.version;
616
- if (Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0) {
617
- if (!hasVersionMismatch) {
618
- UI.showBanner("update", `v${latestVersion} available - github.com/chrrxs/robloxstudio-mcp`);
619
- }
620
- }
621
- }
622
- }
623
- });
624
- }
625
-
626
- export = {
627
- getConnectionStatus,
628
- activatePlugin,
629
- deactivatePlugin,
630
- deactivateAll,
631
- checkForUpdates,
632
- };