# MCP and agent connections

OAuth-authenticated agents use the existing Worker and the shared tool catalog.

## Connection flow [#connection-flow]

The MCP resource is `/api/mcp`. Better Auth provides OAuth 2.1, PKCE S256 and dynamic client registration. Protected-resource and authorization-server metadata live at the well-known endpoints; JWKS is `/api/auth/jwks`. Read the [connection guide](/docs/connect-your-agent) for client commands and supported scopes.

An unauthenticated request receives a Bearer challenge. `data:read` is required; `data:write` permits requesting writes, each still subject to exact User approval. Read-only grants do not see write tools. Revoking a connection rejects later access even before its JWT expires. The `email` scope lets UserInfo return `email` and `email_verified`, which OpenAI's plugin review expects.

`initialize` names the server after the OAuth issuer host, returns `instructions` that summarize the approval flow, and declares `listChanged: false` for tools, resources and prompts: the server is built per request and never publishes list changes. During an OpenAI plugin review, `/.well-known/openai-apps-challenge` serves the `OPENAI_APPS_CHALLENGE_TOKEN` Worker var as plain text; unset, the path is 404.

## Scope and approval [#scope-and-approval]

MCP is pinned to the User's personal Organization because an OAuth token does not carry the app's Active Organization. Tools check current membership. Writes return an approval request with an absolute `approvalUrl` (`/integrations?approval=<id>`) that opens Integrations on that record; `approval_status` lets the agent read the outcome, including the stored result of an executed approval. Requests expire after 15 minutes, with at most 20 unexpired pending records per User.

Every tool sets `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`. Reads are read-only and idempotent; writes are neither, because each call creates a new approval record; nothing is destructive or open-world. Write tools publish the approval envelope as their `outputSchema`.

Legacy draft tools create inert proposals. Approving proposal creation does not apply its domain changes; the separate Apply step remains. Replaying an executed approval returns the stored result.

## Resources and widgets [#resources-and-widgets]

The server adapts the same 16-tool catalog as the [assistant](/docs/features/assistant), subject to OAuth scope filtering. It exposes authorized Todo resources and planning prompts. `todo_list` advertises one MCP Apps widget (`ui://widget/todos/v2`, `text/html;profile=mcp-app`, an explicit empty CSP) under `_meta.ui.resourceUri` and the ChatGPT alias `openai/outputTemplate`; the view runs the `ui/initialize` → `ui/notifications/initialized` handshake and renders `ui/notifications/tool-result`. Hosts without iframe support still receive JSON. Public demo posts/profiles are separate static content, not private domain records.

`template://organization/documents/{fileId}` returns the first bounded JSON page of converted text. Continue through `document_read` with `nextCursor`. The `plan-from-document` prompt reads relevant pages and calls `draft_task_plan`; creating that proposal needs approval and its separate Apply step creates Todos. Document resources use the same membership and read-scope checks as tools. Files uploaded by external clients still need [browser text preparation](/docs/features/documents) before document tools can read them.

## Extend [#extend]

Add shared operations in the catalog, retain scope filtering and the in-process JWKS lookup, and use the server builder per connection. A Worker HTTP self-fetch for JWKS is not equivalent. Verify protocol errors, revoked tokens, insufficient scope and approval binding. Do not advertise an experimental MCP server-card endpoint as a stable discovery requirement.

## Key files [#key-files]

- [`packages/api/src/features/mcp/mcp.server.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/mcp/mcp.server.ts)
- [`packages/api/src/features/mcp/mcp.tools-ui.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/mcp/mcp.tools-ui.ts)
- [`packages/api/src/features/agent-tools/agent-tools.approval-status.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/agent-tools/agent-tools.approval-status.ts)
- [`packages/api/src/features/mcp/mcp.guard.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/mcp/mcp.guard.ts)
- [`packages/api/src/features/mcp/mcp.context.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/mcp/mcp.context.ts)
- [`packages/auth/src/mcp-oauth.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/auth/src/mcp-oauth.ts)
- [`apps/web/src/routes/api/mcp.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/routes/api/mcp.ts)

