
Write HTML. Render video. Built for agents.
This repository provides the Rust SDK for the Model Context Protocol, including client and server lifecycle support, transports, tools, resources, prompts, and notifications. The macros and examples show how to wire Rust code into MCP-compatible agent workflows.
Builders who want to connect Rust services or apps to MCP clients and servers.
You can build Rust MCP integrations instead of hand-rolling the protocol layer.
Implements MCP client and server behavior with tokio async runtime.
Provides `#[tool]`, `#[tool_router]`, and `#[tool_handler]` for exposing callable server tools.
Lets servers expose reusable prompts with `#[prompt]` and typed arguments.
Supports listing and reading resources, plus resource templates and change notifications.
Supports legacy initialize flow and newer discover-based startup and negotiation.
Includes runnable examples and a conformance suite for protocol behavior.
cargo add rmcp --features server
cargo add rmcp --features server --git https://github.com/modelcontextprotocol/rust-sdk --branch main
An official Rust Model Context Protocol SDK implementation with tokio async runtime.
Migrating to 3.x? See the migration guide for breaking changes and upgrade instructions.
This repository contains the following crates:
This SDK implements the stable MCP 2026-07-28 specification while
remaining fully compatible with the 2025-11-25 release and earlier
versions. Features introduced in 2026-07-28 — server discovery & negotiation,
transport-neutral subscriptions, long-running tasks, response caching,
multi-round-trip requests, and standard HTTP routing headers — are documented
below. For the full MCP specification, see
modelcontextprotocol.io.
Add the latest published version with cargo:
cargo add rmcp --features server
Or use the dev channel:
cargo add rmcp --features server --git https://github.com/modelcontextprotocol/rust-sdk --branch main
Basic dependencies:
use rmcp::{ServiceExt, transport::{TokioChildProcess, ConfigureCommandExt}};
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ().serve(TokioChildProcess::new(Command::new("npx").configure(|cmd| {
cmd.arg("-y").arg("@modelcontextprotocol/server-everything");
}))?).await?;
Ok(())
}
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 to
select another lifecycle explicitly:
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 or does not respond within 10 seconds.
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.
use tokio::io::{stdin, stdout};
let transport = (stdin(), stdout());
You can easily build a service by using ServerHandler or ClientHandler.
let service = common::counter::Counter::new();
// this call will finish the initialization process
let server = service.serve(transport).await?;
Once the server is initialized, you can send requests or notifications:
// request
let roots = server.list_roots().await?;
// or send notification
server.notify_cancelled(...).await?;
let quit_reason = server.waiting().await?;
// or cancel it
let quit_reason = server.cancel().await?;
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
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:
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<AddParams>) -> 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):outputSchemamay now be any JSON Schema type (not justobject), and a tool result'sstructuredContentmay 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]:
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<AddParams>) -> 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 for full macro documentation.
Beyond a plain String, tools can return images, audio, embedded resources, and
mixed content. Build a CallToolResult from a Vec<ContentBlock>:
use rmcp::model::{CallToolResult, ContentBlock, ResourceContents};
#[tool(description = "Render a chart")]
async fn chart(&self) -> Result<CallToolResult, McpError> {
let png_base64 = render_png(); // base64-encoded image bytes
let wav_base64 = render_wav(); // base64-encoded audio bytes
Ok(CallToolResult::success(vec![
// Text
ContentBlock::text("Here is your chart:"),
// Image — base64 data + MIME type
ContentBlock::image(png_base64, "image/png"),
// Audio — base64 data + MIME type
ContentBlock::audio(wav_base64, "audio/wav"),
// Embedded resource — inline text (or ResourceContents::blob for binary)
ContentBlock::resource(ResourceContents::text(
"chart source data",
"chart://last/data.csv",
)),
]))
}
# fn render_png() -> String { String::new() }
# fn render_wav() -> String { String::new() }
Image and audio data are base64 strings with a MIME type. For embedded
resources, ResourceContents::text(..) inlines text and
ResourceContents::blob(base64, uri) inlines binary.
Two failure modes, chosen by whose problem it is:
Ok(CallToolResult::error(vec![...])). The tool ran but
failed in a way the caller should see (no rows matched, upstream 500). The
client renders your content, so the message reaches the user. Use this for
almost every "the tool ran and didn't work" case.Err(McpError) with a JSON-RPC code (e.g.
McpError::invalid_params(..)). Use this when the server can't route or process
the request at all; clients render these opaquely, so the caller does not
see your message.use rmcp::model::{CallToolResult, ContentBlock};
use rmcp::ErrorData as McpError;
#[tool(description = "Look up a record")]
async fn lookup(&self, Parameters(args): Parameters<LookupArgs>) -> Result<CallToolResult, McpError> {
// Malformed request — the server can't run anything → protocol error.
if args.query.is_empty() {
return Err(McpError::invalid_params("query must be non-empty", None));
}
// Tool ran, no result → tool-level error the user should see.
let rows = self.run_query(&args.query).await;
if rows.is_empty() {
return Ok(CallToolResult::error(vec![ContentBlock::text(
format!("no rows matched '{}'", args.query),
)]));
}
Ok(CallToolResult::success(vec![ContentBlock::text(format_rows(&rows))]))
}
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 (server), examples/servers/src/calculator_stdio.rs (stdio runner)
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
Implement list_resources(), read_resource(), and optionally list_resource_templates() on the ServerHandler trait. Enable the resources capability in get_info().
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<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
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<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
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),
])),
// Binary resource — base64-encode the bytes and return a blob.
"file:///logo.png" => {
use base64::{Engine, prelude::BASE64_STANDARD};
let bytes = std::fs::read("logo.png").unwrap_or_default();
let blob = BASE64_STANDARD.encode(bytes);
Ok(ReadResourceResult::new(vec![
ResourceContents::blob(blob, &request.uri)
.with_mime_type("image/png"),
]))
}
// Template-expanded URI — the client fills in `{user_id}` from the
// `users://{user_id}/profile` template declared in
// `list_resource_templates`, and the server reads the concrete URI.
uri if uri.starts_with("users://") && uri.ends_with("/profile") => {
let user_id = uri
.trim_start_matches("users://")
.trim_end_matches("/profile");
Ok(ReadResourceResult::new(vec![ResourceContents::text(
format!(r#"{{"id": "{user_id}", "name": "User {user_id}"}}"#),
uri,
)]))
}
_ => Err(McpError::resource_not_found(
"resource_not_found",
Some(json!({ "uri": request.uri })),
)),
}
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
// Declare a URI template with a `{user_id}` parameter. Clients expand it
// (e.g. `users://42/profile`) and pass the concrete URI to `read_resource`.
Ok(ListResourceTemplatesResult {
resource_templates: vec![
ResourceTemplate::new("users://{user_id}/profile", "user-profile"),
],
next_cursor: None,
meta: None,
})
}
}
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, then read a resource through one by expanding its
// parameters into a concrete URI (`users://{user_id}/profile` → `users://42/profile`).
let templates = client.list_all_resource_templates().await?;
let profile = client.read_resource(
ReadResourceRequestParams::new("users://42/profile"),
).await?;
Servers can notify clients when the resource list changes or when a specific resource is updated:
// 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:
impl ClientHandler for MyClient {
async fn on_resource_list_changed(
&self,
_context: NotificationContext<RoleClient>,
) {
// Re-fetch the resource list
}
async fn on_resource_updated(
&self,
params: ResourceUpdatedNotificationParam,
_context: NotificationContext<RoleClient>,
) {
// Re-read the updated resource at params.uri
}
}
Example: examples/servers/src/common/counter.rs (server), examples/clients/src/everything_stdio.rs (client)
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
Use the #[prompt_router], #[prompt], and #[prompt_handler] macros to define prompts declaratively. Arguments are defined as structs deriving JsonSchema.
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<Vec<String>>,
}
#[derive(Clone)]
pub struct MyServer {
prompt_router: PromptRouter<Self>,
}
#[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<PromptMessage> {
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<CodeReviewArgs>,
) -> Result<GetPromptResult, McpError> {
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<PromptMessage> -- simple message listGetPromptResult -- messages with an optional descriptionResult<T, McpError> -- either of the above, with error handlingA PromptMessage can also carry an image or embedded resource. Use the
dedicated constructors (image/audio require the base64 feature):
use rmcp::model::{PromptMessage, Role};
// Image content — raw bytes are base64-encoded for you.
let screenshot: &[u8] = load_png();
let msg = PromptMessage::new_image(Role::User, screenshot, "image/png", None, None);
// Embedded resource — inline a text resource by URI. Pass `Some(text)` for a
// text resource, or `None` for a blob resource.
let msg = PromptMessage::new_resource(
Role::User,
"file:///spec.md".to_string(),
Some("text/markdown".to_string()),
Some("# Specification\n...".to_string()),
None, None, None,
);
# fn load_png() -> &'static [u8] { &[] }
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?;
// Server: notify that available prompts have changed
context.peer.notify_prompt_list_changed().await?;
Example: examples/servers/src/prompt_stdio.rs (server), examples/clients/src/everything_stdio.rs (client)
Deprecated (SEP-2577): Sampling is deprecated and will be removed in a future release. It remains fully functional for now. See SEP-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
Access the client's sampling capability through context.peer.create_message():
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);
On the client side, implement ClientHandler::create_message(). This is where you'd call your actual LLM:
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<RoleClient>,
) -> Result<CreateMessageResult, ErrorData> {
// 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 (server), examples/clients/src/sampling_stdio.rs (client)
Elicitation lets a server pause mid-operation
Sign in to join the discussion.
No comments yet. Be the first to say what this is good for.

Write HTML. Render video. Built for agents.
Ultra-lightweight, open-source, self-hosted personal AI agent framework in Python with WebUI, tools, memory, MCP, multi-agent workflows, automation, and chat apps
SkillOpt is a text-space optimizer that trains reusable natural-language skills for frozen LLM agents through trajectory-driven edits, validation-gated updates, and deployable best_skill.md artifacts.

Omnigent is an open-source AI agent framework and meta-harness: orchestrate Claude Code, Codex, Cursor, Pi, and custom agents — swap harnesses without rewriting, enforce policies and sandboxing, and collaborate in real time from any device.
A theoretical reconstruction of the Claude Mythos architecture, built from first principles using the available research literature.
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!