@chrrxs/robloxstudio-mcp-inspector 2.19.0 → 2.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp-inspector",
3
- "version": "2.19.0",
3
+ "version": "2.19.1",
4
4
  "description": "Read-only MCP server for inspecting and debugging Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -33,6 +33,9 @@ StopPlayMonitor.init(plugin)
33
33
  BreakpointHandlers.init(plugin)
34
34
  ServerUrlSettings.init(plugin)
35
35
  local function applyRememberedServerUrl()
36
+ if ClientBroker.forkRole() == "client" then
37
+ return nil
38
+ end
36
39
  local rememberedServerUrl = ServerUrlSettings.readServerUrl()
37
40
  if rememberedServerUrl == nil then
38
41
  return nil
@@ -1410,9 +1413,9 @@ local function computeBridgeStamp()
1410
1413
  for i = 1, #combined do
1411
1414
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1412
1415
  end
1413
- -- "2.19.0" is replaced with the package version at package time
1416
+ -- "2.19.1" is replaced with the package version at package time
1414
1417
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1415
- return `{tostring(h)}-2.19.0`
1418
+ return `{tostring(h)}-2.19.1`
1416
1419
  end
1417
1420
  local BRIDGE_STAMP = computeBridgeStamp()
1418
1421
  local function setSource(scriptInst, source)
@@ -7419,6 +7422,65 @@ local function getProjectStructure(requestData)
7419
7422
  result.timestamp = tick()
7420
7423
  return result
7421
7424
  end
7425
+ -- Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
7426
+ -- alternation operator, so "foo|bar" would otherwise be matched as the literal
7427
+ -- text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
7428
+ -- keeps both balanced-match delimiter characters.
7429
+ local function splitLuaAlternation(pattern)
7430
+ local parts = {}
7431
+ local current = ""
7432
+ local i = 1
7433
+ local n = #pattern
7434
+ local inCharClass = false
7435
+ while i <= n do
7436
+ local c = string.sub(pattern, i, i)
7437
+ if c == "%" then
7438
+ if string.sub(pattern, i + 1, i + 1) == "b" then
7439
+ current ..= string.sub(pattern, i, math.min(i + 3, n))
7440
+ i += 4
7441
+ continue
7442
+ end
7443
+ -- Preserve an escape pair (e.g. %|, %., %d) intact.
7444
+ current ..= string.sub(pattern, i, i + 1)
7445
+ i += 2
7446
+ elseif c == "[" then
7447
+ inCharClass = true
7448
+ current ..= c
7449
+ i += 1
7450
+ elseif c == "]" then
7451
+ inCharClass = false
7452
+ current ..= c
7453
+ i += 1
7454
+ elseif c == "|" and not inCharClass then
7455
+ local _current = current
7456
+ table.insert(parts, _current)
7457
+ current = ""
7458
+ i += 1
7459
+ else
7460
+ current ..= c
7461
+ i += 1
7462
+ end
7463
+ end
7464
+ local _current = current
7465
+ table.insert(parts, _current)
7466
+ return parts
7467
+ end
7468
+ -- Return the earliest match across alternatives (mirrors regex alternation).
7469
+ local function findFirstPattern(line, alternatives)
7470
+ local bestStart
7471
+ local bestEnd
7472
+ for _, alt in alternatives do
7473
+ if alt == "" then
7474
+ continue
7475
+ end
7476
+ local s, e = string.find(line, alt)
7477
+ if s ~= nil and (bestStart == nil or s < bestStart) then
7478
+ bestStart = s
7479
+ bestEnd = e
7480
+ end
7481
+ end
7482
+ return { bestStart, bestEnd }
7483
+ end
7422
7484
  local function grepScripts(requestData)
7423
7485
  local pattern = requestData.pattern
7424
7486
  if not (pattern ~= "" and pattern) then
@@ -7426,11 +7488,27 @@ local function grepScripts(requestData)
7426
7488
  error = "pattern is required",
7427
7489
  }
7428
7490
  end
7429
- local _condition = (requestData.caseSensitive)
7491
+ local _condition = (requestData.usePattern)
7430
7492
  if _condition == nil then
7431
7493
  _condition = false
7432
7494
  end
