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.
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
# 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. 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.
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
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:
// 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 |
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 functions deploy mcp-server