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.
Files changed (75) hide show
  1. package/docs/README.md +1 -1
  2. package/docs/config.json +1 -1
  3. package/docs/guides/tools.md +27 -2
  4. package/docs/manifest.json +6 -6
  5. package/esm/deno.js +1 -1
  6. package/esm/src/agent/ag-ui/connected-local-platform-tools.d.ts.map +1 -1
  7. package/esm/src/agent/ag-ui/connected-local-platform-tools.js +9 -2
  8. package/esm/src/agent/ag-ui/tool-shared.d.ts +2 -0
  9. package/esm/src/agent/ag-ui/tool-shared.d.ts.map +1 -1
  10. package/esm/src/agent/ag-ui/tool-shared.js +7 -0
  11. package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts +2 -0
  12. package/esm/src/agent/hosted/chat-runtime-tool-assembly.d.ts.map +1 -1
  13. package/esm/src/agent/hosted/chat-runtime-tool-assembly.js +18 -2
  14. package/esm/src/agent/hosted/child-fork-tool-sources.d.ts +1 -0
  15. package/esm/src/agent/hosted/child-fork-tool-sources.d.ts.map +1 -1
  16. package/esm/src/agent/hosted/child-fork-tool-sources.js +49 -12
  17. package/esm/src/agent/hosted/default-chat-runtime.d.ts.map +1 -1
  18. package/esm/src/agent/hosted/default-chat-runtime.js +26 -1
  19. package/esm/src/agent/hosted/executor-runtime-facades.d.ts.map +1 -1
  20. package/esm/src/agent/hosted/executor-runtime-facades.js +3 -0
  21. package/esm/src/agent/hosted/executor-tool-schema.d.ts.map +1 -1
  22. package/esm/src/agent/hosted/executor-tool-schema.js +7 -1
  23. package/esm/src/agent/hosted/platform-dependency-credential.d.ts.map +1 -1
  24. package/esm/src/agent/hosted/platform-dependency-credential.js +26 -1
  25. package/esm/src/agent/hosted/project-remote-tool-source.d.ts.map +1 -1
  26. package/esm/src/agent/hosted/project-remote-tool-source.js +18 -0
  27. package/esm/src/agent/hosted/terminal-credential.d.ts.map +1 -1
  28. package/esm/src/agent/hosted/terminal-credential.js +9 -3
  29. package/esm/src/agent/platform-mcp-tool-source.d.ts.map +1 -1
  30. package/esm/src/agent/platform-mcp-tool-source.js +4 -0
  31. package/esm/src/agent/project/live-studio-mcp-tools.d.ts.map +1 -1
  32. package/esm/src/agent/project/live-studio-mcp-tools.js +14 -2
  33. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  34. package/esm/src/agent/runtime/index.js +2269 -2191
  35. package/esm/src/agent/runtime/mcp-server-tool-sources.d.ts +2 -0
  36. package/esm/src/agent/runtime/mcp-server-tool-sources.d.ts.map +1 -1
  37. package/esm/src/agent/runtime/mcp-server-tool-sources.js +27 -0
  38. package/esm/src/agent/runtime/runtime-tool-config.d.ts +1 -0
  39. package/esm/src/agent/runtime/runtime-tool-config.d.ts.map +1 -1
  40. package/esm/src/agent/runtime/runtime-tool-config.js +4 -0
  41. package/esm/src/agent/runtime/tool-exposure.d.ts +4 -2
  42. package/esm/src/agent/runtime/tool-exposure.d.ts.map +1 -1
  43. package/esm/src/agent/runtime/tool-exposure.js +17 -4
  44. package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
  45. package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
  46. package/esm/src/internal-agents/run-stream.js +95 -23
  47. package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
  48. package/esm/src/server/handlers/request/agent-stream.handler.d.ts +2 -0
  49. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -1
  50. package/esm/src/server/handlers/request/agent-stream.handler.js +24 -3
  51. package/esm/src/server/handlers/request/run-terminal-tool-source.d.ts.map +1 -1
  52. package/esm/src/server/handlers/request/run-terminal-tool-source.js +12 -3
  53. package/esm/src/tool/factory.d.ts +2 -0
  54. package/esm/src/tool/factory.d.ts.map +1 -1
  55. package/esm/src/tool/factory.js +7 -0
  56. package/esm/src/tool/host-tools.d.ts +2 -0
  57. package/esm/src/tool/host-tools.d.ts.map +1 -1
  58. package/esm/src/tool/host-tools.js +14 -0
  59. package/esm/src/tool/project-scoped-remote-tools.d.ts.map +1 -1
  60. package/esm/src/tool/project-scoped-remote-tools.js +12 -0
  61. package/esm/src/tool/registry.d.ts.map +1 -1
  62. package/esm/src/tool/registry.js +3 -0
  63. package/esm/src/tool/remote-mcp.d.ts +11 -0
  64. package/esm/src/tool/remote-mcp.d.ts.map +1 -1
  65. package/esm/src/tool/remote-mcp.js +685 -85
  66. package/esm/src/tool/remote-source-tools.d.ts.map +1 -1
  67. package/esm/src/tool/remote-source-tools.js +1 -0
  68. package/esm/src/tool/remote-tool-source-owner.d.ts +14 -0
  69. package/esm/src/tool/remote-tool-source-owner.d.ts.map +1 -0
  70. package/esm/src/tool/remote-tool-source-owner.js +65 -0
  71. package/esm/src/tool/types.d.ts +4 -0
  72. package/esm/src/tool/types.d.ts.map +1 -1
  73. package/esm/src/utils/version-constant.d.ts +1 -1
  74. package/esm/src/utils/version-constant.js +1 -1
  75. 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
