Model Context Protocol
Condukt agents can talk to external Model Context Protocol servers and call the tools those servers expose alongside their own statically defined tools.
The Condukt.MCP namespace ships a client-only implementation. Three
transports are supported:
stdio: spawns a subprocess and exchanges newline-delimited JSON-RPC over its stdin/stdout. Most local MCP servers ship as binaries that speak this transport.http_sse: the legacy 2024-11 HTTP+SSE transport. A long-livedtext/event-streamGET on the configured URL receives responses, and an SSEendpointevent tells the client where to POST requests.streamable_http: the 2025-03-26 Streamable HTTP transport. A single endpoint URL accepts POSTs that return either a JSON-RPC response inline or atext/event-streambody containing one or more events. New HTTP-based MCP servers should target this transport.
Declaring servers on an agent#
Add an optional mcp_servers/0 function to a module that uses
Condukt. Return a list of Condukt.MCP.Server structs or maps:
defmodule MyApp.Agent do
use Condukt
@impl true
def tools do
[Condukt.Tools.Read, Condukt.Tools.Bash]
end
def mcp_servers do
[
%Condukt.MCP.Server{
name: "github",
transport: {:stdio, command: "github-mcp-server", args: []},
env: ["GITHUB_TOKEN"]
},
%Condukt.MCP.Server{
name: "linear",
transport: {:streamable_http, url: "https://mcp.linear.app/mcp"},
auth: {:bearer, {:env, "LINEAR_API_KEY"}}
}
]
end
end
The session opens one Condukt.MCP.Client per server at startup,
fetches each server's tools/list, and merges the discovered tools
into the agent's tool list under their <server>.<tool> ids
(github.create_issue, linear.list_issues, ...). When the model
calls linear.list_issues, Condukt routes the call as a JSON-RPC
tools/call request to the Linear server and surfaces the response
to the model.
Servers can also be supplied per-call:
{:ok, response} =
Condukt.run("Open a draft PR for the sandbox refactor.",
model: "anthropic:claude-sonnet-4-6",
mcp_servers: [github_server],
tools: [Condukt.Tools.Read]
)
Authentication#
The library supports two HTTP authentication modes declared on the server:
# Static or env-resolved bearer token
auth: {:bearer, {:env, "LINEAR_API_KEY"}}
auth: {:bearer, {:static, "literal-test-token"}}
auth: {:bearer, {:op, "op://Engineering/Linear/token"}}
# OAuth 2.0 client credentials grant
auth:
{:client_credentials,
token_url: "https://auth.example.com/oauth/token",
client_id_env: "MCP_CLIENT_ID",
client_secret_env: "MCP_CLIENT_SECRET",
scope: "mcp.read mcp.call"}
Bearer secret refs use the same shapes accepted by Condukt.Secrets:
{:env, NAME}, {:static, VALUE}, {:op, REF}, or {module, opts}
for a custom Condukt.SecretProvider.
Interactive OAuth flows that require a browser are intentionally not supported. Resolve such tokens out of band, for example through a separate one-time CLI login that writes the resulting refresh or access token into your secret store, and reference the resulting value through the bearer or client_credentials shapes above.
For stdio servers auth is unused. Credentials flow into the
subprocess as environment variables declared on the server's env
field:
env: ["GITHUB_TOKEN", "GITHUB_API_URL"] # passthrough from parent
env: %{"GH_TOKEN" => {:env, "GITHUB_TOKEN"}} # rename
env: %{"GH_TOKEN" => {:static, "ghp_local"}} # literal
Resolved bearer tokens are stored on the in-memory transport and used
as Authorization: Bearer <token> headers on every outgoing request.
If you want them redacted from session transcripts and tool result
snapshots, declare them in :secrets as well:
secrets: [LINEAR_API_KEY: {:env, "LINEAR_API_KEY"}]
Condukt.Secrets then exact-match redacts the value from outbound
messages and stored tool results.
Tool naming#
By default each tool is exposed under <server_name>.<tool_name>.
Override with the :prefix field on the server:
%Condukt.MCP.Server{name: "github", transport: ..., prefix: "gh"}
# tools: gh.create_issue, gh.list_pull_requests, ...
Set prefix: "" to expose tools under their bare server names. Use
this only when you control every server name and can rule out
collisions.
Sandbox interaction#
Stdio MCP servers are user-configured external binaries that Condukt
spawns as part of session setup. They are not routed through
Condukt.Sandbox for the same reason Condukt.Tools.Command is
exempt: the binary is selected by the operator, not by the model.
HTTP and streamable HTTP transports issue plain HTTP requests through
Req and have nothing to sandbox.
If you need stronger isolation, run the MCP server inside a container
or under a process supervisor that enforces the constraints you need,
and point the :command at the container entrypoint.
Lifecycle and supervision#
Condukt.MCP.Client is a GenServer linked to the session that
declared its server. When the session terminates, every client it
opened terminates too: stdio subprocesses receive EOF on stdin and
shut down, and HTTP transports close their open requests.
A client that loses its connection mid-run does not restart in v1.
Outstanding tool calls fail with {:error, {:transport_down, _}} and
the session continues. Future iterations may add a per-server restart
policy.