# How to Authenticate with the OpenSEO MCP Server for Self-Hosted Instances

> Learn how to authenticate with the OpenSEO MCP server for self-hosted instances using Cloudflare Access or local no-auth modes. Secure your production or development environments easily.

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

---

**OpenSEO's self-hosted MCP server supports two authentication modes: Cloudflare Access (JWT-based) for production deployments and `local_noauth` for private or development environments.**

When you self-host the [every-app/open-seo](https://github.com/every-app/open-seo) repository, every request to the Model Context Protocol (MCP) endpoint must carry a valid authentication context. The server resolves this context differently based on the `AUTH_MODE` environment variable, either verifying a Cloudflare Access JWT or generating a synthetic identity for local use.

## Supported Authentication Modes

The `AUTH_MODE` variable in [`src/lib/auth-mode.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-mode.ts) accepts three values, though only two apply to self-hosted deployments:

- **`cloudflare_access`** — Production-grade JWT verification via Cloudflare Access
- **`local_noauth`** — Bypass authentication for internal tooling or private networks
- **`hosted`** — Reserved for the managed OpenSEO cloud service

```typescript
// src/lib/auth-mode.ts
export const authModeSchema = z.enum(["cloudflare_access", "local_noauth", "hosted"]);

```

(source: [`auth-mode.ts:1-7`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-mode.ts#L1-L7))

### Mode Comparison

| Mode | Security Level | Use Case | Required Configuration |
|------|---------------|----------|------------------------|
| **Cloudflare Access** | High | Production deployments with team-based access control | `AUTH_MODE=cloudflare_access`, `TEAM_DOMAIN`, `POLICY_AUD` |
| **Local No-Auth** | None | Development, testing, air-gapped private networks | `AUTH_MODE=local_noauth` |

## Cloudflare Access Authentication (Production)

For production self-hosted instances, OpenSEO integrates with **Cloudflare Access** to enforce identity-aware policies. Clients must obtain a valid Cloudflare Access JWT and include it in the `cf-access-jwt-assertion` header.

### Required Environment Variables

```bash
AUTH_MODE=cloudflare_access
TEAM_DOMAIN=https://your-team.cloudflareaccess.com
POLICY_AUD=your-cloudflare-access-audience-tag

```

### JWT Verification Flow

The verification pipeline resides in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts):

1. **Extract the JWT** from the `cf-access-jwt-assertion` header
2. **Fetch the JWK set** from `https://${TEAM_DOMAIN}/cdn-cgi/access/certs`
3. **Verify the token** using the `jose` library's `jwtVerify` function
4. **Validate the audience** matches `POLICY_AUD`
5. **Extract identity claims**: `sub` (user ID), `email`, and organization context

```typescript
// src/middleware/ensure-user/cloudflareAccess.ts
export async function resolveCloudflareAccessContext(
  headers: Headers,
): Promise<CloudflareAccessContext> {
  const jwt = headers.get("cf-access-jwt-assertion");
  if (!jwt) throw new Error("Missing Cloudflare Access JWT");

  const { payload } = await jwtVerify(jwt, jwks, {
    issuer: TEAM_DOMAIN,
    audience: POLICY_AUD,
  });

  return {
    userId: payload.sub as string,
    email: payload.email as string,
    organizationId: await resolveOrganization(payload),
  };
}

```

(source: [`cloudflareAccess.ts:39-90`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts#L39-L90))

### Making Authenticated MCP Requests

Once your Cloudflare Access policy is configured, obtain a JWT through your identity provider and include it in requests:

```bash

# Obtain JWT via Cloudflare Access login flow, then:

curl -X POST "https://your-worker.example.com/mcp" \
     -H "Content-Type: application/json" \
     -H "cf-access-jwt-assertion: $CF_ACCESS_TOKEN" \
     -d '{
       "jsonrpc":"2.0",
       "method":"tools/list",
       "id":1
     }'

```

### Using the MCP Client Library

For TypeScript applications, pass the JWT through custom headers:

```typescript
import { createMcpClient } from "agents/mcp/client";

const client = createMcpClient({
  baseUrl: "https://your-worker.example.com/mcp",
  headers: {
    "cf-access-jwt-assertion": process.env.CF_ACCESS_JWT!,
  },
});

// Now call any OpenSEO MCP tool
const backlinks = await client.call("tools/call", {
  name: "get_backlinks_profile",
  arguments: { projectId: "proj_123" },
});

```

## Local No-Auth Mode (Development)

For private networks or local development, set `AUTH_MODE=local_noauth` to disable JWT verification entirely. The server generates a **synthetic user context** via `resolveLocalNoAuthContext()` in the delegated auth middleware.

```bash

# Minimal configuration for development

AUTH_MODE=local_noauth

```

In this mode, the MCP client requires no authentication headers:

```typescript
const client = createMcpClient({
  baseUrl: "http://localhost:8787/mcp",
  // No headers needed
});

```

(source: delegated auth implementation in [`src/middleware/ensure-user/delegated.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/delegated.ts))

## MCP Transport Implementation

The `handleSelfHostedOpenSeoMcpRequest` function in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) orchestrates the authentication flow for both modes:

```typescript
// src/server/mcp/transport.ts
export async function handleSelfHostedOpenSeoMcpRequest(
  request: Request,
  authMode: "cloudflare_access" | "local_noauth",
  env: unknown,
  ctx: ExecutionContext,
) {
  // 1. Resolve identity based on authMode
  const userContext = authMode === "cloudflare_access"
    ? await resolveCloudflareAccessContext(request.headers)
    : await resolveLocalNoAuthContext();

  // 2. Build MCP props with resolved identity
  const mcpProps = createWorkersOAuthMcpProps(userContext, getPublicOrigin(request));

  // 3. Create and return request handler
  return createRequestHandler(mcpProps, { corsHeaders: getCorsHeaders() });
}

```

(source: [`transport.ts:64-88`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts#L64-L88))

The resolved `ToolAuthContext` is validated against Zod schemas in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) before being passed to the underlying Agents SDK handler.

## Summary

- **Cloudflare Access mode** (`AUTH_MODE=cloudflare_access`) provides production-ready authentication using JWT tokens from Cloudflare's identity platform
- **Local no-auth mode** (`AUTH_MODE=local_noauth`) eliminates authentication for trusted private deployments
- The **entry point** for all MCP requests is `handleSelfHostedOpenSeoMcpRequest` in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)
- **JWT verification** occurs in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) using the `jose` library
- **Identity resolution** branches based on `authMode`, then builds a `ToolAuthContext` for MCP tool execution

## Frequently Asked Questions

### How do I obtain a Cloudflare Access JWT for testing?

Log in through your Cloudflare Access-protected application in a browser, then extract the `CF_Authorization` cookie or use the Cloudflare Access CLI (`cloudflared access token`). The JWT can also be programmatically obtained via OAuth flows configured in your Cloudflare Access policy.

### Can I switch between authentication modes without redeploying?

No. The `AUTH_MODE` environment variable is read at worker initialization. Changing modes requires restarting or redeploying the Cloudflare Worker after updating your environment configuration.

### What happens if the Cloudflare Access JWT expires?

The `jwtVerify` call in [`cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/cloudflareAccess.ts) will throw a verification error, and the MCP endpoint returns a 401 response with a clear error message indicating token expiration. Clients must refresh their JWT through the Cloudflare Access login flow.

### Does local_noauth mode support multiple synthetic users?

The current implementation in `resolveLocalNoAuthContext()` returns a fixed synthetic identity. For multi-user testing scenarios, you would need to modify the delegated auth middleware or use Cloudflare Access with test policies.