Skip to main content

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(&params.protocol_version.as_str());
1748                let requested_is_supported = requested_is_legacy
1749                    && protocol_support
1750                        .is_none_or(|support| support.contains(&params.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                        &params.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(&params.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(&params.name)));
1913                }
1914
1915                // Look up static tools first, then dynamic
1916                let tool = self.inner.tools.get(&params.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(&params.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(&params.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: &params.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(&params.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                                &params.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 (&params.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                            &params.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) = &notifier.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, &params.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(&params.uri)
2601                {
2602                    return Err(Error::JsonRpc(JsonRpcError::resource_not_found(
2603                        &params.uri,
2604                    )));
2605                }
2606                let filter_context = self.capability_filter_context(
2607                    &extensions,
2608                    CapabilityOperation::Access {
2609                        target: &params.uri,
2610                    },
2611                );
2612
2613                // First, try to find a static resource
2614                if let Some(resource) = self.inner.resources.get(&params.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(&params.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(&params.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(&params.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(&params.uri) {
2701                        self.authorize_resource_template(&filter_context, template, &params.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, &params.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(&params.uri) {
2748                        self.authorize_resource_template(&filter_context, &template, &params.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, &params.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                    &params.uri,
2793                )))
2794            }
2795
2796            McpRequest::SubscribeResource(params) => {
2797                self.authorize_static_resource_subscription(&extensions, &params.uri)?;
2798
2799                tracing::debug!(uri = %params.uri, "Subscribing to resource");
2800                self.subscribe(&params.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, &params.uri)?;
2810                self.unsubscribe(&params.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(&params.name)
2881                {
2882                    return Err(prompt_not_found(&params.name));
2883                }
2884
2885                // Look up static prompts first, then dynamic
2886                let prompt = self.inner.prompts.get(&params.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(&params.name))
2893                });
2894                let prompt = prompt.ok_or_else(|| prompt_not_found(&params.name))?;
2895
2896                // Check prompt filter if configured
2897                let filter_context = self.capability_filter_context(
2898                    &extensions,
2899                    CapabilityOperation::Access {
2900                        target: &params.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(&params.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, &params.arguments);
2914                if !missing.is_empty() {
2915                    return Err(Error::JsonRpc(crate::prompt::missing_arguments_error(
2916                        &params.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, &params.task_id, &extensions)
2961                        .await?;
2962                    return self.final_get_task(&params.task_id, &extensions).await;
2963                }
2964                self.authorize_task(TaskOperation::Get, &params.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(&params.task_id)
2977                    .await
2978                    .map_err(|error| {
2979                        self.task_store_error(TaskOperation::Get, Some(&params.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, &params.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, &params.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                            &params.task_id,
3018                            decode_input_responses(self, &params.task_id, &params.input_responses)?,
3019                        )
3020                        .await
3021                        .map_err(|error| {
3022                            self.task_store_error(
3023                                TaskOperation::Update,
3024                                Some(&params.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, &params.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                                &params.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(&params.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(&params.task_id) {
3070                            self.resume_task(&params.task_id).await;
3071                        }
3072                    }
3073                    return Ok(McpResponse::FinalTaskAck(
3074                        crate::tasks::TaskAcknowledgement::new(),
3075                    ));
3076                }
3077
3078                self.authorize_task(TaskOperation::Update, &params.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                        &params.task_id,
3092                        decode_input_responses(self, &params.task_id, &params.input_responses)?,
3093                    )
3094                    .await
3095                    .map_err(|error| {
3096                        self.task_store_error(TaskOperation::Update, Some(&params.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, &params.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                            &params.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(&params.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(&params.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, &params.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(&params.task_id, params.reason.as_deref()) {
3144                        self.notify_task_state(&params.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(&params.task_id, params.reason.as_deref())
3156                        .await
3157                        .map_err(|error| {
3158                            self.task_store_error(
3159                                TaskOperation::Cancel,
3160                                Some(&params.task_id),
3161                                error,
3162                            )
3163                        })?;
3164                    if cancelled.is_none() {
3165                        return Err(self
3166                            .classify_absent_task(
3167                                TaskOperation::Cancel,
3168                                &params.task_id,
3169                                &extensions,
3170                            )
3171                            .await);
3172                    }
3173                    self.notify_task_state(&params.task_id).await;
3174                    return Ok(McpResponse::FinalTaskAck(
3175                        crate::tasks::TaskAcknowledgement::new(),
3176                    ));
3177                }
3178
3179                self.authorize_task(TaskOperation::Cancel, &params.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(&params.task_id, params.reason.as_deref()) {
3185                    self.notify_task_state(&params.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(&params.task_id)
3194                    .await
3195                    .map_err(|error| {
3196                        self.task_store_error(TaskOperation::Cancel, Some(&params.task_id), error)
3197                    })?
3198                else {
3199                    return Err(self
3200                        .classify_absent_task(TaskOperation::Cancel, &params.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(&params.task_id, params.reason.as_deref())
3215                    .await
3216                    .map_err(|error| {
3217                        self.task_store_error(TaskOperation::Cancel, Some(&params.task_id), error)
3218                    })?;
3219                if cancelled.is_none() {
3220                    return Err(self
3221                        .classify_absent_task(TaskOperation::Cancel, &params.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, &params.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(&params.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;