1. Create the Supabase project
Sign in at supabase.com, click New Project, pick an organization, and choose a region close to you. Wait ~90 seconds for it to provision. On the project page, copy down:
- Project URL:
https://<ref>.supabase.co(Settings → API) - Project ref: the
<ref>part — you'll use it with the CLI service_rolesecret key: Settings → API → Project API keys →service_role
Unlike the OAuth version, we do use the service role key here — it's how the function reaches the RLS-locked table. Treat it like a password: it goes into function secrets only, never anywhere client-side. (More on why in step 3.)
2. Install the Supabase CLI
# macOS
brew install supabase/tap/supabase
# Or via npm
npm install -g supabaseVerify:
supabase --version
# Expect: 2.x3. Scaffold the repo
mkdir team-token-mcp && cd team-token-mcp
git init
supabase initsupabase init creates a supabase/ directory with config files and migrations. Add a .gitignore:
cat > .gitignore <<'EOF'
.env
.env.local
node_modules/
.supabase/
EOF4. Link the local repo to your cloud project
supabase login # opens browser, authenticates the CLI
supabase link --project-ref <your-project-ref>You can now push migrations and deploy functions to the cloud project.
5. Create the Edge Function
supabase functions new mcpThat creates supabase/functions/mcp/index.ts with a tiny Deno starter. We'll replace it shortly.
6. Install the MCP SDK + Hono via an import map
Edge Functions run on Deno, so dependencies come from URLs or npm: specifiers. Create supabase/functions/import_map.json:
{
"imports": {
"@modelcontextprotocol/sdk": "npm:/@modelcontextprotocol/sdk@1.29.0",
"@modelcontextprotocol/sdk/": "npm:/@modelcontextprotocol/sdk@1.29.0/",
"hono": "npm:hono@4.12.27",
"hono/": "npm:/hono@4.12.27/",
"@supabase/supabase-js": "npm:@supabase/supabase-js@^2.45.0",
"zod": "npm:zod@^3.23.0"
}
}Two details will bite you if you get them wrong:
- The trailing-slash entries need
npm:/— slash after the colon. The keys ending in/are what make subpath imports like@modelcontextprotocol/sdk/server/mcp.jsresolve. Deno rejects the plainnpm:pkg@version/form for these;npm:/pkg@version/is the one that works. - The MCP SDK and Hono are pinned exactly (
1.29.0,4.12.27), not with^ranges. The MCP SDK is evolving fast, and a caret range can silently pull in a release that changes the server API.
There's no jose here — the OAuth build needed it to verify JWTs, and we have none. zod validates tool inputs.
7. Replace the function stub with a Hello-MCP
Open supabase/functions/mcp/index.ts and replace it with:
import { Hono } from "hono";
import { logger } from "hono/logger";
// Supabase serves this function at /functions/v1/mcp/..., and the function
// itself sees paths prefixed with its own name — so every route hangs
// off a "/mcp" base path.
const app = new Hono().basePath("/mcp");
app.use("*", logger());
app.get("/health", (c) => c.json({ ok: true, ts: Date.now() }));
app.all("*", (c) => c.json({ message: "MCP server placeholder" }));
Deno.serve(app.fetch);The .basePath("/mcp") is load-bearing: without it, Hono matches /health while the platform hands your function /mcp/health, and every route 404s.
Tell the CLI to use the import map by adding to supabase/config.toml:
[functions.mcp]
import_map = "./functions/import_map.json"
verify_jwt = false # we check the shared token ourselves; the platform's
# default JWT check would 401 every request first8. Serve locally and verify
Make sure Docker Desktop is running — the local functions runtime is containerized — then:
supabase functions serveIn another terminal:
curl http://127.0.0.1:54321/functions/v1/mcp/health
# {"ok":true,"ts":1782000000000}That confirms Deno + Hono + the import map all work together. A "module not found" error usually means the path in config.toml is wrong — it's relative to the supabase/ directory.
9. Local environment variables
Create supabase/functions/.env (gitignored by Supabase's defaults):
PROJECT_URL=https://<your-ref>.supabase.co
SERVICE_ROLE_KEY=<your-service-role-key>
TEAM_TOKEN=placeholder-we-set-a-real-one-in-step-4supabase functions serve auto-loads these. We'll use SERVICE_ROLE_KEY in step 3's client and TEAM_TOKEN in step 4's middleware.
Why not SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY? Names starting with SUPABASE_ are reserved: the runtime injects its own values and supabase secrets set refuses to override them. Worse, when you serve locally the injected SUPABASE_URL points at the local stack, not your cloud project. Our own names sidestep all of it.
10. Smoke test: deploy the placeholder
supabase functions deploy mcpLook for Deployed Function mcp on project <ref>, then:
curl https://<ref>.supabase.co/functions/v1/mcp/health
# {"ok":true,"ts":...}You now have a linked project, a function that builds and deploys, and the MCP SDK, Hono, zod, and supabase-js available via the import map. Step 3 adds the one table this whole thing stores.