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.
The four shapes
Section titled “The four shapes”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.
emittedAtMs
Section titled “emittedAtMs”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.
Request ids: who mints what
Section titled “Request ids: who mints what”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):
idis 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
idnever 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.
Connection-scoped ids vs durable identity
Section titled “Connection-scoped ids vs durable identity”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 samecommandIdwith 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.