@chrrxs/robloxstudio-mcp-inspector 2.9.1 → 2.10.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/dist/index.js CHANGED
@@ -109,10 +109,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
109
109
  bridge.updateInstanceActivity(instanceId);
110
110
  }
111
111
  let callerRole = "edit";
112
+ let knownInstance = false;
112
113
  if (instanceId) {
113
114
  const inst = bridge.getInstances().find((i) => i.instanceId === instanceId);
114
115
  if (inst) {
115
116
  callerRole = inst.role;
117
+ knownInstance = true;
116
118
  }
117
119
  }
118
120
  if (!isMCPServerActive()) {
@@ -120,6 +122,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
120
122
  error: "MCP server not connected",
121
123
  pluginConnected: true,
122
124
  mcpConnected: false,
125
+ knownInstance,
123
126
  request: null
124
127
  });
125
128
  return;
@@ -131,6 +134,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
131
134
  requestId: pendingRequest.requestId,
132
135
  mcpConnected: true,
133
136
  pluginConnected: true,
137
+ knownInstance,
134
138
  proxyInstanceCount: proxyInstances.size
135
139
  });
136
140
  } else {
@@ -138,6 +142,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
138
142
  request: null,
139
143
  mcpConnected: true,
140
144
  pluginConnected: true,
145
+ knownInstance,
141
146
  proxyInstanceCount: proxyInstances.size
142
147
  });
143
148
  }
@@ -340,6 +345,7 @@ var init_http_server = __esm({
340
345
  start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers),
341
346
  stop_playtest: (tools) => tools.stopPlaytest(),
342
347
  get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target),
348
+ get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.target, body.since, body.tail, body.filter),
343
349
  get_connected_instances: (tools) => tools.getConnectedInstances(),
344
350
  export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style),
345
351
  create_build: (tools, body) => tools.createBuild(body.id, body.style, body.palette, body.parts, body.bounds),
@@ -1206,6 +1212,37 @@ function luaLongQuote(s) {
1206
1212
  ${s}
1207
1213
  ]${eq}]`;
1208
1214
  }
1215
+ function buildModuleScriptInvokeWrapper(opts) {
1216
+ const wrapped = `return ((function()
1217
+ ${opts.userCode}
1218
+ end)())`;
1219
+ return `
1220
+ local HttpService = game:GetService("HttpService")
1221
+ local bf = game:GetService("${opts.service}"):FindFirstChild("${opts.bridgeName}")
1222
+ if not bf then
1223
+ return HttpService:JSONEncode({
1224
+ bridge = "missing",
1225
+ error = ${luaLongQuote(opts.missingError)},
1226
+ })
1227
+ end
1228
+ local USER_CODE = ${luaLongQuote(wrapped)}
1229
+ local m = Instance.new("ModuleScript")
1230
+ m.Name = "__MCPEvalPayload"
1231
+ local okSet, setErr = pcall(function() m.Source = USER_CODE end)
1232
+ if not okSet then
1233
+ m:Destroy()
1234
+ return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
1235
+ end
1236
+ m.Parent = workspace
1237
+ local ok, result = bf:Invoke(m)
1238
+ m:Destroy()
1239
+ return HttpService:JSONEncode({
1240
+ bridge = "ok",
1241
+ ok = ok,
1242
+ result = if result == nil then nil else tostring(result),
1243
+ })
1244
+ `;
1245
+ }
1209
1246
  function parseBridgeResponse(response) {
1210
1247
  const r = response;
1211
1248
  if (r && typeof r.returnValue === "string") {
@@ -1765,23 +1802,12 @@ ${code}`
1765
1802
  if (!code) {
1766
1803
  throw new Error("Code is required for eval_server_runtime");
1767
1804
  }
