How to Set Up Authentication for OpenSEO's MCP Server in Hosted vs Self-Hosted Modes
OpenSEO's MCP server uses Better Auth OAuth for hosted deployments and switches to Cloudflare Access or local no-auth for self-hosted environments based on the AUTH_MODE environment variable.
Setting up authentication for OpenSEO's Model Context Protocol (MCP) server requires different configurations depending on your deployment model. The official OpenSEO repository (every-app/open-seo) implements two distinct authentication paths that share the same underlying OAuth infrastructure but diverge in identity resolution and origin validation. This guide walks through both hosted and self-hosted authentication setups using actual source code references.
Hosted Mode: Better Auth OAuth Flow
Hosted mode connects to the official Better Auth service for user authentication. This is the default when BETTER_AUTH_URL is configured and no AUTH_MODE override exists.
OAuth Provider Initialization
The hosted flow begins in src/server/mcp/oauth-provider.ts where createOpenSeoOAuthProvider lazily instantiates the OAuth provider:
// src/server/mcp/oauth-provider.ts lines 38-44
export function createOpenSeoOAuthProvider() {
const authUrl = getHostedBaseUrl(); // Reads BETTER_AUTH_URL
return createOAuthProvider({
resource: authUrl,
// ...
});
}
The BETTER_AUTH_URL environment variable determines the canonical resource name for MCP scope requests. This must be an HTTPS URL in production.
Authorization and Consent Flow
The OAuth flow follows three stages defined in the same file:
handleOAuthAuthorizeRequest(lines 51-70): Parses authorization requests and redirects to/oauth-consenthandleOAuthConsentResponse(lines 72-120): Validates CSRF origin, resolves hosted user context, and attaches theMCP_SCOPEcreateWorkersOAuthMcpProps(lines 36-44): Builds the final MCP context with user-ID, email, organization-ID, and hosted base URL
Authenticated Request Handling
The entry point handleAuthenticatedOpenSeoMcpRequest in src/server/mcp/transport.ts enforces three security checks:
- Presence of
MCP_AUTH_CONTEXT_PROPin the request context - Valid
MCP_SCOPEin the authorization - Strict origin validation against the hosted base URL or Surfmind Chrome extension (lines 49-73)
CORS headers are injected via MCP_CORS_HEADERS (lines 26-33), matching the legacy SDK behavior.
Hosted Mode Implementation
// 1. Register OAuth client
await fetch(`${process.env.BETTER_AUTH_URL}/api/auth/oauth2/register`, {
method: 'POST',
body: JSON.stringify({
redirect_uris: ['https://myapp.com/callback']
}),
headers: { 'Content-Type': 'application/json' },
});
// 2. Exchange authorization code for token (request 'mcp' scope)
const tokenResp = await fetch(
`${process.env.BETTER_AUTH_URL}/api/auth/oauth2/token`,
{
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: '<auth-code>',
redirect_uri: 'https://myapp.com/callback',
client_id: '<client-id>',
client_secret: '<client-secret>',
scope: 'mcp', // Required scope
}),
}
);
const { access_token } = await tokenResp.json();
// 3. Call MCP endpoint with validated origin
await fetch(`${process.env.BETTER_AUTH_URL}/mcp`, {
method: 'POST',
headers: {
Authorization: `Bearer ${access_token}`,
Origin: 'https://myapp.com', // Must match registered redirect_uri
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'search_console',
params: { /* ... */ },
}),
});
Self-Hosted Mode: Cloudflare Access or Local No-Auth
Self-hosted deployments bypass the Better Auth service and use the AUTH_MODE environment variable to select an authentication strategy.
Environment Configuration
Set AUTH_MODE to one of two values:
| Value | Authentication Method |
|---|---|
cloudflare_access |
Cloudflare Access JWT validation |
local_noauth |
Bypass authentication (development only) |
The preflight validator in src/lib/selfhost-preflight.ts checks these values at startup.
Request Flow Architecture
All self-hosted requests route through handleSelfHostedOpenSeoMcpRequest in src/server/mcp/transport.ts:
- Pre-flight handling (lines 86-88): Returns
200 OKwithMCP_CORS_HEADERSfor OPTIONS requests - Identity resolution (lines 90-95): Branch based on
AUTH_MODE - MCP props construction (lines 95-100): Call
createWorkersOAuthMcpPropswith resolved identity andgetPublicOrigin - Request delegation (lines 101-102): Forward to
createRequestHandlerwithout strict origin whitelist
Cloudflare Access Authentication
When AUTH_MODE=cloudflare_access, the middleware in src/middleware/ensure-user/cloudflareAccess.ts extracts and validates the Cloudflare Access JWT from request headers:
// .env configuration
AUTH_MODE=cloudflare_access
# BETTER_AUTH_URL not required
// Cloudflare Access token added automatically by Zero-Trust tunnel
// or manually via Access API
await fetch('https://selfhosted.example.com/mcp', {
method: 'POST',
headers: {
// Token injected by Cloudflare tunnel/browser
// Or explicitly: Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'search_console',
params: { /* ... */ },
}),
});
The resolveCloudflareAccessContext function transforms the JWT into a user/organization identity compatible with the MCP prop schema.
Local No-Auth Development
For isolated development or private networks, AUTH_MODE=local_noauth creates a synthetic identity without token validation:
// .env configuration
AUTH_MODE=local_noauth
// No authentication required
await fetch('http://127.0.0.1:8787/mcp', {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'search_console',
params: {},
}),
});
The synthetic identity is generated by resolveLocalNoAuthContext in src/middleware/ensure-user/delegated.ts.
Key Differences: Hosted vs Self-Hosted Authentication
| Aspect | Hosted Mode | Self-Hosted Mode |
|---|---|---|
| Identity source | Better Auth service | Cloudflare Access or synthetic |
| Required env vars | BETTER_AUTH_URL (HTTPS) |
AUTH_MODE |
| OAuth scope | mcp required |
Not applicable |
| Origin validation | Strict whitelist (hosted URL + extension) | Permissive (request's own host) |
| CORS headers | MCP_CORS_HEADERS from transport.ts |
Same headers, pre-flight handled separately |
| Entry function | handleAuthenticatedOpenSeoMcpRequest |
handleSelfHostedOpenSeoMcpRequest |
Both modes ultimately call createWorkersOAuthMcpProps from src/server/mcp/context.ts to construct the standardized MCP authentication context.
Critical Source Files
Reference these files when implementing or debugging OpenSEO MCP authentication:
src/server/mcp/oauth-provider.ts: Hosted OAuth provider setup, consent handling, and prop buildingsrc/server/mcp/transport.ts: Dual-mode request dispatcher with CORS and validation logicsrc/lib/auth.ts: Hosted base URL resolution and API client stacksrc/lib/selfhost-preflight.ts: Self-hosted environment validationsrc/middleware/ensure-user/cloudflareAccess.ts: Cloudflare Access JWT resolutionsrc/middleware/ensure-user/delegated.ts: Synthetic identity for no-auth modesrc/server/mcp/context.ts:MCP_AUTH_CONTEXT_PROPdefinition and prop schema
Summary
- Hosted authentication requires
BETTER_AUTH_URLand enforces Better Auth OAuth with strict origin validation and mandatorymcpscope - Self-hosted authentication uses
AUTH_MODEto select between Cloudflare Access JWT validation (cloudflare_access) and development bypass (local_noauth) - The same
createWorkersOAuthMcpPropsfunction standardizes context construction across both deployment models - Origin validation is the primary security divergence: hosted mode uses a strict whitelist while self-hosted falls back to permissive host matching
- All authentication flows are traceable through
handleAuthenticatedOpenSeoMcpRequest(hosted) andhandleSelfHostedOpenSeoMcpRequest(self-hosted) insrc/server/mcp/transport.ts
Frequently Asked Questions
What happens if BETTER_AUTH_URL is not set in hosted mode?
The application will fail to initialize the OAuth provider. The getHostedBaseUrl() function in src/lib/auth.ts validates that BETTER_AUTH_URL is present and properly formatted, throwing an error during bootstrap if missing.
Can I use local_noauth in production?
No. The local_noauth mode creates a synthetic identity without any token validation, making it suitable only for isolated development environments or private networks without external access. Production self-hosted deployments should use cloudflare_access or implement a custom authentication middleware.
How does Cloudflare Access integrate with the MCP context?
The resolveCloudflareAccessContext middleware extracts the Cloudflare Access JWT from the CF-Access-Jwt-Assertion header, validates it against Cloudflare's public keys, and maps claims to user-ID, email, and organization-ID fields that match the hosted authentication schema. This allows seamless portability of MCP tools between hosted and self-hosted deployments.
Is the mcp OAuth scope required for self-hosted mode?
No. The mcp scope is specific to the Better Auth OAuth flow in hosted mode. Self-hosted authentication relies on either Cloudflare Access token validation or bypasses scopes entirely in local_noauth mode. The MCP_SCOPE constant in src/server/mcp/context.ts is only checked by handleAuthenticatedOpenSeoMcpRequest in hosted deployments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →