JSON-RPC
JSON-RPC
A request/response protocol with JSON payloads. Specifies what a request and response look like, leaves transport unspecified. The de facto standard for local IPC where you want a familiar message shape but don't need protobuf's compactness.
DAP is JSON-RPC-shaped (close but not strictly compliant). LSP is JSON-RPC. MCP is JSON-RPC. Lots of tooling speaks it.
The shape
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "search",
"params": { "query": "from:alice", "limit": 10 }
}
jsonrpc: "2.0"— versionid— correlation token; included in the response so the caller knows which request it answersmethod— string naming the operationparams— arguments (object or array)
Response (success)
{
"jsonrpc": "2.0",
"id": 1,
"result": { "messages": [...] }
}
Response (error)
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32603, "message": "Internal error", "data": {...} }
}
Standard error codes (per spec):
-32700Parse error-32600Invalid request-32601Method not found-32602Invalid params-32603Internal error-32000to-32099Server-defined errors
Notification (fire-and-forget)
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {...}
}
No id — no response expected. Pure event push.
Batch
A JSON array of requests. Server returns a JSON array of responses. Lets you send multiple operations in one round-trip. Used in LSP for related operations.
Transport
JSON-RPC says nothing about transport. Common pairings:
- stdio — over a child process's stdin/stdout. LSP, DAP, MCP all use this.
- Unix socket — local IPC. mxr, lazydap.
- TCP — when local-only isn't enough.
- HTTP — JSON-RPC over POST. Common for blockchain APIs (Ethereum), some web RPC.
- WebSocket — bidirectional streaming for browsers.
Each transport needs a framing layer. Over stdio/socket, use Length-Prefixed Framing or Content-Length headers (LSP/DAP-style). Over HTTP/WebSocket, the transport already frames.
Why JSON-RPC
Two strengths:
- Boring on purpose. Every language has a JSON parser. Reading a JSON-RPC trace by hand is feasible. Adding a new client doesn't require generating bindings from a schema file.
- Standard error model. Code + message + optional data. Everyone agrees what an error looks like.
Weaknesses:
- Slower than binary. JSON parsing + UTF-8 decoding is overhead. For local IPC at human-driven rates, irrelevant. For service-to-service at thousands of req/sec, gRPC wins.
- No schema. Compared to gRPC or GraphQL, JSON-RPC has no built-in way to describe the methods, params, return types. You're relying on documentation.
- Verbose.
{"jsonrpc": "2.0", "id": 1, ...}has overhead bytes per message that protobuf wouldn't.
For local IPC where the human-readability and language-portability matter more than perf, JSON-RPC is the right call. That's mxr's case, lazydap's case, and the X-Protocol family's case (LSP, DAP, MCP).
What "JSON-RPC-shaped" means
DAP technically isn't strictly JSON-RPC compliant — its message structure differs slightly:
// DAP request:
{ "seq": 1, "type": "request", "command": "initialize", "arguments": {...} }
// DAP response:
{ "seq": 2, "type": "response", "request_seq": 1, "command": "initialize", "success": true, "body": {...} }
// DAP event:
{ "seq": 3, "type": "event", "event": "stopped", "body": {...} }
Different field names, but same structural idea: a seq correlates request with response, an event has no request_seq, errors have a success flag.
LSP is closer to literal JSON-RPC compliance. MCP is fully compliant.
When designing new protocols, just use literal JSON-RPC unless you have a strong reason. Tooling (codecs, validators, debug helpers) is broadest for the spec.
What mxr and lazydap do
Neither is strict JSON-RPC. Both use a JSON-RPC-shaped message:
pub struct IpcMessage {
pub version: u32,
pub id: u64,
pub payload: IpcPayload, // Request | Response | Event | Error
}
id correlates request and response. Events have id: 0. Errors carry a code + message in the same envelope.
The choice to not use literal JSON-RPC was pragmatic: the projects' own envelope is slightly more compact and fits Rust's enum dispatch naturally. Compatibility with JSON-RPC tooling wasn't worth the friction.
If a future bridge needs to expose mxr or lazydap as MCP servers, a thin translation layer turns the internal envelope into JSON-RPC and back.
JSON-RPC libraries
Useful starting points:
- Rust:
jsonrpc-core,tower-jsonrpc, or hand-roll (often easier). - TypeScript:
vscode-jsonrpc(used by VS Code's LSP client). - Python:
python-jsonrpc-server. - Go:
golang.org/x/exp/jsonrpc2.
For local-IPC use cases, hand-rolling the codec is often simpler than pulling in a library. The protocol is small.
See also
- Inter-Process Communication (IPC) — the broader menu
- Length-Prefixed Framing — a common transport
- Unix Domain Sockets — common transport
- Debug Adapter Protocol (DAP) — close-but-not-exact JSON-RPC
- LSP and the X-Protocol Family — the protocols built on JSON-RPC
- JSON-RPC 2.0 spec: https://www.jsonrpc.org/specification