# How OpenSEO Manages OAuth Tokens for MCP: Complete Implementation Guide

> Learn how OpenSEO manages OAuth tokens for MCP integration. Discover its Cloudflare-hosted OAuth flow, @cloudflare/workers-oauth-provider library, and Cloudflare KV token storage.

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

---

**OpenSEO manages OAuth tokens for its Managed Cloud Platform (MCP) integration using a Cloudflare-hosted OAuth flow built on the `@cloudflare/workers-oauth-provider` library, with token storage and retrieval backed by Cloudflare KV.**

This article examines the complete token lifecycle in the `every-app/open-seo` repository, from provider initialization through automatic refresh to request-time retrieval. Understanding this architecture helps developers implement secure, scalable OAuth for their own MCP integrations.

## OAuth Provider Architecture

The MCP OAuth system centers on [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts), which exports `createOpenSeoOAuthProvider` to configure and instantiate the provider. This factory function wires together callbacks that drive authorization, consent, token issuance, and refresh.

```typescript
// src/server/mcp/oauth-provider.ts (L438-L452)
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';

export function createOpenSeoOAuthProvider(handler: RequestHandler) {
  return new OAuthProvider({
    onAuthorize: handleOAuthAuthorize,
    onConsent: handleOAuthConsent,
    onToken: handleOAuthToken,
    onRefresh: handleOAuthRefresh,
    // additional configuration
  });
}

```

Each callback handles a specific OAuth lifecycle event. The provider instance is consumed by Cloudflare Workers to intercept and process OAuth requests before they reach application handlers.

## Token Storage in Cloudflare KV

When `handleOAuthToken` executes—either after initial user consent or during token refresh—the provider persists token data to **Cloudflare KV** using a key derived from the MCP authentication context.

```typescript
// src/server/mcp/oauth-provider.ts (L424-L426)
const parsed = workersOAuthMcpPropsSchema.parse(tokenData);
await env.MCP_TOKENS.put(context[MCP_AUTH_CONTEXT_PROP], JSON.stringify(parsed));

```

The `workersOAuthMcpPropsSchema` validates token structure before storage, ensuring `access_token`, `refresh_token`, `expires_at`, and scope fields are present. The `MCP_AUTH_CONTEXT_PROP` constant provides a stable key namespace per authenticated MCP session.

## Automatic Token Refresh

OpenSEO implements `handleOAuthRefresh` to exchange near-expiry access tokens without user intervention. This callback (L258-L272) coordinates with Google's token endpoint, validates the response, and atomically updates the KV entry.

```typescript
// src/server/mcp/oauth-provider.ts (L258-L272)
async function handleOAuthRefresh(params: OAuthRefreshParams) {
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: params.refreshToken,
      client_id: env.GOOGLE_CLIENT_ID,
      client_secret: env.GOOGLE_CLIENT_SECRET,
    }),
  });
  
  const refreshed = await response.json();
  const validated = workersOAuthMcpPropsSchema.parse(refreshed);
  
  // KV update happens automatically via onToken callback
  return validated;
}

```

The OAuth library invokes this handler proactively before token expiration, eliminating manual refresh logic from application code.

## Request-Time Token Retrieval

The MCP transport layer in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts#L183-L201) retrieves stored tokens for each authenticated request. When a valid token exists, it attaches the `Authorization` header; otherwise, it triggers an interactive OAuth flow.

```typescript
// src/server/mcp/transport.ts (L183-L201)
export async function authenticateMcpRequest(request: Request, env: Env) {
  const context = getMcpContext(request);
  const tokenData = await env.MCP_TOKENS.get(context.authKey);
  
  if (!tokenData) {
    throw new McpAuthRequiredError('Token missing, re-authentication required');
  }
  
  const token = workersOAuthMcpPropsSchema.parse(JSON.parse(tokenData));
  
  if (isExpired(token.expires_at)) {
    // Delegates to provider's refresh mechanism
    return refreshAndRetry(request, env);
  }
  
  return {
    headers: { Authorization: `Bearer ${token.access_token}` },
  };
}

```

This pattern decouples token management from business logic—transport concerns remain in [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts) while OAuth semantics live in [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts).

## Error Handling and Re-Authentication

When token refresh fails—typically due to revoked scopes or deleted grants—the provider throws an `OAuthError` with code `invalid_scope` (L418-L424). The transport layer catches this and forces re-authentication.

```typescript
// src/server/mcp/oauth-provider.ts (L418-L424)
if (refreshResponse.error === 'invalid_grant') {
  throw new OAuthError('invalid_scope', 'Refresh token revoked, re-consent required');
}

```

This error propagates through [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts), which returns a `302 Redirect` to the OAuth authorization endpoint, preserving the original request URL for post-auth resumption.

