Bridge between an external Model Context Protocol (MCP) server and
the Hot agent run-loop. Resolves a server config, negotiates modern
discovery or legacy initialization, lists the server's tools, and wraps each
remote tool as an ::ai::tool/Tool so it can be merged with
locally-defined tools and dispatched by ::ai::chat/run-loop.
Server configs
A server is described by a Map with a required url field and an
optional headers map:
{url: "https://mcp.weather.example.com/mcp",
headers: {Authorization: `Bearer ${::hot::ctx/get(\"WEATHER_TOKEN\")}`}}
Any other keys are ignored.
Specs accepted by for-agent
- a Map literal — used as-is.
- a Var or Fn — resolved by calling it (with no args) to produce a
Map; lets you compose a config from
::hot::ctx/getlookups at runtime without bundling secrets into source files. - a single-key Map
{<name>: <config-or-fn>}— names the server in logs and error messages while still resolving the inner value.
Example
::ai-mcp ::mcp::ai
weather-config fn (): Map {
{url: ::hot::ctx/get("WEATHER_MCP_URL"),
headers: {Authorization: `Bearer ${::hot::ctx/get(\"WEATHER_MCP_TOKEN\")}`}}
}
tools ::ai-mcp/for-agent([
weather-config,
{time: {url: "https://time.example.com/mcp"}}
])
// tools is Vec<::ai::tool/Tool> ready to drop into ChatOptions.tools
Values
CLIENT-NAME
CLIENT-NAME: Str "hot-ai-agent"
CLIENT-VERSION
CLIENT-VERSION: Str "1.0.0"
Functions
for-agent
fn (specs: Vec): Vec<::ai::tool/Tool>
Resolve every entry in specs to a remote tool list and concatenate
the results. Empty specs returns []. Errors from individual
servers propagate as failures so the agent operator sees the
misconfiguration immediately rather than silently dropping tools.
Example
::ai-mcp ::mcp::ai
::ai-tool ::ai::tool
tools ::ai-mcp/for-agent([
() { {url: ::hot::ctx/get(\"WEATHER_MCP_URL\")} },
{time: {url: \"https://time.example.com/mcp\"}}
])
chat-tools concat(tools, ::ai-tool/for-agent([my-local-fn]))
from-server
fn (config: Map): Vec<::ai::tool/Tool>
Connect to a single MCP server (lazy: this performs the network
handshake) and return its tool list as Vec<::ai::tool/Tool>.
config must contain a url and may contain headers; other
keys are ignored (see validate-config).
options-with-mcp
fn (opts: ::ai::chat/ChatOptions, mcp-servers: Vec?): ::ai::chat/ChatOptions
Return a new ::ai::chat/ChatOptions with remote MCP tools merged
into tools: so the rest of the agent run-loop is unchanged.
Local tools win on name collision: any remote tool whose name
already appears in opts.tools is dropped (with no warning — the
agent author is the source of truth for their tool surface).
The MCP handshake happens here, not inside ::ai::chat/run-loop, so
callers can decide when to pay the network cost (typically once per
agent invocation) and surface connection failures in their own
error path rather than mid-conversation.
Example
::ai-mcp ::mcp::ai
::ai-chat ::ai::chat
base ::ai-chat/ChatOptions({
chat-fn: ::anthropic::messages/chat-with-tools,
model: "claude-sonnet-4-5",
tools: [::ai::tool/from-fn(local-add)],
})
opts ::ai-mcp/options-with-mcp(base, [
() { {url: ::hot::ctx/get(\"WEATHER_MCP_URL\")} },
{time: {url: \"https://time.example.com/mcp\"}}
])
answer ::ai-chat/run-loop(opts, "What's the weather in Portland?")
resolve-spec
fn (spec: Any): Map
Reduce one entry from mcp-servers: to {name?, config: Map}.
Accepted shapes:
- A Fn (or Var bound to a Fn): called with no args; the returned
value is recursively resolved (so a fn may return either a bare
Map or a
{name: ...}named entry). - A bare Map (any keys): returned as
{name: null, config}. - A single-key Map
{<name>: <Map-or-Fn>}: the key becomes the label and the inner value is recursively resolved. The single key is recognized as "named" only when its value is a Map or Fn — single-key configs that happen to use a primitive value (e.g.{url: \"...\"}is itself a multi-key map in practice; a literal{onlykey: 1}is treated as a config).
Anything else fails with a clear error.
validate-config
fn (resolved: Map): Map
Extract the {url, headers} slice the rest of the bridge needs.
Requires a non-empty Str url; headers is optional and defaults
to {}. Other keys on the input config are silently ignored.
Fails with a single error pointing at the server label when the required shape isn't met.
wrap-remote-tool
fn (session: ::mcp::types/Session, remote: ::mcp::types/Tool): ::ai::tool/Tool
Wrap a single remote MCP tool as an ::ai::tool/Tool. The tool's
fn is a closure that calls ::mcp::tools/call with the
captured session and tool name, and pass-input is set so
::ai::tool/dispatch forwards the raw input map.
The wrapped tool's name is the remote name unchanged; the chat
layer is responsible for namespacing if it wants to disambiguate
tools coming from multiple servers.