Threat Model: What Happens With REQUIRE_AUTH=false in Production
An unprotected HTTP MCP server exposes every registered tool to any caller who can reach the endpoint. Vercel functions are publicly accessible by default. An open server can appear in MCP registries like glama.ai within 24 hours of deployment.
| Threat | Mechanism | Example Impact |
|---|---|---|
| Unauthorized tool execution | Direct POST to /mcp |
initiate_checkout runs arbitrary Stripe sessions charged to your account |
| Cost overruns | Unbounded tool calls | Thousands of DB queries or LLM calls billed to your account |
| Data exfiltration | Read-only tools return sensitive data | search_products leaks pricing and inventory to competitors |
| Prompt injection amplification | Attacker plants malicious instructions via public tool results | Model executes destructive operations on attacker's behalf |
| Downstream credential abuse | Your server holds API keys for other services | Those keys leak through tool call responses or logs |
When REQUIRE_AUTH=false is acceptable
- Local development on
localhostwith no public URL or tunnel - STDIO transport — the MCP spec explicitly says STDIO servers SHOULD NOT implement OAuth; they get credentials from the environment
- A demo environment with no real data and no real payment keys
Toggle auth by environment (safe default pattern)
API Key Middleware: FlexAuthMiddleware
The simplest production-viable auth for a server you control completely. 15 lines of middleware that run before every tool call — no database, no IdP, no infrastructure.
FastMCP 3.3.1 has no dedicated ApiKeyVerifier class; use the Middleware base class. The implementation below accepts both X-API-Key header and Authorization: Bearer — supporting mcp-remote and direct API clients from one class:
One key per client (stronger pattern)
A single shared key means one leak invalidates all clients. Store a JSON map for per-client identity and individual revocation:
VALID_API_KEYS first, redeploy, then distribute the new key to clients, then remove the old key and redeploy again. Both keys work during the transition window.
Bearer Token: Wiring mcp-remote to Your Server
mcp-remote is the Node.js bridge Claude Desktop uses for HTTP MCP transports. It passes custom headers via the --header flag on every request — SSE connection and all tool calls.
mcp-remote@0.1.38. Versions 0.0.5–0.1.15 have an active security vulnerability (CVE-2025-6514). Never use an unpinned npx mcp-remote.
X-API-Key header
Authorization: Bearer header
Note: the space is inside the env var value ("Bearer sk-..."), not around the colon in the arg. This avoids the Windows/Cursor npx argument-splitting bug.
StreamableHttpTransport("https://your-server.vercel.app/mcp", auth="sk-prod-abc123") — FastMCP wraps this as Authorization: Bearer automatically.
JWT Verification with FastMCP's JWTVerifier
JWTVerifier (FastMCP 3.3.1, from fastmcp.server.auth.providers.jwt import JWTVerifier) validates cryptographically signed tokens without a database lookup. It checks signature, expiration (exp), issuer (iss), and audience (aud).
Use JWTVerifier when: credentials need to expire, carry per-user scopes, be issued by an identity provider, or when you have multiple service identities needing distinct credentials.
Option A: RS256 asymmetric keys (production)
Generate keys once on your development machine. The server only needs the public key — the private key stays on your machine and never touches Vercel:
Option B: HS256 symmetric key (internal tools, simpler)
Option C: JWKS endpoint (Auth0, Azure AD, Keycloak)
OAuth 2.1: What the MCP Spec Actually Requires
The MCP authorization specification (2025-11-25) is unambiguous:
"Authorization is OPTIONAL for MCP implementations. Implementations using a STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment."
In plain terms: OAuth 2.1 is not required for any MCP server. For Claude Desktop + mcp-remote (STDIO transport), the spec explicitly says to use environment credentials — meaning static Bearer tokens via --header in mcp-remote config is the correct, spec-aligned approach.
Recommended auth by deployment scenario
| Use Case | Recommended Auth | Complexity |
|---|---|---|
| Personal tool, your Claude Desktop only | Static Bearer token via mcp-remote --header | Low |
| Small team (2–10 developers you control) | JWTVerifier + RSAKeyPair-issued long-lived tokens | Medium |
| Enterprise (Auth0, Azure AD, Okta users) | JWTVerifier(jwks_uri=...) against your existing IdP | Medium |
| Public server (any OAuth 2.1 client can connect) | Full OAuth 2.1 via RemoteAuthProvider + external IdP | High |
If you do implement OAuth 2.1 (public servers)
The 2025-11-25 spec mandates for HTTP-transport servers that choose to implement OAuth:
- OAuth 2.1 with PKCE (S256 code challenge — clients must refuse if the AS doesn't advertise S256)
- RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resource - RFC 8414 Authorization Server Metadata at
/.well-known/oauth-authorization-server - RFC 8707 Resource Indicators —
resourceparameter in every token request - Bearer token in
Authorizationheader only — never in URI query string - Token audience must match your server URI (no token passthrough to downstream APIs)
client_id is an HTTPS URL; the authorization server fetches that URL to get the client's redirect URIs without pre-registration. DCR remains for backwards compatibility only.
CORS Lockdown: From * to Specific Origins
CORS applies only to browser-based clients. Claude Desktop and mcp-remote are server-side Node.js processes — they do not send Origin headers and are not affected by your CORS configuration at all.
allow_origins=["*"] combined with allow_credentials=True is prohibited by the browser spec. If your FastAPI app uses credentials, the wildcard silently breaks browser-based clients. Always use an explicit origin list in production.
| Client | Sends CORS Preflight? | Add to allow_origins? |
|---|---|---|
| Claude Desktop (Electron app) | No — Electron bypasses CORS | Nothing needed |
| mcp-remote (CLI bridge) | No — Node.js process | Nothing needed |
| Browser web client | Yes | Add your frontend domain(s) |
| MCP Inspector (browser) | Yes | http://localhost:5173 |
expose_headers=["Mcp-Session-Id"], browsers receive the session ID in the response but JavaScript cannot read it (CORS blocks non-safelisted headers). Stateful MCP sessions break silently for web clients.
Per-Tool Authorization: require_scopes
Once JWT auth is configured, FastMCP's require_scopes decorator gates individual tools based on the JWT scope claim. FastMCP hides unauthorized tools from tools/list automatically — the LLM never sees tools the caller cannot use.
Tag-based auth for servers with many tools
scopes=["read"]. Claude Desktop with full access → scopes=["read", "write"]. Admin service account → scopes=["read", "write", "admin"]. A read-only token calling initiate_checkout gets 403 Forbidden — FastMCP intercepts before your function body runs.
Rate Limiting with slowapi
slowapi (v0.1.9) wraps flask-limiter patterns for Starlette/FastAPI. A 60 req/min per-IP default with in-memory storage is enough to prevent abuse for most solo-operator servers.
pip install "slowapi[redis]==0.1.9" and pass storage_uri=os.environ["REDIS_URL"] to Limiter(). Upstash Redis works with Vercel serverless and has a free tier.
Auth Method Comparison
X-API-Key / Bearer token via mcp-remote --header
Best for personal tools and internal servers. 15 lines of middleware, no infrastructure. Static secrets don't expire — add per-client keys and rotate periodically. Correct spec-aligned approach for Claude Desktop (STDIO transport).
JWTVerifier (RS256 / HS256) — self-issued tokens
Best for small teams and multi-client deployments. Tokens expire automatically, carry scopes for per-tool gating, and never need a database lookup. Generate keys with openssl, issue tokens with RSAKeyPair.create_token(), store base64 public key in Vercel.
JWTVerifier + external IdP (Auth0, Azure AD, Keycloak)
Best for teams with existing SSO. Point JWTVerifier(jwks_uri=...) at your IdP's JWKS endpoint — FastMCP handles key rotation automatically. Users authenticate through your existing IdP flow.
Full OAuth 2.1 (RemoteAuthProvider + CIMD)
Best for public servers where any OAuth 2.1 client should be able to self-configure. Requires building or running an authorization server, implementing 5+ discovery endpoints, handling PKCE flows, and managing CIMD hosting. Only warranted if external users you don't control will connect arbitrary MCP clients.
| Method | MCP Spec Required? | Tokens Expire? | Per-Tool Scopes? |
|---|---|---|---|
No auth (REQUIRE_AUTH=false) | N/A — localhost only | N/A | No |
| X-API-Key / Bearer static key | No | No | No |
| JWTVerifier HS256 | No | Yes | Yes (with require_scopes) |
| JWTVerifier RS256 / JWKS | No (satisfies HTTP transport intent) | Yes | Yes |
| Full OAuth 2.1 | SHOULD if HTTP + auth enabled | Yes | Yes |
8 Common Mistakes That Break MCP Auth
1REQUIRE_AUTH=false copied to Vercel from local .env
Easy to paste a dev env file into Vercel without noticing this flag. Add a startup assertion that crashes the process in production rather than running unsecured:
2API key hardcoded in claude_desktop_config.json args
The config file is stored on disk and may be committed to version control. Keys in git history are permanent even after deletion.
3Middleware only checks X-API-Key, not Authorization: Bearer
mcp-remote and FastMCP Python clients send Authorization: Bearer, not X-API-Key. Middleware that only checks X-API-Key silently rejects these clients with 401. Use FlexAuthMiddleware from Section 2 that checks both headers.
4Using mcp-remote 0.0.5–0.1.15 (CVE-2025-6514)
These versions have an active security vulnerability. Always pin to the patched version:
5Short or guessable HS256 secret
HS256 secrets can be brute-forced if under 32 bytes. A leaked secret compromises every token ever issued with it. Generate 256 bits of entropy:
6Spaces around the colon in mcp-remote --header args
"Authorization: Bearer token" as a single arg string gets split by npx on Windows and in Cursor, producing a mangled header. Put the space inside the env var value:
7allow_origins=["*"] with allow_credentials=True
The browser spec prohibits this combination. Auth headers silently fail for browser-based clients — no error, just CORS rejection. Replace the wildcard with an explicit list of your actual frontend domains.
8No per-tool scope differentiation — all tools share one scope
Granting all tools the same scope means any authenticated client can call initiate_checkout even if it should only have read access. Define distinct scopes and assign per tool:
Add Production Auth in 5 Steps
-
1Add FlexAuthMiddleware to your FastMCP server
Implement
FlexAuthMiddlewarefrom Section 2. SetVALID_API_KEYS=sk-prod-abc123(comma-separated for multiple clients) in Vercel → Settings → Environment Variables. SetREQUIRE_AUTH=true(or omit it and default to true in code). Deploy. Verify that requests without a key receive 401. -
2Update claude_desktop_config.json to pass the key
Add
"--header", "X-API-Key:${MY_API_KEY}"to the mcp-remote args and"MY_API_KEY": "sk-prod-abc123"to the"env"block. Pin tomcp-remote@0.1.38. Fully quit and relaunch Claude Desktop (menu bar quit, not just close window) to load the new config. Verify the tools menu shows your server's tools. -
3Graduate to JWTVerifier for expiring credentials
Generate an RSA keypair with
openssl. ConfigureJWTVerifier(public_key=..., issuer=..., audience=...)on your FastMCP server. Runissue_token.pylocally usingRSAKeyPair.create_token(). Update mcp-remote config to pass"Authorization:${AUTH_HEADER}"with"AUTH_HEADER": "Bearer <jwt>". Store the base64-encoded public key in Vercel asJWT_PUBLIC_KEY_B64. Test by verifying the server rejects expired or wrong-audience tokens. -
4Add per-tool scope gates for destructive tools
Add
auth=require_scopes("write")to@mcp.tooloninitiate_checkoutand any tool that writes data or triggers payments. Issue read-only tokens withscopes=["read"]for monitoring bots andscopes=["read", "write"]for Claude Desktop. Test by callinginitiate_checkoutwith a read-only token — expect 403 Forbidden before your function body executes. -
5Lock down CORS and add rate limiting
Install
slowapi==0.1.9. AddSlowAPIMiddlewarewithdefault_limits=["60/minute"]and a custom 429 handler that returns aRetry-Afterheader. Replaceallow_origins=["*"]with an explicit list. AddCORSMiddlewarebeforeSlowAPIMiddlewareso OPTIONS preflights don't count against the rate limit. Verify: no token → 401 withWWW-Authenticate: Bearer; 61st request → 429 withRetry-After.
Frequently Asked Questions
-
Is REQUIRE_AUTH=false safe on a public Vercel deployment?
No. Vercel functions are publicly accessible by default. Without auth, any MCP client that discovers your endpoint can call all your tools. An open server can appear in MCP registries like glama.ai within 24 hours of deployment. Ifinitiate_checkoutconnects to a real Stripe account, unauthorized callers can create payment sessions charged to you. The minimum viable protection is 15 lines:FlexAuthMiddlewareplus a single API key in a Vercel env var — no infrastructure required. -
Does the MCP spec require OAuth 2.1?
No. The 2025-11-25 spec says authorization is OPTIONAL. When an HTTP server implements it, the implementation SHOULD conform — but the spec is silent on servers that choose not to implement authorization at all. More relevantly, the spec explicitly says STDIO transport implementations SHOULD NOT implement OAuth and should use environment credentials instead. Claude Desktop uses STDIO via mcp-remote, so static Bearer tokens via--headerare the spec-aligned approach for most developers. -
What is JWTVerifier and when should I use it instead of API key middleware?
JWTVerifiervalidates cryptographically signed tokens — it checks RSA or HMAC signature, verifiesexp(expiration),iss(issuer), andaud(audience).FlexAuthMiddlewarejust does a set-membership check: is this string in my valid keys? UseJWTVerifierwhen you need tokens to expire, carry per-user scopes forrequire_scopes, be issued by an identity provider, or when you have multiple service identities needing distinct credentials. UseFlexAuthMiddlewarewhen you just need a shared secret with no expiry complexity. -
How does mcp-remote pass auth headers to my server?
Use the--headerflag withKEY:VALUEformat (no space around the colon). Store secrets in env vars using${VAR_NAME}substitution. Inclaude_desktop_config.json:"args": ["-y", "mcp-remote@0.1.38", "https://server.vercel.app/mcp", "--header", "Authorization:${AUTH_HEADER}"]with"env": {"AUTH_HEADER": "Bearer sk-prod-abc123"}. The space goes inside the env var value, not the arg string. mcp-remote forwards this on every HTTP request including SSE connections and all tool calls. -
How do I gate initiate_checkout so only privileged clients can call it?
Add@mcp.tool(auth=require_scopes("write"))fromfastmcp.server.auth. Issue read-only clients tokens withscopes=["read"]and Claude Desktop tokens withscopes=["read", "write"]. When a read-only JWT callsinitiate_checkout, FastMCP intercepts the call before your function body runs and returns 403 Forbidden. FastMCP also hides unauthorized tools fromtools/listautomatically — the LLM never proposes calling tools the client cannot use. -
Why doesn't CORS affect Claude Desktop connectivity?
Claude Desktop uses mcp-remote, a Node.js process — not a browser. CORS is enforced by browsers only. mcp-remote does not send anOriginheader, so your CORS middleware is irrelevant to Claude Desktop traffic. If Claude Desktop can't connect, the issue is elsewhere: check that the mcp-remote--headerarg matches what your auth middleware expects, verify the server URL returns 200 at/mcp, and check Vercel function logs for auth errors or 404s on the path. -
Does in-memory slowapi rate limiting work accurately on Vercel?
No. In-memory limits reset per function instance — a client spread across 10 Vercel cold starts effectively gets 10× the stated limit. For accurate cross-instance rate limiting, configure a Redis backend:pip install "slowapi[redis]==0.1.9"and passstorage_uri=os.environ["REDIS_URL"]toLimiter(). Upstash Redis works with Vercel serverless and has a free tier — re-verify current limits before launch. -
What mcp-remote version should I use and why does it matter?
Pin tomcp-remote@0.1.38. Versions 0.0.5–0.1.15 have an active security vulnerability (CVE-2025-6514). Always specify the version explicitly in your args:npx -y mcp-remote@0.1.38. Never use an unpinnednpx mcp-remotewhich could resolve to a vulnerable version on next install.
MCP Server Ops Series — All Guides
Get the Full MCP Server Ops Playbook
New guides on monitoring, multi-tool servers, Shopify integration, and production ops — delivered when they ship.
✓ You're in — watch your inbox.