Security
Security considerations specific to vCon MCP Server.
Supabase Keys
SUPABASE_SERVICE_ROLE_KEY
Full access, bypasses RLS
Server-side deployments
SUPABASE_ANON_KEY
Restricted, RLS enforced
Client-facing with user auth
Recommendation: Use SUPABASE_SERVICE_ROLE_KEY for trusted server deployments.
Tool Access Control
Profiles
Control which MCP tools are available:
MCP_TOOLS_PROFILE=readonly # Read-only operations
MCP_TOOLS_PROFILE=user # CRUD without admin ops
MCP_TOOLS_PROFILE=full # All tools (default)Disable Specific Tools
MCP_DISABLED_TOOLS=delete_vcon,execute_sqlAvailable Profiles
full
All tools enabled (default)
readonly
Read-only operations only
user
CRUD without admin operations
admin
Full access including schema changes
minimal
Basic operations only
public
Read and search tools for a hosted public dataset; hides database internals, analytics, deployment-shaped rollups, and the prompts that assume them
OAuth for MCP Connectors
claude.ai and the Claude Desktop chat add remote MCP servers as custom connectors, and those only sign in with OAuth 2.1. Claude Code and scripts keep using a static bearer token from API_KEYS or API_KEYS_READONLY; the server checks static keys first, so turning OAuth on changes nothing for them.
vcon-mcp is the resource server only. An external authorization server handles sign-in, PKCE, dynamic client registration and token issuance. The steps below use Supabase OAuth Server; any issuer that signs JWTs and publishes <issuer>/.well-known/jwks.json works the same way.
What the server does
/.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp
none
RFC 9728 metadata naming OAUTH_ISSUER as the authorization server
/.well-known/oauth-authorization-server
none
302 to the issuer's RFC 8414 metadata
/oauth/consent
none
Consent page, when OAUTH_CONSENT_ANON_KEY is set
/mcp with no or bad token
401 with WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource/mcp"
/mcp with an OAuth token
JWT
Checked on every request: signature against the issuer JWKS, iss, exp, aud equal to OAUTH_RESOURCE, optional email domain
The REST API (/api/v1) still takes static keys only.
Environment
With OAuth on, API_KEYS may be empty: an OAuth-only deployment gets 401s that point at the metadata instead of the "no API keys configured" 503.
Supabase setup
Signing keys. Authentication > JWT Keys: move to asymmetric keys (ES256 or RS256). The server verifies against the JWKS and cannot check legacy HS256 tokens.
OAuth Server. Authentication > OAuth Server: enable it, turn on dynamic client registration (claude.ai registers itself), and set the Authorization Path to
/oauth/consent. Set the project's Site URL to the MCP server's origin (https://mcp.example.com) so the consent link lands on this server.Audience. Supabase access tokens carry
aud: "authenticated", which the server rejects. Add a Custom Access Token hook that setsaudtoOAUTH_RESOURCEon OAuth-issued tokens (those carryingclient_id):Then select it under Authentication > Hooks > Custom Access Token.
Sign-in.
OAUTH_CONSENT_PROVIDERSpicks what the consent page offers.email(default): a six-digit emailed code, existing users only (create_user: false). Needs working SMTP, and{{ .Token }}in the Magic Link email template.A Supabase external provider such as
google: PKCE redirect through the provider, no SMTP. Enable the provider in Supabase, with its callback at<issuer>/callback. With signups disabled, create each allowed user first (admin API, verified email); Supabase links the Google identity to that user on first sign-in.
Self-hosted Supabase
The same steps apply, set through the auth container's environment instead of the dashboard. The OAuth server is in supabase/gotrue from v2.180; use v2.186 or later, which stores and enforces the token_endpoint_auth_method that dynamically registered clients declare.
Keeping the old HS256 secret in GOTRUE_JWT_KEYS as verify-only keeps the existing anon and service-role keys valid, so PostgREST and vcon-mcp's database access are unaffected.
MCP clients call the authorization server without a Supabase apikey, which the stock Kong gateway requires. Route the OAuth paths from the reverse proxy straight to the auth container (port 9999) with /auth/v1 stripped: /auth/v1/oauth/*, /auth/v1/.well-known/*, then /auth/v1/otp and /auth/v1/verify for email sign-in, or /auth/v1/authorize, /auth/v1/callback and /auth/v1/token for a provider such as Google, plus /.well-known/oauth-authorization-server/auth/v1 rewritten to /.well-known/oauth-authorization-server. Give these routes priority over the vcon-mcp route. Nothing else on the auth API needs to be public.
Connect a client
In claude.ai: Settings > Connectors > Add custom connector, URL https://mcp.example.com/mcp. Leave the OAuth client ID and secret empty; the client registers itself. Sign in, enter the emailed code, approve. The connector then shows in Claude Desktop chat as well.
Checks
Container Security
The Docker image includes security defaults:
Runs as non-root user (
vcon, uid 1001)Minimal Alpine base image
No shell access by default
Best Practices
Store secrets in environment variables or secret managers, not in code
Use HTTPS in production (via reverse proxy)
Enable
MCP_HTTP_STATELESS=truefor multi-instance deploymentsRestrict tool profiles based on use case
Next Steps
Last updated