MCP-Bastion 4.0.0

MCP-Bastion Documentation

Hybrid MCP transport

Step-by-step guide for running MCP-Bastion with legacy session-based clients and stateless clients (explicit state handles) on the same proxy - without breaking either path.

Canonical reference: HYBRID_MCP_TRANSPORT.md

Hybrid MCP transport architecture


Prerequisites

  • Python 3.10+
  • pip install mcp-bastion-python[policy,redis] (Redis optional for single-process dev)
  • An upstream MCP server on HTTP (streamable HTTP at /mcp), or use examples/llm_server.py

Step 1 - Copy sample policy

cp examples/bastion-hybrid-transport.yaml bastion.yaml
mcp-bastion validate --config bastion.yaml

For local dev, state_backend.type: memory is fine. For multiple proxy replicas behind a load balancer, switch to Redis:

state_backend:
  type: redis
  redis_url: redis://127.0.0.1:6379/0

Step 2 - Start upstream MCP (loopback)

Bind upstream to 127.0.0.1 only so clients cannot bypass Bastion:

## Terminal A - example upstream
mcp-bastion serve --http 9000 --host 127.0.0.1

Step 3 - Start Bastion HTTP proxy

## Terminal B - boundary proxy with hybrid transport
mcp-bastion serve --proxy http://127.0.0.1:9000/mcp --http 8080 --host 0.0.0.0 --config bastion.yaml

Clients connect to port 8080, not 9000.


Step 4 - Discovery (no initialize handshake)

Orchestrators can discover capabilities before connecting:

curl -s http://127.0.0.1:8080/.well-known/mcp.json | jq .

Expected fields include protocolVersions, transport.modes (stateful, stateless), and bastion.hybridTransport: true.


Step 5 - Stateful client (legacy)

Send MCP requests with a session header - behavior matches pre-3.2 deployments:

curl -s -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Session-Id: my-legacy-session-1234567890" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {"name": "echo", "arguments": {"message": "hello"}}
  }'

Bastion resolves mcp_transport_mode: stateful and keys rate/cost limits by session.


Step 6 - Stateless client (explicit state handle)

Stateless clients pass a server-minted handle on every tool call (Bastion validates entropy; it does not mint handles):

HANDLE="client-state-handle-abc1234567890"

curl -s -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-03-26" \
  -H "MCP-State-Handle: $HANDLE" \
  -d "{
    \"jsonrpc\": \"2.0\",
    \"id\": 2,
    \"method\": \"tools/call\",
    \"params\": {
      \"name\": \"echo\",
      \"arguments\": {\"message\": \"hello\", \"state_handle\": \"$HANDLE\"}
    }
  }"

Bastion resolves mcp_transport_mode: stateless and keys limits by handle:$HANDLE - consistent across load-balanced replicas when Redis is enabled.


Step 7 - Agent stability (infinite loop mitigation)

With stability.enabled: true, repeated identical tool errors trigger graceful recovery:

on_detect Behavior
inject (default) Append circuit-breaker hint to tool result
block Deny with AgentLoopDetectedError (-32030)
warn Metadata flag only

Test by forcing a tool to return the same error three times; the fourth response should include the stability hint (inject mode).


Step 8 - In-process middleware (no proxy)

If you wrap your own server, enable the same mcp_transport block - middleware stamps identity on every guarded surface:

from mcp_bastion import build_middleware_from_config

middleware = build_middleware_from_config("bastion.yaml")

async def handle_mcp(context, call_next):
    return await middleware(context, call_next)

Troubleshooting

Symptom Fix
InvalidStateHandleError Handle must be 16–256 URL-safe chars; set required_in_stateless: false during migration
ProtocolVersionError Add client version to protocol.allowed_versions or disable protocol.enabled
Rate limits not shared across pods Set state_backend.type: redis
Discovery 404 Enable mcp_transport.discovery.enabled and use serve --proxy
Stateful clients broken after enable Keep mode: auto; do not force stateless until clients migrate

Next steps