agents
Version:
A home for your AI agents
318 lines (317 loc) • 13 kB
JavaScript
import { AsyncLocalStorage } from "node:async_hooks";
import { WebStandardStreamableHTTPServerTransport, createMcpHandler, hostHeaderValidationResponse, isJSONRPCRequest, isLegacyRequest, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse } from "@modelcontextprotocol/server";
//#region src/mcp/auth-context.ts
const VERIFIED_OAUTH_CONTEXT = Symbol.for("cloudflare.workers-oauth-provider.verified-context.v1");
const authContextStorage = new AsyncLocalStorage();
function getMcpAuthContext() {
return authContextStorage.getStore();
}
function runWithAuthContext(context, fn) {
return authContextStorage.run(context, fn);
}
function isPlainRecord(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function invalidVerifiedContext() {
throw new TypeError("Invalid verified OAuth request context");
}
function getVerifiedOAuthAuthInfo(ctx) {
const symbolContext = ctx;
if (!(VERIFIED_OAUTH_CONTEXT in symbolContext)) return void 0;
const value = symbolContext[VERIFIED_OAUTH_CONTEXT];
if (!isPlainRecord(value) || value.version !== 1) invalidVerifiedContext();
const { token, clientId, scopes, expiresAt, resource, props } = value;
if (typeof token !== "string" || token.length === 0 || typeof clientId !== "string" || clientId.length === 0 || !Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string") || !isPlainRecord(props)) invalidVerifiedContext();
if (expiresAt !== void 0 && (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0)) invalidVerifiedContext();
let resourceUrl;
if (resource !== void 0) {
if (typeof resource !== "string") invalidVerifiedContext();
try {
resourceUrl = new URL(resource);
} catch {
invalidVerifiedContext();
}
if (resourceUrl.protocol !== "http:" && resourceUrl.protocol !== "https:") invalidVerifiedContext();
}
if (ctx.props !== props) invalidVerifiedContext();
return {
props,
authInfo: {
token,
clientId,
scopes: [...scopes],
...expiresAt !== void 0 && { expiresAt },
...resourceUrl !== void 0 && { resource: resourceUrl },
extra: { props }
}
};
}
//#endregion
//#region src/mcp/handler-errors.ts
function internalErrorResponse(id = null) {
return Response.json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error"
},
id
}, { status: 500 });
}
function requestIdFromParsedBody(body) {
if (typeof body !== "object" || body === null || Array.isArray(body) || !("method" in body) || typeof body.method !== "string" || !("id" in body)) return null;
return typeof body.id === "string" || typeof body.id === "number" ? body.id : null;
}
function reportHandlerError(onerror, error) {
try {
onerror?.(error instanceof Error ? error : new Error(String(error)));
} catch {}
}
//#endregion
//#region src/mcp/handler-legacy-compat.ts
/**
* Temporary adapter for Legacy compatibility on the SDK v2 transport.
*
* Local deltas from the upstream stateless fallback:
*
* - impossible stateless server-to-client requests fail immediately rather
* than leaving the tool handler waiting for a session response.
*
* Streaming and keepalive behavior remain delegated to the SDK transport.
* Remove this adapter once the SDK exposes the reverse-request policy directly.
*/
function createLegacyCompatibilityRequestHandler(factory, handlerOptions = {}) {
const { keepAliveMs, onerror } = handlerOptions;
const fetch = async (request, requestOptions) => {
if (request.method.toUpperCase() !== "POST") return Response.json({
jsonrpc: "2.0",
error: {
code: -32e3,
message: "Method not allowed."
},
id: null
}, { status: 405 });
if (request.signal.aborted) return new Response(null, { status: 499 });
let product;
let transport;
let teardownPromise;
const teardown = () => teardownPromise ??= (async () => {
await Promise.all([transport?.close().catch(() => {}), product?.close().catch(() => {})]);
})();
const onAbort = () => void teardown();
try {
product = await factory({
era: "legacy",
...requestOptions?.authInfo !== void 0 && { authInfo: requestOptions.authInfo },
requestInfo: request
});
transport = new WebStandardStreamableHTTPServerTransport({
keepAliveMs,
sessionIdGenerator: void 0
});
const send = transport.send.bind(transport);
transport.send = async (message, sendOptions) => {
if (isJSONRPCRequest(message)) {
transport?.onmessage?.({
jsonrpc: "2.0",
id: message.id,
error: {
code: -32603,
message: "Server-to-client requests are unavailable in the Legacy compatibility lane. Use inputRequired(...) for Stateless clients, or route Legacy traffic to a sessionful transport."
}
});
return;
}
await send(message, sendOptions);
};
await product.connect(transport);
if (request.signal.aborted) {
await teardown();
return new Response(null, { status: 499 });
}
request.signal.addEventListener("abort", onAbort, { once: true });
const response = await transport.handleRequest(request, {
...requestOptions?.authInfo !== void 0 && { authInfo: requestOptions.authInfo },
...requestOptions?.parsedBody !== void 0 && { parsedBody: requestOptions.parsedBody }
});
if (response.body === null || !response.headers.get("content-type")?.includes("text/event-stream")) {
request.signal.removeEventListener("abort", onAbort);
await teardown();
return response;
}
const reader = response.body.getReader();
const body = new ReadableStream({
async pull(controller) {
try {
const { done, value } = await reader.read();
if (done) {
request.signal.removeEventListener("abort", onAbort);
await teardown();
controller.close();
} else if (value !== void 0) controller.enqueue(value);
} catch (error) {
request.signal.removeEventListener("abort", onAbort);
await teardown();
controller.error(error);
}
},
async cancel(reason) {
request.signal.removeEventListener("abort", onAbort);
await reader.cancel(reason).catch(() => {});
await teardown();
}
});
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
} catch (error) {
request.signal.removeEventListener("abort", onAbort);
await teardown();
reportHandlerError(onerror, error);
return internalErrorResponse(requestIdFromParsedBody(requestOptions?.parsedBody));
}
};
return { fetch };
}
//#endregion
//#region src/mcp/handler-stateless.ts
const DEFAULT_CORS_OPTIONS = {
origin: "*",
headers: "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version, Mcp-Method, Mcp-Name",
methods: "GET, POST, DELETE, OPTIONS",
exposeHeaders: "mcp-session-id",
maxAge: 86400
};
function corsHeaders(options = {}) {
const merged = {
...DEFAULT_CORS_OPTIONS,
...options
};
return new Headers({
"Access-Control-Allow-Headers": merged.headers,
"Access-Control-Allow-Methods": merged.methods,
"Access-Control-Allow-Origin": merged.origin,
"Access-Control-Expose-Headers": merged.exposeHeaders,
"Access-Control-Max-Age": String(merged.maxAge)
});
}
function withCors(response, options) {
const headers = new Headers(response.headers);
if (options === false) {
for (const name of Array.from(headers.keys())) if (name.toLowerCase().startsWith("access-control-")) headers.delete(name);
} else for (const [name, value] of corsHeaders(options)) headers.set(name, value);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
function wrapResponseBodyWithAuthContext(response, authContext) {
if (!authContext || !response.body) return response;
const reader = response.body.getReader();
const body = new ReadableStream({
pull(controller) {
return runWithAuthContext(authContext, async () => {
try {
const { done, value } = await reader.read();
if (done) controller.close();
else controller.enqueue(value);
} catch (error) {
controller.error(error);
}
});
},
cancel(reason) {
return runWithAuthContext(authContext, () => reader.cancel(reason));
}
});
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
function createStatelessMcpHandler(factory, options = {}) {
const optionRecord = options;
if (optionRecord.bus !== void 0) throw new TypeError("createMcpHandler option \"bus\" is not exposed by the Agents SDK.");
const legacyOnlyOption = [
"transport",
"storage",
"sessionIdGenerator",
"onsessioninitialized",
"onsessionclosed",
"enableJsonResponse",
"eventStore",
"allowedHosts",
"allowedOrigins",
"enableDnsRebindingProtection",
"retryInterval"
].find((key) => optionRecord[key] !== void 0);
if (legacyOnlyOption) throw new TypeError(`createMcpHandler option "${legacyOnlyOption}" is only supported with an MCP SDK v1 server. The managed SDK v2 handler is stateless; remove this option or keep the v1 server during migration.`);
const { route = "/mcp", corsOptions = {}, allowedHostnames, allowedOriginHostnames, authContext, legacy = "stateless", ...sdkOptions } = options;
const sdkHandler = createMcpHandler(factory, {
...sdkOptions,
legacy: "reject"
});
const legacyCompatibilityHandler = legacy === "stateless" ? createLegacyCompatibilityRequestHandler(factory, {
keepAliveMs: sdkOptions.keepAliveMs,
onerror: sdkOptions.onerror
}) : void 0;
const serve = async (request, requestOptions, workerCtx) => {
const requestUrl = new URL(request.url);
if (requestUrl.pathname !== route) return withCors(new Response("Not Found", { status: 404 }), corsOptions);
const localEndpoint = localhostAllowedHostnames().includes(requestUrl.hostname);
const workersDevEndpoint = requestUrl.hostname.endsWith(".workers.dev");
const acceptedHostnames = allowedHostnames ?? (localEndpoint ? localhostAllowedHostnames() : workersDevEndpoint ? [requestUrl.hostname] : void 0);
const hostRejection = acceptedHostnames ? hostHeaderValidationResponse(request, acceptedHostnames) : void 0;
if (hostRejection) return withCors(hostRejection, corsOptions);
if (allowedOriginHostnames !== "*") {
let acceptedOriginHostnames = allowedOriginHostnames;
if (acceptedOriginHostnames === void 0) {
const defaults = new Set(localhostAllowedOrigins());
if (workersDevEndpoint) defaults.add(requestUrl.hostname);
if (corsOptions !== false && corsOptions.origin !== void 0) try {
const configuredOrigin = new URL(corsOptions.origin);
if ((configuredOrigin.protocol === "http:" || configuredOrigin.protocol === "https:") && configuredOrigin.hostname) defaults.add(configuredOrigin.hostname);
} catch {}
acceptedOriginHostnames = [...defaults];
}
const originRejection = originValidationResponse(request, acceptedOriginHostnames ?? []);
if (originRejection) return withCors(originRejection, corsOptions);
}
if (request.method === "OPTIONS" && corsOptions !== false) return new Response(null, { headers: corsHeaders(corsOptions) });
const legacyRequest = legacyCompatibilityHandler !== void 0 && await isLegacyRequest(request, requestOptions?.parsedBody);
try {
const verified = workerCtx ? getVerifiedOAuthAuthInfo(workerCtx) : void 0;
const explicitAuthInfo = requestOptions?.authInfo;
if (verified && explicitAuthInfo && explicitAuthInfo.clientId !== verified.authInfo.clientId) throw new TypeError("Conflicting verified OAuth client identity");
const authInfo = explicitAuthInfo ?? verified?.authInfo;
const resolvedAuthContext = authContext ?? (verified ? { props: verified.props } : workerCtx?.props && Object.keys(workerCtx.props).length > 0 ? { props: workerCtx.props } : void 0);
const upstreamOptions = requestOptions || authInfo ? {
...requestOptions,
...authInfo && { authInfo }
} : void 0;
const invoke = async () => {
if (legacyRequest && legacyCompatibilityHandler) return legacyCompatibilityHandler.fetch(request, upstreamOptions);
return sdkHandler.fetch(request, upstreamOptions);
};
return withCors(wrapResponseBodyWithAuthContext(resolvedAuthContext ? await runWithAuthContext(resolvedAuthContext, invoke) : await invoke(), resolvedAuthContext), corsOptions);
} catch (error) {
reportHandlerError(sdkOptions.onerror, error);
return withCors(internalErrorResponse(), corsOptions);
}
};
const callable = (request, _env, ctx) => serve(request, void 0, ctx);
const fetch = (request, requestOptions) => serve(request, requestOptions);
return Object.assign(callable, {
fetch,
notify: sdkHandler.notify
});
}
//#endregion
export { getMcpAuthContext as n, runWithAuthContext as r, createStatelessMcpHandler as t };
//# sourceMappingURL=handler-stateless-CIkKPETH.js.map