Chumbo turns an existing Supabase application into a Streamable HTTP MCP server
running as a Supabase Edge Function. Your application keeps its Auth, Postgres
data, Row Level Security, Storage, and authorization model. You choose what
agents can do. Chumbo handles the MCP layer around it.
Start in one command
From a repository that already contains supabase/config.toml:
npx chumbo setup
MCP made easy on Supabase.
2026-11-14
星數
—
工具
—
可用率
—
For agent-assisted development, install the version-matched project skill too:
npx chumbo skill install
It gives the agent the ordinary setup, capability, auth, result, local proof,
deployment, and upgrade paths without replacing your project instructions.
Setup asks who may connect, previews every file it will write, generates the
Edge Function and tests, and reports the remaining deployment or OAuth steps in
order. It is resumable and does not overwrite application-authored
capabilities.
Requirements: Node 22+, the Supabase CLI, and preferably Deno for the generated
local type-check and tests.
Finish Chumbo Cloud setup with your agent
After Chumbo Cloud installs the observation plane and shows “one code change
left,” run this from the same Supabase repository:
npx chumbo cloud setup
The CLI shows a short pairing code and opens Cloud for approval. Approval is
bound to the selected project and this CLI instance; it does not copy a browser
session or Supabase credential to the terminal. Chumbo then finds the approved
MCP Edge Function, previews a bounded analytics-hook change, and stops for
confirmation. Deployment remains explicit:
npx chumbo cloud setup --deploy --yes
For agents and automation, add --json. Pairing information is written to
stderr while the final machine-readable receipt stays on stdout. JSON mode
never prompts; without --yes, it returns the exact proposed additions with a
needs_confirmation status. Use --plan to inspect the same local file change
without writing it.
Edit the generated capabilities.ts. Chumbo uses the official MCP SDK's
registration API, so your capabilities remain ordinary MCP tools, Resources,
and prompts.
import {
collectionInputSchema,
collectionResult,
type SupabaseMcpContext,
type SupabaseMcpServer,
} from "chumbo";
import { z } from "zod";
export function registerCapabilities(
server: SupabaseMcpServer,
ctx: SupabaseMcpContext,
) {
const taskSummary = z.object({
id: z.string().uuid(),
title: z.string(),
status: z.string(),
});
server.registerTool(
"list_tasks",
{
description:
"Browse tasks visible to the connected user in stable ID order.",
inputSchema: collectionInputSchema({ cursorSchema: z.string().uuid() }),
},
async ({ limit, cursor }) => {
let query = ctx.supabase
.from("tasks")
.select("id, title, status")
.order("id")
.limit(limit + 1);
if (cursor) query = query.gt("id", cursor);
const { data, error } = await query;
if (error) throw error;
return collectionResult({
items: data ?? [],
limit,
hasMore: false,
itemSchema: taskSummary,
project: ({ id, title, status }) => ({ id, title, status }),
cursorFor: ({ id }) => id,
tool: "list_tasks",
arguments: cursor ? { cursor } : {},
render: ({ items }) =>
items.length
? items
.map((task) => `- ${task.title} – ${task.status} (${task.id})`)
.join("\n")
: "No tasks are visible to the connected user.",
});
},
);
}
The important part is ctx.supabase. With Supabase user tokens, it is a fresh
client carrying the connected user's access token. The same Postgres grants
and RLS policies used by the rest of the application apply to every tool call.
With an application-owned OAuth verifier, ctx.supabase is anonymous and
capability code must enforce the caller's application permissions.
You choose the application operations worth exposing and shape each result for
its real consumer. Chumbo handles the protocol and request-authority boundary
around that application code.
Project one capability into a branded CLI
A capability can carry an optional command projection while remaining an
ordinary MCP tool. Define and register it inside the request-scoped
registerCapabilities function so the handler retains the same ctx.supabase
user and RLS authority:
import {
defineCapability,
registerCapability,
structuredResult,
type SupabaseMcpContext,
type SupabaseMcpServer,
} from "chumbo";
import { z } from "zod";
export function registerCapabilities(
server: SupabaseMcpServer,
ctx: SupabaseMcpContext,
) {
registerCapability(
server,
defineCapability({
id: "tasks.get",
mcpName: "get_task",
title: "Get task",
description: "Get one task visible to the signed-in user.",
inputSchema: z.object({ id: z.string().uuid() }),
outputSchema: z.object({ id: z.string(), title: z.string() }),
scopes: ["tasks:read"],
risk: "read",
idempotent: true,
cli: { command: ["tasks", "get"] },
async handler({ id }) {
const { data, error } = await ctx.supabase
.from("tasks")
.select("id, title")
.eq("id", id)
.single();
if (error) throw error;
return structuredResult(data);
},
}),
);
}
Authenticated MCP clients still discover get_task. A project-branded CLI can
render the same visible tool as acme tasks get --id ..., call the same MCP
endpoint, and return human output or a stable --json receipt. Tools registered
directly with server.registerTool() remain available through the explicit
acme run <tool-name> --args '{}' fallback.
The Node-only chumbo/cli-host entry provides browser PKCE login, OS-keychain
credential storage partitioned by authorization-server issuer, authenticated
command discovery, logout, and fail-closed write confirmation. The
chumbo/cli-package entry renders a tiny project-owned npm package whose
package name, binary, display name, endpoint, support URL, and optional
“powered by” attribution are configuration. Render options can also set bounded
license, npm access, and canonical repository metadata. Rendering does not
publish a package or put Chumbo Cloud in the application's data path.
Run, deploy, and verify
Start Supabase, serve the generated function, then prove the real MCP boundary
before deploying. Keep chumbo dev running in one terminal:
supabase start
npx chumbo dev --function mcp
For public mode, run supabase migration up --local after supabase start
and before serving the function so the generated local rate limiter is ready.
For generated API-key mode, put MCP_API_KEY in the gitignored file
supabase/functions/.env.local and add
--env-file supabase/functions/.env.local to the chumbo dev command.
In another terminal, run the generated contract test and invoke the starter:
deno task --config supabase/functions/mcp/deno.json test
npx chumbo doctor \
--function mcp \
--url http://127.0.0.1:API_PORT/functions/v1/mcp \
--call-tool whoami
Use the exact Local MCP URL printed by chumbo dev. API_PORT comes from
[api].port in supabase/config.toml and defaults to 54321 when omitted.
Add --token <MCP_API_KEY> to doctor for generated API-key mode or
--token <LOCAL_USER_JWT> for bearer or OAuth mode. Chumbo does not add a local
authentication bypass. Doctor reports initialization, tool discovery, and the
explicit tool call separately. If the stack or function is stopped, it prints
the next recovery command. The locally served files are the same files deployed
below.
The generated function sets verify_jwt = false at the Supabase gateway so the
function can issue the MCP OAuth challenge itself. Protected servers still
authenticate 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 RLS by default; application authority with a custom verifier
API key
You want the shortest authenticated start or already maintain application keys.
Application subject and scopes; ctx.supabase uses the anon role
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:
Start with OAuth for an end-user product and API key for a prototype or trusted
machine caller. One endpoint can also compose Supabase-user and application-key
strategies without merging their identities or database behavior.
An application with its own OAuth authorization server can set issuer and
supply an oauth verifier. Chumbo passes the token, canonical resource URL, and
configured issuer to verify({ token, resourceUrl, issuer }). The verifier must
check the issuer, exact resource, expiry, and revocation, then return a subject,
optional client ID and scopes, and the expiry in Unix seconds. Return null
for an invalid token. Chumbo exposes an anonymous Supabase client for these
requests; it does not send the opaque MCP bearer to Supabase. Omitting verify
retains the ordinary Supabase JWT path.
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 bearer
clients send their credential as an Authorization: Bearer header.
For claude.ai or Claude Desktop, open Settings → Connectors → Add custom
connector and paste the endpoint URL. Hosted custom connectors require OAuth
with dynamic client registration enabled.
Cursor, MCP Inspector, and other Streamable HTTP clients use the same endpoint.
See Connect your MCP client for exact setup
and verified combinations.
Host it where your app runs
A Chumbo app is a web-standard fetch handler. The Supabase Edge Function is
the default home, not a requirement: your Supabase project stays authoritative
for auth and data wherever the handler runs.
npx chumbo setup --target next # App Router route handler in your Next.js app
npx chumbo setup --target node # standalone server for Cloud Run, Fly, Railway
--target next generates a colocated app/mcp/ scaffold whose route handler
serves /mcp and its OAuth discovery suffixes alongside the rest of your
application. --target node generates a server entry that listens on PORT
through chumbo/node. Both share the same capabilities.ts seam, access
modes, and result contracts as the Edge Function path, and
npx chumbo doctor --url <MCP_URL> verifies any of them.
Host targets covers environment configuration,
deployment verification, and when to prefer a proxy to the Edge Function
instead.
Request isolation. Every request receives a new MCP server, normalized
principal, and Supabase client. Caller identity never lives in shared mutable
module state.
Deliberate authentication. Supabase users receive an RLS-aware client.
Application keys retain their application-owned subject and scopes.
Rotation-safe verification. Supabase-token OAuth and bearer requests use
Supabase's public JWKS. Remote JWKS configuration is cached briefly per
runtime to avoid adding a key-network round trip to every MCP request while
still observing signing-key rotation quickly. An application-owned OAuth
verifier handles its issuer's token validity and revocation.
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, and
usable non-interactively by agents and CI. doctor verifies the real remote
MCP boundary.
No required Chumbo service. The runtime deploys into an ordinary Supabase
project. Public mode's default guardrail is Postgres-backed.
The boundary stays simple:
MCP client
↓
Supabase Edge Function
↓
fresh request-scoped identity and mode-appropriate Supabase client
↓
your capabilities, application checks, grants, and RLS policies
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
appendResultText(result, text)
Optional model-facing guidance after a successful authored result
prependResultText(result, text)
Optional model-facing context before a successful authored result
Shape each result around the consumer's next reasoning or interaction step.
Preserve useful identifiers, omit internal fields, and use Resources or
pagination for large payloads.
Successful results can carry an optional follow-up without rebuilding their
structured data or metadata:
return appendResultText(
structuredResult({ draftId: draft.id }),
"Optional follow-up: call review_draft with this draftId when you want to review it.",
);
For cross-cutting guidance, resultMiddleware may return bounded prepend or
append content for successful tools. Every middleware receives the same
read-only authored-result snapshot, and a middleware failure leaves that
result unchanged and reaches onError with phase: "results".
This guidance is ordinary model-facing tool-result content. It is not a system
message and cannot require the client to call another tool.
The capability and result showcase
keeps tools, Resources, prompts, elicitation, and all result patterns
executable without loading them into the generated starter.
Observe capability execution
Add onEvent when your application needs audit, usage, or operational data.
Chumbo emits versioned capability.started and capability.finished events for
invoked tools, Resources, and prompts. Each event contains the request trace,
server and capability identity, normalized principal and authentication,
timestamp, and terminal outcome. Arguments, results, credentials, and thrown
exception text are excluded by construction.
The sink is optional and application-owned. Chumbo observes a returned promise
for failure but does not await it, so a slow or unavailable sink never changes
the MCP response. Use the deployment platform's background-work primitive when
delivery must continue after the response. Sink failures reach onError with
phase: "events" and never recursively produce another event.
Capture the advertised tool surface
Add onSurface when your application needs durable evidence of the tool
catalog an authenticated client can actually discover:
Chumbo calls the sink only after a complete successful tools/list. The
versioned proof contains normalized tool names, descriptions, supported
annotations, input and output schemas, truthful server/runtime/auth metadata,
the requested protocol version when available, and a stable SHA-256 content
digest. Tools disabled for the current request are
absent, so protected callers can prove different effective surfaces without
exporting the caller, scopes, credentials, headers, arguments, results,
prompts, errors, cursors, or arbitrary _meta.
The callback is optional and application-owned. Its returned promise is
observed but not awaited, and failures reach onError with phase: "surface"
without changing discovery. When onSurface is absent, the runtime performs no
request cloning, response inspection, hashing, delivery, account, or network
work for surface proofs.
Correlate an application run
Some products need several tool calls to belong to one application-defined run
or work order. Configure createRunCorrelation only for that advanced case:
A builder-authored begin tool can call runs.mint(ctx) and return its opaque
handle. Generic MCP clients pass that handle through run_id only on the
tools that deliberately expose the field. A client you control may instead
send the same handle in _meta["dev.chumbo/run"]. Matching carriers are
accepted. Disagreement or an invalid handle stops before application code.
When configured, lifecycle events use schema v2 and contain the same bounded
opaque run fact or run: null. Without runCorrelation, Chumbo continues to
emit lifecycle v1 exactly as before. A run handle is correlation, not
authorization or execution. Auth, scopes, grants, RLS, and your application's
data-plane checks remain authoritative.
Opt into small durable state
Most Chumbo servers should remain stateless. An authenticated capability that
genuinely needs request-to-request coordination can explicitly generate one
allowlisted namespace:
The runtime derives an opaque partition from the exact credential with a
deployment-secret HMAC and keeps its service-role state client
closure-confined. Public mode never receives state. Same-project storage is the
default. Advanced compositions can set state.supabase.env to keep receipts in
a separate Supabase 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 retain
RLS or an atomic application-level version precondition for real mutations.
See Observation before action for
the complete executable read-before-edit pattern, safe cross-database ordering,
credential-rotation behavior, and split-project runbook. This is coordination
storage, not a resident actor or Durable Object runtime.
Advanced patterns
The ordinary path remains one Edge Function with builder-authored capabilities.
The same library also supports more demanding applications without changing
that starting point:
Project-local capability guidance with npx chumbo skill install
These are composition patterns, not additional frameworks or required product
architecture.
Reference project
This repository includes an open-source Supabase reference project. Its
patterns run through the real MCP transport against local Postgres. The suite
covers two-user RLS isolation, explicit result contracts, many row-defined MCP
surfaces, composed user and application identities, and interactive MCP Apps.
Its tools search Chumbo's own guides and return complete documents through MCP
Resources. It links to official Supabase documentation for the platform
underneath instead of reproducing it.
To rebuild the reference project from a clean clone:
Chumbo 0.11 adds collection building blocks. They make compact, navigable pages
easy to implement while you keep control of queries, authorization and response
meaning. Existing tools and single-record helpers keep their current behavior.
The list_tasks capability above demonstrates the default text contract.
Defaults are 20 records per page, at most 100, and 16 KiB for the complete
serialized result. Fetch one extra record to detect continuation. The helper
bounds the returned prefix by count and bytes and includes an exact next call
from the last returned cursor. Supply safe filter arguments when your query
has filters. A first item that cannot fit returns an explicit recoverable error;
add onOversizedItem to point to your implemented detail tool or Resource.
Text is the default and requires a deliberate renderer. For typed clients use
mode: "structured" with collectionOutputSchema(taskSummary); for both consumers
use mode: "hybrid" plus that output schema. Both lanes describe the same page.
Additive result middleware also respects the complete collection budget.
The builder owns semantic cursor validation and stable ordering. Each page uses
the current caller's authority; a cursor does not grant access or promise a
snapshot. The shipped result-design guide
covers filters, live-data consistency, details, mutation receipts and recovery.
For automation, use npx chumbo setup --plan --json to inspect changes and
--yes --json to apply them without prompts. Run npx chumbo --help for the
complete command reference.