@phake/mcp supports five authentication strategies. Set AUTH_STRATEGY in your environment to select one, or let the framework infer it from which env vars are present.
# .dev.vars or wrangler secret
AUTH_STRATEGY=bearer
If AUTH_STRATEGY is not set, the framework infers the strategy:
api_key — if API_KEY is present
bearer — if BEARER_TOKEN is present
none — otherwise
Strategies at a glance
| Strategy | Description | Required env vars |
|---|
oauth | Full OAuth 2.1 PKCE flow; links provider tokens to MCP sessions | OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_SCOPES, OAUTH_REDIRECT_URI, PROVIDER_CLIENT_ID, PROVIDER_CLIENT_SECRET, PROVIDER_ACCOUNTS_URL |
bearer | Static Bearer token checked on every request | BEARER_TOKEN |
api_key | Static API key sent in a request header | API_KEY, API_KEY_HEADER (optional, default: x-api-key) |
custom | Arbitrary custom request headers forwarded to tool context | CUSTOM_HEADERS |
none | No authentication — all requests are accepted | — |
oauth — OAuth 2.1 PKCE
Use oauth when you want users to authenticate with a third-party provider (Google, GitHub, Spotify, etc.). The server runs a full OAuth 2.1 authorization code flow with PKCE and maintains a mapping between its own RS tokens and the provider’s access tokens.
When to use it: when your tools need to call external APIs on behalf of individual users, or when you need multi-user isolation with per-session tokens.
AUTH_STRATEGY=oauth
OAUTH_CLIENT_ID=your-mcp-client-id
OAUTH_CLIENT_SECRET=your-mcp-client-secret
OAUTH_SCOPES=openid profile email
OAUTH_REDIRECT_URI=https://your-server.com/oauth/callback
PROVIDER_CLIENT_ID=your-provider-client-id
PROVIDER_CLIENT_SECRET=your-provider-client-secret
PROVIDER_ACCOUNTS_URL=https://provider.example.com/accounts
Once a user completes the flow, context.providerToken in tool handlers is set to the provider’s access token. Use context.resolvedHeaders to forward it:
handler: async (_args, context) => {
const response = await fetch("https://api.provider.com/me", {
headers: context.resolvedHeaders,
// => { Authorization: "Bearer <provider_access_token>" }
});
return await response.json();
},
The OAuth endpoints exposed by the server are:
| Path | Description |
|---|
/.well-known/oauth-authorization-server | OAuth discovery document |
/.well-known/oauth-protected-resource | Protected resource metadata |
/authorize | Authorization request |
/token | Token exchange |
/oauth/callback | OAuth callback |
/oauth/provider-callback | Provider callback |
/revoke | Token revocation |
/register | Dynamic client registration |
bearer — Static Bearer token
Use bearer for server-to-server integrations or personal deployments where a single shared secret is sufficient. Every incoming request must include Authorization: Bearer <token>.
When to use it: simple scenarios where you control all clients and a single token is enough.
AUTH_STRATEGY=bearer
BEARER_TOKEN=super-secret-token
In your tools, context.providerToken is set to the value of BEARER_TOKEN and context.resolvedHeaders contains { Authorization: "Bearer super-secret-token" }.
Configure clients to send the header:
{
"mcpServers": {
"my-api": {
"url": "https://your-server.com/mcp",
"headers": {
"Authorization": "Bearer super-secret-token"
}
}
}
}
api_key — Static API key
Use api_key to authenticate with a key sent in a custom header instead of the Authorization header. The header name defaults to x-api-key but is configurable.
When to use it: APIs that expect a non-standard auth header (e.g., x-api-key, Api-Key), or when you want to avoid the Authorization header for routing reasons.
AUTH_STRATEGY=api_key
API_KEY=my-api-key-value
API_KEY_HEADER=x-api-key # optional, this is the default
context.resolvedHeaders will contain { "x-api-key": "my-api-key-value" } (or whichever header name you configure).
handler: async (_args, context) => {
const response = await fetch("https://api.example.com/data", {
headers: context.resolvedHeaders,
// => { "x-api-key": "my-api-key-value" }
});
return await response.json();
},
Use custom when the upstream API you’re wrapping expects arbitrary headers that don’t fit the bearer or API key patterns — for example, multiple headers or non-standard schemes.
When to use it: wrapping APIs that use proprietary auth headers, or when you need to forward multiple headers to every tool call.
Provide headers as a comma-separated key:value string:
AUTH_STRATEGY=custom
CUSTOM_HEADERS=X-App-Id:my-app,X-App-Secret:my-secret
All headers in CUSTOM_HEADERS are available via context.resolvedHeaders:
handler: async (_args, context) => {
const response = await fetch("https://api.example.com/data", {
headers: context.resolvedHeaders,
// => { "X-App-Id": "my-app", "X-App-Secret": "my-secret" }
});
return await response.json();
},
none — No authentication
Use none to accept all requests without checking credentials. Suitable for local development or fully public tools.
Do not use none in production deployments that are publicly accessible. Any client can call your tools without restrictions.
assertProviderToken
When a tool has requiresAuth: true the dispatcher already rejects unauthenticated calls before your handler runs. If you need a compile-time guarantee that context.providerToken is typed as string (not string | undefined), call assertProviderToken:
import { assertProviderToken } from "@phake/mcp";
const myTool = defineTool({
name: "get_data",
description: "Fetch data for the current user",
inputSchema: z.object({}),
requiresAuth: true,
handler: async (_args, context) => {
assertProviderToken(context); // throws if token is missing
// After this line, context.providerToken is typed as string
const token = context.providerToken;
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${token}` },
});
return await response.json();
},
});
assertProviderToken throws Error("Authentication required") if the token is absent. In practice this only happens in tools without requiresAuth: true, since the dispatcher already guards authenticated tools.
Prefer context.resolvedHeaders over constructing headers manually. The framework builds the correct header shape for the active strategy automatically:
| Strategy | resolvedHeaders content |
|---|
oauth | { Authorization: "Bearer <provider_access_token>" } |
bearer | { Authorization: "Bearer <BEARER_TOKEN>" } |
api_key | { "<API_KEY_HEADER>": "<API_KEY>" } |
custom | All key/value pairs from CUSTOM_HEADERS |
none | {} |
handler: async (_args, context) => {
// Works correctly regardless of which strategy is active
const response = await fetch("https://api.example.com/resource", {
headers: context.resolvedHeaders,
});
return await response.json();
},