tower_mcp/lib.rs
1//! # tower-mcp
2//!
3//! Tower-native Model Context Protocol (MCP) implementation for Rust.
4//!
5//! This crate provides a composable, middleware-friendly approach to building
6//! MCP servers and clients using the [Tower](https://docs.rs/tower) service abstraction.
7//!
8//! ## Philosophy
9//!
10//! Unlike framework-style MCP implementations, tower-mcp treats MCP as just another
11//! protocol that can be served through Tower's `Service` trait. This means:
12//!
13//! - Standard tower middleware (tracing, metrics, rate limiting, auth) just works
14//! - Same service can be exposed over multiple transports (stdio, HTTP, WebSocket)
15//! - Easy integration with existing tower-based applications (axum, tonic, etc.)
16//!
17//! ## Familiar to axum Users
18//!
19//! If you've used [axum](https://docs.rs/axum), tower-mcp's API will feel familiar.
20//! We've adopted axum's patterns for a consistent Rust web ecosystem experience:
21//!
22//! - **Extractor pattern**: Tool handlers use extractors like [`extract::State<T>`],
23//! [`extract::Json<T>`], and [`extract::Context`] - just like axum's request extractors
24//! - **Router composition**: [`McpRouter::merge()`] and [`McpRouter::nest()`] work like
25//! axum's router methods for combining routers
26//! - **Per-route middleware**: Apply Tower layers to individual tools, resources, or
27//! prompts via `.layer()` on builders
28//! - **Builder pattern**: Fluent builders for tools, resources, and prompts
29//!
30//! ```rust
31//! use std::sync::Arc;
32//! use tower_mcp::{ToolBuilder, CallToolResult};
33//! use tower_mcp::extract::{State, Json, Context};
34//! use schemars::JsonSchema;
35//! use serde::Deserialize;
36//!
37//! #[derive(Clone)]
38//! struct AppState { db_url: String }
39//!
40//! #[derive(Deserialize, JsonSchema)]
41//! struct SearchInput { query: String }
42//!
43//! // Looks just like an axum handler!
44//! let tool = ToolBuilder::new("search")
45//! .description("Search the database")
46//! .extractor_handler(
47//! Arc::new(AppState { db_url: "postgres://...".into() }),
48//! |State(app): State<Arc<AppState>>,
49//! ctx: Context,
50//! Json(input): Json<SearchInput>| async move {
51//! ctx.report_progress(0.5, Some(1.0), Some("Searching...")).await;
52//! Ok(CallToolResult::text(format!("Found results for: {}", input.query)))
53//! },
54//! )
55//! .build();
56//! ```
57//!
58//! ## Quick Start: Server
59//!
60//! Build an MCP server with tools, resources, and prompts:
61//!
62//! ```rust,no_run
63//! use tower_mcp::{BoxError, McpRouter, ToolBuilder, CallToolResult, StdioTransport};
64//! use schemars::JsonSchema;
65//! use serde::Deserialize;
66//!
67//! #[derive(Debug, Deserialize, JsonSchema)]
68//! struct GreetInput {
69//! name: String,
70//! }
71//!
72//! #[tokio::main]
73//! async fn main() -> Result<(), BoxError> {
74//! // Define a tool
75//! let greet = ToolBuilder::new("greet")
76//! .description("Greet someone by name")
77//! .handler(|input: GreetInput| async move {
78//! Ok(CallToolResult::text(format!("Hello, {}!", input.name)))
79//! })
80//! .build();
81//!
82//! // Create router and run over stdio
83//! let router = McpRouter::new()
84//! .server_info("my-server", "1.0.0")
85//! .tool(greet);
86//!
87//! StdioTransport::new(router).run().await?;
88//! Ok(())
89//! }
90//! ```
91//!
92//! ## Quick Start: Client
93//!
94//! Connect to an MCP server and call tools:
95//!
96//! ```rust,no_run
97//! use tower_mcp::BoxError;
98//! use tower_mcp::client::{McpClient, StdioClientTransport};
99//!
100//! #[tokio::main]
101//! async fn main() -> Result<(), BoxError> {
102//! // Connect to server
103//! let transport = StdioClientTransport::spawn("my-mcp-server", &[]).await?;
104//! let mut client = McpClient::new(transport);
105//!
106//! // Initialize and list tools
107//! client.initialize("my-client", "1.0.0").await?;
108//! let tools = client.list_tools().await?;
109//!
110//! // Call a tool
111//! let result = client.call_tool("greet", serde_json::json!({"name": "World"})).await?;
112//! println!("{:?}", result);
113//!
114//! Ok(())
115//! }
116//! ```
117//!
118//! ## Key Types
119//!
120//! ### Server
121//! - [`McpRouter`] - Routes MCP requests to tools, resources, and prompts
122//! - [`ToolBuilder`] - Builder for defining tools with type-safe handlers
123//! - [`ResourceBuilder`] - Builder for defining resources
124//! - [`PromptBuilder`] - Builder for defining prompts
125//! - [`StdioTransport`] - Stdio transport for CLI servers
126//!
127//! ### Client
128//! - [`McpClient`] - Client for connecting to MCP servers
129//! - [`StdioClientTransport`] - Spawn and connect to server subprocesses
130//!
131//! ### Protocol
132//! - [`CallToolResult`] - Tool execution result with content
133//! - [`ReadResourceResult`] - Resource read result
134//! - [`GetPromptResult`] - Prompt expansion result
135//! - [`Content`] - Text, image, audio, or resource content
136//!
137//! ## Feature Flags
138//!
139//! - `full` - Enable all optional features
140//! - `http` - HTTP/SSE transport for web servers
141//! - `websocket` - WebSocket transport for bidirectional communication
142//! - `childproc` - Child process transport for subprocess management
143//! - `oauth` - OAuth 2.1 resource server support (token validation, metadata endpoint)
144//! - `testing` - Test utilities ([`TestClient`]) for ergonomic MCP server testing
145//!
146//! ## Middleware Placement Guide
147//!
148//! tower-mcp supports Tower middleware at multiple levels. Choose based on scope:
149//!
150//! | Level | Method | Scope | Use Cases |
151//! |-------|--------|-------|-----------|
152//! | **Transport** | `HttpTransport::layer()` | All MCP requests | Global timeout, rate limit, metrics |
153//! | **axum** | `.into_router().layer()` | HTTP layer only | CORS, compression, request logging |
154//! | **Per-tool** | `ToolBuilder::...layer()` | Single tool | Tool-specific timeout, concurrency |
155//! | **Per-resource** | `ResourceBuilder::...layer()` | Single resource | Caching, read timeout |
156//! | **Per-prompt** | `PromptBuilder::...layer()` | Single prompt | Generation timeout |
157//!
158//! ### Decision Tree
159//!
160//! ```text
161//! Where should my middleware go?
162//! │
163//! ├─ Affects ALL MCP requests?
164//! │ └─ Yes → Transport: HttpTransport::layer() or WebSocketTransport::layer()
165//! │
166//! ├─ HTTP-specific (CORS, compression, headers)?
167//! │ └─ Yes → axum: transport.into_router().layer(...)
168//! │
169//! ├─ Only one specific tool?
170//! │ └─ Yes → Per-tool: ToolBuilder::...handler(...).layer(...)
171//! │
172//! ├─ Only one specific resource?
173//! │ └─ Yes → Per-resource: ResourceBuilder::...handler(...).layer(...)
174//! │
175//! └─ Only one specific prompt?
176//! └─ Yes → Per-prompt: PromptBuilder::...handler(...).layer(...)
177//! ```
178//!
179//! ### Example: Layered Timeouts
180//!
181//! ```rust,ignore
182//! use std::time::Duration;
183//! use tower::timeout::TimeoutLayer;
184//! use tower_mcp::{McpRouter, ToolBuilder, CallToolResult, HttpTransport};
185//! use schemars::JsonSchema;
186//! use serde::Deserialize;
187//!
188//! #[derive(Debug, Deserialize, JsonSchema)]
189//! struct SearchInput { query: String }
190//!
191//! // This tool gets a longer timeout than the global default
192//! let slow_search = ToolBuilder::new("slow_search")
193//! .description("Thorough search (may take a while)")
194//! .handler(|input: SearchInput| async move {
195//! // ... slow operation ...
196//! Ok(CallToolResult::text("results"))
197//! })
198//! .layer(TimeoutLayer::new(Duration::from_secs(60))) // 60s for this tool
199//! .build();
200//!
201//! let router = McpRouter::new()
202//! .server_info("example", "1.0.0")
203//! .tool(slow_search);
204//!
205//! // Global 30s timeout for all OTHER requests
206//! let transport = HttpTransport::new(router)
207//! .layer(TimeoutLayer::new(Duration::from_secs(30)));
208//! ```
209//!
210//! In this example:
211//! - `slow_search` tool has a 60-second timeout (per-tool layer)
212//! - All other MCP requests have a 30-second timeout (transport layer)
213//! - The per-tool layer is **inner** to the transport layer
214//!
215//! ### Layer Ordering
216//!
217//! Layers wrap from outside in. The first layer added is the outermost:
218//!
219//! ```text
220//! Request → [Transport Layer] → [Per-tool Layer] → Handler → Response
221//! ```
222//!
223//! For per-tool/resource/prompt, chained `.layer()` calls also wrap outside-in:
224//!
225//! ```rust,ignore
226//! ToolBuilder::new("api")
227//! .handler(...)
228//! .layer(TimeoutLayer::new(...)) // Outer: timeout checked first
229//! .layer(ConcurrencyLimitLayer::new(5)) // Inner: concurrency after timeout
230//! .build()
231//! ```
232//!
233//! ### Full Example
234//!
235//! See [`examples/tool_middleware.rs`](https://github.com/joshrotenberg/tower-mcp/blob/main/examples/tool_middleware.rs)
236//! for a complete example demonstrating:
237//! - Different timeouts per tool
238//! - Concurrency limiting for expensive operations
239//! - Multiple layers combined on a single tool
240//!
241//! ## Advanced Features
242//!
243//! ### Sampling (LLM Requests)
244//!
245//! Tools can request LLM completions from the client via [`RequestContext::sample()`].
246//! This enables AI-assisted tools like "suggest a query" or "analyze results":
247//!
248//! ```rust,ignore
249//! use tower_mcp::{ToolBuilder, CallToolResult, CreateMessageParams, SamplingMessage};
250//! use tower_mcp::extract::Context;
251//!
252//! let tool = ToolBuilder::new("suggest")
253//! .description("Get AI suggestions")
254//! .extractor_handler(|ctx: Context| async move {
255//! if !ctx.can_sample() {
256//! return Ok(CallToolResult::error("Sampling not available"));
257//! }
258//!
259//! let params = CreateMessageParams::new()
260//! .message(SamplingMessage::user("Suggest 3 search queries for: rust async"))
261//! .max_tokens(200);
262//!
263//! let result = ctx.sample(params).await?;
264//! let text = result.first_text().unwrap_or("No response");
265//! Ok(CallToolResult::text(text))
266//! })
267//! .build();
268//! ```
269//!
270//! ### Elicitation (User Input)
271//!
272//! Tools can request user input via forms using [`RequestContext::elicit_form()`]
273//! or the convenience method [`RequestContext::confirm()`]:
274//!
275//! ```rust,ignore
276//! use tower_mcp::{ToolBuilder, CallToolResult};
277//! use tower_mcp::extract::Context;
278//!
279//! // Simple confirmation dialog
280//! let delete_tool = ToolBuilder::new("delete")
281//! .description("Delete a file")
282//! .extractor_handler(|ctx: Context| async move {
283//! if !ctx.confirm("Are you sure you want to delete this file?").await? {
284//! return Ok(CallToolResult::text("Cancelled"));
285//! }
286//! // ... perform deletion ...
287//! Ok(CallToolResult::text("Deleted"))
288//! })
289//! .build();
290//! ```
291//!
292//! For complex forms, use [`ElicitFormSchema`] to define multiple fields.
293//!
294//! ### Progress Notifications
295//!
296//! Long-running tools can report progress via [`RequestContext::report_progress()`]:
297//!
298//! ```rust,ignore
299//! use tower_mcp::{ToolBuilder, CallToolResult};
300//! use tower_mcp::extract::Context;
301//!
302//! let process_tool = ToolBuilder::new("process")
303//! .description("Process items")
304//! .extractor_handler(|ctx: Context| async move {
305//! let items = vec!["a", "b", "c", "d", "e"];
306//! let total = items.len() as f64;
307//!
308//! for (i, item) in items.iter().enumerate() {
309//! ctx.report_progress(i as f64, Some(total), Some(&format!("Processing {}", item))).await;
310//! // ... process item ...
311//! }
312//!
313//! Ok(CallToolResult::text("Done"))
314//! })
315//! .build();
316//! ```
317//!
318//! ### Router Composition
319//!
320//! Combine multiple routers using [`McpRouter::merge()`] or [`McpRouter::nest()`]:
321//!
322//! ```rust,ignore
323//! use tower_mcp::McpRouter;
324//!
325//! // Create domain-specific routers
326//! let db_router = McpRouter::new()
327//! .tool(query_tool)
328//! .tool(insert_tool);
329//!
330//! let api_router = McpRouter::new()
331//! .tool(fetch_tool);
332//!
333//! // Nest with prefixes: tools become "db.query", "db.insert", "api.fetch"
334//! let combined = McpRouter::new()
335//! .server_info("combined", "1.0")
336//! .nest("db", db_router)
337//! .nest("api", api_router);
338//!
339//! // Or merge without prefixes
340//! let merged = McpRouter::new()
341//! .merge(db_router)
342//! .merge(api_router);
343//! ```
344//!
345//! ## MCP Specification
346//!
347//! This crate implements the MCP specification (2025-11-25):
348//! <https://modelcontextprotocol.io/specification/2025-11-25>
349
350pub mod async_task;
351pub mod auth;
352pub mod client;
353pub mod context;
354pub mod error;
355pub mod extract;
356pub mod filter;
357pub mod jsonrpc;
358#[cfg(feature = "oauth")]
359pub mod oauth;
360pub mod prompt;
361pub mod protocol;
362pub mod resource;
363pub mod router;
364pub mod session;
365#[cfg(feature = "testing")]
366pub mod testing;
367pub mod tool;
368pub mod tracing_layer;
369pub mod transport;
370
371// Re-exports
372pub use async_task::{Task, TaskStore};
373pub use client::{ClientTransport, McpClient, StdioClientTransport};
374pub use context::{
375 ChannelClientRequester, ClientRequester, ClientRequesterHandle, Extensions,
376 NotificationReceiver, NotificationSender, OutgoingRequest, OutgoingRequestReceiver,
377 OutgoingRequestSender, RequestContext, RequestContextBuilder, ServerNotification,
378 outgoing_request_channel,
379};
380pub use error::{BoxError, Error, Result, ResultExt, ToolError};
381pub use filter::{
382 CapabilityFilter, DenialBehavior, Filterable, PromptFilter, ResourceFilter, ToolFilter,
383};
384pub use jsonrpc::{JsonRpcLayer, JsonRpcService};
385pub use prompt::{BoxPromptService, Prompt, PromptBuilder, PromptHandler, PromptRequest};
386pub use protocol::{
387 CallToolResult, CompleteParams, CompleteResult, Completion, CompletionArgument,
388 CompletionReference, CompletionsCapability, Content, ContentRole, CreateMessageParams,
389 CreateMessageResult, ElicitAction, ElicitFieldValue, ElicitFormParams, ElicitFormSchema,
390 ElicitMode, ElicitRequestParams, ElicitResult, ElicitUrlParams, ElicitationCapability,
391 ElicitationCompleteParams, GetPromptResult, GetPromptResultBuilder, IncludeContext,
392 JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, JsonRpcResponseMessage, ListRootsParams,
393 ListRootsResult, McpRequest, McpResponse, ModelHint, ModelPreferences,
394 PrimitiveSchemaDefinition, PromptMessage, PromptReference, PromptRole, ReadResourceResult,
395 ResourceContent, ResourceReference, Root, RootsCapability, SamplingCapability, SamplingContent,
396 SamplingContentOrArray, SamplingMessage, SamplingTool, ToolChoice,
397};
398pub use resource::{
399 BoxResourceService, Resource, ResourceBuilder, ResourceHandler, ResourceRequest,
400 ResourceTemplate, ResourceTemplateBuilder, ResourceTemplateHandler,
401};
402pub use router::{McpRouter, RouterRequest, RouterResponse};
403pub use session::{SessionPhase, SessionState};
404pub use tool::{BoxToolService, GuardLayer, NoParams, Tool, ToolBuilder, ToolHandler, ToolRequest};
405pub use tracing_layer::{McpTracingLayer, McpTracingService};
406pub use transport::{
407 BidirectionalStdioTransport, CatchError, GenericStdioTransport, StdioTransport,
408 SyncStdioTransport,
409};
410
411#[cfg(feature = "http")]
412pub use transport::HttpTransport;
413
414#[cfg(feature = "websocket")]
415pub use transport::WebSocketTransport;
416
417#[cfg(any(feature = "http", feature = "websocket"))]
418pub use transport::McpBoxService;
419
420#[cfg(feature = "childproc")]
421pub use transport::{ChildProcessConnection, ChildProcessTransport};
422
423#[cfg(feature = "oauth")]
424pub use oauth::{ScopeEnforcementLayer, ScopeEnforcementService};
425
426#[cfg(feature = "jwks")]
427pub use oauth::{JwksError, JwksValidator, JwksValidatorBuilder};
428
429#[cfg(feature = "testing")]
430pub use testing::TestClient;