How the MCP OAuth Provider Handles Dynamic Client Registration in OpenSEO

The OpenSEO MCP OAuth provider handles dynamic client registration by intercepting requests to /api/auth/oauth2/register, normalizing the client metadata to enforce confidential client authentication, and delegating actual client creation to Cloudflare's workers-oauth-provider.

The every-app/open-seo repository implements a Machine-Client-Powered (MCP) OAuth flow that supports dynamic client registration (DCR) for third-party applications. This implementation bridges the gap between public client registration requests and Cloudflare's confidential client requirements through a lightweight normalization shim.

Dynamic Client Registration Endpoint

The provider exposes a dedicated registration path that acts as the entry point for all DCR requests. In src/server/mcp/oauth-provider.ts, the constant OAUTH_REGISTER_PATH defines this endpoint:

const OAUTH_REGISTER_PATH = "/api/auth/oauth2/register";

When createOpenSeoOAuthProvider receives a request matching this path, it intercepts the payload before forwarding it to the underlying Cloudflare OAuth provider. The handler checks the URL pathname and triggers the normalization process:

if (url.pathname === OAUTH_REGISTER_PATH) {
  // Cloudflare's provider can reject public DCR clients, but Perplexity
  // does not appear to retry as confidential and instead expects a
  // client_secret. Normalize before handing the request to Cloudflare so
  // it still owns client creation, secret hashing, and token storage.
  request = await normalizeClientRegistrationRequest(request);
}
return provider.fetch(request, env, ctx);

This interception point ensures that OpenSEO can modify incoming registration metadata while allowing Cloudflare to retain responsibility for secure client creation and token management.

Normalizing Client Registration Requests

The core transformation logic resides in src/server/mcp/oauth-registration.ts within the normalizeClientRegistrationRequest function. This shim preprocesses the request body to ensure compatibility with MCP authentication requirements.

Payload Size Limits and Parsing

The shim first protects against oversized payloads by rejecting bodies larger than 1 MiB:

if (request.body && contentLength > 1024 * 1024) {
  // Let Cloudflare handle the error for oversized bodies
  return request;
}

If the body parses successfully as JSON, the function proceeds with metadata transformation. If parsing fails, the original request returns unchanged, allowing Cloudflare to handle malformed JSON errors natively.

Enforcing Confidential Authentication

The critical transformation occurs when the shim inspects the token_endpoint_auth_method field. To ensure the MCP can use client secrets during subsequent token exchanges, the function rewrites public client registrations to use client_secret_post:

if (
  metadata.token_endpoint_auth_method === undefined ||
  metadata.token_endpoint_auth_method === "none"
) {
  // Perplexity registers as a public client but then rejects DCR responses
  // without a client_secret. Use client_secret_post because its validator
  // accepts that method but rejects client_secret_basic.
  metadata.token_endpoint_auth_method = CONFIDENTIAL_CLIENT_AUTH_METHOD;
}

After modifying the metadata, the shim constructs a fresh Request object with updated headers to ensure Cloudflare processes the normalized payload correctly:

const headers = new Headers(request.headers);
headers.set("Content-Type", "application/json");
headers.delete("Content-Length");

return new Request(request.url, {
  method: request.method,
  headers,
  body: JSON.stringify(metadata),
});

Stripping the Content-Length header prevents request mismatch errors since the JSON stringification may alter the body size from the original request.

Delegation to Cloudflare OAuth Provider

After normalization, the sanitized request flows to new OAuthProvider(options) from Cloudflare's workers-oauth-provider library. Cloudflare handles the security-critical operations:

  • Client creation – Generates and stores unique client identifiers
  • Secret hashing – Cryptographically hashes the client secret before persistence
  • Token storage – Manages refresh and access tokens in Cloudflare's built-in storage layer

This architecture allows OpenSEO to maintain minimal custom code for DCR while leveraging Cloudflare's hardened OAuth implementation for sensitive cryptographic operations.

MCP Scope Validation

