@chrrxs/robloxstudio-mcp-inspector 3.0.3 → 3.0.4

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
@@ -1971,6 +1971,8 @@ var DUPLICATE_TAKEOVER_MS = 3e3;
1971
1971
  var INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
1972
1972
  var ACCEPTED_REQUEST_TOMBSTONE_TTL_MS = 6e4;
1973
1973
  var MAX_ACCEPTED_REQUEST_TOMBSTONES = 4096;
1974
+ var CANCELLATION_TOMBSTONE_TTL_MS = 6e4;
1975
+ var MAX_CANCELLATION_TOMBSTONES = 4096;
1974
1976
  function publishedInstanceId(placeId) {
1975
1977
  if (placeId === void 0 || !Number.isFinite(placeId) || placeId <= 0)
1976
1978
  return void 0;
@@ -1979,6 +1981,7 @@ function publishedInstanceId(placeId) {
1979
1981
  var BridgeService = class {
1980
1982
  pendingRequests = /* @__PURE__ */ new Map();
1981
1983
  acceptedRequestIds = /* @__PURE__ */ new Map();
1984
+ pendingCancellations = /* @__PURE__ */ new Map();
1982
1985
  // Keyed by pluginSessionId (the per-plugin GUID).
1983
1986
  instances = /* @__PURE__ */ new Map();
1984
1987
  instanceAliases = /* @__PURE__ */ new Map();
@@ -2037,6 +2040,21 @@ var BridgeService = class {
2037
2040
  }
2038
2041
  }
2039
2042
  }
2043
+ notifyRequestCancelled(request, reason) {
2044
+ const physicalSessionId = request.lastDeliveryPhysicalSessionId;
2045
+ if (!physicalSessionId || this.pendingCancellations.has(request.id))
2046
+ return;
2047
+ const now = Date.now();
2048
+ this.prunePendingCancellations(now);
2049
+ this.pendingCancellations.set(request.id, {
2050
+ requestId: request.id,
2051
+ reason,
2052
+ physicalSessionId,
2053
+ createdAt: now
2054
+ });
2055
+ this.prunePendingCancellations(now);
2056
+ this.notifyRequestAvailable(physicalSessionId);
2057
+ }
2040
2058
  physicalSessionsForTarget(targetInstanceId, targetRole) {
2041
2059
  const physicalSessionIds = /* @__PURE__ */ new Set();
2042
2060
  for (const instance of this.instances.values()) {
@@ -2196,11 +2214,10 @@ var BridgeService = class {
2196
2214
  }
2197
2215
  for (const child of logicalChildren)
2198
2216
  this.unregisterInstance(child.pluginSessionId);
2199
- for (const [id, req] of this.pendingRequests.entries()) {
2217
+ for (const req of Array.from(this.pendingRequests.values())) {
2200
2218
  const stillHasHandler = Array.from(this.instances.values()).some((i) => i.instanceId === req.targetInstanceId && i.role === req.targetRole);
2201
2219
  if (!stillHasHandler) {
2202
- clearTimeout(req.timeoutId);
2203
- this.pendingRequests.delete(id);
2220
+ this.removePendingRequest(req);
2204
2221
  req.reject(new Error(`Target (${req.targetInstanceId}, ${req.targetRole}) disconnected`));
2205
2222
  }
2206
2223
  }
@@ -2390,16 +2407,21 @@ var BridgeService = class {
2390
2407
  const onlyInstanceId = distinctInstanceIds.values().next().value;
2391
2408
  return this.resolveTarget({ instance_id: onlyInstanceId, target });
2392
2409
  }
2393
- async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout) {
2410
+ async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout, signal) {
2394
2411
  const requestId = randomUUID();
2395
2412
  const effectiveTimeoutMs = Math.max(1, timeoutMs);
2413
+ if (signal?.aborted)
2414
+ throw new Error("Request aborted");
2396
2415
  return new Promise((resolve5, reject) => {
2397
- const timeoutId = setTimeout(() => {
2398
- if (this.pendingRequests.has(requestId)) {
2399
- this.pendingRequests.delete(requestId);
2400
- reject(new Error("Request timeout"));
2401
- }
2402
- }, effectiveTimeoutMs);
2416
+ const cancelPending = (reason, error) => {
2417
+ const pending = this.pendingRequests.get(requestId);
2418
+ if (!pending || !this.removePendingRequest(pending))
2419
+ return;
2420
+ this.notifyRequestCancelled(pending, reason);
2421
+ pending.reject(error);
2422
+ };
2423
+ const timeoutId = setTimeout(() => cancelPending("timeout", new Error("Request timeout")), effectiveTimeoutMs);
2424
+ const abortListener = () => cancelPending("aborted", new Error("Request aborted"));
2403
2425
  const request = {
2404
2426
  id: requestId,
2405
2427
  endpoint,
@@ -2410,14 +2432,31 @@ var BridgeService = class {
2410
2432
  resolve: resolve5,
2411
2433
  reject,
2412
2434
  timeoutId,
2413
- timeoutMs: effectiveTimeoutMs
2435
+ timeoutMs: effectiveTimeoutMs,
2436
+ abortSignal: signal,
2437
+ abortListener
2414
2438
  };
2415
2439
  this.pendingRequests.set(requestId, request);
2416
- for (const physicalSessionId of this.physicalSessionsForTarget(targetInstanceId, targetRole)) {
2417
- this.notifyRequestAvailable(physicalSessionId);
2440
+ signal?.addEventListener("abort", abortListener, { once: true });
2441
+ if (signal?.aborted)
2442
+ abortListener();
2443
+ if (this.pendingRequests.has(requestId)) {
2444
+ for (const physicalSessionId of this.physicalSessionsForTarget(targetInstanceId, targetRole)) {
2445
+ this.notifyRequestAvailable(physicalSessionId);
2446
+ }
2418
2447
  }
2419
2448
  });
2420
2449
  }
2450
+ removePendingRequest(request) {
2451
+ if (this.pendingRequests.get(request.id) !== request)
2452
+ return false;
2453
+ clearTimeout(request.timeoutId);
2454
+ if (request.abortSignal && request.abortListener) {
2455
+ request.abortSignal.removeEventListener("abort", request.abortListener);
2456
+ }
2457
+ this.pendingRequests.delete(request.id);
2458
+ return true;
2459
+ }
2421
2460
  claimNextRequestForPhysical(physicalSessionId, claimOwner) {
2422
2461
  let oldestRequest;
2423
2462
  let logicalSessionId = "";
@@ -2441,14 +2480,27 @@ var BridgeService = class {
2441
2480
  if (!oldestRequest)
2442
2481
  return null;
2443
2482
  oldestRequest.claimOwner = claimOwner;
2483
+ oldestRequest.lastDeliveryPhysicalSessionId = physicalSessionId;
2444
2484
  return {
2445
2485
  requestId: oldestRequest.id,
2446
2486
  logicalSessionId,
2447
2487
  target: oldestRequest.targetRole,
2448
2488
  endpoint: oldestRequest.endpoint,
2449
- data: oldestRequest.data
2489
+ data: oldestRequest.data,
2490
+ remainingMs: Math.max(1, oldestRequest.timeoutMs - (Date.now() - oldestRequest.timestamp))
2450
2491
  };
2451
2492
  }
2493
+ claimNextCancellationForPhysical(physicalSessionId, claimOwner) {
2494
+ this.prunePendingCancellations(Date.now());
2495
+ for (const cancellation of this.pendingCancellations.values()) {
2496
+ if (cancellation.physicalSessionId !== physicalSessionId || cancellation.claimOwner !== void 0) {
2497
+ continue;
2498
+ }
2499
+ cancellation.claimOwner = claimOwner;
2500
+ return { requestId: cancellation.requestId, reason: cancellation.reason };
2501
+ }
2502
+ return null;
2503
+ }
2452
2504
  releaseDeliveryClaims(claimOwner) {
2453
2505
  const physicalSessionIds = /* @__PURE__ */ new Set();
2454
2506
  for (const request of this.pendingRequests.values()) {
@@ -2459,6 +2511,12 @@ var BridgeService = class {
2459
2511
  physicalSessionIds.add(physicalSessionId);
2460
2512
  }
2461
2513
  }
2514
+ for (const cancellation of this.pendingCancellations.values()) {
2515
+ if (cancellation.claimOwner !== claimOwner)
2516
+ continue;
2517
+ cancellation.claimOwner = void 0;
2518
+ physicalSessionIds.add(cancellation.physicalSessionId);
2519
+ }
2462
2520
  for (const physicalSessionId of physicalSessionIds) {
2463
2521
  this.notifyRequestAvailable(physicalSessionId);
2464
2522
  }
@@ -2476,8 +2534,7 @@ var BridgeService = class {
2476
2534
  if (!request) {
2477
2535
  return this.acceptedRequestIds.has(requestId) ? "already_settled" : "unknown";
2478
2536
  }
2479
- clearTimeout(request.timeoutId);
2480
- this.pendingRequests.delete(requestId);
2537
+ this.removePendingRequest(request);
2481
2538
  this.acceptedRequestIds.set(requestId, now);
2482
2539
  this.pruneAcceptedRequestIds(now);
2483
2540
  settle(request);
@@ -2496,22 +2553,34 @@ var BridgeService = class {
2496
2553
  this.acceptedRequestIds.delete(oldestRequestId);
2497
2554
  }
2498
2555
  }
2556
+ prunePendingCancellations(now) {
2557
+ for (const [requestId, cancellation] of this.pendingCancellations) {
2558
+ if (now - cancellation.createdAt < CANCELLATION_TOMBSTONE_TTL_MS)
2559
+ break;
2560
+ this.pendingCancellations.delete(requestId);
2561
+ }
2562
+ while (this.pendingCancellations.size > MAX_CANCELLATION_TOMBSTONES) {
2563
+ const oldestRequestId = this.pendingCancellations.keys().next().value;
2564
+ if (oldestRequestId === void 0)
2565
+ break;
2566
+ this.pendingCancellations.delete(oldestRequestId);
2567
+ }
2568
+ }
2499
2569
  cleanupOldRequests() {
2500
2570
  const now = Date.now();
2501
- for (const [id, request] of this.pendingRequests.entries()) {
2502
- if (now - request.timestamp > request.timeoutMs) {
2503
- clearTimeout(request.timeoutId);
2504
- this.pendingRequests.delete(id);
2571
+ for (const request of this.pendingRequests.values()) {
2572
+ if (now - request.timestamp > request.timeoutMs && this.removePendingRequest(request)) {
2573
+ this.notifyRequestCancelled(request, "timeout");
2505
2574
  request.reject(new Error("Request timeout"));
2506
2575
  }
2507
2576
  }
2508
2577
  }
2509
2578
  clearAllPendingRequests() {
2510
- for (const [, request] of this.pendingRequests.entries()) {
2511
- clearTimeout(request.timeoutId);
2579
+ for (const request of Array.from(this.pendingRequests.values())) {
2580
+ this.removePendingRequest(request);
2512
2581
  request.reject(new Error("Connection closed"));
2513
2582
  }
2514
- this.pendingRequests.clear();
2583
+ this.pendingCancellations.clear();
2515
2584
  }
2516
2585
  };
2517
2586
 
@@ -5536,9 +5605,9 @@ function createToolServer(options) {
5536
5605
  inputSchema: fromJsonSchema(publicDefinition.inputSchema),
5537
5606
  ...publicDefinition.outputSchema ? { outputSchema: fromJsonSchema(publicDefinition.outputSchema) } : {},
5538
5607
  annotations: publicDefinition.annotations
5539
- }, async (args) => {
5608
+ }, async (args, context) => {
5540
5609
  try {
5541
- const raw = await options.invoke(options.getTools(), definition.name, args);
5610
+ const raw = await options.invoke(options.getTools(), definition.name, args, { signal: context.mcpReq.signal });
5542
5611
  return normalizeToolResult(raw, options.era);
5543
5612
  } catch (error) {
5544
5613
  return normalizeToolResult({
@@ -5723,6 +5792,13 @@ var SseStudioTransport = class {
5723
5792
  return;
5724
5793
  }
5725
5794
  }
5795
+ while (!stream.closed && !stream.blocked) {
5796
+ const cancellation = this.queue.claimNextCancellationForPhysical(stream.physicalSessionId, stream.claimOwner);
5797
+ if (!cancellation)
5798
+ break;
5799
+ if (!this.write(stream, { kind: "cancel", ...cancellation }))
5800
+ return;
5801
+ }
5726
5802
  while (!stream.closed && !stream.blocked) {
5727
5803
  const request = this.queue.claimNextRequestForPhysical(stream.physicalSessionId, stream.claimOwner);
5728
5804
  if (!request)
@@ -5733,7 +5809,8 @@ var SseStudioTransport = class {
5733
5809
  logicalSessionId: request.logicalSessionId,
5734
5810
  target: request.target,
5735
5811
  endpoint: request.endpoint,
5736
- data: request.data === void 0 ? null : request.data
5812
+ data: request.data === void 0 ? null : request.data,
5813
+ remainingMs: request.remainingMs
5737
5814
  };
5738
5815
  if (!this.write(stream, event))
5739
5816
  return;
@@ -5834,7 +5911,7 @@ var TOOL_HANDLERS = {
5834
5911
  get_instance_properties: (tools, body) => tools.getInstanceProperties(body.instancePath, body.excludeSource, body.instance_id),
5835
5912
  get_project_structure: (tools, body) => tools.getProjectStructure(body.path, body.maxDepth, body.scriptsOnly, body.instance_id),
5836
5913
  set_properties: (tools, body) => tools.setProperties(body.instancePath, body.properties, body.instance_id),
5837
- grep_scripts: (tools, body) => tools.grepScripts(body.pattern, {
5914
+ grep_scripts: (tools, body, context) => tools.grepScripts(body.pattern, {
5838
5915
  caseSensitive: body.caseSensitive,
5839
5916
  usePattern: body.usePattern,
5840
5917
  contextLines: body.contextLines,
@@ -5843,7 +5920,7 @@ var TOOL_HANDLERS = {
5843
5920
  filesOnly: body.filesOnly,
5844
5921
  path: body.path,
5845
5922
  classFilter: body.classFilter
5846
- }, body.instance_id),
5923
+ }, body.instance_id, context?.signal),
5847
5924
  get_script_source: (tools, body) => {
5848
5925
  const { startLine, endLine } = optionalLineRange(body, "get_script_source");
5849
5926
  return tools.getScriptSource(body.instancePath, startLine, endLine, body.instance_id);
@@ -6273,30 +6350,45 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
6273
6350
  res.json({ success: true, disposition });
6274
6351
  });
6275
6352
  app.post("/proxy", async (req, res) => {
6276
- const { endpoint, data, targetInstanceId, targetRole, proxyInstanceId } = req.body;
6353
+ const { endpoint, data, targetInstanceId, targetRole, proxyInstanceId, timeoutMs } = req.body;
6277
6354
  if (!endpoint || !targetInstanceId || !targetRole) {
6278
6355
  res.status(400).json({ error: "endpoint, targetInstanceId, and targetRole are required" });
6279
6356
  return;
6280
6357
  }
6358
+ if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5)) {
6359
+ res.status(400).json({ error: "timeoutMs must be an integer between 1 and 300000" });
6360
+ return;
6361
+ }
6281
6362
  if (proxyInstanceId) {
6282
6363
  proxyInstances.add(proxyInstanceId);
6283
6364
  }
6365
+ const controller = new AbortController();
6366
+ const abort = () => controller.abort();
6367
+ req.once("aborted", abort);
6368
+ res.once("close", abort);
6284
6369
  try {
6285
- const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
6370
+ const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs, controller.signal);
6286
6371
  res.json({ response });
6287
- } catch (err2) {
6288
- res.status(500).json({ error: err2.message || "Proxy request failed" });
6372
+ } catch (error) {
6373
+ if (!res.headersSent && !res.destroyed) {
6374
+ res.status(500).json({
6375
+ error: error instanceof Error ? error.message : "Proxy request failed"
6376
+ });
6377
+ }
6378
+ } finally {
6379
+ req.removeListener("aborted", abort);
6380
+ res.removeListener("close", abort);
6289
6381
  }
6290
6382
  });
6291
6383
  const mcpHandler = serverConfig ? createToolHttpHandler({
6292
6384
  config: serverConfig,
6293
6385
  getTools: () => tools,
6294
6386
  allowedTools,
6295
- invoke: async (currentTools, name, args) => {
6387
+ invoke: async (currentTools, name, args, context) => {
6296
6388
  const handler = TOOL_HANDLERS[name];
6297
6389
  if (!handler)
6298
6390
  throw new Error(`Unknown tool: ${name}`);
6299
- return handler(currentTools, args);
6391
+ return handler(currentTools, args, context);
6300
6392
  }
6301
6393
  }) : void 0;
6302
6394
  const nodeMcpHandler = mcpHandler ? toNodeHandler(mcpHandler) : void 0;
@@ -6341,25 +6433,21 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
6341
6433
  };
6342
6434
  return app;
6343
6435
  }
6344
- function listenWithRetry(app, host, startPort, maxAttempts = 5) {
6345
- return new Promise(async (resolve5, reject) => {
6346
- for (let i = 0; i < maxAttempts; i++) {
6347
- const port = startPort + i;
6348
- try {
6349
- const server = await bindPort(app, host, port);
6350
- resolve5({ server, port });
6351
- return;
6352
- } catch (err2) {
6353
- if (err2.code === "EADDRINUSE") {
6354
- console.error(`Port ${port} in use, trying next...`);
6355
- continue;
6356
- }
6357
- reject(err2);
6358
- return;
6436
+ async function listenWithRetry(app, host, startPort, maxAttempts = 5) {
6437
+ for (let i = 0; i < maxAttempts; i++) {
6438
+ const port = startPort + i;
6439
+ try {
6440
+ const server = await bindPort(app, host, port);
6441
+ return { server, port };
6442
+ } catch (error) {
6443
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
6444
+ console.error(`Port ${port} in use, trying next...`);
6445
+ continue;
6359
6446
  }
6447
+ throw error;
6360
6448
  }
6361
- reject(new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`));
6362
- });
6449
+ }
6450
+ throw new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`);
6363
6451
  }
6364
6452
  function bindPort(app, host, port) {
6365
6453
  return new Promise((resolve5, reject) => {
@@ -6382,9 +6470,9 @@ var StudioHttpClient = class {
6382
6470
  constructor(bridge) {
6383
6471
  this.bridge = bridge;
6384
6472
  }
6385
- async request(endpoint, data, targetInstanceId, targetRole, timeoutMs) {
6473
+ async request(endpoint, data, targetInstanceId, targetRole, timeoutMs, signal) {
6386
6474
  try {
6387
- const response = await this.bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs);
6475
+ const response = await this.bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs, signal);
6388
6476
  return response;
6389
6477
  } catch (error) {
6390
6478
  if (error instanceof Error && error.message === "Request timeout") {
@@ -9199,6 +9287,8 @@ var MAX_SEARCH_ASSET_DESCRIPTION_LENGTH = 240;
9199
9287
  var ROBLOX_CREATOR_USER_ID = 1;
9200
9288
  var MAX_DEVICE_MATRIX_ENTRIES = 6;
9201
9289
  var MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
9290
+ var GREP_SCRIPTS_TIMEOUT_MS = 12e4;
9291
+ var MAX_GREP_PATTERN_UTF8_BYTES = 4096;
9202
9292
  var STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
9203
9293
  var CREATOR_STORE_SEARCH_TYPES = /* @__PURE__ */ new Set([
9204
9294
  "Audio",
@@ -10025,7 +10115,7 @@ var RobloxStudioTools = class {
10025
10115
  // ambiguous, missing, or asks for fanout on a non-fanout-capable tool —
10026
10116
  // the MCP transport layer surfaces it as a structured error result so
10027
10117
  // the LLM can recover via the embedded data.instances list.
10028
- async _callSingle(endpoint, data, target, instance_id, timeoutMs) {
10118
+ async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal) {
10029
10119
  const r = this.bridge.resolveTarget({ instance_id, target });
10030
10120
  if (!r.ok)
10031
10121
  throw new RoutingFailure(r.error);
@@ -10039,7 +10129,10 @@ var RobloxStudioTools = class {
10039
10129
  }
10040
10130
  });
10041
10131
  }
10042
- return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs);
10132
+ if (signal === void 0) {
10133
+ return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs);
10134
+ }
10135
+ return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs, signal);
10043
10136
  }
10044
10137
  // Resolves which connected place a tool should target and whether a playtest
10045
10138
  // CLIENT peer is present on it. Used by capture/input to auto-route to the
@@ -10539,14 +10632,17 @@ var RobloxStudioTools = class {
10539
10632
  ]
10540
10633
  };
10541
10634
  }
10542
- async grepScripts(pattern, options, instance_id) {
10635
+ async grepScripts(pattern, options, instance_id, signal) {
10543
10636
  if (!pattern) {
10544
10637
  throw new Error("Pattern is required for grep_scripts");
10545
10638
  }
10639
+ if (Buffer.byteLength(pattern, "utf8") > MAX_GREP_PATTERN_UTF8_BYTES) {
10640
+ throw new Error(`Pattern must not exceed ${MAX_GREP_PATTERN_UTF8_BYTES} UTF-8 bytes`);
10641
+ }
10546
10642
  const response = await this._callSingle("/api/grep-scripts", {
10547
10643
  pattern,
10548
10644
  ...options ?? {}
10549
- }, void 0, instance_id);
10645
+ }, void 0, instance_id, GREP_SCRIPTS_TIMEOUT_MS, signal);
10550
10646
  return {
10551
10647
  content: [
10552
10648
  {
@@ -12988,6 +13084,7 @@ var RobloxStudioTools = class {
12988
13084
 
12989
13085
  // ../core/dist/proxy-bridge-service.js
12990
13086
  import { randomUUID as randomUUID4 } from "crypto";
13087
+ var PROXY_RESPONSE_GRACE_MS = 5e3;
12991
13088
  var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
12992
13089
  primaryBaseUrl;
12993
13090
  authToken;
@@ -13063,9 +13160,20 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
13063
13160
  this.refreshTimer = void 0;
13064
13161
  }
13065
13162
  }
13066
- async sendRequest(endpoint, data, targetInstanceId, targetRole) {
13163
+ async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.proxyRequestTimeout, signal) {
13164
+ if (signal?.aborted)
13165
+ throw new Error("Request aborted");
13067
13166
  const controller = new AbortController();
13068
- const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
13167
+ const effectiveTimeoutMs = Math.max(1, timeoutMs);
13168
+ let timedOut = false;
13169
+ const abortFromCaller = () => controller.abort();
13170
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
13171
+ if (signal?.aborted)
13172
+ controller.abort();
13173
+ const timeoutId = setTimeout(() => {
13174
+ timedOut = true;
13175
+ controller.abort();
13176
+ }, effectiveTimeoutMs + PROXY_RESPONSE_GRACE_MS);
13069
13177
  try {
13070
13178
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
13071
13179
  method: "POST",
@@ -13075,26 +13183,34 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
13075
13183
  data,
13076
13184
  targetInstanceId,
13077
13185
  targetRole,
13078
- proxyInstanceId: this.proxyInstanceId
13186
+ proxyInstanceId: this.proxyInstanceId,
13187
+ timeoutMs: effectiveTimeoutMs
13079
13188
  }),
13080
13189
  signal: controller.signal
13081
13190
  });
13082
- clearTimeout(timeoutId);
13083
13191
  if (!response.ok) {
13084
13192
  const body = await response.text();
13085
13193
  throw new Error(`Proxy request failed (${response.status}): ${body}`);
13086
13194
  }
13087
13195
  const result = await response.json();
13088
- if (result.error) {
13196
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
13197
+ throw new Error("Proxy returned an invalid response");
13198
+ }
13199
+ if ("error" in result && typeof result.error === "string" && result.error.length > 0) {
13089
13200
  throw new Error(result.error);
13090
13201
  }
13091
- return result.response;
13092
- } catch (err2) {
13093
- clearTimeout(timeoutId);
13094
- if (err2.name === "AbortError") {
13202
+ return "response" in result ? result.response : void 0;
13203
+ } catch (error) {
13204
+ const isAbortError = error instanceof Error ? error.name === "AbortError" : !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
13205
+ if (isAbortError) {
13206
+ if (!timedOut && signal?.aborted)
13207
+ throw new Error("Request aborted");
13095
13208
  throw new Error("Proxy request timeout");
13096
13209
  }
13097
- throw err2;
13210
+ throw error;
13211
+ } finally {
13212
+ clearTimeout(timeoutId);
13213
+ signal?.removeEventListener("abort", abortFromCaller);
13098
13214
  }
13099
13215
  }
13100
13216
  cleanupOldRequests() {
@@ -13190,11 +13306,11 @@ var RobloxStudioMCPServer = class {
13190
13306
  getTools: () => this.tools,
13191
13307
  allowedTools: this.allowedToolNames,
13192
13308
  era: context.era,
13193
- invoke: async (tools, name, args) => {
13309
+ invoke: async (tools, name, args, invocation) => {
13194
13310
  const handler = TOOL_HANDLERS[name];
13195
13311
  if (!handler)
13196
13312
  throw new Error(`Unknown tool: ${name}`);
13197
- return handler(tools, args);
13313
+ return handler(tools, args, invocation);
13198
13314
  }
13199
13315
  }), { onerror: (error) => console.error("[mcp:stdio]", error) });
13200
13316
  console.error(`${this.config.name} v${this.config.version} running on stdio`);
@@ -13641,37 +13757,44 @@ var TOOL_DEFINITIONS = [
13641
13757
  {
13642
13758
  name: "grep_scripts",
13643
13759
  category: "read",
13644
- description: "Use to locate text or Lua pattern matches across script sources.",
13760
+ description: "Use to search script sources.",
13645
13761
  inputSchema: {
13646
13762
  type: "object",
13647
13763
  properties: {
13648
13764
  pattern: {
13649
13765
  type: "string",
13650
- description: "Literal text, or a Lua pattern when usePattern is true."
13766
+ maxLength: 4096,
13767
+ description: "Literal or Lua pattern; max 4096 UTF-8 bytes."
13651
13768
  },
13652
13769
  caseSensitive: {
13653
13770
  type: "boolean",
13654
- description: "Literal match casing; patterns are always case-sensitive."
13771
+ description: "Lua patterns are always case-sensitive."
13655
13772
  },
13656
13773
  usePattern: {
13657
13774
  type: "boolean",
13658
- description: "Use Lua patterns with top-level | alternation; not PCRE."
13775
+ description: "Enable Lua pattern with top-level | (not PCRE)."
13659
13776
  },
13660
13777
  contextLines: {
13661
- type: "number",
13662
- description: "Lines before and after each match; defaults to 0."
13778
+ type: "integer",
13779
+ minimum: 0,
13780
+ maximum: 100,
13781
+ description: "Surrounding lines (0-100; default 0)."
13663
13782
  },
13664
13783
  maxResults: {
13665
- type: "number",
13666
- description: "Total match limit; defaults to 100."
13784
+ type: "integer",
13785
+ minimum: 1,
13786
+ maximum: 1e4,
13787
+ description: "Default 100."
13667
13788
  },
13668
13789
  maxResultsPerScript: {
13669
- type: "number",
13670
- description: "Match limit per script."
13790
+ type: "integer",
13791
+ minimum: 0,
13792
+ maximum: 1e4,
13793
+ description: "Per-script cap; 0 is unlimited."
13671
13794
  },
13672
13795
  filesOnly: {
13673
13796
  type: "boolean",
13674
- description: "Return only script paths; defaults to false."
13797
+ description: "Paths only; default false."
13675
13798
  },
13676
13799
  path: {
13677
13800
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp-inspector",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "DataModel read-only MCP server for inspecting and debugging Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",