# How OpenSEO Handles MCP Server Authentication: OAuth 2.0 and Self-Hosted Methods

> Discover how OpenSEO secures MCP server authentication with OAuth 2.0 and self-hosted methods. Learn about scope validation and secure tool execution.

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

---

**OpenSEO uses a layered authentication system for its MCP server that supports OAuth 2.0 for external clients and first-party contexts for self-hosted deployments, enforcing scope validation and storing auth context in AsyncLocalStorage for secure tool execution.**

The `every-app/open-seo` repository implements a robust Model Context Protocol (MCP) server protected by multiple authentication strategies. Understanding OpenSEO MCP server authentication requires examining how the codebase balances security for public API consumers with flexibility for internal and self-hosted deployments.

## Understanding OpenSEO's MCP Authentication Architecture

OpenSEO's MCP server operates in two distinct authentication modes depending on deployment context. The **OAuth 2.0 provider** handles external third-party clients through standard authorization flows, while **self-hosted deployments** leverage first-party contexts via Cloudflare Access JWTs or local admin credentials. Both paths ultimately generate a `McpToolAuthContext` that propagates through the request lifecycle using Node.js `AsyncLocalStorage`.

## OAuth 2.0 Authentication Flow

### Creating the OAuth Provider

The authentication entry point resides in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts), where `createOpenSeoOAuthProvider` initializes a Cloudflare Workers-compatible OAuth implementation. This provider exposes standard endpoints under `/api/auth/oauth2/*` and enforces the `mcp` scope requirement defined in [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts).

When initializing the provider, the system configures token lifetimes: access tokens remain valid for **24 hours** and refresh tokens persist for **30 days**. The provider maps token properties into the MCP authentication context using the `MCP_AUTH_CONTEXT_PROP` constant.

### Scope Validation and Token Generation

Before issuing tokens, `getGrantedMcpScopes` validates that the requested scope includes the mandatory `MCP_SCOPE` value (`"mcp"`). If the scope check fails, the function throws an error that translates to a **400 Bad Request** response for the client. Successful validation proceeds to `oauth.completeAuthorization`, which generates the token pair and constructs the redirect URL.

### Building the Auth Context

Upon successful authorization, the system creates a `McpToolAuthContext` in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) (lines 22-37). This context encapsulates:
- User IDs and organization membership
- The MCP resource URL (audience)
- Granted OAuth scopes
- Additional token properties

The context attaches to the request as `openSeoAuth`, making user identity available to downstream MCP tool handlers.

## Transport Layer Security and Request Handling

### Authenticated Request Handler

Incoming MCP requests hit `handleAuthenticatedOpenSeoMcpRequest` in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) (lines 44-58). This function extracts authentication data using `workersOAuthMcpPropsSchema` and performs critical validation:

1. Verifies the auth context exists
2. Confirms the `mcp` scope is present
3. Returns **403 "MCP auth context required"** if either check fails

Validated requests execute within `runWithMcpToolAuthContext`, which stores the context in `AsyncLocalStorage` for the duration of the request.

### Self-Hosted Authentication Methods

For self-hosted instances, `handleSelfHostedOpenSeoMcpRequest` (lines 62-90 in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)) provides alternative authentication paths:

- **Cloudflare Access JWT**: Resolves tokens via [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts), converting the JWT into a first-party user context
- **Local admin context**: Uses [`src/middleware/ensure-user/delegated.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/delegated.ts) to provide development-mode authentication without external providers

Both methods call `buildFirstPartyMcpAuthContext` to create a compatible auth context, then forward to the same MCP handlers used in OAuth mode.

## Async-Local Storage and Tool-Level Enforcement

The `mcpToolAuthContextStorage` instance in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) maintains request-scoped authentication state. Every MCP tool implementation calls `requireMcpToolAuthContext` (lines 92-108) to retrieve the stored context, ensuring:
- Tools execute only with verified user identity
- Authentication persists across async operations
- Each request maintains isolated auth state

This pattern prevents unauthorized tool execution by making authentication mandatory at the implementation level rather than relying solely on transport-layer checks.

## Implementation Examples

### Obtaining an MCP OAuth Token (Client-Side)

```typescript
// Redirect user to authorization endpoint
const authUrl = new URL('/api/auth/oauth2/authorize', window.location.origin);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'mcp');  // Required scope
window.location.href = authUrl.toString();

// After consent, server returns:
// { redirectTo: "https://your-app.com/callback?code=XYZ&state=…" }

```

### Exchanging Authorization Code for Access Token

```typescript
const tokenResp = await fetch('/api/auth/oauth2/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: CODE_FROM_QUERY,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: CODE_VERIFIER,
  }),
});
const { access_token } = await tokenResp.json();

```

### Making Authenticated MCP Requests

```typescript
const mcpResp = await fetch('/mcp', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'Content-Type': 'application/json',
    'mcp-protocol-version': '2.0',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'get_keywords',
    params: { projectId: 'proj_123' },
  }),
});

```

### Self-Hosted Cloudflare Access Authentication

```typescript
// Uses existing Cloudflare Access JWT from browser session
const resp = await fetch('https://selfhosted.example.com/mcp', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${CLOUDFLARE_ACCESS_JWT}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ /* MCP payload */ }),
});

```

### Local Development (No Authentication)

```typescript
// Local dev mode automatically injects admin context
await fetch('http://localhost:8787/mcp', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ /* MCP payload */ }),
});

```

## Summary

- **OpenSEO MCP server authentication** implements OAuth 2.0 for external clients via `createOpenSeoOAuthProvider` in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts)
- The mandatory `mcp` scope enforcement occurs through `getGrantedMcpScopes`, returning 400 errors for invalid requests
- **Self-hosted deployments** bypass OAuth using `handleSelfHostedOpenSeoMcpRequest`, supporting Cloudflare Access JWTs or local admin contexts via `buildFirstPartyMcpAuthContext`
- Authentication contexts persist in `AsyncLocalStorage` (`mcpToolAuthContextStorage`), with `requireMcpToolAuthContext` ensuring tool-level validation
- Access tokens expire after 24 hours, while refresh tokens remain valid for 30 days according to the provider configuration

## Frequently Asked Questions

### What OAuth scopes are required for OpenSEO MCP authentication?

OpenSEO requires the **`mcp`** scope for all MCP server authentication requests. The `getGrantedMcpScopes` function in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) validates this scope strictly, throwing an error that results in a 400 response if the scope is missing from the authorization request.

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

Access tokens expire after **24 hours**, while refresh tokens remain valid for **30 days**. These durations are configured in the `createOpenSeoOAuthProvider` implementation, allowing clients to refresh short-lived access tokens without re-prompting users for consent.

### Can I use OpenSEO's MCP server without OAuth in self-hosted environments?

Yes. Self-hosted deployments can use `handleSelfHostedOpenSeoMcpRequest` in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts), which supports **Cloudflare Access JWTs** for authenticated users or **local admin contexts** for development. The latter requires no Authorization header and is handled by [`src/middleware/ensure-user/delegated.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/delegated.ts).

### How does OpenSEO validate authentication contexts internally?

OpenSEO stores authentication contexts in `AsyncLocalStorage` via `mcpToolAuthContextStorage` and enforces validation through `requireMcpToolAuthContext` in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts). Every MCP tool calls this function to retrieve the verified user identity, ensuring tools execute only within authenticated request scopes.