Files
supabase/apps/ui-library/content/docs/headless/mcp-server.mdx
19d7233580 feat(ui-library): add headless app block for TanStack Start (#49579)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature — a new UI Library block. Stacked on #49573 (already in main)

Fixes AI-1064

## What is the new behavior?

Adds `headless-app-tanstack`: customers sign in, authorize an MCP
client, and use the product through agent tool calls. It composes the
existing Password-Based Auth, OAuth Consent, and MCP Server blocks.

- `/agents` provides a copyable connection prompt, lists OAuth
authorizations, and lets customers revoke access.
- The shared MCP runtime exposes `whoami` plus example task CRUD tools.
Tools use the caller's Supabase client, with database grants and RLS
enforcing ownership.
- A root-level `supabase/` directory supplies local Auth/OAuth
configuration, a declarative tasks schema, and Edge Function files,
including `.env.example`.
- Docs cover local setup, signing keys, migrations, environment
configuration, deployment, and extending the tools.
`/example/headless-app` previews the sign-in, consent, connect, and
connected states.

Shared block fixes make a fresh install work:

- Explicit public URL resolution fixes OAuth discovery in local Edge
Runtime when middleware runtime detection fails. Both external OAuth
access tokens and ordinary authenticated app session tokens remain
supported; embedded agents do not need an additional consent flow.
- Registry targets keep backend files outside `src/`, and generated
consumer routes omit source-only TypeScript suppressions.
- Signup respects `auth.email.enable_confirmations`; sign-in/signup
preserve the return destination. Missing consent IDs retain the existing
error state without serializing `null` into the URL.

## How to test

Use the UI Library on **staging** and follow the block pages'
instructions.

1. Open the **Headless App** block page for TanStack Start. Install it
into a fresh app and follow the setup instructions through connecting an
MCP client.
2. Sign up, open `/agents`, and use the connection prompt to authorize a
client. Call `whoami`, then create, list, update, and delete a task.
3. Confirm the client appears on `/agents`. Revoke access and verify it
disappears and token refresh fails. An existing access token can
continue working until it expires.
4. Follow the **MCP Server** block page's embedded-agent instructions
using an authenticated app session. Confirm tools work without another
OAuth consent flow and `whoami` returns `client_id: null`.
5. With a second user, confirm each user can only access their own
tasks. Check that signup behaves correctly for the configured
email-confirmation setting.
6. Check the Headless App preview states and run the installed app's
typecheck and production build.

## Validation performed

Fresh local installation and browser/SDK verification passed: 26 live
MCP/Data API checks, 10 Deno tests, and 7 connection-page component
tests. Also passed UI Library typecheck, targeted lint,
registry/Markdown builds, and fresh consumer typecheck/production build.
Both OAuth and ordinary app session authentication were exercised.

Hosted deployment and consuming the confirmation-email link were not
tested.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a TanStack Headless App example with sign-in, OAuth consent, MCP
connection, and connected-agent screens.
- Added task management tools for listing, creating, updating, and
deleting tasks through MCP.
- Added connected-agent management, including server URL and prompt
copying, refresh, and access revocation.
  - Added a new Headless App registry block and documentation.

- **Bug Fixes**
- Preserved intended destinations through sign-up, email confirmation,
and protected-route login redirects.
- Improved OAuth discovery URL handling across forwarded-host
deployments.

- **Documentation**
- Updated setup, environment, deployment, and Supabase CLI guidance for
headless apps and MCP servers.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: repro <repro@local>
Co-authored-by: Raúl Barroso <code@raulb.dev>
2026-09-14 10:30:26 +10:00

228 lines
8.1 KiB
Plaintext

---
title: MCP Server
description: Add a user-scoped MCP server to your product
---
Give embedded product agents and external clients such as Codex, Claude Code,
and ChatGPT secure, user-scoped access to your product through MCP tools. This
block runs as a Supabase Edge Function, verifies Supabase user access tokens,
and gives every tool an RLS-scoped client.
## Installation
<BlockItem name="mcp-server" showOpenInV0={false} />
Installs Deno Edge Function files into a Supabase project or empty directory. No
`components.json` is required. Backend files stay in `supabase/` at the project
root even when the frontend uses `src/`.
In a frontend app, add `"supabase/functions/**"` to the app's `tsconfig.json` `exclude`
list, preserving existing entries. Check the function separately with Deno.
## Folder structure
<RegistryBlock itemName="mcp-server" />
## Configure the project
The function verifies access tokens itself, so disable the gateway JWT check:
```toml
[functions.mcp-server]
verify_jwt = false
```
The project must sign JWTs with an asymmetric key. `withSupabase` verifies user
tokens against the project JWKS and rejects legacy HS256 tokens, so a project
that still uses the legacy secret cannot authenticate embedded product sessions
or external MCP clients. Switch to an ES256 or RS256 key in
[JWT Keys](https://supabase.com/dashboard/project/_/settings/jwt).
Use Supabase CLI 2.117.0 or later. It supplies asymmetric signing keys for local
development and injects the function slug into the Edge Function, so the URL
advertised in the OAuth discovery metadata is canonical whatever path a request
arrives on.
## Choose how agents authenticate
### Embedded product agents
A trusted product backend can forward its signed-in user's Supabase access
token as `Authorization: Bearer <token>`. This reuses the product session, so
the user does not need to authorize their own product again.
Keep the token inside your backend or agent orchestrator. Never place it in a
prompt or expose it directly to a model provider.
### External MCP clients
External clients authenticate with OAuth, so users approve and revoke each
client separately. Install the [OAuth Consent block](../nextjs/oauth-consent),
then enable OAuth in `supabase/config.toml`:
```toml
[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = true
```
Set the Auth **Site URL** to the origin that serves `/oauth/consent`. Use HTTPS
in production. Run `supabase config push` or restart the local stack to apply the
change.
`allow_dynamic_registration` lets any compatible client register itself. Set it
to `false` if you register clients yourself.
## Authentication
The function is a `pipeline` from `@supabase/middleware` with two entries from
`@supabase/server`, in this order:
```ts
Deno.serve(
pipeline(
[withOAuthProtectedResource(), withSupabase({ auth: 'user', cors: { headers: CORS_HEADERS } })],
handleMcp
)
)
```
`withOAuthProtectedResource()` runs before the auth gate. It serves RFC 9728
metadata at `/functions/v1/mcp-server/oauth-protected-resource` and adds a
`WWW-Authenticate` challenge to `401` responses so MCP clients can discover the
authorization server. On Edge Functions it derives the public URLs itself, locally
and hosted; off Edge Functions pass `resourceServer` and `authorizationServer`.
`withSupabase({ auth: 'user' })` is the gate. It verifies the JWT and hands
`handleMcp` an RLS-scoped client. It accepts both product session tokens and
OAuth access tokens. OAuth tokens include `client_id`; ordinary product sessions
do not. The included `whoami` tool exposes that difference.
Composing `withSupabase` as a `pipeline` entry is alpha in `@supabase/server`
and tracks `@supabase/middleware` 0.x. The nested form,
`withOAuthProtectedResource(withSupabase(config, handleMcp))`, is stable and
behaves the same.
Any holder of a valid user token can call this function directly. Treat its
tools as an authenticated product API: keep RLS enabled, check authorization for
business operations, and do not add admin clients to the shared tool context.
OAuth scopes control identity, not database or tool access. Use `client_id` for
client-specific policies when it is present, and define the intended behavior
for product sessions where it is null. Never use user-editable metadata for
authorization decisions.
## Add tools
Each tool module exports one registration function:
```ts
// supabase/functions/mcp-server/tools/tasks.ts
import type { McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'
import { z } from 'npm:zod@4.4.3'
import { jsonResult, runtimeErrorResult } from './result.ts'
import type { ToolContext } from './types.ts'
export function registerTasksTools(server: McpServer, { supabase }: ToolContext): void {
server.registerTool(
'close_task',
{
description: 'Mark a task as closed.',
inputSchema: z.object({ id: z.string().uuid() }),
annotations: { readOnlyHint: false, idempotentHint: true },
},
async ({ id }) => {
try {
const { data, error } = await supabase
.from('tasks')
.update({ closed: true })
.eq('id', id)
.select()
if (error) throw error
return jsonResult(data)
} catch (error) {
return runtimeErrorResult(error)
}
}
)
}
```
Then add one call in `tools/index.ts`, the server's composition point:
```ts
import type { McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'
import { registerTasksTools } from './tasks.ts'
import type { ToolContext } from './types.ts'
import { registerWhoamiTool } from './whoami.ts'
export function registerTools(server: McpServer, context: ToolContext): void {
registerWhoamiTool(server, context)
registerTasksTools(server, context)
}
```
Each registration function receives:
- `supabase`, a user-scoped client for Database, Auth, Storage, and Functions
- `userClaims`, the normalized signed-in user identity
- `jwtClaims`, including `client_id` when the caller used OAuth
The context deliberately excludes `supabaseAdmin`. The MCP SDK rejects duplicate
tool names, and `jsonResult` returns both structured data and a text fallback for
older clients.
For typed table and column autocomplete, generate `database.types.ts` and make
the `SupabaseClient` in `tools/types.ts` a `SupabaseClient<Database>`.
## Environment
| Variable | Default | Purpose |
| ------------------------ | ---------------- | -------------------------------- |
| `MCP_SERVER_NAME` | `supabase-mcp` | Server name shown to MCP clients |
| `MCP_SERVER_DESCRIPTION` | Generic sentence | Instructions shown to clients |
The block includes `supabase/functions/mcp-server/.env.example`. Copy it before
serving locally, then customize the name and description:
```bash
cp supabase/functions/mcp-server/.env.example supabase/functions/.env
```
Add `supabase/functions/.env` to `.gitignore`. Supabase supplies the project URL,
API keys, and function slug to Edge Functions automatically; OAuth discovery
combines the slug with the public origin the gateway forwards to advertise the
function's public URL.
## Deploy
Check the function before serving or deploying it:
```bash
cd supabase/functions/mcp-server
deno task check
cd ../../..
supabase functions serve mcp-server --env-file supabase/functions/.env
```
Then deploy:
```bash
supabase config push
supabase secrets set --env-file supabase/functions/.env
supabase functions deploy mcp-server
```
## Further reading
- [OAuth Consent block](../nextjs/oauth-consent)
- [`withOAuthProtectedResource` and `pipeline` composition](https://github.com/supabase/server/blob/main/docs/api-reference.md)
- [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication)
- [OAuth 2.1 server](https://supabase.com/docs/guides/auth/oauth-server/getting-started)
- [Token security and RLS](https://supabase.com/docs/guides/auth/oauth-server/token-security)
- [OAuth grant management](https://supabase.com/docs/guides/auth/oauth-server/oauth-flows#managing-user-grants)
- [Edge Functions](https://supabase.com/docs/guides/functions)