Deployment11 min read

Deploying MCP Servers to Supabase: Edge Functions, Postgres & Realtime

Learn how to deploy MCP servers to Supabase using Edge Functions. Complete guide covering Deno runtime, Postgres tool exposure, realtime subscriptions, and production deployment patterns for Supabase-hosted MCP infrastructure.

By MyMCPTools Team·

Supabase has become the default backend-as-a-service for full-stack developers — and its Edge Functions, Postgres database, and Realtime system make it a compelling deployment target for MCP servers. Running your MCP server on Supabase means zero server management, built-in Postgres integration, and global edge deployment close to your users.

This guide walks through everything you need to deploy a production-ready MCP server on Supabase, from initial setup to handling authentication and database access.

Why Deploy MCP to Supabase?

Supabase offers several advantages over traditional hosting for MCP servers:

  • Edge Functions with Deno — Supabase Edge Functions run on Deno, which has excellent TypeScript support and fast cold starts
  • Native Postgres integration — your MCP server can query the same Postgres database your app uses, with zero additional setup
  • Realtime support — Supabase Realtime lets your MCP server subscribe to database changes and push live data to AI clients
  • Built-in auth — Supabase Auth handles user authentication, so you can scope MCP server access per user without building auth from scratch
  • Free tier available — for development and low-volume production use

Architecture Overview

A Supabase-hosted MCP server typically looks like this:

AI Client (Claude Desktop / Cursor)
    ↓ HTTP (SSE or streamable HTTP)
Supabase Edge Function (MCP Server)
    ↓
Supabase Postgres (data layer)
Supabase Storage (file layer)
Supabase Realtime (subscriptions)

The Edge Function acts as the MCP transport layer — it receives MCP protocol messages, executes tools against your Supabase backend, and returns results to the AI client.

Step 1: Install the Supabase CLI

# Install Supabase CLI
npm install -g supabase

# Initialize a new Supabase project (if starting fresh)
supabase init

# Link to existing project
supabase link --project-ref your-project-ref

Step 2: Create Your Edge Function

supabase functions new mcp-server

This creates supabase/functions/mcp-server/index.ts. Here's a starter MCP server for Supabase:

// supabase/functions/mcp-server/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { Server } from 'https://esm.sh/@modelcontextprotocol/sdk/server/index.js'
import { StreamableHTTPServerTransport } from 'https://esm.sh/@modelcontextprotocol/sdk/server/streamableHttp.js'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)

const server = new Server(
  { name: 'supabase-mcp', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

server.setRequestHandler('tools/list', async () => ({
  tools: [
    {
      name: 'query_table',
      description: 'Query any table in the Supabase database',
      inputSchema: {
        type: 'object',
        properties: {
          table: { type: 'string', description: 'Table name to query' },
          filter: { type: 'object', description: 'Optional filter object' },
          limit: { type: 'number', description: 'Max rows to return', default: 50 }
        },
        required: ['table']
      }
    },
    {
      name: 'insert_row',
      description: 'Insert a row into a Supabase table',
      inputSchema: {
        type: 'object',
        properties: {
          table: { type: 'string' },
          data: { type: 'object', description: 'Row data to insert' }
        },
        required: ['table', 'data']
      }
    }
  ]
}))

server.setRequestHandler('tools/call', async (request) => {
  const { name, arguments: args } = request.params

  if (name === 'query_table') {
    let query = supabase.from(args.table).select('*')
    if (args.filter) {
      Object.entries(args.filter).forEach(([key, val]) => {
        query = query.eq(key, val)
      })
    }
    const { data, error } = await query.limit(args.limit ?? 50)
    if (error) throw new Error(error.message)
    return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
  }

  if (name === 'insert_row') {
    const { data, error } = await supabase.from(args.table).insert(args.data).select()
    if (error) throw new Error(error.message)
    return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
  }

  throw new Error(`Unknown tool: ${name}`)
})

Deno.serve(async (req) => {
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
  await server.connect(transport)
  return transport.handleRequest(req)
})

Step 3: Configure Environment Variables

Supabase Edge Functions automatically inject SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY — no manual configuration needed for database access. For additional secrets:

# Set a custom secret
supabase secrets set MY_CUSTOM_KEY=your-value

# List existing secrets
supabase secrets list

Security note: Never use the service_role key in client-side code. In Edge Functions, it's safe because Edge Functions run server-side — but still apply Row Level Security (RLS) policies to limit what data each function can access.

Step 4: Deploy the Edge Function

# Deploy to Supabase cloud
supabase functions deploy mcp-server

# Test locally first
supabase functions serve mcp-server

Your MCP server will be available at:

https://your-project-ref.supabase.co/functions/v1/mcp-server

Step 5: Configure Your MCP Client

Add the deployed Edge Function as an MCP server in Claude Desktop:

// claude_desktop_config.json
{
  "mcpServers": {
    "supabase": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://your-project-ref.supabase.co/functions/v1/mcp-server"],
      "env": {
        "MCP_AUTH_TOKEN": "your-anon-or-user-jwt"
      }
    }
  }
}

Note: Use mcp-remote to proxy HTTP-based MCP servers into the stdio format expected by Claude Desktop. Cursor and other clients with native HTTP transport can connect directly.

Step 6: Add Row-Level Security

If your MCP server will be accessed by multiple users, RLS ensures each user only sees their own data:

-- Enable RLS on your table
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

-- Policy: users can only read their own rows
CREATE POLICY "Users can read own rows"
ON your_table FOR SELECT
USING (auth.uid() = user_id);

-- Policy: users can only insert their own rows
CREATE POLICY "Users can insert own rows"
ON your_table FOR INSERT
WITH CHECK (auth.uid() = user_id);

In your Edge Function, use the user's JWT (passed via Authorization header) instead of the service role key:

const authHeader = req.headers.get('Authorization')
const userClient = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
  { global: { headers: { Authorization: authHeader ?? '' } } }
)

