@chrrxs/robloxstudio-mcp-inspector 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 +130 -14
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +508 -127
- package/studio-plugin/MCPPlugin.rbxmx +508 -127
- package/studio-plugin/src/modules/ClientBroker.ts +125 -48
- package/studio-plugin/src/modules/Communication.ts +59 -26
- package/studio-plugin/src/modules/EvalBridges.ts +15 -3
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +138 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +15 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +57 -2
- package/studio-plugin/src/server/index.server.ts +5 -0
- package/studio-plugin/src/types/index.d.ts +4 -0
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:
|
|
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:
|
|
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
|
-
|
|
2736
|
-
this.
|
|
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-inspector",
|
|
3
|
-
"version": "2.
|
|
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",
|