mcp-handler
Version:
Framework-agnostic HTTP adapter for Model Context Protocol servers
367 lines (355 loc) • 17.6 kB
text/typescript
import { ServerOptions, CreateMcpHandlerOptions, McpServer, AuthInfo, StandardSchemaWithJSON, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server';
type McpEventType = "REQUEST_RECEIVED" | "REQUEST_COMPLETED" | "ERROR";
interface McpEventBase {
type: McpEventType;
timestamp: number;
}
interface McpRequestEvent extends McpEventBase {
type: "REQUEST_RECEIVED" | "REQUEST_COMPLETED";
method: string;
parameters?: unknown;
result?: unknown;
duration?: number;
status: "success" | "error";
}
interface McpErrorEvent extends McpEventBase {
type: "ERROR";
error: Error | string;
context?: string;
source: "request" | "system";
severity: "warning" | "error" | "fatal";
}
type McpEvent = McpRequestEvent | McpErrorEvent;
/**
* Options for the WebMCP bridge script endpoint.
*/
type WebMcpScriptHandlerOptions = {
/**
* URL of the MCP endpoint the script talks to. May be a path relative to
* the page origin ("/api/mcp") or an absolute URL.
*/
endpoint: string;
/**
* Explicit allowlist of tool names exposed to in-page agents. Tools not
* listed here are never registered with the browser, even though they
* remain reachable through the MCP endpoint itself.
*/
tools: string[];
/**
* Credentials mode for the tool-call fetches issued from the page.
* @default "same-origin"
*/
credentials?: "same-origin" | "include" | "omit";
/**
* Value served in the script response's Cache-Control header.
* @default "public, max-age=300"
*/
cacheControl?: string;
};
type WebMcpOptions = Omit<WebMcpScriptHandlerOptions, "endpoint">;
/**
* Options for the MCP handler: the SDK's `ServerOptions` (capabilities,
* instructions, ...) plus handler-level extras.
*/
type McpHandlerOptions = ServerOptions & {
/**
* Maximum number of concurrent `subscriptions/listen` streams.
* Set to `0` to reject subscriptions without opening an SSE stream.
* @default 1024
*/
maxSubscriptions?: CreateMcpHandlerOptions["maxSubscriptions"];
/**
* Name and version reported to clients during initialization.
*/
serverInfo?: {
name: string;
version: string;
};
/**
* If true, enables console logging.
* @default false
*/
verboseLogs?: boolean;
/**
* Callback function that receives MCP events.
* This can be used to track analytics, debug issues, or implement custom behaviors.
*/
onEvent?: (event: McpEvent) => void;
/**
* Publishes an allowlisted subset of this server's tools to in-page agents
* through WebMCP. Load the generated bridge from the MCP route with the
* `?webmcp-script` query parameter.
*
* @experimental WebMCP is an early-stage browser API.
*/
experimental_webMcp?: WebMcpOptions;
};
/**
* Creates a MCP handler that can be used to handle MCP requests.
*
* The returned handler is a web-standard `(request: Request) => Promise<Response>`
* function. It serves every request it receives — routing belongs to the host
* framework, so mount it at the route you want (a Next.js route handler,
* Hono, Nitro, ...).
*
* @param initializeServer - A function that initializes the MCP server. Use this to access the server instance and register tools, prompts, and resources.
* @param options - The SDK's server options plus handler extras (`serverInfo`, `verboseLogs`, `onEvent`, `maxSubscriptions`, `experimental_webMcp`).
* @returns A function that can be used to handle MCP requests.
*/
declare function createMcpRouteHandler(initializeServer: ((server: McpServer) => Promise<void>) | ((server: McpServer) => void), options?: McpHandlerOptions): (request: Request) => Promise<Response>;
/** @experimental Authorization shared by subscription requests and workers. */
interface ExperimentalMcpEventAuthorizationContext {
/** Stable, application-defined identity, including its tenant/issuer scope. */
principal: string;
signal: AbortSignal;
}
/** @experimental Request-only context; never persist its access token. */
interface ExperimentalMcpEventContext extends ExperimentalMcpEventAuthorizationContext {
authInfo: AuthInfo;
}
/** @experimental A webhook event exposed through events/list. */
interface ExperimentalMcpEventDefinition {
name: string;
description?: string;
/** A non-transforming schema: arguments are part of subscription identity. */
inputSchema: StandardSchemaWithJSON;
payloadSchema: StandardSchemaWithJSON;
/**
* Optional request-time visibility policy for listing and subscription lookup.
* No event arguments exist at discovery time. Omit to use the catalog
* resolver's visibility; this does not replace per-argument authorization.
*/
authorizeDiscovery?(context: ExperimentalMcpEventContext): boolean | Promise<boolean>;
/**
* Authorize these filters against current permissions and application-held
* grants. Used at subscribe time and by delivery validation, without a token.
*/
authorize(args: Record<string, unknown>, context: ExperimentalMcpEventAuthorizationContext): boolean | Promise<boolean>;
}
/** @experimental Identity passed to storage; never supplied by the client. */
interface ExperimentalMcpSubscriptionKey {
id: string;
principal: string;
name: string;
arguments: Record<string, unknown>;
url: string;
}
/** @experimental A verified subscription to persist and activate atomically. */
interface ExperimentalMcpWebhookSubscription extends ExperimentalMcpSubscriptionKey {
secret: string;
/** The granted expiry. Null requires durable state across restarts. */
refreshBefore: string | null;
/** Replay request, not a persisted safe delivery watermark. */
cursor: string | null;
maxAgeMs?: number;
}
/** @experimental Inputs for worker-side validation; no signing secret needed. */
interface ExperimentalMcpEventDeliveryValidationOptions {
event: ExperimentalMcpEventDefinition;
/** Load current, active state from trusted storage before each attempt. */
subscription: Pick<ExperimentalMcpWebhookSubscription, "principal" | "name" | "arguments" | "refreshBefore"> | null | undefined;
/** The event's data, not the webhook envelope. Must already match its schema. */
payload: unknown;
signal?: AbortSignal;
}
/** @experimental Known validation failures, distinct from backend exceptions. */
type ExperimentalMcpEventDeliveryErrorCode = "invalid_subscription" | "event_mismatch" | "expired" | "invalid_arguments" | "invalid_payload" | "forbidden";
/** @experimental Safe webhook diagnostics, never raw endpoint responses. */
type ExperimentalMcpWebhookFailure = "connection_refused" | "timeout" | "tls_error" | "http_4xx" | "http_5xx" | "challenge_failed";
/** @experimental State returned by the application's durable delivery system. */
interface ExperimentalMcpSubscriptionState {
/** Safe watermark, never ahead of unacknowledged deliveries; null for no replay. */
cursor: string | null;
truncated: boolean;
deliveryStatus?: {
active: boolean;
lastDeliveryAt?: string;
lastError: ExperimentalMcpWebhookFailure | null;
failedSince?: string;
throttled?: boolean;
retryAfterMs?: number;
};
}
/**
* @experimental Application-owned storage and lifecycle adapter.
*
* Methods must be atomic/idempotent per key across concurrent HTTP requests.
* Use an outbox or equivalent durable reconciliation for delivery activation and
* cancellation. A request-local Map or fire-and-forget task is not sufficient.
*/
interface ExperimentalMcpSubscriptionStore {
/**
* Reserve a bounded, durable operation before callback verification. Return a
* unique, nonempty fencing token for this attempt. Supersede older attempts
* for this key without changing any active grant, secret or delivery state.
* Enforce admission limits here, before network work. Reservations must expire
* and be reclaimed after failed verification, aborted requests or crashes.
*/
prepare(key: ExperimentalMcpSubscriptionKey, context: ExperimentalMcpEventContext): Promise<string>;
/**
* Persist the verified grant and arrange delivery before resolving. Refresh
* updates the secret/expiry, resumes suspended work, and applies cursor replay
* without rewinding a live subscription. Enforce quotas here. Protect secrets
* at rest and retain state for the entire granted lifetime. Atomically require
* operationId to match the current, unexpired reservation before any mutation;
* reject stale attempts, including ones invalidated by remove(). A repeated
* commit for the same operation must converge without applying stale state.
*/
upsert(subscription: ExperimentalMcpWebhookSubscription, context: ExperimentalMcpEventContext, operationId: string): Promise<ExperimentalMcpSubscriptionState>;
/**
* Atomically invalidate pending operations and stop delivery. Return true for
* an existing grant or pending reservation; false only when neither exists.
* Retain enough fencing state that delayed upserts cannot revive this key.
*/
remove(key: ExperimentalMcpSubscriptionKey, context: ExperimentalMcpEventContext): Promise<boolean>;
}
/**
* @experimental Adapter to the application's webhook delivery system.
*
* This package makes no outbound HTTP requests. The adapter must enforce its
* egress policy on every request (including DNS resolution), disable redirects,
* and bound timeouts/response sizes. Cache consent per (principal, url), rate
* limit failed verifications, and honor the abort signal.
*/
interface ExperimentalMcpWebhookDelivery {
/**
* Prove endpoint consent using a signed verification challenge or another
* mechanism allowed by the draft before any subscription is activated. Use a
* vetted Standard Webhooks implementation for signing; true must never mean
* merely that the URL was reachable. The delivery worker behind the store must
* sign events/controls, enforce expiry/revocation, and retry durably.
*/
verifyEndpoint(target: ExperimentalMcpSubscriptionKey & {
secret: string;
}, context: ExperimentalMcpEventContext): Promise<{
verified: true;
} | {
verified: false;
reason: ExperimentalMcpWebhookFailure;
}>;
}
/** @experimental Options for experimental_registerMcpEvents. */
interface ExperimentalMcpEventsOptions {
/** Resolve only from verified AuthInfo; do not use clientId as an end-user ID. */
getPrincipal(authInfo: AuthInfo): string | Promise<string>;
/** Use a resolver when the catalog depends on the authenticated account. */
events: readonly ExperimentalMcpEventDefinition[] | ((context: ExperimentalMcpEventContext) => readonly ExperimentalMcpEventDefinition[] | Promise<readonly ExperimentalMcpEventDefinition[]>);
subscriptions: ExperimentalMcpSubscriptionStore;
delivery: ExperimentalMcpWebhookDelivery;
ttl?: {
/** @default 60000 */
minMs?: number;
/** @default 3600000 */
defaultMs?: number;
/** @default 86400000 */
maxMs?: number;
/** Enable only with durable no-expiry storage/delivery. @default false */
allowNoExpiry?: boolean;
};
}
/**
* Register the draft MCP Events webhook control methods on the existing MCP
* endpoint. Storage and delivery must live outside the per-request server.
*
* @experimental Follows the experimental MCP Events design sketch, not a stable
* MCP specification. Poll/stream delivery and model-tool generation are excluded.
*/
declare function experimental_registerMcpEvents(server: McpServer, options: ExperimentalMcpEventsOptions): void;
/** @experimental A validation failure; contains no payload or credentials. */
declare class ExperimentalMcpEventDeliveryError extends Error {
readonly code: ExperimentalMcpEventDeliveryErrorCode;
constructor(code: ExperimentalMcpEventDeliveryErrorCode);
}
/**
* Validate a candidate event against a current stored subscription. Resolves
* without modifying/transforming the payload, or throws to prevent delivery.
*
* @experimental Does not load state, match source events to filters, sign/send,
* or coordinate cancellation. The worker must gate dispatch on active state.
* Authorization/schema backend exceptions and abort reasons propagate unchanged.
*/
declare function experimental_validateMcpEventDelivery({ event, subscription, payload, signal, }: ExperimentalMcpEventDeliveryValidationOptions): Promise<void>;
declare global {
interface Request {
auth?: AuthInfo;
}
}
declare function withMcpAuth(handler: (req: Request) => Response | Promise<Response>, verifyToken: (req: Request, bearerToken?: string) => AuthInfo | undefined | Promise<AuthInfo | undefined>, { required, resourceMetadataPath, requiredScopes, resourceUrl, }?: {
required?: boolean;
resourceMetadataPath?: string;
requiredScopes?: string[];
/**
* Explicit resource URL override. When provided, this URL is used as the
* origin for constructing the resource_metadata URL. Use this when running
* behind a proxy that doesn't set standard forwarding headers, or when you
* need to specify a specific public URL.
*
* If not provided, the origin is automatically detected from proxy headers
* (X-Forwarded-Host, X-Forwarded-Proto, Forwarded) or falls back to req.url.
*/
resourceUrl?: string;
}): (req: Request) => Promise<Response>;
/**
* OAuth 2.0 Protected Resource Metadata endpoint based on RFC 9728.
* @see https://datatracker.ietf.org/doc/html/rfc9728
*
* @param authServerUrls - Array of issuer URLs of the OAuth 2.0 Authorization Servers.
* These should match the "issuer" field in the authorization servers'
* OAuth metadata (RFC 8414).
* @param resourceUrl - Optional explicit resource URL override. When provided, this URL is
* used instead of deriving it from the request. Use this when running
* behind a proxy that doesn't set standard forwarding headers.
* If not provided, the URL is automatically detected from proxy headers
* (X-Forwarded-Host, X-Forwarded-Proto, Forwarded) or falls back to req.url.
*/
declare function protectedResourceHandler({ authServerUrls, resourceUrl: explicitResourceUrl, }: {
authServerUrls: string[];
resourceUrl?: string;
}): (req: Request) => Response;
/**
* Generates protected resource metadata for the given auth server URLs and
* protected resource identifier. The protected resource identifier, as defined
* in RFC 9728, should be a a URL that uses the https scheme and has no fragment
* component.
*
* @param authServerUrls - Array of issuer URLs of the authorization servers. Each URL should
* match the "issuer" field in the respective authorization server's
* OAuth metadata (RFC 8414).
* @param resourceUrl - the protected resource identifier
* @param additionalMetadata - Additional metadata fields to include in the response
* @returns Protected resource metadata, serializable to JSON
*/
declare function generateProtectedResourceMetadata({ authServerUrls, resourceUrl, additionalMetadata, }: {
authServerUrls: string[];
resourceUrl: string;
additionalMetadata?: Partial<OAuthProtectedResourceMetadata>;
}): OAuthProtectedResourceMetadata;
/**
* CORS options request handler for OAuth metadata endpoints.
* Necessary for MCP clients that operate in web browsers.
*/
declare function metadataCorsOptionsRequestHandler(): () => Response;
/**
* Get the public-facing origin from a request, respecting proxy headers.
*
* When running behind a reverse proxy (e.g., nginx, Vercel, Cloudflare),
* the `req.url` typically reflects the internal URL (e.g., http://localhost:3000).
* This function reconstructs the public-facing origin using standard proxy headers.
*
* Header precedence:
* 1. X-Forwarded-Host + X-Forwarded-Proto (most common)
* 2. Forwarded header (RFC 7239)
* 3. Falls back to req.url origin
*
* @param req - The incoming request
* @returns The public-facing origin (e.g., "https://example.org")
*/
declare function getPublicOrigin(req: Request): string;
/**
* Get the public-facing URL from a request, respecting proxy headers.
*
* @param req - The incoming request
* @returns The public-facing URL with the correct origin
*/
declare function getPublicUrl(req: Request): URL;
export { type ExperimentalMcpEventAuthorizationContext, type ExperimentalMcpEventContext, type ExperimentalMcpEventDefinition, ExperimentalMcpEventDeliveryError, type ExperimentalMcpEventDeliveryErrorCode, type ExperimentalMcpEventDeliveryValidationOptions, type ExperimentalMcpEventsOptions, type ExperimentalMcpSubscriptionKey, type ExperimentalMcpSubscriptionState, type ExperimentalMcpSubscriptionStore, type ExperimentalMcpWebhookDelivery, type ExperimentalMcpWebhookFailure, type ExperimentalMcpWebhookSubscription, type McpHandlerOptions, type WebMcpOptions, createMcpRouteHandler as createMcpHandler, experimental_registerMcpEvents, experimental_validateMcpEventDelivery, withMcpAuth as experimental_withMcpAuth, generateProtectedResourceMetadata, getPublicOrigin, getPublicUrl, metadataCorsOptionsRequestHandler, protectedResourceHandler, withMcpAuth };