Migrating to 3.0.0-beta.1 #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.io.modelcontextprotocol/tasksextension; the old task hint, core capability,tasks/list, andtasks/resultare removed.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 fallbackstateful_modetolegacy_session_modesubscriptions/listen; deprecate legacy resource subscription APIs for the draft protocolttlMs/cacheScope, with notification invalidation and configurable policyEventStoreand stateless cross-instance SSE replayAuthorizationRequestprivate_key_jwtaudiencermcpandrmcp-macros3.0.0-beta.1Release and conformance status
rmcpandrmcp-macros3.0.0-beta.1 are available now. The release CI run at maine660b80verifies the following with@modelcontextprotocol/conformance@0.2.0-alpha.9:tasks-status-notificationsremains upstream-skippedauth/client-credentials-basic8/8 andauth/client-credentials-jwt8/8;auth/enterprise-managed-authorizationremains an expected informational failureThere are no known failures in the dated 2026-07-28 core suites. The completed conformance epic #977 has the full matrix; stable 3.0 and non-conformance Tier 1 follow-ups remain tracked in the
2026-07-28 specmilestone and ROADMAP.md.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>fieldsPeerSEP-2549 cache; configure withClientCacheConfigStreamableHttpService<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 armstateful_mode/with_stateful_modelegacy_session_mode/with_legacy_session_mode; draft requests are always statelessresources/subscribe,resources/unsubscribe, standalone HTTP GET streamsubscriptions/listenrequest returning aSubscriptionhandleTasksCapability, clienttaskhint,tasks/list/tasks/result,#[task_handler]io.modelcontextprotocol/tasksextension, server-directedCreateTaskResult,tasks/get/tasks/update/tasks/cancel,TaskManageroutputSchematype: "object"Metafor every_metaMetaObject/RequestMetaObject/NotificationMetaObject;Metastays as a deprecated aliasstart_authorization(AuthorizationRequest)entry point selects the best supported mechanismget_stream(..., session_id: Arc<str>, ...)session_id: Option<Arc<str>>supports stateless replayEventStoreonLocalSessionManager/NeverSessionManagerIf 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).
3.1 Client response cache (#1025, non-breaking)
Clients now cache
server/discover,tools/list,prompts/list,resources/list,resources/templates/list, andresources/readresponses perPeer. Existing call sites need no changes. The cache is enabled by default, but a response is only stored when the server sends a positivettlMs; the default TTL for responses that omit the hint is zero.Matching tool, prompt, resource-list, and resource-updated notifications invalidate affected entries automatically. Private entries are isolated by authorization partition, pagination cursors are part of list cache keys, and MRTR
resources/readrounds are not cached.Configure or disable the cache through the
Peer:By default, an expired cached value may be returned as
Ok(..)when re-fetching fails. Setwith_serve_stale_on_error(false)when callers must observe transport or server errors. Gateways or clients that reuse a connection across principals should set and updateprivate_partitionwhenever the authorization context changes.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.10. Stateless HTTP and subscription streams (SEP-2567 / SEP-2575, #999 + #1000)
Streamable HTTP requests negotiating
2026-07-28are always served statelessly, regardless of the legacy session setting. The public configuration name now makes that version boundary explicit:Direct struct construction must similarly rename the
stateful_modefield tolegacy_session_mode. The setting continues to control sessions, GET/DELETE, priming, and resumability for protocol versions older than2026-07-28; it does not opt draft requests back into sessions.The draft protocol replaces
resources/subscribe,resources/unsubscribe, and the standalone HTTP GET stream with a long-lived, transport-neutralsubscriptions/listenrequest:ServerHandler::accepted_subscription_filter, and produce notifications fromServerHandler::listen(SubscriptionContext).SubscriptionSinkenforces the accepted filter and adds the subscription request ID.Peer<RoleClient>::listen(SubscriptionFilter)orlisten_with_capacity, consume notifications from the returnedSubscription, and cancel that request when finished.Last-Event-ID, or automatic resumption after reconnect.subscribe()andunsubscribe()helpers keep their existing wire behavior for older negotiated protocol versions.See the
subscriptions_streamhttpclient and server examples for complete implementations.11. Tasks moved to an extension (SEP-2663, #1020)
The experimental 2025-11-25 tasks API has been replaced by the official
io.modelcontextprotocol/tasksextension. There is no compatibility shim for the old API.Removed APIs and wire methods:
TaskMetadata,TasksCapability,TaskRequestsCapability,TaskAugmentedRequestParamsMeta, and.with_task(..)tasks/listandtasks/result(task status and terminal payloads now come fromtasks/get)#[task_handler]and the toolexecution(task_support)attributeClients declare support through the extension capability and use the one-round tool call API so they can observe a task handle:
The high-level
RunningService::call_toolhelper drives MRTR but does not poll Tasks-extension results; when the server may create a task, usecall_tool_once, thenPeer::get_task,update_task, andcancel_task.Servers advertise the extension with
ServerCapabilities::builder().enable_tasks(), checkRequestContext::client_capabilities().is_some_and(|caps| caps.supports_tasks()), and returnCallToolResponse::Task(CreateTaskResult::new(task))when they choose asynchronous execution.rmcp::task_manager::TaskManagerprovides durable task creation,tasks/get/tasks/update/tasks/cancelhandlers, TTL enforcement, in-task input requests, and cooperative cancellation throughTaskContext::cancelled()/TaskExit.Task status notifications through
subscriptions/listenare not wired yet, so clients must currently polltasks/get. See theservers_task_stdioandclients_task_stdioexamples for the complete lifecycle.12. Distributed Streamable HTTP event store (#1024)
A shared
EventStorenow lets Streamable HTTP responses survive disconnects and server-instance changes. Event IDs identify their stream without relying on a session ID, so stateless clients can reconnect withLast-Event-IDand noMcp-Session-Id. Legacy session replay uses the same store contract.Attach your implementation to either bundled session manager:
EventStoreimplementations must assign globally unique event IDs, persist before returning fromstore_event, and replay only later events from the same stream.Two API changes affect custom integrations:
StreamableHttpClient::get_streamandget_stream_with_max_sse_event_sizenow receivesession_id: Option<Arc<str>>. HandleNoneby reconnecting withoutMcp-Session-Id;last_event_idstill identifies the stream.LocalSessionManagerandNeverSessionManagerno longer implementUnwindSafe/RefUnwindSafebecause they may containArc<dyn EventStore>. Code that moves them acrosscatch_unwindmust adjust that boundary.13. OAuth startup uses
AuthorizationRequest(#1009)The three OAuth startup paths are now one declarative API.
OAuthState::start_authorization_with_metadata_urlandstart_authorization_with_preregistered_clientwere removed, and the positional form ofstart_authorizationchanged.Add
.with_preregistered_client(client_id)and optionally.with_client_secret(secret)for pre-registered credentials, or.with_client_metadata_url(url)for CIMD. The SDK selects pre-registration first, then CIMD when advertised, then Dynamic Client Registration. Omitting.with_scopes(...)keeps automatic scope selection. Direct callers ofAuthorizationSession::newnow pass the sameAuthorizationRequest; failures return theAuthorizationManageralongside the error so the flow can be retried.14. SEP-2260 request association (#1029, non-breaking)
Sampling, elicitation, and roots requests created while handling a client request are now associated with that request. The bundled local Streamable HTTP session manager routes them over the originating POST SSE stream instead of the standalone GET stream. Clients negotiating
2026-07-28also reject an unassociated restricted server request with-32602;pingis exempt.No migration is required for the bundled manager. Custom
SessionManagerimplementations that serialize messages across processes must preserve the association themselves because the in-processOriginatingRequestIdextension is not serialized.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)StreamableHttpServerConfig::stateful_mode/.with_stateful_mode(..)→legacy_session_mode/.with_legacy_session_mode(..)subscribe()/unsubscribe()and standalone GET handling withlisten(SubscriptionFilter); server implementations addaccepted_subscription_filterandlistenenable_tasks(), replace the client task hint andtasks/list/tasks/resultwithcall_tool_onceplustasks/get/tasks/update/tasks/cancel, and replace#[task_handler]with explicitTaskManagerintegrationprivate_partitionwhen one connection can serve multiple authorization contextsAuthorizationRequestbuildersStreamableHttpClientimpls → acceptOption<Arc<str>>for SSE reconnect session IDs and handleNoneEventStoreand attach it with.with_event_store(..)catch_unwindaround bundled session managers → account for the loss ofUnwindSafe/RefUnwindSafeSessionManagerimpls → preserve SEP-2260 originating-request association across the process boundarycargo buildand follow remaining compiler errors — the compiler is your friend hereQuestions or migration snags? Reply here and we'll help.
Started 2026-07-10 and updated through main
e660b80/rmcp-v3.0.0-beta.1on 2026-07-23. This guide will be kept current through the stable 3.0 release.All reactions