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:
- Route Detection – The main server detects the
/mcppath only whenAUTH_MODEis set to"cloudflare_access"or"local_noauth", then forwards the request tohandleSelfHostedOpenSeoMcpRequest. - Authentication Resolution – The transport resolves the user context via either
resolveCloudflareAccessContext(for Cloudflare Access tokens) orresolveLocalNoAuthContext(for development). - Property Construction – The resolved identity is packed into
McpPropsusingcreateWorkersOAuthMcpProps, containinguserId,userEmail,organizationId, andbaseUrl. - Handler Execution – The
createRequestHandlerfunction builds a modern MCP handler with CORS headers and route configuration. - 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 theCF-Access-Tokenheader 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/jsonAccept: application/json, text/event-streamCF-Access-Token: <token>(required only forcloudflare_accessmode)
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– ContainshandleSelfHostedOpenSeoMcpRequest, CORS header definitions, and the request handler factory.src/server.ts– Routes incoming requests to the MCP transport when the path matches/mcpand the auth mode is compatible.src/server/mcp/context.ts– DefinesMCP_ROUTEandcreateWorkersOAuthMcpPropsfor 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_MODEtocloudflare_accessorlocal_noauthand accessing the/mcpendpoint. - Authentication flows through
resolveCloudflareAccessContextfor production tokens orresolveLocalNoAuthContextfor development admin access. - Requests must include a JSON-RPC 2.0 body with a
_metaenvelope specifying protocol version2026-07-28to 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.tsand integrated via the main fetch router insrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →