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_role secret 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 supabase

Verify:

supabase --version
# Expect: 2.x

3. Scaffold the repo

mkdir team-token-mcp && cd team-token-mcp
git init
supabase init

supabase init creates a supabase/ directory with config files and migrations. Add a .gitignore:

cat > .gitignore <<'EOF'
.env
.env.local
node_modules/
.supabase/
EOF
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 mcp

That 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:

  1. The trailing-slash entries need npm:/ — slash after the colon. The keys ending in / are what make subpath imports like @modelcontextprotocol/sdk/server/mcp.js resolve. Deno rejects the plain npm:pkg@version/ form for these; npm:/pkg@version/ is the one that works.
  2. 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 first

8. Serve locally and verify

Make sure Docker Desktop is running — the local functions runtime is containerized — then:

supabase functions serve

In 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-4

supabase 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 mcp

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