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

> Learn to use the self-hosted MCP transport in OpenSEO by configuring environment variables and sending POST requests to the /mcp endpoint. Implement secure and efficient communication.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-09-05

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) and integrated into the main fetch router at [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) and [`src/middleware/ensure-user/delegated.ts`](https://github.com/every-app/open-seo/blob/main/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:

```bash

# 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`](https://github.com/every-app/open-seo/blob/main/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:

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

```

### Code Examples

**Example 1: Cloudflare Access Authentication**

```typescript
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**

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)** – Contains `handleSelfHostedOpenSeoMcpRequest`, CORS header definitions, and the request handler factory.
- **[`src/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)** – Defines `MCP_ROUTE` and `createWorkersOAuthMcpProps` for building the property context.
- **[`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) and integrated via the main fetch router in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.