Expand description
§tower-mcp
Tower-native Model Context Protocol (MCP) implementation for Rust.
This crate provides a composable, middleware-friendly approach to building MCP servers and clients using the Tower service abstraction.
§Philosophy
Unlike framework-style MCP implementations, tower-mcp treats MCP as just another
protocol that can be served through Tower’s Service trait. This means:
- Standard tower middleware (tracing, metrics, rate limiting, auth) just works
- Same service can be exposed over multiple transports (stdio, HTTP, WebSocket)
- Easy integration with existing tower-based applications (axum, tonic, etc.)
§Familiar to axum Users
If you’ve used axum, tower-mcp’s API will feel familiar. We’ve adopted axum’s patterns for a consistent Rust web ecosystem experience:
- Extractor pattern: Tool handlers use extractors like
extract::State<T>,extract::Json<T>, andextract::Context- just like axum’s request extractors - Router composition:
McpRouter::merge()andMcpRouter::nest()work like axum’s router methods for combining routers - Per-route middleware: Apply Tower layers to individual tools, resources, or
prompts via
.layer()on builders - Builder pattern: Fluent builders for tools, resources, and prompts
use std::sync::Arc;
use tower_mcp::{ToolBuilder, CallToolResult};
use tower_mcp::extract::{State, Json, Context};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Clone)]
struct AppState { db_url: String }
#[derive(Deserialize, JsonSchema)]
struct SearchInput { query: String }
// Looks just like an axum handler!
let tool = ToolBuilder::new("search")
.description("Search the database")
.extractor_handler_typed::<_, _, _, SearchInput>(
Arc::new(AppState { db_url: "postgres://...".into() }),
|State(app): State<Arc<AppState>>,
ctx: Context,
Json(input): Json<SearchInput>| async move {
ctx.report_progress(0.5, Some(1.0), Some("Searching...")).await;
Ok(CallToolResult::text(format!("Found results for: {}", input.query)))
},
)
.build()
.unwrap();§Quick Start: Server
Build an MCP server with tools, resources, and prompts:
use tower_mcp::{BoxError, McpRouter, ToolBuilder, CallToolResult, StdioTransport};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct GreetInput {
name: String,
}
#[tokio::main]
async fn main() -> Result<(), BoxError> {
// Define a tool
let greet = ToolBuilder::new("greet")
.description("Greet someone by name")
.handler(|input: GreetInput| async move {
Ok(CallToolResult::text(format!("Hello, {}!", input.name)))
})
.build()?;
// Create router and run over stdio
let router = McpRouter::new()
.server_info("my-server", "1.0.0")
.tool(greet);
StdioTransport::new(router).run().await?;
Ok(())
}§Quick Start: Client
Connect to an MCP server and call tools:
use tower_mcp::BoxError;
use tower_mcp::client::{McpClient, StdioClientTransport};
#[tokio::main]
async fn main() -> Result<(), BoxError> {
// Connect to server
let transport = StdioClientTransport::spawn("my-mcp-server", &[]).await?;
let mut client = McpClient::new(transport);
// Initialize and list tools
client.initialize("my-client", "1.0.0").await?;
let tools = client.list_tools().await?;
// Call a tool
let result = client.call_tool("greet", serde_json::json!({"name": "World"})).await?;
println!("{:?}", result);
Ok(())
}§Key Types
§Server
McpRouter- Routes MCP requests to tools, resources, and promptsToolBuilder- Builder for defining tools with type-safe handlersResourceBuilder- Builder for defining resourcesPromptBuilder- Builder for defining promptsStdioTransport- Stdio transport for CLI servers
§Client
McpClient- Client for connecting to MCP serversStdioClientTransport- Spawn and connect to server subprocesses
§Protocol
CallToolResult- Tool execution result with contentReadResourceResult- Resource read resultGetPromptResult- Prompt expansion resultContent- Text, image, audio, or resource content
§Feature Flags
full- Enable all optional featureshttp- HTTP/SSE transport for web serverswebsocket- WebSocket transport for bidirectional communicationchildproc- Child process transport for subprocess managementoauth- OAuth 2.1 resource server support (token validation, metadata endpoint)testing- Test utilities ([TestClient]) for ergonomic MCP server testing
§Middleware Placement Guide
tower-mcp supports Tower middleware at multiple levels. Choose based on scope:
| Level | Method | Scope | Use Cases |
|---|---|---|---|
| Transport | HttpTransport::layer() | All MCP requests | Global timeout, rate limit, metrics |
| axum | .into_router().layer() | HTTP layer only | CORS, compression, request logging |
| Per-tool | ToolBuilder::...layer() | Single tool | Tool-specific timeout, concurrency |
| Per-resource | ResourceBuilder::...layer() | Single resource | Caching, read timeout |
| Per-prompt | PromptBuilder::...layer() | Single prompt | Generation timeout |
§Decision Tree
Where should my middleware go?
│
├─ Affects ALL MCP requests?
│ └─ Yes → Transport: HttpTransport::layer() or WebSocketTransport::layer()
│
├─ HTTP-specific (CORS, compression, headers)?
│ └─ Yes → axum: transport.into_router().layer(...)
│
├─ Only one specific tool?
│ └─ Yes → Per-tool: ToolBuilder::...handler(...).layer(...)
│
├─ Only one specific resource?
│ └─ Yes → Per-resource: ResourceBuilder::...handler(...).layer(...)
│
└─ Only one specific prompt?
└─ Yes → Per-prompt: PromptBuilder::...handler(...).layer(...)§Example: Layered Timeouts
use std::time::Duration;
use tower::timeout::TimeoutLayer;
use tower_mcp::{McpRouter, ToolBuilder, CallToolResult, HttpTransport};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct SearchInput { query: String }
// This tool gets a longer timeout than the global default
let slow_search = ToolBuilder::new("slow_search")
.description("Thorough search (may take a while)")
.handler(|input: SearchInput| async move {
// ... slow operation ...
Ok(CallToolResult::text("results"))
})
.layer(TimeoutLayer::new(Duration::from_secs(60))) // 60s for this tool
.build()
.unwrap();
let router = McpRouter::new()
.server_info("example", "1.0.0")
.tool(slow_search);
// Global 30s timeout for all OTHER requests
let transport = HttpTransport::new(router)
.layer(TimeoutLayer::new(Duration::from_secs(30)));In this example:
slow_searchtool has a 60-second timeout (per-tool layer)- All other MCP requests have a 30-second timeout (transport layer)
- The per-tool layer is inner to the transport layer
§Layer Ordering
Layers wrap from outside in. The first layer added is the outermost:
Request → [Transport Layer] → [Per-tool Layer] → Handler → ResponseFor per-tool/resource/prompt, chained .layer() calls also wrap outside-in:
ToolBuilder::new("api")
.handler(...)
.layer(TimeoutLayer::new(...)) // Outer: timeout checked first
.layer(ConcurrencyLimitLayer::new(5)) // Inner: concurrency after timeout
.build()§Full Example
See examples/tool_middleware.rs
for a complete example demonstrating:
- Different timeouts per tool
- Concurrency limiting for expensive operations
- Multiple layers combined on a single tool
§MCP Specification
This crate implements the MCP specification (2025-11-25): https://modelcontextprotocol.io/specification/2025-11-25
Re-exports§
pub use async_task::Task;pub use async_task::TaskStore;pub use client::ClientTransport;pub use client::McpClient;pub use client::StdioClientTransport;pub use context::ChannelClientRequester;pub use context::ClientRequester;pub use context::ClientRequesterHandle;pub use context::Extensions;pub use context::NotificationReceiver;pub use context::NotificationSender;pub use context::OutgoingRequest;pub use context::OutgoingRequestReceiver;pub use context::OutgoingRequestSender;pub use context::RequestContext;pub use context::RequestContextBuilder;pub use context::ServerNotification;pub use context::outgoing_request_channel;pub use error::BoxError;pub use error::Error;pub use error::Result;pub use error::ToolError;pub use filter::CapabilityFilter;pub use filter::DenialBehavior;pub use filter::Filterable;pub use filter::PromptFilter;pub use filter::ResourceFilter;pub use filter::ToolFilter;pub use jsonrpc::JsonRpcLayer;pub use jsonrpc::JsonRpcService;pub use prompt::BoxPromptService;pub use prompt::Prompt;pub use prompt::PromptBuilder;pub use prompt::PromptHandler;pub use prompt::PromptRequest;pub use protocol::CallToolResult;pub use protocol::CompleteParams;pub use protocol::CompleteResult;pub use protocol::Completion;pub use protocol::CompletionArgument;pub use protocol::CompletionReference;pub use protocol::CompletionsCapability;pub use protocol::Content;pub use protocol::ContentRole;pub use protocol::CreateMessageParams;pub use protocol::CreateMessageResult;pub use protocol::ElicitAction;pub use protocol::ElicitFieldValue;pub use protocol::ElicitFormParams;pub use protocol::ElicitFormSchema;pub use protocol::ElicitMode;pub use protocol::ElicitRequestParams;pub use protocol::ElicitResult;pub use protocol::ElicitUrlParams;pub use protocol::ElicitationCapability;pub use protocol::ElicitationCompleteParams;pub use protocol::GetPromptResult;pub use protocol::GetPromptResultBuilder;pub use protocol::IncludeContext;pub use protocol::JsonRpcMessage;pub use protocol::JsonRpcRequest;pub use protocol::JsonRpcResponse;pub use protocol::JsonRpcResponseMessage;pub use protocol::ListRootsParams;pub use protocol::ListRootsResult;pub use protocol::McpRequest;pub use protocol::McpResponse;pub use protocol::ModelHint;pub use protocol::ModelPreferences;pub use protocol::PrimitiveSchemaDefinition;pub use protocol::PromptMessage;pub use protocol::PromptReference;pub use protocol::PromptRole;pub use protocol::ReadResourceResult;pub use protocol::ResourceContent;pub use protocol::ResourceReference;pub use protocol::Root;pub use protocol::RootsCapability;pub use protocol::SamplingCapability;pub use protocol::SamplingContent;pub use protocol::SamplingContentOrArray;pub use protocol::SamplingMessage;pub use protocol::SamplingTool;pub use protocol::ToolChoice;pub use resource::BoxResourceService;pub use resource::Resource;pub use resource::ResourceBuilder;pub use resource::ResourceHandler;pub use resource::ResourceRequest;pub use resource::ResourceTemplate;pub use resource::ResourceTemplateBuilder;pub use resource::ResourceTemplateHandler;pub use router::McpRouter;pub use router::RouterRequest;pub use router::RouterResponse;pub use session::SessionPhase;pub use session::SessionState;pub use tool::BoxToolService;pub use tool::NoParams;pub use tool::Tool;pub use tool::ToolBuilder;pub use tool::ToolHandler;pub use tool::ToolRequest;pub use transport::BidirectionalStdioTransport;pub use transport::CatchError;pub use transport::GenericStdioTransport;pub use transport::StdioTransport;pub use transport::SyncStdioTransport;
Modules§
- async_
task - Async task management for long-running MCP operations
- auth
- Authentication middleware helpers for MCP servers
- client
- MCP Client implementation
- context
- Request context for MCP handlers
- error
- Error types for tower-mcp
- extract
- Extractor pattern for tool handlers
- filter
- Session-based capability filtering.
- jsonrpc
- JSON-RPC 2.0 service layer
- prompt
- Prompt definition and builder API
- protocol
- MCP protocol types based on JSON-RPC 2.0
- resource
- Resource definition and builder API
- router
- MCP Router - routes requests to tools, resources, and prompts
- session
- MCP session state management
- tool
- Tool definition and builder API
- transport
- MCP transport implementations