Skip to content

rmcp 3.0 emits resultType: "complete" for legacy protocol sessions #1036

Description

@tsarlandie-oai

Summary

rmcp 3.0 unconditionally serializes resultType: "complete" on ordinary
response types, including CallToolResult, paginated list results, and
resource results.

That behavior is correct for MCP 2026-07-28, but it also changes the wire
format when a server is implementing or negotiating an older MCP version.
Consequently, upgrading rmcp can break existing legacy servers or clients even
when they do not enable the new protocol.

Background

The discriminator was introduced intentionally by:

The SDK documentation says:

Servers implementing this protocol version MUST include resultType in
every result. For backward compatibility, clients MUST treat an absent field
as "complete".

However, backward compatibility here appears to cover deserialization by new
clients, not serialization by new servers that are still speaking a legacy
protocol.

Reproduction

use rmcp::model::CallToolResult;

let result = CallToolResult::success(Vec::new());
let value = serde_json::to_value(result)?;

assert_eq!(
    value,
    serde_json::json!({
        "resultType": "complete",
        "content": [],
        "isError": false,
    })
);

This serialization is identical regardless of whether the peer negotiated MCP
2026-07-28 or an older version such as 2025-06-18.

For a legacy session, the historical wire representation was:

{
  "content": [],
  "isError": false
}

The same issue also applies to other result types to which #915 added a
result_type field.

Why this matters

Applications can upgrade rmcp globally while continuing to implement legacy MCP
server behavior. Adding a new field to their existing responses can break:

  • Clients that strictly validate legacy response schemas.
  • Clients with exact response-shape expectations.
  • Applications promising no wire-format changes during an SDK-only upgrade.

We currently work around this by serializing the response to JSON and removing
resultType when its value is "complete" before sending responses from our
legacy MCP server.

Existing upstream signal

The SDK already acknowledges this compatibility concern for empty results:

#[test]
fn empty_result_serializes_without_result_type() {
    assert_eq!(serde_json::to_value(EmptyResult {})?, serde_json::json!({}));
}

The accompanying comment explains that older or strict peers may reject
additional fields. Ordinary result types have the same issue when used in a
legacy protocol session.

Expected behavior

Serialization should respect the negotiated protocol version:

  • MCP 2026-07-28: include resultType: "complete".
  • Older MCP versions: preserve the legacy representation without resultType.
  • Continue accepting absent resultType as "complete" when deserializing
    legacy responses.

A session-aware serializer, transport-level compatibility adapter, or explicit
legacy serialization API would all address this without weakening 2026-07-28
compliance.

Question

Is unconditional serialization intentional even for legacy sessions, or should
rmcp provide a supported protocol-version-aware compatibility path?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions