Migrating to 3.x (draft) #969
DaleSeo
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Upgrading to RMCP 3.x — Migration Guide
Why 3.0?
The 2026-07-28 revision changes the shape of server results: every server result now carries a
resultTypediscriminator, andtools/call,prompts/get, andresources/readmay return an intermediateinput_requiredresult instead of a final one. Modeling that faithfully requires widening handler return types and theServerResultunion, which is breaking at the Rust API level.Unlike 2.0 (which was wire-compatible), 3.0 includes wire-visible changes — all of them additive or version-gated:
"resultType": "complete"(per the spec's MUST; absent values still deserialize as"complete", so older peers interoperate).InputRequiredResultand the SEP-2243 headers are only emitted to peers that negotiated protocol version2026-07-28or newer.ttlMs/cacheScopeare new optional fields.Changes included so far
resultTypeon server resultsMcp-Method/Mcp-Name/Mcp-Param-*HTTP headersttlMs/cacheScopecache hints on list/read resultsToolResultContent.structured_contentaccepts any JSON valueAnnotations.lastModifiedtyped as a stringoutputSchemaaccepts non-object JSON Schema root typesMetasplit intoMetaObject/RequestMetaObject/NotificationMetaObjectserver/discover) and inline version negotiation; protocol union enums#[non_exhaustive]ClientLifecycleMode::{Initialize, Discover, Auto}) with per-request metadata and legacy fallbackRemaining work before v3
Tracked in the conformance epic #977 and the
2026-07-28 specmilestone. Conformance snapshot (2026-07-16, main2e2c791, suite0.2.0-alpha.9): server 37/40, client 15/32 scenarios passing. The client number dropped from the 2026-07-13 baseline (24/32) because of conformance harness gaps, not library regressions: ten auth scenarios lost their dispatch mapping in #991 (#1001) and the legacy-lifecycle scenarios die on a missingMCP-Protocol-Versionheader (#1002) — this also means the suite cannot currently verify the merged #888 and #996 fixes end-to-end. See #977 for the full matrix.server-statelessgaps: capability declaration in discover,ping404, missing diagnostic fixturesserver-stateless(18/25)tools-call-with-progressauth/authorization-server-migrationhttp-custom-headers,http-standard-headers,http-invalid-tool-headersauth/pre-registrationauth/iss-*,auth/offline-access-*,auth/authorization-server-migration,auth/metadata-issuer-mismatch-32020at 2026-07-28 (blocks verifying #888)tools_call,auth/scope-step-up,http-standard-headersjson-schema-2020-12(4/7)TL;DR
ServerHandler::call_toolResult<CallToolResult, ErrorData>Result<CallToolResponse, ErrorData>ServerHandler::get_promptResult<GetPromptResult, ErrorData>Result<GetPromptResponse, ErrorData>ServerHandler::read_resourceResult<ReadResourceResult, ErrorData>Result<ReadResourceResponse, ErrorData>ServerResultunionInputRequiredResultresult_type: ResultType, always serializedRunningService::call_tool/get_prompt/read_resource*_onceto opt outReadResourceResultttl_ms: Option<u64>,cache_scope: Option<CacheScope>fieldsStreamableHttpService<S, M>S: Service<RoleServer>S: ServerHandlerAnnotations.last_modifiedOption<DateTime<Utc>>Option<String>ToolResultContent.structured_contentOption<JsonObject>Option<Value>ProtocolVersionV_2025_11_25V_2026_07_28(LATESTstays2025-11-25until the spec is final)server/discoverRPCDiscoverRequest/DiscoverResult; defaultServerHandler::discoverand configurable supported versionsinitializehandshake onlyserve()unchanged; opt intoClientServiceExt::serve_with_lifecyclewithClientLifecycleMode::{Initialize, Discover, Auto}(non-breaking)#[non_exhaustive]; downstream matches need a wildcard armoutputSchematype: "object"Metafor every_metaMetaObject/RequestMetaObject/NotificationMetaObject;Metastays as a deprecated aliasIf you use the
#[tool_router]+#[tool_handler]/#[prompt_router]+#[prompt_handler]macros, the return-type changes are handled for you — your individual#[tool]/#[prompt]methods keep compiling unchanged. ManualServerHandlerimplementations need the signature updates below.1. Multi round-trip requests — MRTR (SEP-2322)
A server may now respond to
tools/call,prompts/get, orresources/readwith anInputRequiredResult(carryinginputRequestsand/or an opaquerequestState) instead of a final result. The client fulfills the requests and retries. This replaces the previousURLElicitationRequiredError(-32042) approach.1.1 Server: handler return types widened
ServerHandler::call_tool,get_prompt, andread_resourcenow return MRTR-aware response enums.Fromimpls are provided, so wrap your existing result with.into():The response enums are:
tools/callCallToolResponseComplete(CallToolResult)|InputRequired(InputRequiredResult)prompts/getGetPromptResponseComplete(GetPromptResult)|InputRequired(InputRequiredResult)resources/readReadResourceResponseComplete(ReadResourceResult)|InputRequired(InputRequiredResult)To request input, return an
InputRequiredResult(tool methods under#[tool_router]can return it directly —IntoCallToolResultis implemented for it):The SDK only lets an
InputRequiredResultreach a peer that negotiated protocol version2026-07-28or newer; older peers get a protocol error instead. Task-based tool calls (#[task_handler]) rejectinput_requiredwith an internal error.1.2 Client: high-level calls drive MRTR automatically
RunningService::call_tool,get_prompt, andread_resourcenow automatically fulfill eachinput_requiredround through your registeredClientHandler(sampling, elicitation, roots) and retry, up toDEFAULT_MRTR_MAX_ROUNDS(10). Your call sites don't change.New APIs:
call_tool_once/get_prompt_once/read_resource_once(onPeer<RoleClient>andRunningService) — send one request and get the response enum back, so you can drive the rounds yourself.call_tool_with_mrtr_max_rounds(andget_prompt/read_resourcevariants) — explicit round cap.ServiceError::InputRequiredRoundsExceeded { max_rounds }— returned when the peer keeps respondinginput_requiredbeyond the cap.Note: the low-level
Peer::call_tool/get_prompt/read_resourcestill return the final result types and fail withServiceError::UnexpectedResponseif the server returnsinput_required. Prefer theRunningServicehelpers or the*_oncevariants.1.3
ServerResultgained a variantServerResultnow includesInputRequiredResult. If you match exhaustively onServerResult, add an arm.1.4
requestStateis untrustedThe client echoes
requestStateback verbatim, so a stateless server MUST verify its integrity before trusting the echoed value. The new opt-inrequest-statefeature providesRequestStateCodec(HMAC-SHA256) with associated-data and TTL bindings:See the
servers_mrtrexample for a complete walkthrough.2.
resultTypeon server results (SEP-2322)All server result types that carry
resultTypein the schema gained aresult_type: ResultTypefield:CallToolResult,GetPromptResult,ReadResourceResult,CompleteResult,ListToolsResult,ListPromptsResult,ListResourcesResult, andListResourceTemplatesResult.ResultTypeis an open string type (ResultType::COMPLETE,ResultType::INPUT_REQUIRED, unknown values preserved) withis_complete()/is_input_required()accessors."resultType": "complete"), per the spec's MUST. Absent values deserialize as"complete", so older peers are unaffected.::new(..),::success(..),with_all_items(..),Default) initialize it for you — since these types are#[non_exhaustive]you were already using constructors, so most code needs no change. Only code that reads all fields or constructs results inside forks of the crate is affected.3. Cache hints (SEP-2549)
ListToolsResult,ListPromptsResult,ListResourcesResult,ListResourceTemplatesResult, andReadResourceResultgained two optional fields:ttl_ms: Option<u64>— how long the result may be treated as fresh, in milliseconds. Negative wire values are clamped to0on deserialization, per the SEP.cache_scope: Option<CacheScope>— who may cache it.CacheScopeis a new#[non_exhaustive]enum:Public(default) orPrivate.Set them with the new builders:
Both fields are optional on the wire for compatibility with older spec versions (the 2026-07-28 spec makes them required in these results).
4. Standard HTTP headers (SEP-2243)
Streamable HTTP requests now carry routing metadata in headers so proxies, gateways, and load balancers can route without parsing the JSON-RPC body. Everything is gated on a negotiated protocol version of
2026-07-28or newer (ProtocolVersion::STANDARD_HEADERS), so older peers are unaffected.Mcp-Method(e.g.tools/call),Mcp-Name(fromparams.nameorparams.uri), andMcp-Param-*headers.Mcp-Param-*promotion by annotating top-level, primitive-typed input-schema properties withx-mcp-header:{ "type": "object", "properties": { "region": { "type": "string", "x-mcp-header": "Region" } } }Header-unsafe values are Base64-wrapped automatically.
400and JSON-RPC error-32020.Breaking:
StreamableHttpService<S, M>now requiresS: ServerHandlerinstead ofS: Service<RoleServer>, so the server can read tool schemas forMcp-Param-*validation. If you were plugging a hand-rolledService<RoleServer>intoStreamableHttpService, implementServerHandlerinstead.New constants:
ProtocolVersion::V_2026_07_28andProtocolVersion::STANDARD_HEADERS.ProtocolVersion::LATESTremainsV_2025_11_25until the 2026-07-28 spec is finalized.5.
Annotations.lastModifiedis now a stringThe spec types
lastModifiedas a plain string, and schema-valid values (date-only, offset-less date-times) are not strict RFC 3339 — previously they could fail deserialization of a whole message. The field now preserves the raw string:Annotations::for_resource(priority, ts),.with_timestamp(ts), and.with_timestamp_now()still takeDateTime<Utc>and serialize RFC 3339 strings.annotations.last_modified.as_deref().and_then(|s| DateTime::parse_from_rfc3339(s).ok()), and decide how to handle non-RFC-3339 values.This matches how
Task.createdAt/Task.lastUpdatedAtwere already represented, and keeps proxying/forwarding lossless.6.
ToolResultContent.structured_contentaccepts any JSON value (SEP-2106)This matches
CallToolResult.structured_content(alreadyOption<Value>) and the SEP-2106 relaxation ofoutputSchema. If you consumed the field as an object, usevalue.as_object(). (Note thatToolUseContent/ToolResultContentare deprecated by SEP-2577 and will be removed in a future release.)7.
outputSchemaaccepts non-object root types (non-breaking)schema_for_output(used byJson<T>tool returns andTool::with_output_schema) now accepts any JSON Schema 2020-12 root type — primitives, arrays, and compositions likeOption<T>. Input schemas still requiretype: "object". Types previously rejected are now accepted; nothing to migrate, but tools can now declare e.g.Json<Vec<String>>outputs.8. Metadata models:
Metasplit intoMetaObject/RequestMetaObject/NotificationMetaObject(#993)The 2026-07-28 revision names the
_metashapes — no released spec version has these types (spec #2038, spec #2889). rmcp's singleMetatype predates them; 3.0 mirrors the spec hierarchy (#993):MetaObject— general_metaon results, content blocks, and descriptors (tools, prompts, resources, roots). Carries the SEP-414 trace-context helpers.RequestMetaObject— request_meta:progressTokenplus typed accessors for the SEP-2575io.modelcontextprotocol/*keys. The keys the draft marks as required stay optional at runtime and in the generated schema; validate against a negotiated version withmissing_required_keys(&ProtocolVersion).NotificationMetaObject— notification_meta:io.modelcontextprotocol/subscriptionIdaccessors (previously unimplemented).All three are transparent wrappers over the JSON map, so arbitrary extension keys still round-trip and the map APIs remain available through
Deref.Metastays as a deprecated re-export ofMetaObject, so existingMeta(...)construction keeps compiling with a warning.RequestParamsMetaOption<&Meta>/&mut Option<Meta>Option<&RequestMetaObject>/&mut Option<RequestMetaObject>RequestContext.metaMetaRequestMetaObjectNotificationContext.metaMetaNotificationMetaObjectPeerRequestOptions.metaOption<Meta>Option<RequestMetaObject>GetMetafn get_meta(&self) -> &Metatype Metadata(RequestMetaObjectfor requests,NotificationMetaObjectfor notifications)crate::model::Metacrate::model::RequestMetaObjectmetafieldsOption<Meta>Option<MetaObject>(the alias keeps this code compiling)Wire compatibility is unchanged: unknown
_metakeys round-trip untouched, requests with absent or partial_metastill deserialize, and legacyMetaObjectextensions inserted under the deprecated name are still merged into outgoing_meta. Generated JSON schemas now expose the three named definitions. This change also fixed a 2.x bug where no-param requests and notifications (ping,notifications/initialized) droppedparams._metaduring (de)serialization.9. Server discovery and protocol negotiation (SEP-2575, #973 + #995)
#973 adds the server-side discovery and inline version-negotiation foundation for stateless MCP; #995 adds the client-side lifecycle modes on top of it.
New protocol models and APIs:
DiscoverRequest/DiscoverResultmodelserver/discover, includingsupported_versions, capabilities, server identity, instructions, and cache fields.ServerHandler::discoverhas a default implementation derived fromget_info(); overridesupported_protocol_versions()ordiscover()to customize the advertised versions or result.Peer<RoleClient>::discover(RequestMetaObject)sends a typed discovery request on an already-running peer.select_protocol_version(client_preferences, server_supported)selects the first mutually supported client-preferred version and returnsNonewhen there is no overlap.UnsupportedProtocolVersionError(-32022) carries{ requested, supported }; missing client capability errors use-32021._metaand map modern protocol errors to HTTP400/404, while legacy requests retain their existing HTTP200JSON-RPC-error behavior.9.1 Client lifecycle modes (#995, non-breaking)
serve()keeps the legacy lifecycle (initialize→notifications/initialized) unchanged. To start withserver/discoverinstead, select a lifecycle explicitly viaClientServiceExt::serve_with_lifecycle:ClientLifecycleMode::Initializeis equivalent toserve(). Discover startup does not sendnotifications/initialized— discovery completes startup, and each subsequent request carries its protocol version, client information, and capabilities in_meta. Bounded version retry and per-request metadata injection are handled for you.Peer<RoleClient>::discover(RequestMetaObject)remains available for typed discovery on an already-running peer.Breaking enum changes
DiscoverRequestadds aClientRequestvariant andDiscoverResultadds aServerResultvariant. The six protocol union enums are now#[non_exhaustive]so future protocol additions do not repeatedly break exhaustive downstream matches:ClientRequestClientNotificationClientResultServerRequestServerNotificationServerResultDownstream code must add a wildcard arm when matching these enums outside rmcp:
The existing legacy
initializeflow remains available andProtocolVersion::LATESTremainsV_2025_11_25until the draft revision is finalized.Quick migration checklist
ServerHandlerimpls: changecall_tool/get_prompt/read_resourcereturn types toCallToolResponse/GetPromptResponse/ReadResourceResponseand wrap results with.into()(macro users: no change)DiscoverRequest/DiscoverResultwhere relevantServerHandler::supported_protocol_versions()ordiscover()matchonServerResult→ add anInputRequiredResultarmStreamableHttpServiceservice types → implementServerHandler(not justService<RoleServer>)Annotations.last_modified→ it's aStringnow; parse explicitly if you need aDateTimeToolResultContent.structured_contentas an object → use.as_object()"resultType": "complete"Peerdirectly → handleinput_required(use*_oncehelpers) or switch to theRunningServicehelpersMeta→MetaObjectfor results/content/descriptors,RequestMetaObjectfor request params/contexts,NotificationMetaObjectfor notification params/contexts (the deprecated alias keeps general uses compiling)cargo buildand follow remaining compiler errors — the compiler is your friend hereQuestions or migration snags? Reply here and we'll help.
Draft started 2026-07-10, based on the PRs listed above. This guide will be kept up to date until v3 is released — maintainers and contributors, please edit as new breaking changes merge.
Beta Was this translation helpful? Give feedback.
All reactions