1. Create the Supabase project
Sign in at supabase.com, click New Project, pick an organization, and choose a region close to you. Region matters less than you'd think for a low-traffic MCP server, but pick the same region you'd put the rest of your stack in for future compatibility.
Wait for the project to provision (about 90 seconds). On the project page, copy down:
- Project URL:
https://<ref>.supabase.co(under Settings → API) - Project ref: the
<ref>part of the URL — you'll use it with the CLI anonpublic key: under Settings → API → Project API keys
You don't need the service role key for this blueprint. Resist the temptation. We'll be running every database query as the calling user so RLS does its job.
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 shared-skills-mcp && cd shared-skills-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're now able to push migrations to the cloud project and deploy functions to it.
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-via-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",
"jose": "npm:jose@^5.9.0",
"zod": "npm:zod@^3.23.0"
}
}Two details in there 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; thenpm:/pkg@version/form 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 out from under a tutorial. Pin, and upgrade deliberately.
jose will verify Supabase JWTs in step 6. zod will validate tool inputs. The MCP SDK ships its TypeScript types via the NPM package.
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 would be matching /health while the platform hands your function /mcp/health, and every route would 404.
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 do auth inside the function; the platform-level
# check would also block the public OAuth metadata
# endpoints, which must be unauthenticated8. Serve locally and verify
Make sure Docker Desktop is running — the local functions runtime is containerized — then:
supabase functions serve(serve runs every function in your supabase/functions/ directory; there's only one right now.) 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. If you see a "module not found" error, double-check the path in config.toml — it's relative to the supabase/ directory.
9. Local environment variables
Create supabase/functions/.env (this file is gitignored by Supabase's defaults):
PROJECT_URL=https://<your-ref>.supabase.co
PROJECT_ANON_KEY=<your-anon-public-key>These get auto-loaded by supabase functions serve. We'll use them in step 7 to instantiate Supabase clients in request scope.
Why not SUPABASE_URL? Names starting with SUPABASE_ are reserved: the runtime injects its own values for them 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 — while our function needs to talk to the cloud project's Auth and Postgres. Using our own names sidesteps all of that, at the cost of setting two values ourselves.
10. Smoke test: deploy the placeholder
supabase functions deploy mcpLook for the line: Deployed Function mcp on project <ref>. Hit it:
curl https://<ref>.supabase.co/functions/v1/mcp/health
# {"ok":true,"ts":...}You now have:
- A linked Supabase project
- An Edge Function that builds, deploys, and responds
- The MCP SDK, Hono, jose, zod, and supabase-js available via the import map
That's the boring foundation. Step 3 adds the database schema for workspaces, members, and snippets — with the RLS policies that will do most of our security work.