How the Open-SEO OAuth Provider Works for MCP Authentication: A Complete Technical Guide

Open-SEO implements MCP authentication through a custom OAuth 2.0 provider built on Cloudflare's @cloudflare/workers-oauth-provider library, using short-lived access tokens, mandatory scope enforcement, and dynamic client registration to secure Model-Context-Protocol endpoints.

The every-app/open-seo repository provides a self-contained OAuth 2.0 provider that authenticates MCP (Model-Context-Protocol) requests. This implementation bridges standard OAuth flows with MCP-specific requirements, ensuring every tool call originates from a properly scoped and validated client.

Core OAuth Provider Architecture

The createOpenSeoOAuthProvider() function in [src/server/mcp/oauth-provider.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) wires together three interconnected systems:

Component Constants/Functions Purpose
OAuth endpoints OAUTH_AUTHORIZE_PATH, OAUTH_TOKEN_PATH, OAUTH_REGISTER_PATH Handle Authorization Code flow, token issuance, and Dynamic Client Registration
Token lifecycles MCP_ACCESS_TOKEN_TTL_SECONDS (24h), MCP_REFRESH_TOKEN_TTL_SECONDS (30d), MCP_CLIENT_REGISTRATION_TTL_SECONDS (365d) Balance security with usability for automated MCP clients
Request routing createOpenSeoMcpServer + handleAuthenticatedOpenSeoMcpRequest Bridge validated tokens to actual MCP tool execution

The provider stores authentication context in Cloudflare KV, enabling stateless validation of bearer tokens on every request.

Step-by-Step OAuth Flow for MCP

1. Authorization Request Handling

When a client initiates authentication, handleOAuthAuthorizeRequest() (lines 51-70 of the provider) processes the incoming request:

// Client redirects user to:
// /api/auth/oauth2/authorize?response_type=code&client_id=...&scope=offline_access+mcp&state=...

// The provider parses with:
const authRequest = await oauth.parseAuthRequest(request);

If the user lacks a session, they redirect to the sign-in page. Authenticated users proceed to the consent interface at /oauth-consent.

The consent endpoint (/api/oauth/consent) invokes handleOAuthConsentResponse(). This function:

// Scope enforcement ensures 'mcp' is always present
const grantedScopes = getGrantedMcpScopes(requestedScopes); // ['mcp', ...]

3. Token Exchange with Lifecycle Management

After consent, clients exchange the authorization code at /api/auth/oauth2/token. The tokenExchangeCallback (lines 416-431 of the provider) executes on every exchange:

// Token request (POST to /api/auth/oauth2/token)
const body = new URLSearchParams({
  grant_type: "authorization_code",
  code: AUTHORIZATION_CODE,
  redirect_uri: REDIRECT_URI,
  client_id: CLIENT_ID,
  code_verifier: PKCE_VERIFIER,
});

const response = await fetch(`${BASE_URL}/api/auth/oauth2/token`, {
  method: "POST",
  body,
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
// Returns: { access_token, refresh_token, expires_in, scope }

The callback verifies scope integrity and retrieves the stored workersOAuthMcpPropsSchema context for the new token pair.

4. MCP Request Authentication and Routing

Every protected MCP request passes through handleAuthenticatedOpenSeoMcpRequest() in [src/server/mcp/transport.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) (lines 45-62):

async function callMcpTool(accessToken: string) {
  const response = await fetch(`${BASE_URL}/mcp`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Mcp-Method": "whoami",
      "Mcp-Name": "whoami",
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: "whoami",
      id: 1,
    }),
  });
  return response.json();
}

The transport layer validates three critical properties:

  1. Token payload conforms to hostedWorkersOAuthMcpPropsSchema
  2. clientId and scopes fields are present
  3. The mcp scope is explicitly granted

5. Token Refresh for Long-Running Clients

MCP clients with offline_access scope can obtain fresh access tokens:

async function rotateCredentials(refreshToken: string) {
  const response = await fetch(`${BASE_URL}/api/auth/oauth2/token`, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
    }),
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
  });
  return response.json(); // New token pair with rotated refresh token
}

