tower-mcp 0.3.2

Tower-native Model Context Protocol (MCP) implementation
Documentation

tower-mcp

Crates.io Documentation CI License MSRV MCP Conformance

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<T>, Json<T>, and Context
  • Router composition: McpRouter::merge() and McpRouter::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

Live Demo

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.

Try the Examples

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 automatically

For 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)

Status

Active development - Core protocol, routing, and transports are implemented. Used in production for MCP server deployments.

Implemented

  • 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
  • McpTool trait for complex tools
  • McpRouter implementing Tower's Service trait
  • JsonRpcService layer 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 RequestContext in tool handlers
  • Request cancellation: Via CancellationToken in tool handlers
  • Completion: Autocomplete for prompt arguments and resource URIs
  • Sampling types: CreateMessageParams/CreateMessageResult for 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

Installation

Add to your Cargo.toml:

[dependencies]
tower-mcp = "0.3"

Feature Flags

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"] }

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")
    .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

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()?;

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<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());

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<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()?;

Tool with Icons and Title

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()?;

Per-Tool Middleware

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()?;

Raw JSON Handler (Escape Hatch)

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()?;

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, 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);

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"

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<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);

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));

Architecture

                    +-----------------+
                    |  Your App       |
                    +-----------------+
                           |
                    +-----------------+
                    | Tower Middleware|  <-- tracing, metrics, auth, etc.
                    +-----------------+
                           |
                    +-----------------+
                    | JsonRpcService  |  <-- JSON-RPC 2.0 framing
                    +-----------------+
                           |
                    +-----------------+
                    |   McpRouter     |  <-- Request dispatch
                    +-----------------+
                           |
              +------------+------------+
              |            |            |
         +--------+   +--------+   +--------+
         | Tool 1 |   | Tool 2 |   | Tool N |
         +--------+   +--------+   +--------+

Design Philosophy

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

Protocol Compliance

tower-mcp targets the MCP specification 2025-11-25. Current compliance:

We track all MCP Specification Enhancement Proposals (SEPs) as GitHub issues. A weekly workflow syncs status from the upstream spec repository.

Development

# Format, lint, and test
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

License

MIT OR Apache-2.0