Loams v0.1.0 is out: run the whole stack yourself with Docker Compose. See the roadmap

Blog/The protocols Loams speaks

MCP: how agents call Loams, and how Loams agents call each other

The Model Context Protocol's JSON-RPC messages, handshake, transports and auth; the stdio MCP server Loams' agent daemon injects into every coding-agent run today; and the stateless HTTP surface planned for the engine.

MCP: how agents call Loams, and how Loams agents call each other
On this page
  1. What the protocol is
  2. Why Loams speaks it
  3. What ships: the agent daemon's MCP shim
  4. What is planned: the engine's MCP surface
  5. Gotchas

The Model Context Protocol (MCP) is how an AI agent discovers and calls tools that live outside it. Claude Code, Codex, opencode and most other coding agents are MCP clients: give them a server's command line or URL and they list its tools, show them to the model, and call them when the model asks. Loams speaks MCP in two places. One ships today, inside the agent daemon behind Loams Desktop and Studio. The other, the engine's own MCP surface, is planned.

Specmodelcontextprotocol.io/specification (latest revision 2026-07-28 at the time of writing; the shim implements the handshake-based revisions 2025-06-18, 2025-03-26 and 2024-11-05)
Base protocolJSON-RPC 2.0
Shippingloams-agentd-mcp: a stdio MCP server, run as loams-agentd mcp, injected into every harness run
PlannedThe engine's MCP listener (stateless Streamable HTTP, 127.0.0.1:8083 by default), loams mcp serve (stdio bootstrap), memory and schema tools
DecisionsD24, D111, D288, D289, D788, D849, D885, D1022, D1049
In LoamsAvailable agent daemon shim Planned engine MCP

What the protocol is

Messages. MCP is JSON-RPC 2.0. A request has jsonrpc: "2.0", an id, a method and params; a response echoes the id with a result or an error (code, message, optional data); a notification is a request without an id and gets no response. The methods a tool server cares about are few: tools/list returns tool definitions (a name, a description, and a JSON Schema for the arguments), and tools/call runs one. A tool result is a list of content items (text, images, resource links) plus an optional structuredContent value, and an isError flag. That flag matters: a tool that fails tells the model through isError: true, not through a JSON-RPC error, so the model can read the message and try something else. Beyond tools, servers can offer resources (readable data addressed by URI) and prompts (templates the user picks), and clients can offer capabilities back to the server.

Handshake. Up to revision 2025-11-25, every connection began with initialize: the client sends the protocol revision it wants and its capabilities, the server answers with the revision it will use, its own capabilities (tools, resources, prompts, and whether each sends list-changed notifications), serverInfo and optional instructions for the model, and the client confirms with notifications/initialized. Revisions are dates: they name the last backward-incompatible change.

Revision 2026-07-28, the current one, removes that handshake. Every request now carries its protocol version and client capabilities in _meta (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities); a server that does not support the version answers UnsupportedProtocolVersionError with the versions it does. A new mandatory server/discover returns versions, capabilities and identity in one call for clients that want to choose up front. Protocol-level sessions are gone too, along with ping and SSE resumability. The changelog lists the rest; for a server, the consequence is that any node can answer any request.

Transports. Two are standard. stdio: the client launches the server as a child process and they exchange newline-delimited JSON-RPC messages on stdin and stdout, with logs on stderr. Streamable HTTP: the client POSTs each JSON-RPC message to one endpoint, and the server answers with a JSON body or an SSE stream for requests that produce progress. In 2026-07-28, HTTP requests also carry MCP-Protocol-Version, Mcp-Method and Mcp-Name headers, so a proxy can route and authorize without parsing the body. The older HTTP+SSE transport is deprecated.

Auth. stdio has none in the protocol: the server inherits whatever credentials its environment gives it. For HTTP, the authorization spec builds on OAuth 2.1. The MCP server is a resource server; it publishes Protected Resource Metadata (RFC 9728) pointing at its authorization server, clients obtain tokens with authorization code and PKCE, and send them as Authorization: Bearer. Client registration now prefers Client ID Metadata Documents over Dynamic Client Registration.

Why Loams speaks it

Two reasons, one for each side of the protocol.

Agents are users of Loams. An agent that searches a collection, reads a table or recalls a memory should not need an SDK written for it. MCP is the interface every agent host already has, so the plan since D24 is an MCP surface on the engine, with tool definitions retrieved by hybrid search and a tool graph rather than dumped into the context, exposed as find_tools and call_tool. Loams Memory adds memory and code tools on the same surface (D885).

Agents work together inside Loams. Loams Desktop and Studio run coding agents (Claude Code, Codex, opencode, Pi and ACP agents) in chats, inside projects, on your devices. An agent in one chat often needs to start another, hand it work, and wait for the answer. Each harness has its own plugin system; all of them have MCP. So the agent daemon gives every run an MCP server whose tools are the daemon's own chat operations.

What ships: the agent daemon's MCP shim

loams-agentd-mcp is the crate, run as loams-agentd mcp. It is an MCP server on stdio that forwards every tool call into the daemon's loopback RPC, the same WebSocket that Electron main and the CLI use. It holds no state and touches no files; the daemon stays the only authority over chats, devices and projects.