- constructor(status) {
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 && !Array.isArray(value);
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(inputSchema, toolName) {
134
- const snapshot = snapshotBoundedJsonValue(inputSchema);
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 input schema for tool "${toolName}"`);
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
- throw new RemoteMCPHttpError(response.status);
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
- return {
750
- id,
751
- async listTools(context) {
752
- const endpoint = validateEndpoint(await resolveValue(config.endpoint, context));
753
- const headers = await resolveHeaders(config.headers, context);
754
- const listMeta = config.listMeta === undefined ? undefined : Object.fromEntries(Object.entries(config.listMeta).filter(([key]) => key !== "tool_call_admission"));
755
- const definitions = [];
756
- const definitionNames = new Set();
757
- const seenCursors = new Set();
758
- let cursor;
759
- for (let page = 0; page < MAX_REMOTE_MCP_TOOL_LIST_PAGES; page += 1) {
760
- const requestId = `${id}:tools:list`;
761
- const payload = await postJsonRpc(endpoint, headers, {
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
- ...(cursor !== undefined || listMeta !== undefined
766
- ? {
767
- params: {
768
- ...(cursor !== undefined ? { cursor } : {}),
769
- ...(listMeta !== undefined ? { _meta: listMeta } : {}),
770
- },
771
- }
772
- : {}),
773
- }, getRequestFetch(endpoint), context?.abortSignal, MAX_REMOTE_MCP_TOOL_LIST_RESPONSE_BYTES);
774
- const result = getJsonRpcResult(payload, requestId);
775
- const { definitions: pageDefinitions, nextCursor } = normalizeToolListPage(result);
776
- if (definitions.length + pageDefinitions.length >
777
- MAX_REMOTE_MCP_TOOL_DEFINITIONS) {
778
- throw protocolError(`Remote MCP tools/list cannot contain more than ${MAX_REMOTE_MCP_TOOL_DEFINITIONS} tools`);
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
- throw protocolError(`Remote MCP tools/list exceeded ${MAX_REMOTE_MCP_TOOL_LIST_PAGES} pages`);
800
- },
801
- async executeTool(toolName, args, context) {
802
- const endpoint = validateEndpoint(await resolveValue(config.endpoint, context));
803
- const headers = await resolveHeaders(config.headers, context);
804
- const meta = buildRunContextMeta(context, endpoint);
805
- const requestId = `${id}:tools:call:${toolName}`;
806
- try {
807
- const payload = await postJsonRpc(endpoint, headers, {
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
- }, getHostedToolCallAdmissionRequestFetch(endpoint) ?? getRequestFetch(endpoint), context?.abortSignal, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES);
817
- const result = getJsonRpcResult(payload, requestId, true);
818
- if (isJsonRpcToolErrorResult(result)) {
819
- return preserveToolExecutionErrorMarker(normalizeKnownToolError(result.result, toolName, endpoint, context));
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
- // The response text is already bounded, so one string may use the whole response.
822
- const resultSnapshot = snapshotBoundedParsedJsonValue(result, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES);
823
- if (!resultSnapshot.success) {
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
- return normalizeCallToolResult({
827
- result: resultSnapshot.value,
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
- throw error;
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
  }