# How to Set Up Authentication for OpenSEO's MCP Server in Hosted vs Self-Hosted Modes

> Learn to set up authentication for OpenSEO MCP server. Explore OAuth for hosted and Cloudflare Access or no-auth for self-hosted modes with the AUTH_MODE variable.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-09-01

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) where `createOpenSeoOAuthProvider` lazily instantiates the OAuth provider:

```typescript
// 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:

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`](https://github.com/every-app/open-seo/blob/main/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

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) extracts and validates the Cloudflare Access JWT from request headers:

```typescript
// .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:

```typescript
// .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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/transport.ts) | Same headers, pre-flight handled separately |
| **Entry function** | `handleAuthenticatedOpenSeoMcpRequest` | `handleSelfHostedOpenSeoMcpRequest` |

Both modes ultimately call `createWorkersOAuthMcpProps` from [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts)**: Hosted OAuth provider setup, consent handling, and prop building
- **[`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)**: Dual-mode request dispatcher with CORS and validation logic
- **[`src/lib/auth.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth.ts)**: Hosted base URL resolution and API client stack
- **[`src/lib/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/selfhost-preflight.ts)**: Self-hosted environment validation
- **[`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts)**: Cloudflare Access JWT resolution
- **[`src/middleware/ensure-user/delegated.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/delegated.ts)**: Synthetic identity for no-auth mode
- **[`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)**: `MCP_AUTH_CONTEXT_PROP` definition and prop schema

---

## 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) is only checked by `handleAuthenticatedOpenSeoMcpRequest` in hosted deployments.