MCP Server
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
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
# Copy this file to supabase/functions/.env before serving locally:
# cp supabase/functions/mcp-server/.env.example supabase/functions/.env
# supabase functions serve mcp-server --env-file supabase/functions/.env
# Deploy these values after linking your project:
# supabase secrets set --env-file supabase/functions/.env
# Supabase provides the project URL and API keys automatically.
# Keep the protocol-level server name short and project-specific.
MCP_SERVER_NAME=supabase-mcp
MCP_SERVER_DESCRIPTION="MCP access to this Supabase project for the signed-in user."Configure the project
The function verifies access tokens itself, so disable the gateway JWT check:
[functions.mcp-server]
verify_jwt = falseThe 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.
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,
then enable OAuth in supabase/config.toml:
[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = trueSet 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:
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:
// 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:
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 FunctionsuserClaims, the normalized signed-in user identityjwtClaims, includingclient_idwhen 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:
cp supabase/functions/mcp-server/.env.example supabase/functions/.envAdd 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:
cd supabase/functions/mcp-server
deno task check
cd ../../..
supabase functions serve mcp-server --env-file supabase/functions/.envThen deploy:
supabase config push
supabase secrets set --env-file supabase/functions/.env
supabase functions deploy mcp-server