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.
-
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 typesTL;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)outputSchematype: "object"If 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.Quick migration checklist
ServerHandlerimpls: changecall_tool/get_prompt/read_resourcereturn types toCallToolResponse/GetPromptResponse/ReadResourceResponseand wrap results with.into()(macro users: no change)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 theRunningServicehelperscargo 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