# RMCP [![Crates.io Version](https://img.shields.io/crates/v/rmcp)](https://crates.io/crates/rmcp) [![docs.rs](https://img.shields.io/docsrs/rmcp)](https://docs.rs/rmcp/latest/rmcp) [![CI](https://github.com/modelcontextprotocol/rust-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/modelcontextprotocol/rust-sdk/actions/workflows/ci.yml) [![License](https://img.shields.io/crates/l/rmcp)](LICENSE) An official Rust Model Context Protocol SDK implementation with tokio async runtime. > **Migrating to 1.x?** See the [migration guide](https://github.com/modelcontextprotocol/rust-sdk/discussions/716) for breaking changes and upgrade instructions. This repository contains the following crates: - [rmcp](crates/rmcp): The core crate providing the RMCP protocol implementation - see [rmcp](crates/rmcp/README.md) - [rmcp-macros](crates/rmcp-macros): A procedural macro crate for generating RMCP tool implementations - see [rmcp-macros](crates/rmcp-macros/README.md) This SDK tracks the MCP **`2026-07-28`** draft (the current development spec) while remaining fully compatible with the stable **`2025-11-25`** release and earlier versions. New `2026-07-28` features — server discovery & negotiation, transport-neutral subscriptions, long-running tasks, response caching, multi-round-trip requests, and standard HTTP routing headers — are documented below alongside the stable feature set. For the full MCP specification, see [modelcontextprotocol.io](https://modelcontextprotocol.io/specification/draft). ## Table of Contents - [Usage](#usage) - [Tools](#tools) - [Resources](#resources) - [Prompts](#prompts) - [Sampling](#sampling) - [Roots](#roots) - [Logging](#logging) - [Completions](#completions) - [Notifications](#notifications) - [Subscriptions](#subscriptions) - [Multi-Round-Trip Requests](#multi-round-trip-requests) - [Tasks](#tasks-long-running-tool-invocations) - [Caching](#caching) - [Standard HTTP Headers](#standard-http-headers) - [Stateless Streamable HTTP](#stateless-streamable-http) - [Examples](#examples) - [OAuth Support](#oauth-support) - [Related Resources](#related-resources) - [Related Projects](#related-projects) - [Development](#development) ## Usage ### Import the crate Add the latest published version with cargo: ```sh cargo add rmcp --features server ``` Or use the dev channel: ```sh cargo add rmcp --features server --git https://github.com/modelcontextprotocol/rust-sdk --branch main ``` ### Third Dependencies Basic dependencies: - [tokio](https://github.com/tokio-rs/tokio) - [serde](https://github.com/serde-rs/serde) Json Schema generation (version 2020-12): - [schemars](https://github.com/GREsau/schemars) ### Build a Client
Start a client ```rust, ignore use rmcp::{ServiceExt, transport::{TokioChildProcess, ConfigureCommandExt}}; use tokio::process::Command; #[tokio::main] async fn main() -> Result<(), Box> { let client = ().serve(TokioChildProcess::new(Command::new("npx").configure(|cmd| { cmd.arg("-y").arg("@modelcontextprotocol/server-everything"); }))?).await?; Ok(()) } ```
### Client lifecycle modes `serve()` uses the legacy MCP lifecycle: the client sends `initialize`, receives the negotiated server information, and then sends `notifications/initialized`. Use [`ClientServiceExt::serve_with_lifecycle`](crates/rmcp/src/service/client.rs) to select another lifecycle explicitly: ```rust, ignore use rmcp::{ClientInfo, ClientLifecycleMode, ClientServiceExt, ProtocolVersion}; // Start directly with server/discover and include client metadata on every request. let client = ClientInfo::default() .serve_with_lifecycle( transport, ClientLifecycleMode::Discover { preferred_versions: vec![ProtocolVersion::V_2026_07_28], }, ) .await?; // Or probe the discover lifecycle and fall back when a legacy server reports // that server/discover is not implemented. let client = ClientInfo::default() .serve_with_lifecycle( transport, ClientLifecycleMode::Auto { preferred_versions: vec![ProtocolVersion::V_2026_07_28], legacy_version: Some(ProtocolVersion::V_2025_11_25), }, ) .await?; ``` `ClientLifecycleMode::Initialize` is equivalent to the existing `serve()` behavior. Discover startup does not send `notifications/initialized`; discovery completes startup, and each subsequent request carries its protocol version, client information, and capabilities in `_meta`. ### Build a Server
Build a transport ```rust, ignore use tokio::io::{stdin, stdout}; let transport = (stdin(), stdout()); ```
Build a service You can easily build a service by using [`ServerHandler`](crates/rmcp/src/handler/server.rs) or [`ClientHandler`](crates/rmcp/src/handler/client.rs). ```rust, ignore let service = common::counter::Counter::new(); ```
Start the server ```rust, ignore // this call will finish the initialization process let server = service.serve(transport).await?; ```
Interact with the server Once the server is initialized, you can send requests or notifications: ```rust, ignore // request let roots = server.list_roots().await?; // or send notification server.notify_cancelled(...).await?; ```
Waiting for service shutdown ```rust, ignore let quit_reason = server.waiting().await?; // or cancel it let quit_reason = server.cancel().await?; ```
--- ## Tools Tools let servers expose callable functions to clients. Each tool has a name, description, and a JSON Schema for its parameters. Clients discover tools via `list_tools` and invoke them via `call_tool`. **MCP Spec:** [Tools](https://modelcontextprotocol.io/specification/draft/server/tools) ### Server-side The `#[tool]`, `#[tool_router]`, and `#[tool_handler]` macros handle all the wiring. For a tools-only server you can use `#[tool_router(server_handler)]` to skip the separate `ServerHandler` impl: ```rust,ignore use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router, ServiceExt, transport::stdio}; #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] struct AddParams { a: i32, b: i32, } #[derive(Clone)] struct Calculator; #[tool_router(server_handler)] impl Calculator { #[tool(description = "Add two numbers")] fn add(&self, Parameters(AddParams { a, b }): Parameters) -> String { (a + b).to_string() } } #[tokio::main] async fn main() -> anyhow::Result<()> { let service = Calculator.serve(stdio()).await?; service.waiting().await?; Ok(()) } ``` The generated tool `inputSchema` and `outputSchema` are derived from the fields of `T`. The type name and documentation on `T` are ignored; only field names, field types, and field documentation are used. > **`2026-07-28` (SEP-2106):** `outputSchema` may now be any JSON Schema type > (not just `object`), and a tool result's `structuredContent` may be any JSON > value (string, array, number, …) rather than only an object. Existing > object-typed tools are unaffected. When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`: ```rust,ignore use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router, tool_handler, ServerHandler, ServiceExt}; #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] struct AddParams { a: i32, b: i32, } #[derive(Clone)] struct Calculator; #[tool_router] impl Calculator { #[tool(description = "Add two numbers")] fn add(&self, Parameters(AddParams { a, b }): Parameters) -> String { (a + b).to_string() } } #[tool_handler(name = "calculator", version = "1.0.0", instructions = "A simple calculator")] impl ServerHandler for Calculator {} ``` See [`crates/rmcp-macros`](crates/rmcp-macros/README.md) for full macro documentation. ### Client-side ```rust,ignore use rmcp::model::CallToolRequestParams; // List all tools let tools = client.list_all_tools().await?; // Call a tool by name let result = client.call_tool(CallToolRequestParams::new("add")).await?; ``` **Example:** [`examples/servers/src/common/calculator.rs`](examples/servers/src/common/calculator.rs) (server), [`examples/servers/src/calculator_stdio.rs`](examples/servers/src/calculator_stdio.rs) (stdio runner) --- ## Resources Resources let servers expose data (files, database records, API responses) that clients can read. Each resource is identified by a URI and returns content as text or binary (base64-encoded) data. Resource templates allow servers to declare URI patterns with dynamic parameters. **MCP Spec:** [Resources](https://modelcontextprotocol.io/specification/draft/server/resources) ### Server-side Implement `list_resources()`, `read_resource()`, and optionally `list_resource_templates()` on the `ServerHandler` trait. Enable the resources capability in `get_info()`. ```rust use rmcp::{ ErrorData as McpError, RoleServer, ServerHandler, ServiceExt, model::*, service::RequestContext, transport::stdio, }; use serde_json::json; #[derive(Clone)] struct MyServer; impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() .enable_resources() .build(), ) } async fn list_resources( &self, _request: Option, _context: RequestContext, ) -> Result { Ok(ListResourcesResult { resources: vec![ Resource::new("file:///config.json", "config"), Resource::new("memo://insights", "insights"), ], next_cursor: None, meta: None, }) } async fn read_resource( &self, request: ReadResourceRequestParams, _context: RequestContext, ) -> Result { match request.uri.as_str() { "file:///config.json" => Ok(ReadResourceResult::new(vec![ ResourceContents::text(r#"{"key": "value"}"#, &request.uri), ])), "memo://insights" => Ok(ReadResourceResult::new(vec![ ResourceContents::text("Analysis results...", &request.uri), ])), _ => Err(McpError::resource_not_found( "resource_not_found", Some(json!({ "uri": request.uri })), )), } } async fn list_resource_templates( &self, _request: Option, _context: RequestContext, ) -> Result { Ok(ListResourceTemplatesResult { resource_templates: vec![], next_cursor: None, meta: None, }) } } ``` ### Client-side ```rust use rmcp::model::{ReadResourceRequestParams}; // List all resources (handles pagination automatically) let resources = client.list_all_resources().await?; // Read a specific resource by URI let result = client.read_resource( ReadResourceRequestParams::new("file:///config.json"), ).await?; // List resource templates let templates = client.list_all_resource_templates().await?; ``` ### Notifications Servers can notify clients when the resource list changes or when a specific resource is updated: ```rust // Notify that the resource list has changed (clients should re-fetch) context.peer.notify_resource_list_changed().await?; // Notify that a specific resource was updated context.peer.notify_resource_updated( ResourceUpdatedNotificationParam::new("file:///config.json"), ).await?; ``` Clients handle these via `ClientHandler`: ```rust impl ClientHandler for MyClient { async fn on_resource_list_changed( &self, _context: NotificationContext, ) { // Re-fetch the resource list } async fn on_resource_updated( &self, params: ResourceUpdatedNotificationParam, _context: NotificationContext, ) { // Re-read the updated resource at params.uri } } ``` **Example:** [`examples/servers/src/common/counter.rs`](examples/servers/src/common/counter.rs) (server), [`examples/clients/src/everything_stdio.rs`](examples/clients/src/everything_stdio.rs) (client) --- ## Prompts Prompts are reusable message templates that servers expose to clients. They accept typed arguments and return conversation messages. The `#[prompt]` macro handles argument validation and routing automatically. **MCP Spec:** [Prompts](https://modelcontextprotocol.io/specification/draft/server/prompts) ### Server-side Use the `#[prompt_router]`, `#[prompt]`, and `#[prompt_handler]` macros to define prompts declaratively. Arguments are defined as structs deriving `JsonSchema`. ```rust use rmcp::{ ErrorData as McpError, RoleServer, ServerHandler, ServiceExt, handler::server::{router::prompt::PromptRouter, wrapper::Parameters}, model::*, prompt, prompt_handler, prompt_router, schemars::JsonSchema, service::RequestContext, transport::stdio, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CodeReviewArgs { #[schemars(description = "Programming language of the code")] pub language: String, #[schemars(description = "Focus areas for the review")] pub focus_areas: Option>, } #[derive(Clone)] pub struct MyServer { prompt_router: PromptRouter, } #[prompt_router] impl MyServer { fn new() -> Self { Self { prompt_router: Self::prompt_router() } } /// Simple prompt without parameters #[prompt(name = "greeting", description = "A simple greeting")] async fn greeting(&self) -> Vec { vec![PromptMessage::new_text( Role::User, "Hello! How can you help me today?", )] } /// Prompt with typed arguments #[prompt(name = "code_review", description = "Review code in a given language")] async fn code_review( &self, Parameters(args): Parameters, ) -> Result { let focus = args.focus_areas .unwrap_or_else(|| vec!["correctness".into()]); Ok(GetPromptResult::new(vec![ PromptMessage::new_text( Role::User, format!("Review my {} code. Focus on: {}", args.language, focus.join(", ")), ), ]) .with_description(format!("Code review for {}", args.language))) } } #[prompt_handler] impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new(ServerCapabilities::builder().enable_prompts().build()) } } ``` Prompt functions support several return types: - `Vec` -- simple message list - `GetPromptResult` -- messages with an optional description - `Result` -- either of the above, with error handling ### Client-side ```rust use rmcp::model::GetPromptRequestParams; // List all prompts let prompts = client.list_all_prompts().await?; // Get a prompt with arguments let result = client.get_prompt(GetPromptRequestParams { meta: None, name: "code_review".into(), arguments: Some(rmcp::object!({ "language": "Rust", "focus_areas": ["performance", "safety"] })), }).await?; ``` ### Notifications ```rust // Server: notify that available prompts have changed context.peer.notify_prompt_list_changed().await?; ``` **Example:** [`examples/servers/src/prompt_stdio.rs`](examples/servers/src/prompt_stdio.rs) (server), [`examples/clients/src/everything_stdio.rs`](examples/clients/src/everything_stdio.rs) (client) --- ## Sampling > **Deprecated (SEP-2577):** Sampling is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577). Sampling flips the usual direction: the server asks the client to run an LLM completion. The server sends a `create_message` request, the client processes it through its LLM, and returns the result. **MCP Spec:** [Sampling](https://modelcontextprotocol.io/specification/draft/client/sampling) ### Server-side (requesting sampling) Access the client's sampling capability through `context.peer.create_message()`: ```rust use rmcp::model::*; // Inside a ServerHandler method (e.g., call_tool): let response = context.peer.create_message( CreateMessageRequestParams::new( vec![SamplingMessage::user_text("Explain this error: connection refused")], 150, ) .with_model_preferences( ModelPreferences::new() .with_hints(vec![ModelHint::new("claude")]) .with_cost_priority(0.3) .with_speed_priority(0.8) .with_intelligence_priority(0.7), ) .with_system_prompt("You are a helpful assistant.") .with_include_context(ContextInclusion::None) .with_temperature(0.7), ).await?; // Extract the response text let text = response.message.content .first() .and_then(|c| c.as_text()) .map(|t| &t.text); ``` ### Client-side (handling sampling) On the client side, implement `ClientHandler::create_message()`. This is where you'd call your actual LLM: ```rust use rmcp::{ClientHandler, model::*, service::{RequestContext, RoleClient}}; #[derive(Clone, Default)] struct MyClient; impl ClientHandler for MyClient { async fn create_message( &self, params: CreateMessageRequestParams, _context: RequestContext, ) -> Result { // Forward to your LLM, or return a mock response: let response_text = call_your_llm(¶ms.messages).await; Ok(CreateMessageResult::new( SamplingMessage::assistant_text(response_text), "my-model".into(), ) .with_stop_reason(CreateMessageResult::STOP_REASON_END_TURN)) } } ``` **Example:** [`examples/servers/src/sampling_stdio.rs`](examples/servers/src/sampling_stdio.rs) (server), [`examples/clients/src/sampling_stdio.rs`](examples/clients/src/sampling_stdio.rs) (client) --- ## Roots > **Deprecated (SEP-2577):** Roots is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577). Roots tell servers which directories or projects the client is working in. A root is a URI (typically `file://`) pointing to a workspace or repository. Servers can query roots to know where to look for files and how to scope their work. **MCP Spec:** [Roots](https://modelcontextprotocol.io/specification/draft/client/roots) ### Server-side Ask the client for its root list, and handle change notifications: ```rust use rmcp::{ServerHandler, model::*, service::{NotificationContext, RoleServer}}; impl ServerHandler for MyServer { // Query the client for its roots async fn call_tool( &self, request: CallToolRequestParams, context: RequestContext, ) -> Result { let roots = context.peer.list_roots().await?; // Use roots.roots to understand workspace boundaries // ... } // Called when the client's root list changes async fn on_roots_list_changed( &self, _context: NotificationContext, ) { // Re-fetch roots to stay current } } ``` ### Client-side Clients declare roots capability and implement `list_roots()`: ```rust use rmcp::{ClientHandler, model::*}; impl ClientHandler for MyClient { async fn list_roots( &self, _context: RequestContext, ) -> Result { Ok(ListRootsResult::new(vec![ Root::new("file:///home/user/project").with_name("My Project"), ])) } } ``` Clients notify the server when roots change: ```rust // After adding or removing a workspace root: client.notify_roots_list_changed().await?; ``` --- ## Logging > **Deprecated (SEP-2577):** Logging is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577). Servers can send structured log messages to clients. The client sets a minimum severity level, and the server sends messages through the peer notification interface. **MCP Spec:** [Logging](https://modelcontextprotocol.io/specification/draft/server/utilities/logging) ### Server-side Enable the logging capability, handle level changes from the client, and send log messages via the peer: ```rust use rmcp::{ServerHandler, model::*, service::RequestContext}; impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() .enable_logging() .build(), ) } // Client sets the minimum log level async fn set_level( &self, request: SetLevelRequestParams, _context: RequestContext, ) -> Result<(), ErrorData> { // Store request.level and filter future log messages accordingly Ok(()) } } // Send a log message from any handler with access to the peer: context.peer.notify_logging_message( LoggingMessageNotificationParam::new( LoggingLevel::Info, serde_json::json!({ "message": "Processing completed", "items_processed": 42 }), ) .with_logger("my-server"), ).await?; ``` Available log levels (from least to most severe): `Debug`, `Info`, `Notice`, `Warning`, `Error`, `Critical`, `Alert`, `Emergency`. ### Client-side Clients handle incoming log messages via `ClientHandler`: ```rust impl ClientHandler for MyClient { async fn on_logging_message( &self, params: LoggingMessageNotificationParam, _context: NotificationContext, ) { println!("[{}] {}: {}", params.level, params.logger.unwrap_or_default(), params.data); } } ``` Clients can also set the server's log level: ```rust client.set_level(SetLevelRequestParams::new(LoggingLevel::Warning)).await?; ``` --- ## Completions Completions give auto-completion suggestions for prompt or resource template arguments. As a user fills in arguments, the client can ask the server for suggestions based on what's already been entered. **MCP Spec:** [Completions](https://modelcontextprotocol.io/specification/draft/server/utilities/completion) ### Server-side Enable the completions capability and implement the `complete()` handler. Use `request.context` to inspect previously filled arguments: ```rust use rmcp::{ErrorData as McpError, ServerHandler, model::*, service::RequestContext, RoleServer}; impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() .enable_completions() .enable_prompts() .build(), ) } async fn complete( &self, request: CompleteRequestParams, _context: RequestContext, ) -> Result { let values = match &request.r#ref { Reference::Prompt(prompt_ref) if prompt_ref.name == "sql_query" => { match request.argument.name.as_str() { "operation" => vec!["SELECT", "INSERT", "UPDATE", "DELETE"], "table" => vec!["users", "orders", "products"], "columns" => { // Adapt suggestions based on previously filled arguments if let Some(ctx) = &request.context { if let Some(op) = ctx.get_argument("operation") { match op.to_uppercase().as_str() { "SELECT" | "UPDATE" => { vec!["id", "name", "email", "created_at"] } _ => vec![], } } else { vec![] } } else { vec![] } } _ => vec![], } } _ => vec![], }; // Filter by the user's partial input let filtered: Vec = values.into_iter() .map(String::from) .filter(|v| v.to_lowercase().contains(&request.argument.value.to_lowercase())) .collect(); let completion = CompletionInfo::with_pagination(filtered, None, false) .map_err(|e| McpError::internal_error(e, None))?; Ok(CompleteResult::new(completion)) } } ``` ### Client-side ```rust use rmcp::model::*; let result = client.complete(CompleteRequestParams::new( Reference::for_prompt("sql_query"), ArgumentInfo::new("operation", "SEL"), )).await?; // result.completion.values contains suggestions like ["SELECT"] ``` **Example:** [`examples/servers/src/completion_stdio.rs`](examples/servers/src/completion_stdio.rs) --- ## Notifications Notifications are fire-and-forget messages -- no response is expected. They cover progress updates, cancellation, and lifecycle events. Both sides can send and receive them. **MCP Spec:** [Notifications](https://modelcontextprotocol.io/specification/draft/basic#notifications) ### Progress notifications Servers can report progress during long-running operations: ```rust use rmcp::model::*; // Inside a tool handler: for i in 0..total_items { process_item(i).await; context.peer.notify_progress( ProgressNotificationParam::new( ProgressToken(NumberOrString::Number(i as i64)), i as f64, ) .with_total(total_items as f64) .with_message(format!("Processing item {}/{}", i + 1, total_items)), ).await?; } ``` ### Cancellation Either side can cancel an in-progress request: ```rust // Send a cancellation context.peer.notify_cancelled(CancelledNotificationParam::new( Some(the_request_id), Some("User requested cancellation".into()), )).await?; ``` Handle cancellation in `ServerHandler` or `ClientHandler`: ```rust impl ServerHandler for MyServer { async fn on_cancelled( &self, params: CancelledNotificationParam, _context: NotificationContext, ) { // Abort work for params.request_id } } ``` ### Initialized notification Legacy clients send `initialized` after the `initialize` handshake completes. Clients using `ClientLifecycleMode::Discover` do not send this notification: ```rust // Sent automatically by rmcp during the legacy serve() handshake. // Servers handle it via: impl ServerHandler for MyServer { async fn on_initialized( &self, _context: NotificationContext, ) { // Server is ready to receive requests } } ``` ### List-changed notifications When available tools, prompts, or resources change, tell the client: ```rust context.peer.notify_tool_list_changed().await?; context.peer.notify_prompt_list_changed().await?; context.peer.notify_resource_list_changed().await?; ``` **Example:** [`examples/servers/src/common/progress_demo.rs`](examples/servers/src/common/progress_demo.rs) --- ## Subscriptions Protocol `2026-07-28` replaces `resources/subscribe`, `resources/unsubscribe`, and the standalone HTTP GET stream with the transport-neutral, long-lived `subscriptions/listen` request. Each requested notification category is opt-in. **MCP Spec:** [Subscriptions](https://modelcontextprotocol.io/specification/draft/basic/patterns/subscriptions) ### Server-side Declare the notification capabilities you serve, return the accepted subset, and use the filter-enforcing subscription sink: ```rust use rmcp::{ ErrorData, ServerHandler, model::*, service::SubscriptionContext, }; impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() .enable_tools() .enable_tool_list_changed() .build(), ) } fn accepted_subscription_filter( &self, requested: &SubscriptionFilter, ) -> Option { Some(requested.clone()) } async fn listen(&self, context: SubscriptionContext) -> Result<(), ErrorData> { if context.accepted().tools_list_changed == Some(true) { context.sink().notify_tool_list_changed().await .map_err(|error| ErrorData::internal_error(error.to_string(), None))?; } context.cancelled().await; Ok(()) } } ``` The SDK intersects the handler's filter with the requested categories and the capabilities advertised by `get_info()`. It sends the acknowledgment before `listen`, tags every sink notification with the listen request ID, and rejects categories or resource URIs outside the accepted filter. ### Client-side ```rust use rmcp::model::*; let mut subscription = client.listen( SubscriptionFilter::builder() .tools_list_changed() .resource_subscription("file:///config.json") .build(), ).await?; println!("accepted: {:?}", subscription.acknowledged()); while let Some(notification) = subscription.next().await? { println!("notification: {notification:?}"); } subscription.cancel().await?; ``` `listen()` buffers up to 64 notifications per subscription. Use `listen_with_capacity()` to choose a different non-zero capacity; if a consumer falls behind, `Subscription::end()` reports `SubscriptionEnd::Lagged`. For older negotiated protocol versions, the deprecated `subscribe()` and `unsubscribe()` APIs retain their legacy wire behavior. Modern Streamable HTTP uses the listen POST response stream directly and does not use sessions, GET, DELETE, or `Last-Event-ID`. After an abrupt transport close, call `listen` again; subscription state is not resumed across HTTP or stdio reconnects. See the [modern subscription server](examples/servers/src/subscriptions_streamhttp.rs) and [client](examples/clients/src/subscriptions_streamhttp.rs) examples. --- ## Multi-Round-Trip Requests Protocol `2026-07-28` adds Multi-Round-Trip Requests (MRTR, SEP-2322): a server can answer a `tools/call`, `prompts/get`, or `resources/read` with an `InputRequiredResult` instead of a final result, asking the client to fulfill one or more embedded server requests (elicitation, sampling, or roots) and then retry. The exchange is stateless — the server carries its progress in an opaque `requestState` that the client echoes back verbatim. **MCP Spec:** [Multiple Round-Trip Requests](https://modelcontextprotocol.io/specification/draft/server/tools#multiple-round-trip-requests) ### Server-side Return an `InputRequiredResult` via the outcome enum for the method (`CallToolResponse`, `GetPromptResponse`, or `ReadResourceResponse`). The SDK only forwards it to peers that negotiated `2026-07-28` or newer — older peers get a protocol error instead. ```rust, ignore async fn call_tool(&self, request: CallToolRequestParams, _ctx: RequestContext) -> Result { match request.request_state { // First round: ask the client for input, seal progress into requestState. None => { let mut input_requests = InputRequests::new(); input_requests.insert("city".into(), InputRequest::Elicitation(elicit_city())); let sealed = self.codec.seal_json(&json!({ "awaiting": "city" }))?; Ok(InputRequiredResult::new(Some(input_requests), Some(sealed)).into()) } // Retry round: verify the echoed state, read the responses, finish. Some(sealed) => { let _state = self.codec.open_json(&sealed) .map_err(|_| ErrorData::invalid_params("tampered request state", None))?; let city = request.input_responses.as_ref() .and_then(|r| r.get("city")); Ok(CallToolResult::success(vec![ContentBlock::text("It is sunny.")]).into()) } } } ``` > **`requestState` is untrusted.** The client echoes it back verbatim, so a > stateless server that stores meaningful data in it MUST verify integrity > first. Enable the `request-state` feature and use `RequestStateCodec` to seal > and open it (HMAC-tagged), or keep state server-side and use `requestState` > only as an opaque handle. ### Client-side The high-level `call_tool`, `get_prompt`, and `read_resource` helpers drive MRTR automatically: they fulfill each embedded request through the local `ClientHandler` and retry, up to `DEFAULT_MRTR_MAX_ROUNDS` (10). ```rust, ignore // Auto mode: the SDK fulfills embedded requests and retries for you. let result = client.call_tool(CallToolRequestParams::new("weather")).await?; // Choose a custom round cap. let result = client .call_tool_with_mrtr_max_rounds(CallToolRequestParams::new("weather"), 3) .await?; // Manual mode: get the intermediate InputRequiredResult and drive rounds yourself. match client.call_tool_once(CallToolRequestParams::new("weather")).await? { CallToolResponse::InputRequired(input_required) => { /* fulfill + retry */ } CallToolResponse::Complete(result) => { /* done */ } _ => {} } ``` **Example:** [`examples/servers/src/mrtr.rs`](examples/servers/src/mrtr.rs) (end-to-end server + client) --- ## Tasks (long-running tool invocations) `rmcp` implements the [MCP Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) (SEP-2663, `io.modelcontextprotocol/tasks`). A client declares the extension in its capabilities; the server then decides per request whether to materialize a `tools/call` as a task, returning a `CreateTaskResult` (`resultType: "task"`). The client polls `tasks/get`, answers in-task input requests via `tasks/update`, and may request cooperative cancellation via `tasks/cancel`. Use `rmcp::task_manager::TaskManager` to manage task lifecycles server-side. ```rust, ignore // Client: declare the tasks extension capability. let caps = ClientCapabilities::builder().enable_tasks().build(); // Server: decide per request whether to materialize a task. async fn call_tool(&self, request: CallToolRequestParams, context: RequestContext) -> Result { let client_supports_tasks = context .client_capabilities() .is_some_and(|caps| caps.supports_tasks()); if client_supports_tasks { let task = self.tasks.spawn(TaskOptions::default(), move |_ctx| { Box::pin(async move { /* long-running work -> Ok(CallToolResult) */ }) }); return Ok(CallToolResponse::Task(CreateTaskResult::new(task))); } // ... fall back to synchronous execution } ``` See [`servers_task_stdio`](examples/servers/src/task_stdio.rs) and the matching [`clients_task_stdio`](examples/clients/src/task_stdio.rs) for a runnable end-to-end example. ## Caching `rmcp` clients transparently cache responses that carry the [SEP-2549](https://modelcontextprotocol.io/specification/draft/server/utilities/caching) caching hints (`ttlMs` / `cacheScope`) for `server/discover`, `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, and `resources/read`. Caching is on by default but only stores a response when the server sends a positive `ttlMs`, so servers that omit the hint behave exactly as before. Entries expire after their TTL, are partitioned by cache scope, and are invalidated automatically by the matching `list_changed` / `resource updated` notifications. No call-site changes are needed — existing calls benefit automatically: ```rust, ignore let tools = peer.list_tools(None).await?; // served from cache while fresh let res = peer.read_resource(params).await?; // cached per-URI ``` Tune or disable it per connection via the `Peer`: ```rust, ignore use std::time::Duration; use rmcp::ClientCacheConfig; // Customize behavior. peer.set_response_cache_config( ClientCacheConfig::default() .with_default_ttl(Duration::from_secs(30)) // TTL for servers that omit ttlMs .with_max_ttl(Duration::from_secs(3600)) // upper bound on any TTL .with_max_entries(1024) .with_private_partition(user_id) // separate private caches per principal .with_serve_stale_on_error(false), // surface errors instead of stale data ).await; // Or turn it off entirely. peer.set_response_cache_config(ClientCacheConfig::disabled()).await; // Manually flush. peer.clear_response_cache().await; ``` > **Note:** with the default `serve_stale_on_error`, a failed re-fetch returns the > last cached response (even if expired) as `Ok(..)` instead of an error. Set > `with_serve_stale_on_error(false)` if callers must observe fetch failures. ## Standard HTTP Headers Protocol `2026-07-28` standardizes a set of Streamable HTTP request headers (SEP-2243) so proxies and gateways can route MCP traffic without parsing the JSON body: `Mcp-Method`, `Mcp-Name`, and `Mcp-Param-*`. `rmcp` emits and validates these automatically once a connection negotiates `2026-07-28` or newer — no call-site changes are required, and older negotiated versions are untouched. **MCP Spec:** [Header standardization](https://modelcontextprotocol.io/specification/draft/basic/transports#header) - `Mcp-Method` — the JSON-RPC method (e.g. `tools/call`). - `Mcp-Name` — the target name, sourced from `params.name` (`tools/call`, `prompts/get`), `params.uri` (`resources/*`), or `params.taskId` (`tasks/*`). - `Mcp-Param-*` — selected `tools/call` arguments, promoted from the tool's input schema. To promote a tool argument into a routing header, annotate the top-level schema property with `x-mcp-header`: ```rust, ignore // A `region` argument surfaces as the `Mcp-Param-Region` request header. let schema = serde_json::json!({ "type": "object", "properties": { "region": { "type": "string", "x-mcp-header": "Region" } } }); ``` Annotations must be non-empty RFC 9110 tokens, case-insensitively unique, and applied only to top-level primitive (`string`/`integer`/`boolean`) properties. Values that cannot travel as a bare header (leading/trailing whitespace, control/non-ASCII characters) are transparently Base64-wrapped as `=?base64??=`. --- ## Stateless Streamable HTTP Per SEP-2567, `rmcp` serves the `2026-07-28` draft statelessly **automatically**: no `Mcp-Session-Id`, no standalone GET/DELETE stream, and no `Last-Event-ID` resumption. The `legacy_session_mode` flag below only controls behavior for *legacy* protocol versions (`< 2026-07-28`). **MCP Spec:** [Transports](https://modelcontextprotocol.io/specification/draft/basic/transports) ### Server-side A default server already serves `2026-07-28` clients statelessly. To also serve *legacy* clients without sessions, disable `legacy_session_mode` (formerly `stateful_mode`; builder `with_stateful_mode`). Optionally set `with_json_response(true)` so simple request/response tools reply with a single `application/json` body instead of an SSE stream (the server still falls back to `text/event-stream` if a handler emits a notification or server request first): ```rust, ignore use rmcp::transport::streamable_http_server::{ StreamableHttpService, StreamableHttpServerConfig, session::local::LocalSessionManager, }; let config = StreamableHttpServerConfig::default() .with_legacy_session_mode(false) // stateless for legacy versions too .with_json_response(true); // plain JSON replies for simple tools let service = StreamableHttpService::new( || Ok(Counter::new()), // a fresh handler per request LocalSessionManager::default().into(), config, ); // `StreamableHttpService` is a Tower service — mount it on any router. let router = axum::Router::new().nest_service("/mcp", service); ``` > Because there is no per-session state, the `service_factory` runs per request. > Keep shared state (DB pools, caches) in a `Clone` handle captured by the > closure; don't rely on in-memory state surviving between requests. ### Client-side The Streamable HTTP client transport allows stateless operation by default (`allow_stateless: true`), so no configuration is needed to talk to a stateless server — it simply omits the session header when the server doesn't issue one: ```rust, ignore use rmcp::transport::StreamableHttpClientTransport; // Defaults are stateless-friendly. let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp"); let client = ClientInfo::default().serve(transport).await?; ``` **Example:** [`examples/servers/src/counter_streamhttp.rs`](examples/servers/src/counter_streamhttp.rs) (server), [`examples/clients/src/streamable_http.rs`](examples/clients/src/streamable_http.rs) (client) --- ## Examples See [examples](examples/README.md). ## OAuth Support See [Oauth_support](docs/OAUTH_SUPPORT.md) for details. ## Related Resources - [MCP Specification](https://modelcontextprotocol.io/specification/draft) - [Schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) ## Related Projects ### Extending `rmcp` - [rmcp-actix-web](https://gitlab.com/lx-industries/rmcp-actix-web) - An `actix_web` backend for `rmcp` - [rmcp-openapi](https://gitlab.com/lx-industries/rmcp-openapi) - Transform OpenAPI definition endpoints into MCP tools ### Built with `rmcp` - [goose](https://github.com/block/goose) - An open-source, extensible AI agent that goes beyond code suggestions - [apollo-mcp-server](https://github.com/apollographql/apollo-mcp-server) - MCP server that connects AI agents to GraphQL APIs via Apollo GraphOS - [rustfs-mcp](https://github.com/rustfs/rustfs/tree/main/crates/mcp) - High-performance MCP server providing S3-compatible object storage operations for AI/LLM integration - [containerd-mcp-server](https://github.com/jokemanfire/mcp-containerd) - A containerd-based MCP server implementation - [rmcp-openapi-server](https://gitlab.com/lx-industries/rmcp-openapi/-/tree/main/crates/rmcp-openapi-server) - High-performance MCP server that exposes OpenAPI definition endpoints as MCP tools - [nvim-mcp](https://github.com/linw1995/nvim-mcp) - A MCP server to interact with Neovim - [terminator](https://github.com/mediar-ai/terminator) - AI-powered desktop automation MCP server with cross-platform support and >95% success rate - [stakpak-agent](https://github.com/stakpak/agent) - Security-hardened terminal agent for DevOps with MCP over mTLS, streaming, secret tokenization, and async task management - [video-transcriber-mcp-rs](https://github.com/nhatvu148/video-transcriber-mcp-rs) - High-performance MCP server for transcribing videos from 1000+ platforms using whisper.cpp - [NexusCore MCP](https://github.com/sjkim1127/Nexuscore_MCP) - Advanced malware analysis & dynamic instrumentation MCP server with Frida integration and stealth unpacking capabilities - [spreadsheet-mcp](https://github.com/PSU3D0/spreadsheet-mcp) - Token-efficient MCP server for spreadsheet analysis with automatic region detection, recalculation, screenshot, and editing support for LLM agents - [hyper-mcp](https://github.com/hyper-mcp-rs/hyper-mcp) - A fast, secure MCP server that extends its capabilities through WebAssembly (WASM) plugins - [rudof-mcp](https://github.com/rudof-project/rudof/tree/master/rudof_mcp) - RDF validation and data processing MCP server with ShEx/SHACL validation, SPARQL queries, and format conversion. Supports stdio and streamable HTTP transports with full MCP capabilities (tools, prompts, resources, logging, completions, tasks) - [MCPMate](https://github.com/loocor/MCPMate) - Desktop app for progressive MCP management: start with guided server import, then grow into multi-client profiles and Unify meta tools to keep tool exposure, token use, and runtime state under control, with more options for efficiency, cost, and reliability - [McpMux](https://github.com/mcpmux/mcp-mux) - Desktop app to configure MCP servers once at McpMux, connect every AI client (Cursor, Claude Desktop, VS Code, Windsurf) through a single encrypted local gateway with Spaces for project organization, FeatureSets to switch toolsets per client, and a built-in server registry - [systemprompt-template](https://github.com/systempromptio/systemprompt-template) - Single-binary Rust runtime providing MCP governance — authentication, authorisation, rate-limiting, audit trails, and cost tracking for AI agents. Self-hosted, air-gap capable, 3,300+ req/s with sub-5ms governance overhead - [jilebi-mcp](https://github.com/datron/jilebi) - an extensible MCP server through plugins in Javascript with a secure permissions model ## Development ### Tips for Contributors See [docs/CONTRIBUTE.MD](docs/CONTRIBUTE.MD) to get some tips for contributing. ### Using Dev Container If you want to use dev container, see [docs/DEVCONTAINER.md](docs/DEVCONTAINER.md) for instructions on using Dev Container for development.