@chrrxs/robloxstudio-mcp 3.1.0 → 3.1.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 +1248 -337
- package/package.json +1 -1
- package/studio-plugin/MCPPlugin.rbxmx +640 -353
package/dist/index.js
CHANGED
|
@@ -1939,10 +1939,82 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
1939
1939
|
// ../core/dist/http-server.js
|
|
1940
1940
|
import express from "express";
|
|
1941
1941
|
import http from "http";
|
|
1942
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1943
|
+
import { WebSocketServer } from "ws";
|
|
1942
1944
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
1943
1945
|
|
|
1944
1946
|
// ../core/dist/bridge-service.js
|
|
1945
|
-
import { randomUUID } from "crypto";
|
|
1947
|
+
import { createHash, randomUUID } from "crypto";
|
|
1948
|
+
function isExecutionOutcome(value) {
|
|
1949
|
+
return value === "success" || value === "error" || value === "not_executed" || value === "unknown";
|
|
1950
|
+
}
|
|
1951
|
+
function isRequestStage(value) {
|
|
1952
|
+
return value === "queued" || value === "dispatched" || value === "executing" || value === "response_delivery";
|
|
1953
|
+
}
|
|
1954
|
+
function handlerOutcome(response) {
|
|
1955
|
+
if (!response || typeof response !== "object")
|
|
1956
|
+
return "success";
|
|
1957
|
+
if ("error" in response && response.error !== void 0 && response.error !== null || "success" in response && response.success === false || "ok" in response && response.ok === false)
|
|
1958
|
+
return "error";
|
|
1959
|
+
if ("summary" in response && response.summary && typeof response.summary === "object" && "failed" in response.summary && typeof response.summary.failed === "number" && response.summary.failed > 0)
|
|
1960
|
+
return "error";
|
|
1961
|
+
return "success";
|
|
1962
|
+
}
|
|
1963
|
+
function observations(status) {
|
|
1964
|
+
return {
|
|
1965
|
+
executionStartedAt: status.executionStartedAt,
|
|
1966
|
+
executionCompletedAt: status.executionCompletedAt,
|
|
1967
|
+
executionOutcome: status.executionOutcome,
|
|
1968
|
+
connectionLostAt: status.connectionLostAt,
|
|
1969
|
+
connectionRestoredAt: status.connectionRestoredAt
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
var RequestFailure = class extends Error {
|
|
1973
|
+
code;
|
|
1974
|
+
details;
|
|
1975
|
+
constructor(message, code, details) {
|
|
1976
|
+
super(message);
|
|
1977
|
+
this.code = code;
|
|
1978
|
+
this.details = details;
|
|
1979
|
+
this.name = "RequestFailure";
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
function parseObservations(value) {
|
|
1983
|
+
const result = {};
|
|
1984
|
+
if ("executionOutcome" in value) {
|
|
1985
|
+
if (!isExecutionOutcome(value.executionOutcome))
|
|
1986
|
+
throw new Error("Invalid execution outcome");
|
|
1987
|
+
result.executionOutcome = value.executionOutcome;
|
|
1988
|
+
}
|
|
1989
|
+
for (const key of ["executionStartedAt", "executionCompletedAt", "connectionLostAt", "connectionRestoredAt"]) {
|
|
1990
|
+
if (!(key in value))
|
|
1991
|
+
continue;
|
|
1992
|
+
const timestamp = Reflect.get(value, key);
|
|
1993
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp))
|
|
1994
|
+
throw new Error("Invalid observation timestamp");
|
|
1995
|
+
result[key] = timestamp;
|
|
1996
|
+
}
|
|
1997
|
+
return result;
|
|
1998
|
+
}
|
|
1999
|
+
function parseFailureDetails(value, identity) {
|
|
2000
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2001
|
+
return void 0;
|
|
2002
|
+
const isHttpRejection = "transportStage" in value && value.transportStage === "http_receive";
|
|
2003
|
+
const requestId = "requestId" in value ? value.requestId : isHttpRejection ? identity.requestId : void 0;
|
|
2004
|
+
const targetPeerId = "targetPeerId" in value ? value.targetPeerId : isHttpRejection ? identity.targetPeerId : void 0;
|
|
2005
|
+
if (typeof requestId !== "string" || typeof targetPeerId !== "string" || requestId !== identity.requestId || targetPeerId !== identity.targetPeerId || !("stage" in value) || !isRequestStage(value.stage) || !("outcome" in value) || value.outcome !== "not_executed" && value.outcome !== "unknown")
|
|
2006
|
+
return void 0;
|
|
2007
|
+
return {
|
|
2008
|
+
requestId,
|
|
2009
|
+
targetPeerId,
|
|
2010
|
+
stage: value.stage,
|
|
2011
|
+
outcome: value.outcome,
|
|
2012
|
+
...parseObservations(value),
|
|
2013
|
+
..."bytes" in value && typeof value.bytes === "number" ? { bytes: value.bytes } : {},
|
|
2014
|
+
..."limitBytes" in value && typeof value.limitBytes === "number" ? { limitBytes: value.limitBytes } : {},
|
|
2015
|
+
..."transportStage" in value && (value.transportStage === "server_send" || value.transportStage === "proxy_send" || value.transportStage === "http_receive") ? { transportStage: value.transportStage } : {}
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
1946
2018
|
var RoutingFailure = class extends Error {
|
|
1947
2019
|
routingError;
|
|
1948
2020
|
constructor(routingError) {
|
|
@@ -1970,11 +2042,16 @@ function toPublicPeer(peer) {
|
|
|
1970
2042
|
};
|
|
1971
2043
|
}
|
|
1972
2044
|
var STALE_PEER_MS = 3e4;
|
|
1973
|
-
var
|
|
1974
|
-
var
|
|
2045
|
+
var OPERATION_RETENTION_MS = 5 * 6e4;
|
|
2046
|
+
var MAX_OPERATION_RECORDS = 32768;
|
|
2047
|
+
var MAX_RETAINED_RESULTS = 1024;
|
|
2048
|
+
var MAX_RETAINED_RESULT_BYTES = 64 * 1024 * 1024;
|
|
2049
|
+
var MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
2050
|
+
var MAX_PENDING_REQUESTS = 1024;
|
|
2051
|
+
var MAX_PENDING_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
1975
2052
|
var CANCELLATION_TOMBSTONE_TTL_MS = 6e4;
|
|
1976
2053
|
var MAX_CANCELLATION_TOMBSTONES = 4096;
|
|
1977
|
-
var
|
|
2054
|
+
var MAX_OUTSTANDING_REQUESTS_PER_TRANSPORT = 4;
|
|
1978
2055
|
function roleOrder(role) {
|
|
1979
2056
|
if (role === "edit")
|
|
1980
2057
|
return 0;
|
|
@@ -2005,7 +2082,10 @@ function copyGroup(group) {
|
|
|
2005
2082
|
}
|
|
2006
2083
|
var BridgeService = class {
|
|
2007
2084
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
2008
|
-
|
|
2085
|
+
operations = /* @__PURE__ */ new Map();
|
|
2086
|
+
retainedResults = /* @__PURE__ */ new Map();
|
|
2087
|
+
retainedResultBytes = 0;
|
|
2088
|
+
pendingRequestBytes = 0;
|
|
2009
2089
|
pendingCancellations = /* @__PURE__ */ new Map();
|
|
2010
2090
|
peersById = /* @__PURE__ */ new Map();
|
|
2011
2091
|
multiplayerGroupsById = /* @__PURE__ */ new Map();
|
|
@@ -2048,13 +2128,25 @@ var BridgeService = class {
|
|
|
2048
2128
|
this.deliveryOwnersByTransportPeer.set(transportPeerId, owners);
|
|
2049
2129
|
}
|
|
2050
2130
|
owners.add(owner);
|
|
2131
|
+
for (const operation of this.operations.values()) {
|
|
2132
|
+
if (operation.transportPeerId === transportPeerId && operation.status.state !== "settled" && operation.status.connectionLostAt !== void 0 && operation.status.connectionRestoredAt === void 0) {
|
|
2133
|
+
operation.status.connectionRestoredAt = Date.now();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2051
2136
|
return;
|
|
2052
2137
|
}
|
|
2053
2138
|
if (!owners)
|
|
2054
2139
|
return;
|
|
2055
2140
|
owners.delete(owner);
|
|
2056
|
-
if (owners.size === 0)
|
|
2141
|
+
if (owners.size === 0) {
|
|
2057
2142
|
this.deliveryOwnersByTransportPeer.delete(transportPeerId);
|
|
2143
|
+
for (const operation of this.operations.values()) {
|
|
2144
|
+
if (operation.transportPeerId !== transportPeerId || operation.status.state === "settled")
|
|
2145
|
+
continue;
|
|
2146
|
+
operation.status.connectionLostAt = Date.now();
|
|
2147
|
+
delete operation.status.connectionRestoredAt;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2058
2150
|
}
|
|
2059
2151
|
notifyRequestAvailable(transportPeerId) {
|
|
2060
2152
|
for (const listener of this.requestAvailableListeners) {
|
|
@@ -2285,8 +2377,7 @@ var BridgeService = class {
|
|
|
2285
2377
|
if (request.targetPeerId !== peerId)
|
|
2286
2378
|
continue;
|
|
2287
2379
|
const deliveryTransportPeerId = request.lastDeliveryTransportPeerId;
|
|
2288
|
-
this.
|
|
2289
|
-
request.reject(new Error(`Target Peer "${peerId}" disconnected`));
|
|
2380
|
+
this.endRequestWaiter(request, "disconnected", `Target Peer "${peerId}" disconnected`);
|
|
2290
2381
|
if (deliveryTransportPeerId)
|
|
2291
2382
|
this.notifyRequestAvailable(deliveryTransportPeerId);
|
|
2292
2383
|
}
|
|
@@ -2545,44 +2636,126 @@ var BridgeService = class {
|
|
|
2545
2636
|
const peers = this.getPeers().filter((peer) => this.peerScopeKey(peer) === onlyScope);
|
|
2546
2637
|
return this.resolveWithinScope(peers, input.target, errorData);
|
|
2547
2638
|
}
|
|
2548
|
-
sendRequest(endpoint, data, targetPeerId, timeoutMs = this.requestTimeout, signal) {
|
|
2549
|
-
const requestId = randomUUID();
|
|
2639
|
+
sendRequest(endpoint, data, targetPeerId, timeoutMs = this.requestTimeout, signal, operationId) {
|
|
2640
|
+
const requestId = operationId ?? randomUUID();
|
|
2550
2641
|
const effectiveTimeoutMs = Math.max(1, timeoutMs);
|
|
2551
|
-
|
|
2552
|
-
|
|
2642
|
+
const details = { requestId, targetPeerId, stage: "queued", outcome: "not_executed", executionOutcome: "not_executed" };
|
|
2643
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0 || requestId.length > 128) {
|
|
2644
|
+
return Promise.reject(new RequestFailure("operationId must be a nonempty string of at most 128 characters", "invalid_operation_id", details));
|
|
2645
|
+
}
|
|
2646
|
+
if (signal?.aborted) {
|
|
2647
|
+
return Promise.reject(new RequestFailure(`Request aborted: ${requestId}; queued; not_executed`, "request_aborted", details));
|
|
2648
|
+
}
|
|
2649
|
+
let requestBytes;
|
|
2650
|
+
let fingerprint;
|
|
2651
|
+
try {
|
|
2652
|
+
const target2 = this.getPeerById(targetPeerId);
|
|
2653
|
+
fingerprint = createHash("sha256").update(JSON.stringify({ targetPeerId, endpoint, data })).digest("hex");
|
|
2654
|
+
requestBytes = Buffer.byteLength(JSON.stringify({
|
|
2655
|
+
kind: "request",
|
|
2656
|
+
requestId,
|
|
2657
|
+
peerId: targetPeerId,
|
|
2658
|
+
target: target2?.role,
|
|
2659
|
+
endpoint,
|
|
2660
|
+
data: data ?? null,
|
|
2661
|
+
remainingMs: effectiveTimeoutMs
|
|
2662
|
+
}));
|
|
2663
|
+
} catch {
|
|
2664
|
+
return Promise.reject(new RequestFailure(`Request ${requestId} cannot be serialized; queued; not_executed`, "request_serialization_failed", details));
|
|
2665
|
+
}
|
|
2666
|
+
this.pruneOperations(Date.now());
|
|
2667
|
+
const existing = this.operations.get(requestId);
|
|
2668
|
+
if (existing) {
|
|
2669
|
+
if (existing.fingerprint !== fingerprint) {
|
|
2670
|
+
return Promise.reject(new RequestFailure(`Request ${requestId} already identifies a different operation; existing operation ${existing.status.stage}; not replayed`, "operation_id_collision", { requestId, targetPeerId: existing.status.targetPeerId, stage: existing.status.stage, outcome: "unknown", ...observations(existing.status) }));
|
|
2671
|
+
}
|
|
2672
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2673
|
+
if (pending)
|
|
2674
|
+
return pending.promise;
|
|
2675
|
+
const status = this.getRequestStatus(requestId);
|
|
2676
|
+
if (status.state === "settled" && !status.resultUnavailable) {
|
|
2677
|
+
const error = status.error;
|
|
2678
|
+
if (error && typeof error === "object" && "name" in error && error.name === "RequestFailure" && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string" && "details" in error) {
|
|
2679
|
+
const failureDetails = parseFailureDetails(error.details, { requestId, targetPeerId });
|
|
2680
|
+
if (failureDetails)
|
|
2681
|
+
return Promise.reject(new RequestFailure(error.message, error.code, failureDetails));
|
|
2682
|
+
}
|
|
2683
|
+
return Object.hasOwn(status, "error") ? Promise.reject(status.error) : Promise.resolve(status.response);
|
|
2684
|
+
}
|
|
2685
|
+
return Promise.reject(new RequestFailure(`Request ${requestId} already exists: ${status.state}; ${status.stage}; ${status.outcome}; use get_request_status; not replayed`, "operation_not_replayed", { requestId, targetPeerId, stage: status.stage, outcome: status.executionOutcome === "not_executed" ? "not_executed" : "unknown", ...observations(status) }));
|
|
2686
|
+
}
|
|
2687
|
+
if (requestBytes > MAX_REQUEST_BYTES) {
|
|
2688
|
+
return Promise.reject(new RequestFailure(`Request ${requestId} is ${requestBytes} bytes at server_send; limit ${MAX_REQUEST_BYTES} bytes; queued; not_executed`, "request_too_large", { ...details, bytes: requestBytes, limitBytes: MAX_REQUEST_BYTES, transportStage: "server_send" }));
|
|
2689
|
+
}
|
|
2690
|
+
if (this.pendingRequests.size >= MAX_PENDING_REQUESTS || this.pendingRequestBytes + requestBytes > MAX_PENDING_REQUEST_BYTES) {
|
|
2691
|
+
return Promise.reject(new RequestFailure(`Request ${requestId} rejected at admission: pending capacity exceeded (${this.pendingRequests.size}/${MAX_PENDING_REQUESTS} requests, ${this.pendingRequestBytes + requestBytes}/${MAX_PENDING_REQUEST_BYTES} bytes); queued; not_executed`, "request_capacity_exceeded", { ...details, bytes: this.pendingRequestBytes + requestBytes, limitBytes: MAX_PENDING_REQUEST_BYTES }));
|
|
2692
|
+
}
|
|
2553
2693
|
const { promise, resolve: resolve5, reject } = Promise.withResolvers();
|
|
2554
|
-
const
|
|
2694
|
+
const abortListener = () => {
|
|
2555
2695
|
const pending = this.pendingRequests.get(requestId);
|
|
2556
|
-
if (
|
|
2557
|
-
|
|
2558
|
-
this.notifyRequestCancelled(pending, reason);
|
|
2559
|
-
pending.reject(error);
|
|
2696
|
+
if (pending)
|
|
2697
|
+
this.endRequestWaiter(pending, "aborted", "Request aborted");
|
|
2560
2698
|
};
|
|
2561
|
-
const timeoutId = setTimeout(() =>
|
|
2562
|
-
|
|
2699
|
+
const timeoutId = setTimeout(() => {
|
|
2700
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2701
|
+
if (pending)
|
|
2702
|
+
this.endRequestWaiter(pending, "timed_out", "Request timeout");
|
|
2703
|
+
}, effectiveTimeoutMs);
|
|
2704
|
+
const now = Date.now();
|
|
2563
2705
|
const request = {
|
|
2564
2706
|
id: requestId,
|
|
2565
2707
|
endpoint,
|
|
2566
2708
|
data,
|
|
2567
2709
|
targetPeerId,
|
|
2568
|
-
timestamp:
|
|
2710
|
+
timestamp: now,
|
|
2569
2711
|
resolve: resolve5,
|
|
2570
2712
|
reject,
|
|
2713
|
+
promise,
|
|
2571
2714
|
timeoutId,
|
|
2572
2715
|
timeoutMs: effectiveTimeoutMs,
|
|
2716
|
+
requestBytes,
|
|
2573
2717
|
abortSignal: signal,
|
|
2574
2718
|
abortListener
|
|
2575
2719
|
};
|
|
2576
2720
|
this.pendingRequests.set(requestId, request);
|
|
2721
|
+
this.pendingRequestBytes += requestBytes;
|
|
2722
|
+
this.operations.set(requestId, {
|
|
2723
|
+
status: { requestId, targetPeerId, queuedAt: now, stage: "queued", state: "pending", outcome: "pending", executionOutcome: "unknown" },
|
|
2724
|
+
fingerprint,
|
|
2725
|
+
updatedAt: now,
|
|
2726
|
+
resultBytes: 0
|
|
2727
|
+
});
|
|
2728
|
+
this.pruneOperations(now);
|
|
2577
2729
|
signal?.addEventListener("abort", abortListener, { once: true });
|
|
2578
2730
|
if (signal?.aborted)
|
|
2579
2731
|
abortListener();
|
|
2580
2732
|
const target = this.getPeerById(targetPeerId);
|
|
2581
|
-
if (this.pendingRequests.has(requestId) && target)
|
|
2733
|
+
if (this.pendingRequests.has(requestId) && target)
|
|
2582
2734
|
this.notifyRequestAvailable(target.transportPeerId);
|
|
2583
|
-
}
|
|
2584
2735
|
return promise;
|
|
2585
2736
|
}
|
|
2737
|
+
endRequestWaiter(request, state, message) {
|
|
2738
|
+
if (!this.removePendingRequest(request))
|
|
2739
|
+
return;
|
|
2740
|
+
const operation = this.operations.get(request.id);
|
|
2741
|
+
const stage = operation?.status.stage ?? (request.lastDeliveryTransportPeerId ? "dispatched" : "queued");
|
|
2742
|
+
const outcome = stage === "queued" || operation?.status.executionOutcome === "not_executed" ? "not_executed" : "unknown";
|
|
2743
|
+
if (operation) {
|
|
2744
|
+
operation.status.state = state;
|
|
2745
|
+
operation.status.outcome = outcome;
|
|
2746
|
+
if (stage === "queued")
|
|
2747
|
+
operation.status.executionOutcome = "not_executed";
|
|
2748
|
+
operation.status.waiterEndedAt = Date.now();
|
|
2749
|
+
operation.updatedAt = Date.now();
|
|
2750
|
+
this.operations.delete(request.id);
|
|
2751
|
+
this.operations.set(request.id, operation);
|
|
2752
|
+
}
|
|
2753
|
+
if (state !== "disconnected") {
|
|
2754
|
+
this.notifyRequestCancelled(request, state === "timed_out" ? "timeout" : "aborted");
|
|
2755
|
+
}
|
|
2756
|
+
const connectionLost = operation?.status.connectionLostAt !== void 0 && operation.status.connectionRestoredAt === void 0;
|
|
2757
|
+
request.reject(new RequestFailure(`${message}: ${request.id}; ${stage}; ${outcome}${connectionLost ? "; connection lost" : ""}; waiter ended, execution is not cancelled or rolled back`, state === "timed_out" ? connectionLost ? "request_connection_lost" : "request_timeout" : `request_${state}`, { requestId: request.id, targetPeerId: request.targetPeerId, stage, outcome, ...operation ? observations(operation.status) : {} }));
|
|
2758
|
+
}
|
|
2586
2759
|
removePendingRequest(request) {
|
|
2587
2760
|
if (this.pendingRequests.get(request.id) !== request)
|
|
2588
2761
|
return false;
|
|
@@ -2591,15 +2764,16 @@ var BridgeService = class {
|
|
|
2591
2764
|
request.abortSignal.removeEventListener("abort", request.abortListener);
|
|
2592
2765
|
}
|
|
2593
2766
|
this.pendingRequests.delete(request.id);
|
|
2767
|
+
this.pendingRequestBytes -= request.requestBytes;
|
|
2594
2768
|
return true;
|
|
2595
2769
|
}
|
|
2596
2770
|
claimNextRequestForTransport(transportPeerId, claimOwner) {
|
|
2597
2771
|
let outstandingCount = 0;
|
|
2598
2772
|
for (const request of this.pendingRequests.values()) {
|
|
2599
|
-
if (request.
|
|
2773
|
+
if (request.lastDeliveryTransportPeerId === transportPeerId)
|
|
2600
2774
|
outstandingCount++;
|
|
2601
2775
|
}
|
|
2602
|
-
if (outstandingCount >=
|
|
2776
|
+
if (outstandingCount >= MAX_OUTSTANDING_REQUESTS_PER_TRANSPORT)
|
|
2603
2777
|
return null;
|
|
2604
2778
|
let oldestRequest;
|
|
2605
2779
|
for (const request of this.pendingRequests.values()) {
|
|
@@ -2618,6 +2792,13 @@ var BridgeService = class {
|
|
|
2618
2792
|
return null;
|
|
2619
2793
|
oldestRequest.claimOwner = claimOwner;
|
|
2620
2794
|
oldestRequest.lastDeliveryTransportPeerId = transportPeerId;
|
|
2795
|
+
const operation = this.operations.get(oldestRequest.id);
|
|
2796
|
+
if (operation) {
|
|
2797
|
+
operation.transportPeerId = transportPeerId;
|
|
2798
|
+
operation.status.stage = "dispatched";
|
|
2799
|
+
operation.status.dispatchedAt = Date.now();
|
|
2800
|
+
operation.updatedAt = Date.now();
|
|
2801
|
+
}
|
|
2621
2802
|
return {
|
|
2622
2803
|
requestId: oldestRequest.id,
|
|
2623
2804
|
peerId: oldestRequest.targetPeerId,
|
|
@@ -2640,14 +2821,6 @@ var BridgeService = class {
|
|
|
2640
2821
|
}
|
|
2641
2822
|
releaseDeliveryClaims(claimOwner) {
|
|
2642
2823
|
const transportPeerIds = /* @__PURE__ */ new Set();
|
|
2643
|
-
for (const request of this.pendingRequests.values()) {
|
|
2644
|
-
if (request.claimOwner !== claimOwner)
|
|
2645
|
-
continue;
|
|
2646
|
-
request.claimOwner = void 0;
|
|
2647
|
-
const peer = this.getPeerById(request.targetPeerId);
|
|
2648
|
-
if (peer)
|
|
2649
|
-
transportPeerIds.add(peer.transportPeerId);
|
|
2650
|
-
}
|
|
2651
2824
|
for (const cancellation of this.pendingCancellations.values()) {
|
|
2652
2825
|
if (cancellation.claimOwner !== claimOwner)
|
|
2653
2826
|
continue;
|
|
@@ -2657,38 +2830,169 @@ var BridgeService = class {
|
|
|
2657
2830
|
for (const transportPeerId of transportPeerIds)
|
|
2658
2831
|
this.notifyRequestAvailable(transportPeerId);
|
|
2659
2832
|
}
|
|
2833
|
+
ownedOperation(transportPeerId, requestId) {
|
|
2834
|
+
this.pruneOperations(Date.now());
|
|
2835
|
+
const operation = this.operations.get(requestId);
|
|
2836
|
+
if (!operation || operation.transportPeerId !== transportPeerId)
|
|
2837
|
+
return void 0;
|
|
2838
|
+
const peer = this.getPeerById(operation.status.targetPeerId);
|
|
2839
|
+
const transport = this.getPeerById(transportPeerId);
|
|
2840
|
+
return peer?.transportPeerId === transportPeerId && transport?.transportPeerId === transportPeerId ? operation : void 0;
|
|
2841
|
+
}
|
|
2842
|
+
observeTransportProgress(transportPeerId, requestId, phase, outcome) {
|
|
2843
|
+
const operation = this.ownedOperation(transportPeerId, requestId);
|
|
2844
|
+
if (!operation || operation.status.state === "settled" || operation.status.stage === "response_delivery" || phase === "executing" && operation.status.stage !== "dispatched")
|
|
2845
|
+
return;
|
|
2846
|
+
const now = Date.now();
|
|
2847
|
+
operation.status.stage = phase;
|
|
2848
|
+
if (phase === "executing")
|
|
2849
|
+
operation.status.executionStartedAt = now;
|
|
2850
|
+
else {
|
|
2851
|
+
operation.status.executionOutcome = outcome ?? "unknown";
|
|
2852
|
+
if (outcome !== "not_executed")
|
|
2853
|
+
operation.status.executionCompletedAt = now;
|
|
2854
|
+
}
|
|
2855
|
+
operation.updatedAt = now;
|
|
2856
|
+
this.operations.delete(requestId);
|
|
2857
|
+
this.operations.set(requestId, operation);
|
|
2858
|
+
}
|
|
2859
|
+
settleTransportResponse(transportPeerId, requestId, response, error, executionOutcome) {
|
|
2860
|
+
const operation = this.ownedOperation(transportPeerId, requestId);
|
|
2861
|
+
if (!operation)
|
|
2862
|
+
return "unknown";
|
|
2863
|
+
if (error !== void 0 && executionOutcome !== void 0) {
|
|
2864
|
+
error = new RequestFailure(typeof error === "string" ? error : "Studio response failed", "studio_response_error", {
|
|
2865
|
+
requestId,
|
|
2866
|
+
targetPeerId: operation.status.targetPeerId,
|
|
2867
|
+
...observations(operation.status),
|
|
2868
|
+
executionCompletedAt: executionOutcome === "not_executed" ? void 0 : operation.status.executionCompletedAt ?? Date.now(),
|
|
2869
|
+
stage: "response_delivery",
|
|
2870
|
+
outcome: executionOutcome === "not_executed" ? "not_executed" : "unknown",
|
|
2871
|
+
executionOutcome
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
return this.recordResponse(requestId, response, error, executionOutcome);
|
|
2875
|
+
}
|
|
2876
|
+
/** Trusted in-process settlement; transport handlers must use settleTransportResponse. */
|
|
2660
2877
|
resolveRequest(requestId, response) {
|
|
2661
|
-
return this.
|
|
2878
|
+
return this.recordResponse(requestId, response);
|
|
2662
2879
|
}
|
|
2880
|
+
/** Trusted in-process settlement; transport handlers must use settleTransportResponse. */
|
|
2663
2881
|
rejectRequest(requestId, error) {
|
|
2664
|
-
return this.
|
|
2882
|
+
return this.recordResponse(requestId, void 0, error);
|
|
2665
2883
|
}
|
|
2666
|
-
|
|
2884
|
+
recordResponse(requestId, response, error, executionOutcome) {
|
|
2667
2885
|
const now = Date.now();
|
|
2668
|
-
this.
|
|
2886
|
+
this.pruneOperations(now);
|
|
2887
|
+
const operation = this.operations.get(requestId);
|
|
2888
|
+
if (!operation)
|
|
2889
|
+
return "unknown";
|
|
2890
|
+
if (operation.status.state === "settled")
|
|
2891
|
+
return "already_settled";
|
|
2892
|
+
const hasError = error !== void 0;
|
|
2893
|
+
operation.status.state = "settled";
|
|
2894
|
+
const localRejection = error instanceof RequestFailure && error.details.transportStage === "server_send";
|
|
2895
|
+
const responseOutcome = handlerOutcome(response);
|
|
2896
|
+
const completedOutcome = !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
|
|
2897
|
+
operation.status.executionOutcome = completedOutcome;
|
|
2898
|
+
operation.status.outcome = hasError || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
|
|
2899
|
+
if (!localRejection) {
|
|
2900
|
+
operation.status.stage = "response_delivery";
|
|
2901
|
+
if (completedOutcome !== "not_executed")
|
|
2902
|
+
operation.status.executionCompletedAt ??= now;
|
|
2903
|
+
}
|
|
2904
|
+
operation.status.settledAt = now;
|
|
2905
|
+
operation.updatedAt = now;
|
|
2906
|
+
try {
|
|
2907
|
+
const recordedError = error instanceof Error ? { ...error, name: error.name, message: error.message } : error;
|
|
2908
|
+
const serialized = JSON.stringify(hasError ? { error: recordedError } : { response });
|
|
2909
|
+
const bytes = Buffer.byteLength(serialized);
|
|
2910
|
+
if (bytes > MAX_RETAINED_RESULT_BYTES) {
|
|
2911
|
+
operation.status.resultUnavailable = { reason: "size_limit", bytes, limitBytes: MAX_RETAINED_RESULT_BYTES };
|
|
2912
|
+
} else {
|
|
2913
|
+
operation.serializedResult = serialized;
|
|
2914
|
+
operation.resultBytes = bytes;
|
|
2915
|
+
this.retainedResultBytes += bytes;
|
|
2916
|
+
this.retainedResults.set(requestId, operation);
|
|
2917
|
+
}
|
|
2918
|
+
} catch {
|
|
2919
|
+
operation.status.resultUnavailable = { reason: "serialization_failed", limitBytes: MAX_RETAINED_RESULT_BYTES };
|
|
2920
|
+
}
|
|
2921
|
+
this.operations.delete(requestId);
|
|
2922
|
+
this.operations.set(requestId, operation);
|
|
2669
2923
|
const request = this.pendingRequests.get(requestId);
|
|
2670
|
-
if (
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2924
|
+
if (request && this.removePendingRequest(request)) {
|
|
2925
|
+
if (hasError)
|
|
2926
|
+
request.reject(error);
|
|
2927
|
+
else
|
|
2928
|
+
request.resolve(response);
|
|
2929
|
+
}
|
|
2930
|
+
this.pendingCancellations.delete(requestId);
|
|
2931
|
+
this.pruneOperations(now);
|
|
2932
|
+
if (operation.transportPeerId)
|
|
2933
|
+
this.notifyRequestAvailable(operation.transportPeerId);
|
|
2679
2934
|
return "accepted";
|
|
2680
2935
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2936
|
+
getRequestStatus(requestId) {
|
|
2937
|
+
this.pruneOperations(Date.now());
|
|
2938
|
+
const operation = this.operations.get(requestId);
|
|
2939
|
+
if (!operation)
|
|
2940
|
+
return void 0;
|
|
2941
|
+
const status = { ...operation.status };
|
|
2942
|
+
if (status.resultUnavailable)
|
|
2943
|
+
status.resultUnavailable = { ...status.resultUnavailable };
|
|
2944
|
+
if (operation.serializedResult !== void 0) {
|
|
2945
|
+
const result = JSON.parse(operation.serializedResult);
|
|
2946
|
+
if (result && typeof result === "object") {
|
|
2947
|
+
if ("response" in result)
|
|
2948
|
+
status.response = result.response;
|
|
2949
|
+
if ("error" in result)
|
|
2950
|
+
status.error = result.error;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
return status;
|
|
2954
|
+
}
|
|
2955
|
+
async getRequestStatusEverywhere(requestId) {
|
|
2956
|
+
return this.getRequestStatus(requestId);
|
|
2957
|
+
}
|
|
2958
|
+
pruneOperations(now) {
|
|
2959
|
+
for (const [requestId, operation] of this.operations) {
|
|
2960
|
+
if (this.pendingRequests.has(requestId))
|
|
2961
|
+
continue;
|
|
2962
|
+
if (now - operation.updatedAt < OPERATION_RETENTION_MS)
|
|
2963
|
+
break;
|
|
2964
|
+
this.operations.delete(requestId);
|
|
2965
|
+
this.retainedResults.delete(requestId);
|
|
2966
|
+
this.retainedResultBytes -= operation.resultBytes;
|
|
2967
|
+
}
|
|
2968
|
+
while (this.operations.size > MAX_OPERATION_RECORDS) {
|
|
2969
|
+
let removed = false;
|
|
2970
|
+
for (const [requestId, operation] of this.operations) {
|
|
2971
|
+
if (this.pendingRequests.has(requestId))
|
|
2972
|
+
continue;
|
|
2973
|
+
this.operations.delete(requestId);
|
|
2974
|
+
this.retainedResults.delete(requestId);
|
|
2975
|
+
this.retainedResultBytes -= operation.resultBytes;
|
|
2976
|
+
removed = true;
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
if (!removed)
|
|
2684
2980
|
break;
|
|
2685
|
-
this.acceptedRequestIds.delete(requestId);
|
|
2686
2981
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2982
|
+
if (this.retainedResults.size <= MAX_RETAINED_RESULTS && this.retainedResultBytes <= MAX_RETAINED_RESULT_BYTES)
|
|
2983
|
+
return;
|
|
2984
|
+
for (const [requestId, operation] of this.retainedResults) {
|
|
2985
|
+
operation.status.resultUnavailable = {
|
|
2986
|
+
reason: "retention_capacity",
|
|
2987
|
+
bytes: operation.resultBytes,
|
|
2988
|
+
limitBytes: MAX_RETAINED_RESULT_BYTES
|
|
2989
|
+
};
|
|
2990
|
+
this.retainedResultBytes -= operation.resultBytes;
|
|
2991
|
+
operation.resultBytes = 0;
|
|
2992
|
+
operation.serializedResult = void 0;
|
|
2993
|
+
this.retainedResults.delete(requestId);
|
|
2994
|
+
if (this.retainedResults.size <= MAX_RETAINED_RESULTS && this.retainedResultBytes <= MAX_RETAINED_RESULT_BYTES)
|
|
2690
2995
|
break;
|
|
2691
|
-
this.acceptedRequestIds.delete(oldestRequestId);
|
|
2692
2996
|
}
|
|
2693
2997
|
}
|
|
2694
2998
|
prunePendingCancellations(now) {
|
|
@@ -2707,16 +3011,15 @@ var BridgeService = class {
|
|
|
2707
3011
|
cleanupOldRequests() {
|
|
2708
3012
|
const now = Date.now();
|
|
2709
3013
|
for (const request of this.pendingRequests.values()) {
|
|
2710
|
-
if (now - request.timestamp
|
|
2711
|
-
this.
|
|
2712
|
-
request.reject(new Error("Request timeout"));
|
|
3014
|
+
if (now - request.timestamp >= request.timeoutMs) {
|
|
3015
|
+
this.endRequestWaiter(request, "timed_out", "Request timeout");
|
|
2713
3016
|
}
|
|
2714
3017
|
}
|
|
3018
|
+
this.pruneOperations(now);
|
|
2715
3019
|
}
|
|
2716
3020
|
clearAllPendingRequests() {
|
|
2717
3021
|
for (const request of Array.from(this.pendingRequests.values())) {
|
|
2718
|
-
this.
|
|
2719
|
-
request.reject(new Error("Connection closed"));
|
|
3022
|
+
this.endRequestWaiter(request, "disconnected", "Connection closed");
|
|
2720
3023
|
}
|
|
2721
3024
|
this.pendingCancellations.clear();
|
|
2722
3025
|
}
|
|
@@ -2981,7 +3284,7 @@ Tool descriptions explain selection. Input schemas explain arguments. This guide
|
|
|
2981
3284
|
|
|
2982
3285
|
- Use selection with action=get when the user's Studio selection should define the scope.
|
|
2983
3286
|
- Use action=set with instance paths to replace, add to, or remove from the selection. An empty paths array in set mode clears it.
|
|
2984
|
-
- Use action=view with a BasePart or Model path to frame it. The current viewing direction is preserved unless from or angleY overrides it. padding below 1 crops closer and above 1 pulls back.
|
|
3287
|
+
- Use action=view with a BasePart or Model path to frame it, or omit path to frame exactly one selected BasePart or Model. Empty, multiple, or unsupported selections require an explicit target or a changed selection. The original camera type is restored after framing, including on failure. The current viewing direction is preserved unless from or angleY overrides it. padding below 1 crops closer and above 1 pulls back.
|
|
2985
3288
|
- For visual proof, change the instance, frame it with selection, then call capture_screenshot.
|
|
2986
3289
|
|
|
2987
3290
|
## Script changes
|
|
@@ -4471,6 +4774,21 @@ async function resolveStudioExeAsync() {
|
|
|
4471
4774
|
}
|
|
4472
4775
|
return candidates[0];
|
|
4473
4776
|
}
|
|
4777
|
+
var WINDOWS_STUDIO_PROCESS_QUERY = [
|
|
4778
|
+
"$ErrorActionPreference = 'Stop'",
|
|
4779
|
+
// Get-Process reports a missing name as an error, not a successful empty set.
|
|
4780
|
+
'$studio = @(); try { $studio = @(Get-Process RobloxStudioBeta -ErrorAction Stop) } catch { if ($_.FullyQualifiedErrorId -notlike "NoProcessFoundForGivenName,*") { throw } }',
|
|
4781
|
+
"$processes = @($studio | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } })",
|
|
4782
|
+
"ConvertTo-Json -InputObject $processes -Compress"
|
|
4783
|
+
].join("; ");
|
|
4784
|
+
function parseWindowsStudioProcesses(output) {
|
|
4785
|
+
const parsed = JSON.parse(output);
|
|
4786
|
+
const processes = Array.isArray(parsed) ? parsed : [parsed];
|
|
4787
|
+
if (!processes.every((value) => value !== null && typeof value === "object" && "Id" in value && typeof value.Id === "number" && Number.isSafeInteger(value.Id) && value.Id > 0 && "Name" in value && typeof value.Name === "string" && "Path" in value && typeof value.Path === "string" && "MainWindowTitle" in value && typeof value.MainWindowTitle === "string" && "StartTimeUtcFileTime" in value && typeof value.StartTimeUtcFileTime === "string" && /^[1-9]\d*$/u.test(value.StartTimeUtcFileTime))) {
|
|
4788
|
+
throw new Error("Malformed Roblox Studio process enumeration result.");
|
|
4789
|
+
}
|
|
4790
|
+
return processes;
|
|
4791
|
+
}
|
|
4474
4792
|
async function observeStudioProcesses() {
|
|
4475
4793
|
const observedAt = Date.now();
|
|
4476
4794
|
try {
|
|
@@ -4493,11 +4811,8 @@ async function observeStudioProcesses() {
|
|
|
4493
4811
|
if (process.platform !== "win32" && !isWsl()) {
|
|
4494
4812
|
return { status: "ok", observedAt, processes: [] };
|
|
4495
4813
|
}
|
|
4496
|
-
const out = await powershellAsync(
|
|
4497
|
-
|
|
4498
|
-
return { status: "ok", observedAt, processes: [] };
|
|
4499
|
-
const parsed = JSON.parse(out);
|
|
4500
|
-
return { status: "ok", observedAt, processes: Array.isArray(parsed) ? parsed : [parsed] };
|
|
4814
|
+
const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY);
|
|
4815
|
+
return { status: "ok", observedAt, processes: parseWindowsStudioProcesses(out) };
|
|
4501
4816
|
} catch (error) {
|
|
4502
4817
|
return {
|
|
4503
4818
|
status: "error",
|
|
@@ -5529,7 +5844,6 @@ var INTERNAL_RESULT_KEYS = /* @__PURE__ */ new Set([
|
|
|
5529
5844
|
"transportPeerId",
|
|
5530
5845
|
"pluginVariant",
|
|
5531
5846
|
"pluginVersion",
|
|
5532
|
-
"requestId",
|
|
5533
5847
|
"serverVersion"
|
|
5534
5848
|
]);
|
|
5535
5849
|
var TEXT_RESULT_TOOLS = /* @__PURE__ */ new Set(["get_roblox_docs"]);
|
|
@@ -5651,8 +5965,11 @@ function publicToolErrorBody(name, error) {
|
|
|
5651
5965
|
}
|
|
5652
5966
|
if (error instanceof RoutingFailure)
|
|
5653
5967
|
return publicRoutingError(error);
|
|
5968
|
+
if (error instanceof RequestFailure) {
|
|
5969
|
+
return { error: error.code, message: error.message.slice(0, 500), ...error.details };
|
|
5970
|
+
}
|
|
5654
5971
|
console.error(`[tool:${name}]`, error);
|
|
5655
|
-
const message = error instanceof Error ? error.message : "Tool execution failed.";
|
|
5972
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Tool execution failed.";
|
|
5656
5973
|
return { error: "tool_failed", message: message.slice(0, 500) };
|
|
5657
5974
|
}
|
|
5658
5975
|
function concise(text, maxLength, firstSentence = false) {
|
|
@@ -5709,6 +6026,9 @@ function serverInstructions(definitions) {
|
|
|
5709
6026
|
if (has("get_connected_instances")) {
|
|
5710
6027
|
instructions.push("When more than one Studio process scope is connected, call get_connected_instances and pass either a top-level instance id or a multiplayer group's role-suffixed instance id as instance_id.");
|
|
5711
6028
|
}
|
|
6029
|
+
if (has("get_request_status", "execute_luau", "set_properties")) {
|
|
6030
|
+
instructions.push("Supply a unique operation_id to execute_luau or set_properties when retry safety matters. After a timeout, query get_request_status with that ID before retrying. Identical arguments reuse a retained outcome; changed arguments are rejected. Recovery and deduplication are bounded to the current server session and five-minute retention window; result payloads may be evicted earlier. Unknown status does not mean unexecuted, and cancellation cannot roll back mutations.", "Request stages are queued, dispatched, executing (plugin handler entered), and response_delivery (handler returned or admission rejected). executionOutcome is separate from waiter state and delivery outcome; handler observations do not prove user Luau instructions ran. A waiter timeout is not an execution deadline or rollback. Neither missing progress nor connection loss proves completion.");
|
|
6031
|
+
}
|
|
5712
6032
|
if (has("search_objects", "get_project_structure", "grep_scripts", "execute_luau")) {
|
|
5713
6033
|
instructions.push("Use search_objects, get_project_structure, or grep_scripts for standard discovery. Use execute_luau for custom traversal or bulk edits.");
|
|
5714
6034
|
}
|
|
@@ -5771,7 +6091,7 @@ function createToolHttpHandler(options) {
|
|
|
5771
6091
|
}
|
|
5772
6092
|
|
|
5773
6093
|
// ../core/dist/auth.js
|
|
5774
|
-
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
6094
|
+
import { randomBytes, createHash as createHash2, timingSafeEqual } from "crypto";
|
|
5775
6095
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, chmodSync } from "fs";
|
|
5776
6096
|
import { join as join3, dirname as dirname2 } from "path";
|
|
5777
6097
|
import { homedir as homedir3 } from "os";
|
|
@@ -5809,192 +6129,308 @@ function resolveAuthToken() {
|
|
|
5809
6129
|
}
|
|
5810
6130
|
}
|
|
5811
6131
|
function tokensMatch(provided, expected) {
|
|
5812
|
-
const a =
|
|
5813
|
-
const b =
|
|
6132
|
+
const a = createHash2("sha256").update(provided).digest();
|
|
6133
|
+
const b = createHash2("sha256").update(expected).digest();
|
|
5814
6134
|
return timingSafeEqual(a, b);
|
|
5815
6135
|
}
|
|
5816
6136
|
|
|
6137
|
+
// ../core/dist/http-body-limits.js
|
|
6138
|
+
var HTTP_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
6139
|
+
|
|
5817
6140
|
// ../core/dist/studio-transport.js
|
|
5818
6141
|
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
5819
|
-
var
|
|
5820
|
-
var
|
|
6142
|
+
var MAX_PENDING_ACKS = 128;
|
|
6143
|
+
var STUDIO_PROTOCOL_VERSION = 1;
|
|
6144
|
+
var MAX_ACTIVE_STUDIO_SOCKETS = 64;
|
|
6145
|
+
var MAX_STUDIO_FRAME_BYTES = 64 * 1024 * 1024;
|
|
6146
|
+
var MAX_STUDIO_BUFFERED_BYTES = MAX_STUDIO_FRAME_BYTES + 14;
|
|
6147
|
+
var WebSocketStudioTransport = class {
|
|
5821
6148
|
queue;
|
|
5822
|
-
|
|
6149
|
+
sockets = /* @__PURE__ */ new Map();
|
|
6150
|
+
closing = /* @__PURE__ */ new Set();
|
|
5823
6151
|
unsubscribeRequestAvailable;
|
|
5824
6152
|
unsubscribePeerClosed;
|
|
5825
6153
|
nextGeneration = 0;
|
|
6154
|
+
closed = false;
|
|
5826
6155
|
constructor(queue) {
|
|
5827
6156
|
this.queue = queue;
|
|
5828
6157
|
this.unsubscribeRequestAvailable = queue.onRequestAvailable((transportPeerId) => {
|
|
5829
|
-
const
|
|
5830
|
-
if (
|
|
5831
|
-
this.pump(
|
|
6158
|
+
const connection = this.sockets.get(transportPeerId);
|
|
6159
|
+
if (connection)
|
|
6160
|
+
this.pump(connection);
|
|
5832
6161
|
});
|
|
5833
|
-
this.unsubscribePeerClosed = queue.onPeerClosed((
|
|
5834
|
-
if (
|
|
5835
|
-
this.closeTransport(
|
|
5836
|
-
}
|
|
6162
|
+
this.unsubscribePeerClosed = queue.onPeerClosed((peer) => {
|
|
6163
|
+
if (peer.peerId === peer.transportPeerId)
|
|
6164
|
+
this.closeTransport(peer.transportPeerId);
|
|
5837
6165
|
});
|
|
5838
6166
|
}
|
|
5839
|
-
get
|
|
5840
|
-
return this.
|
|
6167
|
+
get activeSocketCount() {
|
|
6168
|
+
return this.sockets.size;
|
|
5841
6169
|
}
|
|
5842
6170
|
canOpen(transportPeerId) {
|
|
5843
|
-
return this.
|
|
6171
|
+
return !this.closed && (this.sockets.has(transportPeerId) || this.sockets.size < MAX_ACTIVE_STUDIO_SOCKETS);
|
|
5844
6172
|
}
|
|
5845
|
-
open(transportPeerId,
|
|
5846
|
-
if (!this.canOpen(transportPeerId))
|
|
6173
|
+
open(transportPeerId, socket, status) {
|
|
6174
|
+
if (!this.canOpen(transportPeerId) || socket.readyState !== 1)
|
|
5847
6175
|
return void 0;
|
|
5848
6176
|
this.nextGeneration += 1;
|
|
5849
|
-
const claimOwner = `
|
|
5850
|
-
const
|
|
6177
|
+
const claimOwner = `ws:${transportPeerId}:${this.nextGeneration}`;
|
|
6178
|
+
const connection = {
|
|
5851
6179
|
transportPeerId,
|
|
5852
6180
|
claimOwner,
|
|
5853
|
-
|
|
6181
|
+
socket,
|
|
5854
6182
|
status,
|
|
5855
6183
|
closed: false,
|
|
5856
|
-
|
|
6184
|
+
sending: false,
|
|
6185
|
+
pumping: false,
|
|
6186
|
+
settling: false,
|
|
5857
6187
|
statusPending: true,
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
6188
|
+
heartbeatPending: false,
|
|
6189
|
+
acknowledgements: /* @__PURE__ */ new Map(),
|
|
6190
|
+
onClose: () => {
|
|
6191
|
+
this.closeSocket(connection);
|
|
6192
|
+
this.finishClose(connection);
|
|
6193
|
+
},
|
|
6194
|
+
onError: () => this.closeSocket(connection, 1011, "socket_error"),
|
|
6195
|
+
onMessage: (data, isBinary) => this.receive(connection, data, isBinary)
|
|
5865
6196
|
};
|
|
5866
6197
|
this.queue.setDeliveryActive(transportPeerId, claimOwner, true);
|
|
5867
|
-
this.
|
|
5868
|
-
const replaced = this.streams.get(transportPeerId);
|
|
6198
|
+
const replaced = this.sockets.get(transportPeerId);
|
|
5869
6199
|
if (replaced)
|
|
5870
|
-
this.
|
|
5871
|
-
this.
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
}
|
|
6200
|
+
this.closeSocket(replaced, 1012, "transport_replaced");
|
|
6201
|
+
this.sockets.set(transportPeerId, connection);
|
|
6202
|
+
this.queue.updatePeerActivity(transportPeerId);
|
|
6203
|
+
socket.on("close", connection.onClose);
|
|
6204
|
+
socket.on("error", connection.onError);
|
|
6205
|
+
socket.on("message", connection.onMessage);
|
|
6206
|
+
connection.heartbeatTimer = setInterval(() => {
|
|
6207
|
+
if (!this.isCurrent(connection))
|
|
6208
|
+
return;
|
|
6209
|
+
this.queue.updatePeerActivity(transportPeerId);
|
|
6210
|
+
connection.statusPending = true;
|
|
6211
|
+
connection.heartbeatPending = true;
|
|
6212
|
+
this.pump(connection);
|
|
5884
6213
|
}, HEARTBEAT_INTERVAL_MS);
|
|
5885
|
-
|
|
5886
|
-
this.pump(
|
|
5887
|
-
return {
|
|
5888
|
-
transportPeerId,
|
|
5889
|
-
close: () => this.closeStream(stream, true)
|
|
5890
|
-
};
|
|
6214
|
+
connection.heartbeatTimer.unref();
|
|
6215
|
+
this.pump(connection);
|
|
6216
|
+
return { transportPeerId, close: () => this.closeSocket(connection, 1e3, "transport_closed") };
|
|
5891
6217
|
}
|
|
5892
6218
|
refreshStatus(transportPeerId) {
|
|
5893
6219
|
if (transportPeerId !== void 0) {
|
|
5894
|
-
const
|
|
5895
|
-
if (
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
this.pump(stream);
|
|
6220
|
+
const connection = this.sockets.get(transportPeerId);
|
|
6221
|
+
if (connection) {
|
|
6222
|
+
connection.statusPending = true;
|
|
6223
|
+
this.pump(connection);
|
|
5899
6224
|
}
|
|
5900
6225
|
return;
|
|
5901
6226
|
}
|
|
5902
|
-
for (const
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
this.pump(stream);
|
|
6227
|
+
for (const connection of this.sockets.values()) {
|
|
6228
|
+
connection.statusPending = true;
|
|
6229
|
+
this.pump(connection);
|
|
5906
6230
|
}
|
|
5907
6231
|
}
|
|
5908
6232
|
closeTransport(transportPeerId) {
|
|
5909
|
-
const
|
|
5910
|
-
if (
|
|
5911
|
-
this.
|
|
6233
|
+
const connection = this.sockets.get(transportPeerId);
|
|
6234
|
+
if (connection)
|
|
6235
|
+
this.closeSocket(connection, 1e3, "peer_unregistered");
|
|
5912
6236
|
}
|
|
5913
6237
|
close() {
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
6238
|
+
this.closed = true;
|
|
6239
|
+
for (const connection of this.sockets.values())
|
|
6240
|
+
this.closeSocket(connection, 1001, "server_shutdown");
|
|
5917
6241
|
this.unsubscribeRequestAvailable();
|
|
5918
6242
|
this.unsubscribePeerClosed();
|
|
5919
6243
|
}
|
|
5920
|
-
|
|
5921
|
-
|
|
6244
|
+
isCurrent(connection) {
|
|
6245
|
+
return !connection.closed && this.sockets.get(connection.transportPeerId) === connection;
|
|
6246
|
+
}
|
|
6247
|
+
receive(connection, data, isBinary) {
|
|
6248
|
+
if (!this.isCurrent(connection))
|
|
6249
|
+
return;
|
|
6250
|
+
if (isBinary) {
|
|
6251
|
+
this.closeSocket(connection, 1003, "text_frames_required");
|
|
5922
6252
|
return;
|
|
5923
|
-
if (stream.statusPending) {
|
|
5924
|
-
stream.statusPending = false;
|
|
5925
|
-
let status;
|
|
5926
|
-
try {
|
|
5927
|
-
status = stream.status();
|
|
5928
|
-
} catch {
|
|
5929
|
-
this.closeStream(stream);
|
|
5930
|
-
return;
|
|
5931
|
-
}
|
|
5932
|
-
const statusJson = JSON.stringify(status);
|
|
5933
|
-
if (statusJson !== stream.lastStatusJson) {
|
|
5934
|
-
stream.lastStatusJson = statusJson;
|
|
5935
|
-
if (!this.write(stream, status))
|
|
5936
|
-
return;
|
|
5937
|
-
}
|
|
5938
6253
|
}
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6254
|
+
const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
|
|
6255
|
+
if (bytes > MAX_STUDIO_FRAME_BYTES) {
|
|
6256
|
+
this.closeSocket(connection, 1009, `server_receive bytes=${bytes} limit=${MAX_STUDIO_FRAME_BYTES}`);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
let message;
|
|
6260
|
+
try {
|
|
6261
|
+
const buffer = Array.isArray(data) ? Buffer.concat(data, bytes) : data instanceof ArrayBuffer ? Buffer.from(data) : data;
|
|
6262
|
+
message = JSON.parse(buffer.toString("utf8"));
|
|
6263
|
+
} catch {
|
|
6264
|
+
this.closeSocket(connection, 1007, "invalid_json");
|
|
6265
|
+
return;
|
|
6266
|
+
}
|
|
6267
|
+
if (message === null || typeof message !== "object" || Array.isArray(message) || !("kind" in message) || message.kind !== "response" && message.kind !== "progress" || !("requestId" in message) || typeof message.requestId !== "string" || message.requestId.length === 0 || message.requestId.length > 1024) {
|
|
6268
|
+
this.closeSocket(connection, 1008, "invalid_response");
|
|
6269
|
+
return;
|
|
6270
|
+
}
|
|
6271
|
+
if (message.kind === "progress") {
|
|
6272
|
+
const outcome = "outcome" in message ? message.outcome : void 0;
|
|
6273
|
+
if (!("phase" in message) || message.phase !== "executing" && message.phase !== "response_delivery" || outcome !== void 0 && !isExecutionOutcome(outcome) || message.phase === "executing" && "outcome" in message)
|
|
5944
6274
|
return;
|
|
6275
|
+
this.queue.observeTransportProgress(connection.transportPeerId, message.requestId, message.phase, outcome);
|
|
6276
|
+
this.queue.updatePeerActivity(connection.transportPeerId);
|
|
6277
|
+
return;
|
|
5945
6278
|
}
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
6279
|
+
const executionOutcome = "executionOutcome" in message ? message.executionOutcome : void 0;
|
|
6280
|
+
if (executionOutcome !== void 0 && !isExecutionOutcome(executionOutcome))
|
|
6281
|
+
return;
|
|
6282
|
+
const response = "response" in message ? message.response : void 0;
|
|
6283
|
+
const error = "error" in message ? message.error : void 0;
|
|
6284
|
+
connection.settling = true;
|
|
6285
|
+
try {
|
|
6286
|
+
const disposition = this.queue.settleTransportResponse(connection.transportPeerId, message.requestId, response, error, executionOutcome);
|
|
6287
|
+
if (!this.isCurrent(connection))
|
|
5949
6288
|
return;
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
requestId: request.requestId,
|
|
5953
|
-
peerId: request.peerId,
|
|
5954
|
-
target: request.target,
|
|
5955
|
-
endpoint: request.endpoint,
|
|
5956
|
-
data: request.data === void 0 ? null : request.data,
|
|
5957
|
-
remainingMs: request.remainingMs
|
|
5958
|
-
};
|
|
5959
|
-
if (!this.write(stream, event))
|
|
6289
|
+
if (!connection.acknowledgements.has(message.requestId) && connection.acknowledgements.size >= MAX_PENDING_ACKS) {
|
|
6290
|
+
this.closeSocket(connection, 1013, "ack_backpressure");
|
|
5960
6291
|
return;
|
|
6292
|
+
}
|
|
6293
|
+
connection.acknowledgements.set(message.requestId, {
|
|
6294
|
+
kind: "ack",
|
|
6295
|
+
requestId: message.requestId,
|
|
6296
|
+
disposition
|
|
6297
|
+
});
|
|
6298
|
+
this.queue.updatePeerActivity(connection.transportPeerId);
|
|
6299
|
+
} catch {
|
|
6300
|
+
this.closeSocket(connection, 1011, "response_recording_failed");
|
|
6301
|
+
} finally {
|
|
6302
|
+
connection.settling = false;
|
|
5961
6303
|
}
|
|
6304
|
+
this.pump(connection);
|
|
5962
6305
|
}
|
|
5963
|
-
|
|
5964
|
-
if (
|
|
5965
|
-
return
|
|
6306
|
+
pump(connection) {
|
|
6307
|
+
if (!this.isCurrent(connection) || connection.sending || connection.pumping || connection.settling)
|
|
6308
|
+
return;
|
|
6309
|
+
connection.pumping = true;
|
|
5966
6310
|
try {
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
6311
|
+
while (this.isCurrent(connection) && !connection.sending) {
|
|
6312
|
+
const ack = connection.acknowledgements.values().next().value;
|
|
6313
|
+
if (ack) {
|
|
6314
|
+
connection.acknowledgements.delete(ack.requestId);
|
|
6315
|
+
this.send(connection, ack);
|
|
6316
|
+
continue;
|
|
6317
|
+
}
|
|
6318
|
+
if (connection.statusPending) {
|
|
6319
|
+
connection.statusPending = false;
|
|
6320
|
+
const status = connection.status();
|
|
6321
|
+
const statusJson = JSON.stringify(status);
|
|
6322
|
+
if (statusJson !== connection.lastStatusJson) {
|
|
6323
|
+
connection.lastStatusJson = statusJson;
|
|
6324
|
+
this.send(connection, status, statusJson);
|
|
6325
|
+
continue;
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
const cancellation = this.queue.claimNextCancellationForTransport(connection.transportPeerId, connection.claimOwner);
|
|
6329
|
+
if (cancellation) {
|
|
6330
|
+
this.send(connection, { kind: "cancel", ...cancellation });
|
|
6331
|
+
continue;
|
|
6332
|
+
}
|
|
6333
|
+
const request = this.queue.claimNextRequestForTransport(connection.transportPeerId, connection.claimOwner);
|
|
6334
|
+
if (request) {
|
|
6335
|
+
this.send(connection, { kind: "request", ...request, data: request.data ?? null });
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
if (connection.heartbeatPending) {
|
|
6339
|
+
connection.heartbeatPending = false;
|
|
6340
|
+
this.send(connection, { kind: "heartbeat", timestamp: Date.now() });
|
|
6341
|
+
continue;
|
|
6342
|
+
}
|
|
6343
|
+
return;
|
|
6344
|
+
}
|
|
5973
6345
|
} catch {
|
|
5974
|
-
this.
|
|
5975
|
-
|
|
6346
|
+
this.closeSocket(connection, 1011, "transport_pump_failed");
|
|
6347
|
+
} finally {
|
|
6348
|
+
connection.pumping = false;
|
|
5976
6349
|
}
|
|
5977
6350
|
}
|
|
5978
|
-
|
|
5979
|
-
|
|
6351
|
+
send(connection, event, serialized) {
|
|
6352
|
+
let json;
|
|
6353
|
+
try {
|
|
6354
|
+
json = serialized ?? JSON.stringify(event);
|
|
6355
|
+
} catch {
|
|
6356
|
+
if (event.kind !== "request")
|
|
6357
|
+
throw new Error("Unserializable Studio event");
|
|
6358
|
+
this.queue.settleTransportResponse(connection.transportPeerId, event.requestId, void 0, new RequestFailure("Studio request could not be serialized before WebSocket delivery at server_send", "studio_frame_serialization_failed", {
|
|
6359
|
+
requestId: event.requestId,
|
|
6360
|
+
targetPeerId: event.peerId,
|
|
6361
|
+
stage: "dispatched",
|
|
6362
|
+
outcome: "not_executed",
|
|
6363
|
+
executionOutcome: "not_executed",
|
|
6364
|
+
transportStage: "server_send"
|
|
6365
|
+
}));
|
|
5980
6366
|
return;
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
}
|
|
6367
|
+
}
|
|
6368
|
+
const bytes = Buffer.byteLength(json);
|
|
6369
|
+
if (bytes > MAX_STUDIO_FRAME_BYTES) {
|
|
6370
|
+
if (event.kind === "request") {
|
|
6371
|
+
this.queue.settleTransportResponse(connection.transportPeerId, event.requestId, void 0, new RequestFailure(`Studio request frame is ${bytes} bytes; limit is ${MAX_STUDIO_FRAME_BYTES} at server_send`, "studio_frame_too_large", {
|
|
6372
|
+
requestId: event.requestId,
|
|
6373
|
+
targetPeerId: event.peerId,
|
|
6374
|
+
stage: "dispatched",
|
|
6375
|
+
outcome: "not_executed",
|
|
6376
|
+
executionOutcome: "not_executed",
|
|
6377
|
+
transportStage: "server_send",
|
|
6378
|
+
bytes,
|
|
6379
|
+
limitBytes: MAX_STUDIO_FRAME_BYTES
|
|
6380
|
+
}));
|
|
6381
|
+
} else {
|
|
6382
|
+
this.closeSocket(connection, 1009, `server_send bytes=${bytes} limit=${MAX_STUDIO_FRAME_BYTES}`);
|
|
5996
6383
|
}
|
|
6384
|
+
return;
|
|
5997
6385
|
}
|
|
6386
|
+
if (connection.socket.readyState !== 1 || connection.socket.bufferedAmount + bytes > MAX_STUDIO_BUFFERED_BYTES) {
|
|
6387
|
+
this.closeSocket(connection, 1013, "server_send_backpressure");
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
connection.sending = true;
|
|
6391
|
+
connection.socket.send(json, (error) => {
|
|
6392
|
+
if (!this.isCurrent(connection))
|
|
6393
|
+
return;
|
|
6394
|
+
connection.sending = false;
|
|
6395
|
+
if (error)
|
|
6396
|
+
this.closeSocket(connection, 1011, "server_send_failed");
|
|
6397
|
+
else
|
|
6398
|
+
this.pump(connection);
|
|
6399
|
+
});
|
|
6400
|
+
}
|
|
6401
|
+
closeSocket(connection, code, reason) {
|
|
6402
|
+
if (connection.closed)
|
|
6403
|
+
return;
|
|
6404
|
+
connection.closed = true;
|
|
6405
|
+
clearInterval(connection.heartbeatTimer);
|
|
6406
|
+
connection.socket.removeListener("message", connection.onMessage);
|
|
6407
|
+
connection.acknowledgements.clear();
|
|
6408
|
+
if (this.sockets.get(connection.transportPeerId) === connection)
|
|
6409
|
+
this.sockets.delete(connection.transportPeerId);
|
|
6410
|
+
this.queue.setDeliveryActive(connection.transportPeerId, connection.claimOwner, false);
|
|
6411
|
+
this.queue.releaseDeliveryClaims(connection.claimOwner);
|
|
6412
|
+
if (code !== void 0) {
|
|
6413
|
+
this.closing.add(connection);
|
|
6414
|
+
connection.closeTimer = setTimeout(() => {
|
|
6415
|
+
connection.socket.terminate();
|
|
6416
|
+
this.finishClose(connection);
|
|
6417
|
+
}, 1e3);
|
|
6418
|
+
connection.closeTimer.unref();
|
|
6419
|
+
connection.socket.close(code, reason);
|
|
6420
|
+
if (this.closing.size > MAX_ACTIVE_STUDIO_SOCKETS) {
|
|
6421
|
+
const oldest = this.closing.values().next().value;
|
|
6422
|
+
if (oldest) {
|
|
6423
|
+
oldest.socket.terminate();
|
|
6424
|
+
this.finishClose(oldest);
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
finishClose(connection) {
|
|
6430
|
+
clearTimeout(connection.closeTimer);
|
|
6431
|
+
this.closing.delete(connection);
|
|
6432
|
+
connection.socket.removeListener("close", connection.onClose);
|
|
6433
|
+
connection.socket.removeListener("error", connection.onError);
|
|
5998
6434
|
}
|
|
5999
6435
|
};
|
|
6000
6436
|
|
|
@@ -6074,13 +6510,14 @@ function requiredClosedLineRange(body, toolName) {
|
|
|
6074
6510
|
return { startLine: parsed.startLine, endLine: parsed.endLine };
|
|
6075
6511
|
}
|
|
6076
6512
|
var TOOL_HANDLERS = {
|
|
6513
|
+
get_request_status: (tools, body) => tools.getRequestStatus(body.request_id),
|
|
6077
6514
|
get_roblox_skills: (tools, body) => tools.getRobloxSkills(body.action, body.name),
|
|
6078
6515
|
get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
|
|
6079
6516
|
get_place_info: (tools, body) => tools.getPlaceInfo(body.instance_id),
|
|
6080
6517
|
search_objects: (tools, body) => tools.searchObjects(body.query, body.searchType, body.propertyName, body.instance_id),
|
|
6081
6518
|
get_instance_properties: (tools, body) => tools.getInstanceProperties(body.instancePath, body.excludeSource, body.instance_id),
|
|
6082
6519
|
get_project_structure: (tools, body) => tools.getProjectStructure(body.path, body.maxDepth, body.scriptsOnly, body.instance_id),
|
|
6083
|
-
set_properties: (tools, body) => tools.setProperties(body.instancePath, body.properties, body.instance_id),
|
|
6520
|
+
set_properties: (tools, body) => tools.setProperties(body.instancePath, body.properties, body.instance_id, body.operation_id),
|
|
6084
6521
|
grep_scripts: (tools, body, context) => tools.grepScripts(body.pattern, {
|
|
6085
6522
|
caseSensitive: body.caseSensitive,
|
|
6086
6523
|
usePattern: body.usePattern,
|
|
@@ -6104,7 +6541,7 @@ var TOOL_HANDLERS = {
|
|
|
6104
6541
|
},
|
|
6105
6542
|
get_attributes: (tools, body) => tools.getAttributes(body.instancePath, body.instance_id),
|
|
6106
6543
|
selection: (tools, body) => tools.selection(body.action, body, body.instance_id),
|
|
6107
|
-
execute_luau: (tools, body) => tools.executeLuau(body.code, body.target, body.instance_id),
|
|
6544
|
+
execute_luau: (tools, body) => tools.executeLuau(body.code, body.target, body.instance_id, body.operation_id),
|
|
6108
6545
|
eval_server_runtime: (tools, body) => tools.evalServerRuntime(body.code, body.instance_id),
|
|
6109
6546
|
eval_client_runtime: (tools, body) => tools.evalClientRuntime(body.code, body.target, body.instance_id),
|
|
6110
6547
|
set_network_profile: (tools, body) => tools.setNetworkProfile(body.profile, body.target, body.overrides, body.instance_id),
|
|
@@ -6116,7 +6553,7 @@ var TOOL_HANDLERS = {
|
|
|
6116
6553
|
manage_instance: (tools, body) => tools.manageInstance(body),
|
|
6117
6554
|
solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
|
|
6118
6555
|
multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
|
|
6119
|
-
get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.instance_id, body.multiplayer_group_id, body.cursor, body.cursor_by_instance, body.tail, body.filter),
|
|
6556
|
+
get_runtime_logs: (tools, body, context) => tools.getRuntimeLogs(body.instance_id, body.multiplayer_group_id, body.cursor, body.cursor_by_instance, body.tail, body.filter, context?.signal),
|
|
6120
6557
|
capture_script_profiler: (tools, body) => tools.captureScriptProfiler(body.target, {
|
|
6121
6558
|
duration_ms: body.duration_ms,
|
|
6122
6559
|
frequency: body.frequency,
|
|
@@ -6172,6 +6609,16 @@ var TOOL_HANDLERS = {
|
|
|
6172
6609
|
maxReplacements: body.maxReplacements
|
|
6173
6610
|
}, body.instance_id)
|
|
6174
6611
|
};
|
|
6612
|
+
var MAX_STUDIO_SESSIONS = 256;
|
|
6613
|
+
function rejectStudioUpgrade(socket, status, error) {
|
|
6614
|
+
const body = JSON.stringify({ error });
|
|
6615
|
+
socket.end(`HTTP/1.1 ${status} ${http.STATUS_CODES[status]}\r
|
|
6616
|
+
Connection: close\r
|
|
6617
|
+
Content-Type: application/json\r
|
|
6618
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
6619
|
+
\r
|
|
6620
|
+
${body}`);
|
|
6621
|
+
}
|
|
6175
6622
|
function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
6176
6623
|
const app = express();
|
|
6177
6624
|
const studioLifecycleCallable = !allowedTools || allowedTools.has("manage_instance");
|
|
@@ -6181,8 +6628,19 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6181
6628
|
let mcpServerStartTime = 0;
|
|
6182
6629
|
const proxyInstances = /* @__PURE__ */ new Set();
|
|
6183
6630
|
const rejectedVersionPeers = /* @__PURE__ */ new Set();
|
|
6184
|
-
const
|
|
6185
|
-
const
|
|
6631
|
+
const studioTransport = new WebSocketStudioTransport(bridge);
|
|
6632
|
+
const transportTokens = /* @__PURE__ */ new Map();
|
|
6633
|
+
const boundServers = /* @__PURE__ */ new Set();
|
|
6634
|
+
const webSocketServer = new WebSocketServer({
|
|
6635
|
+
noServer: true,
|
|
6636
|
+
clientTracking: false,
|
|
6637
|
+
perMessageDeflate: false,
|
|
6638
|
+
maxPayload: MAX_STUDIO_FRAME_BYTES
|
|
6639
|
+
});
|
|
6640
|
+
let closed = false;
|
|
6641
|
+
const unsubscribePeerClosed = bridge.onPeerClosed((peer) => {
|
|
6642
|
+
transportTokens.delete(peer.peerId);
|
|
6643
|
+
});
|
|
6186
6644
|
const setMCPServerActive = (active) => {
|
|
6187
6645
|
mcpServerActive = active;
|
|
6188
6646
|
if (active) {
|
|
@@ -6192,14 +6650,14 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6192
6650
|
mcpServerStartTime = 0;
|
|
6193
6651
|
lastMCPActivity = 0;
|
|
6194
6652
|
}
|
|
6195
|
-
|
|
6653
|
+
studioTransport.refreshStatus();
|
|
6196
6654
|
};
|
|
6197
6655
|
const trackMCPActivity = () => {
|
|
6198
6656
|
if (mcpServerActive) {
|
|
6199
6657
|
const wasConnected = Date.now() - lastMCPActivity < 3e4;
|
|
6200
6658
|
lastMCPActivity = Date.now();
|
|
6201
6659
|
if (!wasConnected)
|
|
6202
|
-
|
|
6660
|
+
studioTransport.refreshStatus();
|
|
6203
6661
|
}
|
|
6204
6662
|
};
|
|
6205
6663
|
const isMCPServerActive = () => {
|
|
@@ -6223,6 +6681,67 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6223
6681
|
return bridge.getPeers().length > 0;
|
|
6224
6682
|
};
|
|
6225
6683
|
const allowedOrigins = new Set(security?.allowedOrigins ?? []);
|
|
6684
|
+
const upgradeStudio = (req, socket, head) => {
|
|
6685
|
+
socket.on("error", () => socket.destroy());
|
|
6686
|
+
if (closed) {
|
|
6687
|
+
rejectStudioUpgrade(socket, 503, "server_shutdown");
|
|
6688
|
+
return;
|
|
6689
|
+
}
|
|
6690
|
+
let url;
|
|
6691
|
+
try {
|
|
6692
|
+
url = new URL(req.url ?? "/", "http://localhost");
|
|
6693
|
+
} catch {
|
|
6694
|
+
rejectStudioUpgrade(socket, 400, "invalid_websocket_url");
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
6697
|
+
if (url.pathname !== "/studio") {
|
|
6698
|
+
rejectStudioUpgrade(socket, 404, "unknown_websocket_endpoint");
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6701
|
+
const origin = req.headers.origin;
|
|
6702
|
+
if (origin && !allowedOrigins.has(origin)) {
|
|
6703
|
+
rejectStudioUpgrade(socket, 403, "forbidden_origin");
|
|
6704
|
+
return;
|
|
6705
|
+
}
|
|
6706
|
+
if (req.method !== "GET" || url.searchParams.getAll("protocolVersion").length !== 1 || url.searchParams.get("protocolVersion") !== String(STUDIO_PROTOCOL_VERSION)) {
|
|
6707
|
+
rejectStudioUpgrade(socket, 426, "studio_protocol_mismatch");
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6710
|
+
const peerId = url.searchParams.get("peerId");
|
|
6711
|
+
if (!peerId || url.searchParams.getAll("peerId").length !== 1) {
|
|
6712
|
+
rejectStudioUpgrade(socket, 400, "missing_peer_id");
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
const peer = bridge.getPeerById(peerId);
|
|
6716
|
+
if (!peer) {
|
|
6717
|
+
rejectStudioUpgrade(socket, 404, "unknown_peer");
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
if (peer.transportPeerId !== peerId) {
|
|
6721
|
+
rejectStudioUpgrade(socket, 403, "peer_has_no_socket");
|
|
6722
|
+
return;
|
|
6723
|
+
}
|
|
6724
|
+
const token = transportTokens.get(peerId);
|
|
6725
|
+
const provided = req.headers["x-studio-token"];
|
|
6726
|
+
if (!token || typeof provided !== "string" || !tokensMatch(provided, token)) {
|
|
6727
|
+
rejectStudioUpgrade(socket, 401, "invalid_studio_token");
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6730
|
+
if (!studioTransport.canOpen(peerId)) {
|
|
6731
|
+
rejectStudioUpgrade(socket, 503, "studio_socket_capacity_reached");
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
webSocketServer.handleUpgrade(req, socket, head, (webSocket) => {
|
|
6735
|
+
webSocket.on("error", (error) => {
|
|
6736
|
+
if ("code" in error && error.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH") {
|
|
6737
|
+
console.error(`[studio-websocket] server_receive frame exceeds limitBytes=${MAX_STUDIO_FRAME_BYTES}`);
|
|
6738
|
+
}
|
|
6739
|
+
});
|
|
6740
|
+
const handle = studioTransport.open(peerId, webSocket, () => eventStatus(peerId));
|
|
6741
|
+
if (!handle)
|
|
6742
|
+
webSocket.close(1013, "studio_socket_capacity_reached");
|
|
6743
|
+
});
|
|
6744
|
+
};
|
|
6226
6745
|
app.use((req, res, next) => {
|
|
6227
6746
|
const origin = req.headers.origin;
|
|
6228
6747
|
if (typeof origin !== "string" || origin === "") {
|
|
@@ -6247,7 +6766,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6247
6766
|
next();
|
|
6248
6767
|
});
|
|
6249
6768
|
const authToken = security?.authToken;
|
|
6250
|
-
const authRequired = (
|
|
6769
|
+
const authRequired = (requestPath) => {
|
|
6770
|
+
const path6 = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
6771
|
+
return path6 === "/mcp" || path6.startsWith("/mcp/") || path6 === "/proxy" || path6 === "/topology" || path6 === "/request-status" || path6 === "/unregister-instance-id" || path6 === "/create-multiplayer-group" || path6 === "/remove-multiplayer-group";
|
|
6772
|
+
};
|
|
6251
6773
|
app.use((req, res, next) => {
|
|
6252
6774
|
if (!authToken || !authRequired(req.path)) {
|
|
6253
6775
|
next();
|
|
@@ -6265,8 +6787,31 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6265
6787
|
message: 'Missing or invalid auth token. Send it as "X-MCP-Auth: <token>" or "Authorization: Bearer <token>". ' + (security?.authTokenHint ?? "The token is in ~/.robloxstudio-mcp/auth-token (or ROBLOX_STUDIO_AUTH_TOKEN).")
|
|
6266
6788
|
});
|
|
6267
6789
|
});
|
|
6268
|
-
app.use(express.json({ limit:
|
|
6269
|
-
app.use(express.urlencoded({ limit:
|
|
6790
|
+
app.use(express.json({ limit: HTTP_BODY_LIMIT_BYTES }));
|
|
6791
|
+
app.use(express.urlencoded({ limit: HTTP_BODY_LIMIT_BYTES, extended: true }));
|
|
6792
|
+
const handleBodySizeError = (error, _req, res, next) => {
|
|
6793
|
+
if (!error || typeof error !== "object" || !("type" in error) || error.type !== "entity.too.large") {
|
|
6794
|
+
next(error);
|
|
6795
|
+
return;
|
|
6796
|
+
}
|
|
6797
|
+
const bytes = "received" in error && typeof error.received === "number" ? error.received : "length" in error && typeof error.length === "number" ? error.length : void 0;
|
|
6798
|
+
if (bytes === void 0) {
|
|
6799
|
+
next(error);
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6802
|
+
res.status(413).json({
|
|
6803
|
+
error: `HTTP request body is ${bytes} bytes at http_receive; limit ${HTTP_BODY_LIMIT_BYTES} bytes; queued; not_executed`,
|
|
6804
|
+
code: "request_too_large",
|
|
6805
|
+
details: {
|
|
6806
|
+
bytes,
|
|
6807
|
+
limitBytes: HTTP_BODY_LIMIT_BYTES,
|
|
6808
|
+
stage: "queued",
|
|
6809
|
+
outcome: "not_executed",
|
|
6810
|
+
transportStage: "http_receive"
|
|
6811
|
+
}
|
|
6812
|
+
});
|
|
6813
|
+
};
|
|
6814
|
+
app.use(handleBodySizeError);
|
|
6270
6815
|
app.get("/health", (req, res) => {
|
|
6271
6816
|
const peers = bridge.getPublicPeers().map(toPassivePeer);
|
|
6272
6817
|
const instances = bridge.getPublicInstances().map(toPassiveInstance);
|
|
@@ -6296,7 +6841,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6296
6841
|
uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
|
|
6297
6842
|
pendingRequests: bridge.getPendingRequestCount(),
|
|
6298
6843
|
proxyInstanceCount: proxyInstances.size,
|
|
6299
|
-
|
|
6844
|
+
activeWebSockets: studioTransport.activeSocketCount,
|
|
6845
|
+
studioSocketCapacity: MAX_ACTIVE_STUDIO_SOCKETS,
|
|
6300
6846
|
streamableHttp: !!serverConfig
|
|
6301
6847
|
});
|
|
6302
6848
|
});
|
|
@@ -6401,6 +6947,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6401
6947
|
return;
|
|
6402
6948
|
}
|
|
6403
6949
|
}
|
|
6950
|
+
if (closed || !isProxiedPeer && !transportTokens.has(peerId) && transportTokens.size >= MAX_STUDIO_SESSIONS) {
|
|
6951
|
+
res.status(503).json({ success: false, error: closed ? "server_shutdown" : "studio_session_capacity_reached" });
|
|
6952
|
+
return;
|
|
6953
|
+
}
|
|
6404
6954
|
let result;
|
|
6405
6955
|
try {
|
|
6406
6956
|
result = bridge.registerPeer({
|
|
@@ -6437,14 +6987,20 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6437
6987
|
});
|
|
6438
6988
|
return;
|
|
6439
6989
|
}
|
|
6440
|
-
|
|
6990
|
+
let transportToken;
|
|
6991
|
+
if (!isProxiedPeer) {
|
|
6992
|
+
transportToken = transportTokens.get(peerId) ?? randomBytes2(32).toString("hex");
|
|
6993
|
+
transportTokens.set(peerId, transportToken);
|
|
6994
|
+
}
|
|
6995
|
+
studioTransport.refreshStatus(transportPeerId);
|
|
6441
6996
|
res.json({
|
|
6442
6997
|
success: true,
|
|
6443
6998
|
assignedRole: result.assignedRole,
|
|
6444
6999
|
peerId: result.peerId,
|
|
6445
7000
|
instanceId: result.instanceId,
|
|
6446
7001
|
multiplayerGroupId: result.multiplayerGroupId,
|
|
6447
|
-
serverVersion
|
|
7002
|
+
serverVersion,
|
|
7003
|
+
...!isProxiedPeer ? { protocolVersion: STUDIO_PROTOCOL_VERSION, transportToken } : {}
|
|
6448
7004
|
});
|
|
6449
7005
|
});
|
|
6450
7006
|
app.post("/disconnect", (req, res) => {
|
|
@@ -6504,71 +7060,24 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6504
7060
|
serverVersion: serverConfig?.version
|
|
6505
7061
|
});
|
|
6506
7062
|
});
|
|
6507
|
-
app.get("/events", (
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
res.status(400).json({
|
|
6511
|
-
error: "missing_peer_id",
|
|
6512
|
-
message: "peerId is required"
|
|
6513
|
-
});
|
|
6514
|
-
return;
|
|
6515
|
-
}
|
|
6516
|
-
const peer = bridge.getPeerById(peerId);
|
|
6517
|
-
if (!peer) {
|
|
6518
|
-
res.status(404).json({
|
|
6519
|
-
error: "unknown_peer",
|
|
6520
|
-
knownPeer: false
|
|
6521
|
-
});
|
|
6522
|
-
return;
|
|
6523
|
-
}
|
|
6524
|
-
if (peer.transportPeerId !== peerId) {
|
|
6525
|
-
res.status(409).json({
|
|
6526
|
-
error: "peer_has_no_event_stream",
|
|
6527
|
-
transportPeerId: peer.transportPeerId
|
|
6528
|
-
});
|
|
6529
|
-
return;
|
|
6530
|
-
}
|
|
6531
|
-
if (!eventTransport.canOpen(peerId)) {
|
|
6532
|
-
res.setHeader("Retry-After", "1");
|
|
6533
|
-
res.status(503).json({
|
|
6534
|
-
error: "event_stream_capacity_reached",
|
|
6535
|
-
capacity: MAX_ACTIVE_EVENT_STREAMS
|
|
6536
|
-
});
|
|
6537
|
-
return;
|
|
6538
|
-
}
|
|
6539
|
-
bridge.updatePeerActivity(peerId);
|
|
6540
|
-
res.status(200);
|
|
6541
|
-
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
6542
|
-
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
6543
|
-
res.setHeader("Connection", "keep-alive");
|
|
6544
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
6545
|
-
res.flushHeaders();
|
|
6546
|
-
const handle = eventTransport.open(peerId, res, () => eventStatus(peerId));
|
|
6547
|
-
if (!handle) {
|
|
6548
|
-
res.end();
|
|
6549
|
-
return;
|
|
6550
|
-
}
|
|
6551
|
-
eventStreamHandles.add(handle);
|
|
6552
|
-
res.once("close", () => eventStreamHandles.delete(handle));
|
|
7063
|
+
app.get(["/studio", "/events"], (_req, res) => {
|
|
7064
|
+
res.setHeader("Upgrade", "websocket");
|
|
7065
|
+
res.status(426).json({ error: "studio_websocket_required", protocolVersion: STUDIO_PROTOCOL_VERSION });
|
|
6553
7066
|
});
|
|
6554
|
-
app.post("/response", (
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
}
|
|
6563
|
-
const disposition = error !== void 0 ? bridge.rejectRequest(requestId, error) : bridge.resolveRequest(requestId, response);
|
|
6564
|
-
if (disposition === "unknown") {
|
|
6565
|
-
res.status(404).json({ success: false, disposition });
|
|
7067
|
+
app.post("/response", (_req, res) => {
|
|
7068
|
+
res.setHeader("Upgrade", "websocket");
|
|
7069
|
+
res.status(426).json({ error: "studio_websocket_required", protocolVersion: STUDIO_PROTOCOL_VERSION });
|
|
7070
|
+
});
|
|
7071
|
+
app.get("/request-status", (req, res) => {
|
|
7072
|
+
const requestId = req.query.requestId;
|
|
7073
|
+
if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 1024) {
|
|
7074
|
+
res.status(400).json({ error: "invalid_request_id" });
|
|
6566
7075
|
return;
|
|
6567
7076
|
}
|
|
6568
|
-
res.json({
|
|
7077
|
+
res.json({ status: bridge.getRequestStatus(requestId) ?? null });
|
|
6569
7078
|
});
|
|
6570
7079
|
app.post("/proxy", async (req, res) => {
|
|
6571
|
-
const { endpoint, data, targetPeerId, proxyInstanceId, timeoutMs } = req.body;
|
|
7080
|
+
const { endpoint, data, targetPeerId, proxyInstanceId, timeoutMs, operationId } = req.body;
|
|
6572
7081
|
if (!endpoint || !targetPeerId) {
|
|
6573
7082
|
res.status(400).json({ error: "endpoint and targetPeerId are required" });
|
|
6574
7083
|
return;
|
|
@@ -6580,17 +7089,22 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6580
7089
|
if (proxyInstanceId) {
|
|
6581
7090
|
proxyInstances.add(proxyInstanceId);
|
|
6582
7091
|
}
|
|
7092
|
+
if (operationId !== void 0 && (typeof operationId !== "string" || operationId.trim().length === 0 || operationId.length > 128)) {
|
|
7093
|
+
res.status(400).json({ error: "operationId must be a non-empty string of at most 128 characters" });
|
|
7094
|
+
return;
|
|
7095
|
+
}
|
|
6583
7096
|
const controller = new AbortController();
|
|
6584
7097
|
const abort = () => controller.abort();
|
|
6585
7098
|
req.once("aborted", abort);
|
|
6586
7099
|
res.once("close", abort);
|
|
6587
7100
|
try {
|
|
6588
|
-
const response = await bridge.sendRequest(endpoint, data, targetPeerId, timeoutMs, controller.signal);
|
|
7101
|
+
const response = await bridge.sendRequest(endpoint, data, targetPeerId, timeoutMs, controller.signal, operationId);
|
|
6589
7102
|
res.json({ response });
|
|
6590
7103
|
} catch (error) {
|
|
6591
7104
|
if (!res.headersSent && !res.destroyed) {
|
|
6592
7105
|
res.status(500).json({
|
|
6593
|
-
error: error instanceof Error ? error.message : "Proxy request failed"
|
|
7106
|
+
error: error instanceof Error ? error.message : error === void 0 ? "Proxy request failed" : error,
|
|
7107
|
+
...error instanceof RequestFailure ? { code: error.code, details: error.details } : {}
|
|
6594
7108
|
});
|
|
6595
7109
|
}
|
|
6596
7110
|
} finally {
|
|
@@ -6642,11 +7156,29 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6642
7156
|
app.isMCPServerActive = isMCPServerActive;
|
|
6643
7157
|
app.trackMCPActivity = trackMCPActivity;
|
|
6644
7158
|
app.closeMcpHandler = () => mcpHandler?.close();
|
|
7159
|
+
app.attachStudioTransport = (server) => {
|
|
7160
|
+
if (closed)
|
|
7161
|
+
throw new Error("Cannot attach a closed Studio transport");
|
|
7162
|
+
if (boundServers.has(server))
|
|
7163
|
+
return;
|
|
7164
|
+
boundServers.add(server);
|
|
7165
|
+
server.on("upgrade", upgradeStudio);
|
|
7166
|
+
server.once("close", () => {
|
|
7167
|
+
boundServers.delete(server);
|
|
7168
|
+
server.removeListener("upgrade", upgradeStudio);
|
|
7169
|
+
});
|
|
7170
|
+
};
|
|
6645
7171
|
app.cleanup = async () => {
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
7172
|
+
if (closed)
|
|
7173
|
+
return;
|
|
7174
|
+
closed = true;
|
|
7175
|
+
for (const server of boundServers)
|
|
7176
|
+
server.removeListener("upgrade", upgradeStudio);
|
|
7177
|
+
boundServers.clear();
|
|
7178
|
+
unsubscribePeerClosed();
|
|
7179
|
+
transportTokens.clear();
|
|
7180
|
+
studioTransport.close();
|
|
7181
|
+
webSocketServer.close();
|
|
6650
7182
|
await mcpHandler?.close();
|
|
6651
7183
|
};
|
|
6652
7184
|
return app;
|
|
@@ -6668,18 +7200,26 @@ async function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
|
6668
7200
|
throw new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`);
|
|
6669
7201
|
}
|
|
6670
7202
|
function bindPort(app, host, port) {
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
7203
|
+
const { promise, resolve: resolve5, reject } = Promise.withResolvers();
|
|
7204
|
+
const server = http.createServer(app);
|
|
7205
|
+
const onError = (err2) => {
|
|
7206
|
+
server.removeListener("error", onError);
|
|
7207
|
+
reject(err2);
|
|
7208
|
+
};
|
|
7209
|
+
server.once("error", onError);
|
|
7210
|
+
server.listen(port, host, () => {
|
|
7211
|
+
server.removeListener("error", onError);
|
|
7212
|
+
try {
|
|
7213
|
+
if ("attachStudioTransport" in app && typeof app.attachStudioTransport === "function") {
|
|
7214
|
+
app.attachStudioTransport(server);
|
|
7215
|
+
}
|
|
6680
7216
|
resolve5(server);
|
|
6681
|
-
})
|
|
7217
|
+
} catch (error) {
|
|
7218
|
+
server.close();
|
|
7219
|
+
reject(error);
|
|
7220
|
+
}
|
|
6682
7221
|
});
|
|
7222
|
+
return promise;
|
|
6683
7223
|
}
|
|
6684
7224
|
|
|
6685
7225
|
// ../core/dist/tools/studio-client.js
|
|
@@ -6688,15 +7228,8 @@ var StudioHttpClient = class {
|
|
|
6688
7228
|
constructor(bridge) {
|
|
6689
7229
|
this.bridge = bridge;
|
|
6690
7230
|
}
|
|
6691
|
-
async request(endpoint, data, targetPeerId, timeoutMs, signal) {
|
|
6692
|
-
|
|
6693
|
-
return await this.bridge.sendRequest(endpoint, data, targetPeerId, timeoutMs, signal);
|
|
6694
|
-
} catch (error) {
|
|
6695
|
-
if (error instanceof Error && error.message === "Request timeout") {
|
|
6696
|
-
throw new Error("Studio plugin connection timeout. Make sure the Roblox Studio plugin is running and activated.");
|
|
6697
|
-
}
|
|
6698
|
-
throw error;
|
|
6699
|
-
}
|
|
7231
|
+
async request(endpoint, data, targetPeerId, timeoutMs, signal, operationId) {
|
|
7232
|
+
return this.bridge.sendRequest(endpoint, data, targetPeerId, timeoutMs, signal, operationId);
|
|
6700
7233
|
}
|
|
6701
7234
|
};
|
|
6702
7235
|
|
|
@@ -7521,7 +8054,7 @@ function decodeImagePathToRgba(imagePath) {
|
|
|
7521
8054
|
}
|
|
7522
8055
|
|
|
7523
8056
|
// ../core/dist/studio-skills.js
|
|
7524
|
-
import { createHash as
|
|
8057
|
+
import { createHash as createHash3 } from "crypto";
|
|
7525
8058
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7526
8059
|
import * as path4 from "path";
|
|
7527
8060
|
|
|
@@ -8406,7 +8939,7 @@ function parseBuiltInStudioSkills(buffer) {
|
|
|
8406
8939
|
hasCombinedDocument: group.combined !== void 0,
|
|
8407
8940
|
content: selected.content,
|
|
8408
8941
|
contentLength: Buffer.byteLength(selected.content, "utf8"),
|
|
8409
|
-
contentSha256:
|
|
8942
|
+
contentSha256: createHash3("sha256").update(selected.content).digest("hex")
|
|
8410
8943
|
};
|
|
8411
8944
|
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
8412
8945
|
}
|
|
@@ -8473,7 +9006,7 @@ function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
|
8473
9006
|
const value = {
|
|
8474
9007
|
bundlePath,
|
|
8475
9008
|
bundleModifiedAt: stats.mtime.toISOString(),
|
|
8476
|
-
bundleSha256:
|
|
9009
|
+
bundleSha256: createHash3("sha256").update(buffer).digest("hex"),
|
|
8477
9010
|
studioVersion,
|
|
8478
9011
|
skills
|
|
8479
9012
|
};
|
|
@@ -9506,6 +10039,7 @@ var MAX_DEVICE_MATRIX_ENTRIES = 6;
|
|
|
9506
10039
|
var MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
|
|
9507
10040
|
var GREP_SCRIPTS_TIMEOUT_MS = 12e4;
|
|
9508
10041
|
var MAX_GREP_PATTERN_UTF8_BYTES = 4096;
|
|
10042
|
+
var RUNTIME_LOG_PEER_TIMEOUT_MS = 5e3;
|
|
9509
10043
|
var STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
|
|
9510
10044
|
var CREATOR_STORE_SEARCH_TYPES = /* @__PURE__ */ new Set([
|
|
9511
10045
|
"Audio",
|
|
@@ -10343,8 +10877,8 @@ var RobloxStudioTools = class {
|
|
|
10343
10877
|
_peerForRoleInScope(instanceId, role) {
|
|
10344
10878
|
return this.bridge.getPeersInScope(instanceId).find((peer) => peer.role === role);
|
|
10345
10879
|
}
|
|
10346
|
-
_requestPeer(endpoint, data, targetPeerId, timeoutMs, signal) {
|
|
10347
|
-
return this.client.request(endpoint, data, targetPeerId, timeoutMs, signal);
|
|
10880
|
+
_requestPeer(endpoint, data, targetPeerId, timeoutMs, signal, operationId) {
|
|
10881
|
+
return this.client.request(endpoint, data, targetPeerId, timeoutMs, signal, operationId);
|
|
10348
10882
|
}
|
|
10349
10883
|
_request(endpoint, data, instanceId, role, timeoutMs, signal) {
|
|
10350
10884
|
const peer = this._peerForRoleInScope(instanceId, role);
|
|
@@ -10358,7 +10892,7 @@ var RobloxStudioTools = class {
|
|
|
10358
10892
|
return this._requestPeer(endpoint, data, peer.peerId, timeoutMs, signal);
|
|
10359
10893
|
}
|
|
10360
10894
|
// Resolve an optional Studio process plus role to one exact Peer and dispatch.
|
|
10361
|
-
async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal) {
|
|
10895
|
+
async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal, operationId) {
|
|
10362
10896
|
const resolved = this.bridge.resolveTarget({ instance_id, target });
|
|
10363
10897
|
if (!resolved.ok)
|
|
10364
10898
|
throw new RoutingFailure(resolved.error);
|
|
@@ -10369,7 +10903,7 @@ var RobloxStudioTools = class {
|
|
|
10369
10903
|
data: this._routingErrorData()
|
|
10370
10904
|
});
|
|
10371
10905
|
}
|
|
10372
|
-
return this._requestPeer(endpoint, data, resolved.targetPeerId, timeoutMs, signal);
|
|
10906
|
+
return this._requestPeer(endpoint, data, resolved.targetPeerId, timeoutMs, signal, operationId);
|
|
10373
10907
|
}
|
|
10374
10908
|
// Prefer the first client role in the selected process/group scope for live
|
|
10375
10909
|
// viewport and input operations; otherwise retain the default Peer's Instance.
|
|
@@ -10768,11 +11302,11 @@ var RobloxStudioTools = class {
|
|
|
10768
11302
|
]
|
|
10769
11303
|
};
|
|
10770
11304
|
}
|
|
10771
|
-
async setProperties(instancePath, properties, instance_id) {
|
|
11305
|
+
async setProperties(instancePath, properties, instance_id, operation_id) {
|
|
10772
11306
|
if (!instancePath || !properties) {
|
|
10773
11307
|
throw new Error("instancePath and properties are required for set_properties");
|
|
10774
11308
|
}
|
|
10775
|
-
const response = await this._callSingle("/api/set-properties", { instancePath, properties }, void 0, instance_id);
|
|
11309
|
+
const response = await this._callSingle("/api/set-properties", { instancePath, properties }, void 0, instance_id, void 0, void 0, operation_id);
|
|
10776
11310
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
10777
11311
|
}
|
|
10778
11312
|
async getScriptSource(instancePath, startLine, endLine, instance_id) {
|
|
@@ -10902,9 +11436,6 @@ var RobloxStudioTools = class {
|
|
|
10902
11436
|
}
|
|
10903
11437
|
return this.setSelection(opts.paths, opts.mode, instance_id);
|
|
10904
11438
|
}
|
|
10905
|
-
if (!opts.path) {
|
|
10906
|
-
throw new Error("selection action=view requires path");
|
|
10907
|
-
}
|
|
10908
11439
|
return this.focusViewport(opts.path, opts.from, opts.padding, opts.angleY, instance_id);
|
|
10909
11440
|
}
|
|
10910
11441
|
async getSelection(instance_id) {
|
|
@@ -10937,8 +11468,8 @@ var RobloxStudioTools = class {
|
|
|
10937
11468
|
};
|
|
10938
11469
|
}
|
|
10939
11470
|
async focusViewport(instancePath, from, padding, angleY, instance_id) {
|
|
10940
|
-
if (
|
|
10941
|
-
throw new Error("selection
|
|
11471
|
+
if (instancePath !== void 0 && (typeof instancePath !== "string" || instancePath.length === 0)) {
|
|
11472
|
+
throw new Error("selection path must be a non-empty instance path when provided");
|
|
10942
11473
|
}
|
|
10943
11474
|
if (padding !== void 0 && (padding <= 0 || padding > 10)) {
|
|
10944
11475
|
throw new Error("selection padding must be greater than 0 and at most 10");
|
|
@@ -10962,11 +11493,11 @@ var RobloxStudioTools = class {
|
|
|
10962
11493
|
]
|
|
10963
11494
|
};
|
|
10964
11495
|
}
|
|
10965
|
-
async executeLuau(code, target, instance_id) {
|
|
11496
|
+
async executeLuau(code, target, instance_id, operation_id) {
|
|
10966
11497
|
if (!code) {
|
|
10967
11498
|
throw new Error("Code is required for execute_luau");
|
|
10968
11499
|
}
|
|
10969
|
-
const response = await this._callSingle("/api/execute-luau", { code }, target || "edit", instance_id);
|
|
11500
|
+
const response = await this._callSingle("/api/execute-luau", { code }, target || "edit", instance_id, void 0, void 0, operation_id);
|
|
10970
11501
|
return {
|
|
10971
11502
|
content: [
|
|
10972
11503
|
{
|
|
@@ -11367,7 +11898,7 @@ var RobloxStudioTools = class {
|
|
|
11367
11898
|
]
|
|
11368
11899
|
};
|
|
11369
11900
|
}
|
|
11370
|
-
async getRuntimeLogs(instance_id, multiplayer_group_id, cursor, cursor_by_instance, tail, filter) {
|
|
11901
|
+
async getRuntimeLogs(instance_id, multiplayer_group_id, cursor, cursor_by_instance, tail, filter, signal) {
|
|
11371
11902
|
if (instance_id !== void 0 && multiplayer_group_id !== void 0) {
|
|
11372
11903
|
throw new Error("get_runtime_logs accepts only one of instance_id or multiplayer_group_id.");
|
|
11373
11904
|
}
|
|
@@ -11511,7 +12042,7 @@ var RobloxStudioTools = class {
|
|
|
11511
12042
|
if (filter !== void 0)
|
|
11512
12043
|
data.filter = filter;
|
|
11513
12044
|
try {
|
|
11514
|
-
const responseValue = await this.client.request("/api/get-runtime-logs", data, peer.peerId);
|
|
12045
|
+
const responseValue = await this.client.request("/api/get-runtime-logs", data, peer.peerId, RUNTIME_LOG_PEER_TIMEOUT_MS, signal);
|
|
11515
12046
|
if (typeof responseValue !== "object" || responseValue === null || Array.isArray(responseValue)) {
|
|
11516
12047
|
return {
|
|
11517
12048
|
peerId: peer.peerId,
|
|
@@ -11538,6 +12069,8 @@ var RobloxStudioTools = class {
|
|
|
11538
12069
|
nextSince: response.nextSince
|
|
11539
12070
|
};
|
|
11540
12071
|
} catch (error) {
|
|
12072
|
+
if (signal?.aborted)
|
|
12073
|
+
throw error;
|
|
11541
12074
|
return { peerId: peer.peerId, role: peer.role, error: errorMessage(error) };
|
|
11542
12075
|
}
|
|
11543
12076
|
}));
|
|
@@ -12593,6 +13126,20 @@ var RobloxStudioTools = class {
|
|
|
12593
13126
|
multiplayerGroups: this.bridge.getConnectedMultiplayerGroups()
|
|
12594
13127
|
});
|
|
12595
13128
|
}
|
|
13129
|
+
async getRequestStatus(request_id) {
|
|
13130
|
+
if (typeof request_id !== "string" || request_id.length === 0 || request_id.length > 128) {
|
|
13131
|
+
throw new Error("request_id must contain between 1 and 128 characters");
|
|
13132
|
+
}
|
|
13133
|
+
const status = await this.bridge.getRequestStatusEverywhere(request_id);
|
|
13134
|
+
if (status)
|
|
13135
|
+
return this._textResult({ ...status });
|
|
13136
|
+
return this._textResult({
|
|
13137
|
+
requestId: request_id,
|
|
13138
|
+
state: "unknown",
|
|
13139
|
+
outcome: "unknown",
|
|
13140
|
+
message: "No retained operation record. It may have expired, been evicted, or belonged to an earlier server session. Do not infer that the mutation was not executed."
|
|
13141
|
+
});
|
|
13142
|
+
}
|
|
12596
13143
|
// === Asset Tools ===
|
|
12597
13144
|
async searchAssets(assetType, query, maxResults, sortBy, robloxCreatedOnly) {
|
|
12598
13145
|
const normalized = normalizeCreatorStoreSearch(assetType, query);
|
|
@@ -13435,6 +13982,42 @@ var RobloxStudioTools = class {
|
|
|
13435
13982
|
// ../core/dist/proxy-bridge-service.js
|
|
13436
13983
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
13437
13984
|
var PROXY_RESPONSE_GRACE_MS = 5e3;
|
|
13985
|
+
function parseRequestStatus(value) {
|
|
13986
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13987
|
+
throw new Error("Proxy returned an invalid request status");
|
|
13988
|
+
if (!("requestId" in value) || typeof value.requestId !== "string" || !("targetPeerId" in value) || typeof value.targetPeerId !== "string" || !("queuedAt" in value) || typeof value.queuedAt !== "number" || !("stage" in value) || !isRequestStage(value.stage) || !("state" in value) || value.state !== "pending" && value.state !== "timed_out" && value.state !== "aborted" && value.state !== "disconnected" && value.state !== "settled" || !("outcome" in value) || value.outcome !== "pending" && value.outcome !== "not_executed" && value.outcome !== "unknown" && value.outcome !== "success" && value.outcome !== "error")
|
|
13989
|
+
throw new Error("Proxy returned an invalid request status");
|
|
13990
|
+
const status = {
|
|
13991
|
+
requestId: value.requestId,
|
|
13992
|
+
targetPeerId: value.targetPeerId,
|
|
13993
|
+
queuedAt: value.queuedAt,
|
|
13994
|
+
stage: value.stage,
|
|
13995
|
+
state: value.state,
|
|
13996
|
+
outcome: value.outcome,
|
|
13997
|
+
...parseObservations(value)
|
|
13998
|
+
};
|
|
13999
|
+
if ("dispatchedAt" in value && typeof value.dispatchedAt === "number")
|
|
14000
|
+
status.dispatchedAt = value.dispatchedAt;
|
|
14001
|
+
if ("settledAt" in value && typeof value.settledAt === "number")
|
|
14002
|
+
status.settledAt = value.settledAt;
|
|
14003
|
+
if ("waiterEndedAt" in value && typeof value.waiterEndedAt === "number")
|
|
14004
|
+
status.waiterEndedAt = value.waiterEndedAt;
|
|
14005
|
+
if ("response" in value)
|
|
14006
|
+
status.response = value.response;
|
|
14007
|
+
if ("error" in value)
|
|
14008
|
+
status.error = value.error;
|
|
14009
|
+
if ("resultUnavailable" in value) {
|
|
14010
|
+
const unavailable = value.resultUnavailable;
|
|
14011
|
+
if (!unavailable || typeof unavailable !== "object" || !("reason" in unavailable) || unavailable.reason !== "size_limit" && unavailable.reason !== "serialization_failed" && unavailable.reason !== "retention_capacity" || !("limitBytes" in unavailable) || typeof unavailable.limitBytes !== "number")
|
|
14012
|
+
throw new Error("Proxy returned invalid result availability");
|
|
14013
|
+
status.resultUnavailable = {
|
|
14014
|
+
reason: unavailable.reason,
|
|
14015
|
+
limitBytes: unavailable.limitBytes,
|
|
14016
|
+
..."bytes" in unavailable && typeof unavailable.bytes === "number" ? { bytes: unavailable.bytes } : {}
|
|
14017
|
+
};
|
|
14018
|
+
}
|
|
14019
|
+
return status;
|
|
14020
|
+
}
|
|
13438
14021
|
function peerPublicationChanged(previous, current) {
|
|
13439
14022
|
return previous === void 0 || previous.transportPeerId !== current.transportPeerId || previous.instanceId !== current.instanceId || previous.multiplayerGroupId !== current.multiplayerGroupId || previous.role !== current.role || previous.placeId !== current.placeId || previous.placeName !== current.placeName || previous.placeKey !== current.placeKey || previous.dataModelName !== current.dataModelName || previous.isRunning !== current.isRunning || previous.pluginVersion !== current.pluginVersion || previous.pluginVariant !== current.pluginVariant || previous.serverVersion !== current.serverVersion;
|
|
13440
14023
|
}
|
|
@@ -13587,16 +14170,52 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
13587
14170
|
/** Called when this proxy is being discarded (e.g. promotion to primary
|
|
13588
14171
|
replaced it). Stops the background refresh so it doesn't leak. */
|
|
13589
14172
|
stop() {
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
14173
|
+
clearInterval(this.refreshTimer);
|
|
14174
|
+
this.refreshTimer = void 0;
|
|
14175
|
+
}
|
|
14176
|
+
async getRequestStatusEverywhere(requestId) {
|
|
14177
|
+
const response = await fetch(`${this.primaryBaseUrl}/request-status?requestId=${encodeURIComponent(requestId)}`, {
|
|
14178
|
+
headers: this.authHeaders()
|
|
14179
|
+
});
|
|
14180
|
+
if (!response.ok)
|
|
14181
|
+
throw new Error(`Proxy request status failed (${response.status})`);
|
|
14182
|
+
const body = await response.json();
|
|
14183
|
+
if (!body || typeof body !== "object" || !("status" in body))
|
|
14184
|
+
throw new Error("Proxy returned an invalid request status response");
|
|
14185
|
+
if (body.status === null)
|
|
14186
|
+
return void 0;
|
|
14187
|
+
const status = parseRequestStatus(body.status);
|
|
14188
|
+
if (status.requestId !== requestId)
|
|
14189
|
+
throw new Error("Proxy returned status for a different request");
|
|
14190
|
+
return status;
|
|
13594
14191
|
}
|
|
13595
|
-
async sendRequest(endpoint, data, targetPeerId, timeoutMs = this.proxyRequestTimeout, signal) {
|
|
14192
|
+
async sendRequest(endpoint, data, targetPeerId, timeoutMs = this.proxyRequestTimeout, signal, operationId) {
|
|
14193
|
+
const requestId = operationId ?? randomUUID4();
|
|
14194
|
+
const details = { requestId, targetPeerId, stage: "queued", outcome: "not_executed", executionOutcome: "not_executed" };
|
|
14195
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0 || requestId.length > 128) {
|
|
14196
|
+
throw new RequestFailure("operationId must be a nonempty string of at most 128 characters", "invalid_operation_id", details);
|
|
14197
|
+
}
|
|
13596
14198
|
if (signal?.aborted)
|
|
13597
|
-
throw new
|
|
14199
|
+
throw new RequestFailure(`Request aborted: ${requestId}; queued; not_executed`, "request_aborted", details);
|
|
13598
14200
|
const controller = new AbortController();
|
|
13599
14201
|
const effectiveTimeoutMs = Math.max(1, timeoutMs);
|
|
14202
|
+
let requestBody;
|
|
14203
|
+
try {
|
|
14204
|
+
requestBody = JSON.stringify({
|
|
14205
|
+
endpoint,
|
|
14206
|
+
data,
|
|
14207
|
+
targetPeerId,
|
|
14208
|
+
proxyInstanceId: this.proxyInstanceId,
|
|
14209
|
+
timeoutMs: effectiveTimeoutMs,
|
|
14210
|
+
operationId: requestId
|
|
14211
|
+
});
|
|
14212
|
+
} catch {
|
|
14213
|
+
throw new RequestFailure(`Request ${requestId} cannot be serialized at proxy admission; queued; not_executed`, "request_serialization_failed", details);
|
|
14214
|
+
}
|
|
14215
|
+
const requestBytes = Buffer.byteLength(requestBody);
|
|
14216
|
+
if (requestBytes > HTTP_BODY_LIMIT_BYTES) {
|
|
14217
|
+
throw new RequestFailure(`Request ${requestId} is ${requestBytes} bytes at proxy_send; limit ${HTTP_BODY_LIMIT_BYTES} bytes; queued; not_executed`, "request_too_large", { ...details, bytes: requestBytes, limitBytes: HTTP_BODY_LIMIT_BYTES, transportStage: "proxy_send" });
|
|
14218
|
+
}
|
|
13600
14219
|
let timedOut = false;
|
|
13601
14220
|
const abortFromCaller = () => controller.abort();
|
|
13602
14221
|
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
@@ -13606,39 +14225,54 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
13606
14225
|
timedOut = true;
|
|
13607
14226
|
controller.abort();
|
|
13608
14227
|
}, effectiveTimeoutMs + PROXY_RESPONSE_GRACE_MS);
|
|
14228
|
+
let primaryError = false;
|
|
13609
14229
|
try {
|
|
13610
14230
|
const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
|
|
13611
14231
|
method: "POST",
|
|
13612
14232
|
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
13613
|
-
body:
|
|
13614
|
-
endpoint,
|
|
13615
|
-
data,
|
|
13616
|
-
targetPeerId,
|
|
13617
|
-
proxyInstanceId: this.proxyInstanceId,
|
|
13618
|
-
timeoutMs: effectiveTimeoutMs
|
|
13619
|
-
}),
|
|
14233
|
+
body: requestBody,
|
|
13620
14234
|
signal: controller.signal
|
|
13621
14235
|
});
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
14236
|
+
const body = await response.text();
|
|
14237
|
+
let result;
|
|
14238
|
+
try {
|
|
14239
|
+
result = JSON.parse(body);
|
|
14240
|
+
} catch {
|
|
14241
|
+
throw new Error(`Proxy request failed (${response.status}): ${body || response.statusText}`);
|
|
13625
14242
|
}
|
|
13626
|
-
const result = await response.json();
|
|
13627
14243
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
13628
14244
|
throw new Error("Proxy returned an invalid response");
|
|
13629
14245
|
}
|
|
13630
|
-
if ("error" in result &&
|
|
13631
|
-
|
|
14246
|
+
if ("error" in result && result.error !== void 0 && typeof result.error !== "string") {
|
|
14247
|
+
primaryError = true;
|
|
14248
|
+
throw result.error;
|
|
14249
|
+
}
|
|
14250
|
+
if ("error" in result && typeof result.error === "string") {
|
|
14251
|
+
const details2 = "details" in result ? parseFailureDetails(result.details, { requestId, targetPeerId }) : void 0;
|
|
14252
|
+
if (details2 && "code" in result && typeof result.code === "string") {
|
|
14253
|
+
throw new RequestFailure(result.error, result.code, details2);
|
|
14254
|
+
}
|
|
14255
|
+
throw new RequestFailure(result.error, "studio_response_error", {
|
|
14256
|
+
requestId,
|
|
14257
|
+
targetPeerId,
|
|
14258
|
+
stage: "dispatched",
|
|
14259
|
+
outcome: "unknown",
|
|
14260
|
+
executionOutcome: "unknown"
|
|
14261
|
+
});
|
|
14262
|
+
}
|
|
14263
|
+
if (!response.ok) {
|
|
14264
|
+
throw new Error(`Proxy request failed (${response.status}): ${body || response.statusText}`);
|
|
13632
14265
|
}
|
|
13633
14266
|
return "response" in result ? result.response : void 0;
|
|
13634
14267
|
} catch (error) {
|
|
14268
|
+
if (error instanceof RequestFailure || primaryError)
|
|
14269
|
+
throw error;
|
|
13635
14270
|
const isAbortError = error instanceof Error ? error.name === "AbortError" : !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
13636
14271
|
if (isAbortError) {
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
throw new Error("Proxy request timeout");
|
|
14272
|
+
const message = !timedOut && signal?.aborted ? "Request aborted" : "Proxy request timeout";
|
|
14273
|
+
throw new RequestFailure(`${message}: ${requestId}; dispatched to primary; unknown; use get_request_status`, !timedOut && signal?.aborted ? "request_aborted" : "proxy_request_timeout", { requestId, targetPeerId, stage: "dispatched", outcome: "unknown", executionOutcome: "unknown", transportStage: "proxy_send" });
|
|
13640
14274
|
}
|
|
13641
|
-
throw error;
|
|
14275
|
+
throw new RequestFailure(`Proxy connection lost: ${requestId}; dispatched to primary; unknown; use get_request_status. ${error instanceof Error ? error.message : String(error)}`, "proxy_connection_lost", { requestId, targetPeerId, stage: "dispatched", outcome: "unknown", executionOutcome: "unknown", connectionLostAt: Date.now(), transportStage: "proxy_send" });
|
|
13642
14276
|
} finally {
|
|
13643
14277
|
clearTimeout(timeoutId);
|
|
13644
14278
|
signal?.removeEventListener("abort", abortFromCaller);
|
|
@@ -13650,6 +14284,251 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
13650
14284
|
}
|
|
13651
14285
|
};
|
|
13652
14286
|
|
|
14287
|
+
// ../core/dist/stdio-transport.js
|
|
14288
|
+
import { parseJSONRPCMessage } from "@modelcontextprotocol/server";
|
|
14289
|
+
import { TextDecoder } from "util";
|
|
14290
|
+
var MAX_STDIO_LINE_BYTES = 80 * 1024 * 1024;
|
|
14291
|
+
var MAX_STDIO_PENDING_OUTPUT_BYTES = 256 * 1024 * 1024;
|
|
14292
|
+
var INITIAL_BUFFER_BYTES = 64 * 1024;
|
|
14293
|
+
var BoundedStdioTransport = class {
|
|
14294
|
+
input;
|
|
14295
|
+
output;
|
|
14296
|
+
onclose;
|
|
14297
|
+
onerror;
|
|
14298
|
+
onmessage;
|
|
14299
|
+
limitBytes;
|
|
14300
|
+
outputLimitBytes;
|
|
14301
|
+
decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
14302
|
+
buffer;
|
|
14303
|
+
lineBytes = 0;
|
|
14304
|
+
started = false;
|
|
14305
|
+
closed = false;
|
|
14306
|
+
inputEnded = false;
|
|
14307
|
+
outputBlocked = false;
|
|
14308
|
+
pendingChunk;
|
|
14309
|
+
pendingWrites = 0;
|
|
14310
|
+
pendingSends = /* @__PURE__ */ new Set();
|
|
14311
|
+
constructor(input = process.stdin, output = process.stdout, options = {}) {
|
|
14312
|
+
this.input = input;
|
|
14313
|
+
this.output = output;
|
|
14314
|
+
this.limitBytes = options.maxBufferSize ?? MAX_STDIO_LINE_BYTES;
|
|
14315
|
+
this.outputLimitBytes = options.maxPendingOutputBytes ?? MAX_STDIO_PENDING_OUTPUT_BYTES;
|
|
14316
|
+
if (!Number.isSafeInteger(this.limitBytes) || this.limitBytes < 1) {
|
|
14317
|
+
throw new RangeError("Stdio line limit must be a positive safe integer");
|
|
14318
|
+
}
|
|
14319
|
+
if (!Number.isSafeInteger(this.outputLimitBytes) || this.outputLimitBytes < 1) {
|
|
14320
|
+
throw new RangeError("Stdio output limit must be a positive safe integer");
|
|
14321
|
+
}
|
|
14322
|
+
}
|
|
14323
|
+
async start() {
|
|
14324
|
+
if (this.started || this.closed)
|
|
14325
|
+
throw new Error("Stdio transport cannot be started again");
|
|
14326
|
+
if (this.input.readableEncoding)
|
|
14327
|
+
throw new Error("Stdio transport requires a byte stream, not a decoded string stream");
|
|
14328
|
+
this.started = true;
|
|
14329
|
+
this.input.on("data", this.onData);
|
|
14330
|
+
this.input.on("error", this.onStreamError);
|
|
14331
|
+
this.input.on("end", this.onEnd);
|
|
14332
|
+
this.input.on("close", this.onInputClose);
|
|
14333
|
+
this.output.on("error", this.onStreamError);
|
|
14334
|
+
this.output.on("close", this.onOutputClose);
|
|
14335
|
+
this.output.on("drain", this.onDrain);
|
|
14336
|
+
}
|
|
14337
|
+
onData = (chunk) => {
|
|
14338
|
+
let offset = 0;
|
|
14339
|
+
while (offset < chunk.length && !this.closed) {
|
|
14340
|
+
if (this.outputBlocked) {
|
|
14341
|
+
this.pendingChunk = chunk.subarray(offset);
|
|
14342
|
+
return;
|
|
14343
|
+
}
|
|
14344
|
+
const newline = chunk.indexOf(10, offset);
|
|
14345
|
+
const end = newline === -1 ? chunk.length : newline;
|
|
14346
|
+
const bytes = end - offset;
|
|
14347
|
+
const previousBytes = this.lineBytes;
|
|
14348
|
+
this.lineBytes += bytes;
|
|
14349
|
+
if (this.lineBytes > this.limitBytes) {
|
|
14350
|
+
this.buffer = void 0;
|
|
14351
|
+
} else if (previousBytes === 0 && newline !== -1) {
|
|
14352
|
+
this.buffer = chunk.subarray(offset, end);
|
|
14353
|
+
} else if (bytes > 0) {
|
|
14354
|
+
if (!this.buffer || this.buffer.length < this.lineBytes) {
|
|
14355
|
+
const capacity = Math.min(this.limitBytes, Math.max(INITIAL_BUFFER_BYTES, this.lineBytes, (this.buffer?.length ?? 0) * 2));
|
|
14356
|
+
const grown = Buffer.allocUnsafe(capacity);
|
|
14357
|
+
this.buffer?.copy(grown, 0, 0, previousBytes);
|
|
14358
|
+
this.buffer = grown;
|
|
14359
|
+
}
|
|
14360
|
+
chunk.copy(this.buffer, previousBytes, offset, end);
|
|
14361
|
+
}
|
|
14362
|
+
offset = end + 1;
|
|
14363
|
+
if (newline !== -1)
|
|
14364
|
+
this.finishLine();
|
|
14365
|
+
}
|
|
14366
|
+
};
|
|
14367
|
+
finishLine() {
|
|
14368
|
+
const bytes = this.lineBytes;
|
|
14369
|
+
const buffer = this.buffer;
|
|
14370
|
+
this.lineBytes = 0;
|
|
14371
|
+
this.buffer = void 0;
|
|
14372
|
+
if (bytes > this.limitBytes) {
|
|
14373
|
+
void this.rejectLine(-32600, "stdio_request_too_large", bytes).catch(() => {
|
|
14374
|
+
});
|
|
14375
|
+
return;
|
|
14376
|
+
}
|
|
14377
|
+
let value;
|
|
14378
|
+
try {
|
|
14379
|
+
value = JSON.parse(this.decoder.decode(buffer?.subarray(0, bytes)));
|
|
14380
|
+
} catch {
|
|
14381
|
+
void this.rejectLine(-32700, "stdio_parse_error", bytes).catch(() => {
|
|
14382
|
+
});
|
|
14383
|
+
return;
|
|
14384
|
+
}
|
|
14385
|
+
let message;
|
|
14386
|
+
try {
|
|
14387
|
+
message = parseJSONRPCMessage(value);
|
|
14388
|
+
} catch {
|
|
14389
|
+
void this.rejectLine(-32600, "stdio_invalid_request", bytes).catch(() => {
|
|
14390
|
+
});
|
|
14391
|
+
return;
|
|
14392
|
+
}
|
|
14393
|
+
try {
|
|
14394
|
+
this.onmessage?.(message);
|
|
14395
|
+
} catch (error) {
|
|
14396
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
rejectLine(code, reason, bytes) {
|
|
14400
|
+
const message = `${reason}: received ${bytes} bytes; limit ${this.limitBytes} bytes; stdio_receive; not_executed`;
|
|
14401
|
+
this.onerror?.(new Error(message));
|
|
14402
|
+
return this.writeMessage({
|
|
14403
|
+
jsonrpc: "2.0",
|
|
14404
|
+
id: null,
|
|
14405
|
+
error: {
|
|
14406
|
+
code,
|
|
14407
|
+
message,
|
|
14408
|
+
data: {
|
|
14409
|
+
code: reason,
|
|
14410
|
+
bytes,
|
|
14411
|
+
limitBytes: this.limitBytes,
|
|
14412
|
+
stage: "stdio_receive",
|
|
14413
|
+
transportStage: "stdio_receive",
|
|
14414
|
+
outcome: "not_executed",
|
|
14415
|
+
executionOutcome: "not_executed"
|
|
14416
|
+
}
|
|
14417
|
+
}
|
|
14418
|
+
});
|
|
14419
|
+
}
|
|
14420
|
+
onEnd = () => {
|
|
14421
|
+
if (this.closed)
|
|
14422
|
+
return;
|
|
14423
|
+
this.inputEnded = true;
|
|
14424
|
+
if (this.pendingChunk)
|
|
14425
|
+
return;
|
|
14426
|
+
const bytes = this.lineBytes;
|
|
14427
|
+
this.lineBytes = 0;
|
|
14428
|
+
this.buffer = void 0;
|
|
14429
|
+
if (bytes > 0) {
|
|
14430
|
+
const oversized = bytes > this.limitBytes;
|
|
14431
|
+
void this.rejectLine(oversized ? -32600 : -32700, oversized ? "stdio_request_too_large" : "stdio_truncated_line", bytes).finally(() => this.close()).catch(() => {
|
|
14432
|
+
});
|
|
14433
|
+
} else {
|
|
14434
|
+
void this.close();
|
|
14435
|
+
}
|
|
14436
|
+
};
|
|
14437
|
+
onInputClose = () => {
|
|
14438
|
+
if (!this.input.readableEnded)
|
|
14439
|
+
this.onEnd();
|
|
14440
|
+
};
|
|
14441
|
+
onOutputClose = () => {
|
|
14442
|
+
void this.close();
|
|
14443
|
+
};
|
|
14444
|
+
onStreamError = (error) => {
|
|
14445
|
+
if (this.closed)
|
|
14446
|
+
return;
|
|
14447
|
+
this.onerror?.(error);
|
|
14448
|
+
void this.close();
|
|
14449
|
+
};
|
|
14450
|
+
onDrain = () => {
|
|
14451
|
+
this.outputBlocked = false;
|
|
14452
|
+
for (const send of this.pendingSends)
|
|
14453
|
+
send.resolve();
|
|
14454
|
+
this.pendingSends.clear();
|
|
14455
|
+
const chunk = this.pendingChunk;
|
|
14456
|
+
this.pendingChunk = void 0;
|
|
14457
|
+
if (chunk && !this.closed)
|
|
14458
|
+
this.onData(chunk);
|
|
14459
|
+
if (this.inputEnded && !this.pendingChunk && !this.closed)
|
|
14460
|
+
this.onEnd();
|
|
14461
|
+
if (!this.outputBlocked && !this.closed)
|
|
14462
|
+
this.input.resume();
|
|
14463
|
+
};
|
|
14464
|
+
send(message) {
|
|
14465
|
+
return this.writeMessage(message);
|
|
14466
|
+
}
|
|
14467
|
+
writeMessage(message) {
|
|
14468
|
+
if (this.closed)
|
|
14469
|
+
return Promise.reject(new Error("Stdio transport is closed"));
|
|
14470
|
+
let json;
|
|
14471
|
+
try {
|
|
14472
|
+
json = JSON.stringify(message) + "\n";
|
|
14473
|
+
} catch (error) {
|
|
14474
|
+
return Promise.reject(error);
|
|
14475
|
+
}
|
|
14476
|
+
const bytes = Buffer.byteLength(json);
|
|
14477
|
+
const queuedBytes = this.output.writableLength + bytes;
|
|
14478
|
+
if (queuedBytes > this.outputLimitBytes) {
|
|
14479
|
+
const error = new Error(`Stdio output backpressure capacity exceeded: ${queuedBytes} bytes; limit ${this.outputLimitBytes} bytes`);
|
|
14480
|
+
this.onStreamError(error);
|
|
14481
|
+
return Promise.reject(error);
|
|
14482
|
+
}
|
|
14483
|
+
const { promise, resolve: resolve5, reject } = Promise.withResolvers();
|
|
14484
|
+
const pending = { resolve: resolve5, reject };
|
|
14485
|
+
this.pendingSends.add(pending);
|
|
14486
|
+
this.pendingWrites++;
|
|
14487
|
+
try {
|
|
14488
|
+
const writable = this.output.write(Buffer.from(json), () => {
|
|
14489
|
+
this.pendingWrites--;
|
|
14490
|
+
if (this.closed && this.pendingWrites === 0) {
|
|
14491
|
+
queueMicrotask(() => this.output.off("error", this.onStreamError));
|
|
14492
|
+
}
|
|
14493
|
+
});
|
|
14494
|
+
if (writable) {
|
|
14495
|
+
this.pendingSends.delete(pending);
|
|
14496
|
+
resolve5();
|
|
14497
|
+
} else if (!this.closed) {
|
|
14498
|
+
this.outputBlocked = true;
|
|
14499
|
+
this.input.pause();
|
|
14500
|
+
}
|
|
14501
|
+
} catch (error) {
|
|
14502
|
+
this.pendingWrites--;
|
|
14503
|
+
this.onStreamError(error instanceof Error ? error : new Error(String(error)));
|
|
14504
|
+
}
|
|
14505
|
+
return promise;
|
|
14506
|
+
}
|
|
14507
|
+
async close() {
|
|
14508
|
+
if (this.closed)
|
|
14509
|
+
return;
|
|
14510
|
+
this.closed = true;
|
|
14511
|
+
this.input.off("data", this.onData);
|
|
14512
|
+
this.input.off("error", this.onStreamError);
|
|
14513
|
+
this.input.off("end", this.onEnd);
|
|
14514
|
+
this.input.off("close", this.onInputClose);
|
|
14515
|
+
if (this.pendingWrites === 0)
|
|
14516
|
+
this.output.off("error", this.onStreamError);
|
|
14517
|
+
this.output.off("close", this.onOutputClose);
|
|
14518
|
+
this.output.off("drain", this.onDrain);
|
|
14519
|
+
if (this.input.listenerCount("data") === 0)
|
|
14520
|
+
this.input.pause();
|
|
14521
|
+
this.buffer = void 0;
|
|
14522
|
+
this.pendingChunk = void 0;
|
|
14523
|
+
this.lineBytes = 0;
|
|
14524
|
+
const error = new Error("Stdio transport is closed");
|
|
14525
|
+
for (const send of this.pendingSends)
|
|
14526
|
+
send.reject(error);
|
|
14527
|
+
this.pendingSends.clear();
|
|
14528
|
+
this.onclose?.();
|
|
14529
|
+
}
|
|
14530
|
+
};
|
|
14531
|
+
|
|
13653
14532
|
// ../core/dist/server.js
|
|
13654
14533
|
var RobloxStudioMCPServer = class {
|
|
13655
14534
|
tools;
|
|
@@ -13743,7 +14622,10 @@ var RobloxStudioMCPServer = class {
|
|
|
13743
14622
|
throw new Error(`Unknown tool: ${name}`);
|
|
13744
14623
|
return handler(tools, args, invocation);
|
|
13745
14624
|
}
|
|
13746
|
-
}), {
|
|
14625
|
+
}), {
|
|
14626
|
+
transport: new BoundedStdioTransport(),
|
|
14627
|
+
onerror: (error) => console.error("[mcp:stdio]", error)
|
|
14628
|
+
});
|
|
13747
14629
|
console.error(`${this.config.name} v${this.config.version} running on stdio`);
|
|
13748
14630
|
if (primaryApp) {
|
|
13749
14631
|
primaryApp.setMCPServerActive(true);
|
|
@@ -13901,6 +14783,12 @@ var TOOL_DEFINITIONS = [
|
|
|
13901
14783
|
inputSchema: {
|
|
13902
14784
|
type: "object",
|
|
13903
14785
|
properties: {
|
|
14786
|
+
operation_id: {
|
|
14787
|
+
type: "string",
|
|
14788
|
+
minLength: 1,
|
|
14789
|
+
maxLength: 128,
|
|
14790
|
+
description: "Unique recovery ID; reuse only for identical arguments."
|
|
14791
|
+
},
|
|
13904
14792
|
instancePath: {
|
|
13905
14793
|
type: "string",
|
|
13906
14794
|
description: "Canonical path of the target instance."
|
|
@@ -14076,7 +14964,7 @@ var TOOL_DEFINITIONS = [
|
|
|
14076
14964
|
action: {
|
|
14077
14965
|
type: "string",
|
|
14078
14966
|
enum: ["get", "set", "view"],
|
|
14079
|
-
description: "View frames the
|
|
14967
|
+
description: "View frames a target or the current selection and preserves the camera type."
|
|
14080
14968
|
},
|
|
14081
14969
|
paths: {
|
|
14082
14970
|
type: "array",
|
|
@@ -14092,7 +14980,7 @@ var TOOL_DEFINITIONS = [
|
|
|
14092
14980
|
path: {
|
|
14093
14981
|
type: "string",
|
|
14094
14982
|
minLength: 1,
|
|
14095
|
-
description: "
|
|
14983
|
+
description: "Optional BasePart or Model path for view; omit to frame exactly one selected BasePart or Model."
|
|
14096
14984
|
},
|
|
14097
14985
|
from: {
|
|
14098
14986
|
type: "number",
|
|
@@ -14126,6 +15014,12 @@ var TOOL_DEFINITIONS = [
|
|
|
14126
15014
|
inputSchema: {
|
|
14127
15015
|
type: "object",
|
|
14128
15016
|
properties: {
|
|
15017
|
+
operation_id: {
|
|
15018
|
+
type: "string",
|
|
15019
|
+
minLength: 1,
|
|
15020
|
+
maxLength: 128,
|
|
15021
|
+
description: "Unique recovery ID; reuse only for identical arguments."
|
|
15022
|
+
},
|
|
14129
15023
|
code: {
|
|
14130
15024
|
type: "string",
|
|
14131
15025
|
description: "Luau code to execute."
|
|
@@ -14970,6 +15864,23 @@ var TOOL_DEFINITIONS = [
|
|
|
14970
15864
|
properties: {}
|
|
14971
15865
|
}
|
|
14972
15866
|
},
|
|
15867
|
+
{
|
|
15868
|
+
name: "get_request_status",
|
|
15869
|
+
category: "read",
|
|
15870
|
+
description: "Use to recover a retained Studio operation outcome after a timeout.",
|
|
15871
|
+
inputSchema: {
|
|
15872
|
+
type: "object",
|
|
15873
|
+
properties: {
|
|
15874
|
+
request_id: {
|
|
15875
|
+
type: "string",
|
|
15876
|
+
minLength: 1,
|
|
15877
|
+
maxLength: 128,
|
|
15878
|
+
description: "Caller operation ID or request ID from a server error."
|
|
15879
|
+
}
|
|
15880
|
+
},
|
|
15881
|
+
required: ["request_id"]
|
|
15882
|
+
}
|
|
15883
|
+
},
|
|
14973
15884
|
// === Asset Tools ===
|
|
14974
15885
|
{
|
|
14975
15886
|
name: "search_assets",
|