How to Use the Self-Hosted MCP Transport in OpenSEO: A Complete Implementation Guide

To use the self-hosted MCP transport in OpenSEO, configure the AUTH_MODE environment variable to either cloudflare_access or local_noauth, then send POST requests to the /mcp endpoint with the proper JSON-RPC envelope and authentication headers.

OpenSEO exposes its Model-Context-Protocol (MCP) API through a dedicated HTTP transport designed for self-hosted deployments. Whether you are running Docker locally or deploying to Cloudflare, the self-hosted MCP transport allows AI agents and custom scripts to interact with your SEO data programmatically. This guide breaks down the architecture, configuration, and request patterns using the actual source code implementation from the every-app/open-seo repository.

Understanding the Self-Hosted MCP Architecture

The self-hosted MCP transport is implemented in src/server/mcp/transport.ts and integrated into the main fetch router at src/server.ts. The system follows a strict pipeline to validate, authenticate, and process incoming requests.

Request Flow Overview

When a client hits the /mcp endpoint, the transport executes the following steps:

  1. Route Detection – The main server detects the /mcp path only when AUTH_MODE is set to "cloudflare_access" or "local_noauth", then forwards the request to handleSelfHostedOpenSeoMcpRequest.
  2. Authentication Resolution – The transport resolves the user context via either resolveCloudflareAccessContext (for Cloudflare Access tokens) or resolveLocalNoAuthContext (for development).
  3. Property Construction – The resolved identity is packed into McpProps using createWorkersOAuthMcpProps, containing userId, userEmail, organizationId, and baseUrl.
  4. Handler Execution – The createRequestHandler function builds a modern MCP handler with CORS headers and route configuration.
  5. Response Generation – The handler returns either a JSON payload or an SSE stream depending on the request type.

Authentication Modes

OpenSEO supports two distinct authentication strategies for the self-hosted MCP transport, controlled by the AUTH_MODE environment variable:

  • cloudflare_access – Validates the CF-Access-Token header against Cloudflare Access. This mode extracts the user's identity from the JWT token and is recommended for production deployments requiring per-user access control.
  • local_noauth – Returns a built-in admin identity (local-admin) without requiring external tokens. This mode is intended for local development or trusted internal networks where authentication is handled at the network layer.

Both modes are resolved through the middleware layer in src/middleware/ensure-user/cloudflareAccess.ts and src/middleware/ensure-user/delegated.ts respectively.

Configuring the MCP Transport

Before sending requests, ensure your deployment is properly configured to expose the MCP endpoint.

Environment Variables

Set the following in your Docker or Cloudflare deployment:


# Required: Select authentication mode

AUTH_MODE=cloudflare_access  # or local_noauth

# Optional: Customize the base URL used in MCP properties

PUBLIC_URL=https://my-open-seo.example.com

The transport automatically applies CORS headers defined in MCP_CORS_HEADERS (found in src/server/mcp/transport.ts), allowing cross-origin requests from localhost-class origins in self-hosted mode. OPTIONS pre-flight requests are answered early without requiring authentication.

Making Requests to the MCP Endpoint

The self-hosted MCP transport expects JSON-RPC 2.0 requests with a modern _meta envelope. Requests without this envelope fall back to the legacy JSON handler.

Request Structure

Send POST requests to https://<your-host>/mcp with the following headers:

  • Content-Type: application/json
  • Accept: application/json, text/event-stream
  • CF-Access-Token: <token> (required only for cloudflare_access mode)

The request body must include the protocol version in the _meta field to trigger the modern handler:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    }
  }
}

Code Examples

Example 1: Cloudflare Access Authentication

const BASE_URL = "https://my-open-seo.example.com";
const MCP_ENDPOINT = `${BASE_URL}/mcp`;

async function callMcpWithAccess(token: string) {
  const resp = await fetch(MCP_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json, text/event-stream",
      "CF-Access-Token": token,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "tools/list",
      params: {
        _meta: {
          "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        },
      },
    }),
  });

  const data = await resp.json();
  console.log("Available tools:", data);
}

Example 2: Local No-Auth Development

async function callMcpLocalAdmin() {
  const resp = await fetch("http://localhost:8787/mcp", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json, text/event-stream",
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method": "tools/list",
      params: {
        _meta": {
          "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        },
      },
    }),
  });

  const data = await resp.json();
  return data;
}

The allowedOriginHostnames parameter is set to undefined for self-hosted instances in createRequestHandler, which defaults to allowing localhost-class origins. This facilitates development without compromising the strict origin validation used in the hosted version.

Key Implementation Details

Legacy JSON Fallback

If a request omits the _meta envelope, the transport invokes handleLegacyJsonRequest (lines 78-106 in src/server/mcp/transport.ts). This path uses the WebStandardStreamableHTTPServerTransport for backward compatibility with older MCP agents. However, new implementations should always include the modern envelope to avoid the legacy path.

Stateless Configuration

The self-hosted transport operates statelessly with maxSubscriptions: 0 set in the handler configuration. This means the server does not maintain persistent SSE subscriptions, making it suitable for serverless deployments like Cloudflare Workers.

Source File Reference

The core logic resides in several key files:

  • src/server/mcp/transport.ts – Contains handleSelfHostedOpenSeoMcpRequest, CORS header definitions, and the request handler factory.
  • src/server.ts – Routes incoming requests to the MCP transport when the path matches /mcp and the auth mode is compatible.
  • src/server/mcp/context.ts – Defines MCP_ROUTE and createWorkersOAuthMcpProps for building the property context.
  • src/server/mcp/server.ts – Houses the actual MCP server instance and tool definitions.

Summary

  • The self-hosted MCP transport in OpenSEO is activated by setting AUTH_MODE to cloudflare_access or local_noauth and accessing the /mcp endpoint.
  • Authentication flows through resolveCloudflareAccessContext for production tokens or resolveLocalNoAuthContext for development admin access.
  • Requests must include a JSON-RPC 2.0 body with a _meta envelope specifying protocol version 2026-07-28 to use the modern handler.
  • The transport automatically handles CORS for localhost origins and supports both JSON and SSE response formats.
  • All entry points are defined in src/server/mcp/transport.ts and integrated via the main fetch router in src/server.ts.

Frequently Asked Questions

What is the difference between Cloudflare Access and local no-auth mode?

Cloudflare Access mode validates JWT tokens from Cloudflare Access, extracting real user identities for multi-tenant scenarios, while local no-auth mode returns a static admin identity without requiring tokens. Use Cloudflare Access for production deployments and local no-auth for development or single-tenant internal networks.

Why does my MCP request return a legacy JSON error?

This occurs when the request body lacks the _meta field with the protocol version 2026-07-28. The transport in src/server/mcp/transport.ts detects missing metadata and routes to the legacy handler. Always include the _meta envelope in the params object to ensure the modern handler processes your request.

Can I use SSE streaming with the self-hosted MCP transport?

Yes, but the self-hosted configuration sets maxSubscriptions: 0 in createRequestHandler, making the transport stateless. While the server accepts text/event-stream in the Accept header, it typically returns JSON responses unless specifically configured otherwise. For full SSE support, you would need to modify the handler configuration in src/server/mcp/transport.ts.

How do I enable CORS for a custom domain in self-hosted mode?

The self-hosted transport automatically applies MCP_CORS_HEADERS to all responses and does not restrict origin hostnames when allowedOriginHostnames is undefined. This allows localhost and development origins by default. For production custom domains, ensure your reverse proxy or Cloudflare configuration handles CORS headers, as the transport code deliberately allows flexible origins for self-hosted flexibility.

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 →