1. Deploy
supabase db push # if you haven't already
supabase functions deploy mcp --no-verify-jwt--no-verify-jwt turns off Supabase's platform-level JWT check. That check expects a Supabase auth token on every call and would 401 our token-in-a-header requests before they ever reached requireTeamToken. Our config.toml already sets verify_jwt = false; the flag makes it explicit at deploy time.
Confirm the secrets are set (from step 4) and the health check is live:
supabase secrets list # expect PROJECT_URL, SERVICE_ROLE_KEY, TEAM_TOKEN
curl https://<ref>.supabase.co/functions/v1/mcp/health
# {"ok":true}And confirm the gate holds in production:
# No token → plain 401
curl -i https://<ref>.supabase.co/functions/v1/mcp
# Right token → gets past the gate
curl -i -X POST \
-H "Authorization: Bearer <the-team-token>" \
-H "Content-Type: application/json" \
https://<ref>.supabase.co/functions/v1/mcp2. Connect Claude Code
The token rides in a header, not the URL:
claude mcp add team-skills \
--transport http \
https://<ref>.supabase.co/functions/v1/mcp \
--header "Authorization: Bearer <the-team-token>"(No trailing slash — the route is exact.) Verify:
claude mcp list
# team-skills: connectedThen, inside a Claude session:
"Save this as a snippet called 'PR review checklist', tag it
review."Claude calls
save_snippet→ "Saved 'PR review checklist'.""What snippets do we have tagged
review?"Claude calls `list_snippets({ tag: "review" }) → lists it back.
That's the whole loop — no browser, no sign-in.
3. Connect Claude Desktop
Claude Desktop adds remote servers as connectors (Settings → Connectors → Add custom connector), and its UI currently has no field for a custom header. Two options:
- Token on the URL — paste
https://<ref>.supabase.co/functions/v1/mcp?token=<the-team-token>. This is exactly the "append the token to the MCP URL" convenience, and our middleware accepts it. The cost: the token now sits in the URL, so it can appear in server access logs, proxies, and local history. Fine for an internal tool; think twice before it's anything sensitive. mcp-remoteproxy — for a header instead of a URL token, run the remote server behind a local stdio proxy that injects the header:{ "command": "npx", "args": [ "-y", "mcp-remote", "https://<ref>.supabase.co/functions/v1/mcp", "--header", "Authorization: Bearer <the-team-token>" ] }
For Claude Code, always prefer the header (step 2). The URL token exists for clients that can't do headers.
4. Hand it to the team
This is the part that makes it a team server: send everyone the same one-liner.
claude mcp add team-skills --transport http \
https://<ref>.supabase.co/functions/v1/mcp \
--header "Authorization: Bearer <the-team-token>"Send the token over a channel you'd send any shared secret over — a password manager, a vault, an encrypted DM. Not a public channel, not a committed file, not a screenshot in the demo you're about to post.
5. Rotating the token
Because it's one shared secret, rotation is all-or-nothing — which is the price of the simplicity:
NEW=$(openssl rand -base64 32)
supabase secrets set TEAM_TOKEN="$NEW"
supabase functions deploy mcp --no-verify-jwt # redeploy to pick it upThen everyone re-runs claude mcp add (or edits their config) with the new token. Do this if the token leaks, when someone leaves the team, or on a schedule if you're cautious. There's no way to revoke one person without rotating for all — if that's a dealbreaker, you've outgrown Tier A.
6. What this design protects — and what it doesn't
Be able to say this plainly to anyone you hand the server to:
It protects against: the open internet. Without the token, the function returns 401 and the table is unreachable (RLS-locked, service-role-only). Someone would need the token to read or write anything.
It does not protect against: each other. Everyone with the token is the same caller. There are no private snippets, no per-person audit trail (the author field is a self-declared label, not proof), and anyone can delete anything. The blast radius of a leaked token is the entire library.
A short hardening checklist for a real internal deployment:
- Token is long and random (
openssl rand -base64 32), shared over a secret channel, never committed. - Service-role key is only in function secrets — never in client code, never in the repo.
- RLS is enabled on
snippets— re-run the anon-key read from step 3; it must return[]. - Prefer the header over
?token=wherever the client allows it, so the token stays out of logs. - Logs don't echo snippet bodies. The Hono
logger()logs method + path, not bodies — keep it that way, or snippet contents end up in log retention. - You've written down the rotation steps (section 5) somewhere the team can find them.
7. When to graduate to OAuth
Move to the Shared-Skills (OAuth) build when any of these become true:
- You want private snippets or per-person visibility.
- You need to revoke one person without disrupting everyone.
- You want a real "who wrote this" backed by identity, not a label.
- The team grows past "everyone here is trusted with everything."
The data your snippets live in is portable — same title/body/tags shape — so migrating later is mostly adding the identity columns and the OAuth front door, not rebuilding.
8. What you built
- A remote MCP server on Supabase Edge Functions, deployed and connectable from any machine.
- A single shared-token gate with a constant-time check, accepting a header or a URL token.
- One Postgres table, RLS-locked so only the token-guarded function can reach it.
- Four tools (
list/get/save/delete_snippet) and asnippet://{id}resource. - A clear-eyed account of the security trade you made, and the path to the stronger version when you outgrow it.
Fifteen minutes of setup for a shared prompt library the whole team can talk to. That's the win.