7433
- local caseSensitive = _condition
7495
+ local usePattern = _condition
7496
+ if usePattern and requestData.caseSensitive == false then
7497
+ return {
7498
+ error = "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
7499
+ }
7500
+ end
7501
+ local _result
7502
+ if usePattern then
7503
+ _result = true
7504
+ else
7505
+ local _condition_1 = (requestData.caseSensitive)
7506
+ if _condition_1 == nil then
7507
+ _condition_1 = false
7508
+ end
7509
+ _result = _condition_1
7510
+ end
7511
+ local caseSensitive = _result
7434
7512
  local _condition_1 = (requestData.contextLines)
7435
7513
  if _condition_1 == nil then
7436
7514
  _condition_1 = 0
@@ -7446,21 +7524,16 @@ local function grepScripts(requestData)
7446
7524
  _condition_3 = 0
7447
7525
  end
7448
7526
  local maxResultsPerScript = _condition_3
7449
- local _condition_4 = (requestData.usePattern)
7527
+ local _condition_4 = (requestData.filesOnly)
7450
7528
  if _condition_4 == nil then
7451
7529
  _condition_4 = false
7452
7530
  end
7453
- local usePattern = _condition_4
7454
- local _condition_5 = (requestData.filesOnly)
7531
+ local filesOnly = _condition_4
7532
+ local _condition_5 = (requestData.path)
7455
7533
  if _condition_5 == nil then
7456
- _condition_5 = false
7534
+ _condition_5 = ""
7457
7535
  end
7458
- local filesOnly = _condition_5
7459
- local _condition_6 = (requestData.path)
7460
- if _condition_6 == nil then
7461
- _condition_6 = ""
7462
- end
7463
- local searchPath = _condition_6
7536
+ local searchPath = _condition_5
7464
7537
  local classFilter = requestData.classFilter
7465
7538
  local startInstance = if searchPath ~= "" then getInstanceByPath(searchPath) else game
7466
7539
  if not startInstance then
@@ -7470,6 +7543,8 @@ local function grepScripts(requestData)
7470
7543
  end
7471
7544
  -- Prepare pattern for matching
7472
7545
  local searchPattern = if caseSensitive then pattern else string.lower(pattern)
7546
+ -- Pre-split top-level "|" alternation once (pattern mode only).
7547
+ local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
7473
7548
  local results = {}
7474
7549
  local totalMatches = 0
7475
7550
  local scriptsSearched = 0
@@ -7505,7 +7580,9 @@ local function grepScripts(requestData)
7505
7580
  local matchStart
7506
7581
  local matchEnd
7507
7582
  if usePattern then
7508
- matchStart, matchEnd = string.find(searchLine, searchPattern)
7583
+ local _binding_1 = findFirstPattern(searchLine, patternAlternatives)
7584
+ matchStart = _binding_1[1]
7585
+ matchEnd = _binding_1[2]
7509
7586
  else
7510
7587
  matchStart, matchEnd = string.find(searchLine, searchPattern, 1, true)
7511
7588
  end
@@ -7997,6 +8074,9 @@ local joinLines = _binding.joinLines
7997
8074
  local _binding_1 = Recording
7998
8075
  local beginRecording = _binding_1.beginRecording
7999
8076
  local finishRecording = _binding_1.finishRecording
8077
+ local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
8078
+ local SOURCE_TRUNCATE_LINE_BUDGET = 400
8079
+ local SOURCE_TRUNCATE_TO_LINES = 300
8000
8080
  local function normalizeEscapes(s)
8001
8081
  local result = s
8002
8082
  result = (string.gsub(result, "\\\\", "\x01"))
@@ -8007,6 +8087,44 @@ local function normalizeEscapes(s)
8007
8087
  result = (string.gsub(result, "\x01", "\\"))
8008
8088
  return result
8009
8089
  end
8090
+ local function getTopServiceName(instance)
8091
+ local topServiceInst = instance
8092
+ while topServiceInst.Parent and topServiceInst.Parent ~= game do
8093
+ topServiceInst = topServiceInst.Parent
8094
+ end
8095
+ return topServiceInst.Name
8096
+ end
8097
+ local function sliceLines(lines, startLine, endLine)
8098
+ local selectedLines = {}
8099
+ do
8100
+ local i = startLine
8101
+ local _shouldIncrement = false
8102
+ while true do
8103
+ if _shouldIncrement then
8104
+ i += 1
8105
+ else
8106
+ _shouldIncrement = true
8107
+ end
8108
+ if not (i <= endLine) then
8109
+ break
8110
+ end
8111
+ local _condition = lines[i]
8112
+ if _condition == nil then
8113
+ _condition = ""
8114
+ end
8115
+ table.insert(selectedLines, _condition)
8116
+ end
8117
+ end
8118
+ return selectedLines
8119
+ end
8120
+ local function numberLines(lines, lineOffset)
8121
+ local numberedLines = {}
8122
+ for i = 0, #lines - 1 do
8123
+ local _arg0 = `{i + lineOffset}: {lines[i + 1]}`
8124
+ table.insert(numberedLines, _arg0)
8125
+ end
8126
+ return table.concat(numberedLines, "\n")
8127
+ end
8010
8128
  local function getScriptSource(requestData)
