UNPKG

agents

Version:

A home for your AI agents

318 lines (317 loc) 13 kB
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