Use Supabase’s OAuth 2.1 server to sign users into your MCP server. Supabase handles the OAuth flow and issues the tokens; your server verifies them and you host the consent page.
Prerequisites
Before your MCP server will work, configure these in the Supabase Dashboard:
Enable the OAuth 2.1 server
Authentication → Sign In / Providers → OAuth Server → toggle on. Also toggle Allow Dynamic OAuth Apps so MCP clients can self-register.
Set the consent screen URL
Point it at the route your MCP server will host, e.g. http://localhost:3000/auth/consent. Supabase redirects the browser here with ?authorization_id=<uuid> after /authorize.
Enable a sign-in method
Users must be signed in before they can consent. Pick one under Authentication → Sign In / Providers:
- Anonymous sign-ins - one-click guest sessions, ideal for demos
- Email + password, magic links, or OAuth providers (Google, GitHub, etc.) - for real apps
Copy your keys
From Project Settings:
- Project ID (General)
- Publishable key -
sb_publishable_... (API Keys). Used by your consent route and by tools that call Supabase - not by the provider itself.
Host the consent UI
Supabase redirects the browser to the consent screen URL you configured above. Your route signs the user in, loads the authorization details with the Supabase JS SDK, and submits the approve/deny decision back to Supabase - mcp-use is not involved in this step.
Don’t want to build it from scratch? Start from the mcp-oauth-supabase-template - it has sign-in, consent, and approve/deny already wired up. Or follow Supabase’s OAuth Server - Getting Started guide to roll your own.
Environment variables
# Required (hosted Supabase) - read by oauthSupabaseProvider()
MCP_USE_OAUTH_SUPABASE_PROJECT_ID=your-project-id
# Optional override — point the provider at a self-hosted or local Supabase
# instance (e.g. `supabase start`). Required instead of PROJECT_ID when you
# aren't on supabase.co.
# MCP_USE_OAUTH_SUPABASE_URL=http://localhost:54321
# Used by your consent UI and by tools calling Supabase REST/APIs.
# Not read by oauthSupabaseProvider() itself.
MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
// server.ts
import { MCPServer, oauthSupabaseProvider } from "mcp-use/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthSupabaseProvider(), // reads MCP_USE_OAUTH_SUPABASE_PROJECT_ID
});
await server.listen(3000);
Configuration options
oauthSupabaseProvider({
// Provide one of these (or set the matching env var):
projectId: "your-project-id", // hosted Supabase
// supabaseUrl: "http://localhost:54321", // self-hosted / local
// Skip JWT verification - development only
verifyJwt: process.env.NODE_ENV === "production",
// Override advertised scopes
scopesSupported: ["openid", "profile", "email"],
});
Self-hosted / local Supabase
For supabase start or a self-hosted instance, point the provider at your base URL instead of a project ID:
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: oauthSupabaseProvider({
supabaseUrl: "http://localhost:54321",
}),
});
Or via env var:
MCP_USE_OAUTH_SUPABASE_URL=http://localhost:54321
supabaseUrl takes precedence over projectId when both are set. The provider derives the issuer and JWKS endpoint from this URL, so the local Supabase stack must expose /auth/v1/.well-known/openid-configuration (it does, by default).
New Supabase projects sign tokens with ES256 and expose a JWKS endpoint - the provider fetches it automatically, no secret needed. Legacy HS256 projects can still pass a jwtSecret (or set MCP_USE_OAUTH_SUPABASE_JWT_SECRET), but new projects do not need it.
server.tool(
{
name: "get-user-info",
description: "Get authenticated user info",
},
async (_args, ctx) => ({
userId: ctx.auth.user.userId,
email: ctx.auth.user.email,
name: ctx.auth.user.name,
scopes: ctx.auth.scopes,
}),
);
You can also call Supabase from within a tool using the user’s access token. Create a Supabase client scoped to the request so RLS policies see the caller as the authenticated user:
import { createClient } from "@supabase/supabase-js";
server.tool(
{
name: "list-notes",
description: "Fetch the user's notes",
},
async (_args, ctx) => {
const supabase = createClient(
`https://${process.env.MCP_USE_OAUTH_SUPABASE_PROJECT_ID}.supabase.co`,
process.env.MCP_USE_OAUTH_SUPABASE_PUBLISHABLE_KEY!,
{
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
global: { headers: { Authorization: `Bearer ${ctx.auth.accessToken}` } },
},
);
const { data, error } = await supabase.from("notes").select();
if (error) return { content: [{ type: "text", text: error.message }], isError: true };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
);
Resources
Next Steps