import { type FunctionArgs, type FunctionReference, type FunctionReturnType } from "convex/server"; import type { GenericValidator, Infer, ObjectType, PropertyValidators } from "convex/values"; import type { ComponentApi } from "../component/_generated/component.js"; import { type McpBeforeCallHandler, type McpCaller, type McpIcon, type McpToolAnnotations, type McpToolDefinition, type McpToolKind, type McpToolRegistration, type McpToolSecurityScheme } from "../shared.js"; import { type HandleMcpRequestOptions, type McpHandlerCtx, type McpResource, type McpResourceContent, type McpResourceCaller, type McpResourceProvider, type McpResourceTemplate, type McpResourceTemplateProvider, type McpResourceTemplateReadHandler } from "./mcp-handler.js"; export type { JsonSchema, McpAuthorizerArgs, McpAuthorizerDecision, McpAuthorizerHandler, McpBeforeCallArgs, McpBeforeCallHandler, McpBeforeCallResult, McpBeforeResourceReadArgs, McpBeforeResourceReadHandler, McpBeforeResourceReadResult, McpCaller, McpCompleteCallResult, McpCompleteReadResult, McpDeclineReadResult, McpHostCallbackCtx, McpIcon, McpInputRequiredFallback, McpInputRequiredResult, McpServerInfo, McpToolAnnotations, McpToolDefinition, McpToolFunctionReference, McpToolKind, McpMrtrArgs, McpToolRegistration, McpToolSecurityScheme, } from "../shared.js"; export type { HandleMcpRequestOptions, McpAllowedOriginsOption, McpCorsOption, McpHandlerCtx, McpMrtrOptions, McpIdentityResolver, McpAnonymousResourceAuthorizerArgs, McpCallerIdentity, McpIdentifiedResourceAuthorizerArgs, McpResourceAuditOption, McpResourceAuthorizerArgs, McpResourceAuthorizerHandler, McpResource, McpResourceAnnotations, McpResourceCaller, McpResourceContent, McpResourceOperation, McpResourceProvider, McpResourceTemplate, McpResourceTemplateProvider, McpResourceTemplateReadHandler, McpTaskContext, McpTaskExecutor, McpTasksOptions, } from "./mcp-handler.js"; export { completeCall, completeRead, declineRead, inputRequired, } from "../shared.js"; export { buildProtectedResourceMetadataUrl, buildResourceUrl, convexValidatorToJsonSchema, mcpCallerValidator, resourcePathFromWellKnownRequest, } from "../shared.js"; export type RunQueryCtx = { runQuery: >(query: Query, args: FunctionArgs) => Promise>; }; export type RunMutationCtx = RunQueryCtx & { runMutation: >(mutation: Mutation, args: FunctionArgs) => Promise>; }; export type McpResourceReadHandler = (ctx: McpHandlerCtx, args: { uri: string; /** * `null` only on a mount that set `anonymousResources`, and only for a * resource its `authorizeResource` allowed anonymously. Every other * mount calls this with a principal, as before the option existed. */ identity: McpResourceCaller; }) => Promise; /** * Catalog metadata persisted in the component registry. Intentionally * narrower than {@link McpResource}: the registry stores only stable * catalog fields (see the component schema), so the richer list-response * fields (`title`, `annotations`, `size`) are **runtime-only** and are not * accepted here. They are still served from a resource provider's `list` * output; they just aren't persisted. */ export type McpResourceDescriptor = { uri: string; name: string; description?: string; mimeType?: string; metadata?: Record; }; export type McpResourceRegistration = McpResourceProvider & { resource: McpResourceDescriptor; }; export type McpResourceConfig = McpResource & { /** * Free-form metadata stored alongside the registry descriptor (never sent * to clients). The component does not inspect it. */ metadata?: Record; /** * Read this concrete resource. The gateway only calls this handler when * `resources/read` requests `uri`, so handlers can focus on loading content * and applying any resource-specific checks. */ read: McpResourceReadHandler; }; type ToolFunctionReference = FunctionReference; type AnyToolFunctionReference = ToolFunctionReference; /** * Args validators must produce exactly the function's expected args. * If they don't, TypeScript surfaces a `_typeMismatch` error on the config * object that makes the failing field obvious. */ type ValidateArgs = ArgsV extends PropertyValidators ? ObjectType extends FunctionArgs ? FunctionArgs extends ObjectType ? unknown : { _typeMismatch: "args validator does not match the function's expected arguments"; expected: FunctionArgs; received: ObjectType; } : { _typeMismatch: "args validator does not match the function's expected arguments"; expected: FunctionArgs; received: ObjectType; } : { _typeMismatch: "args must be a Convex property validators object"; }; /** * Mirror of `ValidateArgs` for the optional `returns:` validator. * When the host omits `returns`, ReturnsV resolves to `undefined` and * validation is bypassed (no constraint). When provided, the validator's * inferred type must equal the function's actual return type, drift * between them surfaces as a `_typeMismatch` on the config object. */ type ValidateReturns = ReturnsV extends undefined ? unknown : ReturnsV extends GenericValidator ? Infer extends FunctionReturnType ? FunctionReturnType extends Infer ? unknown : { _typeMismatch: "returns validator does not match the function's return type"; expected: FunctionReturnType; received: Infer; } : { _typeMismatch: "returns validator does not match the function's return type"; expected: FunctionReturnType; received: Infer; } : { _typeMismatch: "returns must be a Convex validator"; }; /** * Keys of `ArgsV` whose validator accepts the injected caller identity * (`McpCaller`). `identityArg` is constrained to these, so it can only * point at an argument the underlying Convex function actually accepts: * naming an arg of the wrong type (e.g. `v.string()`) or one that does * not exist is a compile error, not a runtime surprise. When no arg * accepts a caller, this is `never`, so `identityArg` cannot be set * until you declare one with `mcpCallerValidator`. */ type McpCallerArgKeys = { [K in keyof ArgsV]: McpCaller extends Infer ? K : never; }[keyof ArgsV] & string; type McpArgKey = keyof ArgsV & string; interface McpToolConfigBase { name: string; description: string; fn: Ref; args: ArgsV; /** * Optional Convex return-validator. When set, the tool advertises an * MCP `outputSchema` and every `tools/call` response includes a * `structuredContent` field with the typed value alongside the * text-JSON `content` block (per MCP 2025-06-18). * * Type-checked against `FunctionReturnType` at compile * time, so a drift between the registered Convex function and the * MCP-advertised return shape can't ship undetected. * * Bytes (`v.bytes()`) are intentionally NOT supported in the first * cut, the JSON-Schema mapping is fine but base64-encoding the * runtime value in `structuredContent` adds enough nuance that we * defer it until there's demand. */ returns?: ReturnsV; /** * Name of an `args` key the gateway fills server-side with the * resolved caller identity (`{ subject, claims }`) instead of taking * it from the client. Declare that key with `mcpCallerValidator` so * the compile-time `args` check still matches the function. The key * is excluded from the advertised `inputSchema`, stripped from * caller-supplied arguments (no spoofing), and injected from the * identity resolved at the gateway boundary right before dispatch. * * Use this for identity-scoped tools: Convex strips `ctx.auth` across * the component boundary, so a dispatched tool function cannot read * the caller from the token. `identityArg` is the supported channel. * Calls with no resolved identity are rejected as `Unauthorized` * before dispatch, so the tool never runs unscoped. */ identityArg?: McpCallerArgKeys; /** * Name of the argument the gateway fills with the MRTR continuation's * stable idempotency key when a verified retry continues to dispatch, * and with the task row's own key when the tool is run by the BUILT-IN * task executor. A host executor (`tasks.execute`) gets no injection: * it must thread `task.idempotencyKey` through itself. * Removed from the public schema and stripped from client requests. * Requires `beforeCall`; distinct from `identityArg`. Required for * `defineMcpMutation` / `defineMcpAction` tools that use `beforeCall`, * since a replayed continuation dispatches again; optional only for * queries, which have no durable side effect to deduplicate. (The same * rule is what makes a `taskSupport` mutation/action with a hook carry * the key its deferred run is deduped on; `taskSupport` itself is not * separately validated.) */ mrtrArgs?: { idempotencyKey: McpArgKey; }; /** * Host-side MRTR state machine, run before the underlying Convex * function on the first call AND on every verified continuation * (which additionally carries the decoded `state`, the client's * untrusted `inputResponses`, the stable `idempotencyKey`, and the * `round` number). Return `inputRequired()` for another round, optionally * with `onUnsupported` for clients that cannot satisfy its capabilities, * `completeCall()` to end the call without dispatching (e.g. a * declined confirmation), or `null`/`undefined` to continue to the * Convex function, which stays MCP-unaware. * * Composes with `taskSupport`: for a task-augmented call the hook runs * at task-creation time, so no durable task exists until it approves. */ beforeCall?: McpBeforeCallHandler; /** * Opt-in MCP Tasks support. When `true` and the host configures the * `tasks` option of `handleMcpRequest`, a modern client may invoke * this tool as a task-augmented `tools/call` and poll `tasks/get`. * The function then runs *after* the HTTP request, so it must be safe * to defer and must persist the gateway-issued idempotency key around * its side effect (see docs/tasks.md). */ taskSupport?: boolean | "forbidden" | "optional" | "required"; /** Optional display title advertised in `tools/list`. */ title?: string; /** MCP behavior hints advertised in `tools/list`. */ annotations?: McpToolAnnotations; /** Client-specific protocol metadata advertised in `tools/list`. */ _meta?: Record; /** Authentication schemes advertised in `tools/list`. */ securitySchemes?: McpToolSecurityScheme[]; /** * Icons a client may display next to this tool. Advertised verbatim in * `tools/list`; the gateway never dereferences an icon `src`. */ icons?: McpIcon[]; /** * Free-form metadata stored alongside the tool registration. The * component never inspects this; it is surfaced to the host's * authorize callback as `args.toolMetadata` so per-tool scope/role * checks stay declarative. Use whatever shape your callback expects, * e.g. `{ scopes: ["finance:read"], roles: [...] }`. */ metadata?: Record; } /** * Declare a Convex `query` function as an MCP tool. The `fn` reference must * point to a `query`; passing a mutation or action is a compile error. * * `args` is checked against `FunctionArgs` at compile time, so a * drift between the registered Convex function and the tool descriptor * cannot ship undetected. * * Authorization is *not* configured per-tool. The host passes a single * `authorize` callback to `gateway.handleMcpRequest({ authorize })`; it * sees every `tools/call` (and every `tools/list` filter) and decides * whether to allow it. */ export declare function defineMcpQuery, ArgsV extends PropertyValidators, ReturnsV extends GenericValidator | undefined = undefined>(config: McpToolConfigBase & ValidateArgs & ValidateReturns): McpToolDefinition & { fn: Ref; kind: "query"; }; /** * Declare a Convex `mutation` function as an MCP tool. Mirrors * `defineMcpQuery`; the `fn` reference must point to a mutation * (passing a query or action is a compile error) and `args` is * checked against `FunctionArgs` at compile time. */ export declare function defineMcpMutation, ArgsV extends PropertyValidators, ReturnsV extends GenericValidator | undefined = undefined>(config: McpToolConfigBase & ValidateArgs & ValidateReturns): McpToolDefinition & { fn: Ref; kind: "mutation"; }; /** * Declare a Convex `action` function as an MCP tool. Mirrors * `defineMcpQuery`; the `fn` reference must point to an action * (passing a query or mutation is a compile error) and `args` is * checked against `FunctionArgs` at compile time. Use * this for tools that perform external IO (fetch, third-party APIs) * or non-transactional work. */ export declare function defineMcpAction, ArgsV extends PropertyValidators, ReturnsV extends GenericValidator | undefined = undefined>(config: McpToolConfigBase & ValidateArgs & ValidateReturns): McpToolDefinition & { fn: Ref; kind: "action"; }; /** * Declare a concrete MCP resource. The returned provider can be passed to * `gateway.handleMcpRequest({ resources: [...] })`. * * This is intentionally a lightweight primitive: it gives resources the same * first-class declaration style as `defineMcpQuery` / `defineMcpMutation` / * `defineMcpAction`, while registry sync, audit, authorization hooks, resource * templates, and subscriptions remain separate feature layers. */ export declare function defineMcpResource(config: McpResourceConfig): McpResourceRegistration; export type McpResourceTemplateConfig = McpResourceTemplate & { /** * Optional server-side read handler for URIs that match `uriTemplate`. * When present, the gateway resolves matching `resources/read` requests by * calling this with the extracted `params` (concrete resources still take * precedence). When omitted, the template is listing-only: clients expand * it and read the concrete URI through another provider. */ read?: McpResourceTemplateReadHandler; }; /** * Declare an MCP resource template (RFC 6570). The returned provider can be * passed to `gateway.handleMcpRequest({ resourceTemplates: [...] })`: it is * advertised via `resources/templates/list`, and, when a `read` handler is * supplied, used to resolve `resources/read` requests whose URI matches * `uriTemplate` (concrete resources declared via `defineMcpResource` always * take precedence). * * Use a template when resources are parameterized (e.g. * `weather://{city}/current`); use `defineMcpResource` for a fixed, concrete * URI. Only simple level-1 `{var}` placeholders are supported; an * unsupported template throws here at definition time. */ export declare function defineMcpResourceTemplate(config: McpResourceTemplateConfig): McpResourceTemplateProvider; export declare class McpGateway { component: ComponentApi; constructor(component: ComponentApi); /** * Upsert a single tool by name. Prefer `register(ctx, tools[])` for * the declarative "this is the full registry" pattern; reach for * `registerTool` only in plugin systems that register tools at * runtime from disjoint code paths. Replacing a tool clears its * `metadata` so a stale field can't survive a re-registration. */ registerTool(ctx: RunMutationCtx, tool: McpToolRegistration): Promise; /** * Atomically replace the entire registry with the given list of * tools. Any tool currently in the registry whose name isn't in * `tools` is removed; named tools are upserted. Runs in a single * Convex mutation, so concurrent `tools/list` / `tools/call` * callers never observe a partial swap. * * Replace-always is the only semantics: an additive `register` * leaks stale registrations across deploys (the old tool stays * exposed forever unless you remember to call `unregisterTool`), * which is exactly the kind of silent drift this API exists to * prevent. If you need genuinely incremental upserts (e.g. plugin * systems that register tools at runtime from disjoint codepaths), * call `registerTool` directly per tool. */ register(ctx: RunMutationCtx, tools: McpToolRegistration[]): Promise; /** * Remove a single tool by name. Returns `true` if a row was deleted, * `false` if no tool with that name was registered. Prefer * `register(ctx, tools[])` for declarative cleanup; this method is * for runtime/plugin scenarios. */ unregisterTool(ctx: RunMutationCtx, name: string): Promise; /** * Upsert a single resource by URI. This stores catalog metadata only; * resource contents are still served by the resource provider passed to * `handleMcpRequest({ resources })`. */ registerResource(ctx: RunMutationCtx, resource: McpResourceDescriptor): Promise; /** * Atomically replace the entire resource registry with the given catalog. * Any resource currently in the registry whose URI is not in `resources` * is removed; matching URIs are upserted. This mirrors `register` for * tools, but persists metadata only, not read handlers or contents. */ registerResources(ctx: RunMutationCtx, resources: McpResourceDescriptor[]): Promise; /** * Remove a single resource by URI. Returns `true` if a row was deleted, * `false` if no resource with that URI was registered. */ unregisterResource(ctx: RunMutationCtx, uri: string): Promise; /** * List every tool currently in the registry, raw rows from the * component table. Useful for debugging or building admin UIs. * For the spec-compliant, authorize-filtered catalog that MCP * clients see, use the gateway's `tools/list` JSON-RPC method via * `handleMcpRequest` instead. */ listTools(ctx: RunQueryCtx): Promise<{ _creationTime: number; _id: string; authoredInputSchemaJson?: string; authoredOutputSchemaJson?: string; description: string; functionHandle: string; identityArg?: string; inputSchema: any; kind: "query" | "mutation" | "action"; metadata?: any; mrtrArgs?: { idempotencyKey: string; }; mrtrGated?: boolean; name: string; outputSchema?: any; protocolMetadata?: any; taskSupport?: boolean | "forbidden" | "optional" | "required"; }[]>; /** * List every resource currently in the registry, raw rows from the * component table. For the spec-compliant catalog that MCP clients see, * use `resources/list` via `handleMcpRequest`. */ listResources(ctx: RunQueryCtx): Promise<{ _creationTime: number; _id: string; description?: string; metadata?: any; mimeType?: string; name: string; uri: string; }[]>; /** * Upsert a single resource template by `uriTemplate`. Stores catalog * metadata only; matching reads are still served by a template provider * passed to `handleMcpRequest({ resourceTemplates })`. */ registerResourceTemplate(ctx: RunMutationCtx, template: McpResourceTemplate): Promise; /** * Atomically replace the entire resource-template registry with the given * catalog. Templates whose `uriTemplate` is not in `templates` are removed; * matching ones are upserted. Mirrors `registerResources`. */ registerResourceTemplates(ctx: RunMutationCtx, templates: McpResourceTemplate[]): Promise; /** * Remove a single resource template by `uriTemplate`. Returns `true` if a * row was deleted, `false` if none was registered. */ unregisterResourceTemplate(ctx: RunMutationCtx, uriTemplate: string): Promise; /** * List every resource template currently in the registry, raw rows from * the component table. For the spec-compliant catalog that MCP clients * see, use `resources/templates/list` via `handleMcpRequest`. */ listResourceTemplates(ctx: RunQueryCtx): Promise<{ _creationTime: number; _id: string; annotations?: any; description?: string; icons?: any; mimeType?: string; name: string; title?: string; uriTemplate: string; }[]>; /** * Inspect the audit log written by the component on every `tools/call`, * task lifecycle transition, and (when enabled) resource operation. * Returns newest entries first. Filter by `entryType` * (`"tool"` | `"resource"` | `"task"`), `toolName`, `resourceUri`, * `taskId`, and/or `outcome`; `limit` defaults to 100 and is capped * server-side at 1000. (The server applies one index per call; combining * `resourceUri` and `toolName` is not meaningful since a row has only one.) */ listAuditEntries(ctx: RunQueryCtx, args?: { entryType?: "tool" | "resource" | "task"; toolName?: string; resourceUri?: string; taskId?: string; outcome?: "allowed" | "denied" | "error"; limit?: number; }): Promise<{ _creationTime: number; _id: string; args: any; durationMs: number; entryType?: "tool" | "resource" | "task"; errorCode?: number; errorMessage?: string; identitySubject: string | null; outcome: "allowed" | "denied" | "error"; resourceOperation?: "list" | "read" | "templates_list"; resourceUri?: string; taskId?: string; taskOperation?: "create" | "input" | "cancel" | "complete" | "fail"; toolKind?: "query" | "mutation" | "action"; toolName?: string; }[]>; /** * Drop MCP sessions that have not been touched within `idleMs`. * Returns the number of rows deleted in this call (up to a * bounded batch size, ~200, to stay inside Convex's per-mutation * limits). Hosts on busy deployments loop until the return value * is `0`, or schedule a follow-up mutation if a single tick is * insufficient. The component does not garbage-collect sessions * on its own. */ pruneSessions(ctx: RunMutationCtx, idleMs: number): Promise; /** * Drop audit entries older than `retentionMs`. Returns the number * of rows deleted in this call (up to a bounded batch size, ~200, * to stay inside Convex's per-mutation limits). Callers loop * until the return value is `0` to fully drain. Schedule from * `crons.ts` for time-based retention: * * ```ts * crons.daily("audit cleanup", { hourUTC: 3, minuteUTC: 0 }, * internal.audit.runPrune, {}); * * export const runPrune = internalMutation({ * args: {}, * handler: async (ctx) => { * let total = 0; * for (;;) { * const n = await gateway.pruneAuditEntries(ctx, 30 * 24 * 60 * 60 * 1000); * total += n; * if (n === 0) break; * } * return total; * }, * }); * ``` */ pruneAuditEntries(ctx: RunMutationCtx, retentionMs: number): Promise; /** * Drop expired MRTR bookkeeping: one-time-redemption rows whose * continuation has expired, and resolved-chain claims past their * window. Both drain through this one call, so hosts wire a single * cron. A chain claim is deliberately written with the TTL ceiling * rather than the expiry of the continuation that resolved it: it has * to outlive every continuation of that chain, or pruning it would * let a still-valid sibling resolve the chain a second time. Draining * on a schedule is safe; shortening that window is not. * * Bounded per call; drains by looping until a call deletes nothing. * Wire it into the same cron as `pruneAuditEntries` when the `mrtr` * option is enabled. */ pruneMrtrRedemptions(ctx: RunMutationCtx): Promise; /** * Wipe the entire tool registry. Does **not** touch `config`, * `audit`, or `sessions`, only the `tools` table. Intended for * tests and one-shot deploy resets where you want the next * `register(ctx, [...])` to start from an empty registry. */ clearTools(ctx: RunMutationCtx): Promise; /** * Wipe the entire resource registry. Does not touch tools, config, * audit, or sessions. */ clearResources(ctx: RunMutationCtx): Promise; /** * Wipe the entire resource-template registry. Does not touch resources, * tools, config, audit, or sessions. */ clearResourceTemplates(ctx: RunMutationCtx): Promise; /** * List the session IDs currently subscribed to `uri` via * `resources/subscribe`. A host that fronts the gateway with a * push-capable transport reads this to decide whom to deliver a * `notifications/resources/updated` to. See the `resourceSubscriptions` * option on `handleMcpRequest`. Returned rows may reference sessions that * have since been pruned; treat unknown sessions as no-ops and run * `pruneResourceSubscriptions` to clean them. */ listResourceSubscribers(ctx: RunQueryCtx, uri: string): Promise; /** * Delete subscription rows whose session no longer exists (sessions * dropped by `pruneSessions` do not cascade their subscriptions). Drains * fully by paging through the table in bounded windows (each window is its * own component transaction) and returns the total number deleted. Wire it * alongside `pruneSessions` in a cron when you use resource subscriptions. */ pruneResourceSubscriptions(ctx: RunMutationCtx): Promise; /** * Trusted full read of one task row, for host executor / workflow code. * Unlike the wire-facing `tasks/get` it returns execution data (`args`, * `caller`, `idempotencyKey`) and is NOT owner-bound; never expose its * result to an MCP client without checking ownership. * * It also returns EXPIRED rows as-is, where every owner-facing function * and every trusted finalizer already treats the task as gone. Check * `expiresAt` before acting on a row, or a workflow that sees * `status: "working"` will have its `completeTask` answered * `"not_found"`. */ getTask(ctx: RunQueryCtx, taskId: string): Promise; /** * Mark a task completed with `result`. Called by the host's durable * execution (typically the last step of a `@convex-dev/workflow` run) * when the `tasks` option was configured with a custom `execute`. * * Returns `"finalized"`; `"not_found"` (no such task, or it expired * while you were working); `"conflict"` (already terminal, e.g. the * owner cancelled first, the cancel wins); or `"result_too_large"`, * which means the task WAS finalized but as `failed`, because the * result could not be stored. The last two both mean the client sees * something other than the work you just committed, so check the * outcome rather than discarding it. */ completeTask(ctx: RunMutationCtx, taskId: string, /** * Your tool's own return value. Do NOT hand-build a `CallToolResult`: * the gateway derives `content` / `structuredContent` / `isError` from * this value when a client polls, so the row keeps the value exactly * once and both executors converge on one wire shape. Whether the * envelope carries `structuredContent` is read from the tool's own * registration, not passed here: only you know whether your run * failed, and only the registry knows whether the tool advertises an * `outputSchema`. */ result: unknown, flags?: { /** * The call ran and reported a failure (a declined confirmation, a * validation error). Surfaces as `isError: true` on a COMPLETED * task, which is how the synchronous path reports the same thing; * `failTask` is for the call never producing a result at all. */ isError?: boolean; }): Promise<"conflict" | "not_found" | "finalized" | "result_too_large">; /** * Mark a task failed. `error.message` reaches the polling client * verbatim, so sanitize it like a tool result; pass the full exception * text as `auditErrorMessage` when it should land in the audit log * instead of on the wire. */ failTask(ctx: RunMutationCtx, taskId: string, error: { code: number; message: string; }, auditErrorMessage?: string): Promise<"conflict" | "not_found" | "finalized">; /** * Transition a `working` task to `input_required` with MRTR-shaped * `inputRequests`. The owner answers via `tasks/update`; the gateway * then surfaces the accepted responses through the `onInputResponses` * handler option so the host can resume its workflow. * * Anything other than `"updated"` means the task did NOT enter * `input_required`, so nothing will ask the owner and nothing will * resume: `"conflict"` (not `working` any more), `"invalid_requests"` * (the value is not a plain object, a host-side bug, deliberately * distinct from `"conflict"`), `"too_large"`, `"unsupported_executor"`, * or `"not_found"`. Throwing from `execute` on a non-`"updated"` answer * is the intended handling. Only valid for * host-executed tasks (`tasks.execute` configured): the built-in * executor runs once and could never resume, so component-executed * tasks answer `"unsupported_executor"`. */ requireTaskInput(ctx: RunMutationCtx, taskId: string, inputRequests: Record): Promise<"conflict" | "not_found" | "updated" | "invalid_requests" | "too_large" | "unsupported_executor">; /** * Drop expired task rows. Bounded per call; drains by looping until a * call deletes nothing. Wire it into the same cron as * `pruneAuditEntries` when tasks are enabled. */ pruneTasks(ctx: RunMutationCtx): Promise; /** * Cancel every live (non-terminal) task owned by `ownerSubject`, for * the revocation case: a deferred task executes with the identity * snapshot taken at creation, which stays valid until its TTL even if * the caller's access was revoked minutes later. Call this when a * subject is revoked to stop its pending tasks before they run. * Drains in bounded batches and returns the total cancelled plus the * ids. The host must still cancel any durable execution (workflow run) * for those ids itself, e.g. from its `onCancel` bookkeeping. * * Sweeps every mount by default, which is what a revocation means: the * subject's access is gone, not one mount's. Pass `scope` to narrow it * to the tasks a single mount created (see `tasks.scope` on * `handleMcpRequest`). */ cancelPendingTasksForOwner(ctx: RunMutationCtx, ownerSubject: string, scope?: string): Promise<{ cancelled: number; taskIds: string[]; }>; /** * Build a `notifications/resources/list_changed` JSON-RPC notification for * the host to deliver over its own transport when the resource catalog * changes. The gateway does not deliver it (its HTTP transport cannot * push); see the `resourceSubscriptions` option on `handleMcpRequest`. */ buildResourceListChangedNotification(): { jsonrpc: "2.0"; method: "notifications/resources/list_changed"; }; /** * Build a `notifications/resources/updated` notification for `uri`, for the * host to deliver to that resource's subscribers (see * `listResourceSubscribers`). The payload carries only the URI; clients * re-read via `resources/read`, which re-applies authorization. */ buildResourceUpdatedNotification(uri: string): { jsonrpc: "2.0"; method: "notifications/resources/updated"; params: { uri: string; }; }; /** * Configure OAuth 2.1 protected-resource discovery so MCP clients can * find the authorization server that issues their Bearer tokens. * * Once set, `tools/call` responses with `-32001 Unauthorized` switch * to HTTP 401 with a `WWW-Authenticate: Bearer resource_metadata=...` * header. The host must additionally mount the discovery handler at * the canonical RFC 9728 path on its own `httpRouter`; see * `serveProtectedResourceMetadata`. * * `resourceUrl` is optional; when omitted the discovery handler * derives the resource from the inbound request URL, which is correct * for single-tenant deployments. Pass `authServerUrl: null` to disable * discovery again. Both URLs are validated as absolute http/https URLs * at write time; an invalid value throws `ConvexError` immediately. */ setOAuthConfig(ctx: RunMutationCtx, config: { authServerUrl: string | null; resourceUrl?: string | null; }): Promise; /** * Handle an MCP HTTP request (POST/GET/DELETE on `/mcp/`). Hosts mount * this on their own `httpRouter`; the component's HTTP routes have no * `ctx.auth` per Convex's component-isolation model, so the protocol * surface lives here on the client side instead. * * The host supplies an `authorize` callback that runs in the host's * action context (so `ctx.auth.getUserIdentity()` works). The * callback decides per `tools/call` and per tool in `tools/list`. * * Pass `tools` to declare the catalog inline (recommended): the * gateway reconciles the registry on `initialize`, so you change the * list in code and it just applies on the next connect, no separate * registration mutation to run. The reconcile is change-detected: it * fingerprints the list and only rewrites the registry when something * actually changed, so the steady-state cost per connection is a * single cheap lookup. The imperative `gateway.register(...)` mutation * stays available for dynamic/plugin catalogs. * * ```ts * import { httpRouter } from "convex/server"; * import { httpAction } from "./_generated/server.js"; * import { gateway, tools } from "./mcp.js"; * import { authorize } from "./authorize.js"; * * const http = httpRouter(); * const mcp = httpAction(async (ctx, req) => * gateway.handleMcpRequest(ctx, req, { authorize, tools }), * ); * http.route({ path: "/mcp/", method: "POST", handler: mcp }); * http.route({ path: "/mcp/", method: "GET", handler: mcp }); * http.route({ path: "/mcp/", method: "DELETE", handler: mcp }); * export default http; * ``` */ handleMcpRequest(ctx: RunMutationCtx & { runAction: (ref: any, args: any) => Promise; auth: { getUserIdentity: () => Promise; }; }, request: Request, options: HandleMcpRequestOptions): Promise; /** * Serve the RFC 9728 protected-resource metadata document. Hosts mount * this on their own `httpRouter` at the canonical well-known path: * * ```ts * import { httpRouter } from "convex/server"; * import { httpAction } from "./_generated/server.js"; * import { gateway } from "./mcp.js"; // or wherever you build it * * const http = httpRouter(); * http.route({ * pathPrefix: "/.well-known/oauth-protected-resource", * method: "GET", * handler: httpAction(async (ctx, request) => * gateway.serveProtectedResourceMetadata(ctx, request), * ), * }); * export default http; * ``` * * The host mounts this route alongside the `/mcp/` route from * `handleMcpRequest`; RFC 9728 §3.1 mandates the metadata at * `/.well-known/oauth-protected-resource`. The * component itself does not own any HTTP routes (Convex does not * propagate `ctx.auth` into component code, so all routes live in * the host). * * Returns `404` when no OAuth config has been set via `setOAuthConfig`. */ serveProtectedResourceMetadata(ctx: RunQueryCtx, request: Request): Promise; /** * Serve RFC 8414 OAuth Authorization Server Metadata, wrapping an * upstream IdP. Fetches the upstream's openid-configuration once * per process (in-memory cached), copies the relevant fields, and * substitutes our own `registration_endpoint` so MCP clients DCR * against `handleClientRegistration` instead of the upstream. * * Mount on the host: * * ```ts * http.route({ * path: "/.well-known/oauth-authorization-server", * method: "GET", * handler: httpAction(async (ctx, request) => * gateway.serveAuthorizationServerMetadata(ctx, request, { * upstreamIssuer: "https://id.example.com", * }), * ), * }); * ``` */ serveAuthorizationServerMetadata(_ctx: unknown, request: Request, options: { upstreamIssuer: string; /** Path to your `handleClientRegistration` route. Default: `/oauth/register` */ registrationPath?: string; /** * Advertise Client ID Metadata Documents (CIMD) when the upstream * authorization server declares support. This is intentionally opt-in: * the bridge does not fetch or validate a client's metadata document, * so it can only expose CIMD when the upstream authorization endpoint * performs that validation itself. DCR remains advertised as a * backwards-compatible fallback. */ clientIdMetadataDocuments?: boolean; /** * Fields to override in the bridged metadata. Useful for: * * - Removing `openid` from `scopes_supported` (when the client * would otherwise request an `id_token` and reject it because * the upstream's `iss` claim won't match the bridge's * advertised `issuer`). * - Restricting `response_types_supported` to `["code"]` to * force pure-OAuth code flow (no `id_token` hybrid). * - Setting `issuer` to the upstream issuer instead of the * bridge origin, if the client refuses to accept the * mismatch (technically violates RFC 8414 §2 but works with * stricter clients). * * Any key set here replaces the bridged value verbatim. Keys * not set fall through to the upstream's value (or our default). */ overrides?: Record; }): Promise; /** * Serve RFC 7591 Dynamic Client Registration, returning a fixed * pre-registered upstream client id for every request. This is the * "fake DCR" that lets browser MCP clients (which insist on DCR) * connect to upstream IdPs that don't support DCR. * * **`allowedRedirectPatterns` is required** to prevent open-redirect * attacks: without it any caller could "register" a client with an * attacker-controlled `redirect_uri` and steal auth codes. * * Mount on the host: * * ```ts * http.route({ * path: "/oauth/register", * method: "POST", * handler: httpAction(async (ctx, request) => * gateway.handleClientRegistration(ctx, request, { * upstreamClientId: "", * allowedRedirectPatterns: [ * /^https:\/\/claude\.ai\//, * /^https:\/\/claude\.com\//, * /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?\//, * ], * }), * ), * }); * ``` */ handleClientRegistration(_ctx: unknown, request: Request, options: { upstreamClientId: string; allowedRedirectPatterns: RegExp[]; }): Promise; } export default McpGateway; //# sourceMappingURL=index.d.ts.map