tower_mcp/router.rs
1//! MCP Router - routes requests to tools, resources, and prompts
2//!
3//! The router implements Tower's `Service` trait, making it composable with
4//! standard tower middleware.
5
6use std::collections::{HashMap, HashSet};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
10use std::sync::{Arc, RwLock};
11use std::task::{Context, Poll};
12
13use tower_service::Service;
14
15use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
16
17use crate::async_task::{MemoryTaskStore, TaskStore, TaskStoreError};
18use crate::context::{
19 CancellationToken, ClientRequesterHandle, NotificationSender, RequestContext,
20 ServerNotification,
21};
22use crate::error::{Error, JsonRpcError, Result};
23use crate::filter::{
24 CapabilityFilterContext, CapabilityOperation, PromptFilter, ResourceFilter,
25 ResourceTemplateFilter, ToolFilter,
26};
27use crate::prompt::Prompt;
28use crate::protocol::*;
29#[cfg(feature = "dynamic-tools")]
30use crate::registry::{
31 DynamicPromptRegistry, DynamicPromptsInner, DynamicResourceRegistry,
32 DynamicResourceTemplateRegistry, DynamicResourceTemplatesInner, DynamicResourcesInner,
33 DynamicToolRegistry, DynamicToolsInner,
34};
35use crate::resource::{Resource, ResourceTemplate};
36use crate::session::SessionState;
37use crate::tool::Tool;
38
39/// Type alias for completion handler function
40pub(crate) type CompletionHandler = Arc<
41 dyn Fn(
42 RequestContext,
43 CompleteParams,
44 ) -> Pin<Box<dyn Future<Output = Result<CompleteResult>> + Send>>
45 + Send
46 + Sync,
47>;
48
49fn prompt_not_found(name: &str) -> Error {
50 Error::JsonRpc(JsonRpcError::method_not_found(&format!(
51 "Prompt not found: {name}"
52 )))
53}
54
55/// Whether this request is using the final, stateless 2026-07-28 lifecycle.
56///
57/// Stable sessionful requests retain the crate's legacy task behavior; final
58/// requests use extension negotiation and server-directed task creation.
59#[cfg(feature = "stateless")]
60fn is_final_protocol_request(extensions: &crate::context::Extensions) -> bool {
61 extensions
62 .get::<crate::stateless::StatelessRequestMeta>()
63 .and_then(|meta| meta.protocol_version.as_deref())
64 == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
65}
66
67#[cfg(not(feature = "stateless"))]
68fn is_final_protocol_request(_extensions: &crate::context::Extensions) -> bool {
69 false
70}
71
72/// MCP Router that dispatches requests to registered handlers
73///
74/// Implements `tower::Service<McpRequest>` for middleware composition.
75///
76/// # Example
77///
78/// ```rust
79/// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
80/// use schemars::JsonSchema;
81/// use serde::Deserialize;
82///
83/// #[derive(Debug, Deserialize, JsonSchema)]
84/// struct Input { value: String }
85///
86/// let tool = ToolBuilder::new("echo")
87/// .description("Echo input")
88/// .handler(|i: Input| async move { Ok(CallToolResult::text(i.value)) })
89/// .build();
90///
91/// let router = McpRouter::new()
92/// .server_info("my-server", "1.0.0")
93/// .tool(tool);
94/// ```
95#[derive(Clone)]
96pub struct McpRouter {
97 inner: Arc<McpRouterInner>,
98 session: SessionState,
99 /// Legacy `resources/subscribe` membership for this logical session.
100 ///
101 /// Ordinary router clones share this state because transports clone a
102 /// session router for each request. [`Self::with_fresh_session`] replaces
103 /// it so one client's membership cannot affect another client.
104 subscriptions: Arc<RwLock<HashSet<String>>>,
105}
106
107impl std::fmt::Debug for McpRouter {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("McpRouter")
110 .field("server_name", &self.inner.server_name)
111 .field("server_version", &self.inner.server_version)
112 .field("tools_count", &self.inner.tools.len())
113 .field("resources_count", &self.inner.resources.len())
114 .field("prompts_count", &self.inner.prompts.len())
115 .field("session_phase", &self.session.phase())
116 .finish()
117 }
118}
119
120/// Configuration for auto-generated instructions
121#[derive(Clone, Debug)]
122struct AutoInstructionsConfig {
123 prefix: Option<String>,
124 suffix: Option<String>,
125}
126
127#[cfg(all(feature = "http", feature = "stateless"))]
128type ModernNotificationSink = Arc<dyn Fn(&ServerNotification) -> bool + Send + Sync + 'static>;
129
130#[cfg(feature = "dynamic-tools")]
131type PromptInitializer = Arc<dyn Fn() -> Result<()> + Send + Sync + 'static>;
132
133/// Inner configuration that is shared across clones
134#[derive(Clone)]
135struct McpRouterInner {
136 server_name: String,
137 server_version: String,
138 /// Human-readable title for the server
139 server_title: Option<String>,
140 /// Description of the server
141 server_description: Option<String>,
142 /// Icons for the server
143 server_icons: Option<Vec<ToolIcon>>,
144 /// URL of the server's website
145 server_website_url: Option<String>,
146 instructions: Option<String>,
147 /// How to convert a panicking tool handler into an error result rather
148 /// than letting it unwind out of the service (#1230, #1306).
149 panic_policy: Option<PanicPolicy>,
150 /// Root-owned mapping for client-visible Task lifecycle failures.
151 task_error_policy: TaskErrorPolicy,
152 /// Root-owned mapping from request extensions to a durable Task owner.
153 task_owner_resolver: TaskOwnerResolver,
154 auto_instructions: Option<AutoInstructionsConfig>,
155 tools: HashMap<String, Arc<Tool>>,
156 resources: HashMap<String, Arc<Resource>>,
157 /// Resource templates for dynamic resource matching (keyed by uri_template)
158 resource_templates: Vec<Arc<ResourceTemplate>>,
159 prompts: HashMap<String, Arc<Prompt>>,
160 /// Whether to advertise `resources.subscribe`. Defaults to true, which
161 /// is what this router has always advertised when resources exist (#1261).
162 advertise_resource_subscriptions: bool,
163 /// Explicit override for whether to advertise `tools.listChanged`.
164 /// `None` derives from whether a notification channel is attached, which
165 /// is what this router has always advertised (#1338).
166 advertise_tools_list_changed: Option<bool>,
167 /// Explicit override for whether to advertise `prompts.listChanged`.
168 /// `None` derives from whether a notification channel is attached, which
169 /// is what this router has always advertised (#1338).
170 advertise_prompts_list_changed: Option<bool>,
171 /// Explicit override for whether to advertise `resources.listChanged`.
172 /// `None` derives from whether a notification channel is attached, which
173 /// is what this router has always advertised (#1338).
174 advertise_resources_list_changed: Option<bool>,
175 /// Explicit override for whether to advertise the `logging` capability.
176 /// `None` derives from whether a notification channel is attached, which
177 /// is what this router has always advertised (#1338).
178 advertise_mcp_logging: Option<bool>,
179 /// Admission, cancellation, and drain boundary for built-in live task
180 /// handlers. Shared by every clone and fresh logical session.
181 live_task_executions: crate::LiveTaskExecutionHandle,
182 /// In-flight requests for cancellation tracking (shared across clones).
183 ///
184 /// Keyed by request id for lookup, but each id holds one entry per
185 /// *dispatch*. A client should not reuse an id that is still in flight,
186 /// but when one does, the twins have to coexist: keyed by id alone the
187 /// second registration evicted the first and the first became
188 /// uncancellable (#1270).
189 in_flight: Arc<RwLock<HashMap<RequestId, Vec<InFlightDispatch>>>>,
190 /// Source of the per-dispatch ids in `in_flight`, shared across clones.
191 next_dispatch: Arc<AtomicU64>,
192 /// Channel for sending notifications to connected clients
193 notification_tx: Option<NotificationSender>,
194 /// Transport-lifetime sink for final HTTP subscription notifications.
195 ///
196 /// The lock is shared across router clones so an application-owned clone
197 /// can publish after the transport attaches its subscription registry.
198 #[cfg(all(feature = "http", feature = "stateless"))]
199 modern_notification_sink: Arc<RwLock<Option<ModernNotificationSink>>>,
200 #[cfg(feature = "stateless")]
201 subscription_observer:
202 Arc<RwLock<Option<Arc<dyn crate::transport::subscriptions::SubscriptionObserver>>>>,
203 /// Handle for sending requests to the client (for sampling, etc.)
204 client_requester: Option<ClientRequesterHandle>,
205 /// Task store for async operations
206 task_store: Arc<dyn TaskStore>,
207 /// Handler for completion requests
208 completion_handler: Option<CompletionHandler>,
209 /// Filter for tools based on session state
210 tool_filter: Option<ToolFilter>,
211 /// Filter for resources based on session state
212 resource_filter: Option<ResourceFilter>,
213 /// Filter for resource templates and their concrete resolved URIs
214 resource_template_filter: Option<ResourceTemplateFilter>,
215 /// Filter for prompts based on session state
216 prompt_filter: Option<PromptFilter>,
217 /// Router-level extensions (for state and middleware data)
218 extensions: Arc<crate::context::Extensions>,
219 /// Locally supported MCP protocol extensions and their server settings.
220 protocol_extensions: HashMap<String, serde_json::Value>,
221 /// Minimum log level for filtering outgoing log notifications (set by client via logging/setLevel)
222 min_log_level: Arc<RwLock<LogLevel>>,
223 /// Page size for list method pagination (None = return all results)
224 page_size: Option<usize>,
225 /// TTL hint for list responses in milliseconds (SEP-2549).
226 /// When set, the value is returned as `ttlMs` in tools/list, resources/list,
227 /// and prompts/list responses so clients can cache the list.
228 list_ttl_ms: Option<u64>,
229 /// Default TTL hint for resources/read responses in milliseconds
230 /// (SEP-2549). Applied only when the resource handler did not set its
231 /// own `ttl_ms` on the result.
232 read_ttl_ms: Option<u64>,
233 /// Cache scope for SEP-2549 hints on list and read responses. When a
234 /// TTL is emitted and no scope is configured, `private` is used: it is
235 /// the conservative choice (never shared across authorization
236 /// contexts).
237 cache_scope: Option<CacheScope>,
238 /// Deprecation info for the logging capability (SEP-2577).
239 /// When set, included in the `logging` capability in the initialize result.
240 logging_deprecated: Option<tower_mcp_types::protocol::DeprecationInfo>,
241 /// Names of tools that are currently disabled (hidden from list/call).
242 disabled_tools: Arc<RwLock<HashSet<String>>>,
243 /// URIs of resources that are currently disabled (hidden from list/read).
244 disabled_resources: Arc<RwLock<HashSet<String>>>,
245 /// Names of prompts that are currently disabled (hidden from list/get).
246 disabled_prompts: Arc<RwLock<HashSet<String>>>,
247 /// Dynamic tools registry for runtime tool (de)registration
248 #[cfg(feature = "dynamic-tools")]
249 dynamic_tools: Option<Arc<DynamicToolsInner>>,
250 /// Dynamic prompts registry for runtime prompt (de)registration
251 #[cfg(feature = "dynamic-tools")]
252 dynamic_prompts: Option<Arc<DynamicPromptsInner>>,
253 /// Lazily populates the dynamic prompt registry before list/get access.
254 #[cfg(feature = "dynamic-tools")]
255 prompt_initializer: Option<PromptInitializer>,
256 /// Dynamic resources registry for runtime resource (de)registration
257 #[cfg(feature = "dynamic-tools")]
258 dynamic_resources: Option<Arc<DynamicResourcesInner>>,
259 /// Dynamic resource templates registry for runtime template (de)registration
260 #[cfg(feature = "dynamic-tools")]
261 dynamic_resource_templates: Option<Arc<DynamicResourceTemplatesInner>>,
262}
263
264impl McpRouter {
265 fn capability_filter_context<'a>(
266 &'a self,
267 request_extensions: &Extensions,
268 operation: CapabilityOperation<'a>,
269 ) -> CapabilityFilterContext<'a> {
270 CapabilityFilterContext::new(
271 &self.session,
272 &self.inner.extensions,
273 request_extensions,
274 operation,
275 )
276 }
277
278 fn resource_template_is_visible(
279 &self,
280 context: &CapabilityFilterContext<'_>,
281 template: &ResourceTemplate,
282 ) -> bool {
283 match &self.inner.resource_template_filter {
284 Some(filter) => filter.is_visible_with_context(context, template),
285 None => self.inner.resource_filter.is_none(),
286 }
287 }
288
289 fn authorize_resource_template(
290 &self,
291 context: &CapabilityFilterContext<'_>,
292 template: &ResourceTemplate,
293 concrete_uri: &str,
294 ) -> Result<()> {
295 if let Some(filter) = &self.inner.resource_template_filter {
296 if !filter.is_visible_with_context(context, template) {
297 return Err(filter.denial_error(concrete_uri));
298 }
299 } else if let Some(filter) = &self.inner.resource_filter {
300 // A ResourceFilter cannot safely evaluate a template. Existing
301 // filtered deployments therefore fail closed until they opt into
302 // an explicit ResourceTemplateFilter (#1399).
303 return Err(filter.denial_error(concrete_uri));
304 }
305 Ok(())
306 }
307
308 /// Authorize a resource-template completion without disclosing whether
309 /// the referenced template exists under the default denial policy.
310 fn authorize_completion_resource_template(
311 &self,
312 context: &CapabilityFilterContext<'_>,
313 template: &ResourceTemplate,
314 target: &str,
315 ) -> Result<()> {
316 if let Some(filter) = &self.inner.resource_template_filter {
317 if !filter.is_visible_with_context(context, template) {
318 return Err(filter.denial_error_or_not_found(target, || {
319 Error::JsonRpc(JsonRpcError::resource_not_found(target))
320 }));
321 }
322 } else if let Some(filter) = &self.inner.resource_filter {
323 // An exact-resource filter cannot authorize a template. Match
324 // resources/read's fail-closed policy, but use the completion
325 // reference's canonical unknown response for default NotFound.
326 return Err(filter.denial_error_or_not_found(target, || {
327 Error::JsonRpc(JsonRpcError::resource_not_found(target))
328 }));
329 }
330 Ok(())
331 }
332
333 /// Authorize access to an exact statically registered resource.
334 ///
335 /// Legacy resource subscriptions deliberately retain their existing
336 /// exact-static scope. This helper gives subscribe and unsubscribe the
337 /// same disabled/filter policy and concrete-target denial behavior as a
338 /// static resource read without expanding the accepted URI universe to
339 /// dynamic resources or templates.
340 fn authorize_static_resource_subscription(
341 &self,
342 request_extensions: &Extensions,
343 uri: &str,
344 ) -> Result<()> {
345 let disabled = self.inner.disabled_resources.read().unwrap().contains(uri);
346 if disabled {
347 return Err(Error::JsonRpc(JsonRpcError::resource_not_found(uri)));
348 }
349
350 let resource = self
351 .inner
352 .resources
353 .get(uri)
354 .ok_or_else(|| Error::JsonRpc(JsonRpcError::resource_not_found(uri)))?;
355 let context = self.capability_filter_context(
356 request_extensions,
357 CapabilityOperation::Access { target: uri },
358 );
359 if let Some(filter) = &self.inner.resource_filter
360 && !filter.is_visible_with_context(&context, resource)
361 {
362 return Err(filter.denial_error(uri));
363 }
364
365 Ok(())
366 }
367
368 /// Authorize the capability named by a completion request.
369 ///
370 /// Completion is a second access path to prompts and resources, so it
371 /// must resolve and authorize the same winning registration as the
372 /// ordinary get/read paths before the application handler sees the
373 /// request. In particular, a denied static registration must not fall
374 /// through to a dynamic registration with the same name or URI.
375 fn authorize_completion_reference(
376 &self,
377 request_extensions: &Extensions,
378 reference: &CompletionReference,
379 ) -> Result<()> {
380 match reference {
381 CompletionReference::Prompt { name } => {
382 #[cfg(feature = "dynamic-tools")]
383 if let Some(initializer) = &self.inner.prompt_initializer {
384 initializer()?;
385 }
386
387 if self.inner.disabled_prompts.read().unwrap().contains(name) {
388 return Err(prompt_not_found(name));
389 }
390
391 let prompt = self.inner.prompts.get(name).cloned();
392 #[cfg(feature = "dynamic-tools")]
393 let prompt = prompt.or_else(|| {
394 self.inner
395 .dynamic_prompts
396 .as_ref()
397 .and_then(|dynamic| dynamic.get(name))
398 });
399 let prompt = prompt.ok_or_else(|| prompt_not_found(name))?;
400
401 let context = self.capability_filter_context(
402 request_extensions,
403 CapabilityOperation::Access { target: name },
404 );
405 if let Some(filter) = &self.inner.prompt_filter
406 && !filter.is_visible_with_context(&context, &prompt)
407 {
408 return Err(filter.denial_error_or_not_found(name, || prompt_not_found(name)));
409 }
410 Ok(())
411 }
412 CompletionReference::Resource { uri } => {
413 if self.inner.disabled_resources.read().unwrap().contains(uri) {
414 return Err(Error::JsonRpc(JsonRpcError::resource_not_found(uri)));
415 }
416
417 let context = self.capability_filter_context(
418 request_extensions,
419 CapabilityOperation::Access { target: uri },
420 );
421
422 // Exact resources take precedence over templates, matching
423 // resources/read. Static resources likewise shadow dynamic
424 // resources with the same URI.
425 if let Some(resource) = self.inner.resources.get(uri) {
426 if let Some(filter) = &self.inner.resource_filter
427 && !filter.is_visible_with_context(&context, resource)
428 {
429 return Err(filter.denial_error_or_not_found(uri, || {
430 Error::JsonRpc(JsonRpcError::resource_not_found(uri))
431 }));
432 }
433 return Ok(());
434 }
435 #[cfg(feature = "dynamic-tools")]
436 if let Some(resource) = self
437 .inner
438 .dynamic_resources
439 .as_ref()
440 .and_then(|dynamic| dynamic.get(uri))
441 {
442 if let Some(filter) = &self.inner.resource_filter
443 && !filter.is_visible_with_context(&context, &resource)
444 {
445 return Err(filter.denial_error_or_not_found(uri, || {
446 Error::JsonRpc(JsonRpcError::resource_not_found(uri))
447 }));
448 }
449 return Ok(());
450 }
451
452 // A completion reference may contain either a resource URI
453 // or the registered URI-template pattern. Check exact
454 // patterns before treating the value as a concrete URI so a
455 // template definition authorizes its own completion request.
456 if let Some(template) = self
457 .inner
458 .resource_templates
459 .iter()
460 .find(|template| template.uri_template == *uri)
461 {
462 return self.authorize_completion_resource_template(&context, template, uri);
463 }
464 #[cfg(feature = "dynamic-tools")]
465 if let Some(template) =
466 self.inner
467 .dynamic_resource_templates
468 .as_ref()
469 .and_then(|dynamic| {
470 dynamic
471 .list()
472 .into_iter()
473 .find(|template| template.uri_template == *uri)
474 })
475 {
476 return self.authorize_completion_resource_template(&context, &template, uri);
477 }
478
479 // Concrete URIs produced by a template are resource
480 // references too. Preserve resources/read's first-match and
481 // static-before-dynamic routing semantics.
482 if let Some(template) = self
483 .inner
484 .resource_templates
485 .iter()
486 .find(|template| template.match_uri(uri).is_some())
487 {
488 return self.authorize_completion_resource_template(&context, template, uri);
489 }
490 #[cfg(feature = "dynamic-tools")]
491 if let Some((template, _variables)) = self
492 .inner
493 .dynamic_resource_templates
494 .as_ref()
495 .and_then(|dynamic| dynamic.match_uri(uri))
496 {
497 return self.authorize_completion_resource_template(&context, &template, uri);
498 }
499
500 Err(Error::JsonRpc(JsonRpcError::resource_not_found(uri)))
501 }
502 _ => Err(Error::JsonRpc(JsonRpcError::invalid_params(
503 "Unsupported completion reference",
504 ))),
505 }
506 }
507
508 /// Generate request-filtered instructions from registered capabilities.
509 fn generate_instructions(
510 &self,
511 config: &AutoInstructionsConfig,
512 request_extensions: &Extensions,
513 ) -> String {
514 let mut parts = Vec::new();
515 let context = self.capability_filter_context(request_extensions, CapabilityOperation::List);
516
517 if let Some(prefix) = &config.prefix {
518 parts.push(prefix.clone());
519 }
520
521 // Tools section
522 let disabled_tools = self.inner.disabled_tools.read().unwrap().clone();
523 let mut tools: Vec<_> = self
524 .inner
525 .tools
526 .values()
527 .filter(|tool| {
528 !disabled_tools.contains(&tool.name)
529 && self
530 .inner
531 .tool_filter
532 .as_ref()
533 .is_none_or(|filter| filter.is_visible_with_context(&context, tool))
534 })
535 .collect();
536 if !tools.is_empty() {
537 let mut lines = vec!["## Tools".to_string(), String::new()];
538 tools.sort_by(|a, b| a.name.cmp(&b.name));
539 for tool in tools {
540 let desc = tool.description.as_deref().unwrap_or("No description");
541 let tags = annotation_tags(tool.annotations.as_ref());
542 if tags.is_empty() {
543 lines.push(format!("- **{}**: {}", tool.name, desc));
544 } else {
545 lines.push(format!("- **{}**: {} [{}]", tool.name, desc, tags));
546 }
547 }
548 parts.push(lines.join("\n"));
549 }
550
551 // Resources section
552 let disabled_resources = self.inner.disabled_resources.read().unwrap().clone();
553 let mut resources: Vec<_> = self
554 .inner
555 .resources
556 .values()
557 .filter(|resource| {
558 !disabled_resources.contains(&resource.uri)
559 && self
560 .inner
561 .resource_filter
562 .as_ref()
563 .is_none_or(|filter| filter.is_visible_with_context(&context, resource))
564 })
565 .collect();
566 let mut templates: Vec<_> = self
567 .inner
568 .resource_templates
569 .iter()
570 .filter(|template| self.resource_template_is_visible(&context, template))
571 .collect();
572 if !resources.is_empty() || !templates.is_empty() {
573 let mut lines = vec!["## Resources".to_string(), String::new()];
574 resources.sort_by(|a, b| a.uri.cmp(&b.uri));
575 for resource in resources {
576 let desc = resource.description.as_deref().unwrap_or("No description");
577 lines.push(format!("- **{}**: {}", resource.uri, desc));
578 }
579 templates.sort_by(|a, b| a.uri_template.cmp(&b.uri_template));
580 for template in templates {
581 let desc = template.description.as_deref().unwrap_or("No description");
582 lines.push(format!("- **{}**: {}", template.uri_template, desc));
583 }
584 parts.push(lines.join("\n"));
585 }
586
587 // Prompts section
588 let disabled_prompts = self.inner.disabled_prompts.read().unwrap().clone();
589 let mut prompts: Vec<_> = self
590 .inner
591 .prompts
592 .values()
593 .filter(|prompt| {
594 !disabled_prompts.contains(&prompt.name)
595 && self
596 .inner
597 .prompt_filter
598 .as_ref()
599 .is_none_or(|filter| filter.is_visible_with_context(&context, prompt))
600 })
601 .collect();
602 if !prompts.is_empty() {
603 let mut lines = vec!["## Prompts".to_string(), String::new()];
604 prompts.sort_by(|a, b| a.name.cmp(&b.name));
605 for prompt in prompts {
606 let desc = prompt.description.as_deref().unwrap_or("No description");
607 lines.push(format!("- **{}**: {}", prompt.name, desc));
608 }
609 parts.push(lines.join("\n"));
610 }
611
612 if let Some(suffix) = &config.suffix {
613 parts.push(suffix.clone());
614 }
615
616 parts.join("\n\n")
617 }
618}
619
620/// Build annotation tags like "read-only, idempotent" from tool annotations.
621///
622/// Only includes tags that differ from the MCP spec defaults
623/// (read-only=false, idempotent=false). The destructive and open-world
624/// hints are omitted because they match the default assumptions.
625fn annotation_tags(annotations: Option<&crate::protocol::ToolAnnotations>) -> String {
626 let Some(ann) = annotations else {
627 return String::new();
628 };
629 let mut tags = Vec::new();
630 if ann.is_read_only() {
631 tags.push("read-only");
632 }
633 if ann.is_idempotent() {
634 tags.push("idempotent");
635 }
636 tags.join(", ")
637}
638
639impl McpRouter {
640 /// Create a new MCP router
641 pub fn new() -> Self {
642 Self {
643 inner: Arc::new(McpRouterInner {
644 server_name: "tower-mcp".to_string(),
645 server_version: env!("CARGO_PKG_VERSION").to_string(),
646 server_title: None,
647 server_description: None,
648 server_icons: None,
649 server_website_url: None,
650 instructions: None,
651 panic_policy: None,
652 task_error_policy: TaskErrorPolicy::default(),
653 task_owner_resolver: default_task_owner_resolver(),
654 auto_instructions: None,
655 tools: HashMap::new(),
656 resources: HashMap::new(),
657 resource_templates: Vec::new(),
658 prompts: HashMap::new(),
659 advertise_resource_subscriptions: true,
660 advertise_tools_list_changed: None,
661 advertise_prompts_list_changed: None,
662 advertise_resources_list_changed: None,
663 advertise_mcp_logging: None,
664 live_task_executions: crate::LiveTaskExecutionHandle::new(),
665 in_flight: Arc::new(RwLock::new(HashMap::new())),
666 next_dispatch: Arc::new(AtomicU64::new(0)),
667 notification_tx: None,
668 #[cfg(all(feature = "http", feature = "stateless"))]
669 modern_notification_sink: Arc::new(RwLock::new(None)),
670 #[cfg(feature = "stateless")]
671 subscription_observer: Arc::new(RwLock::new(None)),
672 client_requester: None,
673 task_store: Arc::new(MemoryTaskStore::new()),
674 extensions: Arc::new(crate::context::Extensions::new()),
675 protocol_extensions: HashMap::new(),
676 completion_handler: None,
677 tool_filter: None,
678 resource_filter: None,
679 resource_template_filter: None,
680 prompt_filter: None,
681 min_log_level: Arc::new(RwLock::new(LogLevel::Debug)),
682 page_size: None,
683 list_ttl_ms: None,
684 read_ttl_ms: None,
685 cache_scope: None,
686 logging_deprecated: None,
687 disabled_tools: Arc::new(RwLock::new(HashSet::new())),
688 disabled_resources: Arc::new(RwLock::new(HashSet::new())),
689 disabled_prompts: Arc::new(RwLock::new(HashSet::new())),
690 #[cfg(feature = "dynamic-tools")]
691 dynamic_tools: None,
692 #[cfg(feature = "dynamic-tools")]
693 dynamic_prompts: None,
694 #[cfg(feature = "dynamic-tools")]
695 prompt_initializer: None,
696 #[cfg(feature = "dynamic-tools")]
697 dynamic_resources: None,
698 #[cfg(feature = "dynamic-tools")]
699 dynamic_resource_templates: None,
700 }),
701 session: SessionState::new(),
702 subscriptions: Arc::new(RwLock::new(HashSet::new())),
703 }
704 }
705
706 /// Create a clone with fresh session state.
707 ///
708 /// Use this when creating a new logical session (e.g., per HTTP connection).
709 /// The router configuration (tools, resources, prompts) is shared, but the
710 /// session state (phase, extensions) and legacy resource subscriptions are
711 /// independent.
712 ///
713 /// This is typically called by transports when establishing a new client session.
714 pub fn with_fresh_session(&self) -> Self {
715 Self {
716 inner: self.inner.clone(),
717 session: SessionState::new(),
718 subscriptions: Arc::new(RwLock::new(HashSet::new())),
719 }
720 }
721
722 /// Build a map of tool names to their annotations.
723 ///
724 /// The returned [`ToolAnnotationsMap`] includes annotations from all
725 /// currently registered tools (both static and dynamic). Tools without
726 /// annotations are omitted from the map.
727 ///
728 /// This is used internally by transports to inject annotations into
729 /// request extensions, but can also be called directly for custom
730 /// middleware setups.
731 pub fn tool_annotations_map(&self) -> ToolAnnotationsMap {
732 let disabled = self.inner.disabled_tools.read().unwrap();
733 let mut map = HashMap::new();
734 for (name, tool) in &self.inner.tools {
735 if disabled.contains(name) {
736 continue;
737 }
738 if let Some(annotations) = &tool.annotations {
739 map.insert(name.clone(), annotations.clone());
740 }
741 }
742 #[cfg(feature = "dynamic-tools")]
743 if let Some(dynamic) = &self.inner.dynamic_tools {
744 for tool in dynamic.list() {
745 if disabled.contains(&tool.name) {
746 continue;
747 }
748 // Static tools take precedence
749 if !map.contains_key(&tool.name)
750 && let Some(ref annotations) = tool.annotations
751 {
752 map.insert(tool.name.clone(), annotations.clone());
753 }
754 }
755 }
756 ToolAnnotationsMap { map: Arc::new(map) }
757 }
758
759 /// Configure a pluggable [`TaskStore`] for async task state.
760 ///
761 /// The default is an in-process [`MemoryTaskStore`] with a five-minute task
762 /// TTL and one-minute cleanup cadence. Supply a configured memory store to
763 /// choose the final-protocol task lifetime, or an external store (Redis,
764 /// Postgres, etc.) to share task state across server instances behind a
765 /// load balancer, so `tasks/get` works regardless of which instance
766 /// created the task (SEP-2663).
767 ///
768 /// # Example
769 ///
770 /// ```rust
771 /// use std::sync::Arc;
772 /// use std::time::Duration;
773 /// use tower_mcp::McpRouter;
774 /// use tower_mcp::async_task::{MemoryTaskStore, MemoryTaskStoreConfig, TaskStore};
775 ///
776 /// let config = MemoryTaskStoreConfig::default()
777 /// .default_ttl(Duration::from_secs(30 * 60))
778 /// .cleanup_interval(Duration::from_secs(30));
779 /// let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::with_config(config));
780 /// let router = McpRouter::new().task_store(store);
781 /// ```
782 pub fn task_store(mut self, store: Arc<dyn TaskStore>) -> Self {
783 Arc::make_mut(&mut self.inner).task_store = store;
784 self
785 }
786
787 /// Resolve the authenticated principal used for Task ownership.
788 ///
789 /// The resolver runs for Task creation and every later Task operation. It
790 /// receives the request's transport- and middleware-supplied extensions
791 /// and returns a durable owner key, or `None` for an anonymous request.
792 /// Owned and anonymous callers remain strictly distinct: neither can act
793 /// on the other's Tasks.
794 ///
795 /// Owner keys become authorization data in the configured [`TaskStore`].
796 /// They must be deterministic, nonempty, and collision-resistant across
797 /// every issuer and tenant that can reach the store. Include that
798 /// qualification in the returned value. Never return a bearer token, API
799 /// key, session cookie, or other credential.
800 ///
801 /// Empty or whitespace-only values and panics fail closed. Creation is
802 /// rejected, while an existing Task is reported as absent. Rust's
803 /// process-global panic hook still runs before a panic is caught.
804 ///
805 /// Installing a resolver replaces the default OAuth `TokenClaims.sub`
806 /// mapping. Configure this on the root router: [`merge`](Self::merge) and
807 /// [`nest`](Self::nest) import capabilities, not router policy.
808 ///
809 /// # Example
810 ///
811 /// ```rust
812 /// use tower_mcp::{Extensions, McpRouter};
813 ///
814 /// #[derive(Clone)]
815 /// struct Principal {
816 /// issuer: String,
817 /// subject: String,
818 /// }
819 ///
820 /// let router = McpRouter::new().task_owner_resolver(|extensions: &Extensions| {
821 /// extensions
822 /// .get::<Principal>()
823 /// .map(|principal| format!("{}:{}", principal.issuer, principal.subject))
824 /// });
825 /// # let _ = router;
826 /// ```
827 #[must_use]
828 pub fn task_owner_resolver<F>(mut self, resolver: F) -> Self
829 where
830 F: Fn(&crate::context::Extensions) -> Option<String> + Send + Sync + 'static,
831 {
832 Arc::make_mut(&mut self.inner).task_owner_resolver = custom_task_owner_resolver(resolver);
833 self
834 }
835
836 /// Resolve Task owners from one request extension type.
837 ///
838 /// This is the convenient counterpart to [`task_owner_resolver`](Self::task_owner_resolver)
839 /// for extensions registered with
840 /// [`HttpTransport::bridge_extension`](crate::HttpTransport::bridge_extension)
841 /// or inserted by Tower middleware. A request without `T` is anonymous;
842 /// the mapping only runs when the extension is present.
843 ///
844 /// The returned value has the same durability and secrecy requirements as
845 /// [`task_owner_resolver`](Self::task_owner_resolver).
846 #[must_use]
847 pub fn task_owner_from_extension<T>(
848 self,
849 map: impl Fn(&T) -> String + Send + Sync + 'static,
850 ) -> Self
851 where
852 T: Send + Sync + 'static,
853 {
854 self.task_owner_resolver(move |extensions| extensions.get::<T>().map(&map))
855 }
856
857 /// Return host-side lifecycle control for built-in live Task executions.
858 ///
859 /// Clone this handle before moving the router into a transport. It can
860 /// close live-handler admission, inspect active execution IDs, request
861 /// cancellation, and wait for settlement during graceful shutdown.
862 /// Replay task handlers and durable [`TaskStore`] records are not tracked.
863 ///
864 /// The handle is shared by all clones and fresh sessions of this router.
865 /// Transport shutdown remains separate: the application chooses when to
866 /// close admission, whether to cancel, and how long to await
867 /// [`crate::LiveTaskExecutionHandle::drained`].
868 pub fn live_task_execution_handle(&self) -> crate::LiveTaskExecutionHandle {
869 self.inner.live_task_executions.clone()
870 }
871
872 /// Set the root router's client-visible Task error policy.
873 ///
874 /// The policy applies to task creation, `tasks/get`, `tasks/update`,
875 /// `tasks/cancel`, and failures while parking, executing, resuming, or
876 /// finalizing a handler. Like [`McpRouter::catch_panics_with`], it is root
877 /// configuration: merging or nesting another router imports that router's
878 /// capabilities but not its policy, so the receiving router governs the
879 /// combined catalog.
880 ///
881 /// Tower's default preserves the established missing/expired response
882 /// shapes and redacts every [`TaskStoreError`] to a fixed internal error.
883 #[must_use]
884 pub fn task_error_policy(mut self, policy: TaskErrorPolicy) -> Self {
885 Arc::make_mut(&mut self.inner).task_error_policy = policy;
886 self
887 }
888
889 /// Enable dynamic tool registration and return a registry handle.
890 ///
891 /// The returned [`DynamicToolRegistry`] can be used to add and remove tools
892 /// at runtime. Dynamic tools are merged with static tools when handling
893 /// `tools/list` and `tools/call` requests. Static tools take precedence
894 /// over dynamic tools when names collide.
895 ///
896 /// # Example
897 ///
898 /// ```rust
899 /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
900 /// use schemars::JsonSchema;
901 /// use serde::Deserialize;
902 ///
903 /// #[derive(Debug, Deserialize, JsonSchema)]
904 /// struct Input { value: String }
905 ///
906 /// let (router, registry) = McpRouter::new()
907 /// .server_info("my-server", "1.0.0")
908 /// .with_dynamic_tools();
909 ///
910 /// // Register a tool at runtime
911 /// let tool = ToolBuilder::new("echo")
912 /// .description("Echo input")
913 /// .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
914 /// .build();
915 ///
916 /// registry.register(tool);
917 /// ```
918 #[cfg(feature = "dynamic-tools")]
919 pub fn with_dynamic_tools(mut self) -> (Self, DynamicToolRegistry) {
920 let inner_dyn = Arc::new(DynamicToolsInner::new());
921 Arc::make_mut(&mut self.inner).dynamic_tools = Some(inner_dyn.clone());
922 (self, DynamicToolRegistry::new(inner_dyn))
923 }
924
925 /// Enable dynamic prompt registration and return a registry handle.
926 ///
927 /// The returned [`DynamicPromptRegistry`] can be used to add and remove
928 /// prompts at runtime. Dynamic prompts are merged with static prompts
929 /// when handling `prompts/list` and `prompts/get` requests. Static
930 /// prompts take precedence over dynamic prompts when names collide.
931 ///
932 /// # Example
933 ///
934 /// ```rust
935 /// use tower_mcp::{McpRouter, PromptBuilder};
936 ///
937 /// let (router, registry) = McpRouter::new()
938 /// .server_info("my-server", "1.0.0")
939 /// .with_dynamic_prompts();
940 ///
941 /// let prompt = PromptBuilder::new("greet")
942 /// .description("Greet someone")
943 /// .user_message("Hello!");
944 ///
945 /// registry.register(prompt);
946 /// ```
947 #[cfg(feature = "dynamic-tools")]
948 pub fn with_dynamic_prompts(mut self) -> (Self, DynamicPromptRegistry) {
949 let inner_dyn = Arc::new(DynamicPromptsInner::new());
950 Arc::make_mut(&mut self.inner).dynamic_prompts = Some(inner_dyn.clone());
951 (self, DynamicPromptRegistry::new(inner_dyn))
952 }
953
954 /// Run an initializer before each `prompts/list` or `prompts/get` access.
955 ///
956 /// This supports prompt definitions backed by an application-owned lazy
957 /// catalog. The initializer should populate the registry returned by
958 /// [`Self::with_dynamic_prompts`] and implement its own caching.
959 #[cfg(feature = "dynamic-tools")]
960 pub fn dynamic_prompt_initializer<F>(mut self, initializer: F) -> Self
961 where
962 F: Fn() -> Result<()> + Send + Sync + 'static,
963 {
964 Arc::make_mut(&mut self.inner).prompt_initializer = Some(Arc::new(initializer));
965 self
966 }
967
968 /// Enable dynamic resource registration and return a registry handle.
969 ///
970 /// The returned [`DynamicResourceRegistry`] can be used to add and remove
971 /// resources at runtime. Dynamic resources are merged with static resources
972 /// when handling `resources/list` and `resources/read` requests. Static
973 /// resources take precedence over dynamic resources when URIs collide.
974 ///
975 /// # Example
976 ///
977 /// ```rust
978 /// use tower_mcp::{McpRouter, ResourceBuilder};
979 ///
980 /// let (router, registry) = McpRouter::new()
981 /// .server_info("my-server", "1.0.0")
982 /// .with_dynamic_resources();
983 ///
984 /// let resource = ResourceBuilder::new("file:///data.json")
985 /// .name("Data")
986 /// .text(r#"{"key": "value"}"#);
987 ///
988 /// registry.register(resource);
989 /// ```
990 #[cfg(feature = "dynamic-tools")]
991 pub fn with_dynamic_resources(mut self) -> (Self, DynamicResourceRegistry) {
992 let inner_dyn = Arc::new(DynamicResourcesInner::new());
993 Arc::make_mut(&mut self.inner).dynamic_resources = Some(inner_dyn.clone());
994 (self, DynamicResourceRegistry::new(inner_dyn))
995 }
996
997 /// Enable dynamic resource template registration and return a registry handle.
998 ///
999 /// The returned [`DynamicResourceTemplateRegistry`] can be used to add and
1000 /// remove resource templates at runtime. Dynamic templates are checked
1001 /// after static templates when handling `resources/read` requests.
1002 ///
1003 /// # Example
1004 ///
1005 /// ```rust,ignore
1006 /// use tower_mcp::{McpRouter, ResourceTemplateBuilder};
1007 ///
1008 /// let (router, registry) = McpRouter::new()
1009 /// .server_info("my-server", "1.0.0")
1010 /// .with_dynamic_resource_templates();
1011 ///
1012 /// let template = ResourceTemplateBuilder::new("db://tables/{table}")
1013 /// .name("Database Table")
1014 /// .handler(|uri, vars| async move { /* ... */ });
1015 ///
1016 /// registry.register(template);
1017 /// ```
1018 #[cfg(feature = "dynamic-tools")]
1019 pub fn with_dynamic_resource_templates(mut self) -> (Self, DynamicResourceTemplateRegistry) {
1020 let inner_dyn = Arc::new(DynamicResourceTemplatesInner::new());
1021 Arc::make_mut(&mut self.inner).dynamic_resource_templates = Some(inner_dyn.clone());
1022 (self, DynamicResourceTemplateRegistry::new(inner_dyn))
1023 }
1024
1025 /// Set the notification sender without registering it with the shared
1026 /// dynamic registries.
1027 ///
1028 /// Used by transports for per-request (sessionless) notification
1029 /// capture: the dynamic registries are long-lived and shared across
1030 /// router clones, so registering one sender per request would
1031 /// accumulate senders without bound.
1032 #[cfg(feature = "stateless")]
1033 #[cfg(feature = "http")]
1034 pub(crate) fn with_request_notification_sender(mut self, tx: NotificationSender) -> Self {
1035 Arc::make_mut(&mut self.inner).notification_tx = Some(tx);
1036 self
1037 }
1038
1039 /// Set the notification sender for progress reporting
1040 ///
1041 /// This is typically called by the transport layer to receive notifications.
1042 pub fn with_notification_sender(mut self, tx: NotificationSender) -> Self {
1043 let inner = Arc::make_mut(&mut self.inner);
1044 // Also register the sender with dynamic registries so they can
1045 // broadcast list-changed notifications to this session.
1046 #[cfg(feature = "dynamic-tools")]
1047 if let Some(ref dynamic_tools) = inner.dynamic_tools {
1048 dynamic_tools.add_notification_sender(tx.clone());
1049 }
1050 #[cfg(feature = "dynamic-tools")]
1051 if let Some(ref dynamic_prompts) = inner.dynamic_prompts {
1052 dynamic_prompts.add_notification_sender(tx.clone());
1053 }
1054 #[cfg(feature = "dynamic-tools")]
1055 if let Some(ref dynamic_resources) = inner.dynamic_resources {
1056 dynamic_resources.add_notification_sender(tx.clone());
1057 }
1058 #[cfg(feature = "dynamic-tools")]
1059 if let Some(ref dynamic_resource_templates) = inner.dynamic_resource_templates {
1060 dynamic_resource_templates.add_notification_sender(tx.clone());
1061 }
1062 inner.notification_tx = Some(tx);
1063 self
1064 }
1065
1066 /// Observe the terminal half of `subscriptions/listen` streams.
1067 ///
1068 /// Every transport built from this router reports stream closes (reason
1069 /// and duration) through the observer. The request half of the boundary
1070 /// is ordinary `Service<RouterRequest>` middleware; see
1071 /// [`SubscriptionObserver`](crate::transport::subscriptions::SubscriptionObserver) for how the two compose.
1072 #[cfg(feature = "stateless")]
1073 pub fn with_subscription_observer(
1074 self,
1075 observer: Arc<dyn crate::transport::subscriptions::SubscriptionObserver>,
1076 ) -> Self {
1077 if let Ok(mut slot) = self.inner.subscription_observer.write() {
1078 *slot = Some(observer);
1079 }
1080 self
1081 }
1082
1083 /// The attached close observer, if any.
1084 #[cfg(feature = "stateless")]
1085 pub(crate) fn subscription_observer(
1086 &self,
1087 ) -> Option<Arc<dyn crate::transport::subscriptions::SubscriptionObserver>> {
1088 self.inner
1089 .subscription_observer
1090 .read()
1091 .ok()
1092 .and_then(|slot| slot.clone())
1093 }
1094
1095 /// Attach the transport-lifetime final subscription notification path.
1096 #[cfg(all(feature = "http", feature = "stateless"))]
1097 pub(crate) fn attach_modern_notification_sink(&self, sink: ModernNotificationSink) {
1098 if let Ok(mut active) = self.inner.modern_notification_sink.write() {
1099 *active = Some(sink);
1100 }
1101 }
1102
1103 /// Get the notification sender (if configured)
1104 pub fn notification_sender(&self) -> Option<&NotificationSender> {
1105 self.inner.notification_tx.as_ref()
1106 }
1107
1108 /// Set the client requester for server-to-client requests (sampling, etc.)
1109 ///
1110 /// This is typically called by bidirectional transports (WebSocket, stdio)
1111 /// to enable tool handlers to send requests to the client.
1112 pub fn with_client_requester(mut self, requester: ClientRequesterHandle) -> Self {
1113 Arc::make_mut(&mut self.inner).client_requester = Some(requester);
1114 self
1115 }
1116
1117 /// Get the client requester (if configured)
1118 pub fn client_requester(&self) -> Option<&ClientRequesterHandle> {
1119 self.inner.client_requester.as_ref()
1120 }
1121
1122 /// Add router-level state that handlers can access via the `Extension<T>` extractor.
1123 ///
1124 /// This is the recommended way to share state across all tools, resources, and prompts
1125 /// in a router. The state is available to handlers via the [`crate::extract::Extension`]
1126 /// extractor.
1127 ///
1128 /// # Example
1129 ///
1130 /// ```rust
1131 /// use std::sync::Arc;
1132 /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
1133 /// use tower_mcp::extract::{Extension, Json};
1134 /// use schemars::JsonSchema;
1135 /// use serde::Deserialize;
1136 ///
1137 /// #[derive(Clone)]
1138 /// struct AppState {
1139 /// db_url: String,
1140 /// }
1141 ///
1142 /// #[derive(Deserialize, JsonSchema)]
1143 /// struct QueryInput {
1144 /// sql: String,
1145 /// }
1146 ///
1147 /// let state = Arc::new(AppState { db_url: "postgres://...".into() });
1148 ///
1149 /// // Tool extracts state via Extension<T>
1150 /// let query_tool = ToolBuilder::new("query")
1151 /// .description("Run a database query")
1152 /// .extractor_handler(
1153 /// (),
1154 /// |Extension(state): Extension<Arc<AppState>>, Json(input): Json<QueryInput>| async move {
1155 /// Ok(CallToolResult::text(format!("Query on {}: {}", state.db_url, input.sql)))
1156 /// },
1157 /// )
1158 /// .build();
1159 ///
1160 /// let router = McpRouter::new()
1161 /// .with_state(state) // State is now available to all handlers
1162 /// .tool(query_tool);
1163 /// ```
1164 pub fn with_state<T: Clone + Send + Sync + 'static>(mut self, state: T) -> Self {
1165 let inner = Arc::make_mut(&mut self.inner);
1166 Arc::make_mut(&mut inner.extensions).insert(state);
1167 self
1168 }
1169
1170 /// Add an extension value that handlers can access via the `Extension<T>` extractor.
1171 ///
1172 /// This is a more general form of `with_state()` for when you need multiple
1173 /// typed values available to handlers.
1174 pub fn with_extension<T: Clone + Send + Sync + 'static>(self, value: T) -> Self {
1175 self.with_state(value)
1176 }
1177
1178 /// Advertise one validated MCP protocol extension.
1179 ///
1180 /// This is separate from [`with_extension`](Self::with_extension), which
1181 /// stores process-local Rust values for handlers. Protocol extensions are
1182 /// advertised on the wire and become active only when the client declares
1183 /// the same identifier.
1184 pub fn with_protocol_extension(mut self, extension: crate::ExtensionDeclaration) -> Self {
1185 let (identifier, settings) = extension.into_parts();
1186 Arc::make_mut(&mut self.inner)
1187 .protocol_extensions
1188 .insert(identifier, settings);
1189 self
1190 }
1191
1192 /// Get the router's extensions.
1193 pub fn extensions(&self) -> &crate::context::Extensions {
1194 &self.inner.extensions
1195 }
1196
1197 /// Create a request context for tracking a request
1198 ///
1199 /// This registers the request for cancellation tracking and sets up
1200 /// progress reporting, client requests, and router extensions if configured.
1201 pub fn create_context(
1202 &self,
1203 request_id: RequestId,
1204 progress_token: Option<ProgressToken>,
1205 ) -> RequestContext {
1206 self.create_context_with_extensions(request_id, progress_token, &Extensions::new())
1207 }
1208
1209 /// Internal: build a `RequestContext` and additionally merge per-request
1210 /// extensions on top of the router's extensions. Used by [`Service::call`]
1211 /// to thread `RouterRequest.extensions` (e.g. SEP-2575 per-request
1212 /// `_meta`) through to handlers.
1213 pub(crate) fn create_context_with_extensions(
1214 &self,
1215 request_id: RequestId,
1216 progress_token: Option<ProgressToken>,
1217 per_request: &Extensions,
1218 ) -> RequestContext {
1219 let ctx = RequestContext::new(request_id.clone());
1220
1221 // Set up progress token if provided
1222 let ctx = if let Some(token) = progress_token {
1223 ctx.with_progress_token(token)
1224 } else {
1225 ctx
1226 };
1227
1228 // Set up notification sender if configured
1229 let ctx = if let Some(tx) = &self.inner.notification_tx {
1230 ctx.with_notification_sender(tx.clone())
1231 } else {
1232 ctx
1233 };
1234
1235 // Start with router-level extensions, then layer per-request extensions
1236 // on top so they win on type collision. with_state() data stays
1237 // visible; per-request meta (SEP-2575) is now reachable too.
1238 let mut merged = (*self.inner.extensions).clone();
1239 merged.merge(per_request);
1240 let negotiated_extensions = if is_final_protocol_request(per_request) {
1241 let server_capabilities =
1242 self.capabilities_for_protocol(Some(crate::protocol::PROTOCOL_VERSION_2026_07_28));
1243 final_client_capabilities(per_request)
1244 .map(|client_capabilities| {
1245 crate::NegotiatedExtensions::from_capabilities(
1246 client_capabilities,
1247 &server_capabilities,
1248 )
1249 })
1250 .unwrap_or_default()
1251 } else {
1252 self.session
1253 .get::<crate::NegotiatedExtensions>()
1254 .unwrap_or_default()
1255 };
1256 merged.insert(negotiated_extensions);
1257
1258 // The final protocol does not permit servers to initiate JSON-RPC
1259 // requests. Legacy transports may provide a requester scoped to the
1260 // originating request; prefer it over a transport-wide fallback so
1261 // restricted requests stay on their associated response channel.
1262 let final_lifecycle = is_final_protocol_request(per_request);
1263 let ctx = ctx.with_final_lifecycle(final_lifecycle);
1264 let ctx = if final_lifecycle {
1265 ctx
1266 } else {
1267 ctx.with_resource_subscriptions(self.subscriptions.clone())
1268 };
1269 let ctx = if !final_lifecycle
1270 && let Some(requester) = merged
1271 .get::<ClientRequesterHandle>()
1272 .cloned()
1273 .or_else(|| self.inner.client_requester.clone())
1274 {
1275 ctx.with_client_requester(requester)
1276 } else {
1277 ctx
1278 };
1279
1280 // Adopt a transport-provided cancellation token (e.g. HTTP stateless
1281 // client disconnect) so `ctx.is_cancelled()` / `ctx.cancelled()` and
1282 // in-flight tracking observe the transport's signal.
1283 let ctx = if let Some(token) = merged.get::<CancellationToken>() {
1284 ctx.with_cancellation_token(token.clone())
1285 } else {
1286 ctx
1287 };
1288
1289 let ctx = ctx
1290 .with_extensions(Arc::new(merged))
1291 .with_session(self.session.clone());
1292
1293 // Set up log level filtering
1294 let ctx = ctx.with_min_log_level(self.inner.min_log_level.clone());
1295
1296 // Register for cancellation tracking. `Service::call` mints the
1297 // dispatch id and threads it through the extensions so the guard it
1298 // holds and this registration name the same entry; a caller driving
1299 // the router directly gets a fresh one.
1300 let dispatch = per_request
1301 .get::<DispatchId>()
1302 .copied()
1303 .unwrap_or_else(|| self.next_dispatch());
1304 self.register_in_flight(request_id, dispatch, ctx.cancellation_token());
1305
1306 ctx
1307 }
1308
1309 /// Allocate a dispatch id, unique for the lifetime of this router.
1310 fn next_dispatch(&self) -> DispatchId {
1311 DispatchId(
1312 self.inner
1313 .next_dispatch
1314 .fetch_add(1, AtomicOrdering::Relaxed),
1315 )
1316 }
1317
1318 /// Track one dispatch for cancellation.
1319 ///
1320 /// Appends rather than overwrites: a client that reuses an id which is
1321 /// still in flight gets both requests tracked, so cancelling the id can
1322 /// still reach both (#1270).
1323 fn register_in_flight(
1324 &self,
1325 request_id: RequestId,
1326 dispatch: DispatchId,
1327 token: CancellationToken,
1328 ) {
1329 if let Ok(mut in_flight) = self.inner.in_flight.write() {
1330 in_flight
1331 .entry(request_id)
1332 .or_default()
1333 .push(InFlightDispatch { dispatch, token });
1334 }
1335 }
1336
1337 /// Stop tracking one dispatch, leaving any twin under the same id alone.
1338 fn complete_dispatch(&self, request_id: &RequestId, dispatch: DispatchId) {
1339 if let Ok(mut in_flight) = self.inner.in_flight.write()
1340 && let Some(entries) = in_flight.get_mut(request_id)
1341 {
1342 entries.retain(|entry| entry.dispatch != dispatch);
1343 if entries.is_empty() {
1344 in_flight.remove(request_id);
1345 }
1346 }
1347 }
1348
1349 /// Remove a request from tracking (called when request completes).
1350 ///
1351 /// Untracks *every* dispatch under `request_id`, which is the only
1352 /// granularity this signature offers. Requests dispatched through
1353 /// [`Service::call`] do not need it: each holds a guard that untracks its
1354 /// own dispatch when the future completes, is dropped, or unwinds. It
1355 /// remains for callers driving [`McpRouter::create_context`] and request
1356 /// handling themselves.
1357 pub fn complete_request(&self, request_id: &RequestId) {
1358 if let Ok(mut in_flight) = self.inner.in_flight.write() {
1359 in_flight.remove(request_id);
1360 }
1361 }
1362
1363 /// Cancel a tracked request.
1364 ///
1365 /// Cancels every dispatch still running under `request_id`. The id is the
1366 /// only handle a client has, so a client that reused one in flight gets
1367 /// both stopped rather than an arbitrary one.
1368 fn cancel_request(&self, request_id: &RequestId) -> bool {
1369 let Ok(in_flight) = self.inner.in_flight.read() else {
1370 return false;
1371 };
1372 let Some(entries) = in_flight.get(request_id) else {
1373 return false;
1374 };
1375 for entry in entries {
1376 entry.token.cancel();
1377 }
1378 !entries.is_empty()
1379 }
1380
1381 /// Server capabilities, derived from what is registered.
1382 fn capabilities(&self) -> ServerCapabilities {
1383 let has_resources =
1384 !self.inner.resources.is_empty() || !self.inner.resource_templates.is_empty();
1385 let has_notifications = self.inner.notification_tx.is_some();
1386
1387 // Each of these defaults to `has_notifications`, which is what this
1388 // router has always advertised as soon as a transport attached a
1389 // notification channel. An explicit builder call
1390 // (`tools_list_changed`, `prompts_list_changed`,
1391 // `resources_list_changed`, `mcp_logging`) overrides that default in
1392 // either direction, independently of the channel (#1338).
1393 let tools_list_changed = self
1394 .inner
1395 .advertise_tools_list_changed
1396 .unwrap_or(has_notifications);
1397 let prompts_list_changed = self
1398 .inner
1399 .advertise_prompts_list_changed
1400 .unwrap_or(has_notifications);
1401 let resources_list_changed = self
1402 .inner
1403 .advertise_resources_list_changed
1404 .unwrap_or(has_notifications);
1405 let mcp_logging = self
1406 .inner
1407 .advertise_mcp_logging
1408 .unwrap_or(has_notifications);
1409
1410 #[cfg(feature = "dynamic-tools")]
1411 let has_dynamic_tools = self.inner.dynamic_tools.is_some();
1412 #[cfg(not(feature = "dynamic-tools"))]
1413 let has_dynamic_tools = false;
1414
1415 #[cfg(feature = "dynamic-tools")]
1416 let has_dynamic_prompts = self.inner.dynamic_prompts.is_some();
1417 #[cfg(not(feature = "dynamic-tools"))]
1418 let has_dynamic_prompts = false;
1419
1420 #[cfg(feature = "dynamic-tools")]
1421 let has_dynamic_resources = self.inner.dynamic_resources.is_some()
1422 || self.inner.dynamic_resource_templates.is_some();
1423 #[cfg(not(feature = "dynamic-tools"))]
1424 let has_dynamic_resources = false;
1425
1426 ServerCapabilities {
1427 tools: if self.inner.tools.is_empty() && !has_dynamic_tools {
1428 None
1429 } else {
1430 Some(ToolsCapability {
1431 list_changed: tools_list_changed,
1432 })
1433 },
1434 resources: if has_resources || has_dynamic_resources {
1435 Some(ResourcesCapability {
1436 subscribe: self.inner.advertise_resource_subscriptions,
1437 list_changed: resources_list_changed,
1438 })
1439 } else {
1440 None
1441 },
1442 prompts: if self.inner.prompts.is_empty() && !has_dynamic_prompts {
1443 None
1444 } else {
1445 Some(PromptsCapability {
1446 list_changed: prompts_list_changed,
1447 })
1448 },
1449 // Advertised when a notification channel is configured, unless
1450 // overridden by `mcp_logging` (#1338).
1451 logging: if mcp_logging {
1452 Some(LoggingCapability {
1453 deprecated: self.inner.logging_deprecated.clone(),
1454 })
1455 } else {
1456 None
1457 },
1458 // Tasks capability is advertised if any tool supports tasks.
1459 // SEP-2663 moves the declaration to `capabilities.extensions`
1460 // under the reverse-DNS key `io.modelcontextprotocol/tasks`; we
1461 // continue to set the legacy top-level `tasks` field for back-compat
1462 // with 2025-11-25 clients that key off it.
1463 tasks: {
1464 let has_task_support = self
1465 .inner
1466 .tools
1467 .values()
1468 .any(|t| !matches!(t.task_support, TaskSupportMode::Forbidden));
1469 if has_task_support {
1470 Some(TasksCapability {
1471 // `list` is intentionally not advertised: final
1472 // SEP-2663 removes `tasks/list` and this router
1473 // answers MethodNotFound for it.
1474 list: None,
1475 cancel: Some(TasksCancelCapability {}),
1476 requests: Some(TasksRequestsCapability {
1477 tools: Some(TasksToolsRequestsCapability {
1478 call: Some(TasksToolsCallCapability {}),
1479 }),
1480 }),
1481 })
1482 } else {
1483 None
1484 }
1485 },
1486 // Completions capability when a handler is registered
1487 completions: if self.inner.completion_handler.is_some() {
1488 Some(CompletionsCapability::default())
1489 } else {
1490 None
1491 },
1492 experimental: None,
1493 extensions: {
1494 let mut map = self.inner.protocol_extensions.clone();
1495 let has_task_support = self
1496 .inner
1497 .tools
1498 .values()
1499 .any(|t| !matches!(t.task_support, TaskSupportMode::Forbidden));
1500 if has_task_support {
1501 map.insert(
1502 tower_mcp_types::protocol::TASKS_EXTENSION_ID.to_string(),
1503 serde_json::json!({}),
1504 );
1505 }
1506 (!map.is_empty()).then_some(map)
1507 },
1508 }
1509 }
1510
1511 /// Return the capability surface appropriate for a protocol version.
1512 ///
1513 /// `capabilities.tasks` is the legacy 2025-11-25 shape and is never
1514 /// advertised on the final path. The final extension is advertised only
1515 /// when the server opted in via [`McpRouter::with_tasks`]; merely
1516 /// registering task-capable tools does not advertise it, so a server that
1517 /// has not opted in presents no Tasks surface to a 2026-07-28 client.
1518 fn capabilities_for_protocol(&self, protocol_version: Option<&str>) -> ServerCapabilities {
1519 let mut capabilities = self.capabilities();
1520 if protocol_version == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28) {
1521 capabilities.tasks = None;
1522 // `resources/subscribe` and `resources/unsubscribe` are not part
1523 // of this revision, and the inspector already classifies them as
1524 // unavailable here. Advertising the capability would promise a
1525 // method the same build refuses to route (#1261).
1526 if let Some(resources) = capabilities.resources.as_mut() {
1527 resources.subscribe = false;
1528 }
1529 if !self.final_tasks_enabled()
1530 && let Some(extensions) = capabilities.extensions.as_mut()
1531 {
1532 extensions.remove(tower_mcp_types::protocol::TASKS_EXTENSION_ID);
1533 if extensions.is_empty() {
1534 capabilities.extensions = None;
1535 }
1536 }
1537 }
1538 capabilities
1539 }
1540
1541 /// Whether this server opted into the final Tasks extension.
1542 ///
1543 /// Distinct from the synthesized advertisement in [`Self::capabilities`],
1544 /// which reflects registered tools rather than an explicit choice.
1545 pub(crate) fn final_tasks_enabled(&self) -> bool {
1546 self.inner
1547 .protocol_extensions
1548 .contains_key(tower_mcp_types::protocol::TASKS_EXTENSION_ID)
1549 }
1550
1551 /// Invoke a tool, optionally converting a panic into an error result.
1552 ///
1553 /// Enabled by [`McpRouter::catch_panics`] or
1554 /// [`McpRouter::catch_panics_with`]. Without either this is a direct call
1555 /// and a panic unwinds as before, which is the default because a panic is
1556 /// an invariant violation and hiding one is not always a favour.
1557 async fn invoke_tool(
1558 &self,
1559 tool: &crate::tool::Tool,
1560 ctx: RequestContext,
1561 arguments: serde_json::Value,
1562 tool_name: &str,
1563 ) -> Result<crate::protocol::RequestOutcome<CallToolResult>> {
1564 let Some(policy) = &self.inner.panic_policy else {
1565 return tool.call_outcome_with_context(ctx, arguments).await;
1566 };
1567
1568 use futures::FutureExt;
1569 // AssertUnwindSafe: the future may hold &mut across the await, which
1570 // Rust cannot prove safe to observe post-unwind. Any state a panicking
1571 // handler leaves behind belongs to that handler; the router's own
1572 // state is not mutated by this call.
1573 let called = std::panic::AssertUnwindSafe(async move {
1574 tool.call_outcome_with_context(ctx, arguments).await
1575 })
1576 .catch_unwind()
1577 .await;
1578
1579 match called {
1580 Ok(outcome) => outcome,
1581 Err(payload) => {
1582 let message = self.handle_caught_panic(policy, tool_name, None, &*payload);
1583 Ok(crate::protocol::RequestOutcome::Complete(
1584 CallToolResult::error(message),
1585 ))
1586 }
1587 }
1588 }
1589
1590 /// Apply the selected disclosure policy to a caught handler panic.
1591 ///
1592 /// Payload recovery is intentionally conditional: the fully redacted
1593 /// path never downcasts, clones, or formats the panic payload.
1594 fn handle_caught_panic(
1595 &self,
1596 policy: &PanicPolicy,
1597 tool_name: &str,
1598 task_id: Option<&str>,
1599 payload: &(dyn std::any::Any + Send),
1600 ) -> String {
1601 let payload = policy.needs_payload().then(|| panic_message(payload));
1602 let logged_tool = policy.log_tool_name.value(tool_name);
1603 let logged_payload = policy
1604 .include_payload_in_logs
1605 .then(|| payload.as_deref().unwrap_or("<redacted>"));
1606
1607 Self::log_caught_panic(logged_tool, logged_payload, task_id);
1608 policy.client_message(tool_name, payload.as_deref())
1609 }
1610
1611 /// Build the JSON-RPC error a transport sends for an internal failure of
1612 /// its own, honouring the configured disclosure policy.
1613 ///
1614 /// A transport that hand-builds an error response is outside every path
1615 /// that consults [`PanicPolicy`], so before this existed it sent the
1616 /// error's `Display` text whatever the operator had configured (#1354).
1617 /// Routing the two websocket sites through one helper rather than
1618 /// widening the panic path is what keeps the next transport from
1619 /// reintroducing the gap: the previous round of this, #1335, fixed one of
1620 /// a pair of near-identical sites and left the other to drift.
1621 ///
1622 /// With no policy installed the error's text is returned unchanged, which
1623 /// is both the behaviour these paths already had and the stance the crate
1624 /// takes elsewhere: a panic is not caught at all until `catch_panics` asks
1625 /// for it.
1626 ///
1627 /// Gated on `websocket` because that is where the two sites are. Widen the
1628 /// gate rather than duplicating the decision when another transport needs
1629 /// it, which is the whole point of it being one helper.
1630 #[cfg(feature = "websocket")]
1631 pub(crate) fn transport_internal_error(&self, error: &dyn std::fmt::Display) -> JsonRpcError {
1632 match &self.inner.panic_policy {
1633 Some(policy) => JsonRpcError::internal_error(policy.internal_error_message(error)),
1634 None => JsonRpcError::internal_error(error.to_string()),
1635 }
1636 }
1637
1638 fn log_caught_panic(tool_name: Option<&str>, payload: Option<&str>, task_id: Option<&str>) {
1639 match (tool_name, payload, task_id) {
1640 (Some(tool_name), Some(payload), Some(task_id)) => tracing::error!(
1641 target: "mcp::tools",
1642 tool = %tool_name,
1643 panic = %payload,
1644 task_id = %task_id,
1645 "tool handler panicked; returning an error result"
1646 ),
1647 (Some(tool_name), Some(payload), None) => tracing::error!(
1648 target: "mcp::tools",
1649 tool = %tool_name,
1650 panic = %payload,
1651 "tool handler panicked; returning an error result"
1652 ),
1653 (Some(tool_name), None, Some(task_id)) => tracing::error!(
1654 target: "mcp::tools",
1655 tool = %tool_name,
1656 task_id = %task_id,
1657 "tool handler panicked; returning an error result"
1658 ),
1659 (Some(tool_name), None, None) => tracing::error!(
1660 target: "mcp::tools",
1661 tool = %tool_name,
1662 "tool handler panicked; returning an error result"
1663 ),
1664 (None, Some(payload), Some(task_id)) => tracing::error!(
1665 target: "mcp::tools",
1666 panic = %payload,
1667 task_id = %task_id,
1668 "tool handler panicked; returning an error result"
1669 ),
1670 (None, Some(payload), None) => tracing::error!(
1671 target: "mcp::tools",
1672 panic = %payload,
1673 "tool handler panicked; returning an error result"
1674 ),
1675 (None, None, Some(task_id)) => tracing::error!(
1676 target: "mcp::tools",
1677 task_id = %task_id,
1678 "tool handler panicked; returning an error result"
1679 ),
1680 (None, None, None) => tracing::error!(
1681 target: "mcp::tools",
1682 "tool handler panicked; returning an error result"
1683 ),
1684 }
1685 }
1686
1687 /// Effective SEP-2549 cache scope to emit alongside a TTL hint.
1688 ///
1689 /// Returns the configured scope, or `private` (the conservative choice)
1690 /// when a TTL is being emitted without an explicit scope. Returns `None`
1691 /// when no TTL is emitted and no scope is configured, so responses
1692 /// without hints stay hint-free.
1693 fn effective_cache_scope(&self, ttl_ms: Option<u64>) -> Option<CacheScope> {
1694 self.inner
1695 .cache_scope
1696 .or_else(|| ttl_ms.map(|_| CacheScope::Private))
1697 }
1698
1699 /// Fill in SEP-2549 caching hints on a resources/read result.
1700 ///
1701 /// Handler-set values win; the router-level `read_ttl` and `cache_scope`
1702 /// configuration only fills fields the handler left unset.
1703 fn apply_read_cache_hints(&self, mut result: ReadResourceResult) -> ReadResourceResult {
1704 if result.ttl_ms.is_none() {
1705 result.ttl_ms = self.inner.read_ttl_ms;
1706 }
1707 if result.cache_scope.is_none() {
1708 result.cache_scope = self.effective_cache_scope(result.ttl_ms);
1709 }
1710 result
1711 }
1712
1713 /// Handle an MCP request
1714 async fn handle(
1715 &self,
1716 request_id: RequestId,
1717 request: McpRequest,
1718 extensions: Extensions,
1719 ) -> Result<McpResponse> {
1720 // Enforce session state - reject requests before initialization
1721 let method = request.method_name();
1722 if !is_final_protocol_request(&extensions) && !self.session.is_request_allowed(method) {
1723 tracing::warn!(
1724 method = %method,
1725 phase = ?self.session.phase(),
1726 "Request rejected: session not initialized"
1727 );
1728 return Err(Error::JsonRpc(JsonRpcError::invalid_request(format!(
1729 "Session not initialized. Only 'initialize' and 'ping' are allowed before initialization. Got: {}",
1730 method
1731 ))));
1732 }
1733
1734 match request {
1735 McpRequest::Initialize(params) => {
1736 tracing::info!(
1737 client = %params.client_info.name,
1738 version = %params.client_info.version,
1739 "Client initializing"
1740 );
1741
1742 // HTTP and other configurable transports inject their exact
1743 // runtime allow-list. Direct router use retains the stable
1744 // default policy.
1745 let protocol_support = extensions.get::<crate::ProtocolSupport>();
1746 let requested_is_legacy = crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
1747 .contains(¶ms.protocol_version.as_str());
1748 let requested_is_supported = requested_is_legacy
1749 && protocol_support
1750 .is_none_or(|support| support.contains(¶ms.protocol_version));
1751 let protocol_version = if requested_is_supported {
1752 params.protocol_version
1753 } else {
1754 match protocol_support {
1755 None => crate::protocol::LATEST_PROTOCOL_VERSION.to_string(),
1756 Some(support) => support
1757 .versions()
1758 .iter()
1759 .find(|version| {
1760 crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
1761 .contains(&version.as_str())
1762 })
1763 .cloned()
1764 .ok_or_else(|| {
1765 Error::JsonRpc(JsonRpcError::unsupported_protocol_version(
1766 params.protocol_version,
1767 support.versions().iter().map(String::as_str),
1768 ))
1769 })?,
1770 }
1771 };
1772
1773 // Transition session state to Initializing
1774 self.session.mark_initializing();
1775 let capabilities = self.capabilities_for_protocol(Some(&protocol_version));
1776 self.session.insert(params.capabilities.clone());
1777 self.session
1778 .insert(crate::NegotiatedExtensions::from_capabilities(
1779 ¶ms.capabilities,
1780 &capabilities,
1781 ));
1782
1783 Ok(McpResponse::Initialize(InitializeResult {
1784 protocol_version,
1785 capabilities,
1786 server_info: self.implementation(),
1787 instructions: if let Some(config) = &self.inner.auto_instructions {
1788 Some(self.generate_instructions(config, &extensions))
1789 } else {
1790 self.inner.instructions.clone()
1791 },
1792 meta: None,
1793 }))
1794 }
1795
1796 McpRequest::Discover(_) => {
1797 // SEP-2575 server/discover -- stateless capability advertisement.
1798 // Unlike initialize, this does NOT transition session state and
1799 // does not require a session at all. Returns the same capability
1800 // surface plus the full set of protocol versions we can speak,
1801 // so clients can pick one and signal it via MCP-Protocol-Version
1802 // on subsequent requests.
1803 tracing::debug!("Stateless server/discover request");
1804 let server_info = self.implementation();
1805 let supported_versions = extensions.get::<crate::ProtocolSupport>().map_or_else(
1806 || {
1807 crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
1808 .iter()
1809 .map(|version| (*version).to_string())
1810 .collect()
1811 },
1812 |support| support.versions().to_vec(),
1813 );
1814 // server/discover is itself the entry point for the final
1815 // stateless lifecycle, so its advertised surface must be safe
1816 // even when this router is invoked directly without transport
1817 // metadata.
1818 let capabilities = self
1819 .capabilities_for_protocol(Some(crate::protocol::PROTOCOL_VERSION_2026_07_28));
1820 Ok(McpResponse::Discover(DiscoverResult {
1821 supported_versions,
1822 capabilities,
1823 ttl_ms: None,
1824 cache_scope: None,
1825 instructions: if let Some(config) = &self.inner.auto_instructions {
1826 Some(self.generate_instructions(config, &extensions))
1827 } else {
1828 self.inner.instructions.clone()
1829 },
1830 meta: Some(crate::protocol::ResultMeta {
1831 server_info: Some(server_info),
1832 }),
1833 }))
1834 }
1835
1836 McpRequest::ListTools(params) => {
1837 let final_protocol = is_final_protocol_request(&extensions);
1838 let final_tasks_negotiated = final_protocol
1839 && self.final_tasks_enabled()
1840 && client_declares_tasks(&extensions);
1841 let filter_context =
1842 self.capability_filter_context(&extensions, CapabilityOperation::List);
1843 let filter = self.inner.tool_filter.as_ref();
1844 let disabled = self.inner.disabled_tools.read().unwrap().clone();
1845 let is_visible = |t: &Tool| {
1846 !disabled.contains(&t.name)
1847 && !(final_protocol
1848 && matches!(t.task_support, TaskSupportMode::Required)
1849 && !final_tasks_negotiated)
1850 && filter
1851 .map(|f| f.is_visible_with_context(&filter_context, t))
1852 .unwrap_or(true)
1853 };
1854 let definition = |t: &Tool| {
1855 let mut definition = t.definition();
1856 if final_protocol {
1857 definition.execution = None;
1858 }
1859 definition
1860 };
1861
1862 // Collect static tools
1863 let mut tools: Vec<ToolDefinition> = self
1864 .inner
1865 .tools
1866 .values()
1867 .filter(|t| is_visible(t))
1868 .map(|t| definition(t))
1869 .collect();
1870
1871 // Merge dynamic tools (static tools win on name collision)
1872 #[cfg(feature = "dynamic-tools")]
1873 if let Some(ref dynamic) = self.inner.dynamic_tools {
1874 let static_names: HashSet<String> =
1875 tools.iter().map(|t| t.name.clone()).collect();
1876 for t in dynamic.list() {
1877 if !static_names.contains(&t.name) && is_visible(&t) {
1878 tools.push(definition(&t));
1879 }
1880 }
1881 }
1882
1883 tools.sort_by(|a, b| a.name.cmp(&b.name));
1884
1885 let (tools, next_cursor) =
1886 paginate(tools, params.cursor.as_deref(), self.inner.page_size)?;
1887
1888 Ok(McpResponse::ListTools(ListToolsResult {
1889 tools,
1890 next_cursor,
1891 ttl_ms: self.inner.list_ttl_ms,
1892 cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
1893 meta: None,
1894 }))
1895 }
1896
1897 McpRequest::CallTool(params) => {
1898 // Disabled tools are reported as if they don't exist.
1899 if self
1900 .inner
1901 .disabled_tools
1902 .read()
1903 .unwrap()
1904 .contains(¶ms.name)
1905 {
1906 tracing::info!(
1907 target: "mcp::tools",
1908 tool = %params.name,
1909 status = "disabled",
1910 "tool call completed"
1911 );
1912 return Err(Error::JsonRpc(JsonRpcError::method_not_found(¶ms.name)));
1913 }
1914
1915 // Look up static tools first, then dynamic
1916 let tool = self.inner.tools.get(¶ms.name).cloned();
1917 #[cfg(feature = "dynamic-tools")]
1918 let tool = tool.or_else(|| {
1919 self.inner
1920 .dynamic_tools
1921 .as_ref()
1922 .and_then(|d| d.get(¶ms.name))
1923 });
1924
1925 let tool = match tool {
1926 Some(t) => t,
1927 None => {
1928 tracing::info!(
1929 target: "mcp::tools",
1930 tool = %params.name,
1931 status = "not_found",
1932 "tool call completed"
1933 );
1934 return Err(Error::JsonRpc(JsonRpcError::method_not_found(¶ms.name)));
1935 }
1936 };
1937
1938 // Check tool filter if configured
1939 let filter_context = self.capability_filter_context(
1940 &extensions,
1941 CapabilityOperation::Access {
1942 target: ¶ms.name,
1943 },
1944 );
1945 if let Some(filter) = &self.inner.tool_filter
1946 && !filter.is_visible_with_context(&filter_context, &tool)
1947 {
1948 tracing::info!(
1949 target: "mcp::tools",
1950 tool = %params.name,
1951 status = "denied",
1952 "tool call completed"
1953 );
1954 return Err(filter.denial_error(¶ms.name));
1955 }
1956
1957 // Task creation is client-directed on the legacy protocol and
1958 // server-directed on the final protocol. `Some(None)` means
1959 // create a task using the server-selected TTL.
1960 let final_protocol = is_final_protocol_request(&extensions);
1961 let task_ttl = if final_protocol {
1962 if params.task.is_some() {
1963 return Err(Error::JsonRpc(JsonRpcError::invalid_params(
1964 "The final Tasks extension does not allow a 'task' request parameter",
1965 )));
1966 }
1967
1968 let server_enabled = self.final_tasks_enabled();
1969 let tasks_negotiated = server_enabled && client_declares_tasks(&extensions);
1970 match tool.task_support {
1971 TaskSupportMode::Required if !server_enabled => {
1972 // Match tools/list: a final-only task tool is not
1973 // part of this server's surface until it opts in.
1974 return Err(Error::JsonRpc(JsonRpcError::method_not_found(
1975 ¶ms.name,
1976 )));
1977 }
1978 TaskSupportMode::Required if !tasks_negotiated => {
1979 return Err(Error::JsonRpc(
1980 JsonRpcError::missing_required_client_capability(
1981 tasks_client_capabilities(),
1982 ),
1983 ));
1984 }
1985 TaskSupportMode::Required | TaskSupportMode::Optional
1986 if tasks_negotiated =>
1987 {
1988 Some(None)
1989 }
1990 _ => None,
1991 }
1992 } else {
1993 match (¶ms.task, tool.task_support) {
1994 (Some(_), TaskSupportMode::Forbidden) => {
1995 return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
1996 "Tool '{}' does not support async tasks",
1997 params.name
1998 ))));
1999 }
2000 (None, TaskSupportMode::Required) => {
2001 return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
2002 "Tool '{}' requires async task execution (include 'task' in params)",
2003 params.name
2004 ))));
2005 }
2006 (Some(task), _) => Some(task.ttl),
2007 (None, _) => None,
2008 }
2009 };
2010
2011 // Final 2026-07-28 requests declare client capabilities on
2012 // every request. Reject a tool before any handler work begins
2013 // when its declared requirement is not present.
2014 #[cfg(feature = "stateless")]
2015 if let Some(required) = tool.required_client_capabilities()
2016 && let Some(meta) = extensions.get::<crate::stateless::StatelessRequestMeta>()
2017 && meta.protocol_version.as_deref()
2018 == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
2019 && !meta
2020 .client_capabilities
2021 .as_ref()
2022 .is_some_and(|actual| client_capabilities_satisfy(actual, required))
2023 {
2024 return Err(Error::JsonRpc(
2025 JsonRpcError::missing_required_client_capability(required.clone()),
2026 ));
2027 }
2028
2029 if let Some(task_ttl) = task_ttl {
2030 let owner = (self.inner.task_owner_resolver)(&extensions)
2031 .into_owner()
2032 .ok_or_else(|| {
2033 self.task_error(
2034 TaskOperation::Create,
2035 None,
2036 TaskFailure::Internal("Task owner resolver failed"),
2037 )
2038 })?;
2039
2040 // Reserve a live-execution slot before allocating durable
2041 // Task state. The reservation spans creation and
2042 // preparation, so close + drain cannot miss an invocation
2043 // in the gap before its task ID is registered (#1398).
2044 let mut live_admission = if tool.live_handler.is_some() {
2045 Some(
2046 self.inner
2047 .live_task_executions
2048 .registry
2049 .admit()
2050 .ok_or_else(|| {
2051 self.task_error(
2052 TaskOperation::Create,
2053 None,
2054 TaskFailure::Internal(
2055 "Live task execution admission is closed",
2056 ),
2057 )
2058 })?,
2059 )
2060 } else {
2061 None
2062 };
2063 // Create the task
2064 let (task_id, cancellation_token) = self
2065 .inner
2066 .task_store
2067 .create_task(
2068 ¶ms.name,
2069 // A live task is never replayed, so its arguments
2070 // are not needed and are deliberately not
2071 // persisted. That is how a server keeps prompts or
2072 // credentials out of durable task storage (#1246).
2073 if tool.live_handler.is_some() {
2074 serde_json::Value::Null
2075 } else {
2076 params.arguments.clone()
2077 },
2078 task_ttl,
2079 owner,
2080 )
2081 .await
2082 .map_err(|error| {
2083 self.task_store_error(TaskOperation::Create, None, error)
2084 })?;
2085
2086 tracing::info!(task_id = %task_id, tool = %params.name, "Created async task");
2087
2088 // Host/client cancellation keeps its first-wins reason in
2089 // the live-execution registry. Store expiry is a separate
2090 // persistent signal with no reason; observe both without
2091 // letting expiry overwrite that cancellation policy.
2092 if let Some(admission) = live_admission.as_ref() {
2093 admission
2094 .cancellation()
2095 .attach_task_lifecycle(cancellation_token.clone());
2096 }
2097
2098 // Create a context for the async task execution
2099 let progress_token = params.meta.and_then(|m| m.progress_token);
2100 let ctx = self.create_context_with_extensions(
2101 request_id,
2102 progress_token,
2103 &extensions,
2104 );
2105
2106 let task_store = self.inner.task_store.clone();
2107 let task_context = match live_admission.as_ref() {
2108 Some(admission) => crate::tool::TaskContext::with_cancellation(
2109 task_id.clone(),
2110 admission.cancellation(),
2111 ),
2112 None => crate::tool::TaskContext::new(task_id.clone()),
2113 };
2114 let mut ctx = ctx;
2115 ctx.extensions_mut().insert(task_context.clone());
2116 let preparation = match tokio::select! {
2117 biased;
2118 _ = cancellation_token.cancelled() => {
2119 discard_unprepared_task(&task_store, &task_id).await;
2120 return Err(self.task_error(
2121 TaskOperation::Create,
2122 Some(&task_id),
2123 TaskFailure::Internal(
2124 "Task cancellation or expiry interrupted preparation",
2125 ),
2126 ));
2127 }
2128 preparation = tool.prepare_task(task_context, params.arguments.clone()) => {
2129 preparation
2130 }
2131 } {
2132 Ok(preparation) => preparation,
2133 Err(error) => {
2134 discard_unprepared_task(&task_store, &task_id).await;
2135 return Err(error);
2136 }
2137 };
2138 if let Some(meta) = preparation.meta {
2139 let value = serde_json::Value::Object(meta);
2140 if let Err(error) = crate::protocol::validate_meta_object(&value) {
2141 discard_unprepared_task(&task_store, &task_id).await;
2142 return Err(Error::invalid_params(format!(
2143 "Invalid task metadata: {error}"
2144 )));
2145 }
2146 let persisted = match task_store.set_task_meta(&task_id, value).await {
2147 Ok(persisted) => persisted,
2148 Err(error) => {
2149 discard_unprepared_task(&task_store, &task_id).await;
2150 return Err(self.task_store_error(
2151 TaskOperation::Create,
2152 Some(&task_id),
2153 error,
2154 ));
2155 }
2156 };
2157 if !persisted {
2158 discard_unprepared_task(&task_store, &task_id).await;
2159 return Err(self.task_error(
2160 TaskOperation::Create,
2161 Some(&task_id),
2162 TaskFailure::Internal(
2163 "Task store could not persist preparation metadata",
2164 ),
2165 ));
2166 }
2167 }
2168 ctx.extensions_mut().merge(&preparation.extensions);
2169
2170 // Spawn the task execution in the background
2171 let tool = tool.clone();
2172 let arguments = params.arguments;
2173 let task_id_clone = task_id.clone();
2174
2175 let tool_name = params.name.clone();
2176 let notifier = self.clone();
2177 let live_execution = tool.live_handler.clone().map(|live_handler| {
2178 let admission = live_admission
2179 .take()
2180 .expect("a live handler reserved execution admission");
2181 let cancellation = admission.cancellation();
2182 let handle = std::sync::Arc::new(crate::tool::LiveTask {
2183 store: task_store.clone(),
2184 error_policy: notifier.inner.task_error_policy.clone(),
2185 input_ready: tokio::sync::Notify::new(),
2186 cancellation: cancellation.clone(),
2187 });
2188 // Promotion replaces the anonymous preparation
2189 // reservation with an ID-bearing registration under a
2190 // single lock. It happens before spawn, so close +
2191 // drain cannot observe an empty registry between the
2192 // two lifecycle phases (#1398).
2193 let registration = admission.promote(task_id_clone.clone(), handle.clone());
2194 (live_handler, handle, cancellation, registration)
2195 });
2196 tokio::spawn(async move {
2197 // A live handler owns its execution: it parks inside
2198 // its own future rather than returning, so it is never
2199 // replayed and nothing else writes its terminal state
2200 // (#1246).
2201 if let Some((live_handler, handle, cancellation, registration)) =
2202 live_execution
2203 {
2204 let live_ctx =
2205 crate::tool::TaskContext::with_live(task_id_clone.clone(), handle);
2206
2207 let start = std::time::Instant::now();
2208 // The replay paths get their panic boundary from
2209 // `invoke_tool`; the live branch calls the handler
2210 // directly and had none, so a panic unwound before
2211 // any terminal state was written and left the task
2212 // at `working` forever (#1305).
2213 let outcome = if let Some(policy) = ¬ifier.inner.panic_policy {
2214 use futures::FutureExt;
2215 let called = std::panic::AssertUnwindSafe(async move {
2216 live_handler.call(ctx, live_ctx, arguments).await
2217 })
2218 .catch_unwind()
2219 .await;
2220 match called {
2221 Ok(outcome) => outcome,
2222 Err(payload) => {
2223 let message = notifier.handle_caught_panic(
2224 policy,
2225 &tool_name,
2226 Some(&task_id_clone),
2227 &*payload,
2228 );
2229 // A panic is an execution failure, not
2230 // a tool reporting a domain error, so
2231 // it fails the task rather than
2232 // completing it with `isError`.
2233 Ok(crate::tool::TaskOutcome::Failed(
2234 JsonRpcError::internal_error(message),
2235 ))
2236 }
2237 }
2238 } else {
2239 live_handler.call(ctx, live_ctx, arguments).await
2240 };
2241 let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
2242
2243 let applied = match outcome {
2244 Ok(crate::tool::TaskOutcome::Completed(result)) => notifier
2245 .complete_task_or_fail(&task_id_clone, result)
2246 .await
2247 .then_some("completed"),
2248 Ok(crate::tool::TaskOutcome::Failed(error)) => notifier
2249 .record_task_failure(&task_id_clone, error)
2250 .await
2251 .then_some("failed"),
2252 Ok(crate::tool::TaskOutcome::Cancelled { message }) => {
2253 let message = message.or_else(|| cancellation.reason());
2254 notifier
2255 .record_task_cancellation(
2256 &task_id_clone,
2257 message.as_deref(),
2258 )
2259 .await
2260 .then_some("cancelled")
2261 }
2262 // Propagating the cancellation error is the
2263 // ordinary way a live handler unwinds, so it
2264 // ends the task cancelled rather than failed.
2265 Err(crate::error::Error::TaskCancelled) => {
2266 let reason = cancellation.reason();
2267 notifier
2268 .record_task_cancellation(
2269 &task_id_clone,
2270 reason
2271 .as_deref()
2272 .or(Some("handler observed cancellation")),
2273 )
2274 .await
2275 .then_some("cancelled")
2276 }
2277 // An unclassified error is an execution
2278 // failure the handler declined to describe.
2279 Err(Error::JsonRpc(error)) => notifier
2280 .record_task_failure(&task_id_clone, error)
2281 .await
2282 .then_some("failed"),
2283 Err(_error) => {
2284 tracing::warn!(
2285 task_id = %task_id_clone,
2286 "live task handler returned an unclassified error"
2287 );
2288 let error = notifier.task_json_rpc_error(
2289 TaskOperation::Execute,
2290 Some(&task_id_clone),
2291 TaskFailure::Handler,
2292 );
2293 notifier
2294 .record_task_failure(&task_id_clone, error)
2295 .await
2296 .then_some("failed")
2297 }
2298 };
2299 // The terminal write must win before unregistering
2300 // (#1294), but the dead handle must not remain
2301 // visible through logging or notification awaits.
2302 // If the write failed, a later cancellation can
2303 // now take the store path instead of signalling a
2304 // handler that has already returned (#1305).
2305 drop(registration);
2306
2307 match applied {
2308 Some(status) => tracing::info!(
2309 target: "mcp::tools",
2310 tool = %tool_name,
2311 task_id = %task_id_clone,
2312 duration_ms,
2313 status,
2314 "live task finished"
2315 ),
2316 None => tracing::warn!(
2317 task_id = %task_id_clone,
2318 "failed to record live task outcome"
2319 ),
2320 }
2321 notifier.notify_task_state(&task_id_clone).await;
2322 return;
2323 }
2324
2325 // Check for cancellation before starting
2326 if cancellation_token.is_cancelled() {
2327 tracing::debug!(task_id = %task_id_clone, "Task cancelled before execution");
2328 notifier.notify_task_state(&task_id_clone).await;
2329 return;
2330 }
2331
2332 // Execute the tool.
2333 //
2334 // The outcome-aware call preserves an input-required
2335 // return, which parks the task until the client
2336 // answers with `tasks/update` and the router resumes
2337 // it (#1208).
2338 let start = std::time::Instant::now();
2339 // Ordinary task handlers do not own a cooperative
2340 // teardown contract. Race the invocation against the
2341 // persistent store token so expiry (and explicit
2342 // cancellation) drops even a handler that never polls.
2343 let outcome = tokio::select! {
2344 biased;
2345 _ = cancellation_token.cancelled() => None,
2346 outcome = notifier.invoke_tool(&tool, ctx, arguments, &tool_name) => {
2347 Some(outcome)
2348 }
2349 };
2350 let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
2351
2352 let Some(outcome) = outcome else {
2353 tracing::debug!(
2354 task_id = %task_id_clone,
2355 "Task execution stopped by cancellation or expiry"
2356 );
2357 notifier.notify_task_state(&task_id_clone).await;
2358 return;
2359 };
2360
2361 let result = match outcome {
2362 Ok(crate::protocol::RequestOutcome::Complete(result)) => result,
2363 Ok(crate::protocol::RequestOutcome::InputRequired(input_required)) => {
2364 notifier
2365 .park_task_for_input(&task_id_clone, input_required)
2366 .await;
2367 return;
2368 }
2369 // Preserved from the previous call path: a handler
2370 // error becomes an `isError` result, which
2371 // completes the task rather than failing it.
2372 Err(error) => CallToolResult::error(error.to_string()),
2373 };
2374
2375 if cancellation_token.is_cancelled() {
2376 tracing::debug!(task_id = %task_id_clone, "Task cancelled during execution");
2377 notifier.notify_task_state(&task_id_clone).await;
2378 } else {
2379 // A tool result carrying `isError: true` completes
2380 // the task: the tool ran and produced a domain
2381 // error. SEP-2663 reserves `failed` for execution
2382 // failures, which surface as a JSON-RPC error.
2383 let status = if result.is_error { "error" } else { "success" };
2384 if notifier.complete_task_or_fail(&task_id_clone, result).await {
2385 tracing::info!(
2386 target: "mcp::tools",
2387 tool = %tool_name,
2388 task_id = %task_id_clone,
2389 duration_ms,
2390 status,
2391 "tool call completed"
2392 );
2393 }
2394 notifier.notify_task_state(&task_id_clone).await;
2395 }
2396 });
2397
2398 let task = self
2399 .inner
2400 .task_store
2401 .get_task(&task_id)
2402 .await
2403 .map_err(|error| {
2404 self.task_store_error(TaskOperation::Create, Some(&task_id), error)
2405 })?
2406 .ok_or_else(|| {
2407 self.task_error(
2408 TaskOperation::Create,
2409 Some(&task_id),
2410 TaskFailure::Internal("Failed to retrieve created task"),
2411 )
2412 })?;
2413
2414 // The final wire is flat with `resultType: "task"`; the
2415 // legacy shape nests a `task` compatibility mirror. Pick
2416 // by protocol version rather than emitting a hybrid.
2417 if is_final_protocol_request(&extensions) {
2418 let mut metadata = crate::tasks::TaskMetadata::new(
2419 task.task_id.clone(),
2420 task.created_at.clone(),
2421 task.last_updated_at.clone(),
2422 task.ttl,
2423 );
2424 metadata.status_message = task.status_message.clone();
2425 metadata.poll_interval_ms = task.poll_interval;
2426 let mut result = crate::tasks::CreateTaskResult::new(
2427 crate::tasks::Task::new(metadata, task.status),
2428 );
2429 result.meta = task.meta.and_then(|value| value.as_object().cloned());
2430 return Ok(McpResponse::FinalCreateTask(result));
2431 }
2432 Ok(McpResponse::CreateTask(CreateTaskResult::new(task)))
2433 } else {
2434 // Extract progress token from request metadata
2435 let progress_token = params.meta.and_then(|m| m.progress_token);
2436 let ctx = self.create_context_with_extensions(
2437 request_id,
2438 progress_token,
2439 &extensions,
2440 );
2441 #[cfg(feature = "stateless")]
2442 let ctx = {
2443 let mut ctx = ctx;
2444 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2445 params.input_responses,
2446 params.request_state,
2447 ));
2448 ctx
2449 };
2450
2451 let start = std::time::Instant::now();
2452 let outcome = self
2453 .invoke_tool(&tool, ctx, params.arguments, ¶ms.name)
2454 .await?;
2455 let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
2456
2457 match outcome {
2458 RequestOutcome::Complete(result) => {
2459 let status = if result.is_error { "error" } else { "success" };
2460 tracing::info!(
2461 target: "mcp::tools",
2462 tool = %params.name,
2463 duration_ms,
2464 status,
2465 "tool call completed"
2466 );
2467 Ok(McpResponse::CallTool(result))
2468 }
2469 RequestOutcome::InputRequired(result) => {
2470 #[cfg(feature = "stateless")]
2471 {
2472 validate_input_required_result(&extensions, &result)?;
2473 tracing::info!(
2474 target: "mcp::tools",
2475 tool = %params.name,
2476 duration_ms,
2477 status = "input_required",
2478 "tool call requires client input"
2479 );
2480 Ok(McpResponse::InputRequired(result))
2481 }
2482 #[cfg(not(feature = "stateless"))]
2483 {
2484 let _ = result;
2485 Err(Error::invalid_params(
2486 "InputRequiredResult support was not compiled",
2487 ))
2488 }
2489 }
2490 }
2491 }
2492 }
2493
2494 McpRequest::ListResources(params) => {
2495 let filter_context =
2496 self.capability_filter_context(&extensions, CapabilityOperation::List);
2497 let disabled = self.inner.disabled_resources.read().unwrap().clone();
2498 let is_visible = |r: &Resource| -> bool {
2499 !disabled.contains(&r.uri)
2500 && self
2501 .inner
2502 .resource_filter
2503 .as_ref()
2504 .map(|f| f.is_visible_with_context(&filter_context, r))
2505 .unwrap_or(true)
2506 };
2507
2508 let mut resources: Vec<ResourceDefinition> = self
2509 .inner
2510 .resources
2511 .values()
2512 .filter(|r| is_visible(r))
2513 .map(|r| r.definition())
2514 .collect();
2515
2516 // Merge dynamic resources (static resources win on URI collision)
2517 #[cfg(feature = "dynamic-tools")]
2518 if let Some(ref dynamic) = self.inner.dynamic_resources {
2519 let static_uris: HashSet<String> =
2520 resources.iter().map(|r| r.uri.clone()).collect();
2521 for r in dynamic.list() {
2522 if !static_uris.contains(&r.uri) && is_visible(&r) {
2523 resources.push(r.definition());
2524 }
2525 }
2526 }
2527
2528 resources.sort_by(|a, b| a.uri.cmp(&b.uri));
2529
2530 let (resources, next_cursor) =
2531 paginate(resources, params.cursor.as_deref(), self.inner.page_size)?;
2532
2533 Ok(McpResponse::ListResources(ListResourcesResult {
2534 resources,
2535 next_cursor,
2536 ttl_ms: self.inner.list_ttl_ms,
2537 cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2538 meta: None,
2539 }))
2540 }
2541
2542 McpRequest::ListResourceTemplates(params) => {
2543 let filter_context =
2544 self.capability_filter_context(&extensions, CapabilityOperation::List);
2545 #[cfg(feature = "dynamic-tools")]
2546 let static_patterns: HashSet<String> = self
2547 .inner
2548 .resource_templates
2549 .iter()
2550 .map(|template| template.uri_template.clone())
2551 .collect();
2552 let mut resource_templates: Vec<ResourceTemplateDefinition> = self
2553 .inner
2554 .resource_templates
2555 .iter()
2556 .filter(|template| self.resource_template_is_visible(&filter_context, template))
2557 .map(|t| t.definition())
2558 .collect();
2559
2560 // Resolve static/dynamic precedence before filtering. A hidden
2561 // static template still shadows a dynamic template with the
2562 // same pattern, so policy denial cannot reveal a fallback.
2563 #[cfg(feature = "dynamic-tools")]
2564 if let Some(ref dynamic) = self.inner.dynamic_resource_templates {
2565 for t in dynamic.list() {
2566 if !static_patterns.contains(&t.uri_template)
2567 && self.resource_template_is_visible(&filter_context, &t)
2568 {
2569 resource_templates.push(t.definition());
2570 }
2571 }
2572 }
2573
2574 resource_templates.sort_by(|a, b| a.uri_template.cmp(&b.uri_template));
2575
2576 let (resource_templates, next_cursor) = paginate(
2577 resource_templates,
2578 params.cursor.as_deref(),
2579 self.inner.page_size,
2580 )?;
2581
2582 Ok(McpResponse::ListResourceTemplates(
2583 ListResourceTemplatesResult {
2584 resource_templates,
2585 next_cursor,
2586 ttl_ms: self.inner.list_ttl_ms,
2587 cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2588 meta: None,
2589 },
2590 ))
2591 }
2592
2593 McpRequest::ReadResource(params) => {
2594 // Disabled resources are reported as if they don't exist.
2595 if self
2596 .inner
2597 .disabled_resources
2598 .read()
2599 .unwrap()
2600 .contains(¶ms.uri)
2601 {
2602 return Err(Error::JsonRpc(JsonRpcError::resource_not_found(
2603 ¶ms.uri,
2604 )));
2605 }
2606 let filter_context = self.capability_filter_context(
2607 &extensions,
2608 CapabilityOperation::Access {
2609 target: ¶ms.uri,
2610 },
2611 );
2612
2613 // First, try to find a static resource
2614 if let Some(resource) = self.inner.resources.get(¶ms.uri) {
2615 // Check resource filter if configured
2616 if let Some(filter) = &self.inner.resource_filter
2617 && !filter.is_visible_with_context(&filter_context, resource)
2618 {
2619 return Err(filter.denial_error(¶ms.uri));
2620 }
2621
2622 tracing::debug!(uri = %params.uri, "Reading static resource");
2623 let ctx = self.create_context_with_extensions(request_id, None, &extensions);
2624 #[cfg(feature = "stateless")]
2625 let ctx = {
2626 let mut ctx = ctx;
2627 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2628 params.input_responses.clone(),
2629 params.request_state.clone(),
2630 ));
2631 ctx
2632 };
2633 return match resource.read_outcome_with_context(ctx).await? {
2634 RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
2635 self.apply_read_cache_hints(result),
2636 )),
2637 RequestOutcome::InputRequired(result) => {
2638 #[cfg(feature = "stateless")]
2639 {
2640 validate_input_required_result(&extensions, &result)?;
2641 Ok(McpResponse::InputRequired(result))
2642 }
2643 #[cfg(not(feature = "stateless"))]
2644 {
2645 let _ = result;
2646 Err(Error::invalid_params(
2647 "InputRequiredResult support was not compiled",
2648 ))
2649 }
2650 }
2651 };
2652 }
2653
2654 // Try dynamic resources
2655 #[cfg(feature = "dynamic-tools")]
2656 #[allow(clippy::collapsible_if)]
2657 if let Some(ref dynamic) = self.inner.dynamic_resources {
2658 if let Some(resource) = dynamic.get(¶ms.uri) {
2659 if let Some(filter) = &self.inner.resource_filter
2660 && !filter.is_visible_with_context(&filter_context, &resource)
2661 {
2662 return Err(filter.denial_error(¶ms.uri));
2663 }
2664 tracing::debug!(uri = %params.uri, "Reading dynamic resource");
2665 let ctx =
2666 self.create_context_with_extensions(request_id, None, &extensions);
2667 #[cfg(feature = "stateless")]
2668 let ctx = {
2669 let mut ctx = ctx;
2670 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2671 params.input_responses.clone(),
2672 params.request_state.clone(),
2673 ));
2674 ctx
2675 };
2676 return match resource.read_outcome_with_context(ctx).await? {
2677 RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
2678 self.apply_read_cache_hints(result),
2679 )),
2680 RequestOutcome::InputRequired(result) => {
2681 #[cfg(feature = "stateless")]
2682 {
2683 validate_input_required_result(&extensions, &result)?;
2684 Ok(McpResponse::InputRequired(result))
2685 }
2686 #[cfg(not(feature = "stateless"))]
2687 {
2688 let _ = result;
2689 Err(Error::invalid_params(
2690 "InputRequiredResult support was not compiled",
2691 ))
2692 }
2693 }
2694 };
2695 }
2696 }
2697
2698 // Try static templates
2699 for template in &self.inner.resource_templates {
2700 if let Some(variables) = template.match_uri(¶ms.uri) {
2701 self.authorize_resource_template(&filter_context, template, ¶ms.uri)?;
2702 tracing::debug!(
2703 uri = %params.uri,
2704 template = %template.uri_template,
2705 "Reading resource via template"
2706 );
2707 let ctx =
2708 self.create_context_with_extensions(request_id, None, &extensions);
2709 #[cfg(feature = "stateless")]
2710 let ctx = {
2711 let mut ctx = ctx;
2712 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2713 params.input_responses.clone(),
2714 params.request_state.clone(),
2715 ));
2716 ctx
2717 };
2718 return match template
2719 .read_outcome_with_context(ctx, ¶ms.uri, variables)
2720 .await?
2721 {
2722 RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
2723 self.apply_read_cache_hints(result),
2724 )),
2725 RequestOutcome::InputRequired(result) => {
2726 #[cfg(feature = "stateless")]
2727 {
2728 validate_input_required_result(&extensions, &result)?;
2729 Ok(McpResponse::InputRequired(result))
2730 }
2731 #[cfg(not(feature = "stateless"))]
2732 {
2733 let _ = result;
2734 Err(Error::invalid_params(
2735 "InputRequiredResult support was not compiled",
2736 ))
2737 }
2738 }
2739 };
2740 }
2741 }
2742
2743 // Try dynamic templates
2744 #[cfg(feature = "dynamic-tools")]
2745 #[allow(clippy::collapsible_if)]
2746 if let Some(ref dynamic) = self.inner.dynamic_resource_templates {
2747 if let Some((template, variables)) = dynamic.match_uri(¶ms.uri) {
2748 self.authorize_resource_template(&filter_context, &template, ¶ms.uri)?;
2749 tracing::debug!(
2750 uri = %params.uri,
2751 template = %template.uri_template,
2752 "Reading resource via dynamic template"
2753 );
2754 let ctx =
2755 self.create_context_with_extensions(request_id, None, &extensions);
2756 #[cfg(feature = "stateless")]
2757 let ctx = {
2758 let mut ctx = ctx;
2759 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2760 params.input_responses.clone(),
2761 params.request_state.clone(),
2762 ));
2763 ctx
2764 };
2765 return match template
2766 .read_outcome_with_context(ctx, ¶ms.uri, variables)
2767 .await?
2768 {
2769 RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
2770 self.apply_read_cache_hints(result),
2771 )),
2772 RequestOutcome::InputRequired(result) => {
2773 #[cfg(feature = "stateless")]
2774 {
2775 validate_input_required_result(&extensions, &result)?;
2776 Ok(McpResponse::InputRequired(result))
2777 }
2778 #[cfg(not(feature = "stateless"))]
2779 {
2780 let _ = result;
2781 Err(Error::invalid_params(
2782 "InputRequiredResult support was not compiled",
2783 ))
2784 }
2785 }
2786 };
2787 }
2788 }
2789
2790 // No match found
2791 Err(Error::JsonRpc(JsonRpcError::resource_not_found(
2792 ¶ms.uri,
2793 )))
2794 }
2795
2796 McpRequest::SubscribeResource(params) => {
2797 self.authorize_static_resource_subscription(&extensions, ¶ms.uri)?;
2798
2799 tracing::debug!(uri = %params.uri, "Subscribing to resource");
2800 self.subscribe(¶ms.uri);
2801
2802 Ok(McpResponse::SubscribeResource(EmptyResult {}))
2803 }
2804
2805 McpRequest::UnsubscribeResource(params) => {
2806 // Authorize before consulting or mutating membership. Otherwise
2807 // success for an existing subscription and denial for an
2808 // unowned URI would disclose session state for hidden resources.
2809 self.authorize_static_resource_subscription(&extensions, ¶ms.uri)?;
2810 self.unsubscribe(¶ms.uri);
2811
2812 tracing::debug!(uri = %params.uri, "Unsubscribing from resource");
2813
2814 Ok(McpResponse::UnsubscribeResource(EmptyResult {}))
2815 }
2816
2817 McpRequest::ListPrompts(params) => {
2818 #[cfg(feature = "dynamic-tools")]
2819 if let Some(initializer) = &self.inner.prompt_initializer {
2820 initializer()?;
2821 }
2822 let filter_context =
2823 self.capability_filter_context(&extensions, CapabilityOperation::List);
2824 let disabled = self.inner.disabled_prompts.read().unwrap().clone();
2825 let is_visible = |p: &Prompt| -> bool {
2826 !disabled.contains(&p.name)
2827 && self
2828 .inner
2829 .prompt_filter
2830 .as_ref()
2831 .map(|f| f.is_visible_with_context(&filter_context, p))
2832 .unwrap_or(true)
2833 };
2834
2835 let mut prompts: Vec<PromptDefinition> = self
2836 .inner
2837 .prompts
2838 .values()
2839 .filter(|p| is_visible(p))
2840 .map(|p| p.definition())
2841 .collect();
2842
2843 // Merge dynamic prompts (static prompts win on name collision)
2844 #[cfg(feature = "dynamic-tools")]
2845 if let Some(ref dynamic) = self.inner.dynamic_prompts {
2846 let static_names: HashSet<String> =
2847 prompts.iter().map(|p| p.name.clone()).collect();
2848 for p in dynamic.list() {
2849 if !static_names.contains(&p.name) && is_visible(&p) {
2850 prompts.push(p.definition());
2851 }
2852 }
2853 }
2854
2855 prompts.sort_by(|a, b| a.name.cmp(&b.name));
2856
2857 let (prompts, next_cursor) =
2858 paginate(prompts, params.cursor.as_deref(), self.inner.page_size)?;
2859
2860 Ok(McpResponse::ListPrompts(ListPromptsResult {
2861 prompts,
2862 next_cursor,
2863 ttl_ms: self.inner.list_ttl_ms,
2864 cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2865 meta: None,
2866 }))
2867 }
2868
2869 McpRequest::GetPrompt(params) => {
2870 #[cfg(feature = "dynamic-tools")]
2871 if let Some(initializer) = &self.inner.prompt_initializer {
2872 initializer()?;
2873 }
2874 // Disabled prompts are reported as if they don't exist.
2875 if self
2876 .inner
2877 .disabled_prompts
2878 .read()
2879 .unwrap()
2880 .contains(¶ms.name)
2881 {
2882 return Err(prompt_not_found(¶ms.name));
2883 }
2884
2885 // Look up static prompts first, then dynamic
2886 let prompt = self.inner.prompts.get(¶ms.name).cloned();
2887 #[cfg(feature = "dynamic-tools")]
2888 let prompt = prompt.or_else(|| {
2889 self.inner
2890 .dynamic_prompts
2891 .as_ref()
2892 .and_then(|d| d.get(¶ms.name))
2893 });
2894 let prompt = prompt.ok_or_else(|| prompt_not_found(¶ms.name))?;
2895
2896 // Check prompt filter if configured
2897 let filter_context = self.capability_filter_context(
2898 &extensions,
2899 CapabilityOperation::Access {
2900 target: ¶ms.name,
2901 },
2902 );
2903 if let Some(filter) = &self.inner.prompt_filter
2904 && !filter.is_visible_with_context(&filter_context, &prompt)
2905 {
2906 return Err(filter.denial_error(¶ms.name));
2907 }
2908
2909 // Before dispatch, so every path shares one check: layered and
2910 // unlayered, ordinary and MRTR. A handler never sees a request
2911 // missing an argument it declared required (#1281).
2912 let missing =
2913 crate::prompt::missing_required_arguments(&prompt.arguments, ¶ms.arguments);
2914 if !missing.is_empty() {
2915 return Err(Error::JsonRpc(crate::prompt::missing_arguments_error(
2916 ¶ms.name,
2917 &missing,
2918 )));
2919 }
2920
2921 tracing::debug!(name = %params.name, "Getting prompt");
2922 let ctx = self.create_context_with_extensions(request_id, None, &extensions);
2923 #[cfg(feature = "stateless")]
2924 let ctx = {
2925 let mut ctx = ctx;
2926 ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2927 params.input_responses,
2928 params.request_state,
2929 ));
2930 ctx
2931 };
2932 let outcome = prompt
2933 .get_outcome_with_context(ctx, params.arguments)
2934 .await?;
2935
2936 match outcome {
2937 RequestOutcome::Complete(result) => Ok(McpResponse::GetPrompt(result)),
2938 RequestOutcome::InputRequired(result) => {
2939 #[cfg(feature = "stateless")]
2940 {
2941 validate_input_required_result(&extensions, &result)?;
2942 Ok(McpResponse::InputRequired(result))
2943 }
2944 #[cfg(not(feature = "stateless"))]
2945 {
2946 let _ = result;
2947 Err(Error::invalid_params(
2948 "InputRequiredResult support was not compiled",
2949 ))
2950 }
2951 }
2952 }
2953 }
2954
2955 McpRequest::Ping => Ok(McpResponse::Pong(EmptyResult {})),
2956
2957 McpRequest::GetTaskInfo(params) => {
2958 if is_final_protocol_request(&extensions) {
2959 self.require_negotiated_tasks(&extensions, "tasks/get")?;
2960 self.authorize_task(TaskOperation::Get, ¶ms.task_id, &extensions)
2961 .await?;
2962 return self.final_get_task(¶ms.task_id, &extensions).await;
2963 }
2964 self.authorize_task(TaskOperation::Get, ¶ms.task_id, &extensions)
2965 .await?;
2966
2967 // SEP-2663 DetailedTask: `tasks/get` carries the
2968 // status-discriminated payload inline. `completed` includes
2969 // the result the synchronous request would have returned;
2970 // `failed` includes the JSON-RPC error. This replaced the
2971 // removed blocking `tasks/result` method as the way clients
2972 // retrieve a task's outcome.
2973 let Some((mut task, result, error)) = self
2974 .inner
2975 .task_store
2976 .get_task_result(¶ms.task_id)
2977 .await
2978 .map_err(|error| {
2979 self.task_store_error(TaskOperation::Get, Some(¶ms.task_id), error)
2980 })?
2981 else {
2982 // Present when it was authorized, absent now, so it
2983 // expired in between (#1249).
2984 return Err(self
2985 .classify_absent_task(TaskOperation::Get, ¶ms.task_id, &extensions)
2986 .await);
2987 };
2988
2989 match task.status {
2990 TaskStatus::Completed => task.result = result,
2991 TaskStatus::Failed => {
2992 // The store preserves the structured error, so the
2993 // original code and data survive to the client instead
2994 // of being flattened into an internal-error message.
2995 task.error = Some(
2996 error.unwrap_or_else(|| JsonRpcError::internal_error("Task failed")),
2997 );
2998 }
2999 _ => {}
3000 }
3001
3002 Ok(McpResponse::GetTaskInfo(task))
3003 }
3004
3005 McpRequest::UpdateTask(params) => {
3006 if is_final_protocol_request(&extensions) {
3007 self.require_negotiated_tasks(&extensions, "tasks/update")?;
3008 self.authorize_task(TaskOperation::Update, ¶ms.task_id, &extensions)
3009 .await?;
3010 // Partial responses are the normal case: the store
3011 // consumes what matches an outstanding request and ignores
3012 // unknown, already-answered, and superseded keys.
3013 let Some(applied) = self
3014 .inner
3015 .task_store
3016 .apply_input_responses(
3017 ¶ms.task_id,
3018 decode_input_responses(self, ¶ms.task_id, ¶ms.input_responses)?,
3019 )
3020 .await
3021 .map_err(|error| {
3022 self.task_store_error(
3023 TaskOperation::Update,
3024 Some(¶ms.task_id),
3025 error,
3026 )
3027 })?
3028 else {
3029 // Nothing left to apply. A task the store still knows
3030 // and has not expired is a late or duplicate update,
3031 // so it gets the ordinary empty acknowledgement, which
3032 // makes a client retry idempotent (#1249).
3033 let presence = self
3034 .task_presence(TaskOperation::Update, ¶ms.task_id)
3035 .await?;
3036 return match presence {
3037 crate::async_task::TaskPresence::Present { .. } => Ok(
3038 McpResponse::FinalTaskAck(crate::tasks::TaskAcknowledgement::new()),
3039 ),
3040 absent => Err(self.classify_absent_presence(
3041 TaskOperation::Update,
3042 ¶ms.task_id,
3043 &extensions,
3044 absent,
3045 )),
3046 };
3047 };
3048 // Answering the last outstanding request resumes the task,
3049 // so the status a subscriber sees changes here even though
3050 // the ack itself is empty.
3051 self.notify_task_state(¶ms.task_id).await;
3052 // The client answered everything outstanding, so re-invoke
3053 // the handler with the accumulated responses (#1208). A
3054 // partial answer leaves the task parked for the rest.
3055 //
3056 // `is_complete` is also true when nothing was outstanding
3057 // in the first place, so on its own it would resume a task
3058 // that never parked. Requiring this update to have answered
3059 // something is what distinguishes a real
3060 // `input_required -> working` transition from a stray,
3061 // duplicate, or already-satisfied update, either of which
3062 // would otherwise start a second handler alongside the one
3063 // still running (#1246).
3064 if !applied.accepted.is_empty() && applied.is_complete() {
3065 // A live handler is parked inside its own future and
3066 // must be woken, not replayed. Waking only after the
3067 // store has committed is what guarantees it cannot
3068 // observe an answer that was not recorded (#1246).
3069 if !self.wake_live_task(¶ms.task_id) {
3070 self.resume_task(¶ms.task_id).await;
3071 }
3072 }
3073 return Ok(McpResponse::FinalTaskAck(
3074 crate::tasks::TaskAcknowledgement::new(),
3075 ));
3076 }
3077
3078 self.authorize_task(TaskOperation::Update, ¶ms.task_id, &extensions)
3079 .await?;
3080
3081 // Input responses reach the store on this path exactly as they
3082 // do on the final path above. The spec allowance for ignoring
3083 // `inputResponses` covers keys that are not outstanding, not
3084 // every key, so dropping them wholesale left a server whose
3085 // store models input requests with a working flow on
3086 // 2026-07-28 and a silent stall on 2025-11-25 (#1188).
3087 let Some(applied) = self
3088 .inner
3089 .task_store
3090 .apply_input_responses(
3091 ¶ms.task_id,
3092 decode_input_responses(self, ¶ms.task_id, ¶ms.input_responses)?,
3093 )
3094 .await
3095 .map_err(|error| {
3096 self.task_store_error(TaskOperation::Update, Some(¶ms.task_id), error)
3097 })?
3098 else {
3099 // Nothing left to apply. A task the store still knows and
3100 // has not expired is a late or duplicate update, so it
3101 // gets the ordinary empty acknowledgement, which makes a
3102 // client retry idempotent rather than a not-found (#1249).
3103 let presence = self
3104 .task_presence(TaskOperation::Update, ¶ms.task_id)
3105 .await?;
3106 return match presence {
3107 crate::async_task::TaskPresence::Present { .. } => {
3108 Ok(McpResponse::UpdateTask(EmptyResult {}))
3109 }
3110 absent => Err(self.classify_absent_presence(
3111 TaskOperation::Update,
3112 ¶ms.task_id,
3113 &extensions,
3114 absent,
3115 )),
3116 };
3117 };
3118 // A final-protocol subscriber watching this task should see it
3119 // resume regardless of which lifecycle the updating client
3120 // used. Self-guards when the extension is not enabled.
3121 self.notify_task_state(¶ms.task_id).await;
3122 // A live task parks inside its own future, so answering its
3123 // input on this lifecycle has to wake it just as the final
3124 // path does, or it waits forever (#1246). Waking only after
3125 // the store has committed is what guarantees the handler
3126 // cannot observe an unrecorded answer.
3127 if !applied.accepted.is_empty() && applied.is_complete() {
3128 self.wake_live_task(¶ms.task_id);
3129 }
3130 Ok(McpResponse::UpdateTask(EmptyResult {}))
3131 }
3132
3133 McpRequest::CancelTask(params) => {
3134 if is_final_protocol_request(&extensions) {
3135 self.require_negotiated_tasks(&extensions, "tasks/cancel")?;
3136 self.authorize_task(TaskOperation::Cancel, ¶ms.task_id, &extensions)
3137 .await?;
3138 // A live task is signalled and left non-terminal: its
3139 // handler owns the teardown and reports when it actually
3140 // stopped, so completion can still legitimately win the
3141 // race. SEP-2663 describes cancellation as eventually
3142 // consistent, which is exactly this (#1246).
3143 if self.signal_live_cancellation(¶ms.task_id, params.reason.as_deref()) {
3144 self.notify_task_state(¶ms.task_id).await;
3145 return Ok(McpResponse::FinalTaskAck(
3146 crate::tasks::TaskAcknowledgement::new(),
3147 ));
3148 }
3149 // The final ack does not require a terminal transition:
3150 // cancelling an already-terminal task is acknowledged, and
3151 // the observable status is polled via `tasks/get`.
3152 let cancelled = self
3153 .inner
3154 .task_store
3155 .cancel_task(¶ms.task_id, params.reason.as_deref())
3156 .await
3157 .map_err(|error| {
3158 self.task_store_error(
3159 TaskOperation::Cancel,
3160 Some(¶ms.task_id),
3161 error,
3162 )
3163 })?;
3164 if cancelled.is_none() {
3165 return Err(self
3166 .classify_absent_task(
3167 TaskOperation::Cancel,
3168 ¶ms.task_id,
3169 &extensions,
3170 )
3171 .await);
3172 }
3173 self.notify_task_state(¶ms.task_id).await;
3174 return Ok(McpResponse::FinalTaskAck(
3175 crate::tasks::TaskAcknowledgement::new(),
3176 ));
3177 }
3178
3179 self.authorize_task(TaskOperation::Cancel, ¶ms.task_id, &extensions)
3180 .await?;
3181
3182 // Same reasoning as the final path: a live task owns its own
3183 // teardown, so it is signalled and left non-terminal (#1246).
3184 if self.signal_live_cancellation(¶ms.task_id, params.reason.as_deref()) {
3185 self.notify_task_state(¶ms.task_id).await;
3186 return Ok(McpResponse::CancelTask(EmptyResult {}));
3187 }
3188
3189 // First check if the task exists and is not already terminal
3190 let Some(current) = self
3191 .inner
3192 .task_store
3193 .get_task(¶ms.task_id)
3194 .await
3195 .map_err(|error| {
3196 self.task_store_error(TaskOperation::Cancel, Some(¶ms.task_id), error)
3197 })?
3198 else {
3199 return Err(self
3200 .classify_absent_task(TaskOperation::Cancel, ¶ms.task_id, &extensions)
3201 .await);
3202 };
3203
3204 if current.status.is_terminal() {
3205 return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
3206 "Task {} is already in terminal state: {}",
3207 params.task_id, current.status
3208 ))));
3209 }
3210
3211 let cancelled = self
3212 .inner
3213 .task_store
3214 .cancel_task(¶ms.task_id, params.reason.as_deref())
3215 .await
3216 .map_err(|error| {
3217 self.task_store_error(TaskOperation::Cancel, Some(¶ms.task_id), error)
3218 })?;
3219 if cancelled.is_none() {
3220 return Err(self
3221 .classify_absent_task(TaskOperation::Cancel, ¶ms.task_id, &extensions)
3222 .await);
3223 }
3224
3225 // SEP-2663 (final): the cancel acknowledgment MUST be an empty
3226 // result. The observable status is polled via `tasks/get` and
3227 // may remain non-terminal after this ack.
3228 Ok(McpResponse::CancelTask(EmptyResult {}))
3229 }
3230
3231 McpRequest::SetLoggingLevel(params) => {
3232 tracing::debug!(level = ?params.level, "Client set logging level");
3233 if let Ok(mut level) = self.inner.min_log_level.write() {
3234 *level = params.level;
3235 }
3236 Ok(McpResponse::SetLoggingLevel(EmptyResult {}))
3237 }
3238
3239 McpRequest::Complete(params) => {
3240 tracing::debug!(
3241 reference = ?params.reference,
3242 argument = %params.argument.name,
3243 "Completion request"
3244 );
3245
3246 // Delegate to registered completion handler if available
3247 if let Some(ref handler) = self.inner.completion_handler {
3248 self.authorize_completion_reference(&extensions, ¶ms.reference)?;
3249 let progress_token = params
3250 .meta
3251 .as_ref()
3252 .and_then(|meta| meta.get("progressToken"))
3253 .map(|token| serde_json::from_value(token.clone()))
3254 .transpose()
3255 .map_err(|error| {
3256 Error::invalid_params(format!(
3257 "Invalid completion progress token: {error}"
3258 ))
3259 })?;
3260 let ctx = self.create_context_with_extensions(
3261 request_id,
3262 progress_token,
3263 &extensions,
3264 );
3265 let result = handler(ctx, params).await?;
3266 Ok(McpResponse::Complete(result))
3267 } else {
3268 // No completion handler registered, return empty completions
3269 Ok(McpResponse::Complete(CompleteResult::new(vec![])))
3270 }
3271 }
3272
3273 #[cfg(feature = "stateless")]
3274 McpRequest::SubscriptionsListen(params) => {
3275 // The stream itself is transport-owned: transports dispatch
3276 // the request here before upgrading the connection, so
3277 // `Service<RouterRequest>` middleware observes accepted and
3278 // rejected listens and the validation lives in one place
3279 // (#1182). The response is consumed by the transport, never
3280 // written to the wire.
3281 if !is_final_protocol_request(&extensions) {
3282 // A legacy peer gets exactly what the old catch-all
3283 // produced for this method.
3284 return Err(Error::JsonRpc(JsonRpcError::method_not_found(
3285 "subscriptions/listen",
3286 )));
3287 }
3288 let Some(requested) = params.notifications else {
3289 return Err(Error::JsonRpc(JsonRpcError::invalid_params(
3290 "subscriptions/listen requires a notifications filter",
3291 )));
3292 };
3293 // SEP-2663: task status notifications require the declared
3294 // extension, the same answer the three task methods give.
3295 if requested.task_ids.is_some() && !client_declares_tasks(&extensions) {
3296 return Err(Error::JsonRpc(
3297 JsonRpcError::missing_required_client_capability(
3298 tasks_client_capabilities(),
3299 ),
3300 ));
3301 }
3302 if self.final_tasks_enabled()
3303 && let Some(task_ids) = requested.task_ids.as_deref()
3304 {
3305 for task_id in task_ids {
3306 self.authorize_task_subscription(task_id, &extensions)
3307 .await?;
3308 }
3309 }
3310 let notifications = crate::transport::subscriptions::accepted_subscription_filter(
3311 requested,
3312 self.final_tasks_enabled(),
3313 );
3314 Ok(McpResponse::SubscriptionsAccepted(
3315 crate::protocol::SubscriptionsAcceptedResult { notifications },
3316 ))
3317 }
3318
3319 McpRequest::Unknown { method, .. } => {
3320 Err(Error::JsonRpc(JsonRpcError::method_not_found(&method)))
3321 }
3322 _ => Err(Error::JsonRpc(JsonRpcError::method_not_found(
3323 "unknown method",
3324 ))),
3325 }
3326 }
3327
3328 /// Handle an MCP notification (no response expected)
3329 pub fn handle_notification(&self, notification: McpNotification) {
3330 match notification {
3331 McpNotification::Initialized => {
3332 let phase_before = self.session.phase();
3333 if self.session.mark_initialized() {
3334 if phase_before == crate::session::SessionPhase::Uninitialized {
3335 tracing::info!(
3336 "Session initialized from uninitialized state (race resolved)"
3337 );
3338 } else {
3339 tracing::info!("Session initialized, entering operation phase");
3340 }
3341 } else if phase_before == crate::session::SessionPhase::Uninitialized {
3342 tracing::warn!(
3343 "Ignoring initialized notification: no initialize request has been \
3344 received for this session"
3345 );
3346 } else {
3347 tracing::warn!(
3348 phase = ?self.session.phase(),
3349 "Received initialized notification in unexpected state"
3350 );
3351 }
3352 }
3353 McpNotification::Cancelled(params) => {
3354 if let Some(ref request_id) = params.request_id {
3355 if self.cancel_request(request_id) {
3356 tracing::info!(
3357 request_id = ?request_id,
3358 reason = ?params.reason,
3359 "Request cancelled"
3360 );
3361 } else {
3362 tracing::debug!(
3363 request_id = ?request_id,
3364 reason = ?params.reason,
3365 "Cancellation requested for unknown request"
3366 );
3367 }
3368 } else {
3369 tracing::debug!(
3370 reason = ?params.reason,
3371 "Cancellation notification received without request_id"
3372 );
3373 }
3374 }
3375 McpNotification::Progress(params) => {
3376 tracing::trace!(
3377 token = ?params.progress_token,
3378 progress = params.progress,
3379 total = ?params.total,
3380 "Progress notification"
3381 );
3382 // Client-to-server progress notifications are unusual but
3383 // valid through 2025-11-25. The final 2026-07-28 schema
3384 // removes ProgressNotification from ClientNotification
3385 // entirely -- clients no longer send this. Notifications are
3386 // fire-and-forget with no response to reject with, so an
3387 // off-spec one arriving here is simply logged and ignored
3388 // rather than rejected, regardless of negotiated version.
3389 }
3390 McpNotification::RootsListChanged => {
3391 tracing::info!("Client roots list changed");
3392 // Server should re-request roots if needed
3393 // This is handled by the application layer
3394 }
3395 McpNotification::Unknown { method, .. } => {
3396 tracing::debug!(method = %method, "Unknown notification received");
3397 }
3398 _ => {
3399 tracing::debug!("Unrecognized notification variant received");
3400 }
3401 }
3402 }
3403}
3404
3405impl Default for McpRouter {
3406 fn default() -> Self {
3407 Self::new()
3408 }
3409}
3410
3411// =============================================================================
3412// Tower Service implementation
3413// =============================================================================
3414
3415// Re-export Extensions from context for backwards compatibility
3416pub use crate::context::Extensions;
3417
3418/// A map of tool names to their annotations, for use by middleware.
3419///
3420/// This is automatically inserted into [`RouterRequest::extensions`] for
3421/// `tools/call` requests, allowing middleware to inspect tool safety hints
3422/// (e.g., `read_only_hint`, `destructive_hint`) without needing direct
3423/// access to the router's tool registry.
3424///
3425/// # Example
3426///
3427/// ```rust,ignore
3428/// use tower_mcp::router::ToolAnnotationsMap;
3429/// use tower_mcp::protocol::McpRequest;
3430///
3431/// // In a middleware Service::call():
3432/// fn call(&mut self, req: RouterRequest) -> Self::Future {
3433/// if let McpRequest::CallTool(params) = &req.inner {
3434/// if let Some(map) = req.extensions.get::<ToolAnnotationsMap>() {
3435/// let annotations = map.get(¶ms.name);
3436/// // Check annotations.read_only_hint, destructive_hint, etc.
3437/// }
3438/// }
3439/// self.inner.call(req)
3440/// }
3441/// ```
3442#[derive(Debug, Clone)]
3443pub struct ToolAnnotationsMap {
3444 map: Arc<HashMap<String, ToolAnnotations>>,
3445}
3446
3447impl ToolAnnotationsMap {
3448 /// Look up annotations for a tool by name.
3449 ///
3450 /// Returns `None` if the tool has no annotations or doesn't exist.
3451 pub fn get(&self, tool_name: &str) -> Option<&ToolAnnotations> {
3452 self.map.get(tool_name)
3453 }
3454
3455 /// Check if a tool is read-only (does not modify state).
3456 ///
3457 /// Returns `false` if the tool has no annotations or doesn't exist
3458 /// (the MCP spec default for `readOnlyHint` is `false`).
3459 pub fn is_read_only(&self, tool_name: &str) -> bool {
3460 self.map.get(tool_name).is_some_and(|a| a.read_only_hint)
3461 }
3462
3463 /// Check if a tool may have destructive effects.
3464 ///
3465 /// Returns `true` if the tool has no annotations or doesn't exist
3466 /// (the MCP spec default for `destructiveHint` is `true`).
3467 pub fn is_destructive(&self, tool_name: &str) -> bool {
3468 self.map.get(tool_name).is_none_or(|a| a.destructive_hint)
3469 }
3470
3471 /// Check if a tool is idempotent.
3472 ///
3473 /// Returns `false` if the tool has no annotations or doesn't exist
3474 /// (the MCP spec default for `idempotentHint` is `false`).
3475 pub fn is_idempotent(&self, tool_name: &str) -> bool {
3476 self.map.get(tool_name).is_some_and(|a| a.idempotent_hint)
3477 }
3478}
3479
3480/// Request type for the tower Service implementation.
3481///
3482/// # Preserving extensions in middleware
3483///
3484/// When rewriting a request in middleware, use [`with_inner`](Self::with_inner)
3485/// or [`clone_with_inner`](Self::clone_with_inner) instead of constructing a
3486/// new `RouterRequest` directly. Constructing with `Extensions::new()` will
3487/// silently drop extensions set by earlier middleware layers (token claims,
3488/// RBAC context, etc.).
3489///
3490/// ```rust,ignore
3491/// // WRONG: drops extensions from earlier middleware
3492/// let rewritten = RouterRequest {
3493/// id: req.id.clone(),
3494/// inner: new_inner,
3495/// extensions: Extensions::new(),
3496/// };
3497///
3498/// // RIGHT: preserves extensions
3499/// let rewritten = req.with_inner(new_inner);
3500/// ```
3501#[derive(Debug, Clone)]
3502pub struct RouterRequest {
3503 /// The JSON-RPC request ID.
3504 pub id: RequestId,
3505 /// The parsed MCP request.
3506 pub inner: McpRequest,
3507 /// Type-map for passing data (e.g., `TokenClaims`) through middleware.
3508 pub extensions: Extensions,
3509}
3510
3511impl RouterRequest {
3512 /// Create a new `RouterRequest` with empty extensions.
3513 pub fn new(id: RequestId, inner: McpRequest) -> Self {
3514 Self {
3515 id,
3516 inner,
3517 extensions: Extensions::new(),
3518 }
3519 }
3520
3521 /// Replace the inner MCP request, preserving the id and extensions.
3522 ///
3523 /// This is the recommended way to rewrite requests in middleware,
3524 /// as it ensures extensions set by earlier middleware layers
3525 /// (e.g., token claims, RBAC context) are not lost.
3526 pub fn with_inner(self, inner: McpRequest) -> Self {
3527 Self {
3528 id: self.id,
3529 inner,
3530 extensions: self.extensions,
3531 }
3532 }
3533
3534 /// Replace both the id and inner MCP request, preserving extensions.
3535 ///
3536 /// Useful when middleware needs to assign a new request id
3537 /// (e.g., for fan-out or request duplication) while keeping
3538 /// the extensions from the original request.
3539 pub fn with_id_and_inner(self, id: RequestId, inner: McpRequest) -> Self {
3540 Self {
3541 id,
3542 inner,
3543 extensions: self.extensions,
3544 }
3545 }
3546
3547 /// Create a copy of this request with a different inner request,
3548 /// cloning the id and extensions from the original.
3549 ///
3550 /// Unlike [`with_inner`](Self::with_inner), this borrows `self`,
3551 /// which is useful when the original request is still needed
3552 /// (e.g., for traffic mirroring where you send the request to
3553 /// two backends).
3554 pub fn clone_with_inner(&self, inner: McpRequest) -> Self {
3555 Self {
3556 id: self.id.clone(),
3557 inner,
3558 extensions: self.extensions.clone(),
3559 }
3560 }
3561}
3562
3563/// Response type for the tower Service implementation
3564#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3565pub struct RouterResponse {
3566 /// The JSON-RPC request ID this response corresponds to.
3567 pub id: RequestId,
3568 /// The MCP response or JSON-RPC error.
3569 pub inner: std::result::Result<McpResponse, JsonRpcError>,
3570}
3571
3572impl RouterResponse {
3573 /// Returns `true` if the response contains a JSON-RPC error.
3574 ///
3575 /// Since tower-mcp services use `Error = Infallible` (errors are carried
3576 /// inside the response, not in the `Result`), this method is useful for
3577 /// middleware that needs to inspect whether a request failed -- for example,
3578 /// retry or circuit breaker middleware.
3579 ///
3580 /// # Example
3581 ///
3582 /// ```rust,ignore
3583 /// // Response-based retry predicate for tower-resilience or similar
3584 /// fn is_retriable(response: &RouterResponse) -> bool {
3585 /// response.is_error()
3586 /// }
3587 /// ```
3588 pub fn is_error(&self) -> bool {
3589 self.inner.is_err()
3590 }
3591
3592 /// Convert to JSON-RPC response
3593 pub fn into_jsonrpc(self) -> JsonRpcResponse {
3594 match self.inner {
3595 Ok(response) => match serde_json::to_value(response) {
3596 Ok(result) => JsonRpcResponse::result(self.id, result),
3597 Err(e) => {
3598 tracing::error!(error = %e, "Failed to serialize response");
3599 JsonRpcResponse::error(
3600 Some(self.id),
3601 JsonRpcError::internal_error(format!("Serialization error: {}", e)),
3602 )
3603 }
3604 },
3605 Err(error) => JsonRpcResponse::error(Some(self.id), error),
3606 }
3607 }
3608}
3609
3610/// Identifies one dispatch of one request, unique for a router's lifetime.
3611///
3612/// The request id cannot play this role: a client may reuse one that is still
3613/// in flight, and two requests sharing an id must still be tracked separately
3614/// (#1270). Minted by [`Service::call`] and passed to the handler through the
3615/// request extensions so the registration and the guard name the same entry.
3616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3617struct DispatchId(u64);
3618
3619/// One tracked dispatch in the router's in-flight registry.
3620struct InFlightDispatch {
3621 dispatch: DispatchId,
3622 token: CancellationToken,
3623}
3624
3625/// Untracks a single dispatch when the request future ends, however it ends.
3626///
3627/// Removal used to sit on the success path in [`Service::call`], so a future
3628/// dropped before that point (a timeout layer firing, an HTTP client
3629/// disconnecting, a handler unwinding) left its entry in the registry for the
3630/// process lifetime. `Drop` runs on every one of those paths.
3631struct InFlightGuard {
3632 router: McpRouter,
3633 request_id: RequestId,
3634 dispatch: DispatchId,
3635}
3636
3637impl Drop for InFlightGuard {
3638 fn drop(&mut self) {
3639 self.router
3640 .complete_dispatch(&self.request_id, self.dispatch);
3641 }
3642}
3643
3644impl Service<RouterRequest> for McpRouter {
3645 type Response = RouterResponse;
3646 type Error = std::convert::Infallible; // Errors are in the response
3647 type Future =
3648 Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
3649
3650 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
3651 Poll::Ready(Ok(()))
3652 }
3653
3654 fn call(&mut self, mut req: RouterRequest) -> Self::Future {
3655 let router = self.clone();
3656 let request_id = req.id.clone();
3657
3658 // Name the dispatch before `handle` builds its context, so the
3659 // registration inside and the guard out here refer to the same entry.
3660 let dispatch = router.next_dispatch();
3661 req.extensions.insert(dispatch);
3662
3663 Box::pin(async move {
3664 let _tracked = InFlightGuard {
3665 router: router.clone(),
3666 request_id: request_id.clone(),
3667 dispatch,
3668 };
3669 let result = router.handle(req.id, req.inner, req.extensions).await;
3670 Ok(RouterResponse {
3671 id: request_id,
3672 // Map tower-mcp errors to JSON-RPC errors: a structured
3673 // Error::JsonRpc is forwarded as-is (preserves the original
3674 // code and message); everything else is sanitized to
3675 // -32603 (Internal Error). See Error::into_json_rpc_error.
3676 inner: result.map_err(Error::into_json_rpc_error),
3677 })
3678 })
3679 }
3680}
3681
3682mod builder;
3683mod capabilities;
3684mod merge;
3685mod notify;
3686mod pagination;
3687mod policy;
3688mod task_ops;
3689
3690use capabilities::{
3691 TaskOwnerResolver, custom_task_owner_resolver, default_task_owner_resolver,
3692 final_client_capabilities,
3693};
3694use pagination::paginate;
3695use policy::panic_message;
3696
3697// Gated in `capabilities` too, for the same reason `task_ops` gates below: an
3698// unconditional import here breaks every build that is not `--all-features`.
3699#[cfg(feature = "stateless")]
3700use capabilities::client_capabilities_satisfy;
3701
3702// The cursor tests are siblings and reach these through `super`, but nothing in
3703// this module calls them directly now that `paginate` owns the encoding.
3704#[cfg(test)]
3705use pagination::{decode_cursor, encode_cursor};
3706
3707// These are named in `lib.rs`'s re-export list, so they keep the `router::`
3708// path they have always had rather than gaining a submodule in it.
3709pub use merge::{MergeConflict, MergeConflictKind, MergeConflicts};
3710pub use policy::{PanicPolicy, TaskErrorContext, TaskErrorPolicy, TaskFailure, TaskOperation};
3711
3712use task_ops::{
3713 client_declares_tasks, decode_input_responses, discard_unprepared_task,
3714 tasks_client_capabilities,
3715};
3716// Gated in `task_ops` too, so importing it unconditionally breaks the default
3717// build that `--all-features` never exercises.
3718#[cfg(feature = "stateless")]
3719use task_ops::validate_input_required_result;
3720
3721#[cfg(test)]
3722mod tests;
3723
3724#[cfg(all(test, feature = "stateless"))]
3725mod task_error_tests;
3726
3727#[cfg(test)]
3728mod cursor_property_tests;