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.

The OAuth flow follows three stages defined in the same file:

  1. handleOAuthAuthorizeRequest (lines 51-70): Parses authorization requests and redirects to /oauth-consent
  2. handleOAuthConsentResponse (lines 72-120): Validates CSRF origin, resolves hosted user context, and attaches the MCP_SCOPE
  3. createWorkersOAuthMcpProps (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_PROP in the request context
  • Valid MCP_SCOPE in 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:

  1. Pre-flight handling (lines 86-88): Returns 200 OK with MCP_CORS_HEADERS for OPTIONS requests
  2. Identity resolution (lines 90-95): Branch based on AUTH_MODE
  3. MCP props construction (lines 95-100): Call createWorkersOAuthMcpProps with resolved identity and getPublicOrigin
  4. Request delegation (lines 101-102): Forward to createRequestHandler without 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:


Summary

  • Hosted authentication requires BETTER_AUTH_URL and enforces Better Auth OAuth with strict origin validation and mandatory mcp scope
  • Self-hosted authentication uses AUTH_MODE to select between Cloudflare Access JWT validation (cloudflare_access) and development bypass (local_noauth)
  • The same createWorkersOAuthMcpProps function 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) and handleSelfHostedOpenSeoMcpRequest (self-hosted) in src/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →