
Chumbo
MCP made easy on Supabase.
Chumbo turns an existing Supabase application into a Streamable HTTP MCP serverrunning as a Supabase Edge Function. Your application keeps its Auth, Postgresdata, Row Level Security, Storage, and authorization model. MCP becomes anotherinterface to the product you already built.
MCP client
↓
Supabase Edge Function
↓
request-scoped identity and Supabase client
↓
your capabilities, Postgres data, and RLS policies
Start in one command
From a repository that already contains supabase/config.toml:
npx chumbo setup
Setup asks who may connect, previews every file it will write, generates theEdge Function and tests, and reports the remaining deployment or OAuth steps inorder. It is resumable and does not overwrite application-authoredcapabilities.
Requirements: Node 22+, the Supabase CLI, and preferably Deno for the generatedlocal type-check and tests.
The generated server lives at:
supabase/functions/mcp/
├── index.ts
├── capabilities.ts
├── deno.json
├── index_test.ts
└── README.md
Write one capability
Edit the generated capabilities.ts. Chumbo uses the official MCP SDK'sregistration API, so your capabilities remain ordinary MCP tools, Resources,and prompts.
import {
textResult,
type SupabaseMcpContext,
type SupabaseMcpServer,
} from "chumbo";
import { z } from "zod";
export function registerCapabilities(
server: SupabaseMcpServer,
ctx: SupabaseMcpContext,
) {
server.registerTool(
"list_tasks",
{
description: "List tasks visible to the connected user.",
inputSchema: z.object({}),
},
async () => {
const { data, error } = await ctx.supabase
.from("tasks")
.select("id, title, status")
.order("title");
if (error) throw error;
if (!data?.length) {
return textResult("No tasks are visible to the connected user.");
}
return textResult(
[
`## Tasks – ${data.length}`,
...data.map(
(task) => `- **${task.title}** – ${task.status} · ID: ${task.id}`,
),
].join("\n"),
);
},
);
}
The important part is ctx.supabase. In OAuth and bearer modes, it is a freshclient carrying the connected user's access token, so the same Postgres grantsand RLS policies used by the rest of the application apply to every tool call.
You choose the application operations worth exposing and shape each result forits real consumer. Chumbo handles the MCP and request-authority boundary aroundthat application code.
Run, deploy, and verify
Run the generated checks and exercise MCP discovery locally:
supabase functions serve mcp
deno task --config supabase/functions/mcp/deno.json test
npx chumbo doctor --url http://127.0.0.1:54321/functions/v1/mcp
Then deploy and probe the hosted endpoint:
supabase functions deploy mcp --no-verify-jwt
npx chumbo doctor \
--url https://PROJECT_REF.supabase.co/functions/v1/mcp
The generated function sets verify_jwt = false at the Supabase gateway so thefunction can issue the MCP OAuth challenge itself. Protected servers stillauthenticate the request inside the Chumbo runtime.
Your MCP URL is:
https://PROJECT_REF.supabase.co/functions/v1/mcp
Choose who can connect
| Access mode | Use it when | Request authority |
|---|---|---|
| OAuth | Your users should connect their own accounts. Recommended for a user-facing product. | Supabase user token and existing RLS |
| API key | You want the shortest authenticated start or already maintain application keys. | Application-verified subject and scopes |
| Bearer | Your own client already holds a Supabase user access token. | Supabase user token and existing RLS |
| Public | The capability is intentionally anonymous. | Supabase anon role plus a generated Postgres rate-limit guardrail |
Run npx chumbo setup interactively, or choose directly:
npx chumbo setup --auth oauth
npx chumbo setup --auth api-key
npx chumbo setup --auth bearer
npx chumbo setup --auth public
Start with OAuth for an end-user product and API key for a prototype or trustedmachine caller. One endpoint can also compose Supabase-user and application-keystrategies without merging their identities or database behavior.
Choose an access mode explains the tradeoffs andDifferent capability surfaces showsordinary and privileged identities receiving different MCP surfaces from oneEdge Function.
Connect a real client
For Claude Code:
claude mcp add --transport http my-app \
https://PROJECT_REF.supabase.co/functions/v1/mcp
OAuth mode opens the application's sign-in and consent flow. API-key and bearerclients send their credential as an Authorization: Bearer header.
For claude.ai or Claude Desktop, open Settings → Connectors → Add customconnector and paste the endpoint URL. Hosted custom connectors require OAuthwith dynamic client registration enabled.
Cursor, MCP Inspector, and other Streamable HTTP clients use the same endpoint.See Connect your MCP client for exact setupand verified combinations.
What Chumbo handles
- Supabase-native authority. Auth, RLS, Postgres, Storage, and EdgeFunctions remain authoritative.
- Request isolation. Every request receives a new MCP server, normalizedprincipal, and Supabase client. Caller identity never lives in shared mutablemodule state.
- Deliberate authentication. Supabase users receive an RLS-aware client.Application keys retain their application-owned subject and scopes.
- Rotation-safe verification. OAuth and bearer requests use Supabase'spublic JWKS. Remote JWKS configuration is cached briefly per runtime to avoidadding a key-network round trip to every MCP request while still observingsigning-key rotation quickly.
- Explicit result contracts. Agent-facing text, typed data, hybrids, andlarge Resources are separate choices rather than automatic duplicated output.
- Protocol-native capabilities. Tools, Resources, prompts, instructions,and multi-round-trip flows use the official MCP SDK surface.
- Deployable defaults. Setup is previewable, resumable, conflict-aware, andusable non-interactively by agents and CI.
doctorverifies the real remoteMCP boundary. - No required Chumbo service. The runtime deploys into an ordinary Supabaseproject. Public mode's default guardrail is Postgres-backed.
Choose the result for its consumer
| Helper | Use it for |
|---|---|
textResult(text) |
Purpose-written output for agents and people |
structuredResult(value) |
Typed clients or UI consumers; declare the matching tool outputSchema |
renderResult(value, render) |
A deliberate text and structured-data hybrid |
resourceResult(text, link) |
A concise reading card whose full body is served through MCP Resources |
errorResult(message, nextStep?) |
A failure that tells the agent how to recover |
Shape each result around the consumer's next reasoning or interaction step.Preserve useful identifiers, omit internal fields, and use Resources orpagination for large payloads.
Model-facing results contains executableexamples of all result patterns.
Opt into small durable state
Most Chumbo servers should remain stateless. An authenticated capability thatgenuinely needs request-to-request coordination can explicitly generate oneallowlisted namespace:
npx chumbo setup \
--auth oauth \
--state-namespace file-ide.observations
This adds one opt-in migration and state configuration. Apply the migration andset a unique deployment secret of at least 32 random bytes:
supabase db push
supabase secrets set \
CHUMBO_STATE_HMAC_KEY="replace-with-at-least-32-random-bytes"
Capability code then receives only get, revision-checked put, andrevision-checked delete:
const receipt = await ctx.state?.get(
"file-ide.observations",
`project:${projectId}:document:${documentId}`,
);
The runtime derives an opaque partition from the exact credential with adeployment-secret HMAC and keeps its service-role state client closure-confined.Public mode never receives state. Same-project storage is the default; advancedcompositions can set state.supabase.env to keep receipts in a separateSupabase project without moving authentication or ctx.supabase there.
State CAS protects coordination records, not application rows. Use immutable,scoped resource IDs, keep the capability's total keyspace bounded, and retainRLS or an atomic application-level version precondition for real mutations.
See Observation before action forthe complete executable read-before-edit pattern, safe cross-database ordering,credential-rotation behavior, and split-project runbook. This is coordinationstorage – not a resident actor or Durable Object runtime.
Optional depth when the application needs it
The ordinary path remains one Edge Function with builder-authored capabilities.The same library also supports more demanding applications without changingthat starting point:
- Many MCPs from one function
- Authenticated tools with RLS
- Observation before action
- Different capability surfaces
- Interactive MCP Apps on Supabase
- Clean client-facing URLs
- Project-local capability guidance with
npx chumbo skill install
These are composition patterns, not additional frameworks or required productarchitecture.
Reference project
This repository includes an open-source Supabase reference project. Itspatterns run through the real MCP transport against local Postgres. The suitecovers two-user RLS isolation, explicit result contracts, many row-defined MCPsurfaces, composed user and application identities, and interactive MCP Apps.
The public documentation MCP is available at:
https://dxrpeagddrpbezbkgvdv.supabase.co/functions/v1/docs-mcp
Its tools search Chumbo's own guides and return complete documents throughMCP Resources. It links to official Supabase documentation for the platformunderneath instead of reproducing it.
To rebuild the reference project from a clean clone:
pnpm install --frozen-lockfile
pnpm reference:check
Documentation
- Five-step getting started guide
- Choose an access mode
- Connect an MCP client
- Give an MCP a clean product URL
- Runnable patterns
- Examples
- Architecture and protocol contract
- Roadmap
- Changelog
For automation, use npx chumbo setup --plan --json to inspect changes and--yes --json to apply them without prompts. Run npx chumbo --help for thecomplete command reference.
Development
pnpm install --frozen-lockfile
pnpm check
pnpm format:check
pnpm reference:check
npm pack --dry-run
Released under the MIT License.