@chrrxs/robloxstudio-mcp 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 +106 -3
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +451 -125
- package/studio-plugin/MCPPlugin.rbxmx +451 -125
- package/studio-plugin/src/modules/ClientBroker.ts +125 -48
- package/studio-plugin/src/modules/Communication.ts +59 -26
- 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 +12 -1
- 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),
|
|
@@ -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,
|
|
@@ -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",
|
|
3
|
-
"version": "2.
|
|
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",
|