Skip to content
Developer Preview

Envelope and ids

Every MSP message is a JSON-RPC 2.0 object carrying the literal JSON-RPC version member, exactly as it leads every frame in the excerpts on these pages. There are exactly four frame shapes, and a receiver must ignore unknown top-level members on any frame — that tolerance is how new members are added without breaking old clients.

Request — either direction. Carries id, method, and optional params; it expects exactly one response or error frame with the same id. params, when the method takes none, is omitted entirely, never sent as null. A request may also carry an optional top-level trace member for W3C trace context, which a non-tracing receiver must ignore.

Notification — either direction. Carries method and optional params but no id, and never receives a response. Server-to-client notifications additionally carry a top-level emittedAtMs member; the only client-to-server notification today, initialized, carries none.

Response (success) — carries id and result. result is always a JSON object, possibly empty, never a bare scalar, so every result can grow new optional fields later.

Response (error) — carries id and error with code, message, and optional structured data. Exactly one of result or error appears on a response; a frame with an id and neither is invalid. The Errors page covers the error vocabulary.

Here are three real frames from the corpus, exactly as recorded in the usershell-without-grant scenario — the client’s initialized notification (no id), a session/userShell request (client-minted string id "a1", plus a client-minted commandId in params), and the server’s error response correlating on that same id:

{"dir":"client","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"initialized\"}"}
{"dir":"client","raw":"{\"jsonrpc\":\"2.0\",\"id\":\"a1\",\"method\":\"session/userShell\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"commandId\":\"018f6a24-9999-7bbb-8ccc-00000000feed\",\"commandText\":\"git status\"}}"}
{"dir":"server","raw":"{\"error\":{\"code\":-32010,\"data\":{\"capability\":\"userShell\",\"kind\":\"capabilityRequired\",\"retryable\":false},\"message\":\"session/userShell requires the userShell capability\"},\"id\":\"a1\",\"jsonrpc\":\"2.0\"}"}

Each line above is a transcript record: dir says who sent it, raw holds the exact frame bytes. The Conformance page explains the format.

Every server-to-client notification carries a top-level emittedAtMs: the Unix timestamp in milliseconds at which the server emitted the notification, recorded once before fan-out so every connection sees the same value for the same broadcast. It sits beside method and params, not inside params. Two things to get right:

  • It is optional on the wire so decoders tolerate older servers, but current servers always populate it.
  • It is a transport emission time, not the underlying event’s authoritative timestamp — session-view and log payloads carry their own.

The id member correlates a request with its response, and the rules differ by direction:

  • Client-initiated requests: the client chooses id, a JSON string or integer. Ids must be unique among that client’s in-flight requests on the connection; reuse after the response has arrived is legal but discouraged. A string and an integer never compare equal — the integer one and the string spelling of one are different ids.
  • Server-initiated requests (approval and user-input prompts): id is always a positive integer from a monotonically increasing per-process counter. Per JSON-RPC 2.0, each direction owns its own id space and responses are matched by direction plus id, so your client can answer the server’s integer ids while issuing string ids of its own with no collision risk.
  • A null id never appears on a request; it appears only on a parse-error or invalid-request error frame when the server could not recover an id from the offending input.

A JSON-RPC id lives exactly as long as its request. Server request ids are connection-scoped and never durable identity: the durable identity of an approval is the approvalId carried in the request params, and that is what a client must persist and correlate by. More broadly, MSP separates three kinds of identifier:

  • id — transient request/response correlation on one connection.
  • commandId — a client-minted UUID that makes a command idempotent: replaying the same commandId with an identical payload yields the same ack rather than a duplicate effect.
  • Domain ids (sessionId, turnId, itemId, approvalId) — durable names for protocol objects, minted by the server unless a method says otherwise, and byte-exact when you echo them back.

The transcript corpus enforces this split mechanically: values your client mints (id, commandId) are matched structurally with unification, while values you learned from the wire must be echoed byte-exactly — see Conformance.