Skip to main content

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//!     .title("Search Database")
46//!     .description("Search the database")
47//!     .extractor_handler(
48//!         Arc::new(AppState { db_url: "postgres://...".into() }),
49//!         |State(app): State<Arc<AppState>>,
50//!          ctx: Context,
51//!          Json(input): Json<SearchInput>| async move {
52//!             ctx.report_progress(0.5, Some(1.0), Some("Searching...")).await;
53//!             Ok(CallToolResult::text(format!("Found results for: {}", input.query)))
54//!         },
55//!     )
56//!     .build();
57//! ```
58//!
59//! ## Quick Start: Server
60//!
61//! Build an MCP server with tools, resources, and prompts:
62//!
63//! ```rust,no_run
64//! use tower_mcp::{BoxError, McpRouter, ToolBuilder, CallToolResult, StdioTransport};
65//! use schemars::JsonSchema;
66//! use serde::Deserialize;
67//!
68//! #[derive(Debug, Deserialize, JsonSchema)]
69//! struct GreetInput {
70//!     name: String,
71//! }
72//!
73//! #[tokio::main]
74//! async fn main() -> Result<(), BoxError> {
75//!     // Define a tool
76//!     let greet = ToolBuilder::new("greet")
77//!         .title("Greet")
78//!         .description("Greet someone by name")
79//!         .handler(|input: GreetInput| async move {
80//!             Ok(CallToolResult::text(format!("Hello, {}!", input.name)))
81//!         })
82//!         .build();
83//!
84//!     // Create router and run over stdio
85//!     let router = McpRouter::new()
86//!         .server_info("my-server", "1.0.0")
87//!         .tool(greet);
88//!
89//!     StdioTransport::new(router).run().await?;
90//!     Ok(())
91//! }
92//! ```
93//!
94//! ## Quick Start: Client
95//!
96//! Connect to an MCP server and call tools:
97//!
98//! ```rust,no_run
99//! use tower_mcp::BoxError;
100//! use tower_mcp::client::{McpClient, StdioClientTransport};
101//!
102//! #[tokio::main]
103//! async fn main() -> Result<(), BoxError> {
104//!     // Connect to server
105//!     let transport = StdioClientTransport::spawn("my-mcp-server", &[]).await?;
106//!     let client = McpClient::connect(transport).await?;
107//!
108//!     // Initialize and list tools
109//!     client.initialize("my-client", "1.0.0").await?;
110//!     let tools = client.list_tools().await?;
111//!
112//!     // Call a tool
113//!     let result = client.call_tool("greet", serde_json::json!({"name": "World"})).await?;
114//!     println!("{:?}", result);
115//!
116//!     Ok(())
117//! }
118//! ```
119//!
120//! ## Key Types
121//!
122//! ### Server
123//! - [`McpRouter`] - Routes MCP requests to tools, resources, and prompts
124//! - [`ToolBuilder`] - Builder for defining tools with type-safe handlers
125//! - [`ResourceBuilder`] - Builder for defining resources
126//! - [`PromptBuilder`] - Builder for defining prompts
127//! - [`StdioTransport`] - Stdio transport for CLI servers
128//!
129//! ### Client
130//! - [`McpClient`] - Client for connecting to MCP servers
131//! - [`StdioClientTransport`] - Spawn and connect to server subprocesses
132//!
133//! ### Protocol
134//! - [`CallToolResult`] - Tool execution result with content
135//! - [`ReadResourceResult`] - Resource read result
136//! - [`GetPromptResult`] - Prompt expansion result
137//! - [`Content`] - Text, image, audio, or resource content
138//!
139//! ## Feature Flags
140//!
141//! - `full` - Enable all optional features
142//! - `http` - HTTP/SSE transport for web servers (adds axum, hyper)
143//! - `websocket` - WebSocket transport for bidirectional communication
144//! - `childproc` - Child process transport for subprocess management
145//! - `oauth` - OAuth 2.1 resource server support (JWT validation, metadata endpoint; requires `http`)
146//! - `jwks` - JWKS endpoint fetching for remote key sets (requires `oauth`)
147//! - `testing` - Test utilities (`TestClient`) for ergonomic MCP server testing
148//! - `dynamic-tools` - Runtime registration/deregistration of tools, prompts, and resources via
149//!   [`DynamicToolRegistry`], [`DynamicPromptRegistry`], [`DynamicResourceRegistry`],
150//!   [`DynamicResourceTemplateRegistry`]
151//! - `proxy` - Multi-server aggregation proxy ([`McpProxy`](proxy::McpProxy))
152//! - `http-client` - HTTP client transport for connecting to remote MCP servers
153//! - `oauth-client` - OAuth 2.0 client-side token acquisition via client credentials grant (requires `http-client`)
154//! - `macros` - Optional proc macros (`#[tool_fn]`, `#[prompt_fn]`, `#[resource_fn]`, `#[resource_template_fn]`)
155//!
156//! ## Middleware Placement Guide
157//!
158//! tower-mcp supports Tower middleware at multiple levels. Choose based on scope:
159//!
160//! | Level | Method | Scope | Use Cases |
161//! |-------|--------|-------|-----------|
162//! | **Transport** | `StdioTransport::layer()`, `HttpTransport::layer()` | All MCP requests | Global timeout, rate limit, metrics |
163//! | **axum** | `.into_router().layer()` | HTTP layer only | CORS, compression, request logging |
164//! | **Per-tool** | `ToolBuilder::...layer()` | Single tool | Tool-specific timeout, concurrency |
165//! | **Per-resource** | `ResourceBuilder::...layer()` | Single resource | Caching, read timeout |
166//! | **Per-prompt** | `PromptBuilder::...layer()` | Single prompt | Generation timeout |
167//!
168//! ### Decision Tree
169//!
170//! ```text
171//! Where should my middleware go?
172//! │
173//! ├─ Affects ALL MCP requests?
174//! │  └─ Yes → Transport: StdioTransport::layer(), HttpTransport::layer(), or WebSocketTransport::layer()
175//! │
176//! ├─ HTTP-specific (CORS, compression, headers)?
177//! │  └─ Yes → axum: transport.into_router().layer(...)
178//! │
179//! ├─ Only one specific tool?
180//! │  └─ Yes → Per-tool: ToolBuilder::...handler(...).layer(...)
181//! │
182//! ├─ Only one specific resource?
183//! │  └─ Yes → Per-resource: ResourceBuilder::...handler(...).layer(...)
184//! │
185//! └─ Only one specific prompt?
186//!    └─ Yes → Per-prompt: PromptBuilder::...handler(...).layer(...)
187//! ```
188//!
189//! ### Example: Layered Timeouts
190//!
191//! ```rust,ignore
192//! use std::time::Duration;
193//! use tower::timeout::TimeoutLayer;
194//! use tower_mcp::{McpRouter, ToolBuilder, CallToolResult, HttpTransport};
195//! use schemars::JsonSchema;
196//! use serde::Deserialize;
197//!
198//! #[derive(Debug, Deserialize, JsonSchema)]
199//! struct SearchInput { query: String }
200//!
201//! // This tool gets a longer timeout than the global default
202//! let slow_search = ToolBuilder::new("slow_search")
203//!     .description("Thorough search (may take a while)")
204//!     .handler(|input: SearchInput| async move {
205//!         // ... slow operation ...
206//!         Ok(CallToolResult::text("results"))
207//!     })
208//!     .layer(TimeoutLayer::new(Duration::from_secs(60)))  // 60s for this tool
209//!     .build();
210//!
211//! let router = McpRouter::new()
212//!     .server_info("example", "1.0.0")
213//!     .tool(slow_search);
214//!
215//! // Global 30s timeout for all OTHER requests
216//! let transport = HttpTransport::new(router)
217//!     .layer(TimeoutLayer::new(Duration::from_secs(30)));
218//! ```
219//!
220//! In this example:
221//! - `slow_search` tool has a 60-second timeout (per-tool layer)
222//! - All other MCP requests have a 30-second timeout (transport layer)
223//! - The per-tool layer is **inner** to the transport layer
224//!
225//! ### Layer Ordering
226//!
227//! Layers wrap from outside in. The first layer added is the outermost:
228//!
229//! ```text
230//! Request → [Transport Layer] → [Per-tool Layer] → Handler → Response
231//! ```
232//!
233//! For per-tool/resource/prompt, chained `.layer()` calls also wrap outside-in:
234//!
235//! ```rust,ignore
236//! ToolBuilder::new("api")
237//!     .handler(...)
238//!     .layer(TimeoutLayer::new(...))      // Outer: timeout checked first
239//!     .layer(ConcurrencyLimitLayer::new(5)) // Inner: concurrency after timeout
240//!     .build()
241//! ```
242//!
243//! ### Full Example
244//!
245//! See [`examples/tool_middleware.rs`](https://github.com/joshrotenberg/tower-mcp/blob/main/examples/tool_middleware.rs)
246//! for a complete example demonstrating:
247//! - Different timeouts per tool
248//! - Concurrency limiting for expensive operations
249//! - Multiple layers combined on a single tool
250//!
251//! ## Advanced Features
252//!
253//! ### Sampling (LLM Requests)
254//!
255//! Tools can request LLM completions from the client via [`RequestContext::sample()`].
256//! This enables AI-assisted tools like "suggest a query" or "analyze results":
257//!
258//! ```rust,ignore
259//! use tower_mcp::{ToolBuilder, CallToolResult, CreateMessageParams, SamplingMessage};
260//! use tower_mcp::extract::Context;
261//!
262//! let tool = ToolBuilder::new("suggest")
263//!     .description("Get AI suggestions")
264//!     .extractor_handler(|ctx: Context| async move {
265//!         if !ctx.can_sample() {
266//!             return Ok(CallToolResult::error("Sampling not available"));
267//!         }
268//!
269//!         let params = CreateMessageParams::new()
270//!             .message(SamplingMessage::user("Suggest 3 search queries for: rust async"))
271//!             .max_tokens(200);
272//!
273//!         let result = ctx.sample(params).await?;
274//!         let text = result.first_text().unwrap_or("No response");
275//!         Ok(CallToolResult::text(text))
276//!     })
277//!     .build();
278//! ```
279//!
280//! ### Elicitation (User Input)
281//!
282//! Tools can request user input via forms using [`RequestContext::elicit_form()`]
283//! or the convenience method [`RequestContext::confirm()`]:
284//!
285//! ```rust,ignore
286//! use tower_mcp::{ToolBuilder, CallToolResult};
287//! use tower_mcp::extract::Context;
288//!
289//! // Simple confirmation dialog
290//! let delete_tool = ToolBuilder::new("delete")
291//!     .description("Delete a file")
292//!     .extractor_handler(|ctx: Context| async move {
293//!         if !ctx.confirm("Are you sure you want to delete this file?").await? {
294//!             return Ok(CallToolResult::text("Cancelled"));
295//!         }
296//!         // ... perform deletion ...
297//!         Ok(CallToolResult::text("Deleted"))
298//!     })
299//!     .build();
300//! ```
301//!
302//! For complex forms, use [`ElicitFormSchema`] to define multiple fields.
303//!
304//! ### Progress Notifications
305//!
306//! Long-running tools can report progress via [`RequestContext::report_progress()`]:
307//!
308//! ```rust,ignore
309//! use tower_mcp::{ToolBuilder, CallToolResult};
310//! use tower_mcp::extract::Context;
311//!
312//! let process_tool = ToolBuilder::new("process")
313//!     .description("Process items")
314//!     .extractor_handler(|ctx: Context| async move {
315//!         let items = vec!["a", "b", "c", "d", "e"];
316//!         let total = items.len() as f64;
317//!
318//!         for (i, item) in items.iter().enumerate() {
319//!             ctx.report_progress(i as f64, Some(total), Some(&format!("Processing {}", item))).await;
320//!             // ... process item ...
321//!         }
322//!
323//!         Ok(CallToolResult::text("Done"))
324//!     })
325//!     .build();
326//! ```
327//!
328//! ### Router Composition
329//!
330//! Combine multiple routers using [`McpRouter::merge()`] or [`McpRouter::nest()`]:
331//!
332//! ```rust,ignore
333//! use tower_mcp::McpRouter;
334//!
335//! // Create domain-specific routers
336//! let db_router = McpRouter::new()
337//!     .tool(query_tool)
338//!     .tool(insert_tool);
339//!
340//! let api_router = McpRouter::new()
341//!     .tool(fetch_tool);
342//!
343//! // Nest with prefixes: tools become "db.query", "db.insert", "api.fetch"
344//! let combined = McpRouter::new()
345//!     .server_info("combined", "1.0")
346//!     .nest("db", db_router)
347//!     .nest("api", api_router);
348//!
349//! // Or merge without prefixes
350//! let merged = McpRouter::new()
351//!     .merge(db_router)
352//!     .merge(api_router);
353//! ```
354//!
355//! ### Multi-Server Proxy
356//!
357//! Aggregate multiple backend MCP servers behind a single endpoint using
358//! [`McpProxy`](proxy::McpProxy) (requires the `proxy` feature):
359//!
360//! ```rust,ignore
361//! use tower_mcp::proxy::McpProxy;
362//! use tower_mcp::client::StdioClientTransport;
363//!
364//! let proxy = McpProxy::builder("my-proxy", "1.0.0")
365//!     .backend("db", StdioClientTransport::spawn("db-server", &[]).await?)
366//!     .await
367//!     .backend("fs", StdioClientTransport::spawn("fs-server", &[]).await?)
368//!     .await
369//!     .build()
370//!     .await?;
371//!
372//! // Tools become `db_query`, `fs_read`, etc.
373//! // Serve over any transport -- stdio, HTTP, WebSocket.
374//! GenericStdioTransport::new(proxy).run().await?;
375//! ```
376//!
377//! The proxy supports per-backend Tower middleware, notification forwarding,
378//! health checks, and request coalescing. See the [`proxy`] module for details.
379//!
380//! ## MCP Specification
381//!
382//! This crate implements the MCP specification (2025-11-25):
383//! <https://modelcontextprotocol.io/specification/2025-11-25>
384
385pub mod async_task;
386pub mod auth;
387pub mod client;
388pub mod context;
389pub mod error;
390pub mod extract;
391pub mod filter;
392pub mod jsonrpc;
393pub mod middleware;
394#[cfg(feature = "oauth")]
395pub mod oauth;
396pub mod prompt;
397pub mod protocol;
398#[cfg(feature = "proxy")]
399pub mod proxy;
400#[cfg(feature = "dynamic-tools")]
401pub mod registry;
402pub mod resource;
403pub mod router;
404pub mod session;
405#[cfg(feature = "testing")]
406pub mod testing;
407pub mod tool;
408pub mod tracing_layer;
409pub mod transport;
410
411// Re-export proc macros when the `macros` feature is enabled
412#[cfg(feature = "macros")]
413pub use tower_mcp_macros::prompt_fn;
414#[cfg(feature = "macros")]
415pub use tower_mcp_macros::resource_fn;
416#[cfg(feature = "macros")]
417pub use tower_mcp_macros::resource_template_fn;
418#[cfg(feature = "macros")]
419pub use tower_mcp_macros::tool_fn;
420
421// Re-exports
422pub use async_task::{Task, TaskStore};
423pub use client::{
424    ChannelTransport, ClientHandler, ClientTransport, McpClient, McpClientBuilder,
425    NotificationHandler, StdioClientTransport,
426};
427#[cfg(feature = "http-client")]
428pub use client::{HttpClientConfig, HttpClientTransport};
429#[cfg(feature = "oauth-client")]
430pub use client::{OAuthClientCredentials, OAuthClientError, TokenProvider};
431pub use context::{
432    ChannelClientRequester, ClientRequester, ClientRequesterHandle, Extensions,
433    NotificationReceiver, NotificationSender, OutgoingRequest, OutgoingRequestReceiver,
434    OutgoingRequestSender, RequestContext, RequestContextBuilder, ServerNotification,
435    outgoing_request_channel,
436};
437pub use error::{BoxError, Error, Result, ResultExt, ToolError};
438pub use filter::{
439    CapabilityFilter, DenialBehavior, Filterable, PromptFilter, ResourceFilter, ToolFilter,
440};
441pub use jsonrpc::{JsonRpcLayer, JsonRpcService};
442pub use middleware::{
443    AuditLayer, AuditService, McpTracingLayer, McpTracingService, ToolCallLoggingLayer,
444    ToolCallLoggingService,
445};
446pub use prompt::{BoxPromptService, Prompt, PromptBuilder, PromptHandler, PromptRequest};
447#[allow(deprecated)]
448pub use protocol::{
449    BooleanSchema, CallToolParams, CallToolResult, CancelTaskParams, CancelledParams,
450    ClientCapabilities, ClientTasksCancelCapability, ClientTasksCapability,
451    ClientTasksElicitationCapability, ClientTasksElicitationCreateCapability,
452    ClientTasksListCapability, ClientTasksRequestsCapability, ClientTasksSamplingCapability,
453    ClientTasksSamplingCreateMessageCapability, CompleteParams, CompleteResult, Completion,
454    CompletionArgument, CompletionContext, CompletionReference, CompletionsCapability, Content,
455    ContentAnnotations, ContentRole, CreateMessageParams, CreateMessageResult, CreateTaskResult,
456    ElicitAction, ElicitFieldValue, ElicitFormParams, ElicitFormSchema, ElicitMode,
457    ElicitRequestParams, ElicitResult, ElicitUrlParams, ElicitationCapability,
458    ElicitationCompleteParams, ElicitationFormCapability, ElicitationUrlCapability, EmptyResult,
459    GetPromptParams, GetPromptResult, GetPromptResultBuilder, GetTaskInfoParams,
460    GetTaskResultParams, IconTheme, Implementation, IncludeContext, InitializeParams,
461    InitializeResult, IntegerSchema, JsonRpcErrorResponse, JsonRpcMessage, JsonRpcNotification,
462    JsonRpcRequest, JsonRpcResponse, JsonRpcResponseMessage, JsonRpcResultResponse,
463    ListPromptsParams, ListPromptsResult, ListResourceTemplatesParams, ListResourceTemplatesResult,
464    ListResourcesParams, ListResourcesResult, ListRootsParams, ListRootsResult, ListTasksParams,
465    ListTasksResult, ListToolsParams, ListToolsResult, LogLevel, LoggingCapability,
466    LoggingMessageParams, McpNotification, McpRequest, McpResponse, ModelHint, ModelPreferences,
467    MultiSelectEnumItems, MultiSelectEnumSchema, NumberSchema, PrimitiveSchemaDefinition,
468    ProgressParams, ProgressToken, PromptArgument, PromptDefinition, PromptMessage,
469    PromptReference, PromptRole, PromptsCapability, ReadResourceParams, ReadResourceResult,
470    RequestId, RequestMeta, ResourceContent, ResourceDefinition, ResourceReference,
471    ResourceTemplateDefinition, ResourcesCapability, Root, RootsCapability, SamplingCapability,
472    SamplingContent, SamplingContentOrArray, SamplingContextCapability, SamplingMessage,
473    SamplingTool, SamplingToolsCapability, ServerCapabilities, SetLogLevelParams,
474    SingleSelectEnumSchema, StringSchema, SubscribeResourceParams, TaskInfo, TaskObject,
475    TaskRequestParams, TaskStatus, TaskStatusChangedParams, TaskStatusParams, TaskSupportMode,
476    TasksCancelCapability, TasksCapability, TasksListCapability, TasksRequestsCapability,
477    TasksToolsCallCapability, TasksToolsRequestsCapability, ToolAnnotations, ToolChoice,
478    ToolDefinition, ToolExecution, ToolIcon, ToolsCapability, UnsubscribeResourceParams,
479};
480#[cfg(feature = "dynamic-tools")]
481pub use registry::{
482    DynamicPromptRegistry, DynamicResourceRegistry, DynamicResourceTemplateRegistry,
483    DynamicToolRegistry,
484};
485pub use resource::{
486    BoxResourceService, Resource, ResourceBuilder, ResourceHandler, ResourceRequest,
487    ResourceTemplate, ResourceTemplateBuilder, ResourceTemplateHandler,
488};
489pub use router::{McpRouter, RouterRequest, RouterResponse, ToolAnnotationsMap};
490pub use session::{SessionPhase, SessionState};
491pub use tool::{BoxToolService, GuardLayer, NoParams, Tool, ToolBuilder, ToolHandler, ToolRequest};
492pub use transport::{
493    BidirectionalStdioTransport, CatchError, GenericStdioTransport, StdioTransport,
494    SyncStdioTransport,
495};
496
497#[cfg(feature = "http")]
498pub use transport::{HttpTransport, SessionHandle, SessionInfo};
499
500#[cfg(feature = "websocket")]
501pub use transport::WebSocketTransport;
502
503#[cfg(any(feature = "http", feature = "websocket"))]
504pub use transport::McpBoxService;
505
506#[cfg(feature = "childproc")]
507pub use transport::{ChildProcessConnection, ChildProcessTransport};
508
509#[cfg(feature = "oauth")]
510pub use oauth::{ScopeEnforcementLayer, ScopeEnforcementService};
511
512#[cfg(feature = "jwks")]
513pub use oauth::{JwksError, JwksValidator, JwksValidatorBuilder};
514
515#[cfg(feature = "testing")]
516pub use testing::TestClient;