8011
8129
  local instancePath = requestData.instancePath
8012
8130
  local startLine = requestData.startLine
@@ -8031,105 +8149,58 @@ local function getScriptSource(requestData)
8031
8149
  local fullSource = readScriptSource(instance)
8032
8150
  local lines, hasTrailingNewline = splitLines(fullSource)
8033
8151
  local totalLineCount = #lines
8034
- local sourceToReturn = fullSource
8035
- local returnedStartLine = 1
8036
- local returnedEndLine = totalLineCount
8037
- if startLine ~= nil or endLine ~= nil then
8152
+ local explicitRange = startLine ~= nil or endLine ~= nil
8153
+ local shouldTruncate = not explicitRange and (#fullSource > SOURCE_TRUNCATE_CHAR_BUDGET or totalLineCount > SOURCE_TRUNCATE_LINE_BUDGET)
8154
+ local _result
8155
+ if explicitRange then
8038
8156
  local _condition = startLine
8039
8157
  if _condition == nil then
8040
8158
  _condition = 1
8041
8159
  end
8042
- local actualStartLine = math.max(1, _condition)
8043
- local _exp = #lines
8044
- local _condition_1 = endLine
8045
- if _condition_1 == nil then
8046
- _condition_1 = #lines
8047
- end
8048
- local actualEndLine = math.min(_exp, _condition_1)
8049
- local selectedLines = {}
8050
- do
8051
- local i = actualStartLine
8052
- local _shouldIncrement = false
8053
- while true do
8054
- if _shouldIncrement then
8055
- i += 1
8056
- else
8057
- _shouldIncrement = true
8058
- end
8059
- if not (i <= actualEndLine) then
8060
- break
8061
- end
8062
- local _condition_2 = lines[i]
8063
- if _condition_2 == nil then
8064
- _condition_2 = ""
8065
- end
8066
- table.insert(selectedLines, _condition_2)
8160
+ _result = math.max(1, _condition)
8161
+ else
8162
+ _result = 1
8163
+ end
8164
+ local returnedStartLine = _result
8165
+ local _result_1
8166
+ if shouldTruncate then
8167
+ _result_1 = math.min(SOURCE_TRUNCATE_TO_LINES, totalLineCount)
8168
+ else
8169
+ local _result_2
8170
+ if explicitRange then
8171
+ local _condition = endLine
8172
+ if _condition == nil then
8173
+ _condition = totalLineCount
8067
8174
  end
8175
+ _result_2 = math.min(totalLineCount, _condition)
8176
+ else
8177
+ _result_2 = totalLineCount
8068
8178
  end
8069
- sourceToReturn = table.concat(selectedLines, "\n")
8070
- if hasTrailingNewline and actualEndLine == #lines and string.sub(sourceToReturn, -1) ~= "\n" then
8071
- sourceToReturn ..= "\n"
8072
- end
8073
- returnedStartLine = actualStartLine
8074
- returnedEndLine = actualEndLine
8075
- end
8076
- local numberedLines = {}
8077
- local linesToNumber = if startLine ~= nil then (splitLines(sourceToReturn)) else lines
8078
- local lineOffset = returnedStartLine - 1
8079
- for i = 0, #linesToNumber - 1 do
8080
- local _arg0 = `{i + 1 + lineOffset}: {linesToNumber[i + 1]}`
8081
- table.insert(numberedLines, _arg0)
8179
+ _result_1 = _result_2
8082
8180
  end
8083
- local numberedSource = table.concat(numberedLines, "\n")
8181
+ local returnedEndLine = _result_1
8182
+ local selectedLines = if (explicitRange or shouldTruncate) then sliceLines(lines, returnedStartLine, returnedEndLine) else lines
8183
+ local sourceToReturn = if explicitRange then joinLines(selectedLines, hasTrailingNewline and returnedEndLine == totalLineCount) elseif shouldTruncate then table.concat(selectedLines, "\n") else fullSource
8084
8184
  local resp = {
8085
8185
  instancePath = instancePath,
8086
8186
  className = instance.ClassName,
8087
8187
  name = instance.Name,
8088
8188
  source = sourceToReturn,
8089
- numberedSource = numberedSource,
8189
+ numberedSource = numberLines(selectedLines, returnedStartLine),
8090
8190
  sourceLength = #fullSource,
8091
8191
  lineCount = totalLineCount,
8092
8192
  startLine = returnedStartLine,
8093
8193
  endLine = returnedEndLine,
8094
- isPartial = startLine ~= nil or endLine ~= nil,
8095
- truncated = false,
8194
+ isPartial = explicitRange,
8195
+ truncated = shouldTruncate,
8096
8196
  }
8097
- if startLine == nil and endLine == nil and #fullSource > 50000 then
8098
- local truncatedLines = {}
8099
- local truncatedNumberedLines = {}
8100
- local maxLines = math.min(1000, #lines)
8101
- do
8102
- local i = 0
8103
- local _shouldIncrement = false
8104
- while true do
8105
- if _shouldIncrement then
8106
- i += 1
8107
- else
8108
- _shouldIncrement = true
8109
- end
8110
- if not (i < maxLines) then
8111
- break
8112
- end
8113
- local _arg0 = lines[i + 1]
8114
- table.insert(truncatedLines, _arg0)
8115
- local _arg0_1 = `{i + 1}: {lines[i + 1]}`
8116
- table.insert(truncatedNumberedLines, _arg0_1)
8117
- end
8118
- end
8119
- resp.source = table.concat(truncatedLines, "\n")
8120
- resp.numberedSource = table.concat(truncatedNumberedLines, "\n")
8121
- resp.truncated = true
8122
- resp.endLine = maxLines
8123
- resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
8197
+ if shouldTruncate then
8198
+ resp.note = `Script truncated to first {returnedEndLine} of {totalLineCount} lines ({#fullSource} chars). Use line_range to read specific sections.`
8124
8199
  end
8125
8200
  if instance:IsA("BaseScript") then
8126
8201
  resp.enabled = instance.Enabled
8127
8202
  end
8128
- local topServiceInst = instance
8129
- while topServiceInst.Parent and topServiceInst.Parent ~= game do
8130
- topServiceInst = topServiceInst.Parent
8131
- end
8132
- resp.topService = topServiceInst.Name
8203
+ resp.topService = getTopServiceName(instance)
8133
8204
  return resp
8134
8205
  end)
8135
8206
  if success then
@@ -9673,25 +9744,13 @@ local function multiplayerTestLeaveClient(_requestData)
9673
9744
  localPlayer = localPlayer,
9674
9745
  }
