{"id":42618683,"url":"https://github.com/joshrotenberg/tower-mcp","last_synced_at":"2026-04-01T18:17:23.585Z","repository":{"id":335129550,"uuid":"1143689995","full_name":"joshrotenberg/tower-mcp","owner":"joshrotenberg","description":"Tower-native Model Context Protocol (MCP) implementation","archived":false,"fork":false,"pushed_at":"2026-03-26T17:53:29.000Z","size":1517,"stargazers_count":2,"open_issues_count":68,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T17:55:28.345Z","etag":null,"topics":["ai","json-rpc","mcp","mcp-s","rust","tokio","tower"],"latest_commit_sha":null,"homepage":"https://docs.rs/tower-mcp","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshrotenberg.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-27T22:11:54.000Z","updated_at":"2026-03-26T02:06:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/joshrotenberg/tower-mcp","commit_stats":null,"previous_names":["joshrotenberg/tower-mcp"],"tags_count":55,"template":false,"template_full_name":null,"purl":"pkg:github/joshrotenberg/tower-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshrotenberg%2Ftower-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshrotenberg%2Ftower-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshrotenberg%2Ftower-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshrotenberg%2Ftower-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshrotenberg","download_url":"https://codeload.github.com/joshrotenberg/tower-mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshrotenberg%2Ftower-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290806,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","json-rpc","mcp","mcp-s","rust","tokio","tower"],"created_at":"2026-01-29T04:13:02.446Z","updated_at":"2026-04-01T18:17:23.577Z","avatar_url":"https://github.com/joshrotenberg.png","language":"Rust","funding_links":[],"categories":["SDKs"],"sub_categories":["Community"],"readme":"# tower-mcp\n\n[![Crates.io](https://img.shields.io/crates/v/tower-mcp.svg)](https://crates.io/crates/tower-mcp)\n[![Documentation](https://docs.rs/tower-mcp/badge.svg)](https://docs.rs/tower-mcp)\n[![CI](https://github.com/joshrotenberg/tower-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/joshrotenberg/tower-mcp/actions/workflows/ci.yml)\n[![License](https://img.shields.io/crates/l/tower-mcp.svg)](https://github.com/joshrotenberg/tower-mcp#license)\n[![MSRV](https://img.shields.io/crates/msrv/tower-mcp.svg)](https://github.com/joshrotenberg/tower-mcp)\n[![MCP](https://img.shields.io/badge/MCP-2025--11--25-blue)](https://modelcontextprotocol.io/specification/2025-11-25)\n[![Conformance](https://img.shields.io/badge/conformance-39%2F39_server_%7C_265%2F265_client-brightgreen)](https://github.com/joshrotenberg/tower-mcp/actions/workflows/conformance.yml)\n\nTower-native [Model Context Protocol](https://modelcontextprotocol.io) (MCP) implementation for Rust.\n\n## Overview\n\ntower-mcp provides a composable, middleware-friendly approach to building MCP servers using the [Tower](https://github.com/tower-rs/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.\n\nThis means:\n\n- Standard tower middleware (tracing, metrics, rate limiting, auth) just works\n- Same service can be exposed over multiple transports (stdio, HTTP, WebSocket)\n- Easy integration with existing tower-based applications (axum, tonic)\n\n### Familiar to axum Users\n\nIf you've used [axum](https://docs.rs/axum), tower-mcp's API will feel familiar:\n\n- **Extractor pattern**: Tool handlers use extractors like `State\u003cT\u003e`, `Json\u003cT\u003e`, and `Context`\n- **Router composition**: `McpRouter::merge()` and `McpRouter::nest()` work like axum's router methods\n- **Per-handler middleware**: Apply Tower layers to individual tools, resources, or prompts via `.layer()`\n- **Builder pattern**: Fluent builders for tools, resources, and prompts\n\n## Why tower-mcp?\n\n### Strengths\n\n| | |\n|---|---|\n| **Tower-native middleware** | Timeout, rate-limit, auth, tracing -- on the whole server or on individual tools. Any `tower::Layer` works. |\n| **All transports** | stdio, HTTP/SSE (with stream resumption), WebSocket, and child process. Same router, any transport. |\n| **In-process testing** | `TestClient` lets you test MCP servers without spawning a subprocess or opening a socket. |\n| **Conformance** | 39/39 server and 265/265 client conformance checks pass in CI on every PR. |\n| **Capability filtering** | Session-based tool/resource/prompt visibility for multi-tenant patterns. |\n| **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`). |\n| **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. |\n| **Multi-server proxy** | Aggregate N backend servers behind a single endpoint with per-backend middleware and namespace isolation. |\n| **axum ecosystem** | HTTP and WebSocket transports build on axum, so existing axum middleware and extractors work. |\n\n### Trade-offs\n\n- **More boilerplate than macro-based approaches** for simple servers, though the optional `macros` feature narrows this gap significantly.\n- **Requires Tower/Service familiarity.** The `.layer()` composition model is powerful but has a learning curve if you haven't used Tower before.\n- **Heavier dependency tree** than minimal single-transport implementations, especially with `features = [\"full\"]`.\n\n## Quick Start\n\n```rust\nuse tower_mcp::{McpRouter, ToolBuilder, CallToolResult};\nuse schemars::JsonSchema;\nuse serde::Deserialize;\n\n// Define your input type - schema is auto-generated\n#[derive(Debug, Deserialize, JsonSchema)]\nstruct GreetInput {\n    name: String,\n}\n\n// Build a tool with type-safe handler\nlet greet = ToolBuilder::new(\"greet\")\n    .title(\"Greet\")\n    .description(\"Greet someone by name\")\n    .handler(|input: GreetInput| async move {\n        Ok(CallToolResult::text(format!(\"Hello, {}!\", input.name)))\n    })\n    .build();\n\n// Create router with tools\nlet router = McpRouter::new()\n    .server_info(\"my-server\", \"1.0.0\")\n    .instructions(\"This server provides greeting functionality\")\n    .tool(greet);\n\n// The router implements tower::Service and can be composed with middleware\n```\n\n## Installation\n\nAdd to your `Cargo.toml`:\n\n```toml\n[dependencies]\ntower-mcp = \"0.9\"\n```\n\n### Feature Flags\n\n| Feature | Description |\n|---------|-------------|\n| `full` | Enable all optional features |\n| `http` | HTTP transport with SSE support (adds axum, hyper) |\n| `websocket` | WebSocket transport for full-duplex communication |\n| `childproc` | Child process transport for spawning subprocess MCP servers |\n| `oauth` | OAuth 2.1 resource server support -- JWT validation, protected resource metadata (requires `http`) |\n| `jwks` | JWKS endpoint fetching for remote key sets (requires `oauth`) |\n| `http-client` | HTTP client transport for connecting to remote MCP servers |\n| `oauth-client` | OAuth 2.0 client-side token acquisition -- client credentials grant, auto-discovery, token caching (requires `http-client`) |\n| `testing` | Test utilities (`TestClient`) for in-process testing |\n| `dynamic-tools` | Runtime registration/deregistration of tools, prompts, and resources |\n| `proxy` | Multi-server aggregation proxy (`McpProxy`) |\n| `macros` | Optional proc macros (`#[tool_fn]`, `#[prompt_fn]`, `#[resource_fn]`, `#[resource_template_fn]`) |\n| `resilience` | Re-export tower-resilience circuit breaker, rate limiter, and bulkhead layers |\n| `stateless` | SEP-1442 stateless MCP mode (experimental) -- serve requests without sessions |\n\nExample with features:\n\n```toml\n[dependencies]\ntower-mcp = { version = \"0.9\", features = [\"full\"] }\n```\n\n### Types Only\n\nIf you only need MCP protocol types and error types -- without tower, tokio, or axum --\nuse the [`tower-mcp-types`](https://crates.io/crates/tower-mcp-types) crate directly.\nThis is useful for editor integrations, code generators, protocol validators, or\nany context where you want to serialize/deserialize MCP messages without a runtime.\n\n```toml\n[dependencies]\ntower-mcp-types = \"0.9\"\n```\n\n`tower-mcp-types` provides all types from `tower_mcp::protocol` and `tower_mcp::error`\nwith minimal dependencies (`serde`, `serde_json`, `thiserror`, `base64`). The full\n`tower-mcp` crate re-exports everything from `tower-mcp-types`, so there is no\nduplication if you use both.\n\n## Tool Definition\n\n### Builder Pattern (Recommended)\n\n```rust\nuse tower_mcp::{ToolBuilder, CallToolResult};\nuse schemars::JsonSchema;\nuse serde::Deserialize;\n\n#[derive(Debug, Deserialize, JsonSchema)]\nstruct AddInput {\n    a: i64,\n    b: i64,\n}\n\nlet add = ToolBuilder::new(\"add\")\n    .description(\"Add two numbers\")\n    .read_only()  // Hint: this tool doesn't modify state\n    .handler(|input: AddInput| async move {\n        Ok(CallToolResult::text(format!(\"{}\", input.a + input.b)))\n    })\n    .build();\n```\n\n### Proc Macros (Optional)\n\nEnable with `features = [\"macros\"]`. The macros generate builder code -- you can always eject to the builder pattern for full control.\n\n```rust\nuse tower_mcp::{tool_fn, prompt_fn, resource_fn, resource_template_fn};\nuse tower_mcp::{CallToolResult, McpRouter};\nuse tower_mcp::protocol::{GetPromptResult, ReadResourceResult};\n\n#[derive(Debug, Deserialize, JsonSchema)]\nstruct AddInput { a: i64, b: i64 }\n\n#[tool_fn(description = \"Add two numbers\")]\nasync fn add(input: AddInput) -\u003e Result\u003cCallToolResult, tower_mcp::Error\u003e {\n    Ok(CallToolResult::text(format!(\"{}\", input.a + input.b)))\n}\n\n#[prompt_fn(description = \"Greet someone\", args(name = \"Name to greet\"))]\nasync fn greet(args: HashMap\u003cString, String\u003e) -\u003e Result\u003cGetPromptResult, tower_mcp::Error\u003e {\n    let name = args.get(\"name\").cloned().unwrap_or_default();\n    Ok(GetPromptResult::user_message(format!(\"Hello, {name}!\")))\n}\n\n#[resource_fn(uri = \"app://config\", description = \"App configuration\")]\nasync fn config() -\u003e Result\u003cReadResourceResult, tower_mcp::Error\u003e {\n    Ok(ReadResourceResult::text(\"app://config\", \"debug=true\"))\n}\n\n// Each macro generates a constructor: add_tool(), greet_prompt(), config_resource()\nlet router = McpRouter::new()\n    .server_info(\"my-server\", \"1.0.0\")\n    .tool(add_tool())\n    .prompt(greet_prompt())\n    .resource(config_resource());\n```\n\n### Trait-Based (For Complex Tools)\n\n```rust\nuse tower_mcp::tool::McpTool;\nuse tower_mcp::{Result, CallToolResult};\nuse schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\nuse std::sync::Arc;\n\nstruct Calculator {\n    precision: u32,\n}\n\n#[derive(Debug, Deserialize, JsonSchema)]\nstruct CalcInput {\n    expression: String,\n}\n\nimpl McpTool for Calculator {\n    const NAME: \u0026'static str = \"calculate\";\n    const DESCRIPTION: \u0026'static str = \"Evaluate a mathematical expression\";\n\n    type Input = CalcInput;\n    type Output = f64;\n\n    async fn call(\u0026self, input: Self::Input) -\u003e Result\u003cSelf::Output\u003e {\n        // Your calculation logic here\n        Ok(42.0)\n    }\n}\n\n// Convert to Tool and register\nlet calc = Calculator { precision: 10 };\nlet router = McpRouter::new().tool(calc.into_tool());\n```\n\n### Handler with Extractors (State, Context, JSON)\n\nUse axum-style extractors to access state, context, and typed input:\n\n```rust\nuse std::sync::Arc;\nuse tower_mcp::{ToolBuilder, CallToolResult};\nuse tower_mcp::extract::{State, Context, Json};\n\n#[derive(Clone)]\nstruct AppState { db_url: String }\n\nlet state = Arc::new(AppState { db_url: \"postgres://...\".into() });\n\nlet search = ToolBuilder::new(\"search\")\n    .description(\"Search with progress updates\")\n    .extractor_handler(state, |\n        State(app): State\u003cArc\u003cAppState\u003e\u003e,\n        ctx: Context,\n        Json(input): Json\u003cSearchInput\u003e,\n    | async move {\n        // Report progress\n        ctx.report_progress(0.5, Some(1.0), Some(\"Searching...\")).await;\n        // Use state\n        let results = format!(\"Searched {} for: {}\", app.db_url, input.query);\n        Ok(CallToolResult::text(results))\n    })\n    .build();\n```\n\nSee [docs.rs](https://docs.rs/tower-mcp) for more patterns including per-tool middleware, icons and titles, raw JSON handlers, and output schemas.\n\n## Resource Definition\n\n```rust\nuse tower_mcp::ResourceBuilder;\n\n// Static resource with inline content\nlet config = ResourceBuilder::new(\"file:///config.json\")\n    .name(\"Configuration\")\n    .description(\"Server configuration\")\n    .json(serde_json::json!({\n        \"version\": \"1.0.0\",\n        \"debug\": true\n    }))\n    .build();\n\n// Dynamic resource with handler\nlet status = ResourceBuilder::new(\"app:///status\")\n    .name(\"Server Status\")\n    .description(\"Current server status\")\n    .handler(|| async {\n        Ok(\"Running\".to_string())\n    })\n    .build();\n\nlet router = McpRouter::new()\n    .resource(config)\n    .resource(status);\n```\n\n## Prompt Definition\n\n```rust\nuse tower_mcp::{PromptBuilder, GetPromptResult};\n\nlet greet = PromptBuilder::new(\"greet\")\n    .description(\"Generate a greeting\")\n    .required_arg(\"name\", \"Name to greet\")\n    .optional_arg(\"style\", \"Greeting style (formal/casual)\")\n    .handler(|args| async move {\n        let name = args.get(\"name\").map(|s| s.as_str()).unwrap_or(\"World\");\n        let style = args.get(\"style\").map(|s| s.as_str()).unwrap_or(\"casual\");\n\n        let text = match style {\n            \"formal\" =\u003e format!(\"Good day, {}. How may I assist you?\", name),\n            _ =\u003e format!(\"Hey {}!\", name),\n        };\n\n        // Builder handles message construction\n        Ok(GetPromptResult::builder()\n            .description(\"A friendly greeting\")\n            .user(text)\n            .build())\n    })\n    .build();\n\nlet router = McpRouter::new().prompt(greet);\n```\n\n## Router Composition\n\nCombine routers like in axum:\n\n```rust\n// Merge routers (combines all tools/resources/prompts)\nlet api_router = McpRouter::new()\n    .tool(search_tool)\n    .tool(fetch_tool);\n\nlet admin_router = McpRouter::new()\n    .tool(reset_tool)\n    .tool(stats_tool);\n\nlet combined = McpRouter::new()\n    .merge(api_router)\n    .merge(admin_router);\n\n// Nest with prefix (adds prefix to all tool names)\nlet v1 = McpRouter::new().tool(legacy_tool);\nlet v2 = McpRouter::new().tool(new_tool);\n\nlet versioned = McpRouter::new()\n    .nest(\"v1\", v1)   // Tools become \"v1_legacy_tool\"\n    .nest(\"v2\", v2);  // Tools become \"v2_new_tool\"\n```\n\n## Multi-Server Proxy\n\nAggregate multiple backend MCP servers behind a single endpoint with `McpProxy` (feature: `proxy`). Each backend's tools, resources, and prompts are namespaced to avoid collisions:\n\n```rust\nuse tower_mcp::proxy::McpProxy;\nuse tower_mcp::client::StdioClientTransport;\n\nlet proxy = McpProxy::builder(\"my-proxy\", \"1.0.0\")\n    .backend(\"db\", StdioClientTransport::spawn(\"db-server\", \u0026[]).await?)\n    .await\n    .backend(\"fs\", StdioClientTransport::spawn(\"fs-server\", \u0026[]).await?)\n    .await\n    .build()\n    .await?;\n\n// Tools become db_query, fs_read, etc.\n// Serve over any transport.\nStdioTransport::new(proxy).run().await?;\n```\n\nPer-backend Tower middleware applies to individual backends:\n\n```rust\nuse std::time::Duration;\nuse tower::timeout::TimeoutLayer;\n\nlet proxy = McpProxy::builder(\"proxy\", \"1.0.0\")\n    .backend(\"fast\", cache_transport).await\n    .backend_layer(TimeoutLayer::new(Duration::from_secs(2)))\n    .backend(\"slow\", llm_transport).await\n    .backend_layer(TimeoutLayer::new(Duration::from_secs(60)))\n    .build().await?;\n```\n\nThe 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`.\n\nBackends 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.\n\nSee the [`proxy` module docs](https://docs.rs/tower-mcp/latest/tower_mcp/proxy/) and `examples/proxy.rs`.\n\n## Router-Level State\n\nShare state across all handlers using `with_state()`:\n\n```rust\nuse std::sync::Arc;\nuse tower_mcp::extract::Extension;\n\n#[derive(Clone)]\nstruct AppState {\n    db: DatabasePool,\n    config: Config,\n}\n\nlet state = Arc::new(AppState { /* ... */ });\n\n// Tools access state via Extension\u003cT\u003e extractor\nlet tool = ToolBuilder::new(\"query\")\n    .extractor_handler(\n        (),\n        |Extension(app): Extension\u003cArc\u003cAppState\u003e\u003e, Json(input): Json\u003cQueryInput\u003e| async move {\n            let result = app.db.query(\u0026input.sql).await?;\n            Ok(CallToolResult::text(result))\n        },\n    )\n    .build();\n\nlet router = McpRouter::new()\n    .with_state(state)  // Makes AppState available to all handlers\n    .tool(tool);\n```\n\n## Transports\n\n### Stdio (CLI/local)\n\n```rust\nuse tower_mcp::{McpRouter, StdioTransport};\n\nlet router = McpRouter::new()\n    .server_info(\"my-server\", \"1.0.0\")\n    .tool(my_tool);\n\n// Serve over stdin/stdout\nStdioTransport::new(router).serve().await?;\n```\n\n### HTTP with SSE\n\n```rust\nuse tower_mcp::{McpRouter, HttpTransport};\n\nlet router = McpRouter::new()\n    .server_info(\"my-server\", \"1.0.0\")\n    .tool(my_tool);\n\nlet transport = HttpTransport::new(router);\nlet app = transport.into_router();\n\n// Serve with axum\nlet listener = tokio::net::TcpListener::bind(\"127.0.0.1:3000\").await?;\naxum::serve(listener, app).await?;\n```\n\n### With Authentication Middleware\n\n```rust\nuse tower_mcp::auth::extract_api_key;\nuse axum::middleware;\n\n// Add auth layer to the HTTP transport\nlet app = transport.into_router()\n    .layer(middleware::from_fn(auth_middleware));\n```\n\n## MCP Middleware\n\ntower-mcp ships three MCP-specific middleware layers alongside standard tower middleware:\n\n| Layer | Target | Purpose |\n|-------|--------|---------|\n| `McpTracingLayer` | All requests | Structured tracing with spans for request lifecycle |\n| `ToolCallLoggingLayer` | `tools/call` only | Focused tool call audit logging with annotation hints |\n| `AuditLayer` | All requests | Comprehensive audit events (`mcp::audit` tracing target) |\n\n```rust\nuse tower::ServiceBuilder;\nuse tower_mcp::middleware::{AuditLayer, McpTracingLayer};\n\nlet transport = StdioTransport::new(router)\n    .layer(\n        ServiceBuilder::new()\n            .layer(McpTracingLayer::new())\n            .layer(AuditLayer::new())\n            .into_inner(),\n    );\n```\n\nStandard tower middleware (timeout, rate limiting, concurrency) also composes naturally via `.layer()` on transports and individual tools.\n\n## Testing\n\ntower-mcp includes `TestClient` (feature: `testing`) for in-process server testing -- no subprocess, no network, no port management:\n\n```rust\nuse tower_mcp::TestClient;\nuse serde_json::json;\n\nlet mut client = TestClient::from_router(router);\nclient.initialize().await;\n\n// List and call tools\nlet tools = client.list_tools().await;\nassert_eq!(tools.len(), 1);\n\nlet result = client.call_tool(\"greet\", json!({\"name\": \"World\"})).await;\nassert_eq!(result.all_text(), \"Hello, World!\");\n\n// Typed deserialization\nlet stats: ServerStats = client.call_tool_typed(\"stats\", json!({})).await;\n\n// Assert expected errors\nlet err = client.call_tool_expect_error(\"missing\", json!({})).await;\n```\n\n`TestClient` handles JSON-RPC framing, request IDs, and protocol initialization. Methods panic on unexpected errors, keeping test code concise.\n\n## Capability Filtering\n\nControl which tools, resources, and prompts each session can see. This enables multi-tenant patterns where different clients get different capabilities based on auth claims or session state:\n\n```rust\nuse tower_mcp::CapabilityFilter;\n\n// Hide write tools from sessions that aren't authorized\nlet router = McpRouter::new()\n    .tool(read_tool)\n    .tool(write_tool)\n    .tool_filter(CapabilityFilter::write_guard(|session| {\n        session.get::\u003cUserRole\u003e()\n            .map(|r| r.is_admin())\n            .unwrap_or(false)\n    }));\n```\n\n`write_guard` uses tool annotations: tools marked `.read_only()` are always visible, while other tools are only shown to sessions where the predicate returns `true`. Hidden tools return \"method not found\" by default, or configure `DenialBehavior::Unauthorized` to reveal their existence without granting access.\n\nFilters work on resources and prompts too:\n\n```rust\nlet router = McpRouter::new()\n    .resource(public_resource)\n    .resource(internal_resource)\n    .resource_filter(CapabilityFilter::new(|session, resource: \u0026Resource| {\n        !resource.name().contains(\"internal\") || session.get::\u003cAdminClaim\u003e().is_some()\n    }));\n```\n\n## Architecture\n\n```text\n                    +-----------------+\n                    |  Your App       |\n                    +-----------------+\n                           |\n                    +-----------------+\n                    | Tower Middleware|  \u003c-- tracing, metrics, auth, etc.\n                    +-----------------+\n                           |\n                    +-----------------+\n                    | JsonRpcService  |  \u003c-- JSON-RPC 2.0 framing\n                    +-----------------+\n                           |\n                    +-----------------+\n                    |   McpRouter     |  \u003c-- Request dispatch\n                    +-----------------+\n                           |\n              +------------+------------+\n              |            |            |\n         +--------+   +--------+   +--------+\n         | Tool 1 |   | Tool 2 |   | Tool N |\n         +--------+   +--------+   +--------+\n```\n\n## Protocol Compliance\n\ntower-mcp targets the [MCP specification 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) with backward compatibility for `2025-03-26`. The [official MCP conformance test suite](https://github.com/joshrotenberg/tower-mcp/actions/workflows/conformance.yml) runs in CI on every PR, currently passing 39/39 server tests and 265/265 client checks (24/24 scenarios).\n\n- [x] [JSON-RPC 2.0 message format](https://modelcontextprotocol.io/specification/2025-11-25/basic#messages)\n- [x] [Protocol version negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation) (supports `2025-11-25` and `2025-03-26`)\n- [x] [Capability negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#capability-negotiation)\n- [x] [Initialize/initialized lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle)\n- [x] [tools/list and tools/call](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)\n- [x] [Tool annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)\n- [x] [Batch requests](https://modelcontextprotocol.io/specification/2025-11-25/basic#batching)\n- [x] [resources/list, resources/read, resources/subscribe](https://modelcontextprotocol.io/specification/2025-11-25/server/resources)\n- [x] [resources/templates/list](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates)\n- [x] [prompts/list, prompts/get](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts)\n- [x] [Logging (notifications/message, logging/setLevel)](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging)\n- [x] [Icons on tools/resources/prompts (SEP-973)](https://modelcontextprotocol.io/specification/2025-11-25)\n- [x] [Implementation metadata](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle)\n- [x] [Sampling with tools/toolChoice (SEP-1577)](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling)\n- [x] [Elicitation (form and URL modes)](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation)\n- [x] [Session management](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management)\n- [x] [Progress notifications](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress)\n- [x] [Request cancellation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation)\n- [x] [Completion (autocomplete)](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion)\n- [x] [Roots (filesystem discovery)](https://modelcontextprotocol.io/specification/2025-11-25/client/roots)\n- [x] [Sampling](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) (all transports)\n- [x] [Async tasks](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/async) (task ID, status tracking, TTL cleanup, per-tool task support mode)\n- [x] [SSE event IDs and stream resumption](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redelivery) (SEP-1699)\n- [x] [`_meta` field on all protocol types](https://modelcontextprotocol.io/specification/2025-11-25)\n\nWe track all MCP Specification Enhancement Proposals (SEPs) as [GitHub issues](https://github.com/joshrotenberg/tower-mcp/issues?q=label%3Asep). A weekly workflow syncs status from the upstream spec repository.\n\n## Examples\n\nA full-featured MCP server for querying [crates.io](https://crates.io) is available as a standalone project: [cratesio-mcp](https://github.com/joshrotenberg/cratesio-mcp). A demo instance is deployed at **https://cratesio-mcp.fly.dev** -- connect with any MCP client that supports HTTP transport.\n\nThe repo includes 23 focused examples organized by topic:\n\n| Category | Examples |\n|----------|----------|\n| **Getting started** | [`getting_started`](examples/getting_started.rs) -- tools, resources, prompts, stdio transport |\n| **Transports** | [`http_server`](examples/http_server.rs), [`websocket_server`](examples/websocket_server.rs) |\n| **Middleware** | [`middleware`](examples/middleware.rs) (transport, per-tool, per-resource, per-prompt, guards), [`rate_limiting`](examples/rate_limiting.rs), [`capability_filtering`](examples/capability_filtering.rs), [`tool_selection`](examples/tool_selection.rs) |\n| **Authentication** | [`http_auth`](examples/http_auth.rs), [`oauth_client`](examples/oauth_client.rs), [`external_api_auth`](examples/external_api_auth.rs) |\n| **Clients** | [`client_cli`](examples/client_cli.rs), [`http_client`](examples/http_client.rs), [`http_sse_client`](examples/http_sse_client.rs) |\n| **Bidirectional** | [`sampling_server`](examples/sampling_server.rs), [`client_handler`](examples/client_handler.rs) |\n| **Dynamic** | [`dynamic_capabilities`](examples/dynamic_capabilities.rs) -- runtime tool/prompt/resource registration |\n| **Advanced** | [`proxy`](examples/proxy.rs), [`resource_templates`](examples/resource_templates.rs), [`structured_output`](examples/structured_output.rs), [`error_handling`](examples/error_handling.rs), [`testing`](examples/testing.rs) |\n| **Real-world** | [`weather_server`](examples/weather_server.rs) -- external API integration |\n| **Macros** | [`tool_macro`](examples/tool_macro.rs) -- `#[tool_fn]`, `#[prompt_fn]`, `#[resource_fn]` |\n\nClone the repo and the `.mcp.json` configures example servers automatically:\n\n```bash\ngit clone https://github.com/joshrotenberg/tower-mcp\ncd tower-mcp\n# Run your MCP agent here - servers will be available automatically\n```\n\n## Development\n\n```bash\n# Format, lint, and test\ncargo fmt --all -- --check\ncargo clippy --all-targets --all-features -- -D warnings\ncargo test --all-features\n```\n\n## License\n\nMIT OR Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshrotenberg%2Ftower-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshrotenberg%2Ftower-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshrotenberg%2Ftower-mcp/lists"}