import type { ComponentApi } from "../component/_generated/component.js"; import { type McpAuthorizerDecision, type McpAuthorizerHandler, type McpBeforeResourceReadHandler, type McpIcon, type McpServerInfo, type McpToolRegistration } from "../shared.js"; /** * Browser-based MCP clients (e.g. anything served from a webapp * origin) issue a CORS preflight before each `/mcp/` call. Set this * option to enable preflight handling and the matching response * headers; non-browser clients (CLIs, server-to-server) work without * it. * * - `true`, permissive: `Access-Control-Allow-Origin: *`, * `Access-Control-Allow-Credentials: false` (the spec forbids * credentials with the wildcard origin). Tokens are passed via * `Authorization: Bearer ...` so this works for OAuth flows. * - `string` / `string[]`, exact-match allowlist of origins. The * request's `Origin` header is echoed back if it matches, otherwise * no CORS headers are emitted (the browser then blocks the call). * - `(origin: string) => boolean`, custom matcher for things like * subdomain wildcards or per-tenant rules. * * `Mcp-Session-Id` is automatically exposed via * `Access-Control-Expose-Headers` so JS clients can read it after * `initialize`. * * **Production note**: `cors: true` makes response bodies readable * from any origin. The gateway carries auth via `Authorization: * Bearer ...` (never cookies), so wildcard CORS does not transmit * the user's credentials, but a webapp running in the user's * browser with the Bearer in its own state can read responses * cross-origin. Prefer an explicit allowlist * (`cors: ["https://app.example.com"]`) for any deployment with * non-trivial auth coupling. */ export type McpCorsOption = true | string | string[] | ((origin: string) => boolean); /** * Optional Bearer-token validator for `handleMcpRequest`. When set, * the gateway calls this BEFORE `ctx.auth.getUserIdentity()` and uses * its return value as the identity for the audit row and as a hint * for the authorize callback (via `args.identity`). * * Useful when the upstream IdP issues opaque access tokens that * Convex's local JWT validation can't verify, typical pattern is * to call the IdP's userinfo endpoint: * * ```ts * resolveIdentity: async (token) => { * const r = await fetch("https://id.example.com/api/oidc/userinfo", { * headers: { Authorization: `Bearer ${token}` }, * }); * if (!r.ok) return null; * const u = await r.json(); * return { subject: u.sub, claims: u }; * } * ``` * * Returning `null` means "token rejected" (treated identically to * "no token at all"). Throwing is treated as null with a warning * logged, rejection is not an error condition. * * When this option is omitted, the gateway falls back to * `ctx.auth.getUserIdentity()` (which only handles JWTs validated * by your `auth.config.ts`). */ export type McpIdentityResolver = (token: string) => Promise<{ subject: string; claims?: Record; } | null>; /** * MCP resource/content annotations. All fields optional: * - `audience`: who the resource is for (`"user"` and/or `"assistant"`). * - `priority`: importance from `0` (least) to `1` (most). * - `lastModified`: timestamp of the last change, conventionally ISO 8601. * Validated only as a string; the date format is not enforced. */ export type McpResourceAnnotations = { audience?: ("user" | "assistant")[]; priority?: number; lastModified?: string; }; export type McpResource = { uri: string; name: string; /** Human-friendly display name; falls back to `name` in clients. */ title?: string; description?: string; mimeType?: string; annotations?: McpResourceAnnotations; icons?: McpIcon[]; /** Raw size in bytes, if known. */ size?: number; }; export type McpResourceContent = { uri: string; mimeType?: string; text?: string; blob?: string; }; /** * The caller a resource provider or template read handler sees. * * `null` only on a mount that set `anonymousResources`. Without that * option every one of these calls carries a principal, exactly as before * the option existed, so a provider that does not serve anonymous callers * can narrow once and carry on. */ export type McpCallerIdentity = { subject: string; claims?: Record; }; export type McpResourceCaller = McpCallerIdentity | null; /** * The three resource methods, in the vocabulary the audit log already * uses. Named because it appears in three places: the `operation` an * anonymous authorizer decision carries, the `resourceOperation` on an * audit row, and the helper that builds the authorizer input. (The * `auditResources` OPTION spells the middle one `templatesList`; that is * a host-facing config key rather than this vocabulary, and it stays.) */ export type McpResourceOperation = "list" | "templates_list" | "read"; export type McpResourceProvider = { name: string; list: (ctx: McpHandlerCtx, args: { identity: McpResourceCaller; }) => Promise; read: (ctx: McpHandlerCtx, args: { uri: string; identity: McpResourceCaller; }) => Promise; }; /** * An RFC 6570 resource template advertised via `resources/templates/list`. * `uriTemplate` is a level-1 template (simple `{var}` placeholders, each * matching a single URI path segment); clients expand it to a concrete URI * and read it through `resources/read`. */ export type McpResourceTemplate = { uriTemplate: string; name: string; /** Human-friendly display name; falls back to `name` in clients. */ title?: string; description?: string; mimeType?: string; annotations?: McpResourceAnnotations; icons?: McpIcon[]; }; /** * Server-side read handler for a resource template: invoked when a * `resources/read` URI matches the template, with the extracted template * variables in `params`. Returns `null` to decline the URI (a later * template or a not-found is then used). */ export type McpResourceTemplateReadHandler = (ctx: McpHandlerCtx, args: { uri: string; params: Record; identity: McpResourceCaller; }) => Promise; /** * Runtime form of a resource template, as produced by * `defineMcpResourceTemplate`. `match` returns the extracted template * variables when a concrete URI matches `template.uriTemplate`, or `null` * when it doesn't. `read` is optional: present means the gateway resolves * expanded-URI reads server-side; absent means the template is * listing-only (the client reads the expansion via another provider). */ export type McpResourceTemplateProvider = { template: McpResourceTemplate; match: (uri: string) => Record | null; read?: McpResourceTemplateReadHandler; }; /** * The authorizer input for a caller the gateway has authenticated. Every * resource method resolves to one of these unless the mount opted into * `anonymousResources`. */ export interface McpIdentifiedResourceAuthorizerArgs { /** * `"resource_list"` when filtering `resources/list`, * `"resource_read"` before a `resources/read` handler runs, * `"resource_templates_list"` when filtering `resources/templates/list` * (here `resourceUri` carries the template's `uriTemplate`). * * Note on templates: `resources/read` of a template-expanded URI is * authorized under `"resource_read"` with the **concrete expanded URI** * (e.g. `weather://london/current`), not the `uriTemplate`, and with * `resourceMetadata: null`. So a template hidden at list time * (`"resource_templates_list"` → denied) is NOT automatically unreadable: * `"resource_read"` is the read gate for both concrete and template URIs. * Enforce read access in the `resource_read` branch (match the URI shape) * and/or inside the template's own `read` handler. */ mode: "resource_list" | "resource_read" | "resource_templates_list"; resourceUri: string; /** * Free-form metadata attached to a registered resource. Runtime-only * provider resources that are not present in the registry pass `null`. */ resourceMetadata: unknown; /** * The caller's identity resolved once at the gateway boundary. These * three modes run only for an authenticated caller, so this is non-null * when the callback runs, and the union enforces that: a null identity * on one of these modes does not typecheck. */ identity: McpCallerIdentity; } /** * The authorizer input for an UNAUTHENTICATED caller. Only reachable on a * mount that set `anonymousResources: true`; without it the gateway * refuses anonymous resource requests before the authorizer runs and this * variant never occurs. * * It is a mode of its own rather than the three above with a null * `identity` so that an existing authorizer meets an unrecognised mode * rather than a familiar one with a surprising argument. What happens * next is that authorizer's default branch, and the two common shapes * differ: * * - one that returns nothing for an unknown mode denies, because * `parseAuthorizerDecision` reads a missing decision as a denial * - one that ends in `return { allowed: true }` ALLOWS, and publishes * whatever the anonymous caller asked for * * So this is a smaller guarantee than "cannot accept one by accident": * the lock is `anonymousResources` itself, which no existing mount has * set. Read your default branch before setting it. */ export interface McpAnonymousResourceAuthorizerArgs { mode: "resource_anonymous"; /** * Which resource method the anonymous caller is attempting. One mode * keeps the "do I serve anonymous callers at all" decision in a single * branch; this field is there for a host that wants to allow anonymous * listing without allowing anonymous reads, or the reverse. * * `"list"` and `"read"` carry a concrete resource URI, `"templates_list"` * carries the template's `uriTemplate`. */ operation: McpResourceOperation; resourceUri: string; /** * Free-form metadata attached to a registered resource, exactly as on an * authenticated call. `null` for a runtime-only provider resource, for * every template, and for a read whose URI matched a template rather * than a registered resource, so a public template must be recognised by * its URI shape rather than by metadata. */ resourceMetadata: unknown; /** Always `null`. The discriminant is `mode`; this documents the fact. */ identity: null; } /** * What `authorizeResource` receives. Narrow on `mode` before reading * `identity`: it is non-null for the three authenticated modes and null * for `"resource_anonymous"`. */ export type McpResourceAuthorizerArgs = McpIdentifiedResourceAuthorizerArgs | McpAnonymousResourceAuthorizerArgs; export type McpResourceAuthorizerHandler = (ctx: McpHandlerCtx, args: McpResourceAuthorizerArgs) => Promise | McpAuthorizerDecision; export type McpResourceAuditOption = boolean | { list?: boolean; read?: boolean; templatesList?: boolean; }; export type McpMrtrOptions = { /** At least 32 bytes of private, stable key material for HMAC-SHA-256. */ secret: string; /** Maximum continuation lifetime. Defaults to five minutes. */ ttlMs?: number; }; /** * The snapshot handed to a host task executor and to the update hooks. * `identity` is the caller resolved when the task was created; `args` * are the public tool arguments (identity/reserved keys already * stripped); `idempotencyKey` is issued once per task and must be * persisted by the tool around its side effect. */ export type McpTaskContext = { taskId: string; toolName: string; toolKind: "query" | "mutation" | "action"; args: Record; identity: { subject: string; claims?: Record; }; idempotencyKey: string; expiresAt: number; }; /** * Starts durable execution for a freshly created task, e.g. * `workflow.start(ctx, internal.tasks.runArchive, {...})` with * `@convex-dev/workflow`. It must only *start* the work and return; * the execution itself finalizes the task later via * `gateway.completeTask` / `gateway.failTask` (or pauses it via * `gateway.requireTaskInput`). A throw here fails the task immediately. */ export type McpTaskExecutor = (ctx: McpHandlerCtx, task: McpTaskContext) => Promise | void; export type McpTasksOptions = { /** * Host-owned durable execution. Omit it to use the built-in * scheduled executor, which runs the registered tool function once * and completes/fails the task (no retries, no input rounds). */ execute?: McpTaskExecutor; /** * Whether THIS call should become a task, for a tool registered * `taskSupport: "optional"`. SEP-2663 makes the decision the server's: * a client opts in once through the extension capability and sends no * per-request flag, so something on this side has to choose, and the * knowledge of "is this one going to be slow" lives in the host rather * than in the gateway. * * Omit it and every eligible call becomes a task, which is what the * spec's own conformance scenario requires of a task-supporting tool. * Return `false` to answer inline instead; the spec allows that * explicitly for a fast operation. * * Only consulted when a task is possible at all: the tool says * `optional`, the client declared the extension, and the caller is * authenticated. A `required` tool never reaches it, and neither does * a call this mount's `authorize` denied. */ shouldCreate?: (ctx: McpHandlerCtx, call: { toolName: string; toolKind: "query" | "mutation" | "action"; args: Record; identity: { subject: string; claims?: Record; }; }) => Promise | boolean; /** * Called after a `tasks/update` accepted MRTR-shaped `inputResponses` * for an `input_required` task (now back in `working`). Hosts with a * custom `execute` resume their workflow here. Best-effort AND * at-least-once: a throw is logged and the update still succeeds (the * responses are already durably stored on the task row), and an * idempotent duplicate update re-fires the hook so a client that * re-sends the same responses retries a notification that previously * failed. The hook MUST therefore tolerate repeats. */ onInputResponses?: (ctx: McpHandlerCtx, event: { taskId: string; toolName: string; inputResponses: Record; }) => Promise | void; /** * Called after an owner cancellation, including idempotent repeats: * re-sending the cancel is the retry path for a notification that * previously threw, so the hook MUST tolerate being called for an * already-cancelled task. Hosts cancel their workflow run here. */ onCancel?: (ctx: McpHandlerCtx, event: { taskId: string; toolName: string; }) => Promise | void; /** * Default task retention. A task row (and with it the result) expires * `retentionMs` after creation; expired tasks answer like unknown ids * and are dropped by `gateway.pruneTasks`. Clamped to * [1 minute, 7 days]; defaults to 24 hours. A client may request a * shorter `ttlMs` per call, clamped the same way. */ retentionMs?: number; /** * Identifies THIS mount for task ownership. Stored on every task the * mount creates, and required to match on every `tasks/get` / * `tasks/update`; a mismatch answers exactly like an unknown task id. * * Set it whenever the gateway is mounted more than once with different * `authorize` policies over the same identity namespace. The task table * is component-wide and `authorize` runs only at creation, so without a * scope a caller permitted on a broad mount can start a privileged task * there and collect its result through a narrower one: bypassing the * narrower mount's policy without any bug in it. * * A sealed MRTR `requestState` is not bound to the mount, so two mounts * sharing `mrtr.secret` both accept the same continuation and each ends * up owning its own task row for that chain (their rows are * scope-isolated). The tool still dedupes, because both runs receive the * same chain key in `mrtrArgs`. Give differently-scoped mounts different * secrets if you want continuations to be non-transferable. * * Unset (the default) keeps the pre-scope behaviour, so a single-mount * host needs no migration. Adopting it later only affects tasks created * from then on: rows already in flight have no scope and stay visible * only to unscoped mounts until they expire. */ scope?: string; /** Advertised polling interval hint, defaults to 2000 ms. */ pollIntervalMs?: number; }; /** * Origin allowlist for `handleMcpRequest`. MCP Streamable HTTP requires * servers to validate the `Origin` header to prevent DNS-rebinding * attacks: a request whose `Origin` is present but not allowed is * rejected with HTTP 403 before identity resolution, authorization, * auditing, or dispatch. Requests without an `Origin` header (every * CLI and server-to-server client) are unaffected. * * - `string` / `string[]`, exact-match allowlist of origins. * - `(origin: string) => boolean`, custom matcher for subdomain * wildcards or per-tenant rules. * * This is deliberately independent of `cors`. CORS is a browser * mechanism that decides what a browser is allowed to *read*; * `allowedOrigins` is an authorization gate that decides what the * gateway is willing to *serve*. Coupling the two makes the permissive * `cors: true` silently disable the origin gate, so they are separate * options. * * **Omitting this option disables origin validation entirely.** That is * the default because a Convex deployment is reachable at a fixed * public URL rather than on localhost, which is the DNS-rebinding * scenario the requirement targets. Set it for any deployment that * serves browser clients. */ export type McpAllowedOriginsOption = string | string[] | ((origin: string) => boolean); /** * Options for `gateway.handleMcpRequest`. The host supplies an * `authorize` callback that decides allowed vs denied per * `tools/call` and per tool in a filtered `tools/list`. The callback * runs in the host's HTTP-action context, so it has the host's * `ctx.auth` and can call `ctx.auth.getUserIdentity()` directly. */ export interface HandleMcpRequestOptions { authorize: McpAuthorizerHandler; /** See `McpCorsOption`. Omit for non-browser-only deployments. */ cors?: McpCorsOption; /** * See `McpAllowedOriginsOption`. Omit to disable origin validation. */ allowedOrigins?: McpAllowedOriginsOption; /** * See `McpIdentityResolver`. Omit to use Convex's built-in JWT * validation via `ctx.auth.getUserIdentity()`. */ resolveIdentity?: McpIdentityResolver; /** * Override the `serverInfo` returned in the `initialize` response and * in the `_meta` of every stateless result. Defaults to this package's * own name and version; that constant is intentionally static, because * Convex doesn't expose `package.json` to the runtime. * * Supplying this replaces the whole block rather than merging into it, * so a host that only wants to add an icon still restates `name` and * `version`. Beyond those two, the spec's `Implementation` carries * `title`, `description`, `websiteUrl` and `icons` for hosts that * white-label or want telemetry-grade version reporting. * * Two things to know before adding `icons` here, both measured rather * than reasoned about. * * A SPEC-CORRECT icons block takes the connection down on SDK 1.18.0 * through 1.18.2, and `describeServerInfoProblem` cannot stop it. Those * builds typed `Icon.sizes` as a bare string, so `sizes: ["48x48"]`, * which is what the spec mandates and what the validator rightly * accepts, fails their parse of the whole `InitializeResult` * ("Expected string, received array at serverInfo.icons.0.sizes"). * Omitting `sizes` is accepted by every build we have. The validator * guards the MALFORMED shape, which is a different hazard: it stops the * gateway emitting a block a spec-conformant client would reject. The * old-client hazard is a client-side bug and the only lever here is not * sending `sizes`. Note the escalation over the same field on a tool: * there it costs one `tools/list`, here the client never connects. * * Keep it small, and prefer an `https:` src over a `data:` one. Unlike * a tool descriptor, this block is repeated on every stateless result, * `tools/call` and `resources/read` and every `tasks/get` poll * included, not just the handshake. Measured with the smallest real * inline icon there is, a 1x1 transparent PNG: `name` + `version` * alone is 47 bytes, the same block with two data-URI icons is 534, so * 11x per result. A realistic 48x48 icon is several KB. */ serverInfo?: McpServerInfo; /** * Challenge anonymous requests with `401` instead of letting them * through to `initialize` / `tools/list`. Default `false`. * * Leave this off for **mixed** servers (some tools `public`, * some private): anonymous callers should still see the public * catalog, so the default 200-with-filtered-list is correct. * * Turn it on for **all-private** servers that browser MCP clients * (claude.ai) connect to. Such a client only does `initialize` + * `tools/list` when a connector is added; with the default both * return 200 (an empty, authorize-filtered list), so the client * concludes "connected, no tools" and never starts the OAuth flow, * its only trigger is a `401` + `WWW-Authenticate`. With * `requireAuth: true` an anonymous POST gets that 401, so the login * is prompted and discovery begins. * * Needs `setOAuthConfig` to have run so the `WWW-Authenticate` * header can carry the protected-resource metadata URL. If * `requireAuth` is set but no OAuth config exists, the gate still * returns 401, but without the header (and `console.warn`s once); * browser clients can't begin discovery until `setOAuthConfig` is * called. * * Applies to `POST` only. `GET` already 405s, `DELETE` is * identity-bound, and `OPTIONS` (CORS preflight) is left untouched. */ requireAuth?: boolean; /** * Declarative tool catalog. When set, the registry is reconciled from * this list on `initialize` (change-detected, so an unchanged list is * a cheap no-op), and no separate registration mutation is needed. * Omit it to manage the registry yourself via `gateway.register(...)`. * Annotate an exported list with `McpToolRegistration[]` to avoid a * Convex codegen circular-type error (see that type's docs). */ tools?: McpToolRegistration[]; /** * Server-level guidance returned in the MCP `initialize` result's * `instructions` field (see the spec's `InitializeResult.instructions`). * Clients may hand this to the LLM to explain how to use the server as a * whole, e.g. "call `kira_load_skill` before answering", without bloating * individual tool descriptions. Omitted from the response entirely when * unset, so the default `initialize` shape is unchanged. * * Best-effort hint, not a guarantee: the spec says clients MAY add it to * the system prompt, and some ignore it entirely. Clients that honor it * tend to cap and front-truncate the text, so keep it short and put the * critical guidance first. Enforce hard constraints in each tool's * `authorize` / handler, never here. */ initializeInstructions?: string; /** * Optional MCP resources exposed by this gateway. Resources are listed * in `initialize.capabilities.resources`, served via `resources/list`, * and read via `resources/read`. A provider receives the resolved * caller identity, or `null` on a mount that set `anonymousResources`, * which is also the only mount where an anonymous resource request is * served rather than refused. */ resources?: McpResourceProvider[]; /** * Optional MCP resource templates (RFC 6570) exposed by this gateway. * Advertised via `resources/templates/list` and, for templates declared * with a `read` handler, resolved server-side when `resources/read` * requests a URI that matches the template (concrete resources take * precedence). Build these with `defineMcpResourceTemplate`. */ resourceTemplates?: McpResourceTemplateProvider[]; /** * Optional central authorization hook for MCP resources. If omitted, * authenticated callers can list/read all resources exposed by providers. * If set, `resources/list` filters resources through `resource_list`, and * `resources/read` checks `resource_read` before invoking the provider. */ authorizeResource?: McpResourceAuthorizerHandler; /** * Serve `resources/list`, `resources/templates/list` and * `resources/read` to unauthenticated callers, subject to * `authorizeResource`. Default `false`, and with it off the three * methods refuse an anonymous caller with `-32001` before the * authorizer runs, which is the behaviour every mount had before this * option existed. * * This is the resource counterpart of a public tool, but it is a * gateway option rather than the host-side `metadata.public` * convention tools use, because `authorizeResource` cannot be the only * lock: a mount that never configured one authorizes every resource by * default, so "let the authorizer decide" would silently publish the * whole catalog. Setting this without an `authorizeResource` therefore * throws on the first request through the mount; there is no * deploy-time hook to fail at. * * An anonymous caller reaches the authorizer under * `mode: "resource_anonymous"`, never under the three authenticated * modes. That keeps an existing policy from being applied to a caller * it was not written for, but it does not decide the outcome: an * authorizer whose default branch returns `{ allowed: true }` allows * the anonymous caller too. Audit that branch before opting in. * * Three things it deliberately does not do: * * - `resources/subscribe` and `resources/unsubscribe` stay * authenticated. A subscription is server-side state an anonymous * caller could accumulate, and it buys a client nothing here: this * transport does not push, so the host delivers * `notifications/resources/updated` over its own channel. * - It cannot be combined with `beforeResourceRead`, which throws on * the first request. That hook's contract passes a non-null identity and an * MRTR continuation must bind to a principal, the same reason a * tool's `beforeCall` requires an authenticated caller. * - It does not override `requireAuth`. That gate answers anonymous * POSTs with `401` before the method switch, so a mount setting * both serves no anonymous resource. Same as `requireAuth` with a * public tool. * * Auditing a mount that serves anonymous callers wants a retention * cron. A failing anonymous outcome is never recorded, but a SUCCEEDING * one is, one row per request, so `gateway.pruneAuditEntries` is what * bounds the table. See `docs/audit-log.md`. */ anonymousResources?: boolean; /** * Optional MRTR hook for `resources/read`: the read counterpart of a * tool's `beforeCall`. Runs after `authorizeResource` allowed the read * and before any provider or template is consulted, on the first read * AND on every verified continuation of it (where it additionally * receives the decoded `state`, the client's untrusted `inputResponses`, * and the `round`). * * Return `inputRequired()` to ask the client for input (per MCP * 2026-07-28, `resources/read` may answer with an `InputRequiredResult`), * optionally with `onUnsupported` for clients that cannot satisfy the * requested capabilities, * `completeRead(contents)` to serve content yourself, `declineRead(reason)` * to refuse after the answer, or `null` to fall through to the normal * read path. * * Mount-level rather than per-resource, mirroring `authorizeResource`: a * provider serves many URIs and the gateway cannot know which one owns a * URI without calling it, so the gate has to sit where the URI is known * and nothing has run yet. Branch on `uri` inside the hook. * * Requires the `mrtr` option (the continuation is sealed with its * secret) and the modern protocol; a read that demands input on a legacy * request fails closed rather than silently serving the resource. * * Incompatible with `anonymousResources`: the two together throw on the * first request through the mount, because this hook's contract passes * a non-null identity and the MRTR chain it can open must bind to a * principal. */ beforeResourceRead?: McpBeforeResourceReadHandler; /** * Opt-in audit for MCP resource operations. Defaults to `false`. * `true` records `resources/list`, `resources/read`, and * `resources/templates/list`; the object form (`{ list, read, * templatesList }`) enables each operation independently. Resource * contents are never stored. */ auditResources?: McpResourceAuditOption; /** * Opt-in MCP resource subscription support. **Off by default**, because * this gateway's HTTP transport is request-scoped and cannot push * server-initiated notifications (`notifications/resources/updated` / * `notifications/resources/list_changed`). With both flags off, * `initialize` advertises neither capability and `resources/subscribe` / * `resources/unsubscribe` return a clear `-32601`. * * Set these flags ONLY when the host fronts the gateway with a transport * that CAN deliver notifications (its own SSE/WebSocket layer). The * gateway then advertises the capability and tracks subscribe/unsubscribe * state per session; the host owns delivery, it reads * `gateway.listResourceSubscribers(uri)` and ships payloads built with * `gateway.buildResourceUpdatedNotification` / * `gateway.buildResourceListChangedNotification`. * * - `subscribe`: advertise `capabilities.resources.subscribe` and handle * `resources/subscribe` / `resources/unsubscribe`. * - `listChanged`: advertise `capabilities.resources.listChanged` (the * host emits `notifications/resources/list_changed` itself when its * catalog changes). */ resourceSubscriptions?: { subscribe?: boolean; listChanged?: boolean; }; /** * Opt-in support for stateless-era multi-round-trip requests (MRTR). A * declarative tool's host-side `beforeCall` hook is the state machine: * on the first call it can return `inputRequired(inputRequests, state)` * or provide an `onUnsupported` fallback for clients that cannot satisfy * the requested capabilities * before the underlying Convex function can run; on every verified * continuation it runs again with the decoded state, the client's * untrusted `inputResponses`, and the chain's stable idempotency key, * and decides whether to ask for another round, finish without * dispatching (`completeCall()`), or continue to the Convex function * (which stays MCP-unaware; only the idempotency key is injectable via * `mrtrArgs`). Continuations are HMAC-sealed, TTL-bound, bound to the * caller/tool/arguments, and redeemed once server-side so a captured * state cannot be replayed with different responses. */ mrtr?: McpMrtrOptions; /** * Opt-in MCP Tasks (`io.modelcontextprotocol/tasks`). **Off by * default**; the capability is advertised in `server/discover` only * when this option is set, and only tools registered with * `taskSupport: true` accept a task-augmented stateless `tools/call`. * * Without `execute`, the gateway runs the tool once via the component's * built-in scheduled executor (durable across restarts, no retries) and * completes or fails the task. Hosts that need retry policy, delays, * or `input_required` rounds supply `execute` to start their own * durable execution (typically a `@convex-dev/workflow` run), and * finalize via `gateway.completeTask` / `failTask` / * `requireTaskInput`. See docs/tasks.md. */ tasks?: McpTasksOptions; } /** * Internal handler options: the public `HandleMcpRequestOptions` plus the * catalog synchronizer that `McpGateway.handleMcpRequest` derives from the * declarative catalog options. Not exported, hosts never set it directly. */ type InternalHandleMcpRequestOptions = HandleMcpRequestOptions & { ensureCatalogSynced?: () => Promise; declarativeTools?: McpToolRegistration[]; }; export type McpHandlerCtx = { runQuery: (ref: any, args: any) => Promise; runMutation: (ref: any, args: any) => Promise; runAction: (ref: any, args: any) => Promise; auth: { getUserIdentity: () => Promise; }; }; type HandlerCtx = McpHandlerCtx; /** * Validate the `x-mcp-header` annotations in a tool's `inputSchema`. * Returns a human-readable problem string, or `null` when the schema is * valid. Called when a catalog is registered or synced, so a * schema-authoring mistake surfaces with the tool name attached instead * of failing every stateless `tools/call` for that tool at runtime. */ export declare function describeToolHeaderSchemaProblem(inputSchema: unknown): string | null; /** * Validate an MCP `icons` array (tools, resources, and resource templates all * carry the same shape). Per the spec's `Icon`: `src` is required, everything * else optional, `theme` constrained to `"light"` / `"dark"`. * * `src` is checked for being a non-empty string and nothing more. The gateway * is the producer here and never dereferences the URI; the spec puts the * fetch-side burden on consumers ("SHOULD take steps to ensure URLs serving * icons are from the same domain", "SHOULD take appropriate precautions when * consuming SVGs"). So an icon `src` is host-authored content that reaches * the client verbatim, exactly like a description. * * `label` names the enclosing descriptor in the message ("resource", "tool"), * so a rejection points at what to fix. */ export declare function describeIconsProblem(icons: unknown, label: string): string | null; /** * Validate the `serverInfo` option against the spec's `Implementation`. * Returns a human-readable problem string, or `null` when valid * (including when `undefined`). * * The gateway's own default block is what this guards the wire against * replacing badly. A host builds this object once at mount time from * constants, so a problem here is wrong for every request rather than for * one caller's arguments, which is why `handleMcpRequest` throws on it * instead of answering the request with an error. */ export declare function describeServerInfoProblem(serverInfo: unknown): string | null; /** * Validate MCP resource/content annotations. Returns a human-readable * problem string, or `null` when valid (including when `undefined`). * Exported so the `defineMcp*` helpers can fail loud at declaration time * with the same rules the request handler enforces on provider output. */ export declare function describeAnnotationsProblem(annotations: unknown): string | null; /** * Validate an MCP resource descriptor (a `resources/list` entry). Returns a * problem string or `null`. `uri` and `name` are required non-empty strings; * `title`/`description`/`mimeType` are optional strings; `size` is an * optional non-negative number; `annotations` is validated as above. */ export declare function describeResourceProblem(resource: unknown): string | null; /** * Validate an MCP resource template descriptor (a `resources/templates/list` * entry). Like `describeResourceProblem` but keyed on `uriTemplate` and * without `size`. */ export declare function describeResourceTemplateProblem(template: unknown): string | null; /** * Validate the array a resource read handler returns. Must be an array; each * item needs a non-empty string `uri`, optional string `mimeType`, and at * least one of `text`/`blob` (each a string when present). Returns a problem * string or `null`. */ export declare function describeResourceContentsProblem(contents: unknown): string | null; /** * Project an arbitrary template-shaped object down to exactly the known * `McpResourceTemplate` fields. Shared by the request handler (response * shaping), `defineMcpResourceTemplate`, and the registry-sync projection so * the three never drift, and so a hand-built provider's extra keys never * reach the response or the registry's strict validator. */ export declare function pickTemplateFields(template: McpResourceTemplate): McpResourceTemplate; export declare function handleMcpRequest(ctx: HandlerCtx, request: Request, component: ComponentApi, options: InternalHandleMcpRequestOptions): Promise; export {}; //# sourceMappingURL=mcp-handler.d.ts.map