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

Source: https://bettersupabase.com/docs/guides/supabase-blocks

The [Supabase library](https://supabase.com/library) ships two blocks that put
an MCP server for your users into your project: the
[MCP server](https://supabase.com/library/docs/headless/mcp) block
(`@supabase/mcp`) and the
[headless app](https://supabase.com/library/docs/tanstack/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](/docs/blocks).

better-supabase fits in three ways, from the smallest change to the largest:

1. Keep the block and add typed repositories to its pipeline.
2. Replace the block's tools with [`createMcp`](/docs/frameworks/mcp), which
   generates tools from your tables.
3. Keep an official SDK server and add
   [`better-supabase/mcp/sdk`](/docs/frameworks/mcp#official-mcp-sdk).

## Requirements [#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 keys`](/docs/cli/local#keys) and point
  `[auth] signing_keys_path` at 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-Authenticate`
  challenge, 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](/docs/auth/oauth-consent) for the page and a
  connected-agents list that revokes grants).

```toml title="supabase/config.toml"
[functions.mcp]
verify_jwt = false

[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = true
```

`allow_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:

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

The function imports better-supabase through its `deno.json`:

```json title="supabase/functions/mcp/deno.json"
{
  "imports": {
    "better-supabase": "npm:better-supabase@^0.6",
    "better-supabase/": "npm:/better-supabase@^0.6/"
  }
}
```

## Typed repositories in the block's pipeline [#typed-repositories-in-the-blocks-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:

```ts title="supabase/functions/mcp/index.ts"
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](/docs/auth/middleware)). Add it to the block's
`ToolContext`:

```ts title="supabase/functions/mcp/tools/types.ts"
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:

```ts title="supabase/functions/mcp/tools/customers.ts"
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`](/docs/cli/gen) writes.

`withBetterSupabase` from `better-supabase/server` is a pipeline entry that
resolves the caller itself (see [Middleware](/docs/auth/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`](/docs/frameworks/mcp#official-mcp-sdk).

## Replacing the tools with createMcp [#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`:

```ts title="supabase/functions/mcp/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](/docs/frameworks/mcp) for custom tools, `authorize` and scopes,
and [OAuth consent](/docs/auth/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 [#schema-and-deploy]

The headless app keeps its tables in `supabase/schemas`. Generate the
migration with pg-delta, then deploy:

```bash
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 mcp
```

`supabase config pull` brings settings changed in the dashboard back into
`config.toml`. Run [`better-supabase doctor`](/docs/cli/doctor) before the
push to check grants, RLS and the Auth hook.