How OpenSEO Manages OAuth Tokens for MCP: Complete Implementation Guide
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, which exports createOpenSeoOAuthProvider to configure and instantiate the provider. This factory function wires together callbacks that drive authorization, consent, token issuance, and refresh.
// 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.
// 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.
// 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 retrieves stored tokens for each authenticated request. When a valid token exists, it attaches the Authorization header; otherwise, it triggers an interactive OAuth flow.
// 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 while OAuth semantics live in 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.
// 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, 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
// 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
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)
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 verify token persistence, refresh behavior, and error paths using mocked KV and OAuth helpers.
// 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 |
OAuth provider creation, KV token storage, refresh handling |
src/server/mcp/transport.ts |
Request-time token retrieval, header injection, fallback flows |
src/server/mcp/oauth-provider.test.ts |
Unit tests for token lifecycle and error conditions |
src/lib/oauth-resource.ts |
Context extraction utilities for OAuth data |
src/server/features/google/oauth-config.ts |
Google OAuth client configuration detection |
Summary
createOpenSeoOAuthProviderconfigures 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.
handleOAuthRefreshautomates token exchange before expiration, eliminating manual refresh logic.transport.tsretrieves tokens per-request, attaches Bearer headers, and triggers re-authentication when tokens are missing or invalid.OAuthErrorwithinvalid_scopesignals irrecoverable refresh failures, forcing users through the consent flow again.- Comprehensive test coverage in
oauth-provider.test.tsvalidates 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) 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 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 to retrieve tokens with automatic refresh. The helper abstracts KV operations and expiration checking, returning a validated access_token ready for API calls.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →