# MCP Server

Add a user-scoped MCP server to your product

## Files

- `supabase/`
  - `functions/`
    - `mcp/`
      - `.env.example`
      - `deno.json`
      - `deno.lock`
      - `index.ts`
      - `tools/`
        - `index.ts`
        - `result.ts`
        - `types.ts`
        - `whoami.ts`

Full source: https://supabase.com/library/r/mcp.json

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

Install this block:

```bash
npx shadcn@latest add @supabase/mcp
```

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](https://supabase.com/library/docs/nextjs/oauth-consent.md),
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](https://supabase.com/library/docs/nextjs/oauth-consent.md)
- [`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)