The refresh flow uses the same tokenExchangeCallback, ensuring consistent scope validation.

MCP-OAuth Integration Mechanisms

Resource Identification and Discovery

[src/lib/oauth-resource.ts](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts#L5-L7) defines the canonical MCP resource:

export function getMcpResource(baseUrl: string): string {
  return `${baseUrl}/mcp`;
}

This URL appears in the OAuth provider's discovery document, enabling clients to locate protected endpoints dynamically.

Authentication Context Propagation

The McpProps interface (defined in [src/server/mcp/context.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)) serializes into KV storage:

Field Source Usage in Tools
userId Signed-in session User-specific data filtering
clientId OAuth client registration Rate limiting and audit logging
scopes Granted consent Capability enforcement

createMcpToolContext() extracts these properties for every tool invocation, making them available to whoami, search-console-tools, and other MCP capabilities.

Expired Data Cleanup

The purgeExpiredData() method (lines 64-71 of the provider) removes orphaned grants and tokens that KV TTLs cannot fully reclaim. A Cloudflare cron trigger invokes this routine to maintain storage efficiency.

Complete Provider Initialization Example

import { createOpenSeoOAuthProvider } from "@/server/mcp/oauth-provider";

// appFetch handles non-MCP routes (UI, API, etc.)
const appFetch = async (request: Request, env: Env, ctx: ExecutionContext) => {
  return router.fetch(request, env, ctx);
};

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const oauthProvider = createOpenSeoOAuthProvider(() => appFetch(request, env, ctx));
    return oauthProvider.fetch(request, env, ctx);
  },
};

This pattern allows the OAuth provider to intercept authentication routes while transparently passing other requests to the application router.

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) Provider factory, endpoint handlers, token lifecycle, consent flow
[src/server/mcp/context.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) McpProps definition, schema validation, tool context construction
[src/server/mcp/transport.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) Token validation, CORS handling, request forwarding to MCP server
[src/lib/oauth-resource.ts](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts) Resource URL construction, supported scopes registry
[src/server/mcp/oauth-refresh.e2e.test.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-refresh.e2e.test.ts) End-to-end token lifecycle verification

Summary

  • OAuth 2.0 foundation: Built on @cloudflare/workers-oauth-provider with custom MCP adaptations
  • Mandatory scope enforcement: Every token must include the mcp scope; absence causes immediate rejection
  • Tiered token lifetimes: 24-hour access tokens, 30-day refresh tokens, 1-year client registrations
  • Stateless validation: KV-stored McpProps enables distributed request authentication without session affinity
  • Complete flow coverage: Authorization, consent, token exchange, refresh, and cleanup all implemented

Frequently Asked Questions

What OAuth 2.0 grant types does the Open-SEO MCP provider support?

The provider implements Authorization Code flow with PKCE for initial authentication and Refresh Token flow for credential rotation. These grants are handled by handleOAuthAuthorizeRequest() and the tokenExchangeCallback respectively, as defined in src/server/mcp/oauth-provider.ts.

Why is the mcp scope mandatory for all tokens?

The getGrantedMcpScopes() function (lines 26-38 of the provider) explicitly injects mcp into every granted scope set. This design ensures that handleAuthenticatedOpenSeoMcpRequest() in transport.ts can definitively reject any token lacking MCP authorization, preventing scope confusion attacks.

How does the provider handle token storage and validation?

Authentication context is serialized as workersOAuthMcpPropsSchema and stored in Cloudflare KV at token creation time. Subsequent requests validate the bearer token against hostedWorkersOAuthMcpPropsSchema, which requires clientId and scopes fields. This schema-based approach provides type-safe validation without database queries on the hot path.

Can the OAuth provider be used for non-MCP authentication?

Yes. The createOpenSeoOAuthProvider() factory accepts an arbitrary appFetch handler, allowing the same OAuth infrastructure to protect UI routes, REST APIs, and other endpoints. However, the specialized TTL constants (MCP_ACCESS_TOKEN_TTL_SECONDS, etc.) and scope enforcement are optimized for automated MCP client patterns rather than interactive user sessions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →