Supabase library MCP blocks
Add typed repositories to the MCP server and headless app blocks from the Supabase library, or replace their tools with createMcp.
The Supabase library ships two blocks that put
an MCP server for your users into your project: the
MCP server block
(@supabase/mcp) and the
headless app
template (@supabase/headless-app-tanstack), which adds sign-in and OAuth
consent pages around the same server. Both install an Edge Function in
supabase/functions/mcp that runs every tool as the signed-in user. They are
shadcn registry items from Supabase, not better-supabase blocks.
better-supabase fits in three ways, from the smallest change to the largest:
- Keep the block and add typed repositories to its pipeline.
- Replace the block's tools with
createMcp, which generates tools from your tables. - Keep an official SDK server and add
better-supabase/mcp/sdk.
Requirements
Both blocks need the same project settings. Use Supabase CLI 2.117 or later: it supplies asymmetric signing keys locally and passes the function slug to the Edge Function, so the URLs in the OAuth metadata are the public ones.
- Asymmetric signing keys (ES256 or RS256). Locally, run
better-supabase keysand point[auth] signing_keys_pathat the file; on the hosted project, rotate to an asymmetric key under JWT Keys. - The gateway's JWT check turned off for the function. The function verifies
tokens itself, and the gateway's own 401 has no
WWW-Authenticatechallenge, so MCP clients could not discover how to sign in. - The Supabase Auth OAuth server, so external MCP clients can sign users in. It sends users to a consent page served from the Auth Site URL: the headless app's own, or one in your app (see OAuth consent for the page and a connected-agents list that revokes grants).
[functions.mcp]
verify_jwt = false
[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = trueallow_dynamic_registration lets any compatible client register itself; set
it to false if you register clients yourself. Get an existing project's
config, schema and Edge Functions into the repo first with supabase pull
(Supabase CLI 2.119 or later), then add a block:
npx shadcn@latest add @supabase/mcpThe function imports better-supabase through its deno.json:
{
"imports": {
"better-supabase": "npm:better-supabase@^0.6",
"better-supabase/": "npm:/better-supabase@^0.6/"
}
}Typed repositories in the block's pipeline
The block's index.ts is a pipeline from @supabase/middleware:
withOAuthProtectedResource() serves the RFC 9728 metadata, and
withSupabase({ auth: 'user' }) verifies the token and builds a user-scoped
client. Add withBetterDb(betterSupabase)() from
better-supabase/server after withSupabase, and pass the ctx.db it adds
to the tools:
import { withBetterDb } from "better-supabase/server";
import { betterSupabase } from "../_shared/supabase.ts";
// ...the block's imports, createServer and CORS_HEADERS stay as they are.
Deno.serve(
pipeline(
[
withOAuthProtectedResource(),
withSupabase({ auth: "user", cors: { headers: CORS_HEADERS } }),
withBetterDb(betterSupabase)(),
],
(request, ctx) =>
createMcpHandler(() =>
createServer({
supabase: ctx.supabase,
db: ctx.db,
userClaims: ctx.userClaims!,
jwtClaims: ctx.jwtClaims!,
}),
).fetch(request),
),
);ctx.db holds repositories bound to ctx.supabase, so every query still
goes through RLS, and their context comes from the verified claims (see
@supabase/server pipelines). Add it to the block's
ToolContext:
import type { betterSupabase } from "../../_shared/supabase.ts";
export type ToolContext = {
supabase: SupabaseClient;
db: ReturnType<typeof betterSupabase.connect<SupabaseClient>>;
userClaims: NonNullable<SupabaseContext["userClaims"]>;
jwtClaims: NonNullable<SupabaseContext["jwtClaims"]>;
};Then a tool reads typed rows:
import type { McpServer } from "npm:@modelcontextprotocol/server@2.0.0";
import { errorResult, jsonResult } from "./result.ts";
import type { ToolContext } from "./types.ts";
export function registerCustomersTools(
server: McpServer,
{ db }: ToolContext,
): void {
server.registerTool(
"list_customers",
{
description: "List the customers the signed-in user can see.",
annotations: { readOnlyHint: true, openWorldHint: false },
},
async () => {
const result = await db.customers.findMany({
select: ["id", "name", "status"],
limit: 50,
});
return result.ok
? jsonResult(result.data)
: errorResult(result.error.message);
},
);
}Register it in tools/index.ts next to registerWhoamiTool. Repository
methods return a Result instead of throwing, so the tool turns a DbError
into an MCP error result the model can read; the block's
runtimeErrorResult stays the helper for code that throws.
_shared/supabase.ts exports betterSupabase = defineSupabase(schema) from
the client better-supabase gen writes.
withBetterSupabase from better-supabase/server is a pipeline entry that
resolves the caller itself (see Middleware); next to
the block's withSupabase use withBetterDb as above. For servers on the
official SDK, withBetterSupabaseMcp from better-supabase/mcp/sdk wraps
McpServer.registerTool for
createMcpAuth.
Replacing the tools with createMcp
createMcp replaces both withOAuthProtectedResource and withSupabase in
index.ts. It serves the MCP endpoint, the RFC 9728 metadata and the
WWW-Authenticate challenge itself, answers CORS preflights, and generates
tools from your tables. On Edge Functions it derives the public URL from the
function slug and the gateway's forwarded headers, as the block does, and
serves the metadata at /functions/v1/mcp/oauth-protected-resource. Keep the
block's .env.example, add the imports above to deno.json, and replace
index.ts:
import { createMcp } from "better-supabase/mcp";
import { betterSupabase } from "../_shared/supabase.ts";
const bs = createMcp(betterSupabase, {
name: Deno.env.get("MCP_SERVER_NAME") ?? "app",
version: "1.0.0",
instructions: Deno.env.get("MCP_SERVER_DESCRIPTION"),
resources: { customers: { select: ["id", "name", "status"] } },
});
Deno.serve(bs.fetch);The headless app's consent page, connected-agents page and tasks table keep
working; only the tool code changes. See
MCP servers for custom tools, authorize and scopes,
and OAuth consent to build those two pages in an
app that doesn't use the headless template.
To keep the block's McpServer and its tools instead, use createMcpAuth
and withBetterSupabaseMcp from better-supabase/mcp/sdk. They need
@modelcontextprotocol/server 2.3 or later, and the block pins 2.0.0, so
bump the import in every tool file first.
Schema and deploy
The headless app keeps its tables in supabase/schemas. Generate the
migration with pg-delta, then deploy:
supabase db schema declarative sync -f create_tasks
supabase link --project-ref <project-ref>
supabase db push
supabase config push
supabase secrets set --env-file supabase/functions/.env
supabase functions deploy mcpsupabase config pull brings settings changed in the dashboard back into
config.toml. Run better-supabase doctor before the
push to check grants, RLS and the Auth hook.
Last updated on