Adding Realtime Subscriptions

Supabase Realtime enables your MCP server to push live database updates to the AI client — useful for dashboards, monitoring tools, and collaborative workflows.

// Add a 'subscribe_to_changes' tool
server.setRequestHandler('tools/call', async (request) => {
  if (request.params.name === 'subscribe_to_changes') {
    const { table } = request.params.arguments

    return new Promise((resolve) => {
      const channel = supabase.channel('db-changes')
        .on('postgres_changes', { event: '*', schema: 'public', table }, (payload) => {
          resolve({
            content: [{
              type: 'text',
              text: `Change detected: ${JSON.stringify(payload, null, 2)}`
            }]
          })
          supabase.removeChannel(channel)
        })
        .subscribe()

      // Timeout after 30 seconds
      setTimeout(() => {
        supabase.removeChannel(channel)
        resolve({ content: [{ type: 'text', text: 'No changes detected within 30 seconds' }] })
      }, 30000)
    })
  }
})

Production Considerations

Rate limiting: Supabase Edge Functions are subject to rate limits on the free tier. For production, upgrade to a paid plan and implement your own rate limiting within the function to protect against AI client loops.

Cold starts: Deno-based Edge Functions have cold starts of 200-500ms. For latency-sensitive MCP tools, consider keeping functions warm with periodic health check pings.

Error handling: Always return proper MCP error responses rather than letting exceptions propagate — unhandled errors will crash the MCP session:

try {
  // your tool logic
} catch (err) {
  return {
    content: [{ type: 'text', text: `Error: ${err.message}` }],
    isError: true
  }
}

Logging: Use Supabase's built-in log viewer (Dashboard → Edge Functions → Logs) to debug production issues without adding a separate logging service.

Full Deployment Checklist

  1. ✅ Edge Function deployed to Supabase cloud
  2. ✅ RLS policies applied to all accessed tables
  3. ✅ Secrets set via supabase secrets set (not hardcoded)
  4. ✅ Error handling returns proper MCP error format
  5. ✅ Auth validated on every request (not just tools that write)
  6. ✅ Rate limiting implemented at the function level
  7. ✅ MCP client configured with correct endpoint URL and auth token

Browse the MCP server directory to find more database connectors and Supabase-specific MCP tools for your stack.

Recommended Tools

Better Stack

Free Plan

Get alerted when your APIs, browser tests, payment pipelines, or MCP server dependencies go down. Used by 100K+ developers.

Start monitoring free →

1Password

14-day Free Trial

Store and inject API keys, payment credentials, tokens, and file access secrets into your MCP server configs. Trusted by 150K+ developers.

Try 1Password free →

🔧 MCP Servers Mentioned in This Article

🗄️

Supabase MCP Server

