MP-301g · Module 1
JSON-RPC Framing
4 min read
MCP rides on JSON-RPC 2.0, but the framing layer — how individual messages are delimited on the wire — differs by transport. Stdio uses newline-delimited JSON (NDJSON): each message is a single line terminated by \n, and the parser splits on newlines before attempting JSON.parse(). Streamable HTTP wraps each message in an HTTP request/response body or an SSE data field. The critical invariant in both cases is that one JSON-RPC object equals one complete message. Partial writes, split across pipe buffer boundaries or TCP segments, must be reassembled before parsing. If your parser feeds incomplete JSON to JSON.parse(), you get a syntax error that looks like a protocol violation but is actually a framing bug.
The JSON-RPC 2.0 envelope has four fields that matter: jsonrpc (always "2.0"), method (the operation name), id (present for requests, absent for notifications), and params (the payload). Responses replace method with result or error and echo back the id. The id field is the correlation mechanism — when you send a request with id: 42, the response with id: 42 is the answer. Notifications (no id) are fire-and-forget: the sender does not expect a response, and the receiver must not send one. Confusing requests with notifications is a common implementation bug that causes silent message loss.
Batching is legal in JSON-RPC 2.0 — you can send an array of request objects and receive an array of responses. MCP does not prohibit batching, but most implementations do not use it because the session model already provides request pipelining. If you implement batch support, remember that the response array may be returned in any order and may omit entries for notifications. Batch parsing also complicates error handling: a single malformed entry in the batch should not invalidate the entire array. Parse each element independently.
// Robust NDJSON parser for stdio MCP transport
// Handles partial reads, split messages, and back-to-back messages
class NdjsonParser {
private buffer = "";
feed(chunk: string): object[] {
this.buffer += chunk;
const messages: object[] = [];
let newlineIdx: number;
while ((newlineIdx = this.buffer.indexOf("\n")) !== -1) {
const line = this.buffer.slice(0, newlineIdx).trim();
this.buffer = this.buffer.slice(newlineIdx + 1);
if (line.length === 0) continue; // skip blank lines
try {
const parsed = JSON.parse(line);
// Validate JSON-RPC envelope
if (parsed.jsonrpc !== "2.0") {
throw new Error(`Invalid jsonrpc version: ${parsed.jsonrpc}`);
}
messages.push(parsed);
} catch (err) {
// Log but do not crash — one bad message should not
// kill the connection
console.error("[MCP] Malformed JSON-RPC line:", err);
}
}
return messages;
}
}