Docs
MCP Server

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

  • supabase
    • functions
      • mcp-server
        • tools
# 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 = 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.

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 = 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:

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 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

VariableDefaultPurpose
MCP_SERVER_NAMEsupabase-mcpServer name shown to MCP clients
MCP_SERVER_DESCRIPTIONGeneric sentenceInstructions 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/.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:

cd supabase/functions/mcp-server
deno task check
cd ../../..
supabase functions serve mcp-server --env-file supabase/functions/.env

Then deploy:

supabase config push
supabase secrets set --env-file supabase/functions/.env
supabase functions deploy mcp-server

Further reading