@chrrxs/robloxstudio-mcp-inspector 2.16.1 → 2.16.2
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 +155 -23
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +159 -83
- package/studio-plugin/MCPPlugin.rbxmx +159 -83
- package/studio-plugin/src/modules/Communication.ts +41 -19
- package/studio-plugin/src/modules/ServerUrlSettings.ts +25 -12
- package/studio-plugin/src/modules/StopPlayMonitor.ts +54 -32
package/dist/index.js
CHANGED
|
@@ -27,7 +27,12 @@ function toPublic(inst) {
|
|
|
27
27
|
connectedAt: inst.connectedAt
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
function publishedInstanceId(placeId) {
|
|
31
|
+
if (placeId === void 0 || !Number.isFinite(placeId) || placeId <= 0)
|
|
32
|
+
return void 0;
|
|
33
|
+
return `place:${Math.trunc(placeId)}`;
|
|
34
|
+
}
|
|
35
|
+
var RoutingFailure, STALE_INSTANCE_MS, INSTANCE_ALIAS_TTL_MS, BridgeService;
|
|
31
36
|
var init_bridge_service = __esm({
|
|
32
37
|
"../core/dist/bridge-service.js"() {
|
|
33
38
|
"use strict";
|
|
@@ -40,21 +45,91 @@ var init_bridge_service = __esm({
|
|
|
40
45
|
}
|
|
41
46
|
};
|
|
42
47
|
STALE_INSTANCE_MS = 3e4;
|
|
48
|
+
INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
|
|
43
49
|
BridgeService = class {
|
|
44
50
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
45
51
|
// Keyed by pluginSessionId (the per-plugin GUID).
|
|
46
52
|
instances = /* @__PURE__ */ new Map();
|
|
53
|
+
instanceAliases = /* @__PURE__ */ new Map();
|
|
47
54
|
requestTimeout = 3e4;
|
|
55
|
+
canonicalInstanceId(instanceId, placeId) {
|
|
56
|
+
return publishedInstanceId(placeId) ?? instanceId;
|
|
57
|
+
}
|
|
58
|
+
rememberInstanceAlias(aliasInstanceId, targetInstanceId) {
|
|
59
|
+
if (aliasInstanceId === targetInstanceId)
|
|
60
|
+
return;
|
|
61
|
+
this.instanceAliases.set(aliasInstanceId, {
|
|
62
|
+
targetInstanceId,
|
|
63
|
+
lastSeen: Date.now()
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
resolveInstanceAlias(instanceId) {
|
|
67
|
+
const alias = this.instanceAliases.get(instanceId);
|
|
68
|
+
if (!alias)
|
|
69
|
+
return instanceId;
|
|
70
|
+
alias.lastSeen = Date.now();
|
|
71
|
+
return alias.targetInstanceId;
|
|
72
|
+
}
|
|
73
|
+
migratePendingRequests(fromInstanceId, toInstanceId) {
|
|
74
|
+
if (fromInstanceId === toInstanceId)
|
|
75
|
+
return;
|
|
76
|
+
for (const request of this.pendingRequests.values()) {
|
|
77
|
+
if (request.targetInstanceId === fromInstanceId) {
|
|
78
|
+
request.targetInstanceId = toInstanceId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
cleanupStaleAliases(now = Date.now()) {
|
|
83
|
+
for (const [alias, entry] of this.instanceAliases.entries()) {
|
|
84
|
+
const targetIsLive = this.getInstances().some((inst) => inst.instanceId === entry.targetInstanceId);
|
|
85
|
+
if (!targetIsLive && now - entry.lastSeen > INSTANCE_ALIAS_TTL_MS) {
|
|
86
|
+
this.instanceAliases.delete(alias);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
routingKeyForInstance(inst) {
|
|
91
|
+
return publishedInstanceId(inst.placeId) ?? this.resolveInstanceAlias(inst.instanceId);
|
|
92
|
+
}
|
|
93
|
+
matchingInstancesForInstanceId(instanceId) {
|
|
94
|
+
const resolvedInstanceId = this.resolveInstanceAlias(instanceId);
|
|
95
|
+
const ids = /* @__PURE__ */ new Set([instanceId, resolvedInstanceId]);
|
|
96
|
+
const placeIds = /* @__PURE__ */ new Set();
|
|
97
|
+
const addPlaceId = (placeId) => {
|
|
98
|
+
const published = publishedInstanceId(placeId);
|
|
99
|
+
if (!published || placeId === void 0)
|
|
100
|
+
return;
|
|
101
|
+
ids.add(published);
|
|
102
|
+
placeIds.add(Math.trunc(placeId));
|
|
103
|
+
};
|
|
104
|
+
const placeMatch = resolvedInstanceId.match(/^place:(\d+)$/) ?? instanceId.match(/^place:(\d+)$/);
|
|
105
|
+
if (placeMatch)
|
|
106
|
+
addPlaceId(Number(placeMatch[1]));
|
|
107
|
+
for (const inst of this.getInstances()) {
|
|
108
|
+
if (ids.has(inst.instanceId))
|
|
109
|
+
addPlaceId(inst.placeId);
|
|
110
|
+
}
|
|
111
|
+
return this.getInstances().filter((inst) => ids.has(inst.instanceId) || inst.placeId > 0 && placeIds.has(Math.trunc(inst.placeId)));
|
|
112
|
+
}
|
|
113
|
+
resolveInstanceId(instanceId) {
|
|
114
|
+
return this.resolveInstanceAlias(instanceId);
|
|
115
|
+
}
|
|
48
116
|
registerInstance(input) {
|
|
49
|
-
const { pluginSessionId,
|
|
117
|
+
const { pluginSessionId, role } = input;
|
|
118
|
+
const rawInstanceId = input.instanceId;
|
|
119
|
+
const instanceId = this.canonicalInstanceId(rawInstanceId, input.placeId);
|
|
50
120
|
const prior = this.instances.get(pluginSessionId);
|
|
51
121
|
let assignedRole = role;
|
|
52
122
|
const pluginVersion = input.pluginVersion ?? "";
|
|
53
123
|
const pluginVariant = input.pluginVariant ?? "unknown";
|
|
54
124
|
const serverVersion = input.serverVersion ?? "";
|
|
55
125
|
const versionMismatch = pluginVersion !== "" && serverVersion !== "" && pluginVersion !== serverVersion;
|
|
126
|
+
this.rememberInstanceAlias(rawInstanceId, instanceId);
|
|
127
|
+
if (prior && prior.instanceId !== instanceId) {
|
|
128
|
+
this.rememberInstanceAlias(prior.instanceId, instanceId);
|
|
129
|
+
this.migratePendingRequests(prior.instanceId, instanceId);
|
|
130
|
+
}
|
|
56
131
|
if (role === "client") {
|
|
57
|
-
if (prior && prior.
|
|
132
|
+
if (prior && prior.role.match(/^client-\d+$/)) {
|
|
58
133
|
assignedRole = prior.role;
|
|
59
134
|
} else {
|
|
60
135
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -135,6 +210,7 @@ var init_bridge_service = __esm({
|
|
|
135
210
|
const inst = this.instances.get(pluginSessionId);
|
|
136
211
|
if (!inst)
|
|
137
212
|
return;
|
|
213
|
+
const priorInstanceId = inst.instanceId;
|
|
138
214
|
if (metadata.placeId !== void 0)
|
|
139
215
|
inst.placeId = metadata.placeId;
|
|
140
216
|
if (metadata.placeName !== void 0)
|
|
@@ -143,6 +219,15 @@ var init_bridge_service = __esm({
|
|
|
143
219
|
inst.dataModelName = metadata.dataModelName;
|
|
144
220
|
if (metadata.isRunning !== void 0)
|
|
145
221
|
inst.isRunning = metadata.isRunning;
|
|
222
|
+
const canonicalInstanceId = this.canonicalInstanceId(inst.instanceId, inst.placeId);
|
|
223
|
+
if (canonicalInstanceId !== inst.instanceId) {
|
|
224
|
+
const duplicate = Array.from(this.instances.values()).find((other) => other.pluginSessionId !== pluginSessionId && other.instanceId === canonicalInstanceId && other.role === inst.role);
|
|
225
|
+
if (!duplicate) {
|
|
226
|
+
this.rememberInstanceAlias(priorInstanceId, canonicalInstanceId);
|
|
227
|
+
this.migratePendingRequests(priorInstanceId, canonicalInstanceId);
|
|
228
|
+
inst.instanceId = canonicalInstanceId;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
146
231
|
}
|
|
147
232
|
cleanupStaleInstances() {
|
|
148
233
|
const now = Date.now();
|
|
@@ -151,6 +236,37 @@ var init_bridge_service = __esm({
|
|
|
151
236
|
this.unregisterInstance(id);
|
|
152
237
|
}
|
|
153
238
|
}
|
|
239
|
+
this.cleanupStaleAliases(now);
|
|
240
|
+
}
|
|
241
|
+
getEquivalentInstanceIds(instanceId) {
|
|
242
|
+
const resolvedInstanceId = this.resolveInstanceAlias(instanceId);
|
|
243
|
+
const ids = /* @__PURE__ */ new Set([instanceId, resolvedInstanceId]);
|
|
244
|
+
const placeIds = /* @__PURE__ */ new Set();
|
|
245
|
+
const addPlaceId = (placeId) => {
|
|
246
|
+
const published = publishedInstanceId(placeId);
|
|
247
|
+
if (!published || placeId === void 0)
|
|
248
|
+
return;
|
|
249
|
+
ids.add(published);
|
|
250
|
+
placeIds.add(Math.trunc(placeId));
|
|
251
|
+
};
|
|
252
|
+
const placeMatch = resolvedInstanceId.match(/^place:(\d+)$/) ?? instanceId.match(/^place:(\d+)$/);
|
|
253
|
+
if (placeMatch)
|
|
254
|
+
addPlaceId(Number(placeMatch[1]));
|
|
255
|
+
for (const inst of this.getInstances()) {
|
|
256
|
+
if (ids.has(inst.instanceId)) {
|
|
257
|
+
addPlaceId(inst.placeId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const inst of this.getInstances()) {
|
|
261
|
+
if (inst.placeId > 0 && placeIds.has(Math.trunc(inst.placeId))) {
|
|
262
|
+
ids.add(inst.instanceId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [alias, entry] of this.instanceAliases.entries()) {
|
|
266
|
+
if (ids.has(entry.targetInstanceId))
|
|
267
|
+
ids.add(alias);
|
|
268
|
+
}
|
|
269
|
+
return Array.from(ids);
|
|
154
270
|
}
|
|
155
271
|
// Resolves (instance_id, target-role) MCP arguments to a concrete
|
|
156
272
|
// routing decision: either a single (instanceId, role) tuple or a fanout
|
|
@@ -164,7 +280,7 @@ var init_bridge_service = __esm({
|
|
|
164
280
|
const isFanout = target === "all";
|
|
165
281
|
const role = target && target !== "all" ? target : void 0;
|
|
166
282
|
if (instance_id !== void 0) {
|
|
167
|
-
const matchingInstances =
|
|
283
|
+
const matchingInstances = this.matchingInstancesForInstanceId(instance_id);
|
|
168
284
|
if (matchingInstances.length === 0) {
|
|
169
285
|
return {
|
|
170
286
|
ok: false,
|
|
@@ -197,19 +313,19 @@ var init_bridge_service = __esm({
|
|
|
197
313
|
}
|
|
198
314
|
};
|
|
199
315
|
}
|
|
200
|
-
return { ok: true, mode: "single", targetInstanceId:
|
|
316
|
+
return { ok: true, mode: "single", targetInstanceId: exact.instanceId, targetRole: role };
|
|
201
317
|
}
|
|
202
318
|
if (matchingInstances.length === 1) {
|
|
203
319
|
return {
|
|
204
320
|
ok: true,
|
|
205
321
|
mode: "single",
|
|
206
|
-
targetInstanceId:
|
|
322
|
+
targetInstanceId: matchingInstances[0].instanceId,
|
|
207
323
|
targetRole: matchingInstances[0].role
|
|
208
324
|
};
|
|
209
325
|
}
|
|
210
326
|
const edit = matchingInstances.find((i) => i.role === "edit");
|
|
211
327
|
if (edit) {
|
|
212
|
-
return { ok: true, mode: "single", targetInstanceId:
|
|
328
|
+
return { ok: true, mode: "single", targetInstanceId: edit.instanceId, targetRole: "edit" };
|
|
213
329
|
}
|
|
214
330
|
return {
|
|
215
331
|
ok: false,
|
|
@@ -220,7 +336,7 @@ var init_bridge_service = __esm({
|
|
|
220
336
|
}
|
|
221
337
|
};
|
|
222
338
|
}
|
|
223
|
-
const distinctInstanceIds = new Set(instances.map((i) => i
|
|
339
|
+
const distinctInstanceIds = new Set(instances.map((i) => this.routingKeyForInstance(i)));
|
|
224
340
|
if (distinctInstanceIds.size === 0) {
|
|
225
341
|
return {
|
|
226
342
|
ok: false,
|
|
@@ -236,7 +352,7 @@ var init_bridge_service = __esm({
|
|
|
236
352
|
const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
|
|
237
353
|
return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
|
|
238
354
|
}
|
|
239
|
-
const onlyInstanceId =
|
|
355
|
+
const onlyInstanceId = distinctInstanceIds.values().next().value;
|
|
240
356
|
return this.resolveTarget({ instance_id: onlyInstanceId, target });
|
|
241
357
|
}
|
|
242
358
|
async sendRequest(endpoint, data, targetInstanceId, targetRole) {
|
|
@@ -3042,23 +3158,25 @@ var init_tools = __esm({
|
|
|
3042
3158
|
if (!r.ok)
|
|
3043
3159
|
throw new RoutingFailure(r.error);
|
|
3044
3160
|
const resolvedId = r.targetInstanceId;
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3161
|
+
const equivalentIds = new Set(this.bridge.getEquivalentInstanceIds(resolvedId));
|
|
3162
|
+
const instances = this.bridge.getInstances().filter((i) => equivalentIds.has(i.instanceId));
|
|
3163
|
+
const client = instances.filter((inst) => inst.role.startsWith("client")).sort((a, b) => a.role.localeCompare(b.role))[0];
|
|
3164
|
+
return { instanceId: client?.instanceId ?? resolvedId, clientRole: client?.role };
|
|
3048
3165
|
}
|
|
3049
3166
|
_resolveInstanceIdOnly(instance_id) {
|
|
3050
3167
|
const instances = this.bridge.getInstances();
|
|
3051
3168
|
const publicList = this.bridge.getPublicInstances();
|
|
3052
3169
|
const errorData = { instances: publicList, count: publicList.length };
|
|
3053
3170
|
if (instance_id !== void 0) {
|
|
3054
|
-
|
|
3171
|
+
const resolvedInstanceId = this.bridge.resolveInstanceId(instance_id);
|
|
3172
|
+
if (!instances.some((i) => i.instanceId === resolvedInstanceId)) {
|
|
3055
3173
|
throw new RoutingFailure({
|
|
3056
3174
|
code: "unrecognized_instance_id",
|
|
3057
3175
|
message: `instance_id "${instance_id}" is not connected. Pass one from data.instances.`,
|
|
3058
3176
|
data: errorData
|
|
3059
3177
|
});
|
|
3060
3178
|
}
|
|
3061
|
-
return
|
|
3179
|
+
return resolvedInstanceId;
|
|
3062
3180
|
}
|
|
3063
3181
|
const distinct = Array.from(new Set(instances.map((i) => i.instanceId)));
|
|
3064
3182
|
if (distinct.length === 0) {
|
|
@@ -3096,6 +3214,10 @@ var init_tools = __esm({
|
|
|
3096
3214
|
_rolesForInstance(instanceId) {
|
|
3097
3215
|
return this.bridge.getInstances().filter((i) => i.instanceId === instanceId).map((i) => i.role);
|
|
3098
3216
|
}
|
|
3217
|
+
_rolesForEquivalentInstances(instanceId) {
|
|
3218
|
+
const instanceIds = new Set(this.bridge.getEquivalentInstanceIds(instanceId));
|
|
3219
|
+
return this.bridge.getInstances().filter((i) => instanceIds.has(i.instanceId)).map((i) => i.role);
|
|
3220
|
+
}
|
|
3099
3221
|
_clientRolesForInstance(instanceId) {
|
|
3100
3222
|
return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
|
|
3101
3223
|
}
|
|
@@ -3253,12 +3375,13 @@ var init_tools = __esm({
|
|
|
3253
3375
|
throw new Error(`capture_device_matrix cannot safely restore active custom device "${s.activeDeviceId}". Switch the simulator to default or a built-in preset first, or pass restoreAfter=false only if you intentionally accept changing the simulator state.`);
|
|
3254
3376
|
}
|
|
3255
3377
|
}
|
|
3256
|
-
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
|
|
3378
|
+
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30, equivalentInstances = false) {
|
|
3257
3379
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3258
3380
|
while (Date.now() < deadline) {
|
|
3259
|
-
const roles = this._rolesForInstance(instanceId);
|
|
3381
|
+
const roles = equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId);
|
|
3382
|
+
const clientRoles = equivalentInstances ? roles.filter((role) => /^client-\d+$/.test(role)) : this._clientRolesForInstance(instanceId);
|
|
3260
3383
|
const hasServer = !opts.server || roles.includes("server");
|
|
3261
|
-
const hasClients = opts.clientCount === void 0 ||
|
|
3384
|
+
const hasClients = opts.clientCount === void 0 || clientRoles.length >= opts.clientCount;
|
|
3262
3385
|
const absent = opts.absentRole === void 0 || !roles.includes(opts.absentRole);
|
|
3263
3386
|
const runtimeAbsent = !opts.noRuntime || !roles.some((role) => role === "server" || /^client-\d+$/.test(role));
|
|
3264
3387
|
if (hasServer && hasClients && absent && runtimeAbsent) {
|
|
@@ -3266,7 +3389,11 @@ var init_tools = __esm({
|
|
|
3266
3389
|
}
|
|
3267
3390
|
await sleep(250);
|
|
3268
3391
|
}
|
|
3269
|
-
return {
|
|
3392
|
+
return {
|
|
3393
|
+
ok: false,
|
|
3394
|
+
roles: equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId),
|
|
3395
|
+
timedOut: true
|
|
3396
|
+
};
|
|
3270
3397
|
}
|
|
3271
3398
|
async _waitForExactClientCount(instanceId, expectedClientCount, timeoutSec = 30, stableMs = 3e3) {
|
|
3272
3399
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
@@ -3291,10 +3418,11 @@ var init_tools = __esm({
|
|
|
3291
3418
|
const clientCount = this._clientRolesForInstance(instanceId).length;
|
|
3292
3419
|
return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
|
|
3293
3420
|
}
|
|
3294
|
-
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
|
|
3421
|
+
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60, equivalentInstances = false) {
|
|
3295
3422
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3296
3423
|
while (Date.now() < deadline) {
|
|
3297
|
-
const
|
|
3424
|
+
const instanceIds = equivalentInstances ? new Set(this.bridge.getEquivalentInstanceIds(instanceId)) : /* @__PURE__ */ new Set([instanceId]);
|
|
3425
|
+
const instances = this.bridge.getInstances().filter((i) => instanceIds.has(i.instanceId));
|
|
3298
3426
|
const roles = instances.map((i) => i.role);
|
|
3299
3427
|
const freshRoles = new Set(instances.filter((i) => i.connectedAt >= connectedAfter).map((i) => i.role));
|
|
3300
3428
|
if (requiredRoles.every((role) => freshRoles.has(role))) {
|
|
@@ -3302,7 +3430,11 @@ var init_tools = __esm({
|
|
|
3302
3430
|
}
|
|
3303
3431
|
await sleep(250);
|
|
3304
3432
|
}
|
|
3305
|
-
return {
|
|
3433
|
+
return {
|
|
3434
|
+
ok: false,
|
|
3435
|
+
roles: equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId),
|
|
3436
|
+
timedOut: true
|
|
3437
|
+
};
|
|
3306
3438
|
}
|
|
3307
3439
|
async getFileTree(path2 = "", instance_id) {
|
|
3308
3440
|
const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
|
|
@@ -4336,7 +4468,7 @@ ${code}`
|
|
|
4336
4468
|
let wait;
|
|
4337
4469
|
if (response?.success === true) {
|
|
4338
4470
|
const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
|
|
4339
|
-
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles);
|
|
4471
|
+
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles, 60, true);
|
|
4340
4472
|
}
|
|
4341
4473
|
const body = wait ? {
|
|
4342
4474
|
...response,
|
|
@@ -4358,7 +4490,7 @@ ${code}`
|
|
|
4358
4490
|
const response = await this.client.request("/api/stop-playtest", {}, instanceId, "edit");
|
|
4359
4491
|
let wait;
|
|
4360
4492
|
if (response?.success === true) {
|
|
4361
|
-
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15);
|
|
4493
|
+
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15, true);
|
|
4362
4494
|
}
|
|
4363
4495
|
const body = wait ? {
|
|
4364
4496
|
...response,
|
package/package.json
CHANGED