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 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
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
The server adapts the same 16-tool catalog as the 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 before document tools can read them.
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
packages/api/src/features/mcp/mcp.server.tspackages/api/src/features/mcp/mcp.tools-ui.tspackages/api/src/features/agent-tools/agent-tools.approval-status.tspackages/api/src/features/mcp/mcp.guard.tspackages/api/src/features/mcp/mcp.context.tspackages/auth/src/mcp-oauth.tsapps/web/src/routes/api/mcp.ts