Shipping today. The shim holds no state; the daemon stays the only authority over chats. The run token can call only the shim's methods, on its own session and its side chats.

It is hand-rolled. A stdio tool server needs five methods: initialize, notifications/initialized, ping, tools/list and tools/call. The repository already owns JSON-RPC framing for the Codex and ACP drivers, so the shim adds no crates. It answers resources/list and prompts/list with empty lists rather than errors, because some clients call them whether or not the capability is advertised. Requests are handled concurrently, since a wait_for_turn can block for minutes while the client keeps pinging, and every response goes through one writer task so lines never interleave.

Version negotiation follows the handshake rules: if the client asks for 2025-06-18, 2025-03-26 or 2024-11-05, the shim echoes it; otherwise it offers 2025-06-18 and the client decides.

Tools. Fifteen: whoami, list_projects, list_harnesses, list_models, list_chats, get_chat, create_chat, create_chats, read_chat, send_message, send_messages, wait_for_turn, interrupt_chat, respond_to_input and archive_chat. The two batch tools exist because some harnesses run tool calls one at a time; with create_chats an agent starts up to 32 workers in one call instead of waiting for each. Results come back as text content and as structuredContent (wrapped in an object when the value is not one).

Injection, per harness. When the daemon launches a run, it adds the shim to that agent's MCP configuration with three variables in its environment: LOAMS_AGENTD_RUN_TOKEN, LOAMS_AGENTD_PORT and LOAMS_AGENTD_SESSION_ID. How depends on the harness:

HarnessHow the shim is injected
Claude Codea generated file passed with --mcp-config, merged with the user's own servers
Codexdotted overrides on thread/start (mcp_servers.<name>.command, .args, .env)
ACP agentsmcpServers in session/new
opencodeits MCP config, when the installed version is known
PiPi has no MCP client, so a per-run extension speaks MCP to the shim and registers its tools

Auth. stdio carries no credentials, so the shim's authority is the run token. It has role mcp (D788), it is valid for that run only, and the daemon accepts it only for the shim's methods on the run's own session and its side chats. Every message an agent sends through it is attributed to its originating chat, and a chat cannot message itself. Resolving an approval and the provider and secret methods need the owner role (Studio's remote clients get studio for approvals), and destructive Git actions (discard, restore, merge, push, delete a worktree) are never offered to an agent through the shim.

The shim ships in the loams-agentd image and in Loams Desktop. In Studio, the browser does not speak MCP at all: it drives the daemon through loams.agentd.v1 over Connect (see part 6), and the agents it starts get the shim.

What is planned: the engine's MCP surface

The 0.1.0 release notes list "MCP and analytics helpers" as Planned, and that is accurate for the engine: no loams binary in 0.1.0 serves MCP. The designs are concrete:

  • A listener of its own, 127.0.0.1:8083 by default (--mcp-listen, D111), stateless Streamable HTTP on revision 2026-07-28, with the official Rust SDK, rmcp (pinned at 3.4.1 in the workspace). Design 44 adds the same surface to the main port. Like every compatibility surface, it calls the same internal service traits as the Connect handlers and never reaches storage directly.
  • loams mcp serve, a stdio bootstrap server in the CLI (D289) with a fixed tool list: search_docs, get_sdk_snippet, loams_info, stack_status, stack_create, stack_start, env_export, add_package. No tool deletes, stops, mints keys, logs in or updates, and product mutations are not MCP tools (D849).
  • Schema-generated tools in the gateway's mcp module (D1022, D1049): one per named operation, plus describe_schema, explain and a read-only query. Agents get read-only access by default, writes only on an agent/<session> branch, and results wrapped as untrusted data.
Planned, on MCP revision 2026-07-28. Every request carries its protocol version and client capabilities, so any engine node can answer it.

Why stateless matters to us: an engine cluster has many nodes, and a session pinned to one would need sticky routing or shared session state. With 2026-07-28 each request is self-describing, so the MCP listener is one more stateless front on the same services.

Gotchas

  • Two generations of the protocol are live. The agents we inject into still speak the initialize handshake, so the shim does too. The engine surface targets 2026-07-28, which has no handshake. A server that wants both must accept initialize for old clients and per-request _meta for new ones; the spec's backward-compatibility section describes how.
  • isError is not a protocol error. Return tool failures as results with isError: true. A JSON-RPC error tells the client the call was malformed, and many clients do not show it to the model.
  • stdout is the wire. On stdio, anything a server prints to stdout that is not a JSON-RPC message corrupts the stream. Log to stderr.
  • Secrets never pass through MCP (D288). Tools return names and redacted values; credentials go to files the user controls. A model's context is not a safe place for a key.
  • Tool descriptions are prompts. They are read by a model, so they say when to use the tool, not just what it does. The shim's own instructions tell agents to launch all workers before waiting on any.
  • Untrusted data in, untrusted data out. A tool that returns rows or documents returns text an attacker may have written. Our plan wraps results as data and keeps agents read-only by default for exactly this reason (D1049).

Next in the series: the ClickHouse HTTP interface.

More from the blog