1768
- const wrapper = `
1769
- local HttpService = game:GetService("HttpService")
1770
- local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
1771
- if not bf then
1772
- return HttpService:JSONEncode({
1773
- bridge = "missing",
1774
- error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
1775
- })
1776
- end
1777
- local USER_CODE = ${luaLongQuote(code)}
1778
- local ok, result = bf:Invoke(USER_CODE)
1779
- return HttpService:JSONEncode({
1780
- bridge = "ok",
1781
- ok = ok,
1782
- result = if result == nil then nil else tostring(result),
1783
- })
1784
- `;
1805
+ const wrapper = buildModuleScriptInvokeWrapper({
1806
+ service: "ServerScriptService",
1807
+ bridgeName: SERVER_LOCAL_NAME,
1808
+ missingError: "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
1809
+ userCode: code
1810
+ });
1785
1811
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
1786
1812
  return {
1787
1813
  content: [
@@ -1800,32 +1826,12 @@ return HttpService:JSONEncode({
1800
1826
  if (!clientTarget.startsWith("client-")) {
1801
1827
  throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
1802
1828
  }
1803
- const wrapper = `
1804
- local HttpService = game:GetService("HttpService")
1805
- local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
1806
- if not bf then
1807
- return HttpService:JSONEncode({
1808
- bridge = "missing",
1809
- error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
1810
- })
1811
- end
1812
- local USER_CODE = ${luaLongQuote(code)}
1813
- local m = Instance.new("ModuleScript")
1814
- m.Name = "__MCPEvalPayload"
1815
- local okSet, setErr = pcall(function() m.Source = USER_CODE end)
1816
- if not okSet then
1817
- m:Destroy()
1818
- return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
1819
- end
1820
- m.Parent = workspace
1821
- local ok, result = bf:Invoke(m)
1822
- m:Destroy()
1823
- return HttpService:JSONEncode({
1824
- bridge = "ok",
1825
- ok = ok,
1826
- result = if result == nil then nil else tostring(result),
1827
- })
1828
- `;
1829
+ const wrapper = buildModuleScriptInvokeWrapper({
1830
+ service: "ReplicatedStorage",
1831
+ bridgeName: CLIENT_LOCAL_NAME,
1832
+ missingError: "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
1833
+ userCode: code
1834
+ });
1829
1835
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
1830
1836
  return {
1831
1837
  content: [
@@ -1836,6 +1842,70 @@ return HttpService:JSONEncode({
1836
1842
  ]
1837
1843
  };
1838
1844
  }
1845
+ async getRuntimeLogs(target, since, tail, filter) {
1846
+ const tgt = target ?? "all";
1847
+ const data = {};
1848
+ if (since !== void 0)
1849
+ data.since = since;
1850
+ if (tail !== void 0)
1851
+ data.tail = tail;
1852
+ if (filter !== void 0)
1853
+ data.filter = filter;
1854
+ if (tgt !== "all") {
1855
+ const response = await this.client.request("/api/get-runtime-logs", data, tgt);
1856
+ return {
1857
+ content: [{ type: "text", text: JSON.stringify(response) }]
1858
+ };
1859
+ }
1860
+ const targets = this.bridge.getInstances().filter((i) => i.role !== "edit-proxy").map((i) => i.role);
1861
+ const responses = await Promise.allSettled(targets.map(async (t) => {
1862
+ const r = await this.client.request("/api/get-runtime-logs", data, t);
1863
+ return { ...r, peer: t };
1864
+ }));
1865
+ const merged = [];
1866
+ const perPeerNextSince = {};
1867
+ const perPeerErrors = {};
1868
+ let totalDropped = 0;
1869
+ for (const r of responses) {
1870
+ if (r.status !== "fulfilled")
1871
+ continue;
1872
+ const v = r.value;
1873
+ const peer = v.peer ?? "unknown";
1874
+ if (v.error) {
1875
+ perPeerErrors[peer] = v.error;
1876
+ continue;
1877
+ }
1878
+ if (v.nextSince !== void 0)
1879
+ perPeerNextSince[peer] = v.nextSince;
1880
+ totalDropped += v.totalDropped ?? 0;
1881
+ for (const e of v.entries ?? []) {
1882
+ merged.push({ ...e, peer });
1883
+ }
1884
+ }
1885
+ merged.sort((a, b) => a.ts !== b.ts ? a.ts - b.ts : a.seq - b.seq);
1886
+ const DEDUP_WINDOW = 2;
1887
+ const deduped = [];
1888
+ for (const e of merged) {
1889
+ const isDup = deduped.some((d) => d.message === e.message && d.level === e.level && Math.abs(d.ts - e.ts) <= DEDUP_WINDOW && d.peer !== e.peer);
1890
+ if (!isDup)
1891
+ deduped.push(e);
1892
+ }
1893
+ let final = deduped;
1894
+ if (tail !== void 0 && deduped.length > tail) {
1895
+ final = deduped.slice(deduped.length - tail);
1896
+ }
1897
+ const body = {
1898
+ entries: final,
1899
+ totalDropped,
1900
+ perPeerNextSince
1901
+ };
1902
+ if (Object.keys(perPeerErrors).length > 0) {
1903
+ body.perPeerErrors = perPeerErrors;
1904
+ }
1905
+ return {
1906
+ content: [{ type: "text", text: JSON.stringify(body) }]
1907
+ };
1908
+ }
1839
1909
  async startPlaytest(mode, numPlayers) {
1840
1910
  if (mode !== "play" && mode !== "run") {
1841
1911
  throw new Error('mode must be "play" or "run"');
@@ -2740,13 +2810,20 @@ var init_bridge_service = __esm({
2740
2810
  BridgeService = class {
2741
2811
  pendingRequests = /* @__PURE__ */ new Map();
2742
2812
  instances = /* @__PURE__ */ new Map();
2743
- nextClientIndex = 1;
2744
2813
  requestTimeout = 3e4;
2745
2814
  registerInstance(instanceId, role) {
2746
2815
  let assignedRole = role;
2747
2816
  if (role === "client") {
2748
- assignedRole = `client-${this.nextClientIndex}`;
2749
- this.nextClientIndex++;
2817
+ const used = /* @__PURE__ */ new Set();
2818
+ for (const inst of this.instances.values()) {
2819
+ const match = inst.role.match(/^client-(\d+)$/);
2820
+ if (match)
2821
+ used.add(Number(match[1]));
2822
+ }
2823
+ let idx = 1;
2824
+ while (used.has(idx))
2825
+ idx++;
2826
+ assignedRole = `client-${idx}`;
2750
2827
  }
2751
2828
  this.instances.set(instanceId, {
2752
2829
  instanceId,
@@ -3832,7 +3909,7 @@ var init_definitions = __esm({
3832
3909
  {
3833
3910
  name: "eval_server_runtime",
3834
3911
  category: "write",
3835
- description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Requires ServerScriptService.LoadStringEnabled=true.",
3912
+ description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
3836
3913
  inputSchema: {
3837
3914
  type: "object",
3838
3915
  properties: {
@@ -3847,7 +3924,7 @@ var init_definitions = __esm({
3847
3924
  {
3848
3925
  name: "eval_client_runtime",
3849
3926
  category: "write",
3850
- description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Does not require LoadStringEnabled.",
3927
+ description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
3851
3928
  inputSchema: {
3852
3929
  type: "object",
3853
3930
  properties: {
@@ -3956,6 +4033,32 @@ var init_definitions = __esm({
3956
4033
  }
3957
4034
  }
3958
4035
  },
4036
+ {
4037
+ name: "get_runtime_logs",
4038
+ category: "read",
4039
+ description: "Read the in-memory log buffer captured by the plugin on each peer's LogService.MessageOut. Each peer (edit, server, client-N) captures ~64 KB of recent prints; oldest entries drop when over budget. Drop-oldest semantics preserve the recent tail, unlike get_console_output's 10 KB drop-newest cap. Caveat: peer tag reflects which peer's plugin captured the entry, not which peer's script originated it - LogService reflects prints across peers in Studio Play and origin is undetectable from inside MessageOut. target=all (default) merges all peers and dedups same-message-and-level entries captured within 2s across different peers.",
4040
+ inputSchema: {
4041
+ type: "object",
4042
+ properties: {
4043
+ target: {
4044
+ type: "string",
4045
+ description: 'Peer to read from: "edit", "server", "client-N", or "all" (default). "all" merges all peers and dedups cross-peer reflections within a 2s window.'
4046
+ },
4047
+ since: {
4048
+ type: "number",
4049
+ description: "Return only entries with seq > since. Pass back the previous response's nextSince (single-peer) or perPeerNextSince entry (target=all) for incremental polling."
4050
+ },
4051
+ tail: {
4052
+ type: "number",
4053
+ description: "Return only the last N entries after since/filter is applied."
4054
+ },
4055
+ filter: {
4056
+ type: "string",
4057
+ description: "Plain substring matched against each entry's message (no pattern semantics; literal text). Applied after since, before tail."
4058
+ }
4059
+ }
4060
+ }
4061
+ },
3959
4062
  // === Multi-Instance ===
3960
4063
  {
3961
4064
  name: "get_connected_instances",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp-inspector",
3
- "version": "2.9.1",
3
+ "version": "2.10.1",
4
4
  "description": "Read-only MCP Server for Roblox Studio (fork of boshyxd/robloxstudio-mcp-inspector with per-peer execute_luau fixes baked in)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",