Beyond registration normalization, the MCP OAuth provider enforces scope requirements during the authorization flow. When clients omit requested scopes, the provider automatically grants the full MCP scope list defined in MCP_OAUTH_SCOPES:

if (requestedScopes.length === 0) {
  return [...MCP_OAUTH_SCOPES];
}

The provider strictly validates that all authorized clients possess the essential MCP_SCOPE. If this scope is missing, the authorization aborts immediately, ensuring that registered clients can access required MCP resources.

Practical Implementation Examples

Registering a New MCP Client

Submit a dynamic client registration request without specifying an authentication method:

curl -X POST https://your-openseo.example.com/api/auth/oauth2/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My MCP App",
    "redirect_uris": ["https://myapp.example.com/callback"],
    "grant_types": ["authorization_code"],
    "response_types": ["code"]
  }'

The OpenSEO shim automatically injects "client_secret_post" as the token_endpoint_auth_method before Cloudflare processes the request, ensuring the response includes a client_secret even for clients that initially register as public.

Obtaining an Access Token

Use the registered client credentials to complete the authorization flow. First, initiate user authorization:

curl -G https://your-openseo.example.com/api/auth/oauth2/authorize \
  --data-urlencode "response_type=code" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "redirect_uri=https://myapp.example.com/callback" \
  --data-urlencode "scope=mcp" \
  --data-urlencode "state=xyz"

Then exchange the authorization code for tokens:

curl -X POST https://your-openseo.example.com/api/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://myapp.example.com/callback&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"

Because the registration process enforced client_secret_post, the client secret must be included in the request body rather than the Authorization header.

Accessing MCP Resources

Use the obtained token to call MCP-protected endpoints:

curl -H "Authorization: Bearer ACCESS_TOKEN" \
     https://your-openseo.example.com/mcp/keyword-research?keyword=example

The MCP transport layer validates the token against Cloudflare's storage and extracts the granted scopes before processing the request.

Summary

  • Endpoint interception: The MCP OAuth provider captures DCR requests at /api/auth/oauth2/register in src/server/mcp/oauth-provider.ts before Cloudflare processes them.
  • Metadata normalization: The normalizeClientRegistrationRequest function in src/server/mcp/oauth-registration.ts enforces client_secret_post authentication for all clients to ensure MCP compatibility.
  • Security delegation: Cloudflare's workers-oauth-provider handles actual client creation, secret hashing, and token storage after OpenSEO normalizes the request.
  • Scope enforcement: The provider automatically grants MCP_OAUTH_SCOPES when omitted and validates the presence of the essential MCP_SCOPE during authorization.
  • Request reconstruction: The normalization shim rebuilds the request with updated JSON bodies and stripped Content-Length headers to prevent downstream parsing errors.

Frequently Asked Questions

What happens if a client tries to register with token_endpoint_auth_method set to "none"?

The OpenSEO normalization shim automatically rewrites the token_endpoint_auth_method to "client_secret_post" before forwarding the request to Cloudflare. This ensures the client receives a client_secret in the registration response, which is required for subsequent MCP token exchanges despite the client initially requesting public client status.

Why does the OpenSEO provider strip the Content-Length header during normalization?

The shim reconstructs the request body using JSON.stringify(metadata), which may produce a different byte length than the original payload due to formatting changes or added fields. Removing the Content-Length header prevents HTTP request mismatch errors and allows Cloudflare to calculate the correct content length for the modified body.

Does OpenSEO store client secrets or tokens locally?

No. According to the source code in src/server/mcp/oauth-provider.ts, OpenSEO delegates all client creation, secret hashing, and token storage to Cloudflare's workers-oauth-provider library. The OpenSEO code only modifies the incoming registration metadata; all cryptographic operations and persistence remain within Cloudflare's secure infrastructure.

What is the maximum payload size for dynamic client registration requests?

The normalization shim in src/server/mcp/oauth-registration.ts allows request bodies up to 1 MiB (1024 × 1024 bytes). Requests exceeding this size are passed through unchanged to Cloudflare, which handles the oversized body error response according to its own validation logic.

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 →