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/get lookups 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.