rbxstudio-mcp 2.2.1 → 2.2.3
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/LICENSE +0 -0
- package/README.md +0 -0
- package/dist/__tests__/bridge-service.test.d.ts +0 -0
- package/dist/__tests__/bridge-service.test.d.ts.map +0 -0
- package/dist/__tests__/bridge-service.test.js +0 -0
- package/dist/__tests__/bridge-service.test.js.map +0 -0
- package/dist/__tests__/http-server.test.d.ts +0 -0
- package/dist/__tests__/http-server.test.d.ts.map +0 -0
- package/dist/__tests__/http-server.test.js +0 -0
- package/dist/__tests__/http-server.test.js.map +0 -0
- package/dist/__tests__/integration.test.d.ts +0 -0
- package/dist/__tests__/integration.test.d.ts.map +0 -0
- package/dist/__tests__/integration.test.js +0 -0
- package/dist/__tests__/integration.test.js.map +0 -0
- package/dist/__tests__/smoke.test.d.ts +0 -0
- package/dist/__tests__/smoke.test.d.ts.map +0 -0
- package/dist/__tests__/smoke.test.js +0 -0
- package/dist/__tests__/smoke.test.js.map +0 -0
- package/dist/bridge-service.d.ts +94 -0
- package/dist/bridge-service.d.ts.map +1 -1
- package/dist/bridge-service.js +188 -1
- package/dist/bridge-service.js.map +1 -1
- package/dist/http-server.d.ts +0 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +52 -0
- package/dist/http-server.js.map +1 -1
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/index.d.ts +15 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +121 -10
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/studio-client.d.ts +0 -0
- package/dist/tools/studio-client.d.ts.map +0 -0
- package/dist/tools/studio-client.js +0 -0
- package/dist/tools/studio-client.js.map +0 -0
- package/package.json +1 -1
- package/studio-plugin/INSTALLATION.md +0 -0
- package/studio-plugin/MCPPlugin.rbxmx +0 -0
- package/studio-plugin/plugin.json +0 -0
- package/studio-plugin/plugin.luau +314 -91
|
@@ -7,12 +7,232 @@ local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
|
7
7
|
local CollectionService = game:GetService("CollectionService")
|
|
8
8
|
local LogService = game:GetService("LogService")
|
|
9
9
|
|
|
10
|
-
-- StudioTestService for play/stop control (may not be available in all contexts)
|
|
10
|
+
-- StudioTestService for play/stop control (may not be available in all contexts).
|
|
11
|
+
-- NOTE: The previous version of this file mistakenly fetched "TestService" here
|
|
12
|
+
-- — that's a different (legacy) service. The actual API for ExecutePlayModeAsync
|
|
13
|
+
-- and EndTest lives on "StudioTestService". The playSolo handler also re-fetches
|
|
14
|
+
-- this defensively so an older Studio doesn't blow up at plugin load time.
|
|
11
15
|
local StudioTestService = nil
|
|
12
16
|
pcall(function()
|
|
13
|
-
StudioTestService = game:GetService("
|
|
17
|
+
StudioTestService = game:GetService("StudioTestService")
|
|
14
18
|
end)
|
|
15
19
|
|
|
20
|
+
-- ============================================================================
|
|
21
|
+
-- TEST SESSION COMPANION
|
|
22
|
+
--
|
|
23
|
+
-- StudioTestService:EndTest() can only be called from the Server DataModel of
|
|
24
|
+
-- a running test. The plugin lives in the Edit DataModel, so it cannot call
|
|
25
|
+
-- EndTest itself. To work around this we inject a small Script into
|
|
26
|
+
-- ServerScriptService BEFORE calling ExecutePlayModeAsync. When the test
|
|
27
|
+
-- starts, that Script runs in the Server DataModel and:
|
|
28
|
+
-- 1. Streams LogService.MessageOut to the bridge (/test-session/log)
|
|
29
|
+
-- 2. Polls the bridge (/test-session/poll) for an "end" command
|
|
30
|
+
-- 3. On "end", calls StudioTestService:EndTest() — which terminates the
|
|
31
|
+
-- yielding ExecutePlayModeAsync call back in the plugin.
|
|
32
|
+
--
|
|
33
|
+
-- Cleanup strategy:
|
|
34
|
+
-- - Tagged with TEST_COMPANION_TAG so we can find orphans
|
|
35
|
+
-- - Swept on plugin activate, on every playSolo start, and after the test
|
|
36
|
+
-- ends (whether via stop_play, natural end, or user clicking Stop)
|
|
37
|
+
-- ============================================================================
|
|
38
|
+
local TEST_COMPANION_NAME = "_MCPTestCompanion"
|
|
39
|
+
local TEST_COMPANION_TAG = "_MCPTestCompanion"
|
|
40
|
+
|
|
41
|
+
-- The companion script source. Two placeholders are substituted at injection
|
|
42
|
+
-- time: __MCP_SERVER_URL__ and __MCP_SESSION_ID__. We use distinctive
|
|
43
|
+
-- placeholders (not %%...%% or {{...}}) to avoid collisions with Lua's own
|
|
44
|
+
-- pattern syntax during gsub.
|
|
45
|
+
local COMPANION_SCRIPT_TEMPLATE = [[-- _MCPTestCompanion (auto-generated, safe to delete)
|
|
46
|
+
-- Runs in the Server DataModel of a Studio test session started by the MCP
|
|
47
|
+
-- plugin. Pipes output back to the local MCP bridge and listens for an "end"
|
|
48
|
+
-- command to terminate the test cleanly via StudioTestService:EndTest().
|
|
49
|
+
|
|
50
|
+
local HttpService = game:GetService("HttpService")
|
|
51
|
+
local LogService = game:GetService("LogService")
|
|
52
|
+
local StudioTestService
|
|
53
|
+
do
|
|
54
|
+
local ok, svc = pcall(function() return game:GetService("StudioTestService") end)
|
|
55
|
+
if ok then StudioTestService = svc end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
local SERVER_URL = "__MCP_SERVER_URL__"
|
|
59
|
+
local SESSION_ID = "__MCP_SESSION_ID__"
|
|
60
|
+
local POLL_INTERVAL = 0.4
|
|
61
|
+
local LOG_FLUSH_INTERVAL = 0.25
|
|
62
|
+
local MAX_BUFFER = 5000
|
|
63
|
+
local MAX_CONSECUTIVE_FAILURES = 30 -- ~12s of failed polls before we give up
|
|
64
|
+
|
|
65
|
+
local outputBuffer = {}
|
|
66
|
+
local stopRequested = false
|
|
67
|
+
local consecutiveFailures = 0
|
|
68
|
+
|
|
69
|
+
local function postJSON(path, body)
|
|
70
|
+
local ok, response = pcall(function()
|
|
71
|
+
return HttpService:RequestAsync({
|
|
72
|
+
Url = SERVER_URL .. path,
|
|
73
|
+
Method = "POST",
|
|
74
|
+
Headers = { ["Content-Type"] = "application/json" },
|
|
75
|
+
Body = HttpService:JSONEncode(body),
|
|
76
|
+
})
|
|
77
|
+
end)
|
|
78
|
+
if ok and response and response.Success then
|
|
79
|
+
consecutiveFailures = 0
|
|
80
|
+
return true, response
|
|
81
|
+
else
|
|
82
|
+
consecutiveFailures += 1
|
|
83
|
+
return false, nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
LogService.MessageOut:Connect(function(message, messageType)
|
|
88
|
+
if #outputBuffer >= MAX_BUFFER then
|
|
89
|
+
-- Drop oldest to keep buffer bounded; we'd rather lose stale output
|
|
90
|
+
-- than balloon memory in a long-running test.
|
|
91
|
+
table.remove(outputBuffer, 1)
|
|
92
|
+
end
|
|
93
|
+
table.insert(outputBuffer, {
|
|
94
|
+
message = tostring(message),
|
|
95
|
+
messageType = messageType.Name,
|
|
96
|
+
timestamp = os.time(),
|
|
97
|
+
})
|
|
98
|
+
end)
|
|
99
|
+
|
|
100
|
+
-- Log flusher
|
|
101
|
+
task.spawn(function()
|
|
102
|
+
while not stopRequested do
|
|
103
|
+
if #outputBuffer > 0 then
|
|
104
|
+
local toSend = outputBuffer
|
|
105
|
+
outputBuffer = {}
|
|
106
|
+
local ok = postJSON("/test-session/log", {
|
|
107
|
+
sessionId = SESSION_ID,
|
|
108
|
+
messages = toSend,
|
|
109
|
+
})
|
|
110
|
+
if not ok then
|
|
111
|
+
-- Re-queue (capped) so we don't lose log lines on a transient
|
|
112
|
+
-- bridge hiccup. Newer entries take priority since they're
|
|
113
|
+
-- usually what the AI wants to see.
|
|
114
|
+
for i = 1, math.min(#toSend, MAX_BUFFER - #outputBuffer) do
|
|
115
|
+
table.insert(outputBuffer, toSend[i])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
task.wait(LOG_FLUSH_INTERVAL)
|
|
120
|
+
end
|
|
121
|
+
end)
|
|
122
|
+
|
|
123
|
+
-- Command poller
|
|
124
|
+
task.spawn(function()
|
|
125
|
+
while not stopRequested and consecutiveFailures < MAX_CONSECUTIVE_FAILURES do
|
|
126
|
+
local ok, response = postJSON("/test-session/poll", { sessionId = SESSION_ID })
|
|
127
|
+
if ok and response and response.Body then
|
|
128
|
+
local decodeOk, body = pcall(function()
|
|
129
|
+
return HttpService:JSONDecode(response.Body)
|
|
130
|
+
end)
|
|
131
|
+
if decodeOk and body then
|
|
132
|
+
-- Bridge tells us the session is over (e.g. user clicked Stop
|
|
133
|
+
-- in Studio, or the plugin was reloaded). Stop polling.
|
|
134
|
+
if body.ended then
|
|
135
|
+
stopRequested = true
|
|
136
|
+
break
|
|
137
|
+
end
|
|
138
|
+
if body.command and body.command.cmd == "end" then
|
|
139
|
+
stopRequested = true
|
|
140
|
+
-- Final flush of any pending output before we tear down.
|
|
141
|
+
if #outputBuffer > 0 then
|
|
142
|
+
local final = outputBuffer
|
|
143
|
+
outputBuffer = {}
|
|
144
|
+
postJSON("/test-session/log", {
|
|
145
|
+
sessionId = SESSION_ID,
|
|
146
|
+
messages = final,
|
|
147
|
+
})
|
|
148
|
+
end
|
|
149
|
+
-- Tell the bridge we're acknowledging the stop. This wakes
|
|
150
|
+
-- any stop_play caller waiting on a confirmation.
|
|
151
|
+
postJSON("/test-session/ended", {
|
|
152
|
+
sessionId = SESSION_ID,
|
|
153
|
+
reason = "mcp_stop",
|
|
154
|
+
})
|
|
155
|
+
-- Finally, end the test. This terminates the yielding
|
|
156
|
+
-- ExecutePlayModeAsync call back in the Edit-side plugin.
|
|
157
|
+
if StudioTestService then
|
|
158
|
+
pcall(function()
|
|
159
|
+
StudioTestService:EndTest(body.command.args or "MCP_Stop")
|
|
160
|
+
end)
|
|
161
|
+
end
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
task.wait(POLL_INTERVAL)
|
|
167
|
+
end
|
|
168
|
+
end)
|
|
169
|
+
]]
|
|
170
|
+
|
|
171
|
+
-- gsub-safe replacement (treat replacement as plain string, not a pattern).
|
|
172
|
+
local function plainReplace(haystack, needle, replacement)
|
|
173
|
+
-- Use string.find with plain=true to avoid pattern interpretation in needle.
|
|
174
|
+
local result = {}
|
|
175
|
+
local i = 1
|
|
176
|
+
while true do
|
|
177
|
+
local s, e = string.find(haystack, needle, i, true)
|
|
178
|
+
if not s then
|
|
179
|
+
table.insert(result, string.sub(haystack, i))
|
|
180
|
+
break
|
|
181
|
+
end
|
|
182
|
+
table.insert(result, string.sub(haystack, i, s - 1))
|
|
183
|
+
table.insert(result, replacement)
|
|
184
|
+
i = e + 1
|
|
185
|
+
end
|
|
186
|
+
return table.concat(result)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
local function cleanupTestCompanions()
|
|
190
|
+
local count = 0
|
|
191
|
+
pcall(function()
|
|
192
|
+
for _, inst in ipairs(CollectionService:GetTagged(TEST_COMPANION_TAG)) do
|
|
193
|
+
pcall(function() inst:Destroy() end)
|
|
194
|
+
count += 1
|
|
195
|
+
end
|
|
196
|
+
end)
|
|
197
|
+
-- Belt & suspenders: also remove any name-matching script in
|
|
198
|
+
-- ServerScriptService in case the tag was somehow stripped.
|
|
199
|
+
pcall(function()
|
|
200
|
+
local sss = game:GetService("ServerScriptService")
|
|
201
|
+
for _, child in ipairs(sss:GetChildren()) do
|
|
202
|
+
if child.Name == TEST_COMPANION_NAME then
|
|
203
|
+
pcall(function() child:Destroy() end)
|
|
204
|
+
count += 1
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end)
|
|
208
|
+
return count
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
local function injectTestCompanion(serverUrl, sessionId)
|
|
212
|
+
-- Always sweep before creating to guarantee at most one companion exists.
|
|
213
|
+
cleanupTestCompanions()
|
|
214
|
+
|
|
215
|
+
local source = COMPANION_SCRIPT_TEMPLATE
|
|
216
|
+
source = plainReplace(source, "__MCP_SERVER_URL__", serverUrl)
|
|
217
|
+
source = plainReplace(source, "__MCP_SESSION_ID__", sessionId)
|
|
218
|
+
|
|
219
|
+
local script = Instance.new("Script")
|
|
220
|
+
script.Name = TEST_COMPANION_NAME
|
|
221
|
+
script.Source = source
|
|
222
|
+
-- Archivable=false so a stray companion never gets saved into the user's
|
|
223
|
+
-- place file even if cleanup somehow misses it. The play-test mechanism
|
|
224
|
+
-- still includes it in the test DataModel because it duplicates the live
|
|
225
|
+
-- in-memory tree (not the saved file).
|
|
226
|
+
script.Archivable = false
|
|
227
|
+
script.Parent = game:GetService("ServerScriptService")
|
|
228
|
+
|
|
229
|
+
pcall(function()
|
|
230
|
+
CollectionService:AddTag(script, TEST_COMPANION_TAG)
|
|
231
|
+
end)
|
|
232
|
+
|
|
233
|
+
return script
|
|
234
|
+
end
|
|
235
|
+
|
|
16
236
|
-- Track output log for get_output
|
|
17
237
|
local outputBuffer = {}
|
|
18
238
|
local MAX_OUTPUT_BUFFER = 1000
|
|
@@ -4312,122 +4532,115 @@ end
|
|
|
4312
4532
|
|
|
4313
4533
|
-- ============================================
|
|
4314
4534
|
-- PLAYTEST CONTROL HANDLERS
|
|
4535
|
+
--
|
|
4536
|
+
-- play_solo: receives a sessionId from the bridge, injects a companion
|
|
4537
|
+
-- Script tagged with TEST_COMPANION_TAG into ServerScriptService, then
|
|
4538
|
+
-- spawns ExecutePlayModeAsync in a separate thread so the HTTP response
|
|
4539
|
+
-- can return immediately. When the test ends (for any reason), we sweep
|
|
4540
|
+
-- the companion and notify the bridge.
|
|
4541
|
+
--
|
|
4542
|
+
-- stop_play: this handler is kept only for backward compatibility. The
|
|
4543
|
+
-- real stop path is the bridge enqueueing an "end" command that the
|
|
4544
|
+
-- companion picks up and acts on. Calling this endpoint directly will
|
|
4545
|
+
-- always be a no-op telling the caller to use the new path.
|
|
4315
4546
|
-- ============================================
|
|
4316
4547
|
|
|
4317
|
-
-- Track active play test task
|
|
4318
|
-
local activePlayTestTask = nil
|
|
4319
|
-
|
|
4320
4548
|
handlers.playSolo = function(requestData)
|
|
4321
|
-
local
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
-- If already running, stop first
|
|
4325
|
-
if wasRunning then
|
|
4326
|
-
RunService:Stop()
|
|
4327
|
-
task.wait(0.2)
|
|
4328
|
-
end
|
|
4329
|
-
|
|
4330
|
-
-- Try the new StudioTestService first (available since Dec 2025)
|
|
4331
|
-
local studioTestSuccess, studioTestService = pcall(function()
|
|
4332
|
-
return game:GetService("StudioTestService")
|
|
4333
|
-
end)
|
|
4334
|
-
|
|
4335
|
-
if studioTestSuccess and studioTestService then
|
|
4336
|
-
-- Use the new StudioTestService API for proper Play Solo mode
|
|
4337
|
-
-- Run in background task so we don't block the HTTP response
|
|
4338
|
-
activePlayTestTask = task.spawn(function()
|
|
4339
|
-
-- ExecutePlayModeAsync(args: Variant?) - per official docs, pass a string identifier
|
|
4340
|
-
local testResult = studioTestService:ExecutePlayModeAsync("MCP_PlayTest")
|
|
4341
|
-
activePlayTestTask = nil
|
|
4342
|
-
end)
|
|
4343
|
-
|
|
4344
|
-
return {
|
|
4345
|
-
success = true,
|
|
4346
|
-
wasRunning = wasRunning,
|
|
4347
|
-
method = "StudioTestService:ExecutePlayModeAsync",
|
|
4348
|
-
message = wasRunning and "Restarted Play Solo (was already running)" or "Started Play Solo mode",
|
|
4349
|
-
note = "Use get_output to read script output. Use stop_play when done."
|
|
4350
|
-
}
|
|
4351
|
-
else
|
|
4352
|
-
-- Fallback to RunService for older Studio versions
|
|
4353
|
-
RunService:Run()
|
|
4354
|
-
|
|
4355
|
-
return {
|
|
4356
|
-
success = true,
|
|
4357
|
-
wasRunning = wasRunning,
|
|
4358
|
-
method = "RunService:Run (fallback)",
|
|
4359
|
-
message = wasRunning and "Restarted play test (was already running)" or "Started Run mode (fallback - StudioTestService not available)",
|
|
4360
|
-
note = "Use get_output to read script output. Use stop_play when done."
|
|
4361
|
-
}
|
|
4362
|
-
end
|
|
4363
|
-
end)
|
|
4364
|
-
|
|
4365
|
-
if success then
|
|
4366
|
-
return result
|
|
4367
|
-
else
|
|
4549
|
+
local sessionId = requestData and requestData.sessionId
|
|
4550
|
+
if type(sessionId) ~= "string" or sessionId == "" then
|
|
4368
4551
|
return {
|
|
4369
4552
|
success = false,
|
|
4370
|
-
error = "
|
|
4553
|
+
error = "sessionId required (the bridge generates one and passes it; older clients won't have it).",
|
|
4371
4554
|
}
|
|
4372
4555
|
end
|
|
4373
|
-
end
|
|
4374
4556
|
|
|
4375
|
-
|
|
4376
|
-
local
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
message = "Play test was not running"
|
|
4384
|
-
}
|
|
4385
|
-
end
|
|
4557
|
+
-- Validate StudioTestService is available BEFORE we touch anything.
|
|
4558
|
+
local stsOk, sts = pcall(function() return game:GetService("StudioTestService") end)
|
|
4559
|
+
if not stsOk or not sts then
|
|
4560
|
+
return {
|
|
4561
|
+
success = false,
|
|
4562
|
+
error = "StudioTestService unavailable in this Studio version. Update Roblox Studio to use playtest features.",
|
|
4563
|
+
}
|
|
4564
|
+
end
|
|
4386
4565
|
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4566
|
+
-- If a previous test was somehow left running (e.g. plugin reloaded
|
|
4567
|
+
-- mid-test, user clicked Stop without us hearing), best-effort it.
|
|
4568
|
+
-- We can't detect Play mode from a plugin reliably, so we just do an
|
|
4569
|
+
-- orphan sweep — if there's no actual test running, the sweep is a
|
|
4570
|
+
-- no-op; if there is one, the user will need to stop it manually
|
|
4571
|
+
-- since EndTest can only be called from the test's Server DataModel.
|
|
4572
|
+
cleanupTestCompanions()
|
|
4391
4573
|
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4574
|
+
local success, result = pcall(function()
|
|
4575
|
+
local companion = injectTestCompanion(pluginState.serverUrl, sessionId)
|
|
4576
|
+
|
|
4577
|
+
-- ExecutePlayModeAsync yields until the test ends. Run it on its
|
|
4578
|
+
-- own thread so the HTTP response for play_solo returns now,
|
|
4579
|
+
-- rather than after the test concludes. The plugin's poll loop
|
|
4580
|
+
-- continues to service other requests during the test.
|
|
4581
|
+
task.spawn(function()
|
|
4582
|
+
local execOk, execResult = pcall(function()
|
|
4583
|
+
return sts:ExecutePlayModeAsync(sessionId)
|
|
4395
4584
|
end)
|
|
4396
4585
|
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
success = true,
|
|
4401
|
-
wasRunning = true,
|
|
4402
|
-
method = "StudioTestService:EndTest",
|
|
4403
|
-
message = "Stopped play test (state restored)"
|
|
4404
|
-
}
|
|
4405
|
-
end
|
|
4406
|
-
end
|
|
4586
|
+
-- Test has ended. Always clean up the companion in the Edit
|
|
4587
|
+
-- DataModel (the one in the test DM died with the test).
|
|
4588
|
+
cleanupTestCompanions()
|
|
4407
4589
|
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4590
|
+
-- Tell the bridge the session is over. The companion may have
|
|
4591
|
+
-- already done this if stop was MCP-driven; the bridge's
|
|
4592
|
+
-- endTestSession is idempotent so a duplicate post is fine.
|
|
4593
|
+
pcall(function()
|
|
4594
|
+
HttpService:RequestAsync({
|
|
4595
|
+
Url = pluginState.serverUrl .. "/test-session/ended",
|
|
4596
|
+
Method = "POST",
|
|
4597
|
+
Headers = { ["Content-Type"] = "application/json" },
|
|
4598
|
+
Body = HttpService:JSONEncode({
|
|
4599
|
+
sessionId = sessionId,
|
|
4600
|
+
reason = execOk and "natural" or "exec_error",
|
|
4601
|
+
value = execOk and execResult or nil,
|
|
4602
|
+
error = (not execOk) and tostring(execResult) or nil,
|
|
4603
|
+
}),
|
|
4604
|
+
})
|
|
4605
|
+
end)
|
|
4606
|
+
end)
|
|
4411
4607
|
|
|
4412
4608
|
return {
|
|
4413
4609
|
success = true,
|
|
4414
|
-
|
|
4415
|
-
method = "
|
|
4416
|
-
|
|
4417
|
-
|
|
4610
|
+
sessionId = sessionId,
|
|
4611
|
+
method = "StudioTestService:ExecutePlayModeAsync",
|
|
4612
|
+
companionPath = getInstancePath(companion),
|
|
4613
|
+
message = "Started Play Solo with MCP companion. Use stop_play to end, get_playtest_output to read logs.",
|
|
4418
4614
|
}
|
|
4419
4615
|
end)
|
|
4420
4616
|
|
|
4421
4617
|
if success then
|
|
4422
4618
|
return result
|
|
4423
4619
|
else
|
|
4620
|
+
-- If injection or task spawn fails, make sure we don't leave a
|
|
4621
|
+
-- companion script behind.
|
|
4622
|
+
pcall(cleanupTestCompanions)
|
|
4424
4623
|
return {
|
|
4425
4624
|
success = false,
|
|
4426
|
-
|
|
4625
|
+
sessionId = sessionId,
|
|
4626
|
+
error = "Failed to start play test: " .. tostring(result),
|
|
4427
4627
|
}
|
|
4428
4628
|
end
|
|
4429
4629
|
end
|
|
4430
4630
|
|
|
4631
|
+
handlers.stopPlay = function(requestData)
|
|
4632
|
+
-- Deprecated path. The MCP `stop_play` tool now talks to the bridge
|
|
4633
|
+
-- directly (which queues a command for the in-test companion to pick
|
|
4634
|
+
-- up). This handler exists only so older bridge versions or direct
|
|
4635
|
+
-- HTTP callers don't crash — it never actually stops a test, since
|
|
4636
|
+
-- EndTest cannot be called from the Edit DataModel.
|
|
4637
|
+
return {
|
|
4638
|
+
success = false,
|
|
4639
|
+
deprecated = true,
|
|
4640
|
+
error = "/api/stop-play is deprecated. The MCP stop_play tool now signals the in-test companion via the bridge — update your MCP server to a version that includes the test-session protocol.",
|
|
4641
|
+
}
|
|
4642
|
+
end
|
|
4643
|
+
|
|
4431
4644
|
-- ============================================
|
|
4432
4645
|
-- SCREENSHOT HANDLER
|
|
4433
4646
|
-- ============================================
|
|
@@ -5199,6 +5412,10 @@ local function activatePlugin()
|
|
|
5199
5412
|
screenGui.Enabled = true
|
|
5200
5413
|
updateUIState()
|
|
5201
5414
|
|
|
5415
|
+
-- Sweep any orphan test companions left over from a previous Studio
|
|
5416
|
+
-- session that crashed/was force-closed mid-test. Safe no-op otherwise.
|
|
5417
|
+
pcall(cleanupTestCompanions)
|
|
5418
|
+
|
|
5202
5419
|
pcall(function()
|
|
5203
5420
|
HttpService:RequestAsync({
|
|
5204
5421
|
Url = pluginState.serverUrl .. "/ready",
|
|
@@ -5247,7 +5464,13 @@ local function deactivatePlugin()
|
|
|
5247
5464
|
pluginState.connection:Disconnect()
|
|
5248
5465
|
pluginState.connection = nil
|
|
5249
5466
|
end
|
|
5250
|
-
|
|
5467
|
+
|
|
5468
|
+
-- Don't leave a companion script in the place when the user disables
|
|
5469
|
+
-- the plugin. (If a test is currently running it'll be in the test's
|
|
5470
|
+
-- Server DataModel — that copy dies with the test. We're cleaning the
|
|
5471
|
+
-- Edit-side template so it's not there next time the user saves.)
|
|
5472
|
+
pcall(cleanupTestCompanions)
|
|
5473
|
+
|
|
5251
5474
|
pluginState.consecutiveFailures = 0
|
|
5252
5475
|
pluginState.currentRetryDelay = 0.5
|
|
5253
5476
|
end
|