Files
supabase/apps/ui-library/content/docs/headless/mcp-server.mdx
Saxon Fletcher b04e26872b feat(ui-library): add MCP server block (#49573)
## 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. Bottom of a two-PR stack; #49579
builds on it.

## What is the new behavior?

Adds an `mcp-server` block: a Supabase Edge Function that exposes MCP
tools scoped to the signed-in user. It is backend-only, so every file
has an explicit target and no `components.json` is needed.

- `withSupabase({ auth: 'user' })` verifies the access token and gives
each tool an RLS-scoped client. Both product session tokens and OAuth
tokens work; only the latter carry `client_id`.
- `withOAuthProtectedResource` serves RFC 9728 metadata and adds a
`WWW-Authenticate` challenge to `401`s, so external MCP clients can
discover the authorization server.
- Tools are composed in `tools/index.ts`. One is included, `whoami`,
which shows the caller's identity and OAuth client.

Docs at `/library/docs/headless/mcp-server`, under a new MCP group in
the sidebar. `BlockItem` gained a `showOpenInV0` flag (v0 cannot take
Deno functions), and the file-tree viewer now picks a language per file
instead of always TypeScript.

## To test

1. `npx shadcn@latest add
http://localhost:3004/library/r/mcp-server.json` into a Supabase project
or empty directory.
2. Add `[functions.mcp-server] verify_jwt = false` to
`supabase/config.toml`, then:
   ```bash
   supabase start
supabase functions serve mcp-server --env-file supabase/functions/.env
   ```
3. **Unauthenticated:** `curl -i
localhost:54321/functions/v1/mcp-server` returns `401` with a
`WWW-Authenticate` header, and
`/functions/v1/mcp-server/oauth-protected-resource` returns the metadata
document.
4. **Product session:** sign up a user, then call the endpoint with
`Authorization: Bearer <their access token>`. `tools/list` shows
`whoami`; calling it returns that user's id and `client_id: null`.
5. **External client:** enable `[auth.oauth_server]` with
`allow_dynamic_registration = true`, install the OAuth Consent block,
point an MCP client (Claude Code, Codex) at the function URL, approve
the consent screen, and call `whoami` again. `client_id` is now
populated.
6. Confirm RLS holds: add a table with a user-scoped policy and a tool
that reads it, then check a second user cannot see the first user's
rows.
7. Docs page renders at `/library/docs/headless/mcp-server`, and
`deno.json` / `.env.example` in the folder tree highlight as JSON and
bash rather than TypeScript.

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

- **New Features**
- Added an installable Supabase MCP Server block with user-scoped
authentication and a read-only identity tool.
  - Added MCP Blocks to documentation navigation and setup guidance.
- Code blocks now automatically detect syntax highlighting from file
names.
  - Added an option to hide the “Open in v0” button.

- **Documentation**
- Expanded MCP Server guidance covering installation, configuration,
validation, deployment, OAuth, and security.
<!-- 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>
2026-09-02 14:53:38 +10:00

188 lines
6.3 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.
## 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. Projects that still use the
legacy HS256 secret do not expose signing keys from the JWKS endpoint, so the
function 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).
## 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
`withOAuthProtectedResource` 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.
`withSupabase({ auth: 'user' })` verifies the JWT and provides 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.
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 |
## 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 functions deploy mcp-server
```
## Further reading
- [OAuth Consent block](../nextjs/oauth-consent)
- [OAuth protected resource middleware](https://supabase.com/docs/reference/server/middleware-withoauthprotectedresource)
- [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)