mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Renames the `mcp-server` Library block to `mcp`. Installing it now creates `supabase/functions/mcp`, so the server is served at `/functions/v1/mcp`. - Block, Edge Function folder, and docs page renamed (`/docs/headless/mcp`) - Headless App block now installs its tools into `supabase/functions/mcp` and configures `[functions.mcp]` - Links in the BYO MCP and MCP authentication guides updated - Permanent redirects keep `/r/mcp-server.json` and `/docs/headless/mcp-server` working - `public/r` rebuilt <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * The MCP Server block is now named `mcp` across its documentation, installation links, and setup instructions. * Updated function endpoints and deployment commands to use `/mcp`. * Added permanent redirects from the previous `mcp-server` documentation and install URLs to their new locations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
225 lines
8.0 KiB
Plaintext
225 lines
8.0 KiB
Plaintext
---
|
|
title: MCP Server
|
|
description: Add a user-scoped MCP server to your product
|
|
preview: <BlockOverview name="mcp" showFiles />
|
|
---
|
|
|
|
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" 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.
|
|
|
|
## Configure the project
|
|
|
|
The function verifies access tokens itself, so disable the gateway JWT check:
|
|
|
|
```toml
|
|
[functions.mcp]
|
|
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/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/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/.env.example`. Copy it before
|
|
serving locally, then customize the name and description:
|
|
|
|
```bash
|
|
cp supabase/functions/mcp/.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
|
|
deno task check
|
|
cd ../../..
|
|
supabase functions serve mcp --env-file supabase/functions/.env
|
|
```
|
|
|
|
Then deploy:
|
|
|
|
```bash
|
|
supabase config push
|
|
supabase secrets set --env-file supabase/functions/.env
|
|
supabase functions deploy mcp
|
|
```
|
|
|
|
## 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)
|