agents
Version:
A home for your AI agents
1,506 lines • 53 kB
JavaScript
import { t as MessageType } from "../types-BITaDFf-.js";
import "../email-XHsSYsTO.js";
import "../internal_context-D9eKFth1.js";
import "../client-CtC9E06G.js";
import "../do-oauth-client-provider-DDg8QrEA.js";
import { o as getAgentByName, s as getCurrentAgent, t as Agent } from "../src-i_UcyBYf.js";
import { AsyncLocalStorage } from "node:async_hooks";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema, SUPPORTED_PROTOCOL_VERSIONS, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
//#region src/mcp/utils.ts
/**
* Since we use WebSockets to bridge the client to the
* MCP transport in the Agent, we use this header to signal
* the method of the original request the user made, while
* leaving the WS Upgrade request as GET.
*/
const MCP_HTTP_METHOD_HEADER = "cf-mcp-method";
/**
* Since we use WebSockets to bridge the client to the
* MCP transport in the Agent, we use this header to include
* the original request body.
*/
const MCP_MESSAGE_HEADER = "cf-mcp-message";
const MAXIMUM_MESSAGE_SIZE_BYTES = 4 * 1024 * 1024;
const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
let pathname = basePath;
if (basePath === "/") pathname = "/*";
const basePattern = new URLPattern({ pathname });
return async (request, ctx) => {
const url = new URL(request.url);
if (basePattern.test(url)) {
if (request.method === "POST") {
const acceptHeader = request.headers.get("accept");
if (!acceptHeader?.includes("application/json") || !acceptHeader.includes("text/event-stream")) {
const body = JSON.stringify({
error: {
code: -32e3,
message: "Not Acceptable: Client must accept both application/json and text/event-stream"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 406 });
}
const ct = request.headers.get("content-type");
if (!ct || !ct.includes("application/json")) {
const body = JSON.stringify({
error: {
code: -32e3,
message: "Unsupported Media Type: Content-Type must be application/json"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 415 });
}
if (Number.parseInt(request.headers.get("content-length") ?? "0", 10) > MAXIMUM_MESSAGE_SIZE_BYTES) {
const body = JSON.stringify({
error: {
code: -32e3,
message: `Request body too large. Maximum size is ${MAXIMUM_MESSAGE_SIZE_BYTES} bytes`
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 413 });
}
let sessionId = request.headers.get("mcp-session-id");
let rawMessage;
try {
rawMessage = await request.json();
} catch (_error) {
const body = JSON.stringify({
error: {
code: -32700,
message: "Parse error: Invalid JSON"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 400 });
}
let arrayMessage;
if (Array.isArray(rawMessage)) arrayMessage = rawMessage;
else arrayMessage = [rawMessage];
let messages = [];
for (const msg of arrayMessage) if (!JSONRPCMessageSchema.safeParse(msg).success) {
const body = JSON.stringify({
error: {
code: -32700,
message: "Parse error: Invalid JSON-RPC message"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 400 });
}
messages = arrayMessage.map((msg) => JSONRPCMessageSchema.parse(msg));
const maybeInitializeRequest = messages.find((msg) => InitializeRequestSchema.safeParse(msg).success);
if (!!maybeInitializeRequest && sessionId) {
const body = JSON.stringify({
error: {
code: -32600,
message: "Invalid Request: Initialization requests must not include a sessionId"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 400 });
}
if (!!maybeInitializeRequest && messages.length > 1) {
const body = JSON.stringify({
error: {
code: -32600,
message: "Invalid Request: Only one initialization request is allowed"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 400 });
}
if (!maybeInitializeRequest && !sessionId) {
const body = JSON.stringify({
error: {
code: -32e3,
message: "Bad Request: Mcp-Session-Id header is required"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 400 });
}
sessionId = sessionId ?? namespace.newUniqueId().toString();
const agent = await getAgentByName(namespace, `streamable-http:${sessionId}`, {
props: ctx.props,
jurisdiction: options.jurisdiction
});
const isInitialized = await agent.getInitializeRequest();
if (maybeInitializeRequest) await agent.setInitializeRequest(maybeInitializeRequest);
else if (!isInitialized) {
const body = JSON.stringify({
error: {
code: -32001,
message: "Session not found"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 404 });
}
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const existingHeaders = {};
request.headers.forEach((value, key) => {
existingHeaders[key] = value;
});
const req = new Request(request.url, { headers: {
...existingHeaders,
[MCP_HTTP_METHOD_HEADER]: "POST",
[MCP_MESSAGE_HEADER]: Buffer.from(JSON.stringify(messages)).toString("base64"),
Upgrade: "websocket"
} });
if (ctx.props) agent.updateProps(ctx.props);
const ws = (await agent.fetch(req)).webSocket;
if (!ws) {
console.error("Failed to establish WebSocket connection");
await writer.close();
const body = JSON.stringify({
error: {
code: -32001,
message: "Failed to establish WebSocket connection"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 500 });
}
ws.accept();
ws.addEventListener("message", (event) => {
async function onMessage(event) {
try {
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
const message = JSON.parse(data);
if (message.type !== MessageType.CF_MCP_AGENT_EVENT) return;
await writer.write(encoder.encode(message.event));
if (message.close) {
ws?.close();
await writer.close().catch(() => {});
}
} catch (error) {
console.error("Error forwarding message to SSE:", error);
}
}
onMessage(event).catch(console.error);
});
ws.addEventListener("error", (error) => {
async function onError(_error) {
await writer.close().catch(() => {});
}
onError(error).catch(console.error);
});
ws.addEventListener("close", () => {
async function onClose() {
await writer.close().catch(() => {});
}
onClose().catch(console.error);
});
if (messages.every((msg) => isJSONRPCNotification(msg) || isJSONRPCResultResponse(msg))) {
ws.close();
return new Response(null, {
headers: corsHeaders(request, options.corsOptions),
status: 202
});
}
return new Response(readable, {
headers: {
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
"mcp-session-id": sessionId,
...corsHeaders(request, options.corsOptions)
},
status: 200
});
} else if (request.method === "GET") {
if (!request.headers.get("accept")?.includes("text/event-stream")) {
const body = JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Not Acceptable: Client must accept text/event-stream"
},
id: null
});
return new Response(body, { status: 406 });
}
const sessionId = request.headers.get("mcp-session-id");
if (!sessionId) return new Response(JSON.stringify({
error: {
code: -32e3,
message: "Bad Request: Mcp-Session-Id header is required"
},
id: null,
jsonrpc: "2.0"
}), { status: 400 });
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const agent = await getAgentByName(namespace, `streamable-http:${sessionId}`, {
props: ctx.props,
jurisdiction: options.jurisdiction
});
if (!await agent.getInitializeRequest()) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Session not found"
},
id: null
}), { status: 404 });
const existingHeaders = {};
request.headers.forEach((v, k) => {
existingHeaders[k] = v;
});
if (ctx.props) agent.updateProps(ctx.props);
const ws = (await agent.fetch(new Request(request.url, { headers: {
...existingHeaders,
[MCP_HTTP_METHOD_HEADER]: "GET",
Upgrade: "websocket"
} }))).webSocket;
if (!ws) {
await writer.close();
return new Response("Failed to establish WS to DO", { status: 500 });
}
ws.accept();
ws.addEventListener("message", (event) => {
try {
async function onMessage(ev) {
const data = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);
const message = JSON.parse(data);
if (message.type !== MessageType.CF_MCP_AGENT_EVENT) return;
await writer.write(encoder.encode(message.event));
}
onMessage(event).catch(console.error);
} catch (e) {
console.error("Error forwarding message to SSE:", e);
}
});
ws.addEventListener("error", () => {
writer.close().catch(() => {});
});
ws.addEventListener("close", () => {
writer.close().catch(() => {});
});
return new Response(readable, {
headers: {
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
"mcp-session-id": sessionId,
...corsHeaders(request, options.corsOptions)
},
status: 200
});
} else if (request.method === "DELETE") {
const sessionId = request.headers.get("mcp-session-id");
if (!sessionId) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Bad Request: Mcp-Session-Id header is required"
},
id: null
}), {
status: 400,
headers: corsHeaders(request, options.corsOptions)
});
const agent = await getAgentByName(namespace, `streamable-http:${sessionId}`, { jurisdiction: options.jurisdiction });
if (!await agent.getInitializeRequest()) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Session not found"
},
id: null
}), {
status: 404,
headers: corsHeaders(request, options.corsOptions)
});
ctx.waitUntil(agent.destroy().catch(() => {}));
return new Response(null, {
status: 204,
headers: corsHeaders(request, options.corsOptions)
});
}
}
const body = JSON.stringify({
error: {
code: -32e3,
message: "Not found"
},
id: null,
jsonrpc: "2.0"
});
return new Response(body, { status: 404 });
};
};
const createLegacySseHandler = (basePath, namespace, options = {}) => {
let pathname = basePath;
if (basePath === "/") pathname = "/*";
const basePattern = new URLPattern({ pathname });
const messagePattern = new URLPattern({ pathname: `${basePath}/message` });
return async (request, ctx) => {
const url = new URL(request.url);
if (request.method === "GET" && basePattern.test(url)) {
const sessionId = url.searchParams.get("sessionId") || namespace.newUniqueId().toString();
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const endpointUrl = new URL(request.url);
endpointUrl.pathname = encodeURI(`${basePath}/message`);
endpointUrl.searchParams.set("sessionId", sessionId);
const endpointMessage = `event: endpoint\ndata: ${endpointUrl.pathname + endpointUrl.search + endpointUrl.hash}\n\n`;
writer.write(encoder.encode(endpointMessage));
const agent = await getAgentByName(namespace, `sse:${sessionId}`, {
props: ctx.props,
jurisdiction: options.jurisdiction
});
const existingHeaders = {};
request.headers.forEach((value, key) => {
existingHeaders[key] = value;
});
if (ctx.props) agent.updateProps(ctx.props);
const ws = (await agent.fetch(new Request(request.url, { headers: {
...existingHeaders,
Upgrade: "websocket"
} }))).webSocket;
if (!ws) {
console.error("Failed to establish WebSocket connection");
await writer.close();
return new Response("Failed to establish WebSocket connection", { status: 500 });
}
ws.accept();
ws.addEventListener("message", (event) => {
async function onMessage(event) {
try {
const message = JSON.parse(event.data);
const result = JSONRPCMessageSchema.safeParse(message);
if (!result.success) return;
const messageText = `event: message\ndata: ${JSON.stringify(result.data)}\n\n`;
await writer.write(encoder.encode(messageText));
} catch (error) {
console.error("Error forwarding message to SSE:", error);
}
}
onMessage(event).catch(console.error);
});
ws.addEventListener("error", (error) => {
async function onError(_error) {
try {
await writer.close();
} catch (_e) {}
}
onError(error).catch(console.error);
});
ws.addEventListener("close", () => {
async function onClose() {
try {
await writer.close();
} catch (error) {
console.error("Error closing SSE connection:", error);
}
}
onClose().catch(console.error);
});
return new Response(readable, { headers: {
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
...corsHeaders(request, options.corsOptions)
} });
}
if (request.method === "POST" && messagePattern.test(url)) {
const sessionId = url.searchParams.get("sessionId");
if (!sessionId) return new Response(`Missing sessionId. Expected POST to ${basePath} to initiate new one`, { status: 400 });
const contentType = request.headers.get("content-type") || "";
if (!contentType.includes("application/json")) return new Response(`Unsupported content-type: ${contentType}`, { status: 400 });
const contentLength = Number.parseInt(request.headers.get("content-length") || "0", 10);
if (contentLength > MAXIMUM_MESSAGE_SIZE_BYTES) return new Response(`Request body too large: ${contentLength} bytes`, { status: 400 });
const agent = await getAgentByName(namespace, `sse:${sessionId}`, {
props: ctx.props,
jurisdiction: options.jurisdiction
});
const messageBody = await request.json();
const extraInfo = { requestInfo: { headers: Object.fromEntries(request.headers.entries()) } };
const error = await agent.onSSEMcpMessage(sessionId, messageBody, extraInfo);
if (error) return new Response(error.message, {
headers: {
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
...corsHeaders(request, options.corsOptions)
},
status: 400
});
return new Response("Accepted", {
headers: {
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Content-Type": "text/event-stream",
...corsHeaders(request, options.corsOptions)
},
status: 202
});
}
return new Response("Not Found", { status: 404 });
};
};
function corsHeaders(_request, corsOptions = {}) {
const origin = "*";
return {
"Access-Control-Allow-Headers": corsOptions.headers || "Content-Type, Accept, mcp-session-id, mcp-protocol-version",
"Access-Control-Allow-Methods": corsOptions.methods || "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Origin": corsOptions.origin || origin,
"Access-Control-Expose-Headers": corsOptions.exposeHeaders || "mcp-session-id",
"Access-Control-Max-Age": (corsOptions.maxAge || 86400).toString()
};
}
function handleCORS(request, corsOptions) {
if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders(request, corsOptions) });
return null;
}
function isDurableObjectNamespace(namespace) {
return typeof namespace === "object" && namespace !== null && "newUniqueId" in namespace && typeof namespace.newUniqueId === "function" && "idFromName" in namespace && typeof namespace.idFromName === "function";
}
//#endregion
//#region src/mcp/transport.ts
var McpSSETransport = class {
constructor() {
this._started = false;
const { agent } = getCurrentAgent();
if (!agent) throw new Error("McpAgent was not found in Transport constructor");
this.sessionId = agent.getSessionId();
this._getWebSocket = () => agent.getWebSocket();
}
async start() {
if (this._started) throw new Error("Transport already started");
this._started = true;
}
async send(message) {
if (!this._started) throw new Error("Transport not started");
const websocket = this._getWebSocket();
if (!websocket) throw new Error("WebSocket not connected");
try {
websocket.send(JSON.stringify(message));
} catch (error) {
this.onerror?.(error);
}
}
async close() {
this.onclose?.();
}
};
/**
* Adapted from: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts
* - Validation and initialization are removed as they're handled in `McpAgent.serve()` handler.
* - Replaces the Node-style `req`/`res` with Worker's `Request`.
* - Writes events as WS messages that the Worker forwards to the client as SSE events.
* - Replaces the in-memory maps that track requestID/stream by using `connection.setState()` and `agent.getConnections()`.
*
* Besides these points, the implementation is the same and should be updated to match the original as new features are added.
*/
var StreamableHTTPServerTransport = class {
constructor(options) {
this._started = false;
this._requestResponseMap = /* @__PURE__ */ new Map();
const { agent } = getCurrentAgent();
if (!agent) throw new Error("McpAgent was not found in Transport constructor");
this.sessionId = agent.getSessionId();
this._eventStore = options.eventStore;
}
/**
* Starts the transport. This is required by the Transport interface but is a no-op
* for the Streamable HTTP transport as connections are managed per-request.
*/
async start() {
if (this._started) throw new Error("Transport already started");
this._started = true;
}
/**
* Handles GET requests for SSE stream
*/
async handleGetRequest(req) {
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Connection was not found in handleGetRequest");
if (this._eventStore) {
const lastEventId = req.headers.get("last-event-id");
if (lastEventId) {
await this.replayEvents(lastEventId);
return;
}
}
connection.setState({ _standaloneSse: true });
}
/**
* Replays events that would have been sent after the specified event ID
* Only used when resumability is enabled
*/
async replayEvents(lastEventId) {
if (!this._eventStore) return;
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Connection was not available in replayEvents");
try {
await this._eventStore?.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
try {
this.writeSSEEvent(connection, message, eventId);
} catch (error) {
this.onerror?.(error);
}
} });
} catch (error) {
this.onerror?.(error);
}
}
/**
* Writes an event to the SSE stream with proper formatting
*/
writeSSEEvent(connection, message, eventId, close) {
let eventData = "event: message\n";
if (eventId) eventData += `id: ${eventId}\n`;
eventData += `data: ${JSON.stringify(message)}\n\n`;
return connection.send(JSON.stringify({
type: MessageType.CF_MCP_AGENT_EVENT,
event: eventData,
close
}));
}
/**
* Handles POST requests containing JSON-RPC messages
*/
async handlePostRequest(req, parsedBody) {
const authInfo = req.auth;
const requestInfo = { headers: Object.fromEntries(req.headers.entries()) };
delete requestInfo.headers[MCP_HTTP_METHOD_HEADER];
delete requestInfo.headers[MCP_MESSAGE_HEADER];
delete requestInfo.headers.upgrade;
const rawMessage = parsedBody;
let messages;
if (Array.isArray(rawMessage)) messages = rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg));
else messages = [JSONRPCMessageSchema.parse(rawMessage)];
const hasRequests = messages.some(isJSONRPCRequest);
if (!hasRequests) for (const message of messages) {
if (this.messageInterceptor) {
if (await this.messageInterceptor(message, {
authInfo,
requestInfo
})) continue;
}
this.onmessage?.(message, {
authInfo,
requestInfo
});
}
else if (hasRequests) {
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Connection was not found in handlePostRequest");
const requestIds = messages.filter(isJSONRPCRequest).map((message) => message.id);
connection.setState({ requestIds });
for (const message of messages) {
if (this.messageInterceptor) {
if (await this.messageInterceptor(message, {
authInfo,
requestInfo
})) continue;
}
this.onmessage?.(message, {
authInfo,
requestInfo
});
}
}
}
async close() {
const { agent } = getCurrentAgent();
if (!agent) throw new Error("Agent was not found in close");
for (const conn of agent.getConnections()) conn.close(1e3, "Session closed");
this.onclose?.();
}
async send(message, options) {
const { agent } = getCurrentAgent();
if (!agent) throw new Error("Agent was not found in send");
let requestId = options?.relatedRequestId;
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
if (requestId === void 0) {
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
let standaloneConnection;
for (const conn of agent.getConnections()) if (conn.state?._standaloneSse) standaloneConnection = conn;
if (standaloneConnection === void 0) return;
let eventId;
if (this._eventStore) eventId = await this._eventStore.storeEvent(standaloneConnection.id, message);
this.writeSSEEvent(standaloneConnection, message, eventId);
return;
}
const connection = Array.from(agent.getConnections()).find((conn) => conn.state?.requestIds?.includes(requestId));
if (!connection) throw new Error(`No connection established for request ID: ${String(requestId)}`);
let eventId;
if (this._eventStore) eventId = await this._eventStore.storeEvent(connection.id, message);
let shouldClose = false;
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
this._requestResponseMap.set(requestId, message);
const relatedIds = connection.state?.requestIds ?? [];
shouldClose = relatedIds.every((id) => this._requestResponseMap.has(id));
if (shouldClose) for (const id of relatedIds) this._requestResponseMap.delete(id);
}
this.writeSSEEvent(connection, message, eventId, shouldClose);
}
};
//#endregion
//#region src/mcp/client-transports.ts
/**
* Deprecated transport wrappers
*/
let didWarnAboutSSEEdgeClientTransport = false;
/**
* @deprecated Use SSEClientTransport from @modelcontextprotocol/sdk/client/sse.js instead. This alias will be removed in the next major version.
*/
var SSEEdgeClientTransport = class extends SSEClientTransport {
constructor(url, options) {
super(url, options);
if (!didWarnAboutSSEEdgeClientTransport) {
didWarnAboutSSEEdgeClientTransport = true;
console.warn("SSEEdgeClientTransport is deprecated. Use SSEClientTransport from @modelcontextprotocol/sdk/client/sse.js instead. SSEEdgeClientTransport will be removed in the next major version.");
}
}
};
let didWarnAboutStreamableHTTPEdgeClientTransport = false;
/**
* @deprecated Use StreamableHTTPClientTransport from @modelcontextprotocol/sdk/client/streamableHttp.js instead. This alias will be removed in the next major version.
*/
var StreamableHTTPEdgeClientTransport = class extends StreamableHTTPClientTransport {
constructor(url, options) {
super(url, options);
if (!didWarnAboutStreamableHTTPEdgeClientTransport) {
didWarnAboutStreamableHTTPEdgeClientTransport = true;
console.warn("StreamableHTTPEdgeClientTransport is deprecated. Use StreamableHTTPClientTransport from @modelcontextprotocol/sdk/client/streamableHttp.js instead. StreamableHTTPEdgeClientTransport will be removed in the next major version.");
}
}
};
//#endregion
//#region src/mcp/worker-transport.ts
const MCP_PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version";
const RESTORE_REQUEST_ID = "__restore__";
var WorkerTransport = class {
constructor(options) {
this.started = false;
this.initialized = false;
this.enableJsonResponse = false;
this.standaloneSseStreamId = "_GET_stream";
this.streamMapping = /* @__PURE__ */ new Map();
this.requestToStreamMapping = /* @__PURE__ */ new Map();
this.requestResponseMap = /* @__PURE__ */ new Map();
this.stateRestored = false;
this.sessionIdGenerator = options?.sessionIdGenerator;
this.enableJsonResponse = options?.enableJsonResponse ?? false;
this.onsessioninitialized = options?.onsessioninitialized;
this.onsessionclosed = options?.onsessionclosed;
this.corsOptions = options?.corsOptions;
this.storage = options?.storage;
this.eventStore = options?.eventStore;
this.retryInterval = options?.retryInterval;
}
/**
* Restore transport state from persistent storage.
* This is automatically called on start.
*/
async restoreState() {
if (!this.storage || this.stateRestored) return;
const state = await Promise.resolve(this.storage.get());
if (state) {
this.sessionId = state.sessionId;
this.initialized = state.initialized;
if (state.initializeParams && this.onmessage) this.onmessage({
jsonrpc: "2.0",
id: RESTORE_REQUEST_ID,
method: "initialize",
params: state.initializeParams
});
}
this.stateRestored = true;
}
/**
* Persist current transport state to storage.
*/
async saveState() {
if (!this.storage) return;
const state = {
sessionId: this.sessionId,
initialized: this.initialized,
initializeParams: this.initializeParams
};
await Promise.resolve(this.storage.set(state));
}
async start() {
if (this.started) throw new Error("Transport already started");
this.started = true;
}
/**
* Validates the MCP-Protocol-Version header on incoming requests.
*
* This performs a simple check: if a version header is present, it must be
* in the SUPPORTED_PROTOCOL_VERSIONS list. We do not track the negotiated
* version or enforce version consistency across requests - the SDK handles
* version negotiation during initialization, and we simply reject any
* explicitly unsupported versions.
*
* - Header present and supported: Accept
* - Header present and unsupported: 400 Bad Request
* - Header missing: Accept (version validation is optional)
*/
validateProtocolVersion(request) {
const protocolVersion = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")})`
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
}
getHeaders({ forPreflight } = {}) {
const options = {
origin: "*",
headers: "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version",
methods: "GET, POST, DELETE, OPTIONS",
exposeHeaders: "mcp-session-id",
maxAge: 86400,
...this.corsOptions
};
if (forPreflight) return {
"Access-Control-Allow-Origin": options.origin,
"Access-Control-Allow-Headers": options.headers,
"Access-Control-Allow-Methods": options.methods,
"Access-Control-Max-Age": options.maxAge.toString()
};
return {
"Access-Control-Allow-Origin": options.origin,
"Access-Control-Expose-Headers": options.exposeHeaders
};
}
async handleRequest(request, parsedBody) {
await this.restoreState();
switch (request.method) {
case "OPTIONS": return this.handleOptionsRequest(request);
case "GET": return this.handleGetRequest(request);
case "POST": return this.handlePostRequest(request, parsedBody);
case "DELETE": return this.handleDeleteRequest(request);
default: return this.handleUnsupportedRequest();
}
}
async handleGetRequest(request) {
if (!request.headers.get("Accept")?.includes("text/event-stream")) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Not Acceptable: Client must accept text/event-stream"
},
id: null
}), {
status: 406,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
const sessionError = this.validateSession(request);
if (sessionError) return sessionError;
const versionError = this.validateProtocolVersion(request);
if (versionError) return versionError;
let streamId = this.standaloneSseStreamId;
const lastEventId = request.headers.get("Last-Event-ID");
if (lastEventId && this.eventStore) {
const eventStreamId = await this.eventStore.getStreamIdForEventId?.(lastEventId);
if (eventStreamId) streamId = eventStreamId;
}
if (this.streamMapping.get(streamId) !== void 0) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Conflict: Only one SSE stream is allowed per session"
},
id: null
}), {
status: 409,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const headers = new Headers({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
...this.getHeaders()
});
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
const keepAlive = setInterval(() => {
try {
writer.write(encoder.encode("event: ping\ndata: \n\n"));
} catch {
clearInterval(keepAlive);
}
}, 3e4);
this.streamMapping.set(streamId, {
writer,
encoder,
cleanup: () => {
clearInterval(keepAlive);
this.streamMapping.delete(streamId);
writer.close().catch(() => {});
}
});
if (this.retryInterval !== void 0) await writer.write(encoder.encode(`retry: ${this.retryInterval}\n\n`));
if (lastEventId && this.eventStore) {
const replayedStreamId = await this.eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
const data = `id: ${eventId}\nevent: message\ndata: ${JSON.stringify(message)}\n\n`;
await writer.write(encoder.encode(data));
} });
if (replayedStreamId !== streamId) {
this.streamMapping.delete(streamId);
streamId = replayedStreamId;
this.streamMapping.set(streamId, {
writer,
encoder,
cleanup: () => {
clearInterval(keepAlive);
this.streamMapping.delete(streamId);
writer.close().catch(() => {});
}
});
}
}
return new Response(readable, { headers });
}
async handlePostRequest(request, parsedBody) {
const acceptHeader = request.headers.get("Accept");
if (!acceptHeader?.includes("application/json") || !acceptHeader?.includes("text/event-stream")) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Not Acceptable: Client must accept both application/json and text/event-stream"
},
id: null
}), {
status: 406,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
if (!request.headers.get("Content-Type")?.includes("application/json")) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Unsupported Media Type: Content-Type must be application/json"
},
id: null
}), {
status: 415,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
let rawMessage = parsedBody;
if (rawMessage === void 0) try {
rawMessage = await request.json();
} catch {
return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32700,
message: "Parse error: Invalid JSON"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
}
let messages;
try {
if (Array.isArray(rawMessage)) messages = rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg));
else messages = [JSONRPCMessageSchema.parse(rawMessage)];
} catch {
return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32700,
message: "Parse error: Invalid JSON-RPC message"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
}
const requestInfo = { headers: Object.fromEntries(request.headers.entries()) };
const isInitializationRequest = messages.some(isInitializeRequest);
if (isInitializationRequest) {
if (this.initialized && this.sessionId !== void 0) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request: Server already initialized"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
if (messages.length > 1) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request: Only one initialization request is allowed"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
this.sessionId = this.sessionIdGenerator?.();
this.initialized = true;
const initMessage = messages.find(isInitializeRequest);
if (initMessage && isInitializeRequest(initMessage)) this.initializeParams = {
capabilities: initMessage.params.capabilities,
clientInfo: initMessage.params.clientInfo,
protocolVersion: initMessage.params.protocolVersion
};
await this.saveState();
if (this.sessionId && this.onsessioninitialized) this.onsessioninitialized(this.sessionId);
}
if (!isInitializationRequest) {
const sessionError = this.validateSession(request);
if (sessionError) return sessionError;
const versionError = this.validateProtocolVersion(request);
if (versionError) return versionError;
}
if (!messages.some(isJSONRPCRequest)) {
for (const message of messages) this.onmessage?.(message, { requestInfo });
return new Response(null, {
status: 202,
headers: { ...this.getHeaders() }
});
}
const streamId = crypto.randomUUID();
if (this.enableJsonResponse) return new Promise((resolve) => {
this.streamMapping.set(streamId, {
resolveJson: resolve,
cleanup: () => {
this.streamMapping.delete(streamId);
}
});
for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
for (const message of messages) this.onmessage?.(message, { requestInfo });
});
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
const headers = new Headers({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
...this.getHeaders()
});
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
this.streamMapping.set(streamId, {
writer,
encoder,
cleanup: () => {
this.streamMapping.delete(streamId);
writer.close().catch(() => {});
}
});
for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
for (const message of messages) this.onmessage?.(message, { requestInfo });
return new Response(readable, { headers });
}
async handleDeleteRequest(request) {
const sessionError = this.validateSession(request);
if (sessionError) return sessionError;
const versionError = this.validateProtocolVersion(request);
if (versionError) return versionError;
const closedSessionId = this.sessionId;
await this.close();
if (closedSessionId && this.onsessionclosed) this.onsessionclosed(closedSessionId);
return new Response(null, {
status: 200,
headers: { ...this.getHeaders() }
});
}
handleOptionsRequest(_request) {
return new Response(null, {
status: 200,
headers: { ...this.getHeaders({ forPreflight: true }) }
});
}
handleUnsupportedRequest() {
return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Method not allowed."
},
id: null
}), {
status: 405,
headers: {
Allow: "GET, POST, DELETE, OPTIONS",
"Content-Type": "application/json"
}
});
}
validateSession(request) {
if (this.sessionIdGenerator === void 0) return;
if (!this.initialized) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Bad Request: Server not initialized"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
const sessionId = request.headers.get("mcp-session-id");
if (!sessionId) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Bad Request: Mcp-Session-Id header is required"
},
id: null
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
if (sessionId !== this.sessionId) return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Session not found"
},
id: null
}), {
status: 404,
headers: {
"Content-Type": "application/json",
...this.getHeaders()
}
});
}
async close() {
for (const { cleanup } of this.streamMapping.values()) cleanup();
this.streamMapping.clear();
this.requestResponseMap.clear();
this.onclose?.();
}
/**
* Close an SSE stream for a specific request, triggering client reconnection.
* Use this to implement polling behavior during long-running operations -
* client will reconnect after the retry interval specified in the priming event.
*/
closeSSEStream(requestId) {
const streamId = this.requestToStreamMapping.get(requestId);
if (!streamId) return;
const stream = this.streamMapping.get(streamId);
if (stream) stream.cleanup();
for (const [reqId, sid] of this.requestToStreamMapping.entries()) if (sid === streamId) {
this.requestToStreamMapping.delete(reqId);
this.requestResponseMap.delete(reqId);
}
}
async send(message, options) {
let requestId = options?.relatedRequestId;
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
if (requestId === RESTORE_REQUEST_ID) return;
if (requestId === void 0) {
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
const standaloneSse = this.streamMapping.get(this.standaloneSseStreamId);
if (standaloneSse === void 0) return;
if (standaloneSse.writer && standaloneSse.encoder) {
let eventId;
if (this.eventStore) eventId = await this.eventStore.storeEvent(this.standaloneSseStreamId, message);
const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
await standaloneSse.writer.write(standaloneSse.encoder.encode(data));
}
return;
}
const streamId = this.requestToStreamMapping.get(requestId);
if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
const response = this.streamMapping.get(streamId);
if (!response) throw new Error(`No connection established for request ID: ${String(requestId)}`);
if (!this.enableJsonResponse) {
if (response.writer && response.encoder) {
let eventId;
if (this.eventStore) eventId = await this.eventStore.storeEvent(streamId, message);
const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
await response.writer.write(response.encoder.encode(data));
}
}
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
this.requestResponseMap.set(requestId, message);
const relatedIds = Array.from(this.requestToStreamMapping.entries()).filter(([, sid]) => sid === streamId).map(([id]) => id);
if (relatedIds.every((id) => this.requestResponseMap.has(id))) {
if (this.enableJsonResponse && response.resolveJson) {
const responses = relatedIds.map((id) => this.requestResponseMap.get(id));
const headers = new Headers({
"Content-Type": "application/json",
...this.getHeaders()
});
if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
const body = responses.length === 1 ? responses[0] : responses;
response.resolveJson(new Response(JSON.stringify(body), { headers }));
} else response.cleanup();
for (const id of relatedIds) {
this.requestResponseMap.delete(id);
this.requestToStreamMapping.delete(id);
}
}
}
}
};
//#endregion
//#region src/mcp/auth-context.ts
const authContextStorage = new AsyncLocalStorage();
function getMcpAuthContext() {
return authContextStorage.getStore();
}
function runWithAuthContext(context, fn) {
return authContextStorage.run(context, fn);
}
//#endregion
//#region src/mcp/handler.ts
function createMcpHandler(server, options = {}) {
const route = options.route ?? "/mcp";
return async (request, _env, ctx) => {
const url = new URL(request.url);
if (route && url.pathname !== route) return new Response("Not Found", { status: 404 });
const transport = options.transport ?? new WorkerTransport({
sessionIdGenerator: options.sessionIdGenerator,
enableJsonResponse: options.enableJsonResponse,
onsessioninitialized: options.onsessioninitialized,
corsOptions: options.corsOptions,
storage: options.storage
});
const buildAuthContext = () => {
if (options.authContext) return options.authContext;
if (ctx.props && Object.keys(ctx.props).length > 0) return { props: ctx.props };
};
const handleRequest = async () => {
return await transport.handleRequest(request);
};
const authContext = buildAuthContext();
if (!transport.started) await server.connect(transport);
try {
if (authContext) return await runWithAuthContext(authContext, handleRequest);
else return await handleRequest();
} catch (error) {
console.error("MCP handler error:", error);
return new Response(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32603,
message: error instanceof Error ? error.message : "Internal server error"
},
id: null
}), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
};
}
let didWarnAboutExperimentalCreateMcpHandler = false;
/**
* @deprecated This has been renamed to createMcpHandler, and experimental_createMcpHandler will be removed in the next major version
*/
function experimental_createMcpHandler(server, options = {}) {
if (!didWarnAboutExperimentalCreateMcpHandler) {
didWarnAboutExperimentalCreateMcpHandler = true;
console.warn("experimental_createMcpHandler is deprecated, use createMcpHandler instead. experimental_createMcpHandler will be removed in the next major version.");
}
return createMcpHandler(server, options);
}
//#endregion
//#region src/mcp/index.ts
var McpAgent = class McpAgent extends Agent {
async setInitializeRequest(initializeRequest) {
await this.ctx.storage.put("initializeRequest", initializeRequest);
}
async getInitializeRequest() {
return this.ctx.storage.get("initializeRequest");
}
/** Read the transport type for this agent.
* This relies on the naming scheme being `sse:${sessionId}`
* or `streamable-http:${sessionId}`.
*/
getTransportType() {
const [t, ..._] = this.name.split(":");
switch (t) {
case "sse": return "sse";
case "streamable-http": return "streamable-http";
default: throw new Error("Invalid transport type. McpAgent must be addressed with a valid protocol.");
}
}
/** Read the sessionId for this agent.
* This relies on the naming scheme being `sse:${sessionId}`
* or `streamable-http:${sessionId}`.
*/
getSessionId() {
const [_, sessionId] = this.name.split(":");
if (!sessionId) throw new Error("Invalid session id. McpAgent must be addressed with a valid session id.");
return sessionId;
}
/** Get the unique WebSocket. SSE transport only. */
getWebSocket() {
const websockets = Array.from(this.getConnections());
if (websockets.length === 0) return null;
return websockets[0];
}
/** Returns a new transport matching the type of the Agent. */
initTransport() {
switch (this.getTransportType()) {
case "sse": return new McpSSETransport();
case "streamable-http": {
const transport = new StreamableHTTPServerTransport({});
transport.messageInterceptor = async (message) => {
return this._handleElicitationResponse(message);
};
return transport;
}
}
}
/** Update and store the props */
async updateProps(props) {
await this.ctx.storage.put("props", props ?? {});
this.props = props;
}
async reinitializeServer() {
const initializeRequest = await this.getInitializeRequest();
if (initializeRequest) this._transport?.onmessage?.(initializeRequest);
}
/** Sets up the MCP transport and server every time the Agent is started.*/
async onStart(props) {
if (props) await this.updateProps(props);
this.props = await this.ctx.storage.get("props");
await this.init();
const server = await this.server;
this._transport = this.initTransport();
await server.connect(this._transport);
await this.reinitializeServer();
}
/** Validates new WebSocket connections. */
async onConnect(conn, { request: req }) {
switch (this.getTransportType()) {
case "sse":
if (Array.from(this.getConnections()).length > 1) {
conn.close(1008, "Websocket already connected");
return;
}
break;
case "streamable-http": if (this._transport instanceof StreamableHTTPServerTransport) switch (req.headers.get(MCP_HTTP_METHOD_HEADER)) {
case "POST": {
const payloadHeader = req.headers.get(MCP_MESSAGE_HEADER);
let rawPayload;
if (!payloadHeader) rawPayload = "{}";
else try {
rawPayload = Buffer.from(payloadHeader, "base64").toString("utf-8");
} catch (_error) {
throw new Error("Internal Server Error: Failed to decode MCP message header");
}
const parsedBody = JSON.parse(rawPayload);
this._transport?.handlePostRequest(req, parsedBody);
break;
}
case "GET":
this._transport?.handleGetRequest(req);
break;
}
}
}
/** Handles MCP Messages for the legacy SSE transport. */
async onSSEMcpMessage(_sessionId, messageBody, extraInfo) {
if (this.getTransportType() !== "sse") return /* @__PURE__ */ new Error("Internal Server Error: Expected SSE transport");
try {
let parsedMessage;
try {
parsedMessage = JSONRPCMessageSchema.parse(messageBody);
} catch (error) {
this._transport?.onerror?.(error);
throw error;
}
if (await this._handleElicitationResponse(parsedMessage)) return null;
this._transport?.onmessage?.(parsedMessage, extraInfo);
return null;
} catch (error) {
console.error("Error forwarding message to SSE:", error);
this._transport?.onerror?.(error);
return error;
}
}
/** Elicit user input with a message and schema */
async elicitInput(params) {
const requestId = `elicit_${Math.random().toString(36).substring(2, 11)}`;
await this.ctx.storage.put(`elicitation:${requestId}`, {
message: params.message,
requestedSchema: params.requestedSchema,
timestamp: Date.now()
});
const elicitRequest = {
jsonrpc: "2.0",
id: requestId,
method: "elicitation/create",
params: {
message: params.message,
requestedSchema: params.requestedSchema
}
};
if (this._transport) await this._transport.send(elicitRequest);
else {
const connections = this.getConnections();
if (!connections || Array.from(connections).length === 0) {
await this.ctx.storage.delete(`elicitation:${requestId}`);
throw new Error("No active connections available for elicitation");
}
const connectionList = Array.from(connections);
for (const connection of connectionList) try {
connection.send(JSON.stringify(elicitRequest));
} catch (error) {
console.error("Failed to send elicitation request:", error);
}
}
return this._waitForElicitationResponse(requestId);
}
/** Wait for elicitation response through storage polling */
async _waitForElicitationResponse(requestId) {
const startTime = Date.now();
const timeout = 6e4;
try {
while (Date.now() - startTime < timeout) {
const response = await this.ctx.storage.get(`elicitation:response:${requestId}`);
if (response) {
await this.ctx.storage.delete(`elicitation:${requestId}`);
await this.ctx.storage.delete(`elicitation:response:${requestId}`);
return response;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error("Elicitation request timed out");
} finally {
await this.ctx.storage.delete(`elicitation:${requestId}`);
await this.ctx.storage.delete(`elicitation:response:${requestId}`);
}
}
/** Handle elicitation responses */
async _handleElicitationResponse(message) {
if (isJSONRPCResultResponse(message) && message.result) {
const requestId = message.id?.toString();
if (!requestId || !requestId.startsWith("elicit_")) return false;
if (!await this.ctx.storage.get(`elicitation:${requestId}`)) return false;
await this.ctx.storage.put(`elicitation:response:${requestId}`, message.result);
return true;
}
if (isJSONRPCErrorResponse(message)) {
const requestId = message.id?.toString();
if (!requestId || !requestId.startsWith("elicit_")) return false;
if (!await this.ctx.storage.get(`elicitation:${requestId}`)) return false;
const errorResult = {
action: "cancel",
content: { error: message.error.message || "Elicitation request failed" }
};
await this.ctx.storage.put(`elicitation:response:${requestId}`, errorResult);
return true;
}
return false;
}
/** Return a handler for the given path for this MCP.
* Defaults to Streamable HTTP transport.
*/
static serve(path, { binding = "MCP_OBJECT", corsOptions, transport = "streamable-http", jurisdiction } = {}) {
return { async fetch(request, env, ctx) {
const corsResponse = handleCORS(request, corsOptions);
if (corsResponse) return corsResponse;
const bindingValue = env[binding];
if (bindingValue == null || typeof bindingValue !== "object") throw new Error(`Could not find McpAgent binding for ${binding}. Did you update your wrangler configuration?`);
if (!isDurableObjectNamespace(bindingValue)) throw new Error(`Invalid McpAgent binding for ${binding}. Make sure it's a Durable Object binding.`);
const namespace = bindingValue;
switch (transport) {
case "streamable-http": return createStreamingHttpHandler(path, namespace, {
corsOptions,
jurisdiction
})(request, ctx);
case "sse": return createLegacySseHandler(path, namespace, {
corsOptions,
jurisdiction
})(request, ctx);
default: return new Response("Invalid MCP transport mode. Only `streamable-http` or `sse` are allowed.", { status: 500 });
}
} };
}
/**
* Legacy api
**/
static mount(path, opts = {}) {
return McpAgent.serveSSE(path, opts);
}
static serveSSE(path, opts = {}) {
return McpAgent.serve(path, {
...opts,
transport: "sse"
});
}
};
//#endregion
export { ElicitRequestSchema, McpAgent, SSEEdgeClientTransport, StreamableHTTPEdgeClientTransport, WorkerTransport, createMcpHandler, experimental_createMcpHandler, getMcpAuthContext };
//# sourceMappingURL=index.js.map