MCP integration Experimental
MCP HTTP transport
Understand maintained HTTP/SSE transport, optional OAuth, anonymous fallback, and limited 401 retry.
View Markdown source ↗This is maintained MCP tool/client infrastructure. It is not deprecated by the new agent server; only the separate ChatMD prompt-serving MCP host is legacy. See MCP tool configuration and discovery identity/lifetime.
Mcp_transport_http connects an MCP client to a remote server over
plain HTTP or HTTPS. The module speaks the Streamable-HTTP
variant of the protocol (spec rev. 2025-03-26) and automatically
handles JSON bodies and Server-Sent-Event (SSE) streams.
The implementation is built on top of:
Piaffor the HTTP/1.1 + HTTP/2 client engine.Eiofor portable concurrency and fibres.- OAuth token management for optional bearer-token authentication.
1 How it works
Section titled “1 How it works”connectparses the endpoint URI. If?auth=true(default) it runs a best-effort OAuth 2 client-credentials or PKCE flow viaOauth2_managerand stores the resulting access token.- A persistent
Piaf.Client.tis created for the scheme & authority portion of the URI. Eachsendcall spawns one fibre that performs an HTTPPOSTto the path part with the JSON payload in the request body. - When the response comes back, the transport inspects its
Content-Typeheader:application/json→ parse body once and enqueue all JSON values.text/event-stream→ spawn a reader fibre that decodes SSE events. Within each event,data:fields are joined with newlines and decoded as one JSON value; the special[DONE]marker is ignored. A blank line completes the event immediately, even when the response body stays open.
- Values are delivered to callers via an
Eio.Stream.tso that multiple fibres can callrecvconcurrently.
Session stickiness – if the server sets the Mcp-Session-Id header,
the transport stores the latest present value and includes it in subsequent
requests. A response without that header leaves the previous value unchanged.
2 Public API
Section titled “2 Public API”The module instantiates the
Mcp_transport_interface.TRANSPORT
signature. Only HTTP-specific behaviour is highlighted below.
type t
val connect : ?auth:bool -> sw:Eio.Switch.t -> env:Eio_unix.Stdenv.base -> string -> t
val send : t -> Jsonaf.t -> unitval recv : t -> Jsonaf.t
val is_closed : t -> boolval close : t -> unit
exception Connection_closed2.1 URI scheme
Section titled “2.1 URI scheme”http://api.acme.com/mcp/v1 (HTTP/1.1)https://api.acme.com/mcp/v1 (HTTP/1.1 or HTTP/2 via ALPN)mcp+http://api.acme.com/mcp/v1 (alias for http)mcp+https://api.acme.com/mcp/v1 (alias for https)Any other scheme raises Invalid_argument in connect.
2.2 Authentication
Section titled “2.2 Authentication”When ?auth=true the transport attempts to fetch an access token from
the issuer (scheme + authority of the endpoint URI). Credentials are
looked up in the following order:
- URI query parameters –
?client_id=…&client_secret=… - Environment variables –
MCP_CLIENT_ID/MCP_CLIENT_SECRET - Client store – previously persisted credentials
- Dynamic registration – fetch metadata with
GET /.well-known/oauth-authorization-server, thenPOSTthe registration payload to itsregistration_endpoint, or the conventional/registerfallback.
Persisted confidential-client credentials use the client-credentials grant;
public-client entries use PKCE. If credential setup or token retrieval returns
an operational failure, connection setup continues without an Authorization
header. Eio cancellation propagates and does not trigger anonymous fallback.
Closing the transport wakes blocked receivers with Connection_closed.
POST/body errors, HTTP error responses, malformed JSON, and an SSE stream ending
before its matching RPC response terminate the transport as well. Normal SSE
completion after the matching response does not close the client. An empty
HTTP 202 acknowledgement remains valid for notifications. The high-level
Mcp_client drains pending requests when the transport terminates.
3 Examples
Section titled “3 Examples”3.1 Listing MCP tools over HTTP
Section titled “3.1 Listing MCP tools over HTTP”Use the high-level client to perform initialization and correlate replies. This example targets a placeholder server that does not require OAuth.
open Core
let () = Eio_main.run @@ fun env -> Eio.Switch.run @@ fun sw -> let client = Mcp_client.connect ~auth:false ~sw ~env "https://mcp.example/mcp" in Fun.protect (fun () -> let tools = Mcp_client.list_tools client |> Result.ok_or_failwith in List.iter tools ~f:(fun (tool : Mcp_types.Tool.t) -> Eio.Flow.copy_string (tool.name ^ "\n") (Eio.Stdenv.stdout env))) ~finally:(fun () -> Mcp_client.close client)3.2 Sending a raw MCP request
Section titled “3.2 Sending a raw MCP request”For lower-level integration, after completing the initialize /
notifications/initialized handshake, send a JSON-RPC tools/list request.
The caller must allocate a unique ID and route responses and notifications from
recv. JSON and SSE responses share that same receive API.
let request_tools conn ~id = let request = Mcp_types.Jsonrpc.make_request ~id:(Mcp_types.Jsonrpc.Id.of_int id) ~method_:"tools/list" ~params:(`Object []) () in Mcp_transport_http.send conn (Mcp_types.Jsonrpc.jsonaf_of_request request)4 Behavioural contract
Section titled “4 Behavioural contract”- Non-blocking
send– schedules a background POST; returning does not guarantee that the remote server has received the request. - Blocking
recv– waits for the next complete JSON value. - Idempotent close –
closemay be called multiple times and from any fibre. - Error surface – once
Connection_closedhas been raised the handle is permanently unusable.
5 Known limitations
Section titled “5 Known limitations”- Limited 401 retry – with credentials, the transport retries once. If an access token is already present, it resends that token; it does not force refresh or invalidate the cache on rejection. With no token it calls the token manager before retrying. This pre-existing limitation is separate from token acquisition/refresh decoding fixes. Network-level failures are not retried.
- Back-pressure – the in-memory queue is fixed at 64 messages. If the
client does not call
recvfast enough the enqueue will block the SSE reader fibre and eventually the server. - No HTTP/3 – currently limited to HTTP/1.1 & HTTP/2 (whatever Piaf negotiates).
6 Extending / debugging
Section titled “6 Extending / debugging”- Enable
EIO_TRACE=1to diagnose low-level scheduling and I/O. - Piaf’s own debug logs can be activated with the usual
Logsconfiguration machinery.