@chrrxs/robloxstudio-mcp 2.9.0 → 2.10.0

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,17 @@ function luaLongQuote(s) {
1206
1212
  ${s}
1207
1213
  ]${eq}]`;
1208
1214
  }
1215
+ function parseBridgeResponse(response) {
1216
+ const r = response;
1217
+ if (r && typeof r.returnValue === "string") {
1218
+ try {
1219
+ const parsed = JSON.parse(r.returnValue);
1220
+ return JSON.stringify(parsed);
1221
+ } catch {
1222
+ }
1223
+ }
1224
+ return JSON.stringify(response);
1225
+ }
1209
1226
  var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, RobloxStudioTools;
1210
1227
  var init_tools = __esm({
1211
1228
  "../core/dist/tools/index.js"() {
@@ -1755,27 +1772,28 @@ ${code}`
1755
1772
  throw new Error("Code is required for eval_server_runtime");
1756
1773
  }
1757
1774
  const wrapper = `
1775
+ local HttpService = game:GetService("HttpService")
1758
1776
  local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
1759
1777
  if not bf then
1760
- return {
1778
+ return HttpService:JSONEncode({
1761
1779
  bridge = "missing",
1762
1780
  error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
1763
- }
1781
+ })
1764
1782
  end
1765
1783
  local USER_CODE = ${luaLongQuote(code)}
1766
1784
  local ok, result = bf:Invoke(USER_CODE)
1767
- return {
1785
+ return HttpService:JSONEncode({
1768
1786
  bridge = "ok",
1769
1787
  ok = ok,
1770
1788
  result = if result == nil then nil else tostring(result),
1771
- }
1789
+ })
1772
1790
  `;
1773
1791
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
1774
1792
  return {
1775
1793
  content: [
1776
1794
  {
1777
1795
  type: "text",
1778
- text: JSON.stringify(response)
1796
+ text: parseBridgeResponse(response)
1779
1797
  }
1780
1798
  ]
1781
1799
  };
@@ -1789,12 +1807,13 @@ return {
1789
1807
  throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
1790
1808
  }
1791
1809
  const wrapper = `
1810
+ local HttpService = game:GetService("HttpService")
1792
1811
  local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
1793
1812
  if not bf then
1794
- return {
1813
+ return HttpService:JSONEncode({
1795
1814
  bridge = "missing",
1796
1815
  error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
1797
- }
1816
+ })
1798
1817
  end
1799
1818
  local USER_CODE = ${luaLongQuote(code)}
1800
1819
  local m = Instance.new("ModuleScript")
@@ -1802,27 +1821,91 @@ m.Name = "__MCPEvalPayload"
1802
1821
  local okSet, setErr = pcall(function() m.Source = USER_CODE end)
1803
1822
  if not okSet then
1804
1823
  m:Destroy()
1805
- return { bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) }
1824
+ return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
1806
1825
  end
1807
1826
  m.Parent = workspace
1808
1827
  local ok, result = bf:Invoke(m)
1809
1828
  m:Destroy()
1810
- return {
1829
+ return HttpService:JSONEncode({
1811
1830
  bridge = "ok",
1812
1831
  ok = ok,
1813
1832
  result = if result == nil then nil else tostring(result),
1814
- }
1833
+ })
1815
1834
  `;
1816
1835
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
1817
1836
  return {
1818
1837
  content: [
1819
1838
  {
1820
1839
  type: "text",
1821
- text: JSON.stringify(response)
1840
+ text: parseBridgeResponse(response)
1822
1841
  }
1823
1842
  ]
1824
1843
  };
1825
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
+ }
1826
1909
  async startPlaytest(mode, numPlayers) {
1827
1910
  if (mode !== "play" && mode !== "run") {
1828
1911
  throw new Error('mode must be "play" or "run"');
@@ -2727,13 +2810,20 @@ var init_bridge_service = __esm({
2727
2810
  BridgeService = class {
2728
2811
  pendingRequests = /* @__PURE__ */ new Map();
2729
2812
  instances = /* @__PURE__ */ new Map();
2730
- nextClientIndex = 1;
2731
2813
  requestTimeout = 3e4;
2732
2814
  registerInstance(instanceId, role) {
2733
2815
  let assignedRole = role;
2734
2816
  if (role === "client") {
2735
- assignedRole = `client-${this.nextClientIndex}`;
2736
- 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}`;
2737
2827
  }
2738
2828
  this.instances.set(instanceId, {
2739
2829
  instanceId,
@@ -3943,6 +4033,32 @@ var init_definitions = __esm({
3943
4033
  }
3944
4034
  }
3945
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
+ },
3946
4062
  // === Multi-Instance ===
3947
4063
  {
3948
4064
  name: "get_connected_instances",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "MCP Server for Roblox Studio Integration (fork of boshyxd/robloxstudio-mcp with per-peer execute_luau + stop_playtest fixes)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",