Skip to main content
Most clients reach Reticle over stdio: they spawn reticle mcp, and the tools appear in the agent’s surface. That has one property you cannot always live with. Many clients read their MCP tool list once, at startup. If the daemon was not running when the client launched, the reticle_* tools are absent for the rest of that session, and the fix is to restart the client. The daemon also speaks MCP over HTTP, on the same port as everything else. Nothing has to reload for a client to reach it, so this is the transport for scripted runs, CI, a language with no MCP client library, and any editor that will not pick up tools mid-session. It is the same tool surface. Not a subset, not a simplified one. reticle_snapshot, reticle_act_and_wait, reticle_assert and the rest behave exactly as they do over stdio, because they are the same server behind a different transport.

The two endpoints

The daemon listens on port 4400 by default (RETICLE_PORT, or reticle serve --port N). Run reticle status to confirm which port is live.

The handshake

The one shape worth reading before you write any code: the POST does not answer your request. It replies 202 Accepted with no result in it, and the actual JSON-RPC reply arrives on the SSE stream you opened first. A client that waits on the POST body waits forever.
  1. GET /mcp/sse. Hold the response open.
  2. The first frame is the endpoint announcement, and it carries the session id:
    Use that data value verbatim as the path you POST to. Do not build it yourself: the session id is minted per connection.
  3. POST the initialize request there, then the notifications/initialized notification.
  4. POST tools/list, tools/call, and anything else. Match each reply to its request by the JSON-RPC id, off the SSE stream.
When the daemon shuts down it writes a shutdown frame on the stream before the socket closes, so a client can tell a planned stop from a dropped connection.

Errors

404 and 400 are kept apart on purpose: reconnecting fixes one and nothing about the other.

Authorization

Two tiers, the same ones the WebSocket bridge uses. Local clients need no token. A request is trusted when its peer address, its Host header, and its Origin/Referer (when present) are all loopback. All three are required, because a DNS-rebound page reaches the daemon as a loopback peer while carrying the attacker’s Host, so peer address alone would not be a check. Anything else must present the pairing token, which is required whenever you bind beyond loopback with RETICLE_HOST. Either form works:
Set it with RETICLE_TOKEN. With no token configured, the daemon binds loopback-only and non-local requests are refused outright rather than falling back to trust.

A minimal client

No MCP library. Node’s standard library is enough. This opens a session, lists the tools, and takes a snapshot.
To watch the raw frames instead, curl is enough for the first half:
Then, from a second shell, POST to the path that stream printed:
The 202 comes back here; the tool list appears in the first shell.

What this does not change

The transport carries the tools. It does not replace the rest of the setup. The app still has to be instrumented and connected, and reticle_sessions is still what tells you whether a session is there to drive. A verdict reached over HTTP is a verdict reached the usual way: only verified: "yes" is a pass. The endpoints, the handshake, the three status codes and the 202-then-SSE shape are pinned by packages/server/src/mcp-http-transport.test.ts, which drives them over raw HTTP with no client library, so the contract this page describes fails the build if it changes.
Last modified on September 5, 2026