MCP Remote Transport

August 17, 2026 ยท View on GitHub

Module: bernstein.mcp.remote_transport Class: StreamableHTTPTransport

The MCP remote transport exposes Bernstein's MCP server over HTTP using the streamable HTTP transport spec. This lets remote MCP clients (Claude Desktop, other agents, CI systems) interact with a Bernstein instance over the network -- including deployment on Cloudflare Workers via a Python worker.

This page covers configuring and deploying the streamable HTTP transport as a standalone ASGI app. For the MCP protocol surface itself -- transports, the stateless serving model, auth (bearer and OAuth-2 PKCE), JSON-RPC methods, the cost-meter envelope, cancellation, and worked curl examples -- see Bernstein MCP server. That surface is identical across every deployment and is documented once there.


Configuration

RemoteMCPConfig dataclass fields:

FieldTypeDefaultDescription
hoststr"127.0.0.1"Bind host (loopback by default)
portint8053Bind port
pathstr"/mcp"URL path for MCP endpoint
auth_typestr"bearer"Authentication: "none" or "bearer"
auth_tokenstr""Bearer token; when empty it is read from BERNSTEIN_MCP_TOKEN (or BERNSTEIN_MCP_AUTH_TOKEN)
cors_originslist[str]["http://localhost:*"]CORS allowed origins; clear-text http:// origins must be loopback-pinned

The config is safe by default: it binds to loopback and expects a bearer token. Construction raises RemoteMCPConfigError for any combination that would expose the JSON-RPC surface without authentication -- an auth_type other than "none"/"bearer", auth_type="none" on a non-loopback host, or auth_type="bearer" with no token on a non-loopback host. There is no session store, so there are no session capacity or timeout fields; see Stateless serving.

auth_type does not have an "oauth" value: the streamable HTTP transport authenticates only anonymous or static-bearer callers. The OAuth-2 discovery surface described below (protected-resource metadata and the WWW-Authenticate challenge) exists so a client can locate an external IdP; it is independent of how this transport checks the token that comes back, and does not add a third auth_type. See Auth in the MCP server doc.

The same rule covers browser origins. Bearer tokens ride on whatever origin CORS admits, so a clear-text origin is only accepted when it is pinned to a loopback host (127.0.0.1, localhost or [::1]); that is why the default is safe despite being clear-text. Any other clear-text origin is refused with a RemoteMCPConfigError naming the offending entries, so a non-loopback origin has to use TLS. The clear-text schemes held to this rule are http, ws and ftp.

Origins are parsed with urllib.parse.urlsplit, so a malformed authority (http://[::1]evil.test, http://[::1]@evil.test) is refused rather than being read as loopback. Origins carrying no scheme at all, such as the * and null CORS tokens, are left untouched.


Available tools

The remote transport exposes exactly these MCP tools:

ToolDescriptionRequired args
bernstein_runStart an orchestration run (optionally a subtask via parent_task_id)goal
bernstein_statusLiveness, task counts, cost; optional status filter and detail flagNone
bernstein_cancelCancel one task and its subtask treetask_id
bernstein_shutdown_orchestratorWhole-orchestrator shutdown signalNone
bernstein_approveApprove a pending tasktask_id
bernstein_completeComplete a task the caller is executingtask_id, result_summary

That is 6 advertised tools, a subset of what the stdio server registers. The removed names (bernstein_health, bernstein_tasks, bernstein_cost, bernstein_stop, bernstein_create_subtask) stay callable for one minor release as deprecated aliases that answer with their historical payload plus a notice naming the replacement; they are never advertised. Tools such as bernstein_run_status, bernstein_claim, bernstein_post_message, bernstein_post_artifact, bernstein_task_capsule, load_skill, the scenario tool and bernstein_verify_lineage are not reachable over this transport. The transport does serve resources/list and resources/read for the capability card and the skill index (and, when BERNSTEIN_LINEAGE_MCP_ENABLED=1 opts in, the lineage records).

Argument validation on this transport is weaker than on stdio

The deny-by-default input firewall described in MCP tool-call input validation is applied by the stdio and SSE servers in bernstein.mcp.server. The streamable HTTP transport in bernstein.mcp.remote_transport does not call validate_tool_call. On this path:

  • the size cap, recursion cap and control-character filter are not applied;
  • arguments are not checked against the JSON Schemas in src/bernstein/mcp/tool_schemas/; the schemas in the table above are restated inside the transport module and are looser than the real ones;
  • a malformed argument object is rejected by the handler it reaches, if at all, rather than by a uniform structured validation error.

Treat every client of this transport as trusted, or terminate it behind a gateway that validates request bodies. Starting the transport logs a one-line warning saying the same thing.

Sharing one registry and one validation path between the transports is tracked in issue #3083. This section is an interim notice and goes away with that change.


Starting the server

Python API

from bernstein.mcp.remote_transport import RemoteMCPConfig, run_remote

# Start with defaults (binds to 127.0.0.1:8053; token from BERNSTEIN_MCP_TOKEN)
run_remote()

# Custom configuration. A non-loopback bind requires a bearer token,
# either passed explicitly or via BERNSTEIN_MCP_TOKEN.
run_remote(
    server_url="http://127.0.0.1:8052",  # Bernstein task server
    host="0.0.0.0",
    port=8053,
)

ASGI application

For deployment with any ASGI server (uvicorn, hypercorn, Cloudflare Python workers):

from bernstein.mcp.remote_transport import RemoteMCPConfig, create_asgi_app

config = RemoteMCPConfig(
    host="0.0.0.0",
    port=8053,
    auth_type="bearer",
    auth_token="my-secret-token",
    cors_origins=["https://myapp.example.com"],
)

app = create_asgi_app(
    server_url="http://127.0.0.1:8052",
    config=config,
)

# Run with uvicorn
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8053)

The app serves the single /mcp endpoint (JSON-RPC 2.0 over POST, plus CORS preflight). Authenticate with Authorization: Bearer <token>; the full request/response protocol is in Bernstein MCP server.


CORS configuration

By default only localhost origins are allowed (["http://localhost:*"]). For production, set your application domains:

config = RemoteMCPConfig(
    auth_type="bearer",
    auth_token="secret",
    cors_origins=["https://myapp.example.com", "https://admin.example.com"],
)

The legacy mcp-session-id header stays preflight-allowed during the compat window so older browser clients can still send it (the transport ignores it); no response header exposes it.


Deployment on Cloudflare Workers

The ASGI app can be deployed as a Cloudflare Python worker:

# worker.py
from bernstein.mcp.remote_transport import RemoteMCPConfig, create_asgi_app

config = RemoteMCPConfig(
    auth_type="bearer",
    auth_token="YOUR_SECRET",
)

app = create_asgi_app(
    server_url="https://your-bernstein-server.example.com:8052",
    config=config,
)

This lets MCP clients connect to your Bernstein instance from anywhere, with Cloudflare's global edge network handling TLS and routing.