Summary
ElicitationSchema.properties is typed as BTreeMap<String, PrimitiveSchemaDefinition>, so rmcp
re-orders elicitation form fields alphabetically on both deserialize and serialize. The order the
server declared on the wire is unrecoverable through the typed API.
Elicitation schemas are the one place in MCP where a JSON object's key order carries presentation
meaning: it is the order a client renders the form fields in. A server that declares
{"type":"object","properties":{"firstName":{...},"lastName":{...},"email":{...}}}
is seen by an rmcp client as email, firstName, lastName.
Affected code (3.1.0)
crates/rmcp/src/model/elicitation_schema.rs:1126 — ElicitationSchema.properties: BTreeMap<…>
crates/rmcp/src/model/elicitation_schema.rs:1262 — same field on ElicitationSchemaBuilder
Because the field is shared, this affects all three paths that carry an elicitation schema:
- legacy server-initiated
elicitation/create requests (≤ 2025-06-18),
- SEP-2322 task steering —
TaskPayload::InputRequired { input_requests } (model/task.rs:126),
2026-07-28 non-task MRTR — InputRequiredResult.inputRequests (model/mrtr.rs:235).
So it applies equally to servers and clients, and it survived the transition to MRTR.
Why consumers cannot work around it
serde_json's preserve_order feature does not help. We enable it, and Cargo's feature
unification means rmcp's own serde_json::Value::Object is IndexMap-backed in our builds and
does preserve order — but properties never passes through Value. It deserializes straight into
BTreeMap, so the ordering is discarded before any consumer can observe it.
The only workaround is to tap raw JSON ahead of rmcp's decode, which means decorating every
transport (stdio AsyncRead, Streamable HTTP, SSE, and any in-process bridge) and carrying a
request-id correlation store alongside. We implemented exactly that and are trying to delete it —
it is a lot of accidental machinery for what is a one-type change upstream.
Suggested fix
Use an order-preserving map for properties — indexmap::IndexMap<String, PrimitiveSchemaDefinition>.
indexmap is already in the tree transitively via serde_json, and its serde impl is a drop-in for
BTreeMap (IndexMap deserializes in encounter order and serializes in insertion order).
If a hard dependency is unwelcome, gating it behind a feature that swaps the map type would also
work, though an unconditional change seems preferable given the field's semantics — nothing relies
on properties being sorted, and BTreeMap's ordering guarantee is not meaningful for a JSON
Schema property bag.
InputRequests = BTreeMap<String, InputRequest> (model/mrtr.rs:91) and InputResponses
(model/mrtr.rs:100) have the same shape. Those keys are correlation identifiers looked up by name,
so the impact is much weaker — but if servers ever intend inputRequests to be presented in
declaration order, the same argument applies. Happy to scope a PR to properties alone.
Relationship to #1097
Same defect class as #1097 (which we
hit independently, and which is why we are on 3.1.0): the typed model silently discards information
that was present on the wire, and round-tripping through rmcp is not faithful to the input. This one
loses ordering rather than fields, so it degrades rendering rather than breaking protocol flow —
lower severity, but the same underlying shape.
I'm glad to open a PR if the IndexMap direction is agreeable.
Summary
ElicitationSchema.propertiesis typed asBTreeMap<String, PrimitiveSchemaDefinition>, so rmcpre-orders elicitation form fields alphabetically on both deserialize and serialize. The order the
server declared on the wire is unrecoverable through the typed API.
Elicitation schemas are the one place in MCP where a JSON object's key order carries presentation
meaning: it is the order a client renders the form fields in. A server that declares
{"type":"object","properties":{"firstName":{...},"lastName":{...},"email":{...}}}is seen by an rmcp client as
email,firstName,lastName.Affected code (3.1.0)
crates/rmcp/src/model/elicitation_schema.rs:1126—ElicitationSchema.properties: BTreeMap<…>crates/rmcp/src/model/elicitation_schema.rs:1262— same field onElicitationSchemaBuilderBecause the field is shared, this affects all three paths that carry an elicitation schema:
elicitation/createrequests (≤2025-06-18),TaskPayload::InputRequired { input_requests }(model/task.rs:126),2026-07-28non-task MRTR —InputRequiredResult.inputRequests(model/mrtr.rs:235).So it applies equally to servers and clients, and it survived the transition to MRTR.
Why consumers cannot work around it
serde_json'spreserve_orderfeature does not help. We enable it, and Cargo's featureunification means rmcp's own
serde_json::Value::ObjectisIndexMap-backed in our builds anddoes preserve order — but
propertiesnever passes throughValue. It deserializes straight intoBTreeMap, so the ordering is discarded before any consumer can observe it.The only workaround is to tap raw JSON ahead of rmcp's decode, which means decorating every
transport (stdio
AsyncRead, Streamable HTTP, SSE, and any in-process bridge) and carrying arequest-id correlation store alongside. We implemented exactly that and are trying to delete it —
it is a lot of accidental machinery for what is a one-type change upstream.
Suggested fix
Use an order-preserving map for
properties—indexmap::IndexMap<String, PrimitiveSchemaDefinition>.indexmapis already in the tree transitively viaserde_json, and its serde impl is a drop-in forBTreeMap(IndexMapdeserializes in encounter order and serializes in insertion order).If a hard dependency is unwelcome, gating it behind a feature that swaps the map type would also
work, though an unconditional change seems preferable given the field's semantics — nothing relies
on
propertiesbeing sorted, andBTreeMap's ordering guarantee is not meaningful for a JSONSchema property bag.
InputRequests = BTreeMap<String, InputRequest>(model/mrtr.rs:91) andInputResponses(
model/mrtr.rs:100) have the same shape. Those keys are correlation identifiers looked up by name,so the impact is much weaker — but if servers ever intend
inputRequeststo be presented indeclaration order, the same argument applies. Happy to scope a PR to
propertiesalone.Relationship to #1097
Same defect class as #1097 (which we
hit independently, and which is why we are on 3.1.0): the typed model silently discards information
that was present on the wire, and round-tripping through rmcp is not faithful to the input. This one
loses ordering rather than fields, so it degrades rendering rather than breaking protocol flow —
lower severity, but the same underlying shape.
I'm glad to open a PR if the
IndexMapdirection is agreeable.