How OAuth for MCP Works in Hosted vs Self‑Hosted Mode: A Complete Technical Guide

OpenSEO uses Cloudflare Workers‑OAuth‑Provider to issue and validate OAuth tokens for its MCP API, with strict identity enforcement in hosted mode and flexible external provider support in self‑hosted mode.

The Model‑Chat‑Protocol (MCP) API in the open-seo repository supports two distinct authentication architectures. Whether you run the managed SaaS or deploy to your own Cloudflare account, understanding these differences is critical for securing MCP tool calls correctly.

How Hosted Mode Enforces Built‑In OAuth

In hosted mode, OpenSEO acts as its own OAuth authority. The platform creates a dedicated OAuthProvider via createOpenSeoOAuthProvider in src/server/mcp/oauth-provider.ts (lines 37‑44), exposing standard endpoints at /api/auth/oauth2/*.

Provider Initialization and Endpoints

The provider is instantiated lazily with the current SaaS base URL:

// src/server/mcp/oauth-provider.ts
const provider = createOpenSeoOAuthProvider({
  baseUrl: getHostedBaseUrl(),
  // ... additional configuration
});

This provider supplies three critical routes:

  • /authorize – Initiates the consent flow
  • /token – Exchanges authorization codes for access tokens
  • /register – Allows dynamic client registration

Session Blocker Redirects Anonymous Users

Before any OAuth flow begins, getAuthorizeSessionBlocker enforces authentication. It calls resolveHostedContext to verify a signed‑in user session. If the check fails, the user is redirected to /sign‑in rather than proceeding to consent.

The implementation in src/server/mcp/oauth-provider.ts (lines 62‑71) distinguishes hosted from self‑hosted behavior through the return value: hosted mode throws or redirects, while self‑hosted mode returns null to continue anonymously.

Strict Token Payload Requirements

Hosted deployments use hostedWorkersOAuthMcpPropsSchema, defined in src/server/mcp/context.ts. This schema requires both clientId and scopes (lines 47‑52):

// Hosted schema enforces client identity
const hostedWorkersOAuthMcpPropsSchema = workersOAuthMcpPropsSchema.extend({
  clientId: z.string().min(1),  // Required
  scopes: z.array(z.string()).min(1),  // Non-empty array required
});

The createWorkersOAuthMcpProps function pins the hosted base URL to every token, ensuring MCP calls remain scoped to https://app.openseo.com.

How Self‑Hosted Mode Supports External Providers

Self‑hosted mode removes the built‑in provider entirely. Your deployment must integrate with an external OAuth authority—Google, GitHub, Auth0, or any custom implementation.

Runtime Mode Detection

The switch between modes happens at runtime via isHostedServerAuthMode() in src/server/lib/runtime-env.ts (lines 40‑42):

// Returns true only when AUTH_MODE=hosted
export function isHostedServerAuthMode(): boolean {
  return getRuntimeEnv().AUTH_MODE === "hosted";
}

When this returns false, all code paths bypass hosted‑specific logic.

Permissive Token Validation

Without a built‑in provider, getOAuthHelpers throws if invoked (lines 72‑78), and resolveHostedContext fails silently. The MCP transport accepts tokens minted externally using the generic workersOAuthMcpPropsSchema, where clientId and scopes are optional:

Field Hosted Requirement Self‑Hosted Handling
clientId Required, validated Often omitted by external providers
scopes Must include "mcp" Assumed granted externally
baseUrl Pinned to SaaS domain Uses request's Host header

Flexible Origin Handling

The transport layer in src/server/mcp/transport.ts deliberately avoids origin pinning for self‑hosted deployments. This allows a single OpenSEO instance to serve multiple custom domains without token mismatches.

Code Examples: Authentication Flows

Hosted: Requesting a Token from OpenSEO

// Build the authorization URL for the SaaS provider
const authorizeUrl = new URL("/api/auth/oauth2/authorize", "https://app.openseo.com");

authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("client_id", "openseo-mcp");
authorizeUrl.searchParams.set("redirect_uri", "https://app.openseo.com/callback");
authorizeUrl.searchParams.set("scope", "mcp");

// User is redirected to /sign-in if not authenticated
// After consent, the token payload contains:
// {
//   userId: "u_...",
//   userEmail: "user@example.com",
//   organizationId: "org_...",
//   clientId: "openseo-mcp",
//   scopes: ["mcp"],
//   baseUrl: "https://app.openseo.com"
// }

Self‑Hosted: Integrating Google OAuth

// Self-hosted deployments must obtain tokens externally
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID!,
    client_secret: process.env.GOOGLE_CLIENT_SECRET!,
    code: authorizationCode,
    grant_type: "authorization_code",
    redirect_uri: "https://seo.internal.company.com/callback",
    scope: "openid email mcp",
  }),
});

const { access_token } = await tokenResponse.json();

// Token may lack clientId/scopes—accepted by self-hosted validation

Making an Authenticated MCP Request

// Both modes use identical request formatting
const response = await fetch("https://<your-domain>/mcp/analyze-seo", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${access_token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    depth: "comprehensive"
  }),
});

Key Implementation Files

File Purpose
src/server/mcp/oauth-provider.ts createOpenSeoOAuthProvider, getAuthorizeSessionBlocker, getOAuthHelpers
src/server/mcp/context.ts Schema definitions: workersOAuthMcpPropsSchema, hostedWorkersOAuthMcpPropsSchema, createWorkersOAuthMcpProps
src/server/lib/runtime-env.ts isHostedServerAuthMode() for runtime detection
src/middleware/ensure-user/hosted.ts resolveHostedContext for session resolution
src/server/mcp/transport.ts Request handling and base URL logic

Summary

  • Hosted mode uses AUTH_MODE=hosted to activate a built‑in Cloudflare Workers‑OAuth‑Provider with mandatory clientId/scopes validation and automatic sign‑in redirection.
  • Self‑hosted mode disables the provider, accepts externally minted tokens, and relaxes schema requirements to accommodate diverse OAuth authorities.
  • The isHostedServerAuthMode() function in src/server/lib/runtime-env.ts controls all branching behavior at runtime.
  • Token payloads in hosted mode are locked to the SaaS baseUrl, while self‑hosted deployments respect the incoming request's host header.

Frequently Asked Questions

What happens if I call getOAuthHelpers in self‑hosted mode?

The function throws an error because no OAuthProvider is initialized. Self‑hosted deployments must implement their own token validation logic or rely on middleware that validates external provider signatures before reaching MCP handlers.

Can I use the hosted OAuth provider with a custom domain?

No. The hosted provider pins baseUrl to getHostedBaseUrl(), which returns the canonical SaaS domain. Self‑hosted mode is designed specifically for custom domain deployments where you control the OAuth authority.

Why does the self‑hosted schema allow missing clientId and scopes?

External providers like Google or Azure AD may not include these fields in their access token payloads. Rather than reject valid tokens from major identity platforms, OpenSEO's workersOAuthMcpPropsSchema treats them as optional and assumes the external provider has already enforced appropriate consent.

How do I migrate from hosted to self‑hosted without breaking MCP integrations?

You must reconfigure your MCP clients to authenticate against your new external OAuth provider instead of https://app.openseo.com/api/auth/oauth2/*. Update client_id, redirect_uri, and token endpoint URLs. The MCP tool_call formatting remains identical, but the token acquisition flow changes completely.

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 →