# How to Configure OAuth for MCP in Self-Hosted Mode: A Complete Guide

> Configure OAuth for MCP self-hosted mode by setting environment variables, deploying Cloudflare Workers, and completing the OAuth 2.0 flow to get access tokens for the mcp endpoint.

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

---

**To configure OAuth for MCP in self-hosted mode, set the `BETTER_AUTH_SECRET` and OAuth client credentials in your environment variables, deploy the Cloudflare Workers script, and complete the standard OAuth 2.0 flow via `/api/auth/oauth2/authorize` to obtain access tokens for the `/mcp` endpoint.**

The OpenSEO repository provides a self-hosted Multi-Channel Protocol (MCP) server that exposes SEO tools—such as keyword research, SERP analysis, and backlink tracking—to external AI agents. When deploying this system in self-hosted mode, configuring OAuth 2.0 is essential to secure access to these tools without relying on external identity providers. The entire OAuth implementation runs inside your Cloudflare Workers environment and is fully customizable through the source code in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts).

## Prerequisites and Environment Variables

Before deploying the MCP server, you must configure several critical environment variables. The system performs strict pre-flight checks to ensure security prerequisites are met.

Set the following variables in your Workers environment:

```dotenv

# Required: Encryption key for OAuth tokens (must be ≥ 32 characters)

BETTER_AUTH_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Required: Your OAuth client credentials

OAUTH_CLIENT_ID=your-mcp-client-id
OAUTH_CLIENT_SECRET=your-mcp-client-secret

# Optional: KV namespace for storing OAuth state

OAUTH_KV=oauth_kv_namespace

```

The `BETTER_AUTH_SECRET` validation occurs in [`src/lib/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/selfhost-preflight.ts) (lines 174-182). If this secret is missing or shorter than 32 characters, the self-hosted instance will emit a clear error and disable Google Search Console OAuth features.

## Understanding the OAuth Provider Architecture

The OAuth implementation is modular and consists of several interconnected components:

- **`createOpenSeoOAuthProvider`** in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) (lines 18-33): Initializes the provider with MCP-specific routes, token time-to-live (TTL) settings, and supported scopes.
- **`createWorkersOAuthMcpProps`** in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) (lines 19-28): Constructs the authentication context containing user ID, email, and organization ID that tools access during execution.
- **Route handlers**: The provider automatically registers `/api/auth/oauth2/authorize`, `/api/auth/oauth2/token`, `/api/auth/oauth2/register`, and the MCP API endpoint at `/mcp`.

Token lifetimes are defined as constants: access tokens expire after 24 hours (`MCP_ACCESS_TOKEN_TTL_SECONDS` at line 46) and refresh tokens last 30 days (`MCP_REFRESH_TOKEN_TTL_SECONDS` at line 47).

## Step-by-Step Configuration

### Deploy the Workers OAuth Provider

The main entry point at [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) instantiates the OAuth provider without requiring additional boilerplate:

```typescript
// src/server.ts (excerpt)
import { createOpenSeoOAuthProvider } from "@/server/mcp/oauth-provider";

const openSeoOAuthProvider = createOpenSeoOAuthProvider(appFetch);

return openSeoOAuthProvider.fetch(request, env as OpenSeoOAuthEnv, ctx);

```

This single integration registers all OAuth endpoints and the MCP tool API. The provider handles request routing, consent management, and token issuance automatically.

### Define MCP OAuth Scopes

Scopes control access levels to MCP tools. The supported scopes are defined in [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts):

```typescript
// src/lib/oauth-resource.ts (excerpt)
export const MCP_OAUTH_SCOPES = ["mcp", "gsc", "search-console"];
export const MCP_SCOPE = "mcp";

```

The base `"mcp"` scope is mandatory for accessing core tools. Additional scopes like `"gsc"` enable specific integrations such as Google Search Console data access.

### Execute the Authorization Flow

**Step 1: Direct users to the authorization endpoint**

Construct the authorization URL with the required parameters:

```bash
GET https://<your-host>/api/auth/oauth2/authorize \
    ?response_type=code \
    &client_id=$OAUTH_CLIENT_ID \
    &redirect_uri=https://<your-host>/oauth/callback \
    &scope=mcp \
    &state=xyz123

```

The server redirects to `/oauth-consent`, where the user accepts or denies the request. The `buildConsentUrl` function (lines 85-95 in [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts)) constructs this consent screen.

**Step 2: Exchange the authorization code for tokens**

After the user accepts, exchange the code at the token endpoint:

```bash
POST https://<your-host>/api/auth/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code&code=CODE_FROM_STEP_1&redirect_uri=https://<your-host>/oauth/callback&client_id=$OAUTH_CLIENT_ID&client_secret=$OAUTH_CLIENT_SECRET"

```

The JSON response includes:

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "expires_in": 86400,
  "token_type": "Bearer",
  "scope": "mcp"
}

```

### Call MCP Tools with Access Tokens

With a valid access token, invoke MCP tools via JSON-RPC requests to the `/mcp` endpoint:

```bash
POST https://<your-host>/mcp \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "jsonrpc": "2.0",
      "method": "get_backlinks_profile",
      "params": { "domain": "example.com" },
      "id": 1
    }'

```

The `handleAuthenticatedOpenSeoMcpRequest` function in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) validates the token, extracts the auth context via `createWorkersOAuthMcpProps`, and dispatches the tool call.

### Refresh Expired Tokens

Since access tokens expire after 24 hours, implement token refresh in your client:

```bash
POST https://<your-host>/api/auth/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$OAUTH_CLIENT_ID&client_secret=$OAUTH_CLIENT_SECRET"

```

The `tokenExchangeCallback` in the provider options (lines 33-37) handles scope validation during refresh using `withWorkersOAuthMcpScopes`.

## Troubleshooting Common Configuration Issues

| Issue | Source Location | Resolution |
|-------|----------------|------------|
| **Missing `BETTER_AUTH_SECRET`** | [`src/lib/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/selfhost-preflight.ts) (lines 174-182) | Ensure the secret is set and ≥ 32 characters; check Workers logs for the specific error message. |
| **Invalid request origin** | `csrfProtected` in [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts) (lines 48-50) | Returns HTTP 403 with "Invalid request origin"; verify your `redirect_uri` matches the registered client configuration. |
| **Missing required MCP scope** | `getGrantedMcpScopes` (lines 39-48) | Returns HTTP 400 with "The mcp scope is required"; ensure your authorization request includes `scope=mcp`. |
| **Unauthenticated consent attempts** | `resolveContextForConsent` (lines 73-81) | Returns `null` context, triggering a redirect to sign-in; ensure users are authenticated before initiating OAuth. |

## Summary

- **Set `BETTER_AUTH_SECRET`** (minimum 32 characters) and OAuth client credentials in your environment to enable encryption and token security.
- **Deploy the provider** via [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), which automatically registers `/api/auth/oauth2/authorize`, `/api/auth/oauth2/token`, and `/mcp` endpoints.
- **Request the `"mcp"` scope** during authorization to access core SEO tools; additional scopes like `"gsc"` enable specific integrations.
- **Exchange codes for tokens** using standard OAuth 2.0 flows, then call MCP tools via JSON-RPC to `/mcp` with Bearer token authentication.
- **Implement refresh logic** to handle the 24-hour access token expiration using the 30-day refresh tokens.

## Frequently Asked Questions

### What is the minimum length for `BETTER_AUTH_SECRET`?

The `BETTER_AUTH_SECRET` must be at least 32 characters long. This requirement is enforced in [`src/lib/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/selfhost-preflight.ts) (lines 174-182). If the secret is missing or too short, the self-hosted deployment will disable Google Search Console OAuth features and log a descriptive error.

### How long do MCP access tokens remain valid?

Access tokens expire after 24 hours (86,400 seconds), defined by `MCP_ACCESS_TOKEN_TTL_SECONDS` in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) (line 46). Refresh tokens remain valid for 30 days (`MCP_REFRESH_TOKEN_TTL_SECONDS` at line 47), allowing clients to maintain long-term sessions without re-authorization.

### Can I restrict which MCP tools a client can access?

Yes, scope-based access control is implemented via the `MCP_OAUTH_SCOPES` array in [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts). While the base `"mcp"` scope grants access to core tools, you can define additional scopes (such as `"gsc"` for Google Search Console) and modify the `tokenExchangeCallback` in [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts) to validate specific scopes before issuing tokens.

### Why does my authorization request return "Invalid request origin"?

This error originates from the `csrfProtected` middleware in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) (lines 48-50) and indicates a CSRF protection failure. Verify that the `redirect_uri` parameter in your authorization request exactly matches the origin registered for your OAuth client, and ensure the request includes proper origin headers.