How OpenSEO Handles MCP Server Authentication: OAuth 2.0 and Self-Hosted Methods
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, 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.
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 (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 (lines 44-58). This function extracts authentication data using workersOAuthMcpPropsSchema and performs critical validation:
- Verifies the auth context exists
- Confirms the
mcpscope is present - 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) provides alternative authentication paths:
- Cloudflare Access JWT: Resolves tokens via
src/middleware/ensure-user/cloudflareAccess.ts, converting the JWT into a first-party user context - Local admin context: Uses
src/middleware/ensure-user/delegated.tsto 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 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)
// 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
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
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
// 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)
// 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
createOpenSeoOAuthProviderinsrc/server/mcp/oauth-provider.ts - The mandatory
mcpscope enforcement occurs throughgetGrantedMcpScopes, returning 400 errors for invalid requests - Self-hosted deployments bypass OAuth using
handleSelfHostedOpenSeoMcpRequest, supporting Cloudflare Access JWTs or local admin contexts viabuildFirstPartyMcpAuthContext - Authentication contexts persist in
AsyncLocalStorage(mcpToolAuthContextStorage), withrequireMcpToolAuthContextensuring 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 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, 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.
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. Every MCP tool calls this function to retrieve the verified user identity, ensuring tools execute only within authenticated request scopes.
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 →