Install
$ agentstack add mcp-joshrotenberg-tower-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
tower-mcp
[](https://crates.io/crates/tower-mcp) [](https://docs.rs/tower-mcp) [](https://github.com/joshrotenberg/tower-mcp/actions/workflows/ci.yml) [](https://github.com/joshrotenberg/tower-mcp#license) [](https://github.com/joshrotenberg/tower-mcp) [](https://modelcontextprotocol.io/specification/2025-11-25) [](https://github.com/joshrotenberg/tower-mcp/actions/workflows/conformance.yml)
Tower-native Model Context Protocol (MCP) implementation for Rust.
Overview
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)
Familiar to axum Users
If you've used axum, tower-mcp's API will feel familiar:
- Extractor pattern: Tool handlers use extractors like
State,Json, 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
Why tower-mcp?
Strengths
| | | |---|---| | Tower-native middleware | Timeout, rate-limit, auth, tracing -- on the whole server or on individual tools. Any tower::Layer works. | | All transports | stdio, HTTP/SSE (with stream resumption), WebSocket, and child process. Same router, any transport. | | In-process testing | TestClient lets you test MCP servers without spawning a subprocess or opening a socket. | | Conformance | 39/39 server checks (30 scenarios) and 265/265 client checks (24 scenarios) from the official MCP conformance suite pass in CI on every PR. The suite is upstream-maintained and grows with the spec, so this is a moving target -- not a one-time achievement. SEP-2484 (accepted) makes conformance scenarios a prerequisite for standards-track SEPs reaching final. | | Capability filtering | Session-based tool/resource/prompt visibility for multi-tenant patterns. | | No proc macros required | Builder pattern API with optional trait-based tools. Nothing hidden behind #[derive]. Optional #[tool_fn] / #[prompt_fn] / #[resource_fn] macros available for convenience (feature: macros). | | Async tasks | Full task lifecycle -- background execution, cancellation, TTL cleanup, per-tool task support mode. Clients can poll or wait for long-running tool results. | | Multi-server proxy | Aggregate N backend servers behind a single endpoint with per-backend middleware and namespace isolation. | | axum ecosystem | HTTP and WebSocket transports build on axum, so existing axum middleware and extractors work. |
Trade-offs
- More boilerplate than macro-based approaches for simple servers, though the optional
macrosfeature narrows this gap significantly. - Requires Tower/Service familiarity. The
.layer()composition model is powerful but has a learning curve if you haven't used Tower before. - Heavier dependency tree than minimal single-transport implementations, especially with
features = ["full"].
Quick Start
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")
.title("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 middleware
Installation
Add to your Cargo.toml:
[dependencies]
tower-mcp = "0.12"
Feature Flags
| Feature | Description | |---------|-------------| | full | Enable all optional features | | http | HTTP transport with SSE support (adds axum, hyper) | | websocket | WebSocket transport for full-duplex communication | | childproc | Child process transport for spawning subprocess MCP servers | | oauth | OAuth 2.1 resource server support -- JWT validation, protected resource metadata (requires http) | | jwks | JWKS endpoint fetching for remote key sets (requires oauth) | | http-client | HTTP client transport for connecting to remote MCP servers | | oauth-client | OAuth 2.0 client-side token acquisition -- client credentials grant, auto-discovery, token caching (requires http-client) | | testing | Test utilities (TestClient) for in-process testing | | dynamic-tools | Runtime registration/deregistration of tools, prompts, and resources | | proxy | Multi-server aggregation proxy (McpProxy) | | macros | Optional proc macros (#[tool_fn], #[prompt_fn], #[resource_fn], #[resource_template_fn]) | | resilience | Re-export tower-resilience circuit breaker, rate limiter, and bulkhead layers | | stateless | Experimental 2026-07-28 stateless protocol mode (SEP-2575 final + SEP-2567 accepted) -- version-gated sessionless dispatch, server/discover RPC, messages/listen SSE endpoint, per-request _meta client capabilities. Requires http. |
Example with features:
[dependencies]
tower-mcp = { version = "0.12", features = ["full"] }
Types Only
If you only need MCP protocol types and error types -- without tower, tokio, or axum -- use the tower-mcp-types crate directly. This is useful for editor integrations, code generators, protocol validators, or any context where you want to serialize/deserialize MCP messages without a runtime.
[dependencies]
tower-mcp-types = "0.12"
tower-mcp-types provides all types from tower_mcp::protocol and tower_mcp::error with minimal dependencies (serde, serde_json, thiserror, base64). The full tower-mcp crate re-exports everything from tower-mcp-types, so there is no duplication if you use both.
Tool Definition
Builder Pattern (Recommended)
use 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();
Proc Macros (Optional)
Enable with features = ["macros"]. The macros generate builder code -- you can always eject to the builder pattern for full control.
use tower_mcp::{tool_fn, prompt_fn, resource_fn, resource_template_fn};
use tower_mcp::{CallToolResult, McpRouter};
use tower_mcp::protocol::{GetPromptResult, ReadResourceResult};
#[derive(Debug, Deserialize, JsonSchema)]
struct AddInput { a: i64, b: i64 }
#[tool_fn(description = "Add two numbers")]
async fn add(input: AddInput) -> Result {
Ok(CallToolResult::text(format!("{}", input.a + input.b)))
}
#[prompt_fn(description = "Greet someone", args(name = "Name to greet"))]
async fn greet(args: HashMap) -> Result {
let name = args.get("name").cloned().unwrap_or_default();
Ok(GetPromptResult::user_message(format!("Hello, {name}!")))
}
#[resource_fn(uri = "app://config", description = "App configuration")]
async fn config() -> Result {
Ok(ReadResourceResult::text("app://config", "debug=true"))
}
// Each macro generates a constructor: add_tool(), greet_prompt(), config_resource()
let router = McpRouter::new()
.server_info("my-server", "1.0.0")
.tool(add_tool())
.prompt(greet_prompt())
.resource(config_resource());
Trait-Based (For Complex Tools)
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 {
// 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());
Handler with Extractors (State, Context, JSON)
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>,
ctx: Context,
Json(input): Json,
| 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();
See docs.rs for more patterns including per-tool middleware, icons and titles, raw JSON handlers, and output schemas.
Resource Definition
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);
Prompt Definition
use tower_mcp::{PromptBuilder, GetPromptResult};
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),
};
// Builder handles message construction
Ok(GetPromptResult::builder()
.description("A friendly greeting")
.user(text)
.build())
})
.build();
let router = McpRouter::new().prompt(greet);
Router Composition
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"
Multi-Server Proxy
Aggregate multiple backend MCP servers behind a single endpoint with McpProxy (feature: proxy). Each backend's tools, resources, and prompts are namespaced to avoid collisions:
use tower_mcp::proxy::McpProxy;
use tower_mcp::client::StdioClientTransport;
let proxy = McpProxy::builder("my-proxy", "1.0.0")
.backend("db", StdioClientTransport::spawn("db-server", &[]).await?)
.await
.backend("fs", StdioClientTransport::spawn("fs-server", &[]).await?)
.await
.build()
.await?;
// Tools become db_query, fs_read, etc.
// Serve over any transport.
StdioTransport::new(proxy).run().await?;
Per-backend Tower middleware applies to individual backends:
use std::time::Duration;
use tower::timeout::TimeoutLayer;
let proxy = McpProxy::builder("proxy", "1.0.0")
.backend("fast", cache_transport).await
.backend_layer(TimeoutLayer::new(Duration::from_secs(2)))
.backend("slow", llm_transport).await
.backend_layer(TimeoutLayer::new(Duration::from_secs(60)))
.build().await?;
The proxy also supports notification forwarding (backend list-changed events propagate to clients), health checks (proxy.health_check().await), and request coalescing via tower-resilience's CoalesceLayer.
Backends don't need to be built with tower-mcp -- the proxy communicates over standard MCP (JSON-RPC), so it works with servers written in any language or framework: Python (FastMCP), TypeScript, Go, or anything that speaks the MCP protocol. This makes tower-mcp a natural aggregation and middleware layer for polyglot MCP deployments.
See the proxy module docs and examples/proxy.rs.
Router-Level State
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 extractor
let tool = ToolBuilder::new("query")
.extractor_handler(
(),
|Extension(app): Extension>, Json(input): Json| 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);
Transports
Stdio (CLI/local)
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?;
HTTP with SSE
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?;
With Authentication Middleware
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));
MCP Middleware
tower-mcp ships three MCP-specific middleware layers alongside standard tower middleware:
| Layer | Target | Purpose | |-------|--------|---------| | McpTracingLayer | All requests
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: joshrotenberg
- Source: joshrotenberg/tower-mcp
- License: Apache-2.0
- Homepage: https://docs.rs/tower-mcp
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.