9675
9746
  end
9676
- local function multiplayerTestEnd(requestData)
9677
- if not RunService:IsRunning() or not RunService:IsServer() then
9678
- return {
9679
- error = "multiplayer_test_end must be called on the running server peer. Route with target=server.",
9680
- }
9681
- end
9682
- local value = if requestData.value ~= nil then requestData.value else "ended_by_mcp"
9683
- local ok, result = pcall(function()
9684
- return StudioTestService:EndTest(value)
9685
- end)
9686
- if not ok then
9687
- return {
9688
- error = tostring(result),
9689
- }
9690
- end
9747
+ local function multiplayerTestEnd(_requestData)
9691
9748
  return {
9692
- success = true,
9693
- message = "Multiplayer Studio test end requested.",
9694
- value = value,
9749
+ success = false,
9750
+ error = "multiplayer_stop_disabled",
9751
+ message = "Multiplayer playtest stop/end is disabled because StudioTestService:EndTest is currently broken for this flow. Manually close the Studio multiplayer test windows instead.",
9752
+ reason = "StudioTestService:EndTest does not reliably end StudioTestService multiplayer sessions from MCP right now.",
9753
+ manualCleanupRequired = true,
9695
9754
  }
9696
9755
  end
9697
9756
  return {
@@ -10580,7 +10639,7 @@ local function addUnique(values, value)
10580
10639
  table.insert(_values_1, _value_1)
10581
10640
  end
10582
10641
  end
10583
- local function computeInstanceIds()
10642
+ local function computeInstanceIds(options)
10584
10643
  local ids = {}
10585
10644
  if game.PlaceId ~= 0 then
10586
10645
  addUnique(ids, `place:{tostring(game.PlaceId)}`)
@@ -10588,12 +10647,22 @@ local function computeInstanceIds()
10588
10647
  local existing = ServerStorage:GetAttribute("__MCPPlaceId")
10589
10648
  if type(existing) == "string" and existing ~= "" then
10590
10649
  addUnique(ids, `anon:{existing}`)
10591
- elseif game.PlaceId == 0 then
10592
- local fresh = HttpService:GenerateGUID(false)
10593
- pcall(function()
10594
- return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
10595
- end)
10596
- addUnique(ids, `anon:{fresh}`)
10650
+ else
10651
+ local _condition = game.PlaceId == 0
10652
+ if _condition then
10653
+ local _result = options
10654
+ if _result ~= nil then
10655
+ _result = _result.createAnonymous
10656
+ end
10657
+ _condition = _result == true
10658
+ end
10659
+ if _condition then
10660
+ local fresh = HttpService:GenerateGUID(false)
10661
+ pcall(function()
10662
+ return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
10663
+ end)
10664
+ addUnique(ids, `anon:{fresh}`)
10665
+ end
10597
10666
  end
10598
10667
  return ids
10599
10668
  end
@@ -10630,7 +10699,9 @@ local function rememberServerUrl(serverUrl)
10630
10699
  return nil
10631
10700
  end
10632
10701
  writeSettingString(GLOBAL_SETTING_KEY, normalized)
10633
- for _, instanceId in computeInstanceIds() do
10702
+ for _, instanceId in computeInstanceIds({
10703
+ createAnonymous = true,
10704
+ }) do
10634
10705
  writeSettingString(settingKey(instanceId), normalized)
10635
10706
  writeSettingString(legacySettingKey(instanceId), normalized)
10636
10707
  end
@@ -10639,6 +10710,9 @@ local function readServerUrl()
10639
10710
  if not pluginRef then
10640
10711
  return nil
10641
10712
  end
10713
+ -- Reading settings should not mint a place identity. Client play DMs have
10714
+ -- their own ServerStorage; creating an id there makes a misleading anon id
10715
+ -- that never matches the edit/server bridge identity.
10642
10716
  for _, instanceId in computeInstanceIds() do
10643
10717
  local remembered = readSettingString(settingKey(instanceId))
10644
10718
  if remembered ~= nil then
@@ -10671,7 +10745,7 @@ return {
10671
10745
  <Properties>
10672
10746
  <string name="Name">State</string>
10673
10747
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
10674
- local CURRENT_VERSION = "2.19.0"
10748
+ local CURRENT_VERSION = "2.19.1"
10675
10749
  local PLUGIN_VARIANT = "inspector"
10676
10750
  local BASE_PORT = 58741
10677
10751
  local function createConnection(port)
@@ -12028,6 +12102,38 @@ local function convertPropertyValue(instance, propertyName, propertyValue)
12028
12102
  end
12029
12103
  return UDim2.new(_condition_1, _condition_2, _condition_3, _condition_4)
12030
12104
  end
12105
+ local success, currentVal = pcall(function()
12106
+ return inst[propertyName]
12107
+ end)
12108
+ if success then
12109
+ local currentType = typeof(currentVal)
12110
+ if currentType == "Vector2" then
12111
+ local _condition_1 = (tbl.X)
12112
+ if _condition_1 == nil then
12113
+ _condition_1 = 0
12114
+ end
12115
+ local _condition_2 = (tbl.Y)
12116
+ if _condition_2 == nil then
12117
+ _condition_2 = 0
12118
+ end
12119
+ return Vector2.new(_condition_1, _condition_2)
12120
+ end
12121
+ if currentType == "Vector3" then
12122
+ local _condition_1 = (tbl.X)
12123
+ if _condition_1 == nil then
12124
+ _condition_1 = 0
12125
+ end
12126
+ local _condition_2 = (tbl.Y)
12127
+ if _condition_2 == nil then
12128
+ _condition_2 = 0
12129
+ end
12130
+ local _condition_3 = (tbl.Z)
12131
+ if _condition_3 == nil then
12132
+ _condition_3 = 0
12133
+ end
12134
+ return Vector3.new(_condition_1, _condition_2, _condition_3)
12135
+ end
12136
+ end
12031
12137
  local _condition_1 = (tbl.X)
12032
12138
  if _condition_1 == nil then
12033
12139
  _condition_1 = 0