JSON-RPC STDIO ↔ HTTP Proxy
A messenger agent speaking between worlds
Legatus is a minimal, stateless bridge that translates dialogue over STDIN into HTTP actions. It embodies the Umwelt principle of perception–action unity: every request passes through a complete functional circle — perception, cognition, action — before returning as a meaningful response.
Legatus solves a fundamental problem: how to let any process speak JSON-RPC over STDIO while delegating execution to an HTTP realm.
Use cases:
- Editor integrations: Connect your editor to language servers over HTTP
- CLI tools: Build command-line tools that proxy to HTTP APIs
- Agent communication: Enable processes to communicate via standard streams
- Testing: Interact with HTTP JSON-RPC servers from shell scripts
Legatus is not merely a tool; it is a ritual of translation — a disciplined pattern for agent-to-world communication.
# Build the escript
mix escript.install hex legatus
# or
git clone git@github.com:sovetnik/legatus.git
mix escript.build
# Start your JSON-RPC HTTP server (example on port 4000)
# Then send a request:
echo '{"jsonrpc":"2.0","method":"add","params":[2,3],"id":1}' | \
./legatus http://localhost:4000/rpcExpected output:
{"jsonrpc":"2.0","result":5,"id":1}That's it. One line in, one line out. STDIO becomes HTTP, HTTP becomes STDIO.
The architecture follows Jakob von Uexküll's Umwelt concept — organisms perceive and act within their own "subjective universe":
- Aussenwelt — The external world (STDIO streams)
- Merkwelt — Perception layer (request validation)
- Wirkwelt — Action layer (HTTP transport)
- Umwelt — The complete functional circle (pipeline coordinator)
- Geist — The animating principle (main loop)
Legatus acts within the Clausura Operationalis — it knows only the world it perceives and acts upon it coherently. Its simplicity is not limitation but discipline.
mix escript.build
./legatus http://localhost:4000/rpcmix legatus http://localhost:4000/rpcWhen your upstream server requires authentication, pass the token via environment variable:
# Using escript
token=your_secret_token ./legatus http://localhost:4000/rpc
# Using mix task
token=your_secret_token mix legatus http://localhost:4000/rpcLegatus will automatically add the Authorization: Bearer <token> header to all HTTP requests.
Editor integration example (Zed, Claude Code, etc.):
{
"context_servers": {
"my_server": {
"source": "custom",
"enabled": true,
"command": "legatus",
"args": ["http://localhost:4000/rpc"],
"env": {"token": "your_secret_token"}
}
}
}Every request flows through the complete Umwelt cycle:
STDIN → Geist.loop → Aussenwelt.receptio → Merkwelt.percipere →
Wirkwelt.portare → Aussenwelt.profanatio → STDOUT
Each arrow represents a transformation of meaning, not just data.
The pipeline uses tagged tuples to track data state:
-
Receptio (Aussenwelt): Parse JSON
{:phaenomenon, map}|{:fiasco, json_error} -
Percipere (Merkwelt): Validate request
{:actio, map}|{:fiasco, error_map} -
Portare (Wirkwelt): HTTP transport
{:gloria, map}|{:fiasco, error_map}|{:silentium, map} -
Profanatio (Aussenwelt): Format output
{:gloria, json}|{:fiasco, json}|{:silentium, "Nullius in verba"} -
Emit (Geist): Write to STDOUT or skip
- Legatus — Entry point and configuration
- Geist — Main loop (STDIN → STDOUT cycle)
- Aussenwelt — I/O boundaries (parse/format JSON)
- Merkwelt — Request validation (JSON-RPC compliance)
- Wirkwelt — HTTP coordinator (interprets transport responses into Umwelt concepts)
- Wirkwelt.Httpc — HTTP transport (pure POST with status/body)
- Chronica — Logging (stderr diagnostics)
Wirkwelt follows a clean separation of concerns:
Wirkwelt behaviour contract:
@callback post(request :: map()) ::
{status :: pos_integer(), body :: map() | String.t()} | {:error, reason :: term()}- Transport (Httpc): Returns raw HTTP responses
{200, body}or{:error, reason} - Coordinator (Wirkwelt): Interprets responses into
{:gloria, map()},{:silentium, map()}, or{:fiasco, map()}
This design makes it easy to add new HTTP adapters (Finch, Hackney) without changing business logic.
All errors are JSON-RPC compliant:
-32700Parse error (invalid JSON)-32600Invalid Request (missing method)-32000HTTP errors (4xx/5xx)-32001Transport errors (connection refused)
Legatus is configured via command-line argument — no static config required:
mix legatus http://localhost:4000/rpcThe upstream URL is set dynamically at runtime via Application.put_env/3.
- Tagged tuples for explicit data flow
- Pure functions where possible (I/O at boundaries)
- Pattern matching over conditionals
- Separation of concerns (parse, validate, transport, format)
- Testability through dependency injection
- ✅ Standard requests with
id - ✅ Notifications (no
id) - ✅ Batches (array of requests)
- ✅ Success responses (
result) - ✅ Error responses (
error) - ✅ HTTP 204 handling (notifications)
- ✅ Batch responses
- STDIO-based (one request per line)
- HTTP POST only (no WebSocket/SSE)
- No built-in retry logic
- Single upstream server
See LICENSE file.
- Legatus (Latin) — envoy, ambassador, messenger
- Umwelt (German) — environment, "self-centered world"
- Geist (German) — spirit, mind, animating principle
- Aussenwelt (German) — outer world
- Merkwelt (German) — perceptual world
- Wirkwelt (German) — world of action
Explore further:
- Read umwelt.dev/docs for philosophical foundations
- Join the discussion in Issues
- Contribute via Pull Requests
Related projects:
- Skull — The cognitive substrate for agent systems
- Reticulum Universalis — Communication patterns for distributed agents
Legatus is an experiment in operational closure — every component speaks the same language of perception and action. If this resonates with you, welcome to the conversation.