Tower-native Model Context Protocol (MCP) implementation for Rust.
tower-mcp provides a composable, middleware-friendly approach to building MCP servers using the Tower service abstraction. 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)
If you've used axum, tower-mcp's API will feel familiar:
- Extractor pattern: Tool handlers use extractors like
State<T>,Json<T>, andContext - Router composition:
McpRouter::merge()andMcpRouter::nest()work like axum's router methods - Per-handler middleware: Apply Tower layers to individual tools, resources, or prompts via
.layer() - Builder pattern: Fluent builders for tools, resources, and prompts
A demo MCP server for querying crates.io is deployed at:
https://crates-mcp-demo.fly.dev
Connect with any MCP client that supports HTTP transport, or add to Claude Code's .mcp.json:
{
"mcpServers": {
"crates": {
"type": "http",
"url": "https://crates-mcp-demo.fly.dev"
}
}
}The demo includes 7 tools (search, info, versions, dependencies, reverse deps, downloads, owners), 2 prompts (analyze, compare), and 1 resource (recent searches). See examples/crates-mcp for the full source.
Clone the repo and run your MCP-enabled agent (like Claude Code) in the
tower-mcp directory. The .mcp.json configures several example servers:
| Server | Description |
|---|---|
crates-mcp-local |
Query crates.io for Rust crate info |
markdownlint-mcp |
Lint markdown with 66 rules |
weather |
Weather forecasts via NWS API |
conformance |
Full MCP spec conformance server (39/39 tests) |
git clone https://github.com/joshrotenberg/tower-mcp
cd tower-mcp
# Run your MCP agent here - servers will be available automaticallyFor a guided tour, ask your agent to read examples/README.md.
Or jump straight in:
- "Search for async runtime crates" (crates-mcp)
- "Lint examples/README.md for issues" (markdownlint-mcp)
- "What's the weather in Seattle?" (weather)
Active development - Core protocol, routing, and transports are implemented. Used in production for MCP server deployments.
- JSON-RPC 2.0 message types, validation, and batch request handling
- MCP protocol types (tools, resources, prompts)
- Tool builder with type-safe handlers and JSON Schema generation via schemars
McpTooltrait for complex toolsMcpRouterimplementing Tower'sServicetraitJsonRpcServicelayer for protocol framing- Session state management with reconnection support
- Protocol version negotiation
- Tool annotations (behavior hints for trust/safety)
- Transports: stdio, HTTP (with SSE and stream resumption), WebSocket, child process
- Resources: list, read, subscribe/unsubscribe with change notifications
- Prompts: list and get with argument support
- Authentication: API key and Bearer token middleware helpers
- Elicitation: Server-to-client user input requests (form and URL modes)
- Client support: MCP client for connecting to external servers
- Progress notifications: Via
RequestContextin tool handlers - Request cancellation: Via
CancellationTokenin tool handlers - Completion: Autocomplete for prompt arguments and resource URIs
- Sampling types:
CreateMessageParams/CreateMessageResultfor LLM requests - Sampling runtime: Full support on stdio, WebSocket, and HTTP transports
- Async tasks: Task ID generation, status tracking, TTL-based cleanup for long-running operations
Add to your Cargo.toml:
[dependencies]
tower-mcp = "0.3"| Feature | Description |
|---|---|
full |
Enable all optional features |
http |
HTTP transport with SSE support (adds axum, hyper dependencies) |
websocket |
WebSocket transport for full-duplex communication |
childproc |
Child process transport for spawning subprocess MCP servers |
client |
MCP client support for connecting to external servers |
Example with features:
[dependencies]
tower-mcp = { version = "0.2", features = ["full"] }use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
use schemars::JsonSchema;
use serde::Deserialize;
// Define your input type - schema is auto-generated
#[derive(Debug, Deserialize, JsonSchema)]
struct GreetInput {
name: String,
}
// Build a tool with type-safe handler
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 with tools
let router = McpRouter::new()
.server_info("my-server", "1.0.0")
.instructions("This server provides greeting functionality")
.tool(greet);
// The router implements tower::Service and can be composed with middlewareuse tower_mcp::{ToolBuilder, CallToolResult};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct AddInput {
a: i64,
b: i64,
}
let add = ToolBuilder::new("add")
.description("Add two numbers")
.read_only() // Hint: this tool doesn't modify state
.handler(|input: AddInput| async move {
Ok(CallToolResult::text(format!("{}", input.a + input.b)))
})
.build()?;use tower_mcp::tool::McpTool;
use tower_mcp::{Result, CallToolResult};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
struct Calculator {
precision: u32,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct CalcInput {
expression: String,
}
impl McpTool for Calculator {
const NAME: &'static str = "calculate";
const DESCRIPTION: &'static str = "Evaluate a mathematical expression";
type Input = CalcInput;
type Output = f64;
async fn call(&self, input: Self::Input) -> Result<Self::Output> {
// Your calculation logic here
Ok(42.0)
}
}
// Convert to Tool and register
let calc = Calculator { precision: 10 };
let router = McpRouter::new().tool(calc.into_tool());Use axum-style extractors to access state, context, and typed input:
use std::sync::Arc;
use tower_mcp::{ToolBuilder, CallToolResult};
use tower_mcp::extract::{State, Context, Json};
#[derive(Clone)]
struct AppState { db_url: String }
let state = Arc::new(AppState { db_url: "postgres://...".into() });
let search = ToolBuilder::new("search")
.description("Search with progress updates")
.extractor_handler(state, |
State(app): State<Arc<AppState>>,
ctx: Context,
Json(input): Json<SearchInput>,
| async move {
// Report progress
ctx.report_progress(0.5, Some(1.0), Some("Searching...")).await;
// Use state
let results = format!("Searched {} for: {}", app.db_url, input.query);
Ok(CallToolResult::text(results))
})
.build()?;let tool = ToolBuilder::new("analyze")
.title("Code Analyzer") // Human-readable display name
.description("Analyze code quality")
.icon("https://example.com/icon.svg")
.read_only()
.idempotent()
.build()?;Apply Tower layers to individual tools:
use std::time::Duration;
use tower::timeout::TimeoutLayer;
let slow_tool = 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()?;Use RawArgs extractor when you need the raw JSON:
use tower_mcp::extract::RawArgs;
let echo = ToolBuilder::new("echo")
.description("Echo back the input")
.extractor_handler((), |RawArgs(args): RawArgs| async move {
Ok(CallToolResult::json(args))
})
.build()?;use tower_mcp::ResourceBuilder;
// Static resource with inline content
let config = ResourceBuilder::new("file:///config.json")
.name("Configuration")
.description("Server configuration")
.json(serde_json::json!({
"version": "1.0.0",
"debug": true
}))
.build()?;
// Dynamic resource with handler
let status = ResourceBuilder::new("app:///status")
.name("Server Status")
.description("Current server status")
.handler(|| async {
Ok("Running".to_string())
})
.build()?;
let router = McpRouter::new()
.resource(config)
.resource(status);use tower_mcp::{PromptBuilder, GetPromptResult, PromptMessage, PromptRole, protocol::Content};
let greet = PromptBuilder::new("greet")
.description("Generate a greeting")
.required_arg("name", "Name to greet")
.optional_arg("style", "Greeting style (formal/casual)")
.handler(|args| async move {
let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
let style = args.get("style").map(|s| s.as_str()).unwrap_or("casual");
let text = match style {
"formal" => format!("Good day, {}. How may I assist you?", name),
_ => format!("Hey {}!", name),
};
Ok(GetPromptResult {
description: Some("A friendly greeting".to_string()),
messages: vec![PromptMessage {
role: PromptRole::User,
content: Content::Text { text, annotations: None },
}],
})
})
.build()?;
let router = McpRouter::new().prompt(greet);Combine routers like in axum:
// Merge routers (combines all tools/resources/prompts)
let api_router = McpRouter::new()
.tool(search_tool)
.tool(fetch_tool);
let admin_router = McpRouter::new()
.tool(reset_tool)
.tool(stats_tool);
let combined = McpRouter::new()
.merge(api_router)
.merge(admin_router);
// Nest with prefix (adds prefix to all tool names)
let v1 = McpRouter::new().tool(legacy_tool);
let v2 = McpRouter::new().tool(new_tool);
let versioned = McpRouter::new()
.nest("v1", v1) // Tools become "v1_legacy_tool"
.nest("v2", v2); // Tools become "v2_new_tool"Share state across all handlers using with_state():
use std::sync::Arc;
use tower_mcp::extract::Extension;
#[derive(Clone)]
struct AppState {
db: DatabasePool,
config: Config,
}
let state = Arc::new(AppState { /* ... */ });
// Tools access state via Extension<T> extractor
let tool = ToolBuilder::new("query")
.extractor_handler_typed::<_, _, _, QueryInput>(
(),
|Extension(app): Extension<Arc<AppState>>, Json(input): Json<QueryInput>| async move {
let result = app.db.query(&input.sql).await?;
Ok(CallToolResult::text(result))
},
)
.build()?;
let router = McpRouter::new()
.with_state(state) // Makes AppState available to all handlers
.tool(tool);use tower_mcp::{McpRouter, StdioTransport};
let router = McpRouter::new()
.server_info("my-server", "1.0.0")
.tool(my_tool);
// Serve over stdin/stdout
StdioTransport::new(router).serve().await?;use tower_mcp::{McpRouter, HttpTransport};
let router = McpRouter::new()
.server_info("my-server", "1.0.0")
.tool(my_tool);
let transport = HttpTransport::new(router);
let app = transport.into_router();
// Serve with axum
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app).await?;use tower_mcp::auth::extract_api_key;
use axum::middleware;
// Add auth layer to the HTTP transport
let app = transport.into_router()
.layer(middleware::from_fn(auth_middleware)); +-----------------+
| Your App |
+-----------------+
|
+-----------------+
| Tower Middleware| <-- tracing, metrics, auth, etc.
+-----------------+
|
+-----------------+
| JsonRpcService | <-- JSON-RPC 2.0 framing
+-----------------+
|
+-----------------+
| McpRouter | <-- Request dispatch
+-----------------+
|
+------------+------------+
| | |
+--------+ +--------+ +--------+
| Tool 1 | | Tool 2 | | Tool N |
+--------+ +--------+ +--------+
| Aspect | tower-mcp |
|---|---|
| Style | Library, not framework |
| Tool definition | Builder pattern or trait-based |
| Middleware | Native tower layers |
| Transport | Pluggable (stdio, HTTP, WebSocket, child process) |
| Integration | Composable with axum/tonic |
tower-mcp targets the MCP specification 2025-11-25. Current compliance:
- JSON-RPC 2.0 message format
- Protocol version negotiation
- Capability negotiation
- Initialize/initialized lifecycle
- tools/list and tools/call
- Tool annotations
- Batch requests
- resources/list, resources/read, resources/subscribe
- prompts/list, prompts/get
- Icons on tools/resources/prompts (SEP-973)
- Implementation metadata
- Sampling with tools/toolChoice (SEP-1577)
- Elicitation (user input requests)
- Session management
- Progress notifications
- Request cancellation
- Completion (autocomplete)
- Roots (filesystem discovery)
- Sampling (all transports)
- Async tasks (task ID, status tracking, TTL cleanup)
- SSE event IDs and stream resumption (SEP-1699)
We track all MCP Specification Enhancement Proposals (SEPs) as GitHub issues. A weekly workflow syncs status from the upstream spec repository.
# Format, lint, and test
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-featuresMIT OR Apache-2.0