@chrrxs/robloxstudio-mcp-inspector 2.9.1 → 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),
@@ -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,
@@ -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.0",
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",