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
  • anon public 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 supabase

Verify:

supabase --version
# Expect: 2.x

3. Scaffold the repo

mkdir shared-skills-mcp && cd shared-skills-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're now able to push migrations to the cloud project and deploy functions to it.

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

  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; the npm:/pkg@version/ form 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 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 unauthenticated

8. 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 mcp

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