Supabase MCP Server connects Cursor, Claude Code, Claude Desktop, Windsurf and other MCP clients to a Supabase project, and the first thing to know is that the personal access token setup most guides still describe is gone. Supabase now runs a hosted server at https://mcp.supabase.com/mcp using OAuth 2.1 with dynamic client registration — you add the URL, your client opens a browser, you pick the organization, and there is no PAT to mint or rotate. For Claude Code that is `claude mcp add --scope project --transport http supabase "https://mcp.supabase.com/mcp"` followed by `/mcp` in a plain terminal (not the IDE extension) to run the auth flow. Three URL query parameters do the real configuration work: `read_only=true` runs every statement as a read-only Postgres role, `project_ref=<id>` scopes the server to one project and drops the account-management tools entirely, and `features=` selects the tool groups. Those groups are database (list_tables, list_extensions, list_migrations, apply_migration, execute_sql), debugging (get_logs across API/Postgres/Edge Functions/Auth/Storage/Realtime, plus get_advisors for security and performance findings), development (get_project_url, get_publishable_keys, generate_typescript_types), Edge Functions (list, get, deploy), account management, docs search, experimental branching on paid plans, and storage — storage is the one group disabled by default. Running Supabase locally with the CLI exposes a reduced server at http://localhost:54321/mcp with no OAuth; self-hosted installs are similar. The npm package `@supabase/mcp-server-supabase` still exists for stdio clients and also exports `createToolSchemas()` so Vercel AI SDK users get typed tool inputs and outputs. Read Supabase's security best-practices page before pointing this at anything with production data — the mutating tools are real.

Auth required📘
🗄️

Supabase Realtime

Subscribe to real-time database changes in Supabase via MCP. Listen to row-level changes, broadcast messages, and build live collaborative features.

Local
🗄️

PostgreSQL MCP Server

The PostgreSQL MCP server was the Model Context Protocol reference server for Postgres, and it is retired: the source now sits in modelcontextprotocol/servers-archived — a repository GitHub reports as archived, described as "Reference MCP servers that are no longer maintained" — and the npm package @modelcontextprotocol/server-postgres carries a deprecation notice reading "Package no longer supported." It still installs and still runs, which is why most third-party setup articles have not caught up. What it provides is deliberately small: a single tool, query, which executes read-only SQL inside a READ ONLY transaction, plus per-table schema information exposed as MCP resources at postgres://<host>/<table>/schema, with column names and data types discovered from database metadata. There is no index advice, no health check, no separate schema-listing tool, and no write mode. Install is npx @modelcontextprotocol/server-postgres with a postgres:// connection string as the argument. For active work against Postgres, the maintained alternative is Postgres MCP Pro (crystaldba/postgres-mcp), which exposes nine tools including index tuning against hypothetical indexes and a database health check, and has an explicit restricted access mode; if your database is hosted on Supabase or Neon, their platform servers add branching and logs that a raw Postgres connection cannot see. Reach for this archived server only when you want the smallest possible surface — one process, one read-only query tool, nothing else.

Local📘
🌐

Fetch

Web content fetching and conversion for efficient LLM usage. Extract readable content from any URL.

Local
📁

Filesystem MCP Server

sandboxed read, write, edit, move and search access to an explicit whitelist of local directories, and it is the reference implementation most other filesystem MCP servers are modelled on. Shipped by Anthropic in the official modelcontextprotocol/servers monorepo (89,000+ stars, actively maintained), it is a Node.js server published to npm as @modelcontextprotocol/server-filesystem. The part worth understanding before you install is the access-control model, because there are now two ways to grant directories and they do not compose. Method one is command-line arguments: `npx -y @modelcontextprotocol/server-filesystem /path/one /path/two`. Method two, and the one the maintainers recommend, is MCP Roots — a client that supports the roots protocol sends its roots at initialization, and those roots COMPLETELY REPLACE any directories passed on the command line, then get replaced again on every `notifications/roots/list_changed`. That means allowed directories can change at runtime without restarting the server, but it also means a roots-capable client silently overrides your CLI arguments. If the server starts with no arguments and the client either does not support roots or sends an empty list, initialization throws an error. The tool surface is broad: `read_text_file` (with mutually exclusive `head`/`tail` line windows), `read_media_file` returning base64 image/audio content blocks, `read_multiple_files` which keeps going when individual reads fail, `write_file`, `edit_file`, `create_directory`, `list_directory`, `list_directory_with_sizes`, `move_file`, `search_files`, `directory_tree`, `get_file_info` and `list_allowed_directories`. `edit_file` is the one to learn — it does line-based and multi-line pattern matching with indentation detection and preservation, returns a git-style diff with context, and supports `dryRun: true` so you can preview a change before applying it; the maintainers recommend always running a dry run first. Every operation is refused outside the allowed set, and `list_allowed_directories` is the fastest way to confirm what the server actually believes it can touch.

Local

📚 More from the Blog