agents 0.14.4 → 0.15.0
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/{agent-tool-types-V25Z_HcX.d.ts → agent-tool-types-VPsjVYL0.d.ts} +126 -109
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-C-9s151X.d.ts → agent-tools-BGpgfpJT.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat-sdk/index.d.ts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +261 -486
- package/dist/mcp/index.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/skills/compile.d.ts +40 -0
- package/dist/skills/compile.js +65 -0
- package/dist/skills/compile.js.map +1 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.js +20 -23
- package/dist/skills/index.js.map +1 -1
- package/dist/sub-routing.d.ts +1 -1
- package/dist/utils-CGtGDSgA.d.ts +34 -0
- package/dist/utils.d.ts +5 -29
- package/dist/vite.js +16 -3
- package/dist/vite.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +7 -6
package/dist/mcp/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import { Agent, getAgentByName, getCurrentAgent } from "../index.js";
|
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
6
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
-
import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema,
|
|
7
|
+
import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
9
10
|
//#region src/mcp/sse-keepalive.ts
|
|
10
11
|
/**
|
|
11
12
|
* Shared SSE keepalive utility for MCP transports.
|
|
@@ -1005,512 +1006,291 @@ var StreamableHTTPEdgeClientTransport = class extends StreamableHTTPClientTransp
|
|
|
1005
1006
|
};
|
|
1006
1007
|
//#endregion
|
|
1007
1008
|
//#region src/mcp/worker-transport.ts
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* WorkerTransport
|
|
1011
|
+
*
|
|
1012
|
+
* Thin Cloudflare-Workers wrapper around the official MCP SDK
|
|
1013
|
+
* `WebStandardStreamableHTTPServerTransport`. The wrapper layers a couple of
|
|
1014
|
+
* Workers-specific concerns on top of the SDK transport without forking it:
|
|
1015
|
+
*
|
|
1016
|
+
* 1. **CORS** — preflight handling and response-header injection,
|
|
1017
|
+
* configurable via `corsOptions`.
|
|
1018
|
+
* 2. **Persistent transport state** — when a `storage` adapter
|
|
1019
|
+
* (`MCPStorageApi`) is supplied, the wrapper persists
|
|
1020
|
+
* `{sessionId, initialized, initializeParams}` so that an MCP session can
|
|
1021
|
+
* survive DO hibernation / eviction. On the first request after a cold
|
|
1022
|
+
* start, the saved initialize params are replayed through the `Server`
|
|
1023
|
+
* so client capabilities are re-established.
|
|
1024
|
+
* 3. **SSE keepalive** — SSE responses are wrapped in a TransformStream that
|
|
1025
|
+
* injects a `: keepalive\n\n` comment frame every 25s so the Cloudflare
|
|
1026
|
+
* edge ~5min idle-stream watchdog doesn't kill long-running tool calls.
|
|
1027
|
+
* Disabled on the standalone GET stream when an `eventStore` is
|
|
1028
|
+
* configured — clients recover idle drops via `Last-Event-ID` instead.
|
|
1029
|
+
* POST response streams always keepalive (no resumption path during a
|
|
1030
|
+
* mid-flight tool call). See cloudflare/agents#1583.
|
|
1031
|
+
*
|
|
1032
|
+
* Everything else (session validation, SSE streaming, protocol-version
|
|
1033
|
+
* negotiation, event-store resumability, etc.) is delegated to the SDK
|
|
1034
|
+
* transport.
|
|
1035
|
+
*/
|
|
1036
|
+
/** Sentinel id used when replaying the persisted initialize request. */
|
|
1037
|
+
const RESTORE_REQUEST_ID = "__worker_transport_restore__";
|
|
1038
|
+
const DEFAULT_CORS_OPTIONS = {
|
|
1039
|
+
origin: "*",
|
|
1040
|
+
headers: "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version",
|
|
1041
|
+
methods: "GET, POST, DELETE, OPTIONS",
|
|
1042
|
+
exposeHeaders: "mcp-session-id",
|
|
1043
|
+
maxAge: 86400
|
|
1044
|
+
};
|
|
1045
|
+
var WorkerTransport = class extends WebStandardStreamableHTTPServerTransport {
|
|
1046
|
+
constructor(options = {}) {
|
|
1047
|
+
const { corsOptions, storage, onsessioninitialized, ...sdkOptions } = options;
|
|
1048
|
+
super({
|
|
1049
|
+
...sdkOptions,
|
|
1050
|
+
onsessioninitialized: void 0
|
|
1051
|
+
});
|
|
1052
|
+
this._stateRestored = false;
|
|
1053
|
+
this._bridgeInstalled = false;
|
|
1054
|
+
this._keepaliveCleanups = /* @__PURE__ */ new Map();
|
|
1055
|
+
this._closedRequestIds = /* @__PURE__ */ new Set();
|
|
1056
|
+
this._corsOptions = corsOptions;
|
|
1057
|
+
this._storage = storage;
|
|
1058
|
+
this._userOnSessionInitialized = onsessioninitialized;
|
|
1028
1059
|
}
|
|
1029
1060
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1061
|
+
* Backwards-compatible alias for the SDK's internal `_started` flag.
|
|
1062
|
+
* Several callers and tests check `transport.started` directly.
|
|
1032
1063
|
*/
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
const state = await Promise.resolve(this.storage.get());
|
|
1036
|
-
if (state) {
|
|
1037
|
-
this.sessionId = state.sessionId;
|
|
1038
|
-
this.initialized = state.initialized;
|
|
1039
|
-
if (state.initializeParams && this.onmessage) this.onmessage({
|
|
1040
|
-
jsonrpc: "2.0",
|
|
1041
|
-
id: RESTORE_REQUEST_ID,
|
|
1042
|
-
method: "initialize",
|
|
1043
|
-
params: state.initializeParams
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
this.stateRestored = true;
|
|
1064
|
+
get started() {
|
|
1065
|
+
return this._started;
|
|
1047
1066
|
}
|
|
1048
1067
|
/**
|
|
1049
|
-
*
|
|
1068
|
+
* Top-level request entry point. Handles CORS preflight, restores any
|
|
1069
|
+
* persisted state on first invocation, then delegates to the SDK transport
|
|
1070
|
+
* and finally appends CORS headers + keepalive to whatever response comes
|
|
1071
|
+
* back.
|
|
1050
1072
|
*/
|
|
1051
|
-
async
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1073
|
+
async handleRequest(request, options) {
|
|
1074
|
+
if (request.method === "OPTIONS") return new Response(null, { headers: this.getCorsHeaders({ forPreflight: true }) });
|
|
1075
|
+
await this.restoreState();
|
|
1076
|
+
this.installOnSessionInitializedBridge();
|
|
1077
|
+
await this.captureInitializeParams(request, options);
|
|
1078
|
+
const requestIdForKeepalive = request.method === "GET" ? "_standalone" : this._pendingRequestId;
|
|
1079
|
+
const response = await super.handleRequest(request, options);
|
|
1080
|
+
return this.withCorsHeaders(this.withKeepalive(this.normalizeAllowHeader(response), requestIdForKeepalive));
|
|
1059
1081
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* The SDK's 405 responses advertise `Allow: GET, POST, DELETE` because
|
|
1084
|
+
* OPTIONS is handled outside the SDK. Since our wrapper *does* handle
|
|
1085
|
+
* OPTIONS, advertise it in `Allow` so clients can probe accurately.
|
|
1086
|
+
*/
|
|
1087
|
+
normalizeAllowHeader(response) {
|
|
1088
|
+
if (response.status !== 405) return response;
|
|
1089
|
+
const allow = response.headers.get("Allow");
|
|
1090
|
+
if (!allow || allow.includes("OPTIONS")) return response;
|
|
1091
|
+
const headers = new Headers(response.headers);
|
|
1092
|
+
headers.set("Allow", `${allow}, OPTIONS`);
|
|
1093
|
+
return new Response(response.body, {
|
|
1094
|
+
status: response.status,
|
|
1095
|
+
statusText: response.statusText,
|
|
1096
|
+
headers
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
closeSSEStream(requestId) {
|
|
1100
|
+
this._keepaliveCleanups.get(requestId)?.();
|
|
1101
|
+
this._keepaliveCleanups.delete(requestId);
|
|
1102
|
+
this._closedRequestIds.add(requestId);
|
|
1103
|
+
super.closeSSEStream(requestId);
|
|
1104
|
+
}
|
|
1105
|
+
closeStandaloneSSEStream() {
|
|
1106
|
+
this._keepaliveCleanups.get("_standalone")?.();
|
|
1107
|
+
this._keepaliveCleanups.delete("_standalone");
|
|
1108
|
+
super.closeStandaloneSSEStream();
|
|
1109
|
+
}
|
|
1110
|
+
async close() {
|
|
1111
|
+
for (const cleanup of Array.from(this._keepaliveCleanups.values())) cleanup();
|
|
1112
|
+
this._keepaliveCleanups.clear();
|
|
1113
|
+
this._closedRequestIds.clear();
|
|
1114
|
+
await super.close();
|
|
1063
1115
|
}
|
|
1064
1116
|
/**
|
|
1065
|
-
*
|
|
1117
|
+
* Swallow two classes of message that would otherwise surface as
|
|
1118
|
+
* unhandled rejections from the SDK transport's `send()`:
|
|
1066
1119
|
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1120
|
+
* 1. Replayed initialize responses (the `RESTORE_REQUEST_ID` sentinel)
|
|
1121
|
+
* — we synthesise these in `restoreState()` to rebuild server
|
|
1122
|
+
* capabilities; there's no real client waiting for the response.
|
|
1123
|
+
* 2. Sends for a request id whose SSE stream has been deliberately
|
|
1124
|
+
* closed via `closeSSEStream`. The protocol layer's tool-handler
|
|
1125
|
+
* promise may settle after the close, and the SDK's `send()` throws
|
|
1126
|
+
* "No connection established" — a race the pre-refactor transport
|
|
1127
|
+
* silently swallowed.
|
|
1072
1128
|
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1129
|
+
* Everything else is delegated. We use `await super.send(...)` rather
|
|
1130
|
+
* than `return super.send(...)` so any rejection is observed inside this
|
|
1131
|
+
* async frame; without the await, the test runner's
|
|
1132
|
+
* unhandled-rejection tracker can fire before the caller's own `await`
|
|
1133
|
+
* observes it.
|
|
1076
1134
|
*/
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1135
|
+
async send(message, options) {
|
|
1136
|
+
let requestId = options?.relatedRequestId;
|
|
1137
|
+
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
|
|
1138
|
+
if (requestId === RESTORE_REQUEST_ID) return;
|
|
1139
|
+
if (requestId !== void 0 && this._closedRequestIds.has(requestId)) return;
|
|
1140
|
+
await super.send(message, options);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* If the response is an SSE stream, tee the body through a TransformStream
|
|
1144
|
+
* that injects a `: keepalive\n\n` comment frame every 25s. The interval
|
|
1145
|
+
* is cleared when the wrapped stream closes — which happens both when the
|
|
1146
|
+
* SDK ends the underlying stream naturally and when `closeSSEStream` is
|
|
1147
|
+
* called.
|
|
1148
|
+
*
|
|
1149
|
+
* Keepalive policy:
|
|
1150
|
+
* - POST response streams (`key` is a request id): always keepalive.
|
|
1151
|
+
* In-progress tool calls have no recovery path — if the stream drops
|
|
1152
|
+
* mid-execution the result is lost — so we keep it under the
|
|
1153
|
+
* Cloudflare edge ~5min idle watchdog.
|
|
1154
|
+
* - Standalone GET stream (`key === "_standalone"`): keepalive only
|
|
1155
|
+
* when no `eventStore` is configured. When resumability is enabled,
|
|
1156
|
+
* clients reconnect with `Last-Event-ID` after an idle drop, so we
|
|
1157
|
+
* skip the keepalive and let the DO hibernate.
|
|
1158
|
+
*
|
|
1159
|
+
* Uses the shared `sse-keepalive` constants so both this wrapper and
|
|
1160
|
+
* `McpAgent.serve()` write identical frames at the same cadence.
|
|
1161
|
+
* See cloudflare/agents#1583.
|
|
1162
|
+
*/
|
|
1163
|
+
withKeepalive(response, key) {
|
|
1164
|
+
if (!(response.headers.get("Content-Type") ?? "").includes("text/event-stream") || !response.body) return response;
|
|
1165
|
+
if (key === "_standalone" && this.eventStoreConfigured()) return response;
|
|
1166
|
+
const encoder = new TextEncoder();
|
|
1167
|
+
let intervalId;
|
|
1168
|
+
let controllerRef;
|
|
1169
|
+
const clear = () => {
|
|
1170
|
+
if (intervalId !== void 0) {
|
|
1171
|
+
clearInterval(intervalId);
|
|
1172
|
+
intervalId = void 0;
|
|
1173
|
+
}
|
|
1174
|
+
if (key !== void 0) this._keepaliveCleanups.delete(key);
|
|
1175
|
+
};
|
|
1176
|
+
const transform = new TransformStream({
|
|
1177
|
+
start: (controller) => {
|
|
1178
|
+
controllerRef = controller;
|
|
1179
|
+
intervalId = setInterval(() => {
|
|
1180
|
+
try {
|
|
1181
|
+
controllerRef?.enqueue(encoder.encode(KEEPALIVE_FRAME));
|
|
1182
|
+
} catch {
|
|
1183
|
+
clear();
|
|
1184
|
+
}
|
|
1185
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
1186
|
+
if (key !== void 0) this._keepaliveCleanups.set(key, clear);
|
|
1084
1187
|
},
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1188
|
+
transform(chunk, controller) {
|
|
1189
|
+
controller.enqueue(chunk);
|
|
1190
|
+
},
|
|
1191
|
+
flush() {
|
|
1192
|
+
clear();
|
|
1193
|
+
},
|
|
1194
|
+
cancel() {
|
|
1195
|
+
clear();
|
|
1091
1196
|
}
|
|
1092
1197
|
});
|
|
1198
|
+
const piped = response.body.pipeThrough(transform);
|
|
1199
|
+
return new Response(piped, {
|
|
1200
|
+
status: response.status,
|
|
1201
|
+
statusText: response.statusText,
|
|
1202
|
+
headers: response.headers
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Does the SDK transport have an `eventStore`? Reaches into the SDK's
|
|
1207
|
+
* private field because the option isn't surfaced on the public API —
|
|
1208
|
+
* we only need a yes/no for keepalive policy.
|
|
1209
|
+
*/
|
|
1210
|
+
eventStoreConfigured() {
|
|
1211
|
+
return this._eventStore !== void 0;
|
|
1093
1212
|
}
|
|
1094
|
-
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
methods: "GET, POST, DELETE, OPTIONS",
|
|
1099
|
-
exposeHeaders: "mcp-session-id",
|
|
1100
|
-
maxAge: 86400,
|
|
1101
|
-
...this.corsOptions
|
|
1213
|
+
getCorsHeaders({ forPreflight } = {}) {
|
|
1214
|
+
const merged = {
|
|
1215
|
+
...DEFAULT_CORS_OPTIONS,
|
|
1216
|
+
...this._corsOptions
|
|
1102
1217
|
};
|
|
1103
1218
|
if (forPreflight) return {
|
|
1104
|
-
"Access-Control-Allow-Origin":
|
|
1105
|
-
"Access-Control-Allow-Headers":
|
|
1106
|
-
"Access-Control-Allow-Methods":
|
|
1107
|
-
"Access-Control-Max-Age":
|
|
1219
|
+
"Access-Control-Allow-Origin": merged.origin,
|
|
1220
|
+
"Access-Control-Allow-Headers": merged.headers,
|
|
1221
|
+
"Access-Control-Allow-Methods": merged.methods,
|
|
1222
|
+
"Access-Control-Max-Age": String(merged.maxAge)
|
|
1108
1223
|
};
|
|
1109
1224
|
return {
|
|
1110
|
-
"Access-Control-Allow-Origin":
|
|
1111
|
-
"Access-Control-Expose-Headers":
|
|
1225
|
+
"Access-Control-Allow-Origin": merged.origin,
|
|
1226
|
+
"Access-Control-Expose-Headers": merged.exposeHeaders
|
|
1112
1227
|
};
|
|
1113
1228
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
default: return this.handleUnsupportedRequest();
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
async handleGetRequest(request) {
|
|
1125
|
-
if (!request.headers.get("Accept")?.includes("text/event-stream")) return new Response(JSON.stringify({
|
|
1126
|
-
jsonrpc: "2.0",
|
|
1127
|
-
error: {
|
|
1128
|
-
code: -32e3,
|
|
1129
|
-
message: "Not Acceptable: Client must accept text/event-stream"
|
|
1130
|
-
},
|
|
1131
|
-
id: null
|
|
1132
|
-
}), {
|
|
1133
|
-
status: 406,
|
|
1134
|
-
headers: {
|
|
1135
|
-
"Content-Type": "application/json",
|
|
1136
|
-
...this.getHeaders()
|
|
1137
|
-
}
|
|
1229
|
+
withCorsHeaders(response) {
|
|
1230
|
+
const headers = new Headers(response.headers);
|
|
1231
|
+
for (const [k, v] of Object.entries(this.getCorsHeaders())) headers.set(k, v);
|
|
1232
|
+
return new Response(response.body, {
|
|
1233
|
+
status: response.status,
|
|
1234
|
+
statusText: response.statusText,
|
|
1235
|
+
headers
|
|
1138
1236
|
});
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const eventStreamId = await this.eventStore.getStreamIdForEventId?.(lastEventId);
|
|
1147
|
-
if (eventStreamId) streamId = eventStreamId;
|
|
1148
|
-
}
|
|
1149
|
-
if (this.streamMapping.get(streamId) !== void 0) return new Response(JSON.stringify({
|
|
1150
|
-
jsonrpc: "2.0",
|
|
1151
|
-
error: {
|
|
1152
|
-
code: -32e3,
|
|
1153
|
-
message: "Conflict: Only one SSE stream is allowed per session"
|
|
1154
|
-
},
|
|
1155
|
-
id: null
|
|
1156
|
-
}), {
|
|
1157
|
-
status: 409,
|
|
1158
|
-
headers: {
|
|
1159
|
-
"Content-Type": "application/json",
|
|
1160
|
-
...this.getHeaders()
|
|
1161
|
-
}
|
|
1162
|
-
});
|
|
1163
|
-
const { readable, writable } = new TransformStream();
|
|
1164
|
-
const writer = writable.getWriter();
|
|
1165
|
-
const encoder = new TextEncoder();
|
|
1166
|
-
const headers = new Headers({
|
|
1167
|
-
"Content-Type": "text/event-stream",
|
|
1168
|
-
"Cache-Control": "no-cache",
|
|
1169
|
-
Connection: "keep-alive",
|
|
1170
|
-
...this.getHeaders()
|
|
1171
|
-
});
|
|
1172
|
-
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
|
|
1173
|
-
const keepAlive = this.eventStore ? void 0 : startKeepalive(writer, encoder);
|
|
1174
|
-
const cleanup = () => {
|
|
1175
|
-
if (keepAlive !== void 0) clearInterval(keepAlive);
|
|
1176
|
-
this.streamMapping.delete(streamId);
|
|
1177
|
-
writer.close().catch(() => {});
|
|
1237
|
+
}
|
|
1238
|
+
installOnSessionInitializedBridge() {
|
|
1239
|
+
if (this._bridgeInstalled) return;
|
|
1240
|
+
const sdk = this;
|
|
1241
|
+
sdk._onsessioninitialized = async (sessionId) => {
|
|
1242
|
+
if (this._userOnSessionInitialized) await Promise.resolve(this._userOnSessionInitialized(sessionId));
|
|
1243
|
+
await this.saveState();
|
|
1178
1244
|
};
|
|
1179
|
-
this.
|
|
1180
|
-
writer,
|
|
1181
|
-
encoder,
|
|
1182
|
-
cleanup
|
|
1183
|
-
});
|
|
1184
|
-
if (this.retryInterval !== void 0) await writer.write(encoder.encode(`retry: ${this.retryInterval}\n\n`));
|
|
1185
|
-
if (lastEventId && this.eventStore) {
|
|
1186
|
-
const replayedStreamId = await this.eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
|
|
1187
|
-
const data = `id: ${eventId}\nevent: message\ndata: ${JSON.stringify(message)}\n\n`;
|
|
1188
|
-
await writer.write(encoder.encode(data));
|
|
1189
|
-
} });
|
|
1190
|
-
if (replayedStreamId !== streamId) {
|
|
1191
|
-
this.streamMapping.delete(streamId);
|
|
1192
|
-
streamId = replayedStreamId;
|
|
1193
|
-
this.streamMapping.set(streamId, {
|
|
1194
|
-
writer,
|
|
1195
|
-
encoder,
|
|
1196
|
-
cleanup
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
return new Response(readable, { headers });
|
|
1245
|
+
this._bridgeInstalled = true;
|
|
1201
1246
|
}
|
|
1202
|
-
async
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
1205
|
-
jsonrpc: "2.0",
|
|
1206
|
-
error: {
|
|
1207
|
-
code: -32e3,
|
|
1208
|
-
message: "Not Acceptable: Client must accept both application/json and text/event-stream"
|
|
1209
|
-
},
|
|
1210
|
-
id: null
|
|
1211
|
-
}), {
|
|
1212
|
-
status: 406,
|
|
1213
|
-
headers: {
|
|
1214
|
-
"Content-Type": "application/json",
|
|
1215
|
-
...this.getHeaders()
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
if (!request.headers.get("Content-Type")?.includes("application/json")) return new Response(JSON.stringify({
|
|
1219
|
-
jsonrpc: "2.0",
|
|
1220
|
-
error: {
|
|
1221
|
-
code: -32e3,
|
|
1222
|
-
message: "Unsupported Media Type: Content-Type must be application/json"
|
|
1223
|
-
},
|
|
1224
|
-
id: null
|
|
1225
|
-
}), {
|
|
1226
|
-
status: 415,
|
|
1227
|
-
headers: {
|
|
1228
|
-
"Content-Type": "application/json",
|
|
1229
|
-
...this.getHeaders()
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
let rawMessage = parsedBody;
|
|
1233
|
-
if (rawMessage === void 0) try {
|
|
1234
|
-
rawMessage = await request.json();
|
|
1235
|
-
} catch {
|
|
1236
|
-
return new Response(JSON.stringify({
|
|
1237
|
-
jsonrpc: "2.0",
|
|
1238
|
-
error: {
|
|
1239
|
-
code: -32700,
|
|
1240
|
-
message: "Parse error: Invalid JSON"
|
|
1241
|
-
},
|
|
1242
|
-
id: null
|
|
1243
|
-
}), {
|
|
1244
|
-
status: 400,
|
|
1245
|
-
headers: {
|
|
1246
|
-
"Content-Type": "application/json",
|
|
1247
|
-
...this.getHeaders()
|
|
1248
|
-
}
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
let messages;
|
|
1247
|
+
async captureInitializeParams(request, handleOptions) {
|
|
1248
|
+
this._pendingRequestId = void 0;
|
|
1249
|
+
if (request.method !== "POST") return;
|
|
1252
1250
|
try {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
message: "Parse error: Invalid JSON-RPC message"
|
|
1261
|
-
},
|
|
1262
|
-
id: null
|
|
1263
|
-
}), {
|
|
1264
|
-
status: 400,
|
|
1265
|
-
headers: {
|
|
1266
|
-
"Content-Type": "application/json",
|
|
1267
|
-
...this.getHeaders()
|
|
1268
|
-
}
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
const requestInfo = {
|
|
1272
|
-
headers: Object.fromEntries(request.headers.entries()),
|
|
1273
|
-
url: new URL(request.url)
|
|
1274
|
-
};
|
|
1275
|
-
const isInitializationRequest = messages.some(isInitializeRequest);
|
|
1276
|
-
if (isInitializationRequest) {
|
|
1277
|
-
if (this.initialized && this.sessionId !== void 0) return new Response(JSON.stringify({
|
|
1278
|
-
jsonrpc: "2.0",
|
|
1279
|
-
error: {
|
|
1280
|
-
code: -32600,
|
|
1281
|
-
message: "Invalid Request: Server already initialized"
|
|
1282
|
-
},
|
|
1283
|
-
id: null
|
|
1284
|
-
}), {
|
|
1285
|
-
status: 400,
|
|
1286
|
-
headers: {
|
|
1287
|
-
"Content-Type": "application/json",
|
|
1288
|
-
...this.getHeaders()
|
|
1289
|
-
}
|
|
1290
|
-
});
|
|
1291
|
-
if (messages.length > 1) return new Response(JSON.stringify({
|
|
1292
|
-
jsonrpc: "2.0",
|
|
1293
|
-
error: {
|
|
1294
|
-
code: -32600,
|
|
1295
|
-
message: "Invalid Request: Only one initialization request is allowed"
|
|
1296
|
-
},
|
|
1297
|
-
id: null
|
|
1298
|
-
}), {
|
|
1299
|
-
status: 400,
|
|
1300
|
-
headers: {
|
|
1301
|
-
"Content-Type": "application/json",
|
|
1302
|
-
...this.getHeaders()
|
|
1303
|
-
}
|
|
1304
|
-
});
|
|
1305
|
-
this.sessionId = this.sessionIdGenerator?.();
|
|
1306
|
-
this.initialized = true;
|
|
1307
|
-
const initMessage = messages.find(isInitializeRequest);
|
|
1308
|
-
if (initMessage && isInitializeRequest(initMessage)) this.initializeParams = {
|
|
1309
|
-
capabilities: initMessage.params.capabilities,
|
|
1310
|
-
clientInfo: initMessage.params.clientInfo,
|
|
1311
|
-
protocolVersion: initMessage.params.protocolVersion
|
|
1251
|
+
const parsed = handleOptions?.parsedBody ?? await request.clone().json();
|
|
1252
|
+
const messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
1253
|
+
const init = messages.find((m) => typeof m === "object" && m !== null && isInitializeRequest(m));
|
|
1254
|
+
if (init && isInitializeRequest(init)) this._capturedInitializeParams = {
|
|
1255
|
+
capabilities: init.params.capabilities,
|
|
1256
|
+
clientInfo: init.params.clientInfo,
|
|
1257
|
+
protocolVersion: init.params.protocolVersion
|
|
1312
1258
|
};
|
|
1313
|
-
|
|
1314
|
-
if (this.
|
|
1315
|
-
}
|
|
1316
|
-
if (!isInitializationRequest) {
|
|
1317
|
-
const sessionError = this.validateSession(request);
|
|
1318
|
-
if (sessionError) return sessionError;
|
|
1319
|
-
const versionError = this.validateProtocolVersion(request);
|
|
1320
|
-
if (versionError) return versionError;
|
|
1321
|
-
}
|
|
1322
|
-
if (!messages.some(isJSONRPCRequest)) {
|
|
1323
|
-
for (const message of messages) this.onmessage?.(message, { requestInfo });
|
|
1324
|
-
return new Response(null, {
|
|
1325
|
-
status: 202,
|
|
1326
|
-
headers: { ...this.getHeaders() }
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
const streamId = crypto.randomUUID();
|
|
1330
|
-
if (this.enableJsonResponse) return new Promise((resolve) => {
|
|
1331
|
-
this.streamMapping.set(streamId, {
|
|
1332
|
-
resolveJson: resolve,
|
|
1333
|
-
cleanup: () => {
|
|
1334
|
-
this.streamMapping.delete(streamId);
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
|
|
1338
|
-
for (const message of messages) this.onmessage?.(message, { requestInfo });
|
|
1339
|
-
});
|
|
1340
|
-
const { readable, writable } = new TransformStream();
|
|
1341
|
-
const writer = writable.getWriter();
|
|
1342
|
-
const encoder = new TextEncoder();
|
|
1343
|
-
const headers = new Headers({
|
|
1344
|
-
"Content-Type": "text/event-stream",
|
|
1345
|
-
"Cache-Control": "no-cache",
|
|
1346
|
-
Connection: "keep-alive",
|
|
1347
|
-
...this.getHeaders()
|
|
1348
|
-
});
|
|
1349
|
-
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
|
|
1350
|
-
const keepAlive = startKeepalive(writer, encoder);
|
|
1351
|
-
this.streamMapping.set(streamId, {
|
|
1352
|
-
writer,
|
|
1353
|
-
encoder,
|
|
1354
|
-
cleanup: () => {
|
|
1355
|
-
clearInterval(keepAlive);
|
|
1356
|
-
this.streamMapping.delete(streamId);
|
|
1357
|
-
writer.close().catch(() => {});
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
|
|
1361
|
-
for (const message of messages) this.onmessage?.(message, { requestInfo });
|
|
1362
|
-
return new Response(readable, { headers });
|
|
1363
|
-
}
|
|
1364
|
-
async handleDeleteRequest(request) {
|
|
1365
|
-
const sessionError = this.validateSession(request);
|
|
1366
|
-
if (sessionError) return sessionError;
|
|
1367
|
-
const versionError = this.validateProtocolVersion(request);
|
|
1368
|
-
if (versionError) return versionError;
|
|
1369
|
-
const closedSessionId = this.sessionId;
|
|
1370
|
-
await this.close();
|
|
1371
|
-
if (closedSessionId && this.onsessionclosed) this.onsessionclosed(closedSessionId);
|
|
1372
|
-
return new Response(null, {
|
|
1373
|
-
status: 200,
|
|
1374
|
-
headers: { ...this.getHeaders() }
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
handleOptionsRequest(_request) {
|
|
1378
|
-
return new Response(null, {
|
|
1379
|
-
status: 200,
|
|
1380
|
-
headers: { ...this.getHeaders({ forPreflight: true }) }
|
|
1381
|
-
});
|
|
1259
|
+
const firstRequest = messages.find((m) => typeof m === "object" && m !== null && "id" in m && "method" in m);
|
|
1260
|
+
if (firstRequest) this._pendingRequestId = firstRequest.id;
|
|
1261
|
+
} catch {}
|
|
1382
1262
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
validateSession(request) {
|
|
1400
|
-
if (this.sessionIdGenerator === void 0) return;
|
|
1401
|
-
if (!this.initialized) return new Response(JSON.stringify({
|
|
1402
|
-
jsonrpc: "2.0",
|
|
1403
|
-
error: {
|
|
1404
|
-
code: -32e3,
|
|
1405
|
-
message: "Bad Request: Server not initialized"
|
|
1406
|
-
},
|
|
1407
|
-
id: null
|
|
1408
|
-
}), {
|
|
1409
|
-
status: 400,
|
|
1410
|
-
headers: {
|
|
1411
|
-
"Content-Type": "application/json",
|
|
1412
|
-
...this.getHeaders()
|
|
1413
|
-
}
|
|
1414
|
-
});
|
|
1415
|
-
const sessionId = request.headers.get("mcp-session-id");
|
|
1416
|
-
if (!sessionId) return new Response(JSON.stringify({
|
|
1417
|
-
jsonrpc: "2.0",
|
|
1418
|
-
error: {
|
|
1419
|
-
code: -32e3,
|
|
1420
|
-
message: "Bad Request: Mcp-Session-Id header is required"
|
|
1421
|
-
},
|
|
1422
|
-
id: null
|
|
1423
|
-
}), {
|
|
1424
|
-
status: 400,
|
|
1425
|
-
headers: {
|
|
1426
|
-
"Content-Type": "application/json",
|
|
1427
|
-
...this.getHeaders()
|
|
1428
|
-
}
|
|
1429
|
-
});
|
|
1430
|
-
if (sessionId !== this.sessionId) return new Response(JSON.stringify({
|
|
1263
|
+
async restoreState() {
|
|
1264
|
+
if (!this._storage || this._stateRestored) return;
|
|
1265
|
+
this._stateRestored = true;
|
|
1266
|
+
let state;
|
|
1267
|
+
try {
|
|
1268
|
+
state = await Promise.resolve(this._storage.get());
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
this._stateRestored = false;
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
if (!state) return;
|
|
1274
|
+
const sdk = this;
|
|
1275
|
+
sdk.sessionId = state.sessionId;
|
|
1276
|
+
sdk._initialized = state.initialized;
|
|
1277
|
+
this._capturedInitializeParams = state.initializeParams;
|
|
1278
|
+
if (state.initializeParams && this.onmessage) this.onmessage({
|
|
1431
1279
|
jsonrpc: "2.0",
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
},
|
|
1436
|
-
id: null
|
|
1437
|
-
}), {
|
|
1438
|
-
status: 404,
|
|
1439
|
-
headers: {
|
|
1440
|
-
"Content-Type": "application/json",
|
|
1441
|
-
...this.getHeaders()
|
|
1442
|
-
}
|
|
1280
|
+
id: RESTORE_REQUEST_ID,
|
|
1281
|
+
method: "initialize",
|
|
1282
|
+
params: state.initializeParams
|
|
1443
1283
|
});
|
|
1444
1284
|
}
|
|
1445
|
-
async
|
|
1446
|
-
|
|
1447
|
-
this
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
* client will reconnect after the retry interval specified in the priming event.
|
|
1455
|
-
*/
|
|
1456
|
-
closeSSEStream(requestId) {
|
|
1457
|
-
const streamId = this.requestToStreamMapping.get(requestId);
|
|
1458
|
-
if (!streamId) return;
|
|
1459
|
-
const stream = this.streamMapping.get(streamId);
|
|
1460
|
-
if (stream) stream.cleanup();
|
|
1461
|
-
for (const [reqId, sid] of this.requestToStreamMapping.entries()) if (sid === streamId) {
|
|
1462
|
-
this.requestToStreamMapping.delete(reqId);
|
|
1463
|
-
this.requestResponseMap.delete(reqId);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
async send(message, options) {
|
|
1467
|
-
let requestId = options?.relatedRequestId;
|
|
1468
|
-
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
|
|
1469
|
-
if (requestId === RESTORE_REQUEST_ID) return;
|
|
1470
|
-
if (requestId === void 0) {
|
|
1471
|
-
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
|
|
1472
|
-
const standaloneSse = this.streamMapping.get(this.standaloneSseStreamId);
|
|
1473
|
-
if (standaloneSse === void 0) return;
|
|
1474
|
-
if (standaloneSse.writer && standaloneSse.encoder) {
|
|
1475
|
-
let eventId;
|
|
1476
|
-
if (this.eventStore) eventId = await this.eventStore.storeEvent(this.standaloneSseStreamId, message);
|
|
1477
|
-
const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
|
|
1478
|
-
await standaloneSse.writer.write(standaloneSse.encoder.encode(data));
|
|
1479
|
-
}
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
const streamId = this.requestToStreamMapping.get(requestId);
|
|
1483
|
-
if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
|
|
1484
|
-
const response = this.streamMapping.get(streamId);
|
|
1485
|
-
if (!response) throw new Error(`No connection established for request ID: ${String(requestId)}`);
|
|
1486
|
-
if (!this.enableJsonResponse) {
|
|
1487
|
-
if (response.writer && response.encoder) {
|
|
1488
|
-
let eventId;
|
|
1489
|
-
if (this.eventStore) eventId = await this.eventStore.storeEvent(streamId, message);
|
|
1490
|
-
const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
|
|
1491
|
-
await response.writer.write(response.encoder.encode(data));
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
1495
|
-
this.requestResponseMap.set(requestId, message);
|
|
1496
|
-
const relatedIds = Array.from(this.requestToStreamMapping.entries()).filter(([, sid]) => sid === streamId).map(([id]) => id);
|
|
1497
|
-
if (relatedIds.every((id) => this.requestResponseMap.has(id))) {
|
|
1498
|
-
if (this.enableJsonResponse && response.resolveJson) {
|
|
1499
|
-
const responses = relatedIds.map((id) => this.requestResponseMap.get(id));
|
|
1500
|
-
const headers = new Headers({
|
|
1501
|
-
"Content-Type": "application/json",
|
|
1502
|
-
...this.getHeaders()
|
|
1503
|
-
});
|
|
1504
|
-
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
|
|
1505
|
-
const body = responses.length === 1 ? responses[0] : responses;
|
|
1506
|
-
response.resolveJson(new Response(JSON.stringify(body), { headers }));
|
|
1507
|
-
} else response.cleanup();
|
|
1508
|
-
for (const id of relatedIds) {
|
|
1509
|
-
this.requestResponseMap.delete(id);
|
|
1510
|
-
this.requestToStreamMapping.delete(id);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1285
|
+
async saveState() {
|
|
1286
|
+
if (!this._storage) return;
|
|
1287
|
+
const sdk = this;
|
|
1288
|
+
const state = {
|
|
1289
|
+
sessionId: sdk.sessionId,
|
|
1290
|
+
initialized: sdk._initialized,
|
|
1291
|
+
initializeParams: this._capturedInitializeParams
|
|
1292
|
+
};
|
|
1293
|
+
await Promise.resolve(this._storage.set(state));
|
|
1514
1294
|
}
|
|
1515
1295
|
};
|
|
1516
1296
|
//#endregion
|
|
@@ -1526,30 +1306,25 @@ function runWithAuthContext(context, fn) {
|
|
|
1526
1306
|
//#region src/mcp/handler.ts
|
|
1527
1307
|
function createMcpHandler(server, options = {}) {
|
|
1528
1308
|
const route = options.route ?? "/mcp";
|
|
1309
|
+
const { route: _route, authContext, transport: providedTransport, ...transportOptions } = options;
|
|
1529
1310
|
return async (request, _env, ctx) => {
|
|
1530
1311
|
const url = new URL(request.url);
|
|
1531
1312
|
if (route && url.pathname !== route) return new Response("Not Found", { status: 404 });
|
|
1532
|
-
const transport =
|
|
1533
|
-
sessionIdGenerator: options.sessionIdGenerator,
|
|
1534
|
-
enableJsonResponse: options.enableJsonResponse,
|
|
1535
|
-
onsessioninitialized: options.onsessioninitialized,
|
|
1536
|
-
corsOptions: options.corsOptions,
|
|
1537
|
-
storage: options.storage
|
|
1538
|
-
});
|
|
1313
|
+
const transport = providedTransport ?? new WorkerTransport(transportOptions);
|
|
1539
1314
|
const buildAuthContext = () => {
|
|
1540
|
-
if (
|
|
1315
|
+
if (authContext) return authContext;
|
|
1541
1316
|
if (ctx.props && Object.keys(ctx.props).length > 0) return { props: ctx.props };
|
|
1542
1317
|
};
|
|
1543
1318
|
const handleRequest = async () => {
|
|
1544
1319
|
return await transport.handleRequest(request);
|
|
1545
1320
|
};
|
|
1546
|
-
const
|
|
1321
|
+
const resolvedAuthContext = buildAuthContext();
|
|
1547
1322
|
if (!transport.started) {
|
|
1548
1323
|
if (server instanceof McpServer ? server.isConnected() : server.transport !== void 0) throw new Error("Server is already connected to a transport. Create a new McpServer instance per request for stateless handlers.");
|
|
1549
1324
|
await server.connect(transport);
|
|
1550
1325
|
}
|
|
1551
1326
|
try {
|
|
1552
|
-
if (
|
|
1327
|
+
if (resolvedAuthContext) return await runWithAuthContext(resolvedAuthContext, handleRequest);
|
|
1553
1328
|
else return await handleRequest();
|
|
1554
1329
|
} catch (error) {
|
|
1555
1330
|
console.error("MCP handler error:", error);
|