Interoperability: MCP and A2A
The same tool model that backs the agent loop can be served two other ways: over the Model Context Protocol for a client like Claude Desktop, and over a NATS-based agent-to-agent protocol for peer agents. A run can also import a peer’s tools and present them to the model as if they were local.
Where it lives
internal/mcpserver serves over MCP. The a2a package holds the transport-agnostic protocol types; internal/a2anats binds them to NATS. internal/remotetools is the import-policy layer. The commands are mcp_command.go, a2a_command.go, and discover_command.go.
Serving over MCP
mcp serves util.ServedTools, the loaded set narrowed by expose.agent.tools, over streamable HTTP. The port comes from --port, then the config, then a default of 8080. Each tool keeps its fisk input schema. A tool with a name the protocol rejects is skipped with a warning.
A confirm-tagged command is realized over MCP as an elicitation that asks the client for an approve boolean, driven by a confirm mode: auto asks clients that can elicit and runs others ungated with a warning, always refuses a client that cannot elicit, and never delegates approval to the client’s own UI. The gate fails closed on anything that is not an explicit accept.
Load-bearing decision
The human-in-the-loop and memory tools are never exposed over MCP or A2A. They are built-in tools appended only inside an agent run, so they are structurally absent from the served set; no filter could add them back. Because MCP confirmation is a request a client may auto-approve, ai:deny is the only way to keep a command off a served surface entirely.
A2A over NATS
The a2a package models the protocol as self-describing messages. Every message embeds a Header carrying its own framing, so a captured message is fully decodable without the transport. The NATS binding never infers meaning from the subject; it decodes the header’s protocol id and dispatches on that. The subjects exist only as permission seams, which is what lets the same message bodies move to another transport later without change.
Both servers bound an un-budgeted caller: a shared semaphore caps in-flight calls at two and a per-call timeout defaults to thirty seconds. An execution failure is always an in-band error result, never a transport error, and a non-zero command exit is a successful result carrying the exit code, so the caller can reason about either.
Load-bearing decision
A served A2A agent has no operator, so it drops any confirm-gated tool outright rather than serving it ungated. This is the stricter analogue of the MCP behavior, and the same advice applies: use ai:deny to suppress a command entirely.
Importing a peer’s tools
A run with configured remote hosts discovers each one, filters by name, and assigns final model-facing names. A bare name is prefixed with the host’s alias when a local tool already holds it or when more than one host exposes it, decided over the whole set so runs are reproducible. A residual collision fails the run closed. The imported schema is untrusted and must parse as a JSON object. At call time a remote tool is invoked over NATS and its reply is mapped back to the same result shape a local tool produces, so the model cannot tell remote from local. Import is strict for a run, since the prompt may depend on those tools, but best-effort for info, which still shows local tools if a host is unreachable.
Reserved and planned
The streaming task flow, the Event, Result, Cancel, and Ack messages with their event blocks, is fully modeled and schema-validated but not yet carried by the NATS binding, which transports only discovery and direct tool calls. Wrapping the same message bodies in the Choria Protocol, with its authentication and authorization, is the planned second binding; the subject-contract design exists so that binding needs no change here.
Next
Continue to Reference and Map for the command surface, a source-file map, and a glossary.