Skip to content

Why one IR

The MCP specification has changed five times. The schema files for those five revisions are vendored under crates/protocol-droid-schema/spec/<revision>/. A naïve library picks one revision at compile time, and every consumer pins the revision they wrote their server against. A consumer that wants to speak to a server on a newer revision is stuck: their types do not exist for it.

protocol-droid resolves this with a single canonical internal representation — the IR — and a per-revision codec on either side of it. The IR is a superset of every revision; the codecs lift wire messages into the IR and lower them back. Application code speaks the IR; the wire version is something the negotiation step chose.

What the IR is, and what it is not

The IR is not a model of “the latest MCP”. It is a model that holds the union of every revision’s surface. A 2024-11-05 codec populates a subset of the IR; a 2026-07-28 codec populates a different subset, with _meta fields and stateless-lifecycle directives the older one never heard of. The two never have to agree on what a request looks like; they agree on what the internal form looks like, and the codecs do the rest.

A consequence: an application that has to deal with a peer on revision X writes against the IR, and a separate consumer that has to deal with a peer on revision Y writes against the same IR. The codecs for X and Y are different; the application code is the same.

What this is not for

This is not a “polyglot” claim. The library does not pretend that a 2024-11-05 server and a 2026-07-28 client can interoperate at the wire level. They cannot — the wire types genuinely differ. What the library does do is give both peers a single API to speak to the library, and a single API to drive the library with. The wire compatibility is the bridge’s problem, not the application’s.

A concrete look

A tools/call request is a JSON object. On 2024-11-05, the params.arguments field is JsonObject (stringly-typed). On 2025-06-18, the params.arguments is a typed schema. The IR has neither of those shapes — it has the application’s shape, a typed struct the application defined. The 2024-11-05 codec parses the JsonObject and tries to lift it into the typed struct; if the lift fails, the codec returns a typed LowerError::UnknownField and the application sees that, not a “params was the wrong shape” error from serde.

This is what “the application speaks the IR” means in practice. The wire shape is the codec’s problem; the application never sees it.