## Practical Usage Examples

### Creating the OAuth Provider in a Cloudflare Worker

```typescript
// worker entry point
import { createOpenSeoOAuthProvider } from '@/server/mcp/oauth-provider';

export const onRequest = createOpenSeoOAuthProvider((request, env) => {
  // Application logic with pre-authenticated context
  return handleMcpRequest(request, env);
});

```

### Accessing Tokens in MCP Handlers

```typescript
import { getOAuthHelpers } from '@/server/mcp/oauth-provider';

export async function fetchGoogleSearchConsoleData(request: Request, env: Env) {
  const oauth = getOAuthHelpers(env);
  const token = await oauth.getAccessToken(); // KV lookup with auto-refresh
  
  const response = await fetch('https://www.googleapis.com/webmasters/v3/sites', {
    headers: { Authorization: `Bearer ${token.access_token}` },
  });
  
  return response.json();
}

```

### Forcing Manual Token Refresh (Diagnostic Use)

```typescript
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
import type { OpenSeoOAuthEnv } from '@/server/mcp/oauth-provider';

export async function diagnosticRefresh(env: OpenSeoOAuthEnv) {
  // Rarely needed—the provider handles refresh automatically
  const provider = new OAuthProvider({ /* configuration */ });
  await provider.refresh();
  return { status: 'refreshed' };
}

```

## Testing Token Lifecycle

Unit tests in [`src/server/mcp/oauth-provider.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.test.ts#L174-L242) verify token persistence, refresh behavior, and error paths using mocked KV and OAuth helpers.

```typescript
// Excerpt from test suite
test('should store token in KV after successful authorization', async () => {
  const mockToken = {
    access_token: 'test-access',
    refresh_token: 'test-refresh',
    expires_at: Date.now() + 3600000,
    scope: 'https://www.googleapis.com/auth/webmasters.readonly',
  };
  
  await handleOAuthToken(mockContext, mockToken);
  
  const stored = await env.MCP_TOKENS.get('mcp:user:123');
  expect(stored).toBeDefined();
  expect(JSON.parse(stored!).access_token).toBe('test-access');
});

```

These tests mock the OAuth library's network calls, enabling fast, deterministic validation of storage and retrieval logic without external dependencies.

## Key Implementation Files

| File | Responsibility |
|------|--------------|
| [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) | OAuth provider creation, KV token storage, refresh handling |
| [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) | Request-time token retrieval, header injection, fallback flows |
| [`src/server/mcp/oauth-provider.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.test.ts) | Unit tests for token lifecycle and error conditions |
| [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts) | Context extraction utilities for OAuth data |
| [`src/server/features/google/oauth-config.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/google/oauth-config.ts) | Google OAuth client configuration detection |

## Summary

- **`createOpenSeoOAuthProvider`** configures the OAuth provider with lifecycle callbacks for authorization, consent, token issuance, and refresh.
- **Cloudflare KV** provides durable, globally distributed token storage keyed by MCP authentication context.
- **`handleOAuthRefresh`** automates token exchange before expiration, eliminating manual refresh logic.
- **[`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts)** retrieves tokens per-request, attaches Bearer headers, and triggers re-authentication when tokens are missing or invalid.
- **`OAuthError` with `invalid_scope`** signals irrecoverable refresh failures, forcing users through the consent flow again.
- **Comprehensive test coverage** in [`oauth-provider.test.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.test.ts) validates storage, refresh, and error handling without external network calls.

## Frequently Asked Questions

### What storage backend does OpenSEO use for MCP OAuth tokens?

OpenSEO uses **Cloudflare KV** for OAuth token storage. Tokens are written to a KV namespace bound as `MCP_TOKENS` using keys derived from the MCP authentication context via `MCP_AUTH_CONTEXT_PROP`.

### How does automatic token refresh work in OpenSEO's MCP integration?

The `handleOAuthRefresh` callback (L258-L272 in [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts)) exchanges the `refresh_token` with Google's OAuth endpoint when the `OAuthProvider` detects an approaching expiration. The refreshed token immediately replaces the stored entry in KV, ensuring subsequent requests use valid credentials without user interruption.

### What happens when a refresh token is revoked or expires?

The provider throws an `OAuthError` with code `invalid_scope` (L418-L424), which [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts) catches to redirect the user back through the OAuth consent screen. This recovery flow preserves the original request URL for seamless resumption after re-authentication.

### Can I access MCP OAuth tokens directly in my application code?

Yes—import `getOAuthHelpers` from [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) to retrieve tokens with automatic refresh. The helper abstracts KV operations and expiration checking, returning a validated `access_token` ready for API calls.