@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 +151 -48
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +526 -190
- package/studio-plugin/MCPPlugin.rbxmx +526 -190
- package/studio-plugin/src/modules/ClientBroker.ts +125 -48
- package/studio-plugin/src/modules/Communication.ts +60 -27
- package/studio-plugin/src/modules/EvalBridges.ts +16 -53
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +138 -0
- package/studio-plugin/src/modules/UI.ts +36 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +15 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +12 -1
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +16 -1
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -13
- package/studio-plugin/src/server/index.server.ts +11 -1
- 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,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
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
2749
|
-
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}`;
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|