Skip to content

Cross-language: MCP

Available · pip install "agent-adapter[mcp]"

MCP over stateless Streamable HTTP is the universal export. Every language has an official MCP client, and most agent frameworks load MCP tools in one line. Your tools stay Python functions; the executor's policies (hooks, approval, limits, cache, ledger) still run on every call.

flowchart LR
  REG[("Registry")] --> APP["streamable_http_app<br/>POST /mcp · JSON · no session"]
  APP --> TS["TypeScript / JS"]
  APP --> GO["Go"]
  APP --> JV["Java · Kotlin"]
  APP --> NET[".NET"]
  APP --> PY["Python hosts"]
  APP --> FW["agent frameworks'<br/>MCP adapters"]

Serve

app = streamable_http_app(registry)  # stateless, JSON responses, served at /mcp


def serve() -> None:
    import uvicorn  # installed with the mcp extra

    uvicorn.run(app, host="127.0.0.1", port=8000)  # http://127.0.0.1:8000/mcp
Choice Default Why
stateless=True no Mcp-Session-Id every request stands alone: any worker can answer, load balancers need no stickiness
json_response=True application/json one response per request, no SSE stream to hold open
path="/mcp" where the endpoint lives inside the app
executor= the default executor the policies every call runs under

Mount it in another ASGI app (Starlette, FastAPI) and run its lifespan there: app.router.lifespan_context(app) starts the MCP session manager. The HTTP tool server (agent_adapter.server.create_app) mounts it at /mcp behind its own authenticator.

Production settings

**http_options go to the MCP SDK's Server.streamable_http_app.

Need Pass
a public host name host=… with transport_security=TransportSecuritySettings(allowed_hosts=[…], allowed_origins=[…]) and auth (next row); without transport_security (DNS-rebinding protection) it is always a ConfigError; without auth it is one too, unless allow_anonymous=True
bearer tokens auth=AuthSettings(…, resource_server_url=…, validate_token_resource=True) and token_verifier=…, always together
client approval behind several workers client_approval=True, request_state_keys=[secret], name="<unique>", approval_store=<shared store> (the same secret and store everywhere; without a store it is a ConfigError)
your own notion of "who" principal=lambda ctx: … returning a str; it is not auth, and a None is refused unless allow_anonymous=True

streamable_http_app sets streamable_http_path, json_response and stateless_http itself; passing them in http_options is a ConfigError.

Security

Rule Why
without auth, any client that can reach the port can call every tool auth is auth=AuthSettings(…) with token_verifier=…; without it a local host logs a WARNING at build and refuses non-loopback peers (403 non-local-client)
a custom principal= is not auth the rules above still apply; a request it names no caller for is refused (authentication required) unless allow_anonymous=True
a non-local host with no auth is a ConfigError unless allow_anonymous=True (a WARNING at build); the same fail-fast rule as the HTTP server
auth= and token_verifier= go together either alone checks no token, so it is a ConfigError
client_approval=True makes the remote MCP client the approver the executor's approval port is not asked for confirm tools; an agentic host may accept the form by itself, so use it only where a person really reviews it
run and call ids are derived from the principal and the client's key a client can never name another client's run, call or approval
the default principal is the bearer token (client id, issuer, subject) principal= replaces it; a raising hook refuses the request
anonymous requests: client keys and X-Run-Id are ignored unless allow_anonymous=True (a WARNING at build: anonymous clients can share runs)
a call key counts only to resume a pending approval reusing a key never re-runs a once_per_run tool or dodges max_calls
resumes are checked in the DeferredApproval store a resume works on any worker sharing the ApprovalStore; with other ports resume keys live in one process, and a resume on another worker asks again
every resume and client-approval state is single use a replay never re-runs a tool, on any worker sharing the approval_store, which records the client's decision too

Pass the host you bind to. A local app without auth refuses non-loopback peers, but it cannot see past a proxy. Never put an anonymous app behind a reverse proxy or a unix socket (uvicorn --uds, gunicorn) that remote clients can reach: every request then looks local. Without auth every request is anonymous, and no approval can be resumed.

Connect

One call in each ecosystem; point it at https://<host>/mcp.

Client Connect
TypeScript SDK new Client(…).connect(new StreamableHTTPClientTransport(new URL(url)))
Vercel AI SDK createMCPClient({ transport }), then tools: await mcp.tools()
LangChain.js / LangGraph.js new MCPAdapter({ servers: { tools: { url, headers } } }), then await adapter.listTools() (@langchain/mcp-adapters 2.0)
OpenAI Agents JS new MCPServerStreamableHttp({ url, name })
Go SDK mcp.NewClient(…).Connect(ctx, &mcp.StreamableClientTransport{Endpoint: url})
Java SDK McpClient.sync(HttpClientStreamableHttpTransport.builder(url).build())
.NET SDK McpClient.CreateAsync(new HttpClientTransport(…))
Python mcp_client(url, headers=…) or auth=oauth_client(…), then import_mcp(registry, client)

Per-stack recipes with exact config: Connect anywhere.

What MCP does not carry

Need Use
approval decided in Python, with edited arguments DeferredApproval in the executor; MCP returns approval required: <id>; decide it in the store or with the HTTP server
a host that drops _meta resume keys tasks=TaskOptions(): a 2026-07-28 host with the tasks extension polls the pending call by task id (MCP tasks and OAuth); other hosts cannot resume a confirm tool
the user of the MCP client approves client_approval=True (2026-07-28 clients with form elicitation); the remote client is then the approver, and the executor's port is not asked
cost per run, run events (SSE), provider-shaped catalogues the HTTP tool server

See MCP for the tool mapping, errors, ids and the importer.