Skip to content
Developer Preview

Framing and caps

This page covers how MSP bytes reach the host and how they are cut into messages: the transports, the newline framing, and the frame size cap.

Transports: what you actually build against

Section titled “Transports: what you actually build against”

Stdio is the whole of the v1 wire. Your client spawns muse serve, owns its stdin and stdout, and is the single connection for that process’s lifetime. Two more out-of-process transports — a unix socket and a websocket — are designed but deferred past v1; no v1 implementation may assume them.

There is also an in-process transport: the first-party terminal UI hosts the session server inside its own process and calls the model directly — no frames, no bytes. It is not something an external client can use, and nothing on this page about framing applies to it by construction. What matters to you is the guarantee that pairing creates: both transports must produce identical item streams — same items, kinds, identities, terminal states, and cursors — so the wire you implement against is never a degraded view.

Because only the spawning party can reach the host’s stdin, v1 has no authentication mechanism, and that is a normative rule: a host must not accept a connection from a party that did not spawn it. The clientInfo you send in the handshake is diagnostics and attribution, never authorization.

On stdio, framing is NDJSON — newline-delimited JSON:

  • One complete JSON-RPC message per line: a UTF-8 encoded JSON object followed by a single line-feed byte.
  • A carriage return before the line feed is tolerated on input and never emitted by the server. The corpus pins this with the framing-tolerance-cr-input scenario.
  • A message must not contain unescaped newline bytes; standard JSON string escaping already guarantees this — never pretty-print a frame onto the wire.
  • Empty lines are ignored on input.
  • Every message is a JSON-RPC 2.0 object and carries the literal JSON-RPC version member — MSP deliberately keeps it so off-the-shelf JSON-RPC tooling interoperates.

Order is significant: within one connection the server writes frames in a total order, and per-session view events on that connection are cursor-ordered. Read frames in the order they arrive; do not reorder.

The server enforces a maximum frame size, 10 MiB by default. The cap is directional in both directions:

  • Inbound (your frames): an oversized frame is rejected with the typed inputTooLarge error if the server could recover an id from the frame prefix, or with a parse error whose id is null otherwise. The connection survives — the transport is not torn down for a single oversized frame.
  • Outbound (server frames): the server sizes its own frames under the same configured limit, so a client never receives a frame larger than the one it is expected to accept. You may size your read buffers to the limit.

Every transcript in the conformance corpus records the limit it was generated under as serverConfig.frameLimitBytes in its manifest.json, and replay always starts the host with that recorded configuration — that is how the limit-dependent fixture families reproduce deterministically instead of diverging at the default limit.

  • Do not retry an oversized frame as-is: inputTooLarge is not retryable — the fix is to shrink the input, and the error’s data carries limitBytes telling you the budget.
  • Split, don’t grow: large payloads travel through methods designed to page — for example truncated item output is fetched with byte-ranged reads rather than one giant frame. If a frame you want to send approaches the limit, restructure the call; there is no frame continuation mechanism.
  • Expect budget-shaped responses: when a result cannot fit the frame budget, the server degrades in specified ways — for example serving a smaller history mode on session/resume than the one requested — rather than emitting an oversized frame. Your client reads what was actually served instead of assuming what it asked for.

Each connection performs its own handshake, and framing alone gets you nothing until it completes: until initialize succeeds, every other request is rejected as not-initialized, and the server sends the connection no notifications. The order is fixed: the client sends initialize, the server replies, the client sends the initialized notification, and only then does the server begin delivering notifications and server-initiated requests. Capability and opt-out settings are fixed for the connection lifetime; to change them, reconnect.