ExAgent. MCP. Client
(ExAgent v2.2.1)
View Source
A client for Model Context Protocol servers
over stdio or opt-in Streamable HTTP, exposing tools as ExAgent.Tools.
For HTTP use transport: :streamable_http, url: url, finch: MyFinch and an
application-owned HTTP1-only pool. See ExAgent.MCP.StreamableHTTP for limits
and configuration preconditions. HTTP uses MCP2025-06-18; stdio is unchanged.
After a session404, reconnect/1 explicitly starts a new handshake. No tool
request is replayed. Direct call_tool/3 is transport IO, not runtime approval.
Generated tools use the normal validation and permission boundary. Persisted
continuation requires explicit trusted endpoint/principal references through
:continuation_binding; ordinary tools work without those references.
An MCP server is an external process that speaks JSON-RPC 2.0 over stdin/stdout
(e.g. npx -y @modelcontextprotocol/server-filesystem ./). This client spawns
it, performs the initialize handshake, lists its tools, and turns each into an
ExAgent.Tool whose execution forwards a tools/call back to the server.
Example
chat_model is an explicitly configured ExAgent.Models.ReqLLM instance with
credentials and the qualified tool profile (see the README recipe).
# 1) start the server + handshake
{:ok, client} =
ExAgent.MCP.Client.start_link(
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "./data"]
)
# 2) discover its tools as ExAgent tools
{:ok, tools} = ExAgent.MCP.Client.tools(client)
# 3) use them like any ExAgent tool
agent = ExAgent.new(model: chat_model, tools: tools)
ExAgent.run(agent, "list the files")Concurrency and request ownership
A client owns one transport. Requests are sent independently and can execute
concurrently on the remote server; the GenServer does not serialize remote
tool effects. Each pending request has a deadline and monitors its caller.
A timeout returns {:error, :timeout} and caller death drops the local request.
Neither proves that a remote tool stopped or rolled back, and neither triggers
replay. Requests beyond :max_pending return {:error, :busy} before sending.
Incoming newline-delimited frames are limited by :max_frame_bytes. An oversized
frame closes the transport, fails remaining pending requests, and leaves the
client not ready. Complete responses preceding that frame are processed first,
independently of transport chunk boundaries. These bounds limit pending state
and individual frames, not the BEAM mailbox or aggregate traffic from a push
transport.
Testing seam
The transport is pluggable: pass transport: {send_fun, ref} (mostly for
tests) to inject a fake transport instead of spawning a real process. The
client receives data as messages shaped {ref, {:data, binary}}.
send_fun must return promptly: it runs in the client process, so a blocking
callback delays deadlines and other client operations. {:error, reason},
false, exceptions and throws/exits indicate an unconfirmed send; other return
values (including the value returned by send/2) retain their existing meaning.
Summary
Functions
Call a tool on the server by name. Returns {:ok, text} (the concatenated
content text) or {:error, reason}. Used by the generated ExAgent.Tools.
Returns a specification to start this module under a supervisor.
Shut down. HTTP makes a bounded DELETE when sessionful; errors still stop the client.
Start a fresh HTTP handshake after session invalidation; never replay tools.
Start the client and perform the initialize handshake. Stdio spawns a server;
HTTP connects using the supplied application-owned Finch instance.
List the server's tools as ExAgent.Tools. Each tool's call forwards a
tools/call to the server and returns its text result.
Types
@type t() :: GenServer.server()
Functions
@spec call_tool(GenServer.server(), String.t(), map()) :: {:ok, String.t()} | {:error, term()}
Call a tool on the server by name. Returns {:ok, text} (the concatenated
content text) or {:error, reason}. Used by the generated ExAgent.Tools.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec close(GenServer.server()) :: :ok | {:error, term()}
Shut down. HTTP makes a bounded DELETE when sessionful; errors still stop the client.
Start a fresh HTTP handshake after session invalidation; never replay tools.
@spec start_link(keyword()) :: GenServer.on_start()
Start the client and perform the initialize handshake. Stdio spawns a server;
HTTP connects using the supplied application-owned Finch instance.
Options
:command— executable to spawn (required for stdio).:args— list of args passed to the command (default[]).:env— list of{bin, bin}env vars (default[]).:timeout— handshake / per-request timeout in ms (default5000). Must be a positive integer; requests return{:error, :timeout}on expiry.:max_pending— maximum concurrent pending requests (default128).:max_frame_bytes— maximum bytes in a newline-delimited incoming frame, excluding its newline (default8388608, 8 MiB). Also bounds partial frames. Both limits must be positive integers.:cd— working directory for the spawned server.:transport—{send_fun, ref}test seam (see moduledoc). When set, no process is spawned;send_fun.(ref, iodata)must deliver bytes to the server and responses must arrive as{ref, {:data, binary}}messages. Alternatively:streamable_httpselects the HTTP adapter and requires:urland:finch.:headersdefaults to[]; HTTP protocol version is fixed at2025-06-18. SeeExAgent.MCP.StreamableHTTPand the implementation guide for the positive integer HTTP byte/control/discovery limits.:continuation_binding—%{endpoint: reference, principal: reference}for persisted tool identity; each reference is a non-secret%{"id" => stable_id, "version" => version}supplied by the trusted host. HTTP also binds a digest of its effective public URL and requires no userinfo, query or fragment. Stdio's endpoint reference identifies the command/args/environment's semantics without persisting those values. Transport and protocol are bound by the client. References do not authenticate the peer or prove the principal of supplied credentials; the host must maintain that association. Credential rotation for the same principal uses a newly constructed client with unchanged references. Headers, credentials, Finch names, PIDs and session IDs are never in this binding. Missing binding leaves ordinary IO usable but durable execution fails before model/tool IO; invalid explicit binding rejects startup.
@spec tools(GenServer.server()) :: {:ok, [ExAgent.Tool.t()]} | {:error, term()}
List the server's tools as ExAgent.Tools. Each tool's call forwards a
tools/call to the server and returns its text result.