veryfront 0.1.1289-rc.25708 → 0.1.1289-rc.25721
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/docs/README.md +1 -1
- package/docs/config.json +1 -1
- package/docs/guides/tools.md +27 -2
- package/docs/manifest.json +6 -6
- package/esm/deno.js +1 -1
- package/esm/src/agent/ag-ui/connected-local-platform-tools.d.ts.map +1 -1
- package/esm/src/agent/ag-ui/connected-local-platform-tools.js +9 -2
- package/esm/src/agent/ag-ui/tool-shared.d.ts +2 -0
- package/esm/src/agent/ag-ui/tool-shared.d.ts.map +1 -1
- package/esm/src/agent/ag-ui/tool-shared.js +7 -0
- package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts +2 -0
- package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts.map +1 -1
- package/esm/src/agent/hosted/chat-runtime-tool-assembly.js +18 -2
- package/esm/src/agent/hosted/child-fork-tool-sources.d.ts +1 -0
- package/esm/src/agent/hosted/child-fork-tool-sources.d.ts.map +1 -1
- package/esm/src/agent/hosted/child-fork-tool-sources.js +49 -12
- package/esm/src/agent/hosted/default-chat-runtime.d.ts.map +1 -1
- package/esm/src/agent/hosted/default-chat-runtime.js +26 -1
- package/esm/src/agent/hosted/executor-runtime-facades.d.ts.map +1 -1
- package/esm/src/agent/hosted/executor-runtime-facades.js +3 -0
- package/esm/src/agent/hosted/executor-tool-schema.d.ts.map +1 -1
- package/esm/src/agent/hosted/executor-tool-schema.js +7 -1
- package/esm/src/agent/hosted/platform-dependency-credential.d.ts.map +1 -1
- package/esm/src/agent/hosted/platform-dependency-credential.js +26 -1
- package/esm/src/agent/hosted/project-remote-tool-source.d.ts.map +1 -1
- package/esm/src/agent/hosted/project-remote-tool-source.js +18 -0
- package/esm/src/agent/hosted/terminal-credential.d.ts.map +1 -1
- package/esm/src/agent/hosted/terminal-credential.js +9 -3
- package/esm/src/agent/platform-mcp-tool-source.d.ts.map +1 -1
- package/esm/src/agent/platform-mcp-tool-source.js +4 -0
- package/esm/src/agent/project/live-studio-mcp-tools.d.ts.map +1 -1
- package/esm/src/agent/project/live-studio-mcp-tools.js +14 -2
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +2269 -2191
- package/esm/src/agent/runtime/mcp-server-tool-sources.d.ts +2 -0
- package/esm/src/agent/runtime/mcp-server-tool-sources.d.ts.map +1 -1
- package/esm/src/agent/runtime/mcp-server-tool-sources.js +27 -0
- package/esm/src/agent/runtime/runtime-tool-config.d.ts +1 -0
- package/esm/src/agent/runtime/runtime-tool-config.d.ts.map +1 -1
- package/esm/src/agent/runtime/runtime-tool-config.js +4 -0
- package/esm/src/agent/runtime/tool-exposure.d.ts +4 -2
- package/esm/src/agent/runtime/tool-exposure.d.ts.map +1 -1
- package/esm/src/agent/runtime/tool-exposure.js +17 -4
- package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
- package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
- package/esm/src/internal-agents/run-stream.js +95 -23
- package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
- package/esm/src/server/handlers/request/agent-stream.handler.d.ts +2 -0
- package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/agent-stream.handler.js +24 -3
- package/esm/src/server/handlers/request/run-terminal-tool-source.d.ts.map +1 -1
- package/esm/src/server/handlers/request/run-terminal-tool-source.js +12 -3
- package/esm/src/tool/factory.d.ts +2 -0
- package/esm/src/tool/factory.d.ts.map +1 -1
- package/esm/src/tool/factory.js +7 -0
- package/esm/src/tool/host-tools.d.ts +2 -0
- package/esm/src/tool/host-tools.d.ts.map +1 -1
- package/esm/src/tool/host-tools.js +14 -0
- package/esm/src/tool/project-scoped-remote-tools.d.ts.map +1 -1
- package/esm/src/tool/project-scoped-remote-tools.js +12 -0
- package/esm/src/tool/registry.d.ts.map +1 -1
- package/esm/src/tool/registry.js +3 -0
- package/esm/src/tool/remote-mcp.d.ts +11 -0
- package/esm/src/tool/remote-mcp.d.ts.map +1 -1
- package/esm/src/tool/remote-mcp.js +685 -85
- package/esm/src/tool/remote-source-tools.d.ts.map +1 -1
- package/esm/src/tool/remote-source-tools.js +1 -0
- package/esm/src/tool/remote-tool-source-owner.d.ts +14 -0
- package/esm/src/tool/remote-tool-source-owner.d.ts.map +1 -0
- package/esm/src/tool/remote-tool-source-owner.js +65 -0
- package/esm/src/tool/types.d.ts +4 -0
- package/esm/src/tool/types.d.ts.map +1 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +9 -9
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
2
|
+
import { VERSION } from "../utils/version-constant.js";
|
|
1
3
|
import { defineOwnDataProperty } from "../security/own-data-property.js";
|
|
4
|
+
import { allPrivatePromises, chainPrivatePromise } from "../security/private-promise.js";
|
|
5
|
+
import { mapPrivateArray } from "../security/private-array.js";
|
|
6
|
+
import { testPrivateRegExp } from "../security/private-regexp.js";
|
|
7
|
+
import { privateTextEndsWith, privateTextStartsWith, privateTextTrim, } from "../security/private-text.js";
|
|
8
|
+
import { createPrivateSet } from "../security/private-set.js";
|
|
9
|
+
import { createPrivateWeakStore } from "../security/private-weak-store.js";
|
|
2
10
|
import { NETWORK_ERROR, TIMEOUT_ERROR } from "../errors/index.js";
|
|
3
11
|
import { readMcpToolDiscoveryMetadata } from "./discovery-metadata.js";
|
|
4
12
|
import { getApiBaseUrlEnv } from "../config/env.js";
|
|
@@ -36,6 +44,25 @@ const NativeURL = URL;
|
|
|
36
44
|
// The resolved headers can carry a bearer (the platform source's run token, a
|
|
37
45
|
// user source's OAuth token), so they are only touched through members
|
|
38
46
|
// captured before project code loaded.
|
|
47
|
+
const NativeMap = Map;
|
|
48
|
+
const NativeUint8Array = Uint8Array;
|
|
49
|
+
const MapPrototypeGet = Map.prototype.get;
|
|
50
|
+
const MapPrototypeSet = Map.prototype.set;
|
|
51
|
+
const MapPrototypeHas = Map.prototype.has;
|
|
52
|
+
const MapPrototypeDelete = Map.prototype.delete;
|
|
53
|
+
const MapPrototypeClear = Map.prototype.clear;
|
|
54
|
+
const MapPrototypeValues = Map.prototype.values;
|
|
55
|
+
const MapPrototypeIterator = Map.prototype[Symbol.iterator];
|
|
56
|
+
const MapIteratorNext = Object.getPrototypeOf(new Map().entries()).next;
|
|
57
|
+
const HeadersPrototypeForEach = Headers.prototype.forEach;
|
|
58
|
+
const digest = dntShim.crypto.subtle.digest.bind(dntShim.crypto.subtle);
|
|
59
|
+
const encodeUtf8 = TextEncoder.prototype.encode.bind(UTF8_ENCODER);
|
|
60
|
+
const nativeBtoa = btoa;
|
|
61
|
+
const nativeFromCodePoint = String.fromCodePoint;
|
|
62
|
+
const nativeIsSafeInteger = Number.isSafeInteger;
|
|
63
|
+
const objectHasOwn = Object.hasOwn;
|
|
64
|
+
const nativeIsArray = Array.isArray;
|
|
65
|
+
const typedArrayLengthGetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), "length").get;
|
|
39
66
|
const HeadersPrototypeSet = Headers.prototype.set;
|
|
40
67
|
const HeadersPrototypeGet = Headers.prototype.get;
|
|
41
68
|
const HeadersPrototypeDelete = Headers.prototype.delete;
|
|
@@ -50,11 +77,22 @@ const urlHashGetter = Object.getOwnPropertyDescriptor(NativeURL.prototype, "hash
|
|
|
50
77
|
const urlOriginGetter = Object.getOwnPropertyDescriptor(NativeURL.prototype, "origin")?.get;
|
|
51
78
|
const urlPathnameGetter = Object.getOwnPropertyDescriptor(NativeURL.prototype, "pathname")?.get;
|
|
52
79
|
class RemoteMCPHttpError extends Error {
|
|
53
|
-
|
|
80
|
+
status;
|
|
81
|
+
modernError;
|
|
82
|
+
constructor(status, modernError = false) {
|
|
54
83
|
super(`Remote MCP request failed (${status})`);
|
|
84
|
+
this.status = status;
|
|
85
|
+
this.modernError = modernError;
|
|
55
86
|
this.name = "RemoteMCPHttpError";
|
|
56
87
|
}
|
|
57
88
|
}
|
|
89
|
+
/** Internal signal: a session was (re)negotiated mid-pagination, so discovery restarts. */
|
|
90
|
+
class RemoteMCPDiscoveryRestart extends Error {
|
|
91
|
+
constructor() {
|
|
92
|
+
super("Remote MCP tools/list must restart in the renewed session");
|
|
93
|
+
this.name = "RemoteMCPDiscoveryRestart";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
58
96
|
class RemoteMCPOAuthExpiredHttpError extends RemoteMCPHttpError {
|
|
59
97
|
constructor(status) {
|
|
60
98
|
super(status);
|
|
@@ -63,7 +101,7 @@ class RemoteMCPOAuthExpiredHttpError extends RemoteMCPHttpError {
|
|
|
63
101
|
}
|
|
64
102
|
const JSON_RPC_TOOL_ERROR_RESULT = Symbol("veryfront.remote-mcp.json-rpc-tool-error");
|
|
65
103
|
function isRecord(value) {
|
|
66
|
-
return typeof value === "object" && value !== null && !
|
|
104
|
+
return typeof value === "object" && value !== null && !nativeIsArray(value);
|
|
67
105
|
}
|
|
68
106
|
function isJsonRpcErrorObject(value) {
|
|
69
107
|
return isRecord(value) &&
|
|
@@ -130,15 +168,15 @@ function isJsonStructureWithin(value, maxDepth, maxNodes) {
|
|
|
130
168
|
}
|
|
131
169
|
return true;
|
|
132
170
|
}
|
|
133
|
-
function normalizeParameters(
|
|
134
|
-
const snapshot = snapshotBoundedJsonValue(
|
|
171
|
+
function normalizeParameters(schema, toolName, kind = "input") {
|
|
172
|
+
const snapshot = snapshotBoundedJsonValue(schema);
|
|
135
173
|
if (!snapshot.success ||
|
|
136
174
|
!isRecord(snapshot.value) ||
|
|
137
175
|
!isJsonStructureWithin(snapshot.value, MAX_REMOTE_MCP_TOOL_SCHEMA_DEPTH, MAX_REMOTE_MCP_TOOL_SCHEMA_NODES) ||
|
|
138
176
|
serializedByteLength(snapshot.value) > MAX_REMOTE_MCP_TOOL_SCHEMA_BYTES) {
|
|
139
|
-
throw protocolError(`Remote MCP tools/list returned a malformed
|
|
177
|
+
throw protocolError(`Remote MCP tools/list returned a malformed ${kind} schema for tool "${toolName}"`);
|
|
140
178
|
}
|
|
141
|
-
if (Object.keys(snapshot.value).length === 0) {
|
|
179
|
+
if (kind === "input" && Object.keys(snapshot.value).length === 0) {
|
|
142
180
|
return { type: "object", properties: {} };
|
|
143
181
|
}
|
|
144
182
|
return snapshot.value;
|
|
@@ -158,6 +196,9 @@ function normalizeToolDefinition(entry, index) {
|
|
|
158
196
|
name: entry.name,
|
|
159
197
|
description: entry.description,
|
|
160
198
|
parameters: normalizeParameters(entry.inputSchema, entry.name),
|
|
199
|
+
...(entry.outputSchema === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { outputSchema: normalizeParameters(entry.outputSchema, entry.name, "output") }),
|
|
161
202
|
};
|
|
162
203
|
// Navigation hints only. Invalid metadata is ignored rather than rejecting the tool.
|
|
163
204
|
const discovery = readMcpToolDiscoveryMetadata(entry);
|
|
@@ -584,9 +625,9 @@ function validateEndpoint(endpoint) {
|
|
|
584
625
|
}
|
|
585
626
|
return reflectApply(urlHrefGetter, url, []);
|
|
586
627
|
}
|
|
587
|
-
async function postJsonRpc(endpoint, headers, body, requestFetch, callerSignal, maxResponseBytes) {
|
|
628
|
+
async function postJsonRpc(endpoint, headers, body, requestFetch, callerSignal, maxResponseBytes, onResponse) {
|
|
588
629
|
const expectedId = body.id;
|
|
589
|
-
if (typeof expectedId !== "string" || expectedId.length === 0) {
|
|
630
|
+
if (expectedId !== undefined && (typeof expectedId !== "string" || expectedId.length === 0)) {
|
|
590
631
|
throw new TypeError("Remote MCP JSON-RPC request id must be a non-empty string");
|
|
591
632
|
}
|
|
592
633
|
const serializedBody = serializeJsonRpcRequest(body);
|
|
@@ -610,7 +651,20 @@ async function postJsonRpc(endpoint, headers, body, requestFetch, callerSignal,
|
|
|
610
651
|
if (isOauthExpiredMessage(detail)) {
|
|
611
652
|
throw new RemoteMCPOAuthExpiredHttpError(response.status);
|
|
612
653
|
}
|
|
613
|
-
|
|
654
|
+
let modernError = false;
|
|
655
|
+
try {
|
|
656
|
+
const payload = JSON.parse(text);
|
|
657
|
+
modernError = isRecord(payload) && isRecord(payload.error) &&
|
|
658
|
+
(payload.error.code === -32020 || payload.error.code === -32021 ||
|
|
659
|
+
payload.error.code === -32022 || payload.error.code === -32601);
|
|
660
|
+
}
|
|
661
|
+
catch { /* Legacy servers can return plain text errors. */ }
|
|
662
|
+
throw new RemoteMCPHttpError(response.status, modernError);
|
|
663
|
+
}
|
|
664
|
+
onResponse?.(response);
|
|
665
|
+
if (expectedId === undefined) {
|
|
666
|
+
discardResponseBody(response);
|
|
667
|
+
return undefined;
|
|
614
668
|
}
|
|
615
669
|
return await parseJsonRpcResponse(response, expectedId, maxResponseBytes, requestScope.signal);
|
|
616
670
|
}
|
|
@@ -742,69 +796,545 @@ function buildRunContextMeta(context, endpoint) {
|
|
|
742
796
|
}
|
|
743
797
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
744
798
|
}
|
|
799
|
+
const MODERN_MCP_VERSION = "2026-07-28";
|
|
800
|
+
const LEGACY_MCP_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
801
|
+
const MCP_CLIENT_INFO = { name: "veryfront", version: VERSION };
|
|
802
|
+
/** Validate a minted session identifier without consulting mutable RegExp methods. */
|
|
803
|
+
function isValidMcpSessionId(sessionId) {
|
|
804
|
+
return !!sessionId && sessionId.length <= 4096 && testPrivateRegExp(/^[\x21-\x7e]+$/, sessionId);
|
|
805
|
+
}
|
|
806
|
+
function encodeMcpHeader(value) {
|
|
807
|
+
if (testPrivateRegExp(/^[\x20-\x7e]*$/, value) && privateTextTrim(value) === value &&
|
|
808
|
+
!(privateTextStartsWith(value, "=?base64?") && privateTextEndsWith(value, "?=")))
|
|
809
|
+
return value;
|
|
810
|
+
const bytes = encodeUtf8(value);
|
|
811
|
+
let binary = "";
|
|
812
|
+
const length = reflectApply(typedArrayLengthGetter, bytes, []);
|
|
813
|
+
for (let index = 0; index < length; index++) {
|
|
814
|
+
binary += nativeFromCodePoint(bytes[index]);
|
|
815
|
+
}
|
|
816
|
+
return `=?base64?${nativeBtoa(binary)}?=`;
|
|
817
|
+
}
|
|
818
|
+
/** Build the headers and body for one JSON-RPC request in a modern or legacy session. */
|
|
819
|
+
function buildMcpRequest(headers, body, session, annotations) {
|
|
820
|
+
const finalHeaders = copyNativeHeaders(headers);
|
|
821
|
+
const version = session?.version ?? MODERN_MCP_VERSION;
|
|
822
|
+
reflectApply(HeadersPrototypeSet, finalHeaders, ["MCP-Protocol-Version", version]);
|
|
823
|
+
reflectApply(HeadersPrototypeSet, finalHeaders, ["Mcp-Method", body.method]);
|
|
824
|
+
const params = isRecord(body.params) ? body.params : {};
|
|
825
|
+
reflectApply(HeadersPrototypeDelete, finalHeaders, ["Mcp-Name"]);
|
|
826
|
+
if (typeof params.name === "string") {
|
|
827
|
+
if (!session && isRecord(params.arguments)) {
|
|
828
|
+
addMcpParameterHeaders(finalHeaders, annotations?.get(params.name) ?? [], params.arguments);
|
|
829
|
+
}
|
|
830
|
+
reflectApply(HeadersPrototypeSet, finalHeaders, ["Mcp-Name", encodeMcpHeader(params.name)]);
|
|
831
|
+
}
|
|
832
|
+
if (session?.sessionId) {
|
|
833
|
+
reflectApply(HeadersPrototypeSet, finalHeaders, ["Mcp-Session-Id", session.sessionId]);
|
|
834
|
+
}
|
|
835
|
+
if (session)
|
|
836
|
+
return { headers: finalHeaders, body };
|
|
837
|
+
return {
|
|
838
|
+
headers: finalHeaders,
|
|
839
|
+
body: {
|
|
840
|
+
...body,
|
|
841
|
+
params: {
|
|
842
|
+
...params,
|
|
843
|
+
_meta: {
|
|
844
|
+
...(isRecord(params._meta) ? params._meta : {}),
|
|
845
|
+
"io.modelcontextprotocol/protocolVersion": version,
|
|
846
|
+
"io.modelcontextprotocol/clientInfo": MCP_CLIENT_INFO,
|
|
847
|
+
"io.modelcontextprotocol/clientCapabilities": {},
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/** Whether a failed request may negotiate or renew a legacy session. */
|
|
854
|
+
function shouldNegotiateSession(error, session, operation) {
|
|
855
|
+
if (!(error instanceof RemoteMCPHttpError) ||
|
|
856
|
+
error instanceof RemoteMCPOAuthExpiredHttpError || error.modernError)
|
|
857
|
+
return false;
|
|
858
|
+
// Only discovery may treat an unrecognized 400 as a legacy server; a tool
|
|
859
|
+
// call could have taken effect before failing, so it is never replayed.
|
|
860
|
+
if (!session)
|
|
861
|
+
return operation !== "execution" && error.status === 400;
|
|
862
|
+
return !!session.sessionId && error.status === 404;
|
|
863
|
+
}
|
|
864
|
+
/** Accumulate paginated tools/list results under the aggregate catalog bounds. */
|
|
865
|
+
function createToolListAccumulator() {
|
|
866
|
+
const annotations = new Map();
|
|
867
|
+
const definitions = [];
|
|
868
|
+
// Every returned definition counts toward the bound, including tools
|
|
869
|
+
// excluded below for invalid header annotations.
|
|
870
|
+
const names = new Set();
|
|
871
|
+
const seenCursors = new Set();
|
|
872
|
+
return {
|
|
873
|
+
annotations,
|
|
874
|
+
definitions,
|
|
875
|
+
accept(page) {
|
|
876
|
+
if (names.size + page.length > MAX_REMOTE_MCP_TOOL_DEFINITIONS) {
|
|
877
|
+
throw protocolError(`Remote MCP tools/list cannot contain more than ${MAX_REMOTE_MCP_TOOL_DEFINITIONS} tools`);
|
|
878
|
+
}
|
|
879
|
+
for (const definition of page) {
|
|
880
|
+
if (names.has(definition.name)) {
|
|
881
|
+
throw protocolError(`Remote MCP tools/list returned duplicate tool name "${definition.name}"`);
|
|
882
|
+
}
|
|
883
|
+
names.add(definition.name);
|
|
884
|
+
const headers = readMcpParameterHeaders(definition.parameters);
|
|
885
|
+
if (!headers)
|
|
886
|
+
continue;
|
|
887
|
+
annotations.set(definition.name, headers);
|
|
888
|
+
definitions.push(definition);
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
advance(cursor) {
|
|
892
|
+
if (seenCursors.has(cursor)) {
|
|
893
|
+
throw protocolError(`Remote MCP tools/list returned repeated pagination cursor "${cursor}"`);
|
|
894
|
+
}
|
|
895
|
+
seenCursors.add(cursor);
|
|
896
|
+
return cursor;
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/** Best-effort session cleanup is bounded in aggregate so teardown cannot stall a run. */
|
|
901
|
+
const REMOTE_MCP_SESSION_CLEANUP_BUDGET_MS = 5_000;
|
|
902
|
+
/** Send one session DELETE; resolves with the failure instead of rejecting. */
|
|
903
|
+
async function deleteMcpSession(cleanup, getRequestFetch, budget) {
|
|
904
|
+
const scope = createRequestSignalScope(budget);
|
|
905
|
+
try {
|
|
906
|
+
const headers = copyNativeHeaders(cleanup.headers);
|
|
907
|
+
reflectApply(HeadersPrototypeDelete, headers, ["Mcp-Method"]);
|
|
908
|
+
const requestFetch = getRequestFetch(cleanup.endpoint);
|
|
909
|
+
const init = createNativeRequestInit(undefined, {
|
|
910
|
+
method: "DELETE",
|
|
911
|
+
headers,
|
|
912
|
+
signal: scope.signal,
|
|
913
|
+
redirect: "error",
|
|
914
|
+
});
|
|
915
|
+
// The retained Authorization and Mcp-Session-Id must not reach replaced
|
|
916
|
+
// Request or Headers members, exactly as for the POST dispatch.
|
|
917
|
+
assertNativeRequestProcessing();
|
|
918
|
+
const response = await requestFetch(cleanup.endpoint, init);
|
|
919
|
+
discardResponseBody(response);
|
|
920
|
+
if (!response.ok && response.status !== 404 && response.status !== 405) {
|
|
921
|
+
return new RemoteMCPHttpError(response.status);
|
|
922
|
+
}
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
return error ?? new Error("Remote MCP session cleanup failed");
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
scope.dispose();
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Send DELETE for every retained session concurrently under one aggregate
|
|
934
|
+
* budget. Returns the first failure, if any, after all attempts settle.
|
|
935
|
+
*/
|
|
936
|
+
async function terminateMcpSessions(cleanups, getRequestFetch) {
|
|
937
|
+
if (cleanups.length === 0)
|
|
938
|
+
return undefined;
|
|
939
|
+
const budget = new AbortController();
|
|
940
|
+
const timer = setTimeout(() => budget.abort(new DOMException(`Remote MCP session cleanup exceeded ${REMOTE_MCP_SESSION_CLEANUP_BUDGET_MS}ms`, "TimeoutError")), REMOTE_MCP_SESSION_CLEANUP_BUDGET_MS);
|
|
941
|
+
let outcomes;
|
|
942
|
+
try {
|
|
943
|
+
outcomes = await allPrivatePromises(mapPrivateArray(cleanups, (cleanup) => deleteMcpSession(cleanup, getRequestFetch, budget.signal)));
|
|
944
|
+
}
|
|
945
|
+
finally {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
}
|
|
948
|
+
for (const outcome of outcomes) {
|
|
949
|
+
if (outcome !== undefined)
|
|
950
|
+
return outcome;
|
|
951
|
+
}
|
|
952
|
+
return undefined;
|
|
953
|
+
}
|
|
954
|
+
/** Validate one schema-declared header annotation, or return undefined if invalid. */
|
|
955
|
+
function readHeaderAnnotation(value, path, reachable, names) {
|
|
956
|
+
const name = value["x-mcp-header"];
|
|
957
|
+
if (!reachable || typeof name !== "string")
|
|
958
|
+
return undefined;
|
|
959
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || names.has(name.toLowerCase())) {
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
const type = value.type;
|
|
963
|
+
if (type !== "string" && type !== "integer" && type !== "boolean")
|
|
964
|
+
return undefined;
|
|
965
|
+
return { path, name, type };
|
|
966
|
+
}
|
|
967
|
+
/** Inspect only bounded, normalized JSON schemas, never application objects. */
|
|
968
|
+
function readMcpParameterHeaders(schema) {
|
|
969
|
+
const result = [];
|
|
970
|
+
const names = new Set();
|
|
971
|
+
let valid = true;
|
|
972
|
+
function visit(value, path, reachable) {
|
|
973
|
+
if (Array.isArray(value)) {
|
|
974
|
+
for (const item of value)
|
|
975
|
+
visit(item, path, false);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (!isRecord(value))
|
|
979
|
+
return;
|
|
980
|
+
if (Object.hasOwn(value, "x-mcp-header")) {
|
|
981
|
+
const annotation = readHeaderAnnotation(value, path, reachable, names);
|
|
982
|
+
if (annotation) {
|
|
983
|
+
names.add(annotation.name.toLowerCase());
|
|
984
|
+
result.push(annotation);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
valid = false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
for (const [key, child] of Object.entries(value)) {
|
|
991
|
+
if (key === "properties" && isRecord(child))
|
|
992
|
+
visitProperties(child, path, reachable);
|
|
993
|
+
else if (key !== "x-mcp-header")
|
|
994
|
+
visit(child, path, false);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function visitProperties(properties, path, reachable) {
|
|
998
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
999
|
+
visit(property, [...path, name], reachable);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (Object.hasOwn(schema, "x-mcp-header"))
|
|
1003
|
+
return undefined;
|
|
1004
|
+
// The root is an object container. Only its properties can carry annotations.
|
|
1005
|
+
if (isRecord(schema.properties)) {
|
|
1006
|
+
for (const [name, property] of Object.entries(schema.properties))
|
|
1007
|
+
visit(property, [name], true);
|
|
1008
|
+
for (const [name, value] of Object.entries(schema)) {
|
|
1009
|
+
if (name !== "properties")
|
|
1010
|
+
visit(value, [], false);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
for (const value of Object.values(schema))
|
|
1015
|
+
visit(value, [], false);
|
|
1016
|
+
}
|
|
1017
|
+
return valid ? result : undefined;
|
|
1018
|
+
}
|
|
1019
|
+
function addMcpParameterHeaders(headers, annotations, args) {
|
|
1020
|
+
for (const annotation of annotations) {
|
|
1021
|
+
let value = args;
|
|
1022
|
+
for (const segment of annotation.path) {
|
|
1023
|
+
value = isRecord(value) && objectHasOwn(value, segment) ? value[segment] : undefined;
|
|
1024
|
+
}
|
|
1025
|
+
if (value === undefined || value === null)
|
|
1026
|
+
continue;
|
|
1027
|
+
if ((annotation.type === "integer" && !nativeIsSafeInteger(value)) ||
|
|
1028
|
+
(annotation.type !== "integer" && typeof value !== annotation.type)) {
|
|
1029
|
+
throw protocolError("Remote MCP tool header parameter has an invalid type");
|
|
1030
|
+
}
|
|
1031
|
+
reflectApply(HeadersPrototypeSet, headers, [
|
|
1032
|
+
`Mcp-Param-${annotation.name}`,
|
|
1033
|
+
// A template literal converts these primitives without calling project code.
|
|
1034
|
+
encodeMcpHeader(`${value}`),
|
|
1035
|
+
]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function remoteMcpSessionKey(endpoint, headers, runId) {
|
|
1039
|
+
// Native Headers iteration is sorted. Length prefixes prevent collisions;
|
|
1040
|
+
// raw credentials never enter mutable Array/JSON hooks or cache keys.
|
|
1041
|
+
let identity = `${endpoint.length}:${endpoint}${runId?.length ?? 0}:${runId ?? ""}`;
|
|
1042
|
+
reflectApply(HeadersPrototypeForEach, headers, [(value, name) => {
|
|
1043
|
+
identity += `${name.length}:${name}${value.length}:${value}`;
|
|
1044
|
+
}]);
|
|
1045
|
+
const hash = await digest("SHA-256", encodeUtf8(identity));
|
|
1046
|
+
const bytes = new NativeUint8Array(hash);
|
|
1047
|
+
const hex = "0123456789abcdef";
|
|
1048
|
+
let key = "";
|
|
1049
|
+
for (let index = 0; index < 32; index++) {
|
|
1050
|
+
const byte = bytes[index];
|
|
1051
|
+
key += hex[byte >>> 4] + hex[byte & 15];
|
|
1052
|
+
}
|
|
1053
|
+
return key;
|
|
1054
|
+
}
|
|
1055
|
+
/** Keep retained session credentials away from project-patched Map methods. */
|
|
1056
|
+
function createPrivateMcpMap() {
|
|
1057
|
+
const map = new NativeMap();
|
|
1058
|
+
defineOwnDataProperty(map, "get", (key) => reflectApply(MapPrototypeGet, map, [key]));
|
|
1059
|
+
defineOwnDataProperty(map, "set", (key, value) => reflectApply(MapPrototypeSet, map, [key, value]));
|
|
1060
|
+
defineOwnDataProperty(map, "has", (key) => reflectApply(MapPrototypeHas, map, [key]));
|
|
1061
|
+
defineOwnDataProperty(map, "delete", (key) => reflectApply(MapPrototypeDelete, map, [key]));
|
|
1062
|
+
defineOwnDataProperty(map, "clear", () => reflectApply(MapPrototypeClear, map, []));
|
|
1063
|
+
defineOwnDataProperty(map, "values", () => reflectApply(MapPrototypeValues, map, []));
|
|
1064
|
+
defineOwnDataProperty(map, Symbol.iterator, () => {
|
|
1065
|
+
const iterator = reflectApply(MapPrototypeIterator, map, []);
|
|
1066
|
+
return { next: () => reflectApply(MapIteratorNext, iterator, []) };
|
|
1067
|
+
});
|
|
1068
|
+
return map;
|
|
1069
|
+
}
|
|
1070
|
+
let mcpProtocolProofSequence = 0;
|
|
1071
|
+
function proveMcpProtocol(protocol) {
|
|
1072
|
+
return { protocol, sequence: ++mcpProtocolProofSequence };
|
|
1073
|
+
}
|
|
1074
|
+
const SHARED_MCP_PROTOCOL_STATE = Symbol("veryfront.remote-mcp.shared-protocol-state");
|
|
1075
|
+
const sharedMcpProtocolStates = createPrivateWeakStore();
|
|
1076
|
+
/**
|
|
1077
|
+
* Share endpoint protocol knowledge across sources derived from this config.
|
|
1078
|
+
*
|
|
1079
|
+
* A composite that routes some calls through a source with extra credentials
|
|
1080
|
+
* (terminal or mutation authority) discovers tools only through its ordinary
|
|
1081
|
+
* source. Sources created from configs spread from the returned value fall back
|
|
1082
|
+
* to the protocol and header annotations that discovery recorded for the same
|
|
1083
|
+
* endpoint, so their first call neither probes a legacy server with a call that
|
|
1084
|
+
* cannot be replayed nor omits schema-declared headers.
|
|
1085
|
+
*/
|
|
1086
|
+
export function shareRemoteMCPProtocolState(config) {
|
|
1087
|
+
const token = Object.freeze({});
|
|
1088
|
+
sharedMcpProtocolStates.set(token, {
|
|
1089
|
+
protocols: createPrivateMcpMap(),
|
|
1090
|
+
parameterHeaders: createPrivateMcpMap(),
|
|
1091
|
+
});
|
|
1092
|
+
return { ...config, [SHARED_MCP_PROTOCOL_STATE]: token };
|
|
1093
|
+
}
|
|
1094
|
+
function readSharedMcpProtocolState(config) {
|
|
1095
|
+
if (!objectHasOwn(config, SHARED_MCP_PROTOCOL_STATE))
|
|
1096
|
+
return undefined;
|
|
1097
|
+
const token = config[SHARED_MCP_PROTOCOL_STATE];
|
|
1098
|
+
return typeof token === "object" && token !== null
|
|
1099
|
+
? sharedMcpProtocolStates.get(token)
|
|
1100
|
+
: undefined;
|
|
1101
|
+
}
|
|
745
1102
|
function createRemoteMCPToolSourceWithFetch(config, getRequestFetch) {
|
|
746
1103
|
const id = config.id ?? "remote-mcp";
|
|
747
1104
|
const listMethod = config.listMethod ?? "tools/list";
|
|
748
1105
|
const callMethod = config.callMethod ?? "tools/call";
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1106
|
+
const sessions = createPrivateMcpMap();
|
|
1107
|
+
let nextCleanupId = 0;
|
|
1108
|
+
const cleanupRequests = createPrivateMcpMap();
|
|
1109
|
+
const initializing = createPrivateMcpMap();
|
|
1110
|
+
const parameterHeaders = createPrivateMcpMap();
|
|
1111
|
+
// The protocol each endpoint+credential identity last proved: "legacy" after
|
|
1112
|
+
// a negotiated session, "modern" after a sessionless request succeeded. A
|
|
1113
|
+
// tool call for a new run of a legacy identity initializes first instead of
|
|
1114
|
+
// probing with a modern request, because an ambiguous call failure is never
|
|
1115
|
+
// replayed.
|
|
1116
|
+
const protocols = createPrivateMcpMap();
|
|
1117
|
+
const shared = readSharedMcpProtocolState(config);
|
|
1118
|
+
const leases = createPrivateSet();
|
|
1119
|
+
function recordProtocol(identityKey, endpoint, protocol) {
|
|
1120
|
+
const proof = proveMcpProtocol(protocol);
|
|
1121
|
+
protocols.set(identityKey, proof);
|
|
1122
|
+
shared?.protocols.set(endpoint, proof);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* The most recent proof for this identity or, through a shared config, for
|
|
1126
|
+
* the endpoint. A legacy session negotiated before a later modern proof is
|
|
1127
|
+
* stale: the server accepts sessionless requests and may have dropped it.
|
|
1128
|
+
*/
|
|
1129
|
+
function currentProtocol(identityKey, endpoint) {
|
|
1130
|
+
const own = protocols.get(identityKey);
|
|
1131
|
+
const other = shared?.protocols.get(endpoint);
|
|
1132
|
+
if (!own)
|
|
1133
|
+
return other?.protocol;
|
|
1134
|
+
if (!other)
|
|
1135
|
+
return own.protocol;
|
|
1136
|
+
return other.sequence > own.sequence ? other.protocol : own.protocol;
|
|
1137
|
+
}
|
|
1138
|
+
async function withLease(runId, operation) {
|
|
1139
|
+
const lease = { runId, closed: false };
|
|
1140
|
+
leases.add(lease);
|
|
1141
|
+
try {
|
|
1142
|
+
return await operation(lease);
|
|
1143
|
+
}
|
|
1144
|
+
finally {
|
|
1145
|
+
leases.delete(lease);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async function initialize(key, endpoint, headers, requestFetch, signal, runId, identityKey) {
|
|
1149
|
+
const pending = initializing.get(key);
|
|
1150
|
+
if (pending)
|
|
1151
|
+
return await pending;
|
|
1152
|
+
const promise = (async () => {
|
|
1153
|
+
const initHeaders = copyNativeHeaders(headers);
|
|
1154
|
+
reflectApply(HeadersPrototypeDelete, initHeaders, ["Mcp-Session-Id"]);
|
|
1155
|
+
reflectApply(HeadersPrototypeDelete, initHeaders, ["MCP-Protocol-Version"]);
|
|
1156
|
+
reflectApply(HeadersPrototypeSet, initHeaders, ["Mcp-Method", "initialize"]);
|
|
1157
|
+
reflectApply(HeadersPrototypeDelete, initHeaders, ["Mcp-Name"]);
|
|
1158
|
+
let sessionId;
|
|
1159
|
+
let cleanupHeaders;
|
|
1160
|
+
const requestId = `${id}:initialize`;
|
|
1161
|
+
const payload = await postJsonRpc(endpoint, initHeaders, {
|
|
1162
|
+
jsonrpc: "2.0",
|
|
1163
|
+
id: requestId,
|
|
1164
|
+
method: "initialize",
|
|
1165
|
+
params: {
|
|
1166
|
+
protocolVersion: LEGACY_MCP_VERSIONS[0],
|
|
1167
|
+
capabilities: {},
|
|
1168
|
+
clientInfo: MCP_CLIENT_INFO,
|
|
1169
|
+
},
|
|
1170
|
+
}, requestFetch, signal, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES, (response) => {
|
|
1171
|
+
sessionId = reflectApply(HeadersPrototypeGet, response.headers, ["Mcp-Session-Id"]) ??
|
|
1172
|
+
undefined;
|
|
1173
|
+
if (isValidMcpSessionId(sessionId)) {
|
|
1174
|
+
// The server has minted a session even if parsing or validation fails.
|
|
1175
|
+
cleanupHeaders = copyNativeHeaders(initHeaders);
|
|
1176
|
+
reflectApply(HeadersPrototypeSet, cleanupHeaders, ["Mcp-Session-Id", sessionId]);
|
|
1177
|
+
reflectApply(HeadersPrototypeSet, cleanupHeaders, [
|
|
1178
|
+
"MCP-Protocol-Version",
|
|
1179
|
+
LEGACY_MCP_VERSIONS[0],
|
|
1180
|
+
]);
|
|
1181
|
+
cleanupRequests.set(++nextCleanupId, {
|
|
1182
|
+
sourceKey: key,
|
|
1183
|
+
endpoint,
|
|
1184
|
+
headers: cleanupHeaders,
|
|
1185
|
+
sessionId,
|
|
1186
|
+
runId,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
const result = getJsonRpcResult(payload, requestId);
|
|
1191
|
+
if (!isRecord(result) || typeof result.protocolVersion !== "string" ||
|
|
1192
|
+
!LEGACY_MCP_VERSIONS.includes(result.protocolVersion)) {
|
|
1193
|
+
throw protocolError("Remote MCP initialize returned an unsupported protocol version");
|
|
1194
|
+
}
|
|
1195
|
+
if (sessionId !== undefined && !isValidMcpSessionId(sessionId)) {
|
|
1196
|
+
throw protocolError("Remote MCP initialize returned an invalid session identifier");
|
|
1197
|
+
}
|
|
1198
|
+
const session = { version: result.protocolVersion, sessionId };
|
|
1199
|
+
reflectApply(HeadersPrototypeSet, initHeaders, ["MCP-Protocol-Version", session.version]);
|
|
1200
|
+
if (cleanupHeaders) {
|
|
1201
|
+
reflectApply(HeadersPrototypeSet, cleanupHeaders, [
|
|
1202
|
+
"MCP-Protocol-Version",
|
|
1203
|
+
session.version,
|
|
1204
|
+
]);
|
|
1205
|
+
}
|
|
1206
|
+
if (sessionId)
|
|
1207
|
+
reflectApply(HeadersPrototypeSet, initHeaders, ["Mcp-Session-Id", sessionId]);
|
|
1208
|
+
reflectApply(HeadersPrototypeSet, initHeaders, ["Mcp-Method", "notifications/initialized"]);
|
|
1209
|
+
await postJsonRpc(endpoint, initHeaders, {
|
|
1210
|
+
jsonrpc: "2.0",
|
|
1211
|
+
method: "notifications/initialized",
|
|
1212
|
+
}, requestFetch, signal, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES);
|
|
1213
|
+
sessions.set(key, session);
|
|
1214
|
+
recordProtocol(identityKey, endpoint, "legacy");
|
|
1215
|
+
return session;
|
|
1216
|
+
})();
|
|
1217
|
+
initializing.set(key, promise);
|
|
1218
|
+
try {
|
|
1219
|
+
return await promise;
|
|
1220
|
+
}
|
|
1221
|
+
finally {
|
|
1222
|
+
initializing.delete(key);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async function request(options, lease) {
|
|
1226
|
+
const { endpoint, headers, requestFetch, signal, maxBytes, operation, runId } = options;
|
|
1227
|
+
const snapshot = snapshotBoundedJsonValue(options.body, MAX_REMOTE_MCP_REQUEST_BYTES);
|
|
1228
|
+
if (!snapshot.success || !isRecord(snapshot.value)) {
|
|
1229
|
+
throw new TypeError("Remote MCP JSON-RPC request must be a bounded JSON object");
|
|
1230
|
+
}
|
|
1231
|
+
const body = snapshot.value;
|
|
1232
|
+
const key = await remoteMcpSessionKey(endpoint, headers, runId);
|
|
1233
|
+
// Schema annotations and protocol support describe the endpoint and
|
|
1234
|
+
// credentials, not a run, so they hold for calls under any run.
|
|
1235
|
+
const identityKey = await remoteMcpSessionKey(endpoint, headers);
|
|
1236
|
+
// Session setup never uses a call-only transport such as hosted admission.
|
|
1237
|
+
const sessionFetch = getRequestFetch(endpoint);
|
|
1238
|
+
const establish = () => {
|
|
1239
|
+
// Disposal may have run while this request awaited the server. A session
|
|
1240
|
+
// minted now would outlive the cleanup that already finished.
|
|
1241
|
+
if (lease.closed)
|
|
1242
|
+
throw new Error("Remote MCP source was disposed during the request");
|
|
1243
|
+
return initialize(key, endpoint, headers, sessionFetch, signal, runId, identityKey);
|
|
1244
|
+
};
|
|
1245
|
+
const send = (session) => {
|
|
1246
|
+
const annotations = parameterHeaders.get(identityKey) ??
|
|
1247
|
+
shared?.parameterHeaders.get(endpoint);
|
|
1248
|
+
const built = buildMcpRequest(headers, body, session, annotations);
|
|
1249
|
+
return postJsonRpc(endpoint, built.headers, built.body, requestFetch, signal, maxBytes);
|
|
1250
|
+
};
|
|
1251
|
+
if (initializing.has(key))
|
|
1252
|
+
await initializing.get(key);
|
|
1253
|
+
let session = sessions.get(key);
|
|
1254
|
+
const protocol = currentProtocol(identityKey, endpoint);
|
|
1255
|
+
if (session && protocol === "modern") {
|
|
1256
|
+
// Negotiated before a later sessionless success; the server may have dropped it.
|
|
1257
|
+
sessions.delete(key);
|
|
1258
|
+
session = undefined;
|
|
1259
|
+
}
|
|
1260
|
+
if (!session && operation === "execution" && protocol === "legacy") {
|
|
1261
|
+
// Initialization happens before the call is sent, so a failure here never
|
|
1262
|
+
// leaves a side-effecting request to replay.
|
|
1263
|
+
session = await establish();
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
const result = await send(session);
|
|
1267
|
+
if (!session) {
|
|
1268
|
+
// A sessionless success proves the modern protocol, so a later call
|
|
1269
|
+
// must not initialize a session the server may no longer accept.
|
|
1270
|
+
recordProtocol(identityKey, endpoint, "modern");
|
|
1271
|
+
}
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
catch (error) {
|
|
1275
|
+
if (!shouldNegotiateSession(error, session, operation))
|
|
1276
|
+
throw error;
|
|
1277
|
+
if (session && sessions.get(key) === session)
|
|
1278
|
+
sessions.delete(key);
|
|
1279
|
+
if (session && currentProtocol(identityKey, endpoint) === "modern") {
|
|
1280
|
+
// A concurrent request proved sessionless support while this one used
|
|
1281
|
+
// its rejected session; a new legacy handshake would fail there.
|
|
1282
|
+
if (operation === "discovery-page")
|
|
1283
|
+
throw new RemoteMCPDiscoveryRestart();
|
|
1284
|
+
return await send();
|
|
1285
|
+
}
|
|
1286
|
+
const renewed = sessions.get(key) ?? await establish();
|
|
1287
|
+
// A pagination cursor belongs to the session that issued it.
|
|
1288
|
+
if (operation === "discovery-page")
|
|
1289
|
+
throw new RemoteMCPDiscoveryRestart();
|
|
1290
|
+
return await send(renewed);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function listPages(endpoint, headers, listMeta, context, lease) {
|
|
1294
|
+
const catalog = createToolListAccumulator();
|
|
1295
|
+
let cursor;
|
|
1296
|
+
for (let page = 0; page < MAX_REMOTE_MCP_TOOL_LIST_PAGES; page += 1) {
|
|
1297
|
+
const requestId = `${id}:tools:list`;
|
|
1298
|
+
const params = {
|
|
1299
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
1300
|
+
...(listMeta !== undefined ? { _meta: listMeta } : {}),
|
|
1301
|
+
};
|
|
1302
|
+
const payload = await request({
|
|
1303
|
+
endpoint,
|
|
1304
|
+
headers,
|
|
1305
|
+
body: {
|
|
762
1306
|
jsonrpc: "2.0",
|
|
763
1307
|
id: requestId,
|
|
764
1308
|
method: listMethod,
|
|
765
|
-
...(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
for (const definition of pageDefinitions) {
|
|
781
|
-
if (definitionNames.has(definition.name)) {
|
|
782
|
-
throw protocolError(`Remote MCP tools/list returned duplicate tool name "${definition.name}"`);
|
|
783
|
-
}
|
|
784
|
-
definitionNames.add(definition.name);
|
|
785
|
-
definitions.push(definition);
|
|
786
|
-
}
|
|
787
|
-
if (nextCursor === undefined) {
|
|
788
|
-
return definitions;
|
|
789
|
-
}
|
|
790
|
-
if (seenCursors.has(nextCursor)) {
|
|
791
|
-
throw protocolError(`Remote MCP tools/list returned repeated pagination cursor "${nextCursor}"`);
|
|
792
|
-
}
|
|
793
|
-
seenCursors.add(nextCursor);
|
|
794
|
-
cursor = nextCursor;
|
|
795
|
-
if (page === MAX_REMOTE_MCP_TOOL_LIST_PAGES - 1) {
|
|
796
|
-
throw protocolError(`Remote MCP tools/list exceeded ${MAX_REMOTE_MCP_TOOL_LIST_PAGES} pages`);
|
|
797
|
-
}
|
|
1309
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
1310
|
+
},
|
|
1311
|
+
requestFetch: getRequestFetch(endpoint),
|
|
1312
|
+
signal: context?.abortSignal,
|
|
1313
|
+
maxBytes: MAX_REMOTE_MCP_TOOL_LIST_RESPONSE_BYTES,
|
|
1314
|
+
operation: cursor === undefined ? "discovery" : "discovery-page",
|
|
1315
|
+
runId: context?.runId,
|
|
1316
|
+
}, lease);
|
|
1317
|
+
const { definitions, nextCursor } = normalizeToolListPage(getJsonRpcResult(payload, requestId));
|
|
1318
|
+
catalog.accept(definitions);
|
|
1319
|
+
if (nextCursor === undefined) {
|
|
1320
|
+
parameterHeaders.set(await remoteMcpSessionKey(endpoint, headers), catalog.annotations);
|
|
1321
|
+
shared?.parameterHeaders.set(endpoint, catalog.annotations);
|
|
1322
|
+
return catalog.definitions;
|
|
798
1323
|
}
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1324
|
+
cursor = catalog.advance(nextCursor);
|
|
1325
|
+
}
|
|
1326
|
+
throw protocolError(`Remote MCP tools/list exceeded ${MAX_REMOTE_MCP_TOOL_LIST_PAGES} pages`);
|
|
1327
|
+
}
|
|
1328
|
+
async function executeWithLease(toolName, args, context, lease) {
|
|
1329
|
+
const endpoint = validateEndpoint(await resolveValue(config.endpoint, context));
|
|
1330
|
+
const headers = await resolveHeaders(config.headers, context);
|
|
1331
|
+
const meta = buildRunContextMeta(context, endpoint);
|
|
1332
|
+
const requestId = `${id}:tools:call:${toolName}`;
|
|
1333
|
+
try {
|
|
1334
|
+
const payload = await request({
|
|
1335
|
+
endpoint,
|
|
1336
|
+
headers,
|
|
1337
|
+
body: {
|
|
808
1338
|
jsonrpc: "2.0",
|
|
809
1339
|
id: requestId,
|
|
810
1340
|
method: callMethod,
|
|
@@ -813,30 +1343,100 @@ function createRemoteMCPToolSourceWithFetch(config, getRequestFetch) {
|
|
|
813
1343
|
arguments: args,
|
|
814
1344
|
...(meta ? { _meta: meta } : {}),
|
|
815
1345
|
},
|
|
816
|
-
},
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1346
|
+
},
|
|
1347
|
+
requestFetch: getHostedToolCallAdmissionRequestFetch(endpoint) ??
|
|
1348
|
+
getRequestFetch(endpoint),
|
|
1349
|
+
signal: context?.abortSignal,
|
|
1350
|
+
maxBytes: MAX_REMOTE_MCP_CALL_RESPONSE_BYTES,
|
|
1351
|
+
operation: "execution",
|
|
1352
|
+
runId: context?.runId,
|
|
1353
|
+
}, lease);
|
|
1354
|
+
const result = getJsonRpcResult(payload, requestId, true);
|
|
1355
|
+
if (isJsonRpcToolErrorResult(result)) {
|
|
1356
|
+
return preserveToolExecutionErrorMarker(normalizeKnownToolError(result.result, toolName, endpoint, context));
|
|
1357
|
+
}
|
|
1358
|
+
// The response text is already bounded, so one string may use the whole response.
|
|
1359
|
+
const resultSnapshot = snapshotBoundedParsedJsonValue(result, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES);
|
|
1360
|
+
if (!resultSnapshot.success) {
|
|
1361
|
+
throw protocolError("Remote MCP tools/call returned an unbounded JSON result");
|
|
1362
|
+
}
|
|
1363
|
+
return normalizeCallToolResult({
|
|
1364
|
+
result: resultSnapshot.value,
|
|
1365
|
+
toolName,
|
|
1366
|
+
endpoint,
|
|
1367
|
+
context,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
const normalizedError = normalizeKnownToolException(error, toolName, endpoint, context);
|
|
1372
|
+
if (normalizedError) {
|
|
1373
|
+
return normalizedError;
|
|
1374
|
+
}
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
id,
|
|
1380
|
+
async dispose(context) {
|
|
1381
|
+
// Requests still waiting on the server may not negotiate a session after
|
|
1382
|
+
// this point; handshakes already running are awaited and terminated.
|
|
1383
|
+
for (const lease of leases) {
|
|
1384
|
+
if (!context || lease.runId === context.runId)
|
|
1385
|
+
lease.closed = true;
|
|
1386
|
+
}
|
|
1387
|
+
const handshakes = [];
|
|
1388
|
+
for (const entry of initializing) {
|
|
1389
|
+
defineOwnDataProperty(handshakes, handshakes.length, { settled: entry[1] });
|
|
1390
|
+
}
|
|
1391
|
+
await allPrivatePromises(mapPrivateArray(handshakes, (handshake) => chainPrivatePromise(handshake.settled, () => undefined, () => undefined)));
|
|
1392
|
+
const selected = [];
|
|
1393
|
+
// Native iterator entries are indexed directly. Destructuring would call
|
|
1394
|
+
// a project-patched Array iterator with the retained session credentials.
|
|
1395
|
+
for (const entry of cleanupRequests) {
|
|
1396
|
+
const cleanup = entry[1];
|
|
1397
|
+
if (context && cleanup.runId !== context.runId)
|
|
1398
|
+
continue;
|
|
1399
|
+
sessions.delete(cleanup.sourceKey);
|
|
1400
|
+
cleanupRequests.delete(entry[0]);
|
|
1401
|
+
defineOwnDataProperty(selected, selected.length, cleanup);
|
|
1402
|
+
}
|
|
1403
|
+
const firstError = await terminateMcpSessions(selected, getRequestFetch);
|
|
1404
|
+
if (!context) {
|
|
1405
|
+
sessions.clear();
|
|
1406
|
+
parameterHeaders.clear();
|
|
1407
|
+
protocols.clear();
|
|
1408
|
+
}
|
|
1409
|
+
if (firstError !== undefined)
|
|
1410
|
+
throw firstError;
|
|
1411
|
+
},
|
|
1412
|
+
listTools(context) {
|
|
1413
|
+
// The lease spans configuration resolution and every page, so disposal
|
|
1414
|
+
// during any of those awaits closes it.
|
|
1415
|
+
return withLease(context?.runId, async (lease) => {
|
|
1416
|
+
const endpoint = validateEndpoint(await resolveValue(config.endpoint, context));
|
|
1417
|
+
const headers = await resolveHeaders(config.headers, context);
|
|
1418
|
+
const listMeta = config.listMeta === undefined ? undefined : Object.fromEntries(Object.entries(config.listMeta).filter(([key]) => key !== "tool_call_admission"));
|
|
1419
|
+
// A session renewed mid-pagination invalidates earlier cursors, so the
|
|
1420
|
+
// catalog is rebuilt from the first page, at most once.
|
|
1421
|
+
try {
|
|
1422
|
+
return await listPages(endpoint, headers, listMeta, context, lease);
|
|
820
1423
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
throw protocolError("Remote MCP tools/call returned an unbounded JSON result");
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
if (!(error instanceof RemoteMCPDiscoveryRestart))
|
|
1426
|
+
throw error;
|
|
825
1427
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
toolName,
|
|
829
|
-
endpoint,
|
|
830
|
-
context,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
catch (error) {
|
|
834
|
-
const normalizedError = normalizeKnownToolException(error, toolName, endpoint, context);
|
|
835
|
-
if (normalizedError) {
|
|
836
|
-
return normalizedError;
|
|
1428
|
+
try {
|
|
1429
|
+
return await listPages(endpoint, headers, listMeta, context, lease);
|
|
837
1430
|
}
|
|
838
|
-
|
|
839
|
-
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
if (!(error instanceof RemoteMCPDiscoveryRestart))
|
|
1433
|
+
throw error;
|
|
1434
|
+
throw protocolError("Remote MCP session was renewed again during tools/list pagination");
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
},
|
|
1438
|
+
executeTool(toolName, args, context) {
|
|
1439
|
+
return withLease(context?.runId, (lease) => executeWithLease(toolName, args, context, lease));
|
|
840
1440
|
},
|
|
841
1441
|
};
|
|
842
1442
|
}
|