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::{Arc, RwLock};
10use std::task::{Context, Poll};
11
12use tower_service::Service;
13
14use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
15
16use crate::async_task::{MemoryTaskStore, TaskStore, TaskStoreError};
17use crate::context::{
18    CancellationToken, ClientRequesterHandle, NotificationSender, RequestContext,
19    ServerNotification,
20};
21use crate::error::{Error, JsonRpcError, Result};
22use crate::filter::{PromptFilter, ResourceFilter, ToolFilter};
23use crate::prompt::Prompt;
24use crate::protocol::*;
25#[cfg(feature = "dynamic-tools")]
26use crate::registry::{
27    DynamicPromptRegistry, DynamicPromptsInner, DynamicResourceRegistry,
28    DynamicResourceTemplateRegistry, DynamicResourceTemplatesInner, DynamicResourcesInner,
29    DynamicToolRegistry, DynamicToolsInner,
30};
31use crate::resource::{Resource, ResourceTemplate};
32use crate::session::SessionState;
33use crate::tool::Tool;
34
35/// Type alias for completion handler function
36pub(crate) type CompletionHandler = Arc<
37    dyn Fn(CompleteParams) -> Pin<Box<dyn Future<Output = Result<CompleteResult>> + Send>>
38        + Send
39        + Sync,
40>;
41
42/// Decode a pagination cursor into an offset.
43///
44/// Returns `Err` if the cursor is malformed.
45fn decode_cursor(cursor: &str) -> Result<usize> {
46    let bytes = BASE64
47        .decode(cursor)
48        .map_err(|_| Error::JsonRpc(JsonRpcError::invalid_params("Invalid pagination cursor")))?;
49    let s = String::from_utf8(bytes)
50        .map_err(|_| Error::JsonRpc(JsonRpcError::invalid_params("Invalid pagination cursor")))?;
51    s.parse::<usize>()
52        .map_err(|_| Error::JsonRpc(JsonRpcError::invalid_params("Invalid pagination cursor")))
53}
54
55/// Encode an offset into an opaque pagination cursor.
56fn encode_cursor(offset: usize) -> String {
57    BASE64.encode(offset.to_string())
58}
59
60/// Map a [`TaskStoreError`] to a JSON-RPC internal error.
61fn task_store_error(e: TaskStoreError) -> Error {
62    Error::JsonRpc(JsonRpcError::internal_error(format!(
63        "Task store error: {}",
64        e
65    )))
66}
67
68/// Whether this request is using the final, stateless 2026-07-28 lifecycle.
69///
70/// Stable sessionful requests retain the crate's legacy task behavior; final
71/// requests use extension negotiation and server-directed task creation.
72#[cfg(feature = "stateless")]
73fn is_final_protocol_request(extensions: &crate::context::Extensions) -> bool {
74    extensions
75        .get::<crate::stateless::StatelessRequestMeta>()
76        .and_then(|meta| meta.protocol_version.as_deref())
77        == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
78}
79
80#[cfg(not(feature = "stateless"))]
81fn is_final_protocol_request(_extensions: &crate::context::Extensions) -> bool {
82    false
83}
84
85/// Whether this request's client declared the final Tasks extension.
86///
87/// Final requests carry client capabilities per request, so negotiation is
88/// decided from the request itself rather than from session state.
89#[cfg(feature = "stateless")]
90fn client_declares_tasks(extensions: &crate::context::Extensions) -> bool {
91    final_client_capabilities(extensions).is_some_and(|capabilities| {
92        capabilities.extensions.as_ref().is_some_and(|declared| {
93            declared.contains_key(tower_mcp_types::protocol::TASKS_EXTENSION_ID)
94        })
95    })
96}
97
98#[cfg(not(feature = "stateless"))]
99fn client_declares_tasks(_extensions: &crate::context::Extensions) -> bool {
100    false
101}
102
103/// Decode the wire `inputResponses` map into typed responses.
104///
105/// A key whose value does not match any known response shape is dropped here
106/// rather than failing the request: the store treats an unmatched key as
107/// ignorable, and SEP-2663 requires ignoring responses that do not correspond
108/// to an outstanding request.
109fn decode_input_responses(
110    responses: &std::collections::HashMap<String, serde_json::Value>,
111) -> crate::protocol::InputResponses {
112    responses
113        .iter()
114        .filter_map(|(key, value)| {
115            serde_json::from_value(value.clone())
116                .ok()
117                .map(|response| (key.clone(), response))
118        })
119        .collect()
120}
121
122/// The authenticated principal for this request, if any.
123///
124/// Sourced from the OAuth `sub` claim that the HTTP and WebSocket transports
125/// bridge into MCP extensions. Without the `oauth` feature there is no
126/// principal, so tasks are unowned and behave as they did before ownership
127/// existed.
128#[cfg(feature = "oauth")]
129fn request_principal(extensions: &crate::context::Extensions) -> Option<String> {
130    extensions
131        .get::<crate::oauth::token::TokenClaims>()
132        .and_then(|claims| claims.sub.clone())
133}
134
135#[cfg(not(feature = "oauth"))]
136fn request_principal(_extensions: &crate::context::Extensions) -> Option<String> {
137    None
138}
139
140/// Error for a task the server cannot serve.
141///
142/// Unknown and expired tasks are deliberately indistinguishable, so a caller
143/// cannot probe for the existence of a task whose retention window closed.
144fn unknown_task_error(task_id: &str) -> JsonRpcError {
145    JsonRpcError::invalid_params(format!("Task not found: {task_id}"))
146}
147
148/// The client capability shape a server names in a `-32021` when it cannot
149/// service a request without the Tasks extension.
150pub(crate) fn tasks_client_capabilities() -> crate::protocol::ClientCapabilities {
151    crate::protocol::ClientCapabilities {
152        extensions: Some(
153            [(
154                tower_mcp_types::protocol::TASKS_EXTENSION_ID.to_string(),
155                serde_json::json!({}),
156            )]
157            .into_iter()
158            .collect(),
159        ),
160        ..Default::default()
161    }
162}
163
164#[cfg(feature = "stateless")]
165fn final_client_capabilities(
166    extensions: &crate::context::Extensions,
167) -> Option<&ClientCapabilities> {
168    extensions
169        .get::<crate::stateless::StatelessRequestMeta>()
170        .and_then(|meta| meta.client_capabilities.as_ref())
171}
172
173#[cfg(not(feature = "stateless"))]
174fn final_client_capabilities(
175    _extensions: &crate::context::Extensions,
176) -> Option<&ClientCapabilities> {
177    None
178}
179
180/// Return whether `actual` contains every field and value in `required`.
181///
182/// Client capability objects are extensible, so extra advertised properties
183/// must not cause a required-capability check to fail.
184#[cfg(feature = "stateless")]
185fn json_value_contains(actual: &serde_json::Value, required: &serde_json::Value) -> bool {
186    match (actual, required) {
187        (serde_json::Value::Object(actual), serde_json::Value::Object(required)) => {
188            required.iter().all(|(key, value)| {
189                actual
190                    .get(key)
191                    .is_some_and(|a| json_value_contains(a, value))
192            })
193        }
194        _ => actual == required,
195    }
196}
197
198#[cfg(feature = "stateless")]
199fn client_capabilities_satisfy(actual: &ClientCapabilities, required: &ClientCapabilities) -> bool {
200    let actual = serde_json::to_value(actual).expect("ClientCapabilities is always serializable");
201    let mut required =
202        serde_json::to_value(required).expect("ClientCapabilities is always serializable");
203    // `roots.listChanged: false` means the optional notification capability
204    // was not declared; it is not a requirement that the caller also set the
205    // flag to false. Normalize it away before doing the structural subset
206    // comparison so `{roots:{listChanged:true}}` satisfies plain `{roots:{}}`.
207    if required.pointer("/roots/listChanged") == Some(&serde_json::Value::Bool(false))
208        && let Some(roots) = required
209            .get_mut("roots")
210            .and_then(serde_json::Value::as_object_mut)
211    {
212        roots.remove("listChanged");
213    }
214    json_value_contains(&actual, &required)
215}
216
217#[cfg(feature = "stateless")]
218fn validate_input_required_result(
219    extensions: &crate::context::Extensions,
220    result: &InputRequiredResult,
221) -> Result<()> {
222    result.validate().map_err(|message| {
223        Error::invalid_params(format!("invalid InputRequiredResult: {message}"))
224    })?;
225
226    let meta = extensions
227        .get::<crate::stateless::StatelessRequestMeta>()
228        .filter(|meta| {
229            meta.protocol_version.as_deref() == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
230        })
231        .ok_or_else(|| {
232            Error::invalid_params(
233                "InputRequiredResult is only supported by the 2026-07-28 request lifecycle",
234            )
235        })?;
236    let actual = meta.client_capabilities.as_ref().ok_or_else(|| {
237        Error::invalid_params("clientCapabilities is required for InputRequiredResult")
238    })?;
239
240    if let Some(requests) = &result.input_requests {
241        for request in requests.values() {
242            let (supported, required) = match request {
243                InputRequest::CreateMessage(params) => {
244                    let requires_tools = params.tools.is_some();
245                    let requires_context = params
246                        .include_context
247                        .is_some_and(|mode| mode != IncludeContext::None);
248                    let required_sampling = SamplingCapability {
249                        tools: requires_tools.then(SamplingToolsCapability::default),
250                        context: requires_context.then(SamplingContextCapability::default),
251                        ..SamplingCapability::default()
252                    };
253                    let supported = actual.sampling.as_ref().is_some_and(|sampling| {
254                        (!requires_tools || sampling.tools.is_some())
255                            && (!requires_context || sampling.context.is_some())
256                    });
257                    (
258                        supported,
259                        ClientCapabilities {
260                            sampling: Some(required_sampling),
261                            ..ClientCapabilities::default()
262                        },
263                    )
264                }
265                InputRequest::ListRoots(_) => (
266                    actual.roots.is_some(),
267                    ClientCapabilities {
268                        roots: Some(RootsCapability::default()),
269                        ..ClientCapabilities::default()
270                    },
271                ),
272                InputRequest::Elicit(ElicitRequestParams::Form(_)) => {
273                    let supported = actual.elicitation.as_ref().is_some_and(|elicitation| {
274                        elicitation.form.is_some()
275                            || (elicitation.form.is_none() && elicitation.url.is_none())
276                    });
277                    (
278                        supported,
279                        ClientCapabilities {
280                            elicitation: Some(ElicitationCapability {
281                                form: Some(ElicitationFormCapability::default()),
282                                ..ElicitationCapability::default()
283                            }),
284                            ..ClientCapabilities::default()
285                        },
286                    )
287                }
288                InputRequest::Elicit(ElicitRequestParams::Url(_)) => (
289                    actual
290                        .elicitation
291                        .as_ref()
292                        .is_some_and(|elicitation| elicitation.url.is_some()),
293                    ClientCapabilities {
294                        elicitation: Some(ElicitationCapability {
295                            url: Some(ElicitationUrlCapability::default()),
296                            ..ElicitationCapability::default()
297                        }),
298                        ..ClientCapabilities::default()
299                    },
300                ),
301                _ => {
302                    return Err(Error::invalid_params(
303                        "unsupported input request method in InputRequiredResult",
304                    ));
305                }
306            };
307            if !supported {
308                return Err(Error::JsonRpc(
309                    JsonRpcError::missing_required_client_capability(required),
310                ));
311            }
312        }
313    }
314    Ok(())
315}
316
317/// Apply pagination to a collected list of items.
318///
319/// Returns the page of items and an optional `next_cursor`.
320fn paginate<T>(
321    items: Vec<T>,
322    cursor: Option<&str>,
323    page_size: Option<usize>,
324) -> Result<(Vec<T>, Option<String>)> {
325    let Some(page_size) = page_size else {
326        return Ok((items, None));
327    };
328
329    let offset = match cursor {
330        Some(c) => decode_cursor(c)?,
331        None => 0,
332    };
333
334    if offset >= items.len() {
335        return Ok((Vec::new(), None));
336    }
337
338    let end = (offset + page_size).min(items.len());
339    let next_cursor = if end < items.len() {
340        Some(encode_cursor(end))
341    } else {
342        None
343    };
344
345    let mut items = items;
346    let page = items.drain(offset..end).collect();
347    Ok((page, next_cursor))
348}
349
350/// MCP Router that dispatches requests to registered handlers
351///
352/// Implements `tower::Service<McpRequest>` for middleware composition.
353///
354/// # Example
355///
356/// ```rust
357/// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
358/// use schemars::JsonSchema;
359/// use serde::Deserialize;
360///
361/// #[derive(Debug, Deserialize, JsonSchema)]
362/// struct Input { value: String }
363///
364/// let tool = ToolBuilder::new("echo")
365///     .description("Echo input")
366///     .handler(|i: Input| async move { Ok(CallToolResult::text(i.value)) })
367///     .build();
368///
369/// let router = McpRouter::new()
370///     .server_info("my-server", "1.0.0")
371///     .tool(tool);
372/// ```
373#[derive(Clone)]
374pub struct McpRouter {
375    inner: Arc<McpRouterInner>,
376    session: SessionState,
377}
378
379impl std::fmt::Debug for McpRouter {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        f.debug_struct("McpRouter")
382            .field("server_name", &self.inner.server_name)
383            .field("server_version", &self.inner.server_version)
384            .field("tools_count", &self.inner.tools.len())
385            .field("resources_count", &self.inner.resources.len())
386            .field("prompts_count", &self.inner.prompts.len())
387            .field("session_phase", &self.session.phase())
388            .finish()
389    }
390}
391
392/// Configuration for auto-generated instructions
393#[derive(Clone, Debug)]
394struct AutoInstructionsConfig {
395    prefix: Option<String>,
396    suffix: Option<String>,
397}
398
399#[cfg(all(feature = "http", feature = "stateless"))]
400type ModernNotificationSink = Arc<dyn Fn(&ServerNotification) -> bool + Send + Sync + 'static>;
401
402/// Inner configuration that is shared across clones
403#[derive(Clone)]
404struct McpRouterInner {
405    server_name: String,
406    server_version: String,
407    /// Human-readable title for the server
408    server_title: Option<String>,
409    /// Description of the server
410    server_description: Option<String>,
411    /// Icons for the server
412    server_icons: Option<Vec<ToolIcon>>,
413    /// URL of the server's website
414    server_website_url: Option<String>,
415    instructions: Option<String>,
416    auto_instructions: Option<AutoInstructionsConfig>,
417    tools: HashMap<String, Arc<Tool>>,
418    resources: HashMap<String, Arc<Resource>>,
419    /// Resource templates for dynamic resource matching (keyed by uri_template)
420    resource_templates: Vec<Arc<ResourceTemplate>>,
421    prompts: HashMap<String, Arc<Prompt>>,
422    /// In-flight requests for cancellation tracking (shared across clones)
423    in_flight: Arc<RwLock<HashMap<RequestId, CancellationToken>>>,
424    /// Channel for sending notifications to connected clients
425    notification_tx: Option<NotificationSender>,
426    /// Transport-lifetime sink for final HTTP subscription notifications.
427    ///
428    /// The lock is shared across router clones so an application-owned clone
429    /// can publish after the transport attaches its subscription registry.
430    #[cfg(all(feature = "http", feature = "stateless"))]
431    modern_notification_sink: Arc<RwLock<Option<ModernNotificationSink>>>,
432    /// Handle for sending requests to the client (for sampling, etc.)
433    client_requester: Option<ClientRequesterHandle>,
434    /// Task store for async operations
435    task_store: Arc<dyn TaskStore>,
436    /// Subscribed resource URIs
437    subscriptions: Arc<RwLock<HashSet<String>>>,
438    /// Handler for completion requests
439    completion_handler: Option<CompletionHandler>,
440    /// Filter for tools based on session state
441    tool_filter: Option<ToolFilter>,
442    /// Filter for resources based on session state
443    resource_filter: Option<ResourceFilter>,
444    /// Filter for prompts based on session state
445    prompt_filter: Option<PromptFilter>,
446    /// Router-level extensions (for state and middleware data)
447    extensions: Arc<crate::context::Extensions>,
448    /// Locally supported MCP protocol extensions and their server settings.
449    protocol_extensions: HashMap<String, serde_json::Value>,
450    /// Minimum log level for filtering outgoing log notifications (set by client via logging/setLevel)
451    min_log_level: Arc<RwLock<LogLevel>>,
452    /// Page size for list method pagination (None = return all results)
453    page_size: Option<usize>,
454    /// TTL hint for list responses in milliseconds (SEP-2549).
455    /// When set, the value is returned as `ttlMs` in tools/list, resources/list,
456    /// and prompts/list responses so clients can cache the list.
457    list_ttl_ms: Option<u64>,
458    /// Default TTL hint for resources/read responses in milliseconds
459    /// (SEP-2549). Applied only when the resource handler did not set its
460    /// own `ttl_ms` on the result.
461    read_ttl_ms: Option<u64>,
462    /// Cache scope for SEP-2549 hints on list and read responses. When a
463    /// TTL is emitted and no scope is configured, `private` is used: it is
464    /// the conservative choice (never shared across authorization
465    /// contexts).
466    cache_scope: Option<CacheScope>,
467    /// Deprecation info for the logging capability (SEP-2577).
468    /// When set, included in the `logging` capability in the initialize result.
469    logging_deprecated: Option<tower_mcp_types::protocol::DeprecationInfo>,
470    /// Names of tools that are currently disabled (hidden from list/call).
471    disabled_tools: Arc<RwLock<HashSet<String>>>,
472    /// URIs of resources that are currently disabled (hidden from list/read).
473    disabled_resources: Arc<RwLock<HashSet<String>>>,
474    /// Names of prompts that are currently disabled (hidden from list/get).
475    disabled_prompts: Arc<RwLock<HashSet<String>>>,
476    /// Dynamic tools registry for runtime tool (de)registration
477    #[cfg(feature = "dynamic-tools")]
478    dynamic_tools: Option<Arc<DynamicToolsInner>>,
479    /// Dynamic prompts registry for runtime prompt (de)registration
480    #[cfg(feature = "dynamic-tools")]
481    dynamic_prompts: Option<Arc<DynamicPromptsInner>>,
482    /// Dynamic resources registry for runtime resource (de)registration
483    #[cfg(feature = "dynamic-tools")]
484    dynamic_resources: Option<Arc<DynamicResourcesInner>>,
485    /// Dynamic resource templates registry for runtime template (de)registration
486    #[cfg(feature = "dynamic-tools")]
487    dynamic_resource_templates: Option<Arc<DynamicResourceTemplatesInner>>,
488}
489
490impl McpRouterInner {
491    /// Generate instructions text from registered tools, resources, and prompts.
492    fn generate_instructions(&self, config: &AutoInstructionsConfig) -> String {
493        let mut parts = Vec::new();
494
495        if let Some(prefix) = &config.prefix {
496            parts.push(prefix.clone());
497        }
498
499        // Tools section
500        if !self.tools.is_empty() {
501            let mut lines = vec!["## Tools".to_string(), String::new()];
502            let mut tools: Vec<_> = self.tools.values().collect();
503            tools.sort_by(|a, b| a.name.cmp(&b.name));
504            for tool in tools {
505                let desc = tool.description.as_deref().unwrap_or("No description");
506                let tags = annotation_tags(tool.annotations.as_ref());
507                if tags.is_empty() {
508                    lines.push(format!("- **{}**: {}", tool.name, desc));
509                } else {
510                    lines.push(format!("- **{}**: {} [{}]", tool.name, desc, tags));
511                }
512            }
513            parts.push(lines.join("\n"));
514        }
515
516        // Resources section
517        if !self.resources.is_empty() || !self.resource_templates.is_empty() {
518            let mut lines = vec!["## Resources".to_string(), String::new()];
519            let mut resources: Vec<_> = self.resources.values().collect();
520            resources.sort_by(|a, b| a.uri.cmp(&b.uri));
521            for resource in resources {
522                let desc = resource.description.as_deref().unwrap_or("No description");
523                lines.push(format!("- **{}**: {}", resource.uri, desc));
524            }
525            let mut templates: Vec<_> = self.resource_templates.iter().collect();
526            templates.sort_by(|a, b| a.uri_template.cmp(&b.uri_template));
527            for template in templates {
528                let desc = template.description.as_deref().unwrap_or("No description");
529                lines.push(format!("- **{}**: {}", template.uri_template, desc));
530            }
531            parts.push(lines.join("\n"));
532        }
533
534        // Prompts section
535        if !self.prompts.is_empty() {
536            let mut lines = vec!["## Prompts".to_string(), String::new()];
537            let mut prompts: Vec<_> = self.prompts.values().collect();
538            prompts.sort_by(|a, b| a.name.cmp(&b.name));
539            for prompt in prompts {
540                let desc = prompt.description.as_deref().unwrap_or("No description");
541                lines.push(format!("- **{}**: {}", prompt.name, desc));
542            }
543            parts.push(lines.join("\n"));
544        }
545
546        if let Some(suffix) = &config.suffix {
547            parts.push(suffix.clone());
548        }
549
550        parts.join("\n\n")
551    }
552}
553
554/// Build annotation tags like "read-only, idempotent" from tool annotations.
555///
556/// Only includes tags that differ from the MCP spec defaults
557/// (read-only=false, idempotent=false). The destructive and open-world
558/// hints are omitted because they match the default assumptions.
559fn annotation_tags(annotations: Option<&crate::protocol::ToolAnnotations>) -> String {
560    let Some(ann) = annotations else {
561        return String::new();
562    };
563    let mut tags = Vec::new();
564    if ann.is_read_only() {
565        tags.push("read-only");
566    }
567    if ann.is_idempotent() {
568        tags.push("idempotent");
569    }
570    tags.join(", ")
571}
572
573impl McpRouter {
574    /// Create a new MCP router
575    pub fn new() -> Self {
576        Self {
577            inner: Arc::new(McpRouterInner {
578                server_name: "tower-mcp".to_string(),
579                server_version: env!("CARGO_PKG_VERSION").to_string(),
580                server_title: None,
581                server_description: None,
582                server_icons: None,
583                server_website_url: None,
584                instructions: None,
585                auto_instructions: None,
586                tools: HashMap::new(),
587                resources: HashMap::new(),
588                resource_templates: Vec::new(),
589                prompts: HashMap::new(),
590                in_flight: Arc::new(RwLock::new(HashMap::new())),
591                notification_tx: None,
592                #[cfg(all(feature = "http", feature = "stateless"))]
593                modern_notification_sink: Arc::new(RwLock::new(None)),
594                client_requester: None,
595                task_store: Arc::new(MemoryTaskStore::new()),
596                subscriptions: Arc::new(RwLock::new(HashSet::new())),
597                extensions: Arc::new(crate::context::Extensions::new()),
598                protocol_extensions: HashMap::new(),
599                completion_handler: None,
600                tool_filter: None,
601                resource_filter: None,
602                prompt_filter: None,
603                min_log_level: Arc::new(RwLock::new(LogLevel::Debug)),
604                page_size: None,
605                list_ttl_ms: None,
606                read_ttl_ms: None,
607                cache_scope: None,
608                logging_deprecated: None,
609                disabled_tools: Arc::new(RwLock::new(HashSet::new())),
610                disabled_resources: Arc::new(RwLock::new(HashSet::new())),
611                disabled_prompts: Arc::new(RwLock::new(HashSet::new())),
612                #[cfg(feature = "dynamic-tools")]
613                dynamic_tools: None,
614                #[cfg(feature = "dynamic-tools")]
615                dynamic_prompts: None,
616                #[cfg(feature = "dynamic-tools")]
617                dynamic_resources: None,
618                #[cfg(feature = "dynamic-tools")]
619                dynamic_resource_templates: None,
620            }),
621            session: SessionState::new(),
622        }
623    }
624
625    /// Create a clone with fresh session state.
626    ///
627    /// Use this when creating a new logical session (e.g., per HTTP connection).
628    /// The router configuration (tools, resources, prompts) is shared, but the
629    /// session state (phase, extensions) is independent.
630    ///
631    /// This is typically called by transports when establishing a new client session.
632    pub fn with_fresh_session(&self) -> Self {
633        Self {
634            inner: self.inner.clone(),
635            session: SessionState::new(),
636        }
637    }
638
639    /// Build a map of tool names to their annotations.
640    ///
641    /// The returned [`ToolAnnotationsMap`] includes annotations from all
642    /// currently registered tools (both static and dynamic). Tools without
643    /// annotations are omitted from the map.
644    ///
645    /// This is used internally by transports to inject annotations into
646    /// request extensions, but can also be called directly for custom
647    /// middleware setups.
648    pub fn tool_annotations_map(&self) -> ToolAnnotationsMap {
649        let disabled = self.inner.disabled_tools.read().unwrap();
650        let mut map = HashMap::new();
651        for (name, tool) in &self.inner.tools {
652            if disabled.contains(name) {
653                continue;
654            }
655            if let Some(annotations) = &tool.annotations {
656                map.insert(name.clone(), annotations.clone());
657            }
658        }
659        #[cfg(feature = "dynamic-tools")]
660        if let Some(dynamic) = &self.inner.dynamic_tools {
661            for tool in dynamic.list() {
662                if disabled.contains(&tool.name) {
663                    continue;
664                }
665                // Static tools take precedence
666                if !map.contains_key(&tool.name)
667                    && let Some(ref annotations) = tool.annotations
668                {
669                    map.insert(tool.name.clone(), annotations.clone());
670                }
671            }
672        }
673        ToolAnnotationsMap { map: Arc::new(map) }
674    }
675
676    /// Configure a pluggable [`TaskStore`] for async task state.
677    ///
678    /// The default is an in-process [`MemoryTaskStore`]. Supply an external
679    /// store (Redis, Postgres, etc.) to share task state across server
680    /// instances behind a load balancer, so `tasks/get` works regardless of
681    /// which instance created the task (SEP-2663).
682    ///
683    /// # Example
684    ///
685    /// ```rust
686    /// use std::sync::Arc;
687    /// use tower_mcp::McpRouter;
688    /// use tower_mcp::async_task::{MemoryTaskStore, TaskStore};
689    ///
690    /// let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
691    /// let router = McpRouter::new().task_store(store);
692    /// ```
693    pub fn task_store(mut self, store: Arc<dyn TaskStore>) -> Self {
694        Arc::make_mut(&mut self.inner).task_store = store;
695        self
696    }
697
698    /// Enable dynamic tool registration and return a registry handle.
699    ///
700    /// The returned [`DynamicToolRegistry`] can be used to add and remove tools
701    /// at runtime. Dynamic tools are merged with static tools when handling
702    /// `tools/list` and `tools/call` requests. Static tools take precedence
703    /// over dynamic tools when names collide.
704    ///
705    /// # Example
706    ///
707    /// ```rust
708    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
709    /// use schemars::JsonSchema;
710    /// use serde::Deserialize;
711    ///
712    /// #[derive(Debug, Deserialize, JsonSchema)]
713    /// struct Input { value: String }
714    ///
715    /// let (router, registry) = McpRouter::new()
716    ///     .server_info("my-server", "1.0.0")
717    ///     .with_dynamic_tools();
718    ///
719    /// // Register a tool at runtime
720    /// let tool = ToolBuilder::new("echo")
721    ///     .description("Echo input")
722    ///     .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
723    ///     .build();
724    ///
725    /// registry.register(tool);
726    /// ```
727    #[cfg(feature = "dynamic-tools")]
728    pub fn with_dynamic_tools(mut self) -> (Self, DynamicToolRegistry) {
729        let inner_dyn = Arc::new(DynamicToolsInner::new());
730        Arc::make_mut(&mut self.inner).dynamic_tools = Some(inner_dyn.clone());
731        (self, DynamicToolRegistry::new(inner_dyn))
732    }
733
734    /// Enable dynamic prompt registration and return a registry handle.
735    ///
736    /// The returned [`DynamicPromptRegistry`] can be used to add and remove
737    /// prompts at runtime. Dynamic prompts are merged with static prompts
738    /// when handling `prompts/list` and `prompts/get` requests. Static
739    /// prompts take precedence over dynamic prompts when names collide.
740    ///
741    /// # Example
742    ///
743    /// ```rust
744    /// use tower_mcp::{McpRouter, PromptBuilder};
745    ///
746    /// let (router, registry) = McpRouter::new()
747    ///     .server_info("my-server", "1.0.0")
748    ///     .with_dynamic_prompts();
749    ///
750    /// let prompt = PromptBuilder::new("greet")
751    ///     .description("Greet someone")
752    ///     .user_message("Hello!");
753    ///
754    /// registry.register(prompt);
755    /// ```
756    #[cfg(feature = "dynamic-tools")]
757    pub fn with_dynamic_prompts(mut self) -> (Self, DynamicPromptRegistry) {
758        let inner_dyn = Arc::new(DynamicPromptsInner::new());
759        Arc::make_mut(&mut self.inner).dynamic_prompts = Some(inner_dyn.clone());
760        (self, DynamicPromptRegistry::new(inner_dyn))
761    }
762
763    /// Enable dynamic resource registration and return a registry handle.
764    ///
765    /// The returned [`DynamicResourceRegistry`] can be used to add and remove
766    /// resources at runtime. Dynamic resources are merged with static resources
767    /// when handling `resources/list` and `resources/read` requests. Static
768    /// resources take precedence over dynamic resources when URIs collide.
769    ///
770    /// # Example
771    ///
772    /// ```rust
773    /// use tower_mcp::{McpRouter, ResourceBuilder};
774    ///
775    /// let (router, registry) = McpRouter::new()
776    ///     .server_info("my-server", "1.0.0")
777    ///     .with_dynamic_resources();
778    ///
779    /// let resource = ResourceBuilder::new("file:///data.json")
780    ///     .name("Data")
781    ///     .text(r#"{"key": "value"}"#);
782    ///
783    /// registry.register(resource);
784    /// ```
785    #[cfg(feature = "dynamic-tools")]
786    pub fn with_dynamic_resources(mut self) -> (Self, DynamicResourceRegistry) {
787        let inner_dyn = Arc::new(DynamicResourcesInner::new());
788        Arc::make_mut(&mut self.inner).dynamic_resources = Some(inner_dyn.clone());
789        (self, DynamicResourceRegistry::new(inner_dyn))
790    }
791
792    /// Enable dynamic resource template registration and return a registry handle.
793    ///
794    /// The returned [`DynamicResourceTemplateRegistry`] can be used to add and
795    /// remove resource templates at runtime. Dynamic templates are checked
796    /// after static templates when handling `resources/read` requests.
797    ///
798    /// # Example
799    ///
800    /// ```rust,ignore
801    /// use tower_mcp::{McpRouter, ResourceTemplateBuilder};
802    ///
803    /// let (router, registry) = McpRouter::new()
804    ///     .server_info("my-server", "1.0.0")
805    ///     .with_dynamic_resource_templates();
806    ///
807    /// let template = ResourceTemplateBuilder::new("db://tables/{table}")
808    ///     .name("Database Table")
809    ///     .handler(|uri, vars| async move { /* ... */ });
810    ///
811    /// registry.register(template);
812    /// ```
813    #[cfg(feature = "dynamic-tools")]
814    pub fn with_dynamic_resource_templates(mut self) -> (Self, DynamicResourceTemplateRegistry) {
815        let inner_dyn = Arc::new(DynamicResourceTemplatesInner::new());
816        Arc::make_mut(&mut self.inner).dynamic_resource_templates = Some(inner_dyn.clone());
817        (self, DynamicResourceTemplateRegistry::new(inner_dyn))
818    }
819
820    /// Set the notification sender without registering it with the shared
821    /// dynamic registries.
822    ///
823    /// Used by transports for per-request (sessionless) notification
824    /// capture: the dynamic registries are long-lived and shared across
825    /// router clones, so registering one sender per request would
826    /// accumulate senders without bound.
827    #[cfg(feature = "stateless")]
828    #[cfg(feature = "http")]
829    pub(crate) fn with_request_notification_sender(mut self, tx: NotificationSender) -> Self {
830        Arc::make_mut(&mut self.inner).notification_tx = Some(tx);
831        self
832    }
833
834    /// Set the notification sender for progress reporting
835    ///
836    /// This is typically called by the transport layer to receive notifications.
837    pub fn with_notification_sender(mut self, tx: NotificationSender) -> Self {
838        let inner = Arc::make_mut(&mut self.inner);
839        // Also register the sender with dynamic registries so they can
840        // broadcast list-changed notifications to this session.
841        #[cfg(feature = "dynamic-tools")]
842        if let Some(ref dynamic_tools) = inner.dynamic_tools {
843            dynamic_tools.add_notification_sender(tx.clone());
844        }
845        #[cfg(feature = "dynamic-tools")]
846        if let Some(ref dynamic_prompts) = inner.dynamic_prompts {
847            dynamic_prompts.add_notification_sender(tx.clone());
848        }
849        #[cfg(feature = "dynamic-tools")]
850        if let Some(ref dynamic_resources) = inner.dynamic_resources {
851            dynamic_resources.add_notification_sender(tx.clone());
852        }
853        #[cfg(feature = "dynamic-tools")]
854        if let Some(ref dynamic_resource_templates) = inner.dynamic_resource_templates {
855            dynamic_resource_templates.add_notification_sender(tx.clone());
856        }
857        inner.notification_tx = Some(tx);
858        self
859    }
860
861    /// Attach the transport-lifetime final subscription notification path.
862    #[cfg(all(feature = "http", feature = "stateless"))]
863    pub(crate) fn attach_modern_notification_sink(&self, sink: ModernNotificationSink) {
864        if let Ok(mut active) = self.inner.modern_notification_sink.write() {
865            *active = Some(sink);
866        }
867    }
868
869    /// Get the notification sender (if configured)
870    pub fn notification_sender(&self) -> Option<&NotificationSender> {
871        self.inner.notification_tx.as_ref()
872    }
873
874    /// Set the client requester for server-to-client requests (sampling, etc.)
875    ///
876    /// This is typically called by bidirectional transports (WebSocket, stdio)
877    /// to enable tool handlers to send requests to the client.
878    pub fn with_client_requester(mut self, requester: ClientRequesterHandle) -> Self {
879        Arc::make_mut(&mut self.inner).client_requester = Some(requester);
880        self
881    }
882
883    /// Get the client requester (if configured)
884    pub fn client_requester(&self) -> Option<&ClientRequesterHandle> {
885        self.inner.client_requester.as_ref()
886    }
887
888    /// Add router-level state that handlers can access via the `Extension<T>` extractor.
889    ///
890    /// This is the recommended way to share state across all tools, resources, and prompts
891    /// in a router. The state is available to handlers via the [`crate::extract::Extension`]
892    /// extractor.
893    ///
894    /// # Example
895    ///
896    /// ```rust
897    /// use std::sync::Arc;
898    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
899    /// use tower_mcp::extract::{Extension, Json};
900    /// use schemars::JsonSchema;
901    /// use serde::Deserialize;
902    ///
903    /// #[derive(Clone)]
904    /// struct AppState {
905    ///     db_url: String,
906    /// }
907    ///
908    /// #[derive(Deserialize, JsonSchema)]
909    /// struct QueryInput {
910    ///     sql: String,
911    /// }
912    ///
913    /// let state = Arc::new(AppState { db_url: "postgres://...".into() });
914    ///
915    /// // Tool extracts state via Extension<T>
916    /// let query_tool = ToolBuilder::new("query")
917    ///     .description("Run a database query")
918    ///     .extractor_handler(
919    ///         (),
920    ///         |Extension(state): Extension<Arc<AppState>>, Json(input): Json<QueryInput>| async move {
921    ///             Ok(CallToolResult::text(format!("Query on {}: {}", state.db_url, input.sql)))
922    ///         },
923    ///     )
924    ///     .build();
925    ///
926    /// let router = McpRouter::new()
927    ///     .with_state(state)  // State is now available to all handlers
928    ///     .tool(query_tool);
929    /// ```
930    pub fn with_state<T: Clone + Send + Sync + 'static>(mut self, state: T) -> Self {
931        let inner = Arc::make_mut(&mut self.inner);
932        Arc::make_mut(&mut inner.extensions).insert(state);
933        self
934    }
935
936    /// Add an extension value that handlers can access via the `Extension<T>` extractor.
937    ///
938    /// This is a more general form of `with_state()` for when you need multiple
939    /// typed values available to handlers.
940    pub fn with_extension<T: Clone + Send + Sync + 'static>(self, value: T) -> Self {
941        self.with_state(value)
942    }
943
944    /// Advertise one validated MCP protocol extension.
945    ///
946    /// This is separate from [`with_extension`](Self::with_extension), which
947    /// stores process-local Rust values for handlers. Protocol extensions are
948    /// advertised on the wire and become active only when the client declares
949    /// the same identifier.
950    pub fn with_protocol_extension(mut self, extension: crate::ExtensionDeclaration) -> Self {
951        let (identifier, settings) = extension.into_parts();
952        Arc::make_mut(&mut self.inner)
953            .protocol_extensions
954            .insert(identifier, settings);
955        self
956    }
957
958    /// Get the router's extensions.
959    pub fn extensions(&self) -> &crate::context::Extensions {
960        &self.inner.extensions
961    }
962
963    /// Create a request context for tracking a request
964    ///
965    /// This registers the request for cancellation tracking and sets up
966    /// progress reporting, client requests, and router extensions if configured.
967    pub fn create_context(
968        &self,
969        request_id: RequestId,
970        progress_token: Option<ProgressToken>,
971    ) -> RequestContext {
972        self.create_context_with_extensions(request_id, progress_token, &Extensions::new())
973    }
974
975    /// Internal: build a `RequestContext` and additionally merge per-request
976    /// extensions on top of the router's extensions. Used by [`Service::call`]
977    /// to thread `RouterRequest.extensions` (e.g. SEP-2575 per-request
978    /// `_meta`) through to handlers.
979    pub(crate) fn create_context_with_extensions(
980        &self,
981        request_id: RequestId,
982        progress_token: Option<ProgressToken>,
983        per_request: &Extensions,
984    ) -> RequestContext {
985        let ctx = RequestContext::new(request_id.clone());
986
987        // Set up progress token if provided
988        let ctx = if let Some(token) = progress_token {
989            ctx.with_progress_token(token)
990        } else {
991            ctx
992        };
993
994        // Set up notification sender if configured
995        let ctx = if let Some(tx) = &self.inner.notification_tx {
996            ctx.with_notification_sender(tx.clone())
997        } else {
998            ctx
999        };
1000
1001        // Start with router-level extensions, then layer per-request extensions
1002        // on top so they win on type collision. with_state() data stays
1003        // visible; per-request meta (SEP-2575) is now reachable too.
1004        let mut merged = (*self.inner.extensions).clone();
1005        merged.merge(per_request);
1006        let negotiated_extensions = if is_final_protocol_request(per_request) {
1007            let server_capabilities =
1008                self.capabilities_for_protocol(Some(crate::protocol::PROTOCOL_VERSION_2026_07_28));
1009            final_client_capabilities(per_request)
1010                .map(|client_capabilities| {
1011                    crate::NegotiatedExtensions::from_capabilities(
1012                        client_capabilities,
1013                        &server_capabilities,
1014                    )
1015                })
1016                .unwrap_or_default()
1017        } else {
1018            self.session
1019                .get::<crate::NegotiatedExtensions>()
1020                .unwrap_or_default()
1021        };
1022        merged.insert(negotiated_extensions);
1023
1024        // The final protocol does not permit servers to initiate JSON-RPC
1025        // requests. Legacy transports may provide a requester scoped to the
1026        // originating request; prefer it over a transport-wide fallback so
1027        // restricted requests stay on their associated response channel.
1028        let ctx = if !is_final_protocol_request(per_request)
1029            && let Some(requester) = merged
1030                .get::<ClientRequesterHandle>()
1031                .cloned()
1032                .or_else(|| self.inner.client_requester.clone())
1033        {
1034            ctx.with_client_requester(requester)
1035        } else {
1036            ctx
1037        };
1038
1039        // Adopt a transport-provided cancellation token (e.g. HTTP stateless
1040        // client disconnect) so `ctx.is_cancelled()` / `ctx.cancelled()` and
1041        // in-flight tracking observe the transport's signal.
1042        let ctx = if let Some(token) = merged.get::<CancellationToken>() {
1043            ctx.with_cancellation_token(token.clone())
1044        } else {
1045            ctx
1046        };
1047
1048        let ctx = ctx.with_extensions(Arc::new(merged));
1049
1050        // Set up log level filtering
1051        let ctx = ctx.with_min_log_level(self.inner.min_log_level.clone());
1052
1053        // Register for cancellation tracking
1054        let token = ctx.cancellation_token();
1055        if let Ok(mut in_flight) = self.inner.in_flight.write() {
1056            in_flight.insert(request_id, token);
1057        }
1058
1059        ctx
1060    }
1061
1062    /// Remove a request from tracking (called when request completes)
1063    pub fn complete_request(&self, request_id: &RequestId) {
1064        if let Ok(mut in_flight) = self.inner.in_flight.write() {
1065            in_flight.remove(request_id);
1066        }
1067    }
1068
1069    /// Cancel a tracked request
1070    fn cancel_request(&self, request_id: &RequestId) -> bool {
1071        let Ok(in_flight) = self.inner.in_flight.read() else {
1072            return false;
1073        };
1074        let Some(token) = in_flight.get(request_id) else {
1075            return false;
1076        };
1077        token.cancel();
1078        true
1079    }
1080
1081    /// Set server info
1082    pub fn server_info(mut self, name: impl Into<String>, version: impl Into<String>) -> Self {
1083        let inner = Arc::make_mut(&mut self.inner);
1084        inner.server_name = name.into();
1085        inner.server_version = version.into();
1086        self
1087    }
1088
1089    /// Set the page size for list method pagination.
1090    ///
1091    /// When set, list methods (`tools/list`, `resources/list`, etc.) will return
1092    /// at most `page_size` items per response, with a `next_cursor` for fetching
1093    /// subsequent pages. When `None` (the default), all items are returned in a
1094    /// single response.
1095    pub fn page_size(mut self, size: usize) -> Self {
1096        Arc::make_mut(&mut self.inner).page_size = Some(size);
1097        self
1098    }
1099
1100    /// Set a TTL hint on list responses (tools/list, resources/list, prompts/list).
1101    ///
1102    /// When set, the `ttlMs` field is included in list responses so clients can
1103    /// cache the list for up to this many milliseconds before re-fetching.
1104    /// Implements SEP-2549.
1105    pub fn list_ttl(mut self, ms: u64) -> Self {
1106        Arc::make_mut(&mut self.inner).list_ttl_ms = Some(ms);
1107        self
1108    }
1109
1110    /// Set a default TTL hint on resources/read responses (SEP-2549).
1111    ///
1112    /// Applied only when the resource handler did not set its own `ttl_ms`
1113    /// on the [`ReadResourceResult`]. When any TTL is emitted without a
1114    /// configured [`cache_scope`](Self::cache_scope), the scope defaults to
1115    /// `private`.
1116    pub fn read_ttl(mut self, ms: u64) -> Self {
1117        Arc::make_mut(&mut self.inner).read_ttl_ms = Some(ms);
1118        self
1119    }
1120
1121    /// Set the SEP-2549 cache scope emitted alongside TTL hints on list and
1122    /// resources/read responses.
1123    ///
1124    /// `CacheScope::Public` allows any client, gateway, or proxy to reuse
1125    /// the cached result across authorization contexts; `CacheScope::Private`
1126    /// restricts reuse to the same authorization context. When a TTL is
1127    /// emitted and no scope is configured, `private` is used as the
1128    /// conservative default.
1129    pub fn cache_scope(mut self, scope: CacheScope) -> Self {
1130        Arc::make_mut(&mut self.inner).cache_scope = Some(scope);
1131        self
1132    }
1133
1134    /// Mark the logging capability as deprecated in the server's initialize result.
1135    ///
1136    /// When set, the `deprecated` object is included in the `logging` capability
1137    /// in the `initialize` response, signalling to clients that logging notifications
1138    /// are being phased out. Implements SEP-2577.
1139    pub fn logging_deprecated(mut self, info: tower_mcp_types::protocol::DeprecationInfo) -> Self {
1140        Arc::make_mut(&mut self.inner).logging_deprecated = Some(info);
1141        self
1142    }
1143
1144    /// Set instructions for LLMs describing how to use this server
1145    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
1146        Arc::make_mut(&mut self.inner).instructions = Some(instructions.into());
1147        self
1148    }
1149
1150    /// Auto-generate instructions from registered tool, resource, and prompt descriptions.
1151    ///
1152    /// The instructions are generated lazily at initialization time, so this can be
1153    /// called at any point in the builder chain regardless of when tools, resources,
1154    /// and prompts are registered.
1155    ///
1156    /// If both `instructions()` and `auto_instructions()` are set, the auto-generated
1157    /// instructions take precedence.
1158    ///
1159    /// # Example
1160    ///
1161    /// ```rust
1162    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
1163    /// use schemars::JsonSchema;
1164    /// use serde::Deserialize;
1165    ///
1166    /// #[derive(Debug, Deserialize, JsonSchema)]
1167    /// struct QueryInput { sql: String }
1168    ///
1169    /// let query_tool = ToolBuilder::new("query")
1170    ///     .description("Execute a read-only SQL query")
1171    ///     .read_only()
1172    ///     .handler(|input: QueryInput| async move {
1173    ///         Ok(CallToolResult::text("result"))
1174    ///     })
1175    ///     .build();
1176    ///
1177    /// let router = McpRouter::new()
1178    ///     .auto_instructions()
1179    ///     .tool(query_tool);
1180    /// ```
1181    pub fn auto_instructions(mut self) -> Self {
1182        Arc::make_mut(&mut self.inner).auto_instructions = Some(AutoInstructionsConfig {
1183            prefix: None,
1184            suffix: None,
1185        });
1186        self
1187    }
1188
1189    /// Auto-generate instructions with custom prefix and/or suffix text.
1190    ///
1191    /// The prefix is prepended and suffix appended to the generated instructions.
1192    /// See [`auto_instructions`](Self::auto_instructions) for details.
1193    ///
1194    /// # Example
1195    ///
1196    /// ```rust
1197    /// use tower_mcp::McpRouter;
1198    ///
1199    /// let router = McpRouter::new()
1200    ///     .auto_instructions_with(
1201    ///         Some("This server provides database tools."),
1202    ///         Some("Use 'query' for read operations and 'insert' for writes."),
1203    ///     );
1204    /// ```
1205    pub fn auto_instructions_with(
1206        mut self,
1207        prefix: Option<impl Into<String>>,
1208        suffix: Option<impl Into<String>>,
1209    ) -> Self {
1210        Arc::make_mut(&mut self.inner).auto_instructions = Some(AutoInstructionsConfig {
1211            prefix: prefix.map(Into::into),
1212            suffix: suffix.map(Into::into),
1213        });
1214        self
1215    }
1216
1217    /// Set a human-readable title for the server
1218    pub fn server_title(mut self, title: impl Into<String>) -> Self {
1219        Arc::make_mut(&mut self.inner).server_title = Some(title.into());
1220        self
1221    }
1222
1223    /// Set the server description
1224    pub fn server_description(mut self, description: impl Into<String>) -> Self {
1225        Arc::make_mut(&mut self.inner).server_description = Some(description.into());
1226        self
1227    }
1228
1229    /// Set icons for the server
1230    pub fn server_icons(mut self, icons: Vec<ToolIcon>) -> Self {
1231        Arc::make_mut(&mut self.inner).server_icons = Some(icons);
1232        self
1233    }
1234
1235    /// Set the server's website URL
1236    pub fn server_website_url(mut self, url: impl Into<String>) -> Self {
1237        Arc::make_mut(&mut self.inner).server_website_url = Some(url.into());
1238        self
1239    }
1240
1241    /// Register a tool
1242    pub fn tool(mut self, tool: Tool) -> Self {
1243        Arc::make_mut(&mut self.inner)
1244            .tools
1245            .insert(tool.name.clone(), Arc::new(tool));
1246        self
1247    }
1248
1249    /// Conditionally register a tool.
1250    ///
1251    /// Registers the tool only if `condition` is `true`. This keeps fluent
1252    /// builder chains intact when tools are conditionally enabled.
1253    ///
1254    /// # Example
1255    ///
1256    /// ```rust
1257    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
1258    /// use schemars::JsonSchema;
1259    /// use serde::Deserialize;
1260    ///
1261    /// #[derive(Debug, Deserialize, JsonSchema)]
1262    /// struct Input { value: String }
1263    ///
1264    /// let enable_admin = false;
1265    ///
1266    /// let admin_tool = ToolBuilder::new("admin")
1267    ///     .description("Admin tool")
1268    ///     .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1269    ///     .build();
1270    ///
1271    /// let router = McpRouter::new()
1272    ///     .tool_if(enable_admin, admin_tool);
1273    /// ```
1274    pub fn tool_if(self, condition: bool, tool: Tool) -> Self {
1275        if condition { self.tool(tool) } else { self }
1276    }
1277
1278    /// Register a resource
1279    pub fn resource(mut self, resource: Resource) -> Self {
1280        Arc::make_mut(&mut self.inner)
1281            .resources
1282            .insert(resource.uri.clone(), Arc::new(resource));
1283        self
1284    }
1285
1286    /// Conditionally register a resource.
1287    ///
1288    /// Registers the resource only if `condition` is `true`.
1289    ///
1290    /// # Example
1291    ///
1292    /// ```rust
1293    /// use tower_mcp::{McpRouter, ResourceBuilder};
1294    ///
1295    /// let enable_config = false;
1296    ///
1297    /// let config = ResourceBuilder::new("config://system")
1298    ///     .name("config")
1299    ///     .text("secret=xxx");
1300    ///
1301    /// let router = McpRouter::new()
1302    ///     .resource_if(enable_config, config);
1303    /// ```
1304    pub fn resource_if(self, condition: bool, resource: Resource) -> Self {
1305        if condition {
1306            self.resource(resource)
1307        } else {
1308            self
1309        }
1310    }
1311
1312    /// Register a resource template
1313    ///
1314    /// Resource templates allow dynamic resources to be matched by URI pattern.
1315    /// When a client requests a resource URI that doesn't match any static
1316    /// resource, the router tries to match it against registered templates.
1317    ///
1318    /// # Example
1319    ///
1320    /// ```rust
1321    /// use tower_mcp::{McpRouter, ResourceTemplateBuilder};
1322    /// use tower_mcp::protocol::{ReadResourceResult, ResourceContent};
1323    /// use std::collections::HashMap;
1324    ///
1325    /// let template = ResourceTemplateBuilder::new("file:///{path}")
1326    ///     .name("Project Files")
1327    ///     .handler(|uri: String, vars: HashMap<String, String>| async move {
1328    ///         let path = vars.get("path").unwrap_or(&String::new()).clone();
1329    ///         Ok(ReadResourceResult {
1330    ///             contents: vec![ResourceContent {
1331    ///                 uri,
1332    ///                 mime_type: Some("text/plain".to_string()),
1333    ///                 text: Some(format!("Contents of {}", path)),
1334    ///                 blob: None,
1335    ///                 meta: None,
1336    ///             }],
1337    ///             meta: None,
1338    ///             ..Default::default()
1339    ///         })
1340    ///     });
1341    ///
1342    /// let router = McpRouter::new()
1343    ///     .resource_template(template);
1344    /// ```
1345    pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
1346        Arc::make_mut(&mut self.inner)
1347            .resource_templates
1348            .push(Arc::new(template));
1349        self
1350    }
1351
1352    /// Register a prompt
1353    pub fn prompt(mut self, prompt: Prompt) -> Self {
1354        Arc::make_mut(&mut self.inner)
1355            .prompts
1356            .insert(prompt.name.clone(), Arc::new(prompt));
1357        self
1358    }
1359
1360    /// Conditionally register a prompt.
1361    ///
1362    /// Registers the prompt only if `condition` is `true`.
1363    ///
1364    /// # Example
1365    ///
1366    /// ```rust
1367    /// use tower_mcp::{McpRouter, PromptBuilder};
1368    ///
1369    /// let enable_debug = false;
1370    ///
1371    /// let debug_prompt = PromptBuilder::new("debug")
1372    ///     .description("Debug prompt")
1373    ///     .user_message("Debug mode enabled");
1374    ///
1375    /// let router = McpRouter::new()
1376    ///     .prompt_if(enable_debug, debug_prompt);
1377    /// ```
1378    pub fn prompt_if(self, condition: bool, prompt: Prompt) -> Self {
1379        if condition { self.prompt(prompt) } else { self }
1380    }
1381
1382    /// Register multiple tools at once.
1383    ///
1384    /// # Example
1385    ///
1386    /// ```rust
1387    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
1388    /// use schemars::JsonSchema;
1389    /// use serde::Deserialize;
1390    ///
1391    /// #[derive(Debug, Deserialize, JsonSchema)]
1392    /// struct Input { value: String }
1393    ///
1394    /// let tools = vec![
1395    ///     ToolBuilder::new("a")
1396    ///         .description("Tool A")
1397    ///         .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1398    ///         .build(),
1399    ///     ToolBuilder::new("b")
1400    ///         .description("Tool B")
1401    ///         .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1402    ///         .build(),
1403    /// ];
1404    ///
1405    /// let router = McpRouter::new().tools(tools);
1406    /// ```
1407    pub fn tools(self, tools: impl IntoIterator<Item = Tool>) -> Self {
1408        tools
1409            .into_iter()
1410            .fold(self, |router, tool| router.tool(tool))
1411    }
1412
1413    /// Conditionally register multiple tools at once.
1414    ///
1415    /// Registers all tools only if `condition` is `true`.
1416    pub fn tools_if(self, condition: bool, tools: impl IntoIterator<Item = Tool>) -> Self {
1417        if condition { self.tools(tools) } else { self }
1418    }
1419
1420    /// Register multiple resources at once.
1421    ///
1422    /// # Example
1423    ///
1424    /// ```rust
1425    /// use tower_mcp::{McpRouter, ResourceBuilder};
1426    ///
1427    /// let resources = vec![
1428    ///     ResourceBuilder::new("file:///a.txt")
1429    ///         .name("File A")
1430    ///         .text("contents a"),
1431    ///     ResourceBuilder::new("file:///b.txt")
1432    ///         .name("File B")
1433    ///         .text("contents b"),
1434    /// ];
1435    ///
1436    /// let router = McpRouter::new().resources(resources);
1437    /// ```
1438    pub fn resources(self, resources: impl IntoIterator<Item = Resource>) -> Self {
1439        resources
1440            .into_iter()
1441            .fold(self, |router, resource| router.resource(resource))
1442    }
1443
1444    /// Conditionally register multiple resources at once.
1445    ///
1446    /// Registers all resources only if `condition` is `true`.
1447    pub fn resources_if(
1448        self,
1449        condition: bool,
1450        resources: impl IntoIterator<Item = Resource>,
1451    ) -> Self {
1452        if condition {
1453            self.resources(resources)
1454        } else {
1455            self
1456        }
1457    }
1458
1459    /// Register multiple prompts at once.
1460    ///
1461    /// # Example
1462    ///
1463    /// ```rust
1464    /// use tower_mcp::{McpRouter, PromptBuilder};
1465    ///
1466    /// let prompts = vec![
1467    ///     PromptBuilder::new("greet")
1468    ///         .description("Greet someone")
1469    ///         .user_message("Hello!"),
1470    ///     PromptBuilder::new("farewell")
1471    ///         .description("Say goodbye")
1472    ///         .user_message("Goodbye!"),
1473    /// ];
1474    ///
1475    /// let router = McpRouter::new().prompts(prompts);
1476    /// ```
1477    pub fn prompts(self, prompts: impl IntoIterator<Item = Prompt>) -> Self {
1478        prompts
1479            .into_iter()
1480            .fold(self, |router, prompt| router.prompt(prompt))
1481    }
1482
1483    /// Conditionally register multiple prompts at once.
1484    ///
1485    /// Registers all prompts only if `condition` is `true`.
1486    pub fn prompts_if(self, condition: bool, prompts: impl IntoIterator<Item = Prompt>) -> Self {
1487        if condition {
1488            self.prompts(prompts)
1489        } else {
1490            self
1491        }
1492    }
1493
1494    /// Merge another router's capabilities into this one.
1495    ///
1496    /// This combines all tools, resources, resource templates, and prompts from
1497    /// the other router into this router. Uses "last wins" semantics for conflicts,
1498    /// meaning if both routers have a tool/resource/prompt with the same name,
1499    /// the one from `other` will replace the one in `self`.
1500    ///
1501    /// Server info, instructions, filters, and other router-level configuration
1502    /// are NOT merged - only the root router's settings are used.
1503    ///
1504    /// # Example
1505    ///
1506    /// ```rust
1507    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult, ResourceBuilder};
1508    /// use schemars::JsonSchema;
1509    /// use serde::Deserialize;
1510    ///
1511    /// #[derive(Debug, Deserialize, JsonSchema)]
1512    /// struct Input { value: String }
1513    ///
1514    /// // Create a router with database tools
1515    /// let db_tools = McpRouter::new()
1516    ///     .tool(
1517    ///         ToolBuilder::new("query")
1518    ///             .description("Query the database")
1519    ///             .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1520    ///             .build()
1521    ///     );
1522    ///
1523    /// // Create a router with API tools
1524    /// let api_tools = McpRouter::new()
1525    ///     .tool(
1526    ///         ToolBuilder::new("fetch")
1527    ///             .description("Fetch from API")
1528    ///             .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1529    ///             .build()
1530    ///     );
1531    ///
1532    /// // Merge them together
1533    /// let router = McpRouter::new()
1534    ///     .server_info("combined", "1.0")
1535    ///     .merge(db_tools)
1536    ///     .merge(api_tools);
1537    /// ```
1538    pub fn merge(mut self, other: McpRouter) -> Self {
1539        let inner = Arc::make_mut(&mut self.inner);
1540        let other_inner = other.inner;
1541
1542        // Merge tools (last wins)
1543        for (name, tool) in &other_inner.tools {
1544            inner.tools.insert(name.clone(), tool.clone());
1545        }
1546
1547        // Merge resources (last wins)
1548        for (uri, resource) in &other_inner.resources {
1549            inner.resources.insert(uri.clone(), resource.clone());
1550        }
1551
1552        // Merge resource templates (append - no deduplication since templates
1553        // can have complex matching behavior)
1554        for template in &other_inner.resource_templates {
1555            inner.resource_templates.push(template.clone());
1556        }
1557
1558        // Merge prompts (last wins)
1559        for (name, prompt) in &other_inner.prompts {
1560            inner.prompts.insert(name.clone(), prompt.clone());
1561        }
1562
1563        // Merge protocol extension declarations (last wins).
1564        for (identifier, settings) in &other_inner.protocol_extensions {
1565            inner
1566                .protocol_extensions
1567                .insert(identifier.clone(), settings.clone());
1568        }
1569
1570        self
1571    }
1572
1573    /// Nest another router's capabilities under a prefix.
1574    ///
1575    /// This is similar to `merge()`, but all tool names from the nested router
1576    /// are prefixed with the given string and a dot separator. For example,
1577    /// nesting with prefix "db" will turn a tool named "query" into "db.query".
1578    ///
1579    /// Resources, resource templates, and prompts are merged without modification
1580    /// since they use URIs rather than simple names for identification.
1581    ///
1582    /// # Example
1583    ///
1584    /// ```rust
1585    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult};
1586    /// use schemars::JsonSchema;
1587    /// use serde::Deserialize;
1588    ///
1589    /// #[derive(Debug, Deserialize, JsonSchema)]
1590    /// struct Input { value: String }
1591    ///
1592    /// // Create a router with database tools
1593    /// let db_tools = McpRouter::new()
1594    ///     .tool(
1595    ///         ToolBuilder::new("query")
1596    ///             .description("Query the database")
1597    ///             .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1598    ///             .build()
1599    ///     )
1600    ///     .tool(
1601    ///         ToolBuilder::new("insert")
1602    ///             .description("Insert into database")
1603    ///             .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1604    ///             .build()
1605    ///     );
1606    ///
1607    /// // Nest under "db" prefix - tools become "db.query" and "db.insert"
1608    /// let router = McpRouter::new()
1609    ///     .server_info("combined", "1.0")
1610    ///     .nest("db", db_tools);
1611    /// ```
1612    pub fn nest(mut self, prefix: impl Into<String>, other: McpRouter) -> Self {
1613        let prefix = prefix.into();
1614        let inner = Arc::make_mut(&mut self.inner);
1615        let other_inner = other.inner;
1616
1617        // Nest tools with prefix
1618        for tool in other_inner.tools.values() {
1619            let prefixed_tool = tool.with_name_prefix(&prefix);
1620            inner
1621                .tools
1622                .insert(prefixed_tool.name.clone(), Arc::new(prefixed_tool));
1623        }
1624
1625        // Merge resources (no prefix - URIs are already namespaced)
1626        for (uri, resource) in &other_inner.resources {
1627            inner.resources.insert(uri.clone(), resource.clone());
1628        }
1629
1630        // Merge resource templates (no prefix)
1631        for template in &other_inner.resource_templates {
1632            inner.resource_templates.push(template.clone());
1633        }
1634
1635        // Merge prompts (no prefix - could be added in future if needed)
1636        for (name, prompt) in &other_inner.prompts {
1637            inner.prompts.insert(name.clone(), prompt.clone());
1638        }
1639
1640        // Protocol extensions are server-wide declarations and are not
1641        // namespace-prefixed. Nested declarations use last-write-wins.
1642        for (identifier, settings) in &other_inner.protocol_extensions {
1643            inner
1644                .protocol_extensions
1645                .insert(identifier.clone(), settings.clone());
1646        }
1647
1648        self
1649    }
1650
1651    /// Register a completion handler for `completion/complete` requests.
1652    ///
1653    /// The handler receives `CompleteParams` containing the reference (prompt or resource)
1654    /// and the argument being completed, and should return completion suggestions.
1655    ///
1656    /// # Example
1657    ///
1658    /// ```rust
1659    /// use tower_mcp::{McpRouter, CompleteResult};
1660    /// use tower_mcp::protocol::{CompleteParams, CompletionReference};
1661    ///
1662    /// let router = McpRouter::new()
1663    ///     .completion_handler(|params: CompleteParams| async move {
1664    ///         // Provide completions based on the reference and argument
1665    ///         match params.reference {
1666    ///             CompletionReference::Prompt { name } => {
1667    ///                 // Return prompt argument completions
1668    ///                 Ok(CompleteResult::new(vec!["option1".to_string(), "option2".to_string()]))
1669    ///             }
1670    ///             CompletionReference::Resource { uri } => {
1671    ///                 // Return resource URI completions
1672    ///                 Ok(CompleteResult::new(vec![]))
1673    ///             }
1674    ///             _ => Ok(CompleteResult::new(vec![])),
1675    ///         }
1676    ///     });
1677    /// ```
1678    pub fn completion_handler<F, Fut>(mut self, handler: F) -> Self
1679    where
1680        F: Fn(CompleteParams) -> Fut + Send + Sync + 'static,
1681        Fut: Future<Output = Result<CompleteResult>> + Send + 'static,
1682    {
1683        Arc::make_mut(&mut self.inner).completion_handler =
1684            Some(Arc::new(move |params| Box::pin(handler(params))));
1685        self
1686    }
1687
1688    /// Set a filter for tools based on session state.
1689    ///
1690    /// The filter determines which tools are visible to each session. Tools that
1691    /// don't pass the filter will not appear in `tools/list` responses and will
1692    /// return an error if called directly.
1693    ///
1694    /// # Example
1695    ///
1696    /// ```rust
1697    /// use tower_mcp::{McpRouter, ToolBuilder, CallToolResult, CapabilityFilter, Tool, Filterable};
1698    /// use schemars::JsonSchema;
1699    /// use serde::Deserialize;
1700    ///
1701    /// #[derive(Debug, Deserialize, JsonSchema)]
1702    /// struct Input { value: String }
1703    ///
1704    /// let public_tool = ToolBuilder::new("public")
1705    ///     .description("Available to everyone")
1706    ///     .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1707    ///     .build();
1708    ///
1709    /// let admin_tool = ToolBuilder::new("admin")
1710    ///     .description("Admin only")
1711    ///     .handler(|i: Input| async move { Ok(CallToolResult::text(&i.value)) })
1712    ///     .build();
1713    ///
1714    /// let router = McpRouter::new()
1715    ///     .tool(public_tool)
1716    ///     .tool(admin_tool)
1717    ///     .tool_filter(CapabilityFilter::new(|_session, tool: &Tool| {
1718    ///         // In real code, check session.extensions() for auth claims
1719    ///         tool.name() != "admin"
1720    ///     }));
1721    /// ```
1722    pub fn tool_filter(mut self, filter: ToolFilter) -> Self {
1723        Arc::make_mut(&mut self.inner).tool_filter = Some(filter);
1724        self
1725    }
1726
1727    /// Set a filter for resources based on session state.
1728    ///
1729    /// The filter receives the current session state and each resource, returning
1730    /// `true` if the resource should be visible to this session. Resources that
1731    /// don't pass the filter will not appear in `resources/list` responses and will
1732    /// return an error if read directly.
1733    ///
1734    /// # Example
1735    ///
1736    /// ```rust
1737    /// use tower_mcp::{McpRouter, ResourceBuilder, ReadResourceResult, CapabilityFilter, Resource, Filterable};
1738    ///
1739    /// let public_resource = ResourceBuilder::new("file:///public.txt")
1740    ///     .name("Public File")
1741    ///     .description("Available to everyone")
1742    ///     .text("public content");
1743    ///
1744    /// let secret_resource = ResourceBuilder::new("file:///secret.txt")
1745    ///     .name("Secret File")
1746    ///     .description("Admin only")
1747    ///     .text("secret content");
1748    ///
1749    /// let router = McpRouter::new()
1750    ///     .resource(public_resource)
1751    ///     .resource(secret_resource)
1752    ///     .resource_filter(CapabilityFilter::new(|_session, resource: &Resource| {
1753    ///         // In real code, check session.extensions() for auth claims
1754    ///         !resource.name().contains("Secret")
1755    ///     }));
1756    /// ```
1757    pub fn resource_filter(mut self, filter: ResourceFilter) -> Self {
1758        Arc::make_mut(&mut self.inner).resource_filter = Some(filter);
1759        self
1760    }
1761
1762    /// Set a filter for prompts based on session state.
1763    ///
1764    /// The filter receives the current session state and each prompt, returning
1765    /// `true` if the prompt should be visible to this session. Prompts that
1766    /// don't pass the filter will not appear in `prompts/list` responses and will
1767    /// return an error if accessed directly.
1768    ///
1769    /// # Example
1770    ///
1771    /// ```rust
1772    /// use tower_mcp::{McpRouter, PromptBuilder, CapabilityFilter, Prompt, Filterable};
1773    ///
1774    /// let public_prompt = PromptBuilder::new("greeting")
1775    ///     .description("A friendly greeting")
1776    ///     .user_message("Hello!");
1777    ///
1778    /// let admin_prompt = PromptBuilder::new("system_debug")
1779    ///     .description("Admin debugging prompt")
1780    ///     .user_message("Debug info");
1781    ///
1782    /// let router = McpRouter::new()
1783    ///     .prompt(public_prompt)
1784    ///     .prompt(admin_prompt)
1785    ///     .prompt_filter(CapabilityFilter::new(|_session, prompt: &Prompt| {
1786    ///         // In real code, check session.extensions() for auth claims
1787    ///         !prompt.name().contains("system")
1788    ///     }));
1789    /// ```
1790    pub fn prompt_filter(mut self, filter: PromptFilter) -> Self {
1791        Arc::make_mut(&mut self.inner).prompt_filter = Some(filter);
1792        self
1793    }
1794
1795    /// Get access to the session state
1796    pub fn session(&self) -> &SessionState {
1797        &self.session
1798    }
1799
1800    /// Send a log message notification to the client
1801    ///
1802    /// This sends a `notifications/message` notification with the given parameters.
1803    /// Returns `true` if the notification was sent, `false` if no notification channel
1804    /// is configured.
1805    ///
1806    /// # Example
1807    ///
1808    /// ```rust,ignore
1809    /// use tower_mcp::protocol::{LogLevel, LoggingMessageParams};
1810    ///
1811    /// // Simple info message
1812    /// router.log(LoggingMessageParams::new(LogLevel::Info,
1813    ///     serde_json::json!({"message": "Operation completed"})
1814    /// ));
1815    ///
1816    /// // Error with logger name
1817    /// router.log(LoggingMessageParams::new(LogLevel::Error,
1818    ///     serde_json::json!({"error": "Connection failed"}))
1819    ///     .with_logger("database"));
1820    /// ```
1821    pub fn log(&self, params: LoggingMessageParams) -> bool {
1822        let Some(tx) = &self.inner.notification_tx else {
1823            return false;
1824        };
1825        tx.try_send(ServerNotification::LogMessage(params)).is_ok()
1826    }
1827
1828    /// Send an info-level log message
1829    ///
1830    /// Convenience method for sending an info log with a message string.
1831    pub fn log_info(&self, message: &str) -> bool {
1832        self.log(LoggingMessageParams::new(
1833            LogLevel::Info,
1834            serde_json::json!({ "message": message }),
1835        ))
1836    }
1837
1838    /// Send a warning-level log message
1839    pub fn log_warning(&self, message: &str) -> bool {
1840        self.log(LoggingMessageParams::new(
1841            LogLevel::Warning,
1842            serde_json::json!({ "message": message }),
1843        ))
1844    }
1845
1846    /// Send an error-level log message
1847    pub fn log_error(&self, message: &str) -> bool {
1848        self.log(LoggingMessageParams::new(
1849            LogLevel::Error,
1850            serde_json::json!({ "message": message }),
1851        ))
1852    }
1853
1854    /// Send a debug-level log message
1855    pub fn log_debug(&self, message: &str) -> bool {
1856        self.log(LoggingMessageParams::new(
1857            LogLevel::Debug,
1858            serde_json::json!({ "message": message }),
1859        ))
1860    }
1861
1862    /// Check if a resource URI is currently subscribed
1863    pub fn is_subscribed(&self, uri: &str) -> bool {
1864        if let Ok(subs) = self.inner.subscriptions.read() {
1865            return subs.contains(uri);
1866        }
1867        false
1868    }
1869
1870    /// Get a list of all subscribed resource URIs
1871    pub fn subscribed_uris(&self) -> Vec<String> {
1872        if let Ok(subs) = self.inner.subscriptions.read() {
1873            return subs.iter().cloned().collect();
1874        }
1875        Vec::new()
1876    }
1877
1878    /// Subscribe to a resource URI
1879    fn subscribe(&self, uri: &str) -> bool {
1880        if let Ok(mut subs) = self.inner.subscriptions.write() {
1881            return subs.insert(uri.to_string());
1882        }
1883        false
1884    }
1885
1886    /// Unsubscribe from a resource URI
1887    fn unsubscribe(&self, uri: &str) -> bool {
1888        if let Ok(mut subs) = self.inner.subscriptions.write() {
1889            return subs.remove(uri);
1890        }
1891        false
1892    }
1893
1894    /// Notify clients that a subscribed resource has been updated
1895    ///
1896    /// Legacy sessions receive the notification only after
1897    /// `resources/subscribe`. Final HTTP listeners are filtered by their
1898    /// `subscriptions/listen` registration.
1899    /// Returns `true` if the notification was sent.
1900    pub fn notify_resource_updated(&self, uri: &str) -> bool {
1901        let notification = ServerNotification::ResourceUpdated {
1902            uri: uri.to_string(),
1903        };
1904        let mut sent = false;
1905
1906        if self.is_subscribed(uri)
1907            && let Some(tx) = &self.inner.notification_tx
1908        {
1909            sent |= tx.try_send(notification.clone()).is_ok();
1910        }
1911
1912        #[cfg(all(feature = "http", feature = "stateless"))]
1913        if let Ok(active) = self.inner.modern_notification_sink.read()
1914            && let Some(sink) = active.as_ref()
1915        {
1916            sent |= sink(&notification);
1917        }
1918
1919        sent
1920    }
1921
1922    /// Push a task's current state to subscribed `subscriptions/listen`
1923    /// streams as a `notifications/tasks` notification.
1924    ///
1925    /// The router already announces the transitions it drives: completion,
1926    /// failure, cancellation, and the resumption that follows a
1927    /// `tasks/update`. Call this after driving a transition yourself, most
1928    /// commonly [`TaskStore::require_input`], which a tool handler invokes on
1929    /// the store directly.
1930    ///
1931    /// Announcing task creation is deliberately left out. A client learns the
1932    /// task ID from the `tools/call` result, so it cannot have subscribed to a
1933    /// task before that result reaches it.
1934    ///
1935    /// [`TaskStore::require_input`]: crate::async_task::TaskStore::require_input
1936    pub async fn notify_task_status_changed(&self, task_id: &str) {
1937        self.notify_task_state(task_id).await;
1938    }
1939
1940    /// Notify clients that the list of available resources has changed
1941    ///
1942    /// Returns `true` if the notification was sent.
1943    pub fn notify_resources_list_changed(&self) -> bool {
1944        let Some(tx) = &self.inner.notification_tx else {
1945            return false;
1946        };
1947        tx.try_send(ServerNotification::ResourcesListChanged)
1948            .is_ok()
1949    }
1950
1951    /// Notify clients that the list of available tools has changed
1952    ///
1953    /// Returns `true` if the notification was sent.
1954    pub fn notify_tools_list_changed(&self) -> bool {
1955        let Some(tx) = &self.inner.notification_tx else {
1956            return false;
1957        };
1958        tx.try_send(ServerNotification::ToolsListChanged).is_ok()
1959    }
1960
1961    /// Notify clients that the list of available prompts has changed
1962    ///
1963    /// Returns `true` if the notification was sent.
1964    pub fn notify_prompts_list_changed(&self) -> bool {
1965        let Some(tx) = &self.inner.notification_tx else {
1966            return false;
1967        };
1968        tx.try_send(ServerNotification::PromptsListChanged).is_ok()
1969    }
1970
1971    /// Disable a tool by name. Disabled tools are hidden from `tools/list`
1972    /// and return a method-not-found error from `tools/call`, but the tool
1973    /// definition stays attached to the router and can be flipped back on
1974    /// with [`enable_tool`](Self::enable_tool).
1975    ///
1976    /// State is shared across all clones produced by
1977    /// [`with_fresh_session`](Self::with_fresh_session), so flipping it once
1978    /// affects every connected session at the next request boundary. Call
1979    /// [`notify_tools_list_changed`](Self::notify_tools_list_changed) to nudge
1980    /// clients to re-fetch.
1981    pub fn disable_tool(&self, name: impl Into<String>) {
1982        let mut set = self.inner.disabled_tools.write().unwrap();
1983        set.insert(name.into());
1984    }
1985
1986    /// Re-enable a previously disabled tool. No-op if the tool was not
1987    /// disabled.
1988    pub fn enable_tool(&self, name: &str) {
1989        let mut set = self.inner.disabled_tools.write().unwrap();
1990        set.remove(name);
1991    }
1992
1993    /// Returns `true` if the named tool is currently enabled (i.e. not in
1994    /// the disabled set). Returns `true` even for unknown tool names; this
1995    /// only reports disable state, not registration.
1996    pub fn is_tool_enabled(&self, name: &str) -> bool {
1997        !self.inner.disabled_tools.read().unwrap().contains(name)
1998    }
1999
2000    /// Disable a resource by URI. Disabled resources are hidden from
2001    /// `resources/list` and return a not-found error from `resources/read`.
2002    pub fn disable_resource(&self, uri: impl Into<String>) {
2003        let mut set = self.inner.disabled_resources.write().unwrap();
2004        set.insert(uri.into());
2005    }
2006
2007    /// Re-enable a previously disabled resource.
2008    pub fn enable_resource(&self, uri: &str) {
2009        let mut set = self.inner.disabled_resources.write().unwrap();
2010        set.remove(uri);
2011    }
2012
2013    /// Returns `true` if the resource at this URI is currently enabled.
2014    pub fn is_resource_enabled(&self, uri: &str) -> bool {
2015        !self.inner.disabled_resources.read().unwrap().contains(uri)
2016    }
2017
2018    /// Disable a prompt by name. Disabled prompts are hidden from
2019    /// `prompts/list` and return a method-not-found error from `prompts/get`.
2020    pub fn disable_prompt(&self, name: impl Into<String>) {
2021        let mut set = self.inner.disabled_prompts.write().unwrap();
2022        set.insert(name.into());
2023    }
2024
2025    /// Re-enable a previously disabled prompt.
2026    pub fn enable_prompt(&self, name: &str) {
2027        let mut set = self.inner.disabled_prompts.write().unwrap();
2028        set.remove(name);
2029    }
2030
2031    /// Returns `true` if the named prompt is currently enabled.
2032    pub fn is_prompt_enabled(&self, name: &str) -> bool {
2033        !self.inner.disabled_prompts.read().unwrap().contains(name)
2034    }
2035
2036    /// Get server capabilities based on registered handlers
2037    /// The server's identity, as configured via `.server_info()` and the
2038    /// related `.server_title()` / `.server_description()` / etc. builders.
2039    ///
2040    /// Shared by the `initialize` and `server/discover` handlers, and by the
2041    /// 2026-07-28 stateless HTTP dispatch (SEP-2575's "servers SHOULD
2042    /// identify themselves in each result's `_meta`") since that path calls
2043    /// in from outside this module and has no other way to read identity
2044    /// off a router wrapped behind arbitrary `.layer()` middleware.
2045    pub(crate) fn implementation(&self) -> Implementation {
2046        Implementation {
2047            name: self.inner.server_name.clone(),
2048            version: self.inner.server_version.clone(),
2049            title: self.inner.server_title.clone(),
2050            description: self.inner.server_description.clone(),
2051            icons: self.inner.server_icons.clone(),
2052            website_url: self.inner.server_website_url.clone(),
2053            meta: None,
2054        }
2055    }
2056
2057    /// Return a snapshot of a registered tool's input schema.
2058    ///
2059    /// HTTP transport validation uses this before dispatch to enforce
2060    /// SEP-2243 `x-mcp-header` mappings. Static tools take precedence over
2061    /// dynamic tools, matching `tools/list` and `tools/call`.
2062    #[cfg(feature = "http")]
2063    pub(crate) fn tool_input_schema(&self, name: &str) -> Option<serde_json::Value> {
2064        if let Some(tool) = self.inner.tools.get(name) {
2065            return Some(tool.input_schema.clone());
2066        }
2067        #[cfg(feature = "dynamic-tools")]
2068        if let Some(tool) = self
2069            .inner
2070            .dynamic_tools
2071            .as_ref()
2072            .and_then(|tools| tools.get(name))
2073        {
2074            return Some(tool.input_schema.clone());
2075        }
2076        None
2077    }
2078
2079    fn capabilities(&self) -> ServerCapabilities {
2080        let has_resources =
2081            !self.inner.resources.is_empty() || !self.inner.resource_templates.is_empty();
2082        let has_notifications = self.inner.notification_tx.is_some();
2083
2084        #[cfg(feature = "dynamic-tools")]
2085        let has_dynamic_tools = self.inner.dynamic_tools.is_some();
2086        #[cfg(not(feature = "dynamic-tools"))]
2087        let has_dynamic_tools = false;
2088
2089        #[cfg(feature = "dynamic-tools")]
2090        let has_dynamic_prompts = self.inner.dynamic_prompts.is_some();
2091        #[cfg(not(feature = "dynamic-tools"))]
2092        let has_dynamic_prompts = false;
2093
2094        #[cfg(feature = "dynamic-tools")]
2095        let has_dynamic_resources = self.inner.dynamic_resources.is_some()
2096            || self.inner.dynamic_resource_templates.is_some();
2097        #[cfg(not(feature = "dynamic-tools"))]
2098        let has_dynamic_resources = false;
2099
2100        ServerCapabilities {
2101            tools: if self.inner.tools.is_empty() && !has_dynamic_tools {
2102                None
2103            } else {
2104                Some(ToolsCapability {
2105                    list_changed: has_notifications,
2106                })
2107            },
2108            resources: if has_resources || has_dynamic_resources {
2109                Some(ResourcesCapability {
2110                    subscribe: true,
2111                    list_changed: has_notifications,
2112                })
2113            } else {
2114                None
2115            },
2116            prompts: if self.inner.prompts.is_empty() && !has_dynamic_prompts {
2117                None
2118            } else {
2119                Some(PromptsCapability {
2120                    list_changed: has_notifications,
2121                })
2122            },
2123            // Always advertise logging capability when notification channel is configured
2124            logging: if self.inner.notification_tx.is_some() {
2125                Some(LoggingCapability {
2126                    deprecated: self.inner.logging_deprecated.clone(),
2127                })
2128            } else {
2129                None
2130            },
2131            // Tasks capability is advertised if any tool supports tasks.
2132            // SEP-2663 moves the declaration to `capabilities.extensions`
2133            // under the reverse-DNS key `io.modelcontextprotocol/tasks`; we
2134            // continue to set the legacy top-level `tasks` field for back-compat
2135            // with 2025-11-25 clients that key off it.
2136            tasks: {
2137                let has_task_support = self
2138                    .inner
2139                    .tools
2140                    .values()
2141                    .any(|t| !matches!(t.task_support, TaskSupportMode::Forbidden));
2142                if has_task_support {
2143                    Some(TasksCapability {
2144                        // `list` is intentionally not advertised: final
2145                        // SEP-2663 removes `tasks/list` and this router
2146                        // answers MethodNotFound for it.
2147                        list: None,
2148                        cancel: Some(TasksCancelCapability {}),
2149                        requests: Some(TasksRequestsCapability {
2150                            tools: Some(TasksToolsRequestsCapability {
2151                                call: Some(TasksToolsCallCapability {}),
2152                            }),
2153                        }),
2154                    })
2155                } else {
2156                    None
2157                }
2158            },
2159            // Completions capability when a handler is registered
2160            completions: if self.inner.completion_handler.is_some() {
2161                Some(CompletionsCapability::default())
2162            } else {
2163                None
2164            },
2165            experimental: None,
2166            extensions: {
2167                let mut map = self.inner.protocol_extensions.clone();
2168                let has_task_support = self
2169                    .inner
2170                    .tools
2171                    .values()
2172                    .any(|t| !matches!(t.task_support, TaskSupportMode::Forbidden));
2173                if has_task_support {
2174                    map.insert(
2175                        tower_mcp_types::protocol::TASKS_EXTENSION_ID.to_string(),
2176                        serde_json::json!({}),
2177                    );
2178                }
2179                (!map.is_empty()).then_some(map)
2180            },
2181        }
2182    }
2183
2184    /// Return the capability surface appropriate for a protocol version.
2185    ///
2186    /// `capabilities.tasks` is the legacy 2025-11-25 shape and is never
2187    /// advertised on the final path. The final extension is advertised only
2188    /// when the server opted in via [`McpRouter::with_tasks`]; merely
2189    /// registering task-capable tools does not advertise it, so a server that
2190    /// has not opted in presents no Tasks surface to a 2026-07-28 client.
2191    fn capabilities_for_protocol(&self, protocol_version: Option<&str>) -> ServerCapabilities {
2192        let mut capabilities = self.capabilities();
2193        if protocol_version == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28) {
2194            capabilities.tasks = None;
2195            if !self.final_tasks_enabled()
2196                && let Some(extensions) = capabilities.extensions.as_mut()
2197            {
2198                extensions.remove(tower_mcp_types::protocol::TASKS_EXTENSION_ID);
2199                if extensions.is_empty() {
2200                    capabilities.extensions = None;
2201                }
2202            }
2203        }
2204        capabilities
2205    }
2206
2207    /// Whether this server opted into the final Tasks extension.
2208    ///
2209    /// Distinct from the synthesized advertisement in [`Self::capabilities`],
2210    /// which reflects registered tools rather than an explicit choice.
2211    pub(crate) fn final_tasks_enabled(&self) -> bool {
2212        self.inner
2213            .protocol_extensions
2214            .contains_key(tower_mcp_types::protocol::TASKS_EXTENSION_ID)
2215    }
2216
2217    /// Reject a final task method that was not negotiated by both peers.
2218    ///
2219    /// An unnegotiated method is reported as absent rather than forbidden:
2220    /// the server genuinely does not serve it for this client.
2221    fn require_negotiated_tasks(
2222        &self,
2223        extensions: &crate::context::Extensions,
2224        method: &str,
2225    ) -> Result<()> {
2226        if !self.final_tasks_enabled() {
2227            return Err(Error::JsonRpc(JsonRpcError::method_not_found(method)));
2228        }
2229        if client_declares_tasks(extensions) {
2230            return Ok(());
2231        }
2232        Err(Error::JsonRpc(
2233            JsonRpcError::missing_required_client_capability(tasks_client_capabilities()),
2234        ))
2235    }
2236
2237    /// Verify the caller may act on this task.
2238    ///
2239    /// A task the caller does not own is reported exactly as an unknown task.
2240    /// Distinguishing the two would confirm that an ID is real, which is the
2241    /// thing unguessable IDs exist to prevent.
2242    async fn authorize_task(
2243        &self,
2244        task_id: &str,
2245        extensions: &crate::context::Extensions,
2246    ) -> Result<()> {
2247        let owner = self
2248            .inner
2249            .task_store
2250            .task_owner(task_id)
2251            .await
2252            .map_err(task_store_error)?
2253            .ok_or_else(|| Error::JsonRpc(unknown_task_error(task_id)))?;
2254
2255        if crate::async_task::owner_matches(&owner, request_principal(extensions).as_deref()) {
2256            Ok(())
2257        } else {
2258            tracing::debug!(
2259                target: "mcp::tasks",
2260                task_id = %task_id,
2261                "task operation refused: principal does not own the task"
2262            );
2263            Err(Error::JsonRpc(unknown_task_error(task_id)))
2264        }
2265    }
2266
2267    /// Serve a final `tasks/get` as a status-discriminated `DetailedTask`.
2268    async fn final_get_task(&self, task_id: &str) -> Result<McpResponse> {
2269        let detailed = self.detailed_task(task_id).await?;
2270        Ok(McpResponse::FinalGetTask(crate::tasks::GetTaskResult::new(
2271            detailed,
2272        )))
2273    }
2274
2275    /// Build the complete status-discriminated view of a task.
2276    ///
2277    /// Both `tasks/get` and `notifications/tasks` render a task through this
2278    /// one path, which is what makes a pushed notification identical to the
2279    /// poll response a client would have received at that moment.
2280    async fn detailed_task(&self, task_id: &str) -> Result<crate::tasks::DetailedTask> {
2281        let (task, result, error) = self
2282            .inner
2283            .task_store
2284            .get_task_result(task_id)
2285            .await
2286            .map_err(task_store_error)?
2287            .ok_or_else(|| Error::JsonRpc(unknown_task_error(task_id)))?;
2288
2289        let mut metadata = crate::tasks::TaskMetadata::new(
2290            task.task_id.clone(),
2291            task.created_at.clone(),
2292            task.last_updated_at.clone(),
2293            task.ttl,
2294        );
2295        metadata.status_message = task.status_message.clone();
2296        metadata.poll_interval_ms = task.poll_interval;
2297
2298        Ok(match task.status {
2299            TaskStatus::Working => crate::tasks::DetailedTask::working(metadata),
2300            TaskStatus::InputRequired => {
2301                // Every request still awaiting a response, not just the most
2302                // recent one.
2303                let outstanding = self
2304                    .inner
2305                    .task_store
2306                    .outstanding_input_requests(task_id)
2307                    .await
2308                    .map_err(task_store_error)?
2309                    .unwrap_or_default();
2310                crate::tasks::DetailedTask::input_required(metadata, outstanding)
2311            }
2312            TaskStatus::Completed => {
2313                // The exact object the synchronous call would have returned,
2314                // including `isError: true` results.
2315                let mut object = result
2316                    .map(serde_json::to_value)
2317                    .transpose()
2318                    .map_err(|e| {
2319                        Error::JsonRpc(JsonRpcError::internal_error(format!(
2320                            "failed to encode task result: {e}"
2321                        )))
2322                    })?
2323                    .and_then(|value| value.as_object().cloned())
2324                    .unwrap_or_default();
2325                // This object is nested inside tasks/get, so it does not pass
2326                // through the JSON-RPC response stamper that adds the final
2327                // protocol's required complete discriminator.
2328                object.insert(
2329                    "resultType".to_string(),
2330                    serde_json::Value::String("complete".to_string()),
2331                );
2332                crate::tasks::DetailedTask::completed(metadata, object)
2333            }
2334            TaskStatus::Failed => crate::tasks::DetailedTask::failed(
2335                metadata,
2336                error.unwrap_or_else(|| JsonRpcError::internal_error("Task failed")),
2337            ),
2338            TaskStatus::Cancelled => crate::tasks::DetailedTask::cancelled(metadata),
2339            // `TaskStatus` is non_exhaustive. Report an unrecognized status as
2340            // working rather than inventing a terminal state.
2341            _ => crate::tasks::DetailedTask::working(metadata),
2342        })
2343    }
2344
2345    /// Push the current state of a task to subscribed listen streams.
2346    ///
2347    /// Best effort by design. A task outlives the request that created it, so
2348    /// there may be no subscriber at all, and SEP-2663 keeps `tasks/get`
2349    /// authoritative precisely so a dropped notification costs a client
2350    /// nothing beyond a slower poll. A failure to read the task back is
2351    /// therefore logged rather than propagated: the caller has already
2352    /// committed the state change this announces.
2353    async fn notify_task_state(&self, task_id: &str) {
2354        if !self.final_tasks_enabled() {
2355            return;
2356        }
2357
2358        let detailed = match self.detailed_task(task_id).await {
2359            Ok(detailed) => detailed,
2360            Err(error) => {
2361                tracing::debug!(
2362                    target: "mcp::tasks",
2363                    task_id = %task_id,
2364                    %error,
2365                    "skipping task notification: task state unavailable"
2366                );
2367                return;
2368            }
2369        };
2370
2371        let notification = ServerNotification::FinalTaskStatusChanged(
2372            crate::tasks::TaskStatusNotificationParams {
2373                task: detailed,
2374                meta: None,
2375            },
2376        );
2377
2378        // Delivery goes through the transport-lifetime sink rather than the
2379        // originating request's sender: the `tools/call` that created the task
2380        // has usually completed by the time a terminal transition happens, and
2381        // its stream is gone.
2382        #[cfg(all(feature = "http", feature = "stateless"))]
2383        if let Ok(active) = self.inner.modern_notification_sink.read()
2384            && let Some(sink) = active.as_ref()
2385        {
2386            sink(&notification);
2387            return;
2388        }
2389
2390        if let Some(tx) = &self.inner.notification_tx {
2391            let _ = tx.try_send(notification);
2392        }
2393    }
2394
2395    /// Effective SEP-2549 cache scope to emit alongside a TTL hint.
2396    ///
2397    /// Returns the configured scope, or `private` (the conservative choice)
2398    /// when a TTL is being emitted without an explicit scope. Returns `None`
2399    /// when no TTL is emitted and no scope is configured, so responses
2400    /// without hints stay hint-free.
2401    fn effective_cache_scope(&self, ttl_ms: Option<u64>) -> Option<CacheScope> {
2402        self.inner
2403            .cache_scope
2404            .or_else(|| ttl_ms.map(|_| CacheScope::Private))
2405    }
2406
2407    /// Fill in SEP-2549 caching hints on a resources/read result.
2408    ///
2409    /// Handler-set values win; the router-level `read_ttl` and `cache_scope`
2410    /// configuration only fills fields the handler left unset.
2411    fn apply_read_cache_hints(&self, mut result: ReadResourceResult) -> ReadResourceResult {
2412        if result.ttl_ms.is_none() {
2413            result.ttl_ms = self.inner.read_ttl_ms;
2414        }
2415        if result.cache_scope.is_none() {
2416            result.cache_scope = self.effective_cache_scope(result.ttl_ms);
2417        }
2418        result
2419    }
2420
2421    /// Handle an MCP request
2422    async fn handle(
2423        &self,
2424        request_id: RequestId,
2425        request: McpRequest,
2426        extensions: Extensions,
2427    ) -> Result<McpResponse> {
2428        // Enforce session state - reject requests before initialization
2429        let method = request.method_name();
2430        if !is_final_protocol_request(&extensions) && !self.session.is_request_allowed(method) {
2431            tracing::warn!(
2432                method = %method,
2433                phase = ?self.session.phase(),
2434                "Request rejected: session not initialized"
2435            );
2436            return Err(Error::JsonRpc(JsonRpcError::invalid_request(format!(
2437                "Session not initialized. Only 'initialize' and 'ping' are allowed before initialization. Got: {}",
2438                method
2439            ))));
2440        }
2441
2442        match request {
2443            McpRequest::Initialize(params) => {
2444                tracing::info!(
2445                    client = %params.client_info.name,
2446                    version = %params.client_info.version,
2447                    "Client initializing"
2448                );
2449
2450                // HTTP and other configurable transports inject their exact
2451                // runtime allow-list. Direct router use retains the stable
2452                // default policy.
2453                let protocol_support = extensions.get::<crate::ProtocolSupport>();
2454                let requested_is_legacy = crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
2455                    .contains(&params.protocol_version.as_str());
2456                let requested_is_supported = requested_is_legacy
2457                    && protocol_support
2458                        .is_none_or(|support| support.contains(&params.protocol_version));
2459                let protocol_version = if requested_is_supported {
2460                    params.protocol_version
2461                } else {
2462                    match protocol_support {
2463                        None => crate::protocol::LATEST_PROTOCOL_VERSION.to_string(),
2464                        Some(support) => support
2465                            .versions()
2466                            .iter()
2467                            .find(|version| {
2468                                crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
2469                                    .contains(&version.as_str())
2470                            })
2471                            .cloned()
2472                            .ok_or_else(|| {
2473                                Error::JsonRpc(JsonRpcError::unsupported_protocol_version(
2474                                    params.protocol_version,
2475                                    support.versions().iter().map(String::as_str),
2476                                ))
2477                            })?,
2478                    }
2479                };
2480
2481                // Transition session state to Initializing
2482                self.session.mark_initializing();
2483                let capabilities = self.capabilities_for_protocol(Some(&protocol_version));
2484                self.session.insert(params.capabilities.clone());
2485                self.session
2486                    .insert(crate::NegotiatedExtensions::from_capabilities(
2487                        &params.capabilities,
2488                        &capabilities,
2489                    ));
2490
2491                Ok(McpResponse::Initialize(InitializeResult {
2492                    protocol_version,
2493                    capabilities,
2494                    server_info: self.implementation(),
2495                    instructions: if let Some(config) = &self.inner.auto_instructions {
2496                        Some(self.inner.generate_instructions(config))
2497                    } else {
2498                        self.inner.instructions.clone()
2499                    },
2500                    meta: None,
2501                }))
2502            }
2503
2504            McpRequest::Discover(_) => {
2505                // SEP-2575 server/discover -- stateless capability advertisement.
2506                // Unlike initialize, this does NOT transition session state and
2507                // does not require a session at all. Returns the same capability
2508                // surface plus the full set of protocol versions we can speak,
2509                // so clients can pick one and signal it via MCP-Protocol-Version
2510                // on subsequent requests.
2511                tracing::debug!("Stateless server/discover request");
2512                let server_info = self.implementation();
2513                let supported_versions = extensions.get::<crate::ProtocolSupport>().map_or_else(
2514                    || {
2515                        crate::protocol::SUPPORTED_PROTOCOL_VERSIONS
2516                            .iter()
2517                            .map(|version| (*version).to_string())
2518                            .collect()
2519                    },
2520                    |support| support.versions().to_vec(),
2521                );
2522                // server/discover is itself the entry point for the final
2523                // stateless lifecycle, so its advertised surface must be safe
2524                // even when this router is invoked directly without transport
2525                // metadata.
2526                let capabilities = self
2527                    .capabilities_for_protocol(Some(crate::protocol::PROTOCOL_VERSION_2026_07_28));
2528                Ok(McpResponse::Discover(DiscoverResult {
2529                    supported_versions,
2530                    capabilities,
2531                    ttl_ms: None,
2532                    cache_scope: None,
2533                    instructions: if let Some(config) = &self.inner.auto_instructions {
2534                        Some(self.inner.generate_instructions(config))
2535                    } else {
2536                        self.inner.instructions.clone()
2537                    },
2538                    meta: Some(crate::protocol::ResultMeta {
2539                        server_info: Some(server_info),
2540                    }),
2541                }))
2542            }
2543
2544            McpRequest::ListTools(params) => {
2545                let final_protocol = is_final_protocol_request(&extensions);
2546                let final_tasks_negotiated = final_protocol
2547                    && self.final_tasks_enabled()
2548                    && client_declares_tasks(&extensions);
2549                let filter = self.inner.tool_filter.as_ref();
2550                let disabled = self.inner.disabled_tools.read().unwrap().clone();
2551                let is_visible = |t: &Tool| {
2552                    !disabled.contains(&t.name)
2553                        && !(final_protocol
2554                            && matches!(t.task_support, TaskSupportMode::Required)
2555                            && !final_tasks_negotiated)
2556                        && filter
2557                            .map(|f| f.is_visible(&self.session, t))
2558                            .unwrap_or(true)
2559                };
2560                let definition = |t: &Tool| {
2561                    let mut definition = t.definition();
2562                    if final_protocol {
2563                        definition.execution = None;
2564                    }
2565                    definition
2566                };
2567
2568                // Collect static tools
2569                let mut tools: Vec<ToolDefinition> = self
2570                    .inner
2571                    .tools
2572                    .values()
2573                    .filter(|t| is_visible(t))
2574                    .map(|t| definition(t))
2575                    .collect();
2576
2577                // Merge dynamic tools (static tools win on name collision)
2578                #[cfg(feature = "dynamic-tools")]
2579                if let Some(ref dynamic) = self.inner.dynamic_tools {
2580                    let static_names: HashSet<String> =
2581                        tools.iter().map(|t| t.name.clone()).collect();
2582                    for t in dynamic.list() {
2583                        if !static_names.contains(&t.name) && is_visible(&t) {
2584                            tools.push(definition(&t));
2585                        }
2586                    }
2587                }
2588
2589                tools.sort_by(|a, b| a.name.cmp(&b.name));
2590
2591                let (tools, next_cursor) =
2592                    paginate(tools, params.cursor.as_deref(), self.inner.page_size)?;
2593
2594                Ok(McpResponse::ListTools(ListToolsResult {
2595                    tools,
2596                    next_cursor,
2597                    ttl_ms: self.inner.list_ttl_ms,
2598                    cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2599                    meta: None,
2600                }))
2601            }
2602
2603            McpRequest::CallTool(params) => {
2604                // Disabled tools are reported as if they don't exist.
2605                if self
2606                    .inner
2607                    .disabled_tools
2608                    .read()
2609                    .unwrap()
2610                    .contains(&params.name)
2611                {
2612                    tracing::info!(
2613                        target: "mcp::tools",
2614                        tool = %params.name,
2615                        status = "disabled",
2616                        "tool call completed"
2617                    );
2618                    return Err(Error::JsonRpc(JsonRpcError::method_not_found(&params.name)));
2619                }
2620
2621                // Look up static tools first, then dynamic
2622                let tool = self.inner.tools.get(&params.name).cloned();
2623                #[cfg(feature = "dynamic-tools")]
2624                let tool = tool.or_else(|| {
2625                    self.inner
2626                        .dynamic_tools
2627                        .as_ref()
2628                        .and_then(|d| d.get(&params.name))
2629                });
2630
2631                let tool = match tool {
2632                    Some(t) => t,
2633                    None => {
2634                        tracing::info!(
2635                            target: "mcp::tools",
2636                            tool = %params.name,
2637                            status = "not_found",
2638                            "tool call completed"
2639                        );
2640                        return Err(Error::JsonRpc(JsonRpcError::method_not_found(&params.name)));
2641                    }
2642                };
2643
2644                // Check tool filter if configured
2645                if let Some(filter) = &self.inner.tool_filter
2646                    && !filter.is_visible(&self.session, &tool)
2647                {
2648                    tracing::info!(
2649                        target: "mcp::tools",
2650                        tool = %params.name,
2651                        status = "denied",
2652                        "tool call completed"
2653                    );
2654                    return Err(filter.denial_error(&params.name));
2655                }
2656
2657                // Task creation is client-directed on the legacy protocol and
2658                // server-directed on the final protocol. `Some(None)` means
2659                // create a task using the server-selected TTL.
2660                let final_protocol = is_final_protocol_request(&extensions);
2661                let task_ttl = if final_protocol {
2662                    if params.task.is_some() {
2663                        return Err(Error::JsonRpc(JsonRpcError::invalid_params(
2664                            "The final Tasks extension does not allow a 'task' request parameter",
2665                        )));
2666                    }
2667
2668                    let server_enabled = self.final_tasks_enabled();
2669                    let tasks_negotiated = server_enabled && client_declares_tasks(&extensions);
2670                    match tool.task_support {
2671                        TaskSupportMode::Required if !server_enabled => {
2672                            // Match tools/list: a final-only task tool is not
2673                            // part of this server's surface until it opts in.
2674                            return Err(Error::JsonRpc(JsonRpcError::method_not_found(
2675                                &params.name,
2676                            )));
2677                        }
2678                        TaskSupportMode::Required if !tasks_negotiated => {
2679                            return Err(Error::JsonRpc(
2680                                JsonRpcError::missing_required_client_capability(
2681                                    tasks_client_capabilities(),
2682                                ),
2683                            ));
2684                        }
2685                        TaskSupportMode::Required | TaskSupportMode::Optional
2686                            if tasks_negotiated =>
2687                        {
2688                            Some(None)
2689                        }
2690                        _ => None,
2691                    }
2692                } else {
2693                    match (&params.task, tool.task_support) {
2694                        (Some(_), TaskSupportMode::Forbidden) => {
2695                            return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
2696                                "Tool '{}' does not support async tasks",
2697                                params.name
2698                            ))));
2699                        }
2700                        (None, TaskSupportMode::Required) => {
2701                            return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
2702                                "Tool '{}' requires async task execution (include 'task' in params)",
2703                                params.name
2704                            ))));
2705                        }
2706                        (Some(task), _) => Some(task.ttl),
2707                        (None, _) => None,
2708                    }
2709                };
2710
2711                // Final 2026-07-28 requests declare client capabilities on
2712                // every request. Reject a tool before any handler work begins
2713                // when its declared requirement is not present.
2714                #[cfg(feature = "stateless")]
2715                if let Some(required) = tool.required_client_capabilities()
2716                    && let Some(meta) = extensions.get::<crate::stateless::StatelessRequestMeta>()
2717                    && meta.protocol_version.as_deref()
2718                        == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
2719                    && !meta
2720                        .client_capabilities
2721                        .as_ref()
2722                        .is_some_and(|actual| client_capabilities_satisfy(actual, required))
2723                {
2724                    return Err(Error::JsonRpc(
2725                        JsonRpcError::missing_required_client_capability(required.clone()),
2726                    ));
2727                }
2728
2729                if let Some(task_ttl) = task_ttl {
2730                    // Create the task
2731                    let (task_id, cancellation_token) = self
2732                        .inner
2733                        .task_store
2734                        .create_task(
2735                            &params.name,
2736                            params.arguments.clone(),
2737                            task_ttl,
2738                            request_principal(&extensions),
2739                        )
2740                        .await
2741                        .map_err(task_store_error)?;
2742
2743                    tracing::info!(task_id = %task_id, tool = %params.name, "Created async task");
2744
2745                    // Create a context for the async task execution
2746                    let progress_token = params.meta.and_then(|m| m.progress_token);
2747                    let ctx = self.create_context_with_extensions(
2748                        request_id,
2749                        progress_token,
2750                        &extensions,
2751                    );
2752
2753                    // Spawn the task execution in the background
2754                    let task_store = self.inner.task_store.clone();
2755                    let tool = tool.clone();
2756                    let arguments = params.arguments;
2757                    let task_id_clone = task_id.clone();
2758
2759                    let tool_name = params.name.clone();
2760                    let notifier = self.clone();
2761                    tokio::spawn(async move {
2762                        // Check for cancellation before starting
2763                        if cancellation_token.is_cancelled() {
2764                            tracing::debug!(task_id = %task_id_clone, "Task cancelled before execution");
2765                            notifier.notify_task_state(&task_id_clone).await;
2766                            return;
2767                        }
2768
2769                        // Execute the tool
2770                        let start = std::time::Instant::now();
2771                        let result = tool.call_with_context(ctx, arguments).await;
2772                        let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
2773
2774                        if cancellation_token.is_cancelled() {
2775                            tracing::debug!(task_id = %task_id_clone, "Task cancelled during execution");
2776                            notifier.notify_task_state(&task_id_clone).await;
2777                        } else {
2778                            // A tool result carrying `isError: true` completes
2779                            // the task: the tool ran and produced a domain
2780                            // error. SEP-2663 reserves `failed` for execution
2781                            // failures, which surface as a JSON-RPC error.
2782                            let status = if result.is_error { "error" } else { "success" };
2783                            let error_msg = result
2784                                .is_error
2785                                .then(|| result.first_text().unwrap_or("Tool execution failed"))
2786                                .map(str::to_string);
2787                            if let Err(e) = task_store.complete_task(&task_id_clone, result).await {
2788                                tracing::warn!(task_id = %task_id_clone, error = %e, "failed to record task completion");
2789                            }
2790                            tracing::info!(
2791                                target: "mcp::tools",
2792                                tool = %tool_name,
2793                                task_id = %task_id_clone,
2794                                duration_ms,
2795                                status,
2796                                error = error_msg.as_deref().unwrap_or_default(),
2797                                "tool call completed"
2798                            );
2799                            notifier.notify_task_state(&task_id_clone).await;
2800                        }
2801                    });
2802
2803                    let task = self
2804                        .inner
2805                        .task_store
2806                        .get_task(&task_id)
2807                        .await
2808                        .map_err(task_store_error)?
2809                        .ok_or_else(|| {
2810                            Error::JsonRpc(JsonRpcError::internal_error(
2811                                "Failed to retrieve created task",
2812                            ))
2813                        })?;
2814
2815                    // The final wire is flat with `resultType: "task"`; the
2816                    // legacy shape nests a `task` compatibility mirror. Pick
2817                    // by protocol version rather than emitting a hybrid.
2818                    if is_final_protocol_request(&extensions) {
2819                        let mut metadata = crate::tasks::TaskMetadata::new(
2820                            task.task_id.clone(),
2821                            task.created_at.clone(),
2822                            task.last_updated_at.clone(),
2823                            task.ttl,
2824                        );
2825                        metadata.status_message = task.status_message.clone();
2826                        metadata.poll_interval_ms = task.poll_interval;
2827                        return Ok(McpResponse::FinalCreateTask(
2828                            crate::tasks::CreateTaskResult::new(crate::tasks::Task::new(
2829                                metadata,
2830                                task.status,
2831                            )),
2832                        ));
2833                    }
2834                    Ok(McpResponse::CreateTask(CreateTaskResult::new(task)))
2835                } else {
2836                    // Extract progress token from request metadata
2837                    let progress_token = params.meta.and_then(|m| m.progress_token);
2838                    let ctx = self.create_context_with_extensions(
2839                        request_id,
2840                        progress_token,
2841                        &extensions,
2842                    );
2843                    #[cfg(feature = "stateless")]
2844                    let ctx = {
2845                        let mut ctx = ctx;
2846                        ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
2847                            params.input_responses,
2848                            params.request_state,
2849                        ));
2850                        ctx
2851                    };
2852
2853                    let start = std::time::Instant::now();
2854                    let outcome = tool
2855                        .call_outcome_with_context(ctx, params.arguments)
2856                        .await?;
2857                    let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
2858
2859                    match outcome {
2860                        RequestOutcome::Complete(result) => {
2861                            let status = if result.is_error { "error" } else { "success" };
2862                            tracing::info!(
2863                                target: "mcp::tools",
2864                                tool = %params.name,
2865                                duration_ms,
2866                                status,
2867                                "tool call completed"
2868                            );
2869                            Ok(McpResponse::CallTool(result))
2870                        }
2871                        RequestOutcome::InputRequired(result) => {
2872                            #[cfg(feature = "stateless")]
2873                            {
2874                                validate_input_required_result(&extensions, &result)?;
2875                                tracing::info!(
2876                                    target: "mcp::tools",
2877                                    tool = %params.name,
2878                                    duration_ms,
2879                                    status = "input_required",
2880                                    "tool call requires client input"
2881                                );
2882                                Ok(McpResponse::InputRequired(result))
2883                            }
2884                            #[cfg(not(feature = "stateless"))]
2885                            {
2886                                let _ = result;
2887                                Err(Error::invalid_params(
2888                                    "InputRequiredResult support was not compiled",
2889                                ))
2890                            }
2891                        }
2892                    }
2893                }
2894            }
2895
2896            McpRequest::ListResources(params) => {
2897                let disabled = self.inner.disabled_resources.read().unwrap().clone();
2898                let is_visible = |r: &Resource| -> bool {
2899                    !disabled.contains(&r.uri)
2900                        && self
2901                            .inner
2902                            .resource_filter
2903                            .as_ref()
2904                            .map(|f| f.is_visible(&self.session, r))
2905                            .unwrap_or(true)
2906                };
2907
2908                let mut resources: Vec<ResourceDefinition> = self
2909                    .inner
2910                    .resources
2911                    .values()
2912                    .filter(|r| is_visible(r))
2913                    .map(|r| r.definition())
2914                    .collect();
2915
2916                // Merge dynamic resources (static resources win on URI collision)
2917                #[cfg(feature = "dynamic-tools")]
2918                if let Some(ref dynamic) = self.inner.dynamic_resources {
2919                    let static_uris: HashSet<String> =
2920                        resources.iter().map(|r| r.uri.clone()).collect();
2921                    for r in dynamic.list() {
2922                        if !static_uris.contains(&r.uri) && is_visible(&r) {
2923                            resources.push(r.definition());
2924                        }
2925                    }
2926                }
2927
2928                resources.sort_by(|a, b| a.uri.cmp(&b.uri));
2929
2930                let (resources, next_cursor) =
2931                    paginate(resources, params.cursor.as_deref(), self.inner.page_size)?;
2932
2933                Ok(McpResponse::ListResources(ListResourcesResult {
2934                    resources,
2935                    next_cursor,
2936                    ttl_ms: self.inner.list_ttl_ms,
2937                    cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2938                    meta: None,
2939                }))
2940            }
2941
2942            McpRequest::ListResourceTemplates(params) => {
2943                let mut resource_templates: Vec<ResourceTemplateDefinition> = self
2944                    .inner
2945                    .resource_templates
2946                    .iter()
2947                    .map(|t| t.definition())
2948                    .collect();
2949
2950                // Merge dynamic resource templates (static win on collision)
2951                #[cfg(feature = "dynamic-tools")]
2952                if let Some(ref dynamic) = self.inner.dynamic_resource_templates {
2953                    let static_patterns: HashSet<String> = resource_templates
2954                        .iter()
2955                        .map(|t| t.uri_template.clone())
2956                        .collect();
2957                    for t in dynamic.list() {
2958                        if !static_patterns.contains(&t.uri_template) {
2959                            resource_templates.push(t.definition());
2960                        }
2961                    }
2962                }
2963
2964                resource_templates.sort_by(|a, b| a.uri_template.cmp(&b.uri_template));
2965
2966                let (resource_templates, next_cursor) = paginate(
2967                    resource_templates,
2968                    params.cursor.as_deref(),
2969                    self.inner.page_size,
2970                )?;
2971
2972                Ok(McpResponse::ListResourceTemplates(
2973                    ListResourceTemplatesResult {
2974                        resource_templates,
2975                        next_cursor,
2976                        ttl_ms: self.inner.list_ttl_ms,
2977                        cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
2978                        meta: None,
2979                    },
2980                ))
2981            }
2982
2983            McpRequest::ReadResource(params) => {
2984                // Disabled resources are reported as if they don't exist.
2985                if self
2986                    .inner
2987                    .disabled_resources
2988                    .read()
2989                    .unwrap()
2990                    .contains(&params.uri)
2991                {
2992                    return Err(Error::JsonRpc(JsonRpcError::resource_not_found(
2993                        &params.uri,
2994                    )));
2995                }
2996
2997                // First, try to find a static resource
2998                if let Some(resource) = self.inner.resources.get(&params.uri) {
2999                    // Check resource filter if configured
3000                    if let Some(filter) = &self.inner.resource_filter
3001                        && !filter.is_visible(&self.session, resource)
3002                    {
3003                        return Err(filter.denial_error(&params.uri));
3004                    }
3005
3006                    tracing::debug!(uri = %params.uri, "Reading static resource");
3007                    let ctx = self.create_context_with_extensions(request_id, None, &extensions);
3008                    #[cfg(feature = "stateless")]
3009                    let ctx = {
3010                        let mut ctx = ctx;
3011                        ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
3012                            params.input_responses.clone(),
3013                            params.request_state.clone(),
3014                        ));
3015                        ctx
3016                    };
3017                    return match resource.read_outcome_with_context(ctx).await? {
3018                        RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
3019                            self.apply_read_cache_hints(result),
3020                        )),
3021                        RequestOutcome::InputRequired(result) => {
3022                            #[cfg(feature = "stateless")]
3023                            {
3024                                validate_input_required_result(&extensions, &result)?;
3025                                Ok(McpResponse::InputRequired(result))
3026                            }
3027                            #[cfg(not(feature = "stateless"))]
3028                            {
3029                                let _ = result;
3030                                Err(Error::invalid_params(
3031                                    "InputRequiredResult support was not compiled",
3032                                ))
3033                            }
3034                        }
3035                    };
3036                }
3037
3038                // Try dynamic resources
3039                #[cfg(feature = "dynamic-tools")]
3040                #[allow(clippy::collapsible_if)]
3041                if let Some(ref dynamic) = self.inner.dynamic_resources {
3042                    if let Some(resource) = dynamic.get(&params.uri) {
3043                        if let Some(filter) = &self.inner.resource_filter
3044                            && !filter.is_visible(&self.session, &resource)
3045                        {
3046                            return Err(filter.denial_error(&params.uri));
3047                        }
3048                        tracing::debug!(uri = %params.uri, "Reading dynamic resource");
3049                        let ctx =
3050                            self.create_context_with_extensions(request_id, None, &extensions);
3051                        #[cfg(feature = "stateless")]
3052                        let ctx = {
3053                            let mut ctx = ctx;
3054                            ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
3055                                params.input_responses.clone(),
3056                                params.request_state.clone(),
3057                            ));
3058                            ctx
3059                        };
3060                        return match resource.read_outcome_with_context(ctx).await? {
3061                            RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
3062                                self.apply_read_cache_hints(result),
3063                            )),
3064                            RequestOutcome::InputRequired(result) => {
3065                                #[cfg(feature = "stateless")]
3066                                {
3067                                    validate_input_required_result(&extensions, &result)?;
3068                                    Ok(McpResponse::InputRequired(result))
3069                                }
3070                                #[cfg(not(feature = "stateless"))]
3071                                {
3072                                    let _ = result;
3073                                    Err(Error::invalid_params(
3074                                        "InputRequiredResult support was not compiled",
3075                                    ))
3076                                }
3077                            }
3078                        };
3079                    }
3080                }
3081
3082                // Try static templates
3083                for template in &self.inner.resource_templates {
3084                    if let Some(variables) = template.match_uri(&params.uri) {
3085                        tracing::debug!(
3086                            uri = %params.uri,
3087                            template = %template.uri_template,
3088                            "Reading resource via template"
3089                        );
3090                        let ctx =
3091                            self.create_context_with_extensions(request_id, None, &extensions);
3092                        #[cfg(feature = "stateless")]
3093                        let ctx = {
3094                            let mut ctx = ctx;
3095                            ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
3096                                params.input_responses.clone(),
3097                                params.request_state.clone(),
3098                            ));
3099                            ctx
3100                        };
3101                        return match template
3102                            .read_outcome_with_context(ctx, &params.uri, variables)
3103                            .await?
3104                        {
3105                            RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
3106                                self.apply_read_cache_hints(result),
3107                            )),
3108                            RequestOutcome::InputRequired(result) => {
3109                                #[cfg(feature = "stateless")]
3110                                {
3111                                    validate_input_required_result(&extensions, &result)?;
3112                                    Ok(McpResponse::InputRequired(result))
3113                                }
3114                                #[cfg(not(feature = "stateless"))]
3115                                {
3116                                    let _ = result;
3117                                    Err(Error::invalid_params(
3118                                        "InputRequiredResult support was not compiled",
3119                                    ))
3120                                }
3121                            }
3122                        };
3123                    }
3124                }
3125
3126                // Try dynamic templates
3127                #[cfg(feature = "dynamic-tools")]
3128                #[allow(clippy::collapsible_if)]
3129                if let Some(ref dynamic) = self.inner.dynamic_resource_templates {
3130                    if let Some((template, variables)) = dynamic.match_uri(&params.uri) {
3131                        tracing::debug!(
3132                            uri = %params.uri,
3133                            template = %template.uri_template,
3134                            "Reading resource via dynamic template"
3135                        );
3136                        let ctx =
3137                            self.create_context_with_extensions(request_id, None, &extensions);
3138                        #[cfg(feature = "stateless")]
3139                        let ctx = {
3140                            let mut ctx = ctx;
3141                            ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
3142                                params.input_responses.clone(),
3143                                params.request_state.clone(),
3144                            ));
3145                            ctx
3146                        };
3147                        return match template
3148                            .read_outcome_with_context(ctx, &params.uri, variables)
3149                            .await?
3150                        {
3151                            RequestOutcome::Complete(result) => Ok(McpResponse::ReadResource(
3152                                self.apply_read_cache_hints(result),
3153                            )),
3154                            RequestOutcome::InputRequired(result) => {
3155                                #[cfg(feature = "stateless")]
3156                                {
3157                                    validate_input_required_result(&extensions, &result)?;
3158                                    Ok(McpResponse::InputRequired(result))
3159                                }
3160                                #[cfg(not(feature = "stateless"))]
3161                                {
3162                                    let _ = result;
3163                                    Err(Error::invalid_params(
3164                                        "InputRequiredResult support was not compiled",
3165                                    ))
3166                                }
3167                            }
3168                        };
3169                    }
3170                }
3171
3172                // No match found
3173                Err(Error::JsonRpc(JsonRpcError::resource_not_found(
3174                    &params.uri,
3175                )))
3176            }
3177
3178            McpRequest::SubscribeResource(params) => {
3179                // Verify the resource exists
3180                if !self.inner.resources.contains_key(&params.uri) {
3181                    return Err(Error::JsonRpc(JsonRpcError::resource_not_found(
3182                        &params.uri,
3183                    )));
3184                }
3185
3186                tracing::debug!(uri = %params.uri, "Subscribing to resource");
3187                self.subscribe(&params.uri);
3188
3189                Ok(McpResponse::SubscribeResource(EmptyResult {}))
3190            }
3191
3192            McpRequest::UnsubscribeResource(params) => {
3193                // Verify the resource exists
3194                if !self.inner.resources.contains_key(&params.uri) {
3195                    return Err(Error::JsonRpc(JsonRpcError::resource_not_found(
3196                        &params.uri,
3197                    )));
3198                }
3199
3200                tracing::debug!(uri = %params.uri, "Unsubscribing from resource");
3201                self.unsubscribe(&params.uri);
3202
3203                Ok(McpResponse::UnsubscribeResource(EmptyResult {}))
3204            }
3205
3206            McpRequest::ListPrompts(params) => {
3207                let disabled = self.inner.disabled_prompts.read().unwrap().clone();
3208                let is_visible = |p: &Prompt| -> bool {
3209                    !disabled.contains(&p.name)
3210                        && self
3211                            .inner
3212                            .prompt_filter
3213                            .as_ref()
3214                            .map(|f| f.is_visible(&self.session, p))
3215                            .unwrap_or(true)
3216                };
3217
3218                let mut prompts: Vec<PromptDefinition> = self
3219                    .inner
3220                    .prompts
3221                    .values()
3222                    .filter(|p| is_visible(p))
3223                    .map(|p| p.definition())
3224                    .collect();
3225
3226                // Merge dynamic prompts (static prompts win on name collision)
3227                #[cfg(feature = "dynamic-tools")]
3228                if let Some(ref dynamic) = self.inner.dynamic_prompts {
3229                    let static_names: HashSet<String> =
3230                        prompts.iter().map(|p| p.name.clone()).collect();
3231                    for p in dynamic.list() {
3232                        if !static_names.contains(&p.name) && is_visible(&p) {
3233                            prompts.push(p.definition());
3234                        }
3235                    }
3236                }
3237
3238                prompts.sort_by(|a, b| a.name.cmp(&b.name));
3239
3240                let (prompts, next_cursor) =
3241                    paginate(prompts, params.cursor.as_deref(), self.inner.page_size)?;
3242
3243                Ok(McpResponse::ListPrompts(ListPromptsResult {
3244                    prompts,
3245                    next_cursor,
3246                    ttl_ms: self.inner.list_ttl_ms,
3247                    cache_scope: self.effective_cache_scope(self.inner.list_ttl_ms),
3248                    meta: None,
3249                }))
3250            }
3251
3252            McpRequest::GetPrompt(params) => {
3253                // Disabled prompts are reported as if they don't exist.
3254                if self
3255                    .inner
3256                    .disabled_prompts
3257                    .read()
3258                    .unwrap()
3259                    .contains(&params.name)
3260                {
3261                    return Err(Error::JsonRpc(JsonRpcError::method_not_found(&format!(
3262                        "Prompt not found: {}",
3263                        params.name
3264                    ))));
3265                }
3266
3267                // Look up static prompts first, then dynamic
3268                let prompt = self.inner.prompts.get(&params.name).cloned();
3269                #[cfg(feature = "dynamic-tools")]
3270                let prompt = prompt.or_else(|| {
3271                    self.inner
3272                        .dynamic_prompts
3273                        .as_ref()
3274                        .and_then(|d| d.get(&params.name))
3275                });
3276                let prompt = prompt.ok_or_else(|| {
3277                    Error::JsonRpc(JsonRpcError::method_not_found(&format!(
3278                        "Prompt not found: {}",
3279                        params.name
3280                    )))
3281                })?;
3282
3283                // Check prompt filter if configured
3284                if let Some(filter) = &self.inner.prompt_filter
3285                    && !filter.is_visible(&self.session, &prompt)
3286                {
3287                    return Err(filter.denial_error(&params.name));
3288                }
3289
3290                tracing::debug!(name = %params.name, "Getting prompt");
3291                let ctx = self.create_context_with_extensions(request_id, None, &extensions);
3292                #[cfg(feature = "stateless")]
3293                let ctx = {
3294                    let mut ctx = ctx;
3295                    ctx.extensions_mut().insert(crate::mrtr::MrtrRequest::new(
3296                        params.input_responses,
3297                        params.request_state,
3298                    ));
3299                    ctx
3300                };
3301                let outcome = prompt
3302                    .get_outcome_with_context(ctx, params.arguments)
3303                    .await?;
3304
3305                match outcome {
3306                    RequestOutcome::Complete(result) => Ok(McpResponse::GetPrompt(result)),
3307                    RequestOutcome::InputRequired(result) => {
3308                        #[cfg(feature = "stateless")]
3309                        {
3310                            validate_input_required_result(&extensions, &result)?;
3311                            Ok(McpResponse::InputRequired(result))
3312                        }
3313                        #[cfg(not(feature = "stateless"))]
3314                        {
3315                            let _ = result;
3316                            Err(Error::invalid_params(
3317                                "InputRequiredResult support was not compiled",
3318                            ))
3319                        }
3320                    }
3321                }
3322            }
3323
3324            McpRequest::Ping => Ok(McpResponse::Pong(EmptyResult {})),
3325
3326            McpRequest::GetTaskInfo(params) => {
3327                if is_final_protocol_request(&extensions) {
3328                    self.require_negotiated_tasks(&extensions, "tasks/get")?;
3329                    self.authorize_task(&params.task_id, &extensions).await?;
3330                    return self.final_get_task(&params.task_id).await;
3331                }
3332                self.authorize_task(&params.task_id, &extensions).await?;
3333
3334                // SEP-2663 DetailedTask: `tasks/get` carries the
3335                // status-discriminated payload inline. `completed` includes
3336                // the result the synchronous request would have returned;
3337                // `failed` includes the JSON-RPC error. This replaced the
3338                // removed blocking `tasks/result` method as the way clients
3339                // retrieve a task's outcome.
3340                let (mut task, result, error) = self
3341                    .inner
3342                    .task_store
3343                    .get_task_result(&params.task_id)
3344                    .await
3345                    .map_err(task_store_error)?
3346                    .ok_or_else(|| {
3347                        Error::JsonRpc(JsonRpcError::invalid_params(format!(
3348                            "Task not found: {}",
3349                            params.task_id
3350                        )))
3351                    })?;
3352
3353                match task.status {
3354                    TaskStatus::Completed => task.result = result,
3355                    TaskStatus::Failed => {
3356                        // The store preserves the structured error, so the
3357                        // original code and data survive to the client instead
3358                        // of being flattened into an internal-error message.
3359                        task.error = Some(
3360                            error.unwrap_or_else(|| JsonRpcError::internal_error("Task failed")),
3361                        );
3362                    }
3363                    _ => {}
3364                }
3365
3366                Ok(McpResponse::GetTaskInfo(task))
3367            }
3368
3369            McpRequest::UpdateTask(params) => {
3370                if is_final_protocol_request(&extensions) {
3371                    self.require_negotiated_tasks(&extensions, "tasks/update")?;
3372                    self.authorize_task(&params.task_id, &extensions).await?;
3373                    // Partial responses are the normal case: the store
3374                    // consumes what matches an outstanding request and ignores
3375                    // unknown, already-answered, and superseded keys.
3376                    self.inner
3377                        .task_store
3378                        .apply_input_responses(
3379                            &params.task_id,
3380                            decode_input_responses(&params.input_responses),
3381                        )
3382                        .await
3383                        .map_err(task_store_error)?
3384                        .ok_or_else(|| Error::JsonRpc(unknown_task_error(&params.task_id)))?;
3385                    // Answering the last outstanding request resumes the task,
3386                    // so the status a subscriber sees changes here even though
3387                    // the ack itself is empty.
3388                    self.notify_task_state(&params.task_id).await;
3389                    return Ok(McpResponse::FinalTaskAck(
3390                        crate::tasks::TaskAcknowledgement::new(),
3391                    ));
3392                }
3393
3394                self.authorize_task(&params.task_id, &extensions).await?;
3395
3396                // SEP-2663 `tasks/update`: validate the task exists and
3397                // acknowledge with an empty result. tower-mcp does not yet
3398                // model server-initiated `inputRequests` for tasks (that's a
3399                // future MRTR-flavored feature), so we currently treat any
3400                // submitted `inputResponses` as ignorable per spec ("A server
3401                // SHOULD ignore any inputResponses mapped to a key that is
3402                // not currently outstanding").
3403                let _ = self
3404                    .inner
3405                    .task_store
3406                    .get_task(&params.task_id)
3407                    .await
3408                    .map_err(task_store_error)?
3409                    .ok_or_else(|| {
3410                        Error::JsonRpc(JsonRpcError::invalid_params(format!(
3411                            "Task not found: {}",
3412                            params.task_id
3413                        )))
3414                    })?;
3415                Ok(McpResponse::UpdateTask(EmptyResult {}))
3416            }
3417
3418            McpRequest::CancelTask(params) => {
3419                if is_final_protocol_request(&extensions) {
3420                    self.require_negotiated_tasks(&extensions, "tasks/cancel")?;
3421                    self.authorize_task(&params.task_id, &extensions).await?;
3422                    // The final ack does not require a terminal transition:
3423                    // cancelling an already-terminal task is acknowledged, and
3424                    // the observable status is polled via `tasks/get`.
3425                    self.inner
3426                        .task_store
3427                        .cancel_task(&params.task_id, params.reason.as_deref())
3428                        .await
3429                        .map_err(task_store_error)?
3430                        .ok_or_else(|| Error::JsonRpc(unknown_task_error(&params.task_id)))?;
3431                    self.notify_task_state(&params.task_id).await;
3432                    return Ok(McpResponse::FinalTaskAck(
3433                        crate::tasks::TaskAcknowledgement::new(),
3434                    ));
3435                }
3436
3437                self.authorize_task(&params.task_id, &extensions).await?;
3438
3439                // First check if the task exists and is not already terminal
3440                let current = self
3441                    .inner
3442                    .task_store
3443                    .get_task(&params.task_id)
3444                    .await
3445                    .map_err(task_store_error)?
3446                    .ok_or_else(|| {
3447                        Error::JsonRpc(JsonRpcError::invalid_params(format!(
3448                            "Task not found: {}",
3449                            params.task_id
3450                        )))
3451                    })?;
3452
3453                if current.status.is_terminal() {
3454                    return Err(Error::JsonRpc(JsonRpcError::invalid_params(format!(
3455                        "Task {} is already in terminal state: {}",
3456                        params.task_id, current.status
3457                    ))));
3458                }
3459
3460                self.inner
3461                    .task_store
3462                    .cancel_task(&params.task_id, params.reason.as_deref())
3463                    .await
3464                    .map_err(task_store_error)?
3465                    .ok_or_else(|| {
3466                        Error::JsonRpc(JsonRpcError::invalid_params(format!(
3467                            "Task not found: {}",
3468                            params.task_id
3469                        )))
3470                    })?;
3471
3472                // SEP-2663 (final): the cancel acknowledgment MUST be an empty
3473                // result. The observable status is polled via `tasks/get` and
3474                // may remain non-terminal after this ack.
3475                Ok(McpResponse::CancelTask(EmptyResult {}))
3476            }
3477
3478            McpRequest::SetLoggingLevel(params) => {
3479                tracing::debug!(level = ?params.level, "Client set logging level");
3480                if let Ok(mut level) = self.inner.min_log_level.write() {
3481                    *level = params.level;
3482                }
3483                Ok(McpResponse::SetLoggingLevel(EmptyResult {}))
3484            }
3485
3486            McpRequest::Complete(params) => {
3487                tracing::debug!(
3488                    reference = ?params.reference,
3489                    argument = %params.argument.name,
3490                    "Completion request"
3491                );
3492
3493                // Delegate to registered completion handler if available
3494                if let Some(ref handler) = self.inner.completion_handler {
3495                    let result = handler(params).await?;
3496                    Ok(McpResponse::Complete(result))
3497                } else {
3498                    // No completion handler registered, return empty completions
3499                    Ok(McpResponse::Complete(CompleteResult::new(vec![])))
3500                }
3501            }
3502
3503            McpRequest::Unknown { method, .. } => {
3504                Err(Error::JsonRpc(JsonRpcError::method_not_found(&method)))
3505            }
3506            _ => Err(Error::JsonRpc(JsonRpcError::method_not_found(
3507                "unknown method",
3508            ))),
3509        }
3510    }
3511
3512    /// Handle an MCP notification (no response expected)
3513    pub fn handle_notification(&self, notification: McpNotification) {
3514        match notification {
3515            McpNotification::Initialized => {
3516                let phase_before = self.session.phase();
3517                if self.session.mark_initialized() {
3518                    if phase_before == crate::session::SessionPhase::Uninitialized {
3519                        tracing::info!(
3520                            "Session initialized from uninitialized state (race resolved)"
3521                        );
3522                    } else {
3523                        tracing::info!("Session initialized, entering operation phase");
3524                    }
3525                } else {
3526                    tracing::warn!(
3527                        phase = ?self.session.phase(),
3528                        "Received initialized notification in unexpected state"
3529                    );
3530                }
3531            }
3532            McpNotification::Cancelled(params) => {
3533                if let Some(ref request_id) = params.request_id {
3534                    if self.cancel_request(request_id) {
3535                        tracing::info!(
3536                            request_id = ?request_id,
3537                            reason = ?params.reason,
3538                            "Request cancelled"
3539                        );
3540                    } else {
3541                        tracing::debug!(
3542                            request_id = ?request_id,
3543                            reason = ?params.reason,
3544                            "Cancellation requested for unknown request"
3545                        );
3546                    }
3547                } else {
3548                    tracing::debug!(
3549                        reason = ?params.reason,
3550                        "Cancellation notification received without request_id"
3551                    );
3552                }
3553            }
3554            McpNotification::Progress(params) => {
3555                tracing::trace!(
3556                    token = ?params.progress_token,
3557                    progress = params.progress,
3558                    total = ?params.total,
3559                    "Progress notification"
3560                );
3561                // Client-to-server progress notifications are unusual but
3562                // valid through 2025-11-25. The final 2026-07-28 schema
3563                // removes ProgressNotification from ClientNotification
3564                // entirely -- clients no longer send this. Notifications are
3565                // fire-and-forget with no response to reject with, so an
3566                // off-spec one arriving here is simply logged and ignored
3567                // rather than rejected, regardless of negotiated version.
3568            }
3569            McpNotification::RootsListChanged => {
3570                tracing::info!("Client roots list changed");
3571                // Server should re-request roots if needed
3572                // This is handled by the application layer
3573            }
3574            McpNotification::Unknown { method, .. } => {
3575                tracing::debug!(method = %method, "Unknown notification received");
3576            }
3577            _ => {
3578                tracing::debug!("Unrecognized notification variant received");
3579            }
3580        }
3581    }
3582}
3583
3584impl Default for McpRouter {
3585    fn default() -> Self {
3586        Self::new()
3587    }
3588}
3589
3590// =============================================================================
3591// Tower Service implementation
3592// =============================================================================
3593
3594// Re-export Extensions from context for backwards compatibility
3595pub use crate::context::Extensions;
3596
3597/// A map of tool names to their annotations, for use by middleware.
3598///
3599/// This is automatically inserted into [`RouterRequest::extensions`] for
3600/// `tools/call` requests, allowing middleware to inspect tool safety hints
3601/// (e.g., `read_only_hint`, `destructive_hint`) without needing direct
3602/// access to the router's tool registry.
3603///
3604/// # Example
3605///
3606/// ```rust,ignore
3607/// use tower_mcp::router::ToolAnnotationsMap;
3608/// use tower_mcp::protocol::McpRequest;
3609///
3610/// // In a middleware Service::call():
3611/// fn call(&mut self, req: RouterRequest) -> Self::Future {
3612///     if let McpRequest::CallTool(params) = &req.inner {
3613///         if let Some(map) = req.extensions.get::<ToolAnnotationsMap>() {
3614///             let annotations = map.get(&params.name);
3615///             // Check annotations.read_only_hint, destructive_hint, etc.
3616///         }
3617///     }
3618///     self.inner.call(req)
3619/// }
3620/// ```
3621#[derive(Debug, Clone)]
3622pub struct ToolAnnotationsMap {
3623    map: Arc<HashMap<String, ToolAnnotations>>,
3624}
3625
3626impl ToolAnnotationsMap {
3627    /// Look up annotations for a tool by name.
3628    ///
3629    /// Returns `None` if the tool has no annotations or doesn't exist.
3630    pub fn get(&self, tool_name: &str) -> Option<&ToolAnnotations> {
3631        self.map.get(tool_name)
3632    }
3633
3634    /// Check if a tool is read-only (does not modify state).
3635    ///
3636    /// Returns `false` if the tool has no annotations or doesn't exist
3637    /// (the MCP spec default for `readOnlyHint` is `false`).
3638    pub fn is_read_only(&self, tool_name: &str) -> bool {
3639        self.map.get(tool_name).is_some_and(|a| a.read_only_hint)
3640    }
3641
3642    /// Check if a tool may have destructive effects.
3643    ///
3644    /// Returns `true` if the tool has no annotations or doesn't exist
3645    /// (the MCP spec default for `destructiveHint` is `true`).
3646    pub fn is_destructive(&self, tool_name: &str) -> bool {
3647        self.map.get(tool_name).is_none_or(|a| a.destructive_hint)
3648    }
3649
3650    /// Check if a tool is idempotent.
3651    ///
3652    /// Returns `false` if the tool has no annotations or doesn't exist
3653    /// (the MCP spec default for `idempotentHint` is `false`).
3654    pub fn is_idempotent(&self, tool_name: &str) -> bool {
3655        self.map.get(tool_name).is_some_and(|a| a.idempotent_hint)
3656    }
3657}
3658
3659/// Request type for the tower Service implementation.
3660///
3661/// # Preserving extensions in middleware
3662///
3663/// When rewriting a request in middleware, use [`with_inner`](Self::with_inner)
3664/// or [`clone_with_inner`](Self::clone_with_inner) instead of constructing a
3665/// new `RouterRequest` directly. Constructing with `Extensions::new()` will
3666/// silently drop extensions set by earlier middleware layers (token claims,
3667/// RBAC context, etc.).
3668///
3669/// ```rust,ignore
3670/// // WRONG: drops extensions from earlier middleware
3671/// let rewritten = RouterRequest {
3672///     id: req.id.clone(),
3673///     inner: new_inner,
3674///     extensions: Extensions::new(),
3675/// };
3676///
3677/// // RIGHT: preserves extensions
3678/// let rewritten = req.with_inner(new_inner);
3679/// ```
3680#[derive(Debug, Clone)]
3681pub struct RouterRequest {
3682    /// The JSON-RPC request ID.
3683    pub id: RequestId,
3684    /// The parsed MCP request.
3685    pub inner: McpRequest,
3686    /// Type-map for passing data (e.g., `TokenClaims`) through middleware.
3687    pub extensions: Extensions,
3688}
3689
3690impl RouterRequest {
3691    /// Create a new `RouterRequest` with empty extensions.
3692    pub fn new(id: RequestId, inner: McpRequest) -> Self {
3693        Self {
3694            id,
3695            inner,
3696            extensions: Extensions::new(),
3697        }
3698    }
3699
3700    /// Replace the inner MCP request, preserving the id and extensions.
3701    ///
3702    /// This is the recommended way to rewrite requests in middleware,
3703    /// as it ensures extensions set by earlier middleware layers
3704    /// (e.g., token claims, RBAC context) are not lost.
3705    pub fn with_inner(self, inner: McpRequest) -> Self {
3706        Self {
3707            id: self.id,
3708            inner,
3709            extensions: self.extensions,
3710        }
3711    }
3712
3713    /// Replace both the id and inner MCP request, preserving extensions.
3714    ///
3715    /// Useful when middleware needs to assign a new request id
3716    /// (e.g., for fan-out or request duplication) while keeping
3717    /// the extensions from the original request.
3718    pub fn with_id_and_inner(self, id: RequestId, inner: McpRequest) -> Self {
3719        Self {
3720            id,
3721            inner,
3722            extensions: self.extensions,
3723        }
3724    }
3725
3726    /// Create a copy of this request with a different inner request,
3727    /// cloning the id and extensions from the original.
3728    ///
3729    /// Unlike [`with_inner`](Self::with_inner), this borrows `self`,
3730    /// which is useful when the original request is still needed
3731    /// (e.g., for traffic mirroring where you send the request to
3732    /// two backends).
3733    pub fn clone_with_inner(&self, inner: McpRequest) -> Self {
3734        Self {
3735            id: self.id.clone(),
3736            inner,
3737            extensions: self.extensions.clone(),
3738        }
3739    }
3740}
3741
3742/// Response type for the tower Service implementation
3743#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3744pub struct RouterResponse {
3745    /// The JSON-RPC request ID this response corresponds to.
3746    pub id: RequestId,
3747    /// The MCP response or JSON-RPC error.
3748    pub inner: std::result::Result<McpResponse, JsonRpcError>,
3749}
3750
3751impl RouterResponse {
3752    /// Returns `true` if the response contains a JSON-RPC error.
3753    ///
3754    /// Since tower-mcp services use `Error = Infallible` (errors are carried
3755    /// inside the response, not in the `Result`), this method is useful for
3756    /// middleware that needs to inspect whether a request failed -- for example,
3757    /// retry or circuit breaker middleware.
3758    ///
3759    /// # Example
3760    ///
3761    /// ```rust,ignore
3762    /// // Response-based retry predicate for tower-resilience or similar
3763    /// fn is_retriable(response: &RouterResponse) -> bool {
3764    ///     response.is_error()
3765    /// }
3766    /// ```
3767    pub fn is_error(&self) -> bool {
3768        self.inner.is_err()
3769    }
3770
3771    /// Convert to JSON-RPC response
3772    pub fn into_jsonrpc(self) -> JsonRpcResponse {
3773        match self.inner {
3774            Ok(response) => match serde_json::to_value(response) {
3775                Ok(result) => JsonRpcResponse::result(self.id, result),
3776                Err(e) => {
3777                    tracing::error!(error = %e, "Failed to serialize response");
3778                    JsonRpcResponse::error(
3779                        Some(self.id),
3780                        JsonRpcError::internal_error(format!("Serialization error: {}", e)),
3781                    )
3782                }
3783            },
3784            Err(error) => JsonRpcResponse::error(Some(self.id), error),
3785        }
3786    }
3787}
3788
3789impl Service<RouterRequest> for McpRouter {
3790    type Response = RouterResponse;
3791    type Error = std::convert::Infallible; // Errors are in the response
3792    type Future =
3793        Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
3794
3795    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
3796        Poll::Ready(Ok(()))
3797    }
3798
3799    fn call(&mut self, req: RouterRequest) -> Self::Future {
3800        let router = self.clone();
3801        let request_id = req.id.clone();
3802        Box::pin(async move {
3803            let result = router.handle(req.id, req.inner, req.extensions).await;
3804            // Clean up tracking after request completes
3805            router.complete_request(&request_id);
3806            Ok(RouterResponse {
3807                id: request_id,
3808                // Map tower-mcp errors to JSON-RPC errors:
3809                // - Error::JsonRpc: forwarded as-is (preserves original code)
3810                // - Error::Tool: mapped to -32603 (Internal Error)
3811                // - All others: mapped to -32603 (Internal Error)
3812                inner: result.map_err(|e| match e {
3813                    Error::JsonRpc(err) => err,
3814                    Error::Tool(err) => JsonRpcError::internal_error(err.to_string()),
3815                    e => JsonRpcError::internal_error(e.to_string()),
3816                }),
3817            })
3818        })
3819    }
3820}
3821
3822#[cfg(test)]
3823mod tests {
3824    use super::*;
3825    use crate::extract::{Context, Json};
3826    use crate::jsonrpc::JsonRpcService;
3827    use crate::tool::ToolBuilder;
3828    use schemars::JsonSchema;
3829    use serde::Deserialize;
3830    use tower::ServiceExt;
3831
3832    #[derive(Debug, Deserialize, JsonSchema)]
3833    struct AddInput {
3834        a: i64,
3835        b: i64,
3836    }
3837
3838    #[cfg(feature = "stateless")]
3839    fn final_extensions(client_capabilities: ClientCapabilities) -> Extensions {
3840        let mut extensions = Extensions::new();
3841        extensions.insert(crate::stateless::StatelessRequestMeta {
3842            protocol_version: Some(PROTOCOL_VERSION_2026_07_28.to_string()),
3843            client_capabilities: Some(client_capabilities),
3844            ..Default::default()
3845        });
3846        extensions
3847    }
3848
3849    #[cfg(feature = "stateless")]
3850    fn tasks_client_extensions() -> Extensions {
3851        final_extensions(ClientCapabilities {
3852            extensions: Some(
3853                [(TASKS_EXTENSION_ID.to_string(), serde_json::json!({}))]
3854                    .into_iter()
3855                    .collect(),
3856            ),
3857            ..Default::default()
3858        })
3859    }
3860
3861    #[cfg(feature = "stateless")]
3862    #[tokio::test]
3863    async fn final_tasks_require_server_opt_in_and_client_declaration() {
3864        let tool = || {
3865            ToolBuilder::new("optional_task")
3866                .task_support(TaskSupportMode::Optional)
3867                .handler(|input: AddInput| async move {
3868                    Ok(CallToolResult::text(format!("{}", input.a + input.b)))
3869                })
3870                .build()
3871        };
3872        let task_params = |task| CallToolParams {
3873            name: "optional_task".to_string(),
3874            arguments: serde_json::json!({"a": 1, "b": 2}),
3875            input_responses: None,
3876            request_state: None,
3877            meta: None,
3878            task,
3879        };
3880
3881        // Registering a task-capable tool is not an opt-in: a server that
3882        // never called `with_tasks` advertises nothing on the final path and
3883        // still refuses the augmentation even to a declaring client.
3884        let implicit = McpRouter::new().tool(tool());
3885        let McpResponse::Discover(result) = implicit
3886            .handle(
3887                RequestId::Number(1),
3888                McpRequest::Discover(DiscoverParams::default()),
3889                Extensions::new(),
3890            )
3891            .await
3892            .unwrap()
3893        else {
3894            panic!("Expected Discover response");
3895        };
3896        assert!(
3897            result
3898                .capabilities
3899                .extensions
3900                .as_ref()
3901                .is_none_or(|extensions| !extensions.contains_key(TASKS_EXTENSION_ID))
3902        );
3903        let error = implicit
3904            .handle(
3905                RequestId::Number(2),
3906                McpRequest::CallTool(task_params(Some(TaskRequestParams { ttl: None }))),
3907                tasks_client_extensions(),
3908            )
3909            .await
3910            .unwrap_err();
3911        assert!(matches!(error, Error::JsonRpc(e) if e.code == -32602));
3912
3913        // Opting in advertises the extension.
3914        let router = McpRouter::new().tool(tool()).with_tasks();
3915        let McpResponse::Discover(result) = router
3916            .handle(
3917                RequestId::Number(3),
3918                McpRequest::Discover(DiscoverParams::default()),
3919                Extensions::new(),
3920            )
3921            .await
3922            .unwrap()
3923        else {
3924            panic!("Expected Discover response");
3925        };
3926        assert!(
3927            result
3928                .capabilities
3929                .extensions
3930                .as_ref()
3931                .is_some_and(|extensions| extensions.contains_key(TASKS_EXTENSION_ID)),
3932            "with_tasks() must advertise the extension on the final path"
3933        );
3934        assert!(
3935            result.capabilities.tasks.is_none(),
3936            "the legacy capability shape is never advertised on the final path"
3937        );
3938
3939        // A client that did not declare the extension gets the synchronous
3940        // form of an optional tool.
3941        let response = router
3942            .handle(
3943                RequestId::Number(4),
3944                McpRequest::CallTool(task_params(None)),
3945                final_extensions(ClientCapabilities::default()),
3946            )
3947            .await
3948            .unwrap();
3949        assert!(matches!(response, McpResponse::CallTool(_)));
3950
3951        // Both sides declared: the server elects a task from an ordinary
3952        // tools/call request.
3953        let response = router
3954            .handle(
3955                RequestId::Number(5),
3956                McpRequest::CallTool(task_params(None)),
3957                tasks_client_extensions(),
3958            )
3959            .await
3960            .unwrap();
3961        assert!(
3962            matches!(response, McpResponse::FinalCreateTask(_)),
3963            "a negotiated request must receive a task, got {response:?}"
3964        );
3965
3966        // The removed legacy request flag is invalid even when the extension
3967        // was negotiated.
3968        let error = router
3969            .handle(
3970                RequestId::Number(6),
3971                McpRequest::CallTool(task_params(Some(TaskRequestParams { ttl: None }))),
3972                tasks_client_extensions(),
3973            )
3974            .await
3975            .unwrap_err();
3976        assert!(matches!(error, Error::JsonRpc(e) if e.code == -32602));
3977    }
3978
3979    #[cfg(feature = "stateless")]
3980    #[tokio::test]
3981    async fn final_task_methods_serve_the_negotiated_wire_shapes() {
3982        let router = McpRouter::new()
3983            .tool(
3984                ToolBuilder::new("optional_task")
3985                    .task_support(TaskSupportMode::Optional)
3986                    .handler(|input: AddInput| async move {
3987                        Ok(CallToolResult::text(format!("{}", input.a + input.b)))
3988                    })
3989                    .build(),
3990            )
3991            .with_tasks();
3992
3993        let McpResponse::FinalCreateTask(created) = router
3994            .handle(
3995                RequestId::Number(1),
3996                McpRequest::CallTool(CallToolParams {
3997                    name: "optional_task".to_string(),
3998                    arguments: serde_json::json!({"a": 1, "b": 2}),
3999                    input_responses: None,
4000                    request_state: None,
4001                    meta: None,
4002                    task: None,
4003                }),
4004                tasks_client_extensions(),
4005            )
4006            .await
4007            .unwrap()
4008        else {
4009            panic!("Expected a final create-task response");
4010        };
4011
4012        // Flat, with no legacy nested mirror.
4013        let wire = serde_json::to_value(&created).unwrap();
4014        assert_eq!(wire["resultType"], "task");
4015        assert!(wire.get("task").is_none(), "final results are flat: {wire}");
4016        assert!(wire["ttlMs"].is_number() || wire["ttlMs"].is_null());
4017        assert!(wire.get("ttl").is_none(), "legacy field name leaked");
4018        let task_id = created.task.metadata.task_id.clone();
4019
4020        // tasks/get returns a status-discriminated DetailedTask.
4021        let McpResponse::FinalGetTask(fetched) = router
4022            .handle(
4023                RequestId::Number(2),
4024                McpRequest::GetTaskInfo(GetTaskInfoParams {
4025                    task_id: task_id.clone(),
4026                    meta: None,
4027                }),
4028                tasks_client_extensions(),
4029            )
4030            .await
4031            .unwrap()
4032        else {
4033            panic!("Expected a final get-task response");
4034        };
4035        let wire = serde_json::to_value(&fetched).unwrap();
4036        assert_eq!(wire["resultType"], "complete");
4037        assert_eq!(wire["taskId"], serde_json::json!(task_id));
4038        assert!(wire["status"].is_string());
4039
4040        // Both ack methods produce the complete acknowledgement.
4041        for (id, request) in [
4042            (
4043                3,
4044                McpRequest::UpdateTask(UpdateTaskParams {
4045                    task_id: task_id.clone(),
4046                    input_responses: HashMap::new(),
4047                    meta: None,
4048                }),
4049            ),
4050            (
4051                4,
4052                McpRequest::CancelTask(CancelTaskParams {
4053                    task_id: task_id.clone(),
4054                    reason: None,
4055                    meta: None,
4056                }),
4057            ),
4058        ] {
4059            let response = router
4060                .handle(RequestId::Number(id), request, tasks_client_extensions())
4061                .await
4062                .unwrap();
4063            let McpResponse::FinalTaskAck(ack) = response else {
4064                panic!("Expected a final ack for request {id}");
4065            };
4066            assert_eq!(
4067                serde_json::to_value(&ack).unwrap(),
4068                serde_json::json!({"resultType": "complete"})
4069            );
4070        }
4071
4072        // An unknown task is invalid params, not a method error.
4073        let error = router
4074            .handle(
4075                RequestId::Number(5),
4076                McpRequest::GetTaskInfo(GetTaskInfoParams {
4077                    task_id: "does-not-exist".to_string(),
4078                    meta: None,
4079                }),
4080                tasks_client_extensions(),
4081            )
4082            .await
4083            .unwrap_err();
4084        assert!(matches!(error, Error::JsonRpc(e) if e.code == -32602));
4085
4086        // A server that advertises Tasks names the capability a client omitted.
4087        let error = router
4088            .handle(
4089                RequestId::Number(6),
4090                McpRequest::GetTaskInfo(GetTaskInfoParams {
4091                    task_id: task_id.clone(),
4092                    meta: None,
4093                }),
4094                final_extensions(ClientCapabilities::default()),
4095            )
4096            .await
4097            .unwrap_err();
4098        let Error::JsonRpc(error) = error else {
4099            panic!("expected a JSON-RPC error");
4100        };
4101        assert_eq!(error.code, -32021);
4102        assert_eq!(
4103            error.data.as_ref().unwrap()["requiredCapabilities"]["extensions"]["io.modelcontextprotocol/tasks"],
4104            serde_json::json!({})
4105        );
4106    }
4107
4108    #[cfg(feature = "stateless")]
4109    #[tokio::test]
4110    async fn final_required_task_tools_follow_per_request_capabilities() {
4111        let router = McpRouter::new()
4112            .tool(
4113                ToolBuilder::new("required_task")
4114                    .task_support(TaskSupportMode::Required)
4115                    .handler(|input: AddInput| async move {
4116                        Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4117                    })
4118                    .build(),
4119            )
4120            .with_tasks();
4121        let params = || CallToolParams {
4122            name: "required_task".to_string(),
4123            arguments: serde_json::json!({"a": 1, "b": 2}),
4124            input_responses: None,
4125            request_state: None,
4126            meta: None,
4127            task: None,
4128        };
4129
4130        let McpResponse::ListTools(without_tasks) = router
4131            .handle(
4132                RequestId::Number(1),
4133                McpRequest::ListTools(ListToolsParams::default()),
4134                final_extensions(ClientCapabilities::default()),
4135            )
4136            .await
4137            .unwrap()
4138        else {
4139            panic!("expected tools/list")
4140        };
4141        assert!(without_tasks.tools.is_empty());
4142
4143        let McpResponse::ListTools(with_tasks) = router
4144            .handle(
4145                RequestId::Number(2),
4146                McpRequest::ListTools(ListToolsParams::default()),
4147                tasks_client_extensions(),
4148            )
4149            .await
4150            .unwrap()
4151        else {
4152            panic!("expected tools/list")
4153        };
4154        assert_eq!(with_tasks.tools.len(), 1);
4155        assert!(with_tasks.tools[0].execution.is_none());
4156
4157        let error = router
4158            .handle(
4159                RequestId::Number(3),
4160                McpRequest::CallTool(params()),
4161                final_extensions(ClientCapabilities::default()),
4162            )
4163            .await
4164            .unwrap_err();
4165        assert!(matches!(error, Error::JsonRpc(error) if error.code == -32021));
4166
4167        let response = router
4168            .handle(
4169                RequestId::Number(4),
4170                McpRequest::CallTool(params()),
4171                tasks_client_extensions(),
4172            )
4173            .await
4174            .unwrap();
4175        assert!(matches!(response, McpResponse::FinalCreateTask(_)));
4176    }
4177
4178    #[cfg(all(feature = "oauth", feature = "stateless"))]
4179    #[tokio::test]
4180    async fn task_operations_are_bound_to_the_creating_principal() {
4181        fn as_principal(subject: &str) -> Extensions {
4182            let mut extensions = tasks_client_extensions();
4183            extensions.insert(crate::oauth::token::TokenClaims {
4184                sub: Some(subject.to_string()),
4185                iss: None,
4186                aud: None,
4187                exp: None,
4188                scope: None,
4189                client_id: None,
4190                extra: HashMap::new(),
4191            });
4192            extensions
4193        }
4194
4195        let router = McpRouter::new()
4196            .tool(
4197                ToolBuilder::new("optional_task")
4198                    .task_support(TaskSupportMode::Optional)
4199                    .handler(|input: AddInput| async move {
4200                        Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4201                    })
4202                    .build(),
4203            )
4204            .with_tasks();
4205
4206        let McpResponse::FinalCreateTask(created) = router
4207            .handle(
4208                RequestId::Number(1),
4209                McpRequest::CallTool(CallToolParams {
4210                    name: "optional_task".to_string(),
4211                    arguments: serde_json::json!({"a": 1, "b": 2}),
4212                    input_responses: None,
4213                    request_state: None,
4214                    meta: None,
4215                    task: None,
4216                }),
4217                as_principal("alice"),
4218            )
4219            .await
4220            .unwrap()
4221        else {
4222            panic!("Expected a final create-task response");
4223        };
4224        let task_id = created.task.metadata.task_id.clone();
4225
4226        // The owner is served normally.
4227        assert!(
4228            router
4229                .handle(
4230                    RequestId::Number(2),
4231                    McpRequest::GetTaskInfo(GetTaskInfoParams {
4232                        task_id: task_id.clone(),
4233                        meta: None,
4234                    }),
4235                    as_principal("alice"),
4236                )
4237                .await
4238                .is_ok()
4239        );
4240
4241        // Knowing the ID is not authority. Every operation is refused for a
4242        // different principal, and for one that dropped its token.
4243        for (id, label, context) in [
4244            (3, "another principal", as_principal("bob")),
4245            (4, "no principal", tasks_client_extensions()),
4246        ] {
4247            for (offset, request) in [
4248                McpRequest::GetTaskInfo(GetTaskInfoParams {
4249                    task_id: task_id.clone(),
4250                    meta: None,
4251                }),
4252                McpRequest::UpdateTask(UpdateTaskParams {
4253                    task_id: task_id.clone(),
4254                    input_responses: HashMap::new(),
4255                    meta: None,
4256                }),
4257                McpRequest::CancelTask(CancelTaskParams {
4258                    task_id: task_id.clone(),
4259                    reason: None,
4260                    meta: None,
4261                }),
4262            ]
4263            .into_iter()
4264            .enumerate()
4265            {
4266                let error = router
4267                    .handle(
4268                        RequestId::Number(id * 10 + offset as i64),
4269                        request,
4270                        context.clone(),
4271                    )
4272                    .await
4273                    .unwrap_err();
4274                assert!(
4275                    matches!(error, Error::JsonRpc(ref e) if e.code == -32602),
4276                    "{label} was served: {error:?}"
4277                );
4278                // The refusal must be indistinguishable from an unknown task,
4279                // or it confirms the ID is real.
4280                let Error::JsonRpc(error) = error else {
4281                    unreachable!()
4282                };
4283                assert!(
4284                    error.message.contains("not found"),
4285                    "refusal leaked that the task exists: {}",
4286                    error.message
4287                );
4288            }
4289        }
4290
4291        // The task survived every refused operation.
4292        assert!(
4293            router
4294                .handle(
4295                    RequestId::Number(9),
4296                    McpRequest::GetTaskInfo(GetTaskInfoParams {
4297                        task_id: task_id.clone(),
4298                        meta: None,
4299                    }),
4300                    as_principal("alice"),
4301                )
4302                .await
4303                .is_ok(),
4304            "a refused cancel must not have cancelled the task"
4305        );
4306    }
4307
4308    #[cfg(all(feature = "oauth", feature = "stateless"))]
4309    #[tokio::test]
4310    async fn final_tasks_work_across_independent_routers_with_a_shared_store() {
4311        fn as_principal(subject: &str) -> Extensions {
4312            let mut extensions = tasks_client_extensions();
4313            extensions.insert(crate::oauth::token::TokenClaims {
4314                sub: Some(subject.to_string()),
4315                iss: None,
4316                aud: None,
4317                exp: None,
4318                scope: None,
4319                client_id: None,
4320                extra: HashMap::new(),
4321            });
4322            extensions
4323        }
4324
4325        fn router_with_store(store: Arc<dyn TaskStore>) -> McpRouter {
4326            McpRouter::new()
4327                .tool(
4328                    ToolBuilder::new("shared_task")
4329                        .task_support(TaskSupportMode::Optional)
4330                        .handler(|_input: serde_json::Value| async move {
4331                            tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
4332                            Ok(CallToolResult::text("done"))
4333                        })
4334                        .build(),
4335                )
4336                .task_store(store)
4337                .with_tasks()
4338        }
4339
4340        let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
4341        let router_a = router_with_store(store.clone());
4342        let router_b = router_with_store(store);
4343
4344        let McpResponse::FinalCreateTask(created) = router_a
4345            .handle(
4346                RequestId::Number(1),
4347                McpRequest::CallTool(CallToolParams {
4348                    name: "shared_task".to_string(),
4349                    arguments: serde_json::json!({}),
4350                    input_responses: None,
4351                    request_state: None,
4352                    meta: None,
4353                    task: None,
4354                }),
4355                as_principal("alice"),
4356            )
4357            .await
4358            .unwrap()
4359        else {
4360            panic!("router A did not create a final task")
4361        };
4362        let task_id = created.task.metadata.task_id;
4363
4364        // A separate router instance can read the shared task for its owner.
4365        assert!(
4366            router_b
4367                .handle(
4368                    RequestId::Number(2),
4369                    McpRequest::GetTaskInfo(GetTaskInfoParams {
4370                        task_id: task_id.clone(),
4371                        meta: None,
4372                    }),
4373                    as_principal("alice"),
4374                )
4375                .await
4376                .is_ok()
4377        );
4378
4379        // Another principal sees the same response as an unknown ID.
4380        let denied = router_b
4381            .handle(
4382                RequestId::Number(3),
4383                McpRequest::GetTaskInfo(GetTaskInfoParams {
4384                    task_id: task_id.clone(),
4385                    meta: None,
4386                }),
4387                as_principal("bob"),
4388            )
4389            .await
4390            .unwrap_err();
4391        let unknown = router_b
4392            .handle(
4393                RequestId::Number(4),
4394                McpRequest::GetTaskInfo(GetTaskInfoParams {
4395                    task_id: "unknown-task".to_string(),
4396                    meta: None,
4397                }),
4398                as_principal("bob"),
4399            )
4400            .await
4401            .unwrap_err();
4402        let (Error::JsonRpc(denied), Error::JsonRpc(unknown)) = (denied, unknown) else {
4403            panic!("expected JSON-RPC task denials")
4404        };
4405        assert_eq!(denied.code, unknown.code);
4406        assert_eq!(
4407            denied.message.replace(&task_id, "<task-id>"),
4408            unknown.message.replace("unknown-task", "<task-id>")
4409        );
4410        assert_eq!(denied.data, unknown.data);
4411
4412        // Router B mutates the shared task, and router A immediately observes
4413        // the terminal state through the same backend.
4414        assert!(matches!(
4415            router_b
4416                .handle(
4417                    RequestId::Number(5),
4418                    McpRequest::CancelTask(CancelTaskParams {
4419                        task_id: task_id.clone(),
4420                        reason: None,
4421                        meta: None,
4422                    }),
4423                    as_principal("alice"),
4424                )
4425                .await
4426                .unwrap(),
4427            McpResponse::FinalTaskAck(_)
4428        ));
4429        let McpResponse::FinalGetTask(fetched) = router_a
4430            .handle(
4431                RequestId::Number(6),
4432                McpRequest::GetTaskInfo(GetTaskInfoParams {
4433                    task_id,
4434                    meta: None,
4435                }),
4436                as_principal("alice"),
4437            )
4438            .await
4439            .unwrap()
4440        else {
4441            panic!("router A did not read the shared task")
4442        };
4443        assert_eq!(fetched.task.status(), TaskStatus::Cancelled);
4444    }
4445
4446    #[test]
4447    fn router_advertises_only_locally_declared_protocol_extensions() {
4448        let router = McpRouter::new().with_protocol_extension(
4449            crate::ExtensionDeclaration::new(
4450                "com.example/rendering",
4451                serde_json::json!({"formats": ["html"]}),
4452            )
4453            .unwrap(),
4454        );
4455
4456        let stable = router.capabilities();
4457        let final_capabilities =
4458            router.capabilities_for_protocol(Some(crate::protocol::PROTOCOL_VERSION_2026_07_28));
4459        for capabilities in [stable, final_capabilities] {
4460            let extensions = capabilities.extensions.unwrap();
4461            assert_eq!(extensions.len(), 1);
4462            assert_eq!(extensions["com.example/rendering"]["formats"][0], "html");
4463            assert!(!extensions.contains_key("com.example/client-only"));
4464        }
4465    }
4466
4467    #[tokio::test]
4468    async fn initialize_persists_negotiated_extensions_for_legacy_contexts() {
4469        let router = McpRouter::new().with_protocol_extension(
4470            crate::ExtensionDeclaration::new(
4471                "com.example/shared",
4472                serde_json::json!({"server": true}),
4473            )
4474            .unwrap(),
4475        );
4476        let client_capabilities = ClientCapabilities {
4477            extensions: Some(HashMap::from([
4478                (
4479                    "com.example/shared".to_string(),
4480                    serde_json::json!({"client": true}),
4481                ),
4482                ("com.example/client-only".to_string(), serde_json::json!({})),
4483            ])),
4484            ..ClientCapabilities::default()
4485        };
4486
4487        router
4488            .handle(
4489                RequestId::Number(1),
4490                McpRequest::Initialize(InitializeParams {
4491                    protocol_version: crate::protocol::LATEST_PROTOCOL_VERSION.to_string(),
4492                    capabilities: client_capabilities,
4493                    client_info: Implementation {
4494                        name: "extension-test".to_string(),
4495                        version: "1.0.0".to_string(),
4496                        title: None,
4497                        description: None,
4498                        icons: None,
4499                        website_url: None,
4500                        meta: None,
4501                    },
4502                    meta: None,
4503                }),
4504                Extensions::new(),
4505            )
4506            .await
4507            .unwrap();
4508
4509        let context = router.create_context(RequestId::Number(2), None);
4510        let negotiated = context.negotiated_extensions().unwrap();
4511        assert!(negotiated.contains("com.example/shared"));
4512        assert!(!negotiated.contains("com.example/client-only"));
4513    }
4514
4515    #[cfg(feature = "stateless")]
4516    #[test]
4517    fn final_request_context_exposes_only_negotiated_extensions() {
4518        let router = McpRouter::new().with_protocol_extension(
4519            crate::ExtensionDeclaration::new(
4520                "com.example/shared",
4521                serde_json::json!({"server": true}),
4522            )
4523            .unwrap(),
4524        );
4525        let per_request = final_extensions(ClientCapabilities {
4526            extensions: Some(HashMap::from([
4527                (
4528                    "com.example/shared".to_string(),
4529                    serde_json::json!({"client": true}),
4530                ),
4531                ("com.example/client-only".to_string(), serde_json::json!({})),
4532            ])),
4533            ..ClientCapabilities::default()
4534        });
4535
4536        let context =
4537            router.create_context_with_extensions(RequestId::Number(1), None, &per_request);
4538        let negotiated = context.negotiated_extensions().unwrap();
4539
4540        assert_eq!(negotiated.len(), 1);
4541        assert_eq!(
4542            negotiated
4543                .get("com.example/shared")
4544                .unwrap()
4545                .client_settings()["client"],
4546            true
4547        );
4548        assert!(!negotiated.contains("com.example/client-only"));
4549    }
4550
4551    #[cfg(feature = "stateless")]
4552    #[tokio::test]
4553    async fn final_protocol_withholds_incomplete_tasks_advertisement() {
4554        let optional = ToolBuilder::new("optional_task")
4555            .task_support(TaskSupportMode::Optional)
4556            .handler(|input: AddInput| async move {
4557                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4558            })
4559            .build();
4560        let required = ToolBuilder::new("required_task")
4561            .task_support(TaskSupportMode::Required)
4562            .handler(|input: AddInput| async move {
4563                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4564            })
4565            .build();
4566        let mut router = McpRouter::new().tool(optional).tool(required);
4567
4568        // Stable clients retain the existing capability surface.
4569        let stable_capabilities = router.capabilities();
4570        assert!(stable_capabilities.tasks.is_some());
4571        assert!(
4572            stable_capabilities
4573                .extensions
4574                .as_ref()
4575                .is_some_and(|extensions| extensions.contains_key(TASKS_EXTENSION_ID))
4576        );
4577
4578        // Final discovery must not claim support for the incomplete extension.
4579        let response = router
4580            .handle(
4581                RequestId::Number(1),
4582                McpRequest::Discover(DiscoverParams::default()),
4583                Extensions::new(),
4584            )
4585            .await
4586            .unwrap();
4587        let McpResponse::Discover(result) = response else {
4588            panic!("Expected Discover response");
4589        };
4590        assert!(result.capabilities.tasks.is_none());
4591        assert!(
4592            result
4593                .capabilities
4594                .extensions
4595                .as_ref()
4596                .is_none_or(|extensions| !extensions.contains_key(TASKS_EXTENSION_ID))
4597        );
4598
4599        init_router(&mut router).await;
4600
4601        // Stable discovery keeps both tools and their execution metadata.
4602        let response = router
4603            .handle(
4604                RequestId::Number(2),
4605                McpRequest::ListTools(ListToolsParams::default()),
4606                Extensions::new(),
4607            )
4608            .await
4609            .unwrap();
4610        let McpResponse::ListTools(result) = response else {
4611            panic!("Expected ListTools response");
4612        };
4613        assert_eq!(result.tools.len(), 2);
4614        assert!(result.tools.iter().all(|tool| tool.execution.is_some()));
4615
4616        // Final discovery keeps the synchronously callable optional tool, but
4617        // strips Tasks metadata and hides the required-task-only tool.
4618        let response = router
4619            .handle(
4620                RequestId::Number(3),
4621                McpRequest::ListTools(ListToolsParams::default()),
4622                final_extensions(ClientCapabilities::default()),
4623            )
4624            .await
4625            .unwrap();
4626        let McpResponse::ListTools(result) = response else {
4627            panic!("Expected ListTools response");
4628        };
4629        assert_eq!(result.tools.len(), 1);
4630        assert_eq!(result.tools[0].name, "optional_task");
4631        assert!(result.tools[0].execution.is_none());
4632    }
4633
4634    #[cfg(feature = "stateless")]
4635    #[tokio::test]
4636    async fn final_protocol_enforces_tasks_negotiation() {
4637        let optional = ToolBuilder::new("optional_task")
4638            .task_support(TaskSupportMode::Optional)
4639            .handler(|input: AddInput| async move {
4640                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4641            })
4642            .build();
4643        let required = ToolBuilder::new("required_task")
4644            .task_support(TaskSupportMode::Required)
4645            .handler(|input: AddInput| async move {
4646                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4647            })
4648            .build();
4649        let mut router = McpRouter::new().tool(optional).tool(required).with_tasks();
4650        init_router(&mut router).await;
4651
4652        // The optional tool remains synchronously callable on the final path.
4653        let response = router
4654            .handle(
4655                RequestId::Number(1),
4656                McpRequest::CallTool(CallToolParams {
4657                    name: "optional_task".to_string(),
4658                    arguments: serde_json::json!({"a": 1, "b": 2}),
4659                    input_responses: None,
4660                    request_state: None,
4661                    meta: None,
4662                    task: None,
4663                }),
4664                final_extensions(ClientCapabilities::default()),
4665            )
4666            .await
4667            .unwrap();
4668        assert!(matches!(response, McpResponse::CallTool(_)));
4669
4670        // The removed legacy task augmentation is invalid on the final wire.
4671        let error = router
4672            .handle(
4673                RequestId::Number(2),
4674                McpRequest::CallTool(CallToolParams {
4675                    name: "optional_task".to_string(),
4676                    arguments: serde_json::json!({"a": 1, "b": 2}),
4677                    input_responses: None,
4678                    request_state: None,
4679                    meta: None,
4680                    task: Some(TaskRequestParams { ttl: None }),
4681                }),
4682                final_extensions(ClientCapabilities::default()),
4683            )
4684            .await
4685            .unwrap_err();
4686        assert!(matches!(error, Error::JsonRpc(error) if error.code == -32602));
4687
4688        // A required-task tool cannot run without a task, so the server names
4689        // the capability the client is missing rather than pretending the tool
4690        // does not exist.
4691        let error = router
4692            .handle(
4693                RequestId::Number(3),
4694                McpRequest::CallTool(CallToolParams {
4695                    name: "required_task".to_string(),
4696                    arguments: serde_json::json!({"a": 1, "b": 2}),
4697                    input_responses: None,
4698                    request_state: None,
4699                    meta: None,
4700                    task: None,
4701                }),
4702                final_extensions(ClientCapabilities::default()),
4703            )
4704            .await
4705            .unwrap_err();
4706        let Error::JsonRpc(error) = error else {
4707            panic!("expected a JSON-RPC error");
4708        };
4709        assert_eq!(error.code, -32021);
4710        assert_eq!(
4711            error.data.as_ref().unwrap()["requiredCapabilities"]["extensions"]["io.modelcontextprotocol/tasks"],
4712            serde_json::json!({}),
4713            "the error must name the extension the client needs to declare"
4714        );
4715
4716        let task_requests = [
4717            McpRequest::GetTaskInfo(GetTaskInfoParams {
4718                task_id: "task-unknown".to_string(),
4719                meta: None,
4720            }),
4721            McpRequest::UpdateTask(UpdateTaskParams {
4722                task_id: "task-unknown".to_string(),
4723                input_responses: HashMap::new(),
4724                meta: None,
4725            }),
4726            McpRequest::CancelTask(CancelTaskParams {
4727                task_id: "task-unknown".to_string(),
4728                reason: None,
4729                meta: None,
4730            }),
4731        ];
4732        for (index, request) in task_requests.into_iter().enumerate() {
4733            let error = router
4734                .handle(
4735                    RequestId::Number(4 + index as i64),
4736                    request,
4737                    final_extensions(ClientCapabilities::default()),
4738                )
4739                .await
4740                .unwrap_err();
4741            let Error::JsonRpc(error) = error else {
4742                panic!("expected a JSON-RPC error");
4743            };
4744            assert_eq!(error.code, -32021);
4745            assert_eq!(
4746                error.data.as_ref().unwrap()["requiredCapabilities"]["extensions"]["io.modelcontextprotocol/tasks"],
4747                serde_json::json!({})
4748            );
4749        }
4750
4751        // If the server itself did not advertise the extension, the method is
4752        // unavailable regardless of what the client declared.
4753        let router_without_tasks = McpRouter::new();
4754        let error = router_without_tasks
4755            .handle(
4756                RequestId::Number(7),
4757                McpRequest::GetTaskInfo(GetTaskInfoParams {
4758                    task_id: "task-unknown".to_string(),
4759                    meta: None,
4760                }),
4761                final_extensions(tasks_client_capabilities()),
4762            )
4763            .await
4764            .unwrap_err();
4765        assert!(matches!(error, Error::JsonRpc(error) if error.code == -32601));
4766    }
4767
4768    #[cfg(feature = "stateless")]
4769    #[test]
4770    fn input_required_capability_validation_uses_capability_semantics() {
4771        let roots = InputRequiredResult::with_requests(
4772            [(
4773                "roots".to_string(),
4774                InputRequest::ListRoots(ListRootsParams::default()),
4775            )]
4776            .into_iter()
4777            .collect(),
4778        );
4779        let extensions = final_extensions(ClientCapabilities {
4780            roots: Some(RootsCapability {
4781                list_changed: true,
4782                deprecated: None,
4783            }),
4784            ..Default::default()
4785        });
4786        validate_input_required_result(&extensions, &roots).unwrap();
4787        assert!(client_capabilities_satisfy(
4788            extensions
4789                .get::<crate::stateless::StatelessRequestMeta>()
4790                .and_then(|meta| meta.client_capabilities.as_ref())
4791                .unwrap(),
4792            &ClientCapabilities {
4793                roots: Some(RootsCapability::default()),
4794                ..Default::default()
4795            }
4796        ));
4797
4798        let sampling_with_tools = InputRequiredResult::with_requests(
4799            [(
4800                "sample".to_string(),
4801                InputRequest::CreateMessage(CreateMessageParams {
4802                    tools: Some(Vec::new()),
4803                    ..CreateMessageParams::new(vec![SamplingMessage::user("hello")], 10)
4804                }),
4805            )]
4806            .into_iter()
4807            .collect(),
4808        );
4809        let extensions = final_extensions(ClientCapabilities {
4810            sampling: Some(SamplingCapability::default()),
4811            ..Default::default()
4812        });
4813        assert!(validate_input_required_result(&extensions, &sampling_with_tools).is_err());
4814
4815        let form = InputRequiredResult::with_requests(
4816            [(
4817                "form".to_string(),
4818                InputRequest::Elicit(ElicitRequestParams::Form(ElicitFormParams {
4819                    mode: Some(ElicitMode::Form),
4820                    message: "name".into(),
4821                    requested_schema: ElicitFormSchema::new(),
4822                    meta: None,
4823                })),
4824            )]
4825            .into_iter()
4826            .collect(),
4827        );
4828        let extensions = final_extensions(ClientCapabilities {
4829            elicitation: Some(ElicitationCapability::default()),
4830            ..Default::default()
4831        });
4832        validate_input_required_result(&extensions, &form).unwrap();
4833    }
4834
4835    /// Helper to initialize a router for testing
4836    async fn init_router(router: &mut McpRouter) {
4837        // Send initialize request
4838        let init_req = RouterRequest {
4839            id: RequestId::Number(0),
4840            inner: McpRequest::Initialize(InitializeParams {
4841                protocol_version: "2025-11-25".to_string(),
4842                capabilities: ClientCapabilities {
4843                    roots: None,
4844                    sampling: None,
4845                    elicitation: None,
4846                    tasks: None,
4847                    experimental: None,
4848                    extensions: None,
4849                },
4850                client_info: Implementation {
4851                    name: "test".to_string(),
4852                    version: "1.0".to_string(),
4853                    ..Default::default()
4854                },
4855                meta: None,
4856            }),
4857            extensions: Extensions::new(),
4858        };
4859        let _ = router.ready().await.unwrap().call(init_req).await.unwrap();
4860        // Send initialized notification
4861        router.handle_notification(McpNotification::Initialized);
4862    }
4863
4864    #[tokio::test]
4865    async fn test_router_list_tools() {
4866        let add_tool = ToolBuilder::new("add")
4867            .description("Add two numbers")
4868            .handler(|input: AddInput| async move {
4869                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4870            })
4871            .build();
4872
4873        let mut router = McpRouter::new().tool(add_tool);
4874
4875        // Initialize session first
4876        init_router(&mut router).await;
4877
4878        let req = RouterRequest {
4879            id: RequestId::Number(1),
4880            inner: McpRequest::ListTools(ListToolsParams::default()),
4881            extensions: Extensions::new(),
4882        };
4883
4884        let resp = router.ready().await.unwrap().call(req).await.unwrap();
4885
4886        match resp.inner {
4887            Ok(McpResponse::ListTools(result)) => {
4888                assert_eq!(result.tools.len(), 1);
4889                assert_eq!(result.tools[0].name, "add");
4890            }
4891            _ => panic!("Expected ListTools response"),
4892        }
4893    }
4894
4895    #[tokio::test]
4896    async fn test_router_call_tool() {
4897        let add_tool = ToolBuilder::new("add")
4898            .description("Add two numbers")
4899            .handler(|input: AddInput| async move {
4900                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4901            })
4902            .build();
4903
4904        let mut router = McpRouter::new().tool(add_tool);
4905
4906        // Initialize session first
4907        init_router(&mut router).await;
4908
4909        let req = RouterRequest {
4910            id: RequestId::Number(1),
4911            inner: McpRequest::CallTool(CallToolParams {
4912                input_responses: None,
4913                request_state: None,
4914                name: "add".to_string(),
4915                arguments: serde_json::json!({"a": 2, "b": 3}),
4916                meta: None,
4917                task: None,
4918            }),
4919            extensions: Extensions::new(),
4920        };
4921
4922        let resp = router.ready().await.unwrap().call(req).await.unwrap();
4923
4924        match resp.inner {
4925            Ok(McpResponse::CallTool(result)) => {
4926                assert!(!result.is_error);
4927                // Check the text content
4928                match &result.content[0] {
4929                    Content::Text { text, .. } => assert_eq!(text, "5"),
4930                    _ => panic!("Expected text content"),
4931                }
4932            }
4933            _ => panic!("Expected CallTool response"),
4934        }
4935    }
4936
4937    /// Helper to initialize a JsonRpcService for testing
4938    async fn init_jsonrpc_service(service: &mut JsonRpcService<McpRouter>, router: &McpRouter) {
4939        let init_req = JsonRpcRequest::new(0, "initialize").with_params(serde_json::json!({
4940            "protocolVersion": "2025-11-25",
4941            "capabilities": {},
4942            "clientInfo": { "name": "test", "version": "1.0" }
4943        }));
4944        let _ = service.call_single(init_req).await.unwrap();
4945        router.handle_notification(McpNotification::Initialized);
4946    }
4947
4948    #[tokio::test]
4949    async fn test_jsonrpc_service() {
4950        let add_tool = ToolBuilder::new("add")
4951            .description("Add two numbers")
4952            .handler(|input: AddInput| async move {
4953                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4954            })
4955            .build();
4956
4957        let router = McpRouter::new().tool(add_tool);
4958        let mut service = JsonRpcService::new(router.clone());
4959
4960        // Initialize session first
4961        init_jsonrpc_service(&mut service, &router).await;
4962
4963        let req = JsonRpcRequest::new(1, "tools/list");
4964
4965        let resp = service.call_single(req).await.unwrap();
4966
4967        match resp {
4968            JsonRpcResponse::Result(r) => {
4969                assert_eq!(r.id, RequestId::Number(1));
4970                let tools = r.result.get("tools").unwrap().as_array().unwrap();
4971                assert_eq!(tools.len(), 1);
4972            }
4973            JsonRpcResponse::Error(_) => panic!("Expected success response"),
4974            _ => panic!("unexpected response variant"),
4975        }
4976    }
4977
4978    #[tokio::test]
4979    async fn test_batch_request() {
4980        let add_tool = ToolBuilder::new("add")
4981            .description("Add two numbers")
4982            .handler(|input: AddInput| async move {
4983                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
4984            })
4985            .build();
4986
4987        let router = McpRouter::new().tool(add_tool);
4988        let mut service = JsonRpcService::new(router.clone())
4989            .protocol_versions(["2025-03-26"])
4990            .unwrap();
4991
4992        // Initialize session first
4993        init_jsonrpc_service(&mut service, &router).await;
4994
4995        // Create a batch of requests
4996        let requests = vec![
4997            JsonRpcRequest::new(1, "tools/list"),
4998            JsonRpcRequest::new(2, "tools/call").with_params(serde_json::json!({
4999                "name": "add",
5000                "arguments": {"a": 10, "b": 20}
5001            })),
5002            JsonRpcRequest::new(3, "ping"),
5003        ];
5004
5005        let responses = service.call_batch(requests).await.unwrap();
5006
5007        assert_eq!(responses.len(), 3);
5008
5009        // Check first response (tools/list)
5010        match &responses[0] {
5011            JsonRpcResponse::Result(r) => {
5012                assert_eq!(r.id, RequestId::Number(1));
5013                let tools = r.result.get("tools").unwrap().as_array().unwrap();
5014                assert_eq!(tools.len(), 1);
5015            }
5016            JsonRpcResponse::Error(_) => panic!("Expected success for tools/list"),
5017            _ => panic!("unexpected response variant"),
5018        }
5019
5020        // Check second response (tools/call)
5021        match &responses[1] {
5022            JsonRpcResponse::Result(r) => {
5023                assert_eq!(r.id, RequestId::Number(2));
5024                let content = r.result.get("content").unwrap().as_array().unwrap();
5025                let text = content[0].get("text").unwrap().as_str().unwrap();
5026                assert_eq!(text, "30");
5027            }
5028            JsonRpcResponse::Error(_) => panic!("Expected success for tools/call"),
5029            _ => panic!("unexpected response variant"),
5030        }
5031
5032        // Check third response (ping)
5033        match &responses[2] {
5034            JsonRpcResponse::Result(r) => {
5035                assert_eq!(r.id, RequestId::Number(3));
5036            }
5037            JsonRpcResponse::Error(_) => panic!("Expected success for ping"),
5038            _ => panic!("unexpected response variant"),
5039        }
5040    }
5041
5042    #[tokio::test]
5043    async fn test_empty_batch_error() {
5044        let router = McpRouter::new();
5045        let mut service = JsonRpcService::new(router);
5046
5047        let result = service.call_batch(vec![]).await;
5048        assert!(result.is_err());
5049    }
5050
5051    // =========================================================================
5052    // Progress Token Tests
5053    // =========================================================================
5054
5055    #[tokio::test]
5056    async fn test_progress_token_extraction() {
5057        use crate::context::{ServerNotification, notification_channel};
5058        use crate::protocol::ProgressToken;
5059        use std::sync::Arc;
5060        use std::sync::atomic::{AtomicBool, Ordering};
5061
5062        // Track whether progress was reported
5063        let progress_reported = Arc::new(AtomicBool::new(false));
5064        let progress_ref = progress_reported.clone();
5065
5066        // Create a tool that reports progress
5067        let tool = ToolBuilder::new("progress_tool")
5068            .description("Tool that reports progress")
5069            .extractor_handler((), move |ctx: Context, Json(_input): Json<AddInput>| {
5070                let reported = progress_ref.clone();
5071                async move {
5072                    // Report progress - this should work if token was extracted
5073                    ctx.report_progress(50.0, Some(100.0), Some("Halfway"))
5074                        .await;
5075                    reported.store(true, Ordering::SeqCst);
5076                    Ok(CallToolResult::text("done"))
5077                }
5078            })
5079            .build();
5080
5081        // Set up notification channel
5082        let (tx, mut rx) = notification_channel(10);
5083        let router = McpRouter::new().with_notification_sender(tx).tool(tool);
5084        let mut service = JsonRpcService::new(router.clone());
5085
5086        // Initialize
5087        init_jsonrpc_service(&mut service, &router).await;
5088
5089        // Call tool WITH progress token in _meta
5090        let req = JsonRpcRequest::new(1, "tools/call").with_params(serde_json::json!({
5091            "name": "progress_tool",
5092            "arguments": {"a": 1, "b": 2},
5093            "_meta": {
5094                "progressToken": "test-token-123"
5095            }
5096        }));
5097
5098        let resp = service.call_single(req).await.unwrap();
5099
5100        // Verify the tool was called successfully
5101        match resp {
5102            JsonRpcResponse::Result(_) => {}
5103            JsonRpcResponse::Error(e) => panic!("Expected success, got error: {:?}", e),
5104            _ => panic!("unexpected response variant"),
5105        }
5106
5107        // Verify progress was reported by handler
5108        assert!(progress_reported.load(Ordering::SeqCst));
5109
5110        // Verify progress notification was sent through channel
5111        let notification = rx.try_recv().expect("Expected progress notification");
5112        match notification {
5113            ServerNotification::Progress(params) => {
5114                assert_eq!(
5115                    params.progress_token,
5116                    ProgressToken::String("test-token-123".to_string())
5117                );
5118                assert_eq!(params.progress, 50.0);
5119                assert_eq!(params.total, Some(100.0));
5120                assert_eq!(params.message.as_deref(), Some("Halfway"));
5121            }
5122            _ => panic!("Expected Progress notification"),
5123        }
5124    }
5125
5126    #[tokio::test]
5127    async fn test_tool_call_without_progress_token() {
5128        use crate::context::notification_channel;
5129        use std::sync::Arc;
5130        use std::sync::atomic::{AtomicBool, Ordering};
5131
5132        let progress_attempted = Arc::new(AtomicBool::new(false));
5133        let progress_ref = progress_attempted.clone();
5134
5135        let tool = ToolBuilder::new("no_token_tool")
5136            .description("Tool that tries to report progress without token")
5137            .extractor_handler((), move |ctx: Context, Json(_input): Json<AddInput>| {
5138                let attempted = progress_ref.clone();
5139                async move {
5140                    // Try to report progress - should be a no-op without token
5141                    ctx.report_progress(50.0, Some(100.0), None).await;
5142                    attempted.store(true, Ordering::SeqCst);
5143                    Ok(CallToolResult::text("done"))
5144                }
5145            })
5146            .build();
5147
5148        let (tx, mut rx) = notification_channel(10);
5149        let router = McpRouter::new().with_notification_sender(tx).tool(tool);
5150        let mut service = JsonRpcService::new(router.clone());
5151
5152        init_jsonrpc_service(&mut service, &router).await;
5153
5154        // Call tool WITHOUT progress token
5155        let req = JsonRpcRequest::new(1, "tools/call").with_params(serde_json::json!({
5156            "name": "no_token_tool",
5157            "arguments": {"a": 1, "b": 2}
5158        }));
5159
5160        let resp = service.call_single(req).await.unwrap();
5161        assert!(matches!(resp, JsonRpcResponse::Result(_)));
5162
5163        // Handler was called
5164        assert!(progress_attempted.load(Ordering::SeqCst));
5165
5166        // But no notification was sent (no progress token)
5167        assert!(rx.try_recv().is_err());
5168    }
5169
5170    #[tokio::test]
5171    async fn test_batch_errors_returned_not_dropped() {
5172        let add_tool = ToolBuilder::new("add")
5173            .description("Add two numbers")
5174            .handler(|input: AddInput| async move {
5175                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
5176            })
5177            .build();
5178
5179        let router = McpRouter::new().tool(add_tool);
5180        let mut service = JsonRpcService::new(router.clone())
5181            .protocol_versions(["2025-03-26"])
5182            .unwrap();
5183
5184        init_jsonrpc_service(&mut service, &router).await;
5185
5186        // Create a batch with one valid and one invalid request
5187        let requests = vec![
5188            // Valid request
5189            JsonRpcRequest::new(1, "tools/call").with_params(serde_json::json!({
5190                "name": "add",
5191                "arguments": {"a": 10, "b": 20}
5192            })),
5193            // Invalid request - tool doesn't exist
5194            JsonRpcRequest::new(2, "tools/call").with_params(serde_json::json!({
5195                "name": "nonexistent_tool",
5196                "arguments": {}
5197            })),
5198            // Another valid request
5199            JsonRpcRequest::new(3, "ping"),
5200        ];
5201
5202        let responses = service.call_batch(requests).await.unwrap();
5203
5204        // All three requests should have responses (errors are not dropped)
5205        assert_eq!(responses.len(), 3);
5206
5207        // First should be success
5208        match &responses[0] {
5209            JsonRpcResponse::Result(r) => {
5210                assert_eq!(r.id, RequestId::Number(1));
5211            }
5212            JsonRpcResponse::Error(_) => panic!("Expected success for first request"),
5213            _ => panic!("unexpected response variant"),
5214        }
5215
5216        // Second should be an error (tool not found)
5217        match &responses[1] {
5218            JsonRpcResponse::Error(e) => {
5219                assert_eq!(e.id, Some(RequestId::Number(2)));
5220                // Error should indicate method not found
5221                assert!(e.error.message.contains("not found") || e.error.code == -32601);
5222            }
5223            JsonRpcResponse::Result(_) => panic!("Expected error for second request"),
5224            _ => panic!("unexpected response variant"),
5225        }
5226
5227        // Third should be success
5228        match &responses[2] {
5229            JsonRpcResponse::Result(r) => {
5230                assert_eq!(r.id, RequestId::Number(3));
5231            }
5232            JsonRpcResponse::Error(_) => panic!("Expected success for third request"),
5233            _ => panic!("unexpected response variant"),
5234        }
5235    }
5236
5237    // =========================================================================
5238    // Resource Template Tests
5239    // =========================================================================
5240
5241    #[tokio::test]
5242    async fn test_list_resource_templates() {
5243        use crate::resource::ResourceTemplateBuilder;
5244        use std::collections::HashMap;
5245
5246        let template = ResourceTemplateBuilder::new("file:///{path}")
5247            .name("Project Files")
5248            .description("Access project files")
5249            .handler(|uri: String, _vars: HashMap<String, String>| async move {
5250                Ok(ReadResourceResult {
5251                    contents: vec![ResourceContent {
5252                        uri,
5253                        mime_type: None,
5254                        text: None,
5255                        blob: None,
5256                        meta: None,
5257                    }],
5258                    meta: None,
5259                    ..Default::default()
5260                })
5261            });
5262
5263        let mut router = McpRouter::new().resource_template(template);
5264
5265        // Initialize session
5266        init_router(&mut router).await;
5267
5268        let req = RouterRequest {
5269            id: RequestId::Number(1),
5270            inner: McpRequest::ListResourceTemplates(ListResourceTemplatesParams::default()),
5271            extensions: Extensions::new(),
5272        };
5273
5274        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5275
5276        match resp.inner {
5277            Ok(McpResponse::ListResourceTemplates(result)) => {
5278                assert_eq!(result.resource_templates.len(), 1);
5279                assert_eq!(result.resource_templates[0].uri_template, "file:///{path}");
5280                assert_eq!(result.resource_templates[0].name, "Project Files");
5281            }
5282            _ => panic!("Expected ListResourceTemplates response"),
5283        }
5284    }
5285
5286    #[tokio::test]
5287    async fn test_read_resource_via_template() {
5288        use crate::resource::ResourceTemplateBuilder;
5289        use std::collections::HashMap;
5290
5291        let template = ResourceTemplateBuilder::new("db://users/{id}")
5292            .name("User Records")
5293            .handler(|uri: String, vars: HashMap<String, String>| async move {
5294                let id = vars.get("id").unwrap().clone();
5295                Ok(ReadResourceResult {
5296                    contents: vec![ResourceContent {
5297                        uri,
5298                        mime_type: Some("application/json".to_string()),
5299                        text: Some(format!(r#"{{"id": "{}"}}"#, id)),
5300                        blob: None,
5301                        meta: None,
5302                    }],
5303                    meta: None,
5304                    ..Default::default()
5305                })
5306            });
5307
5308        let mut router = McpRouter::new().resource_template(template);
5309
5310        // Initialize session
5311        init_router(&mut router).await;
5312
5313        // Read a resource that matches the template
5314        let req = RouterRequest {
5315            id: RequestId::Number(1),
5316            inner: McpRequest::ReadResource(ReadResourceParams {
5317                input_responses: None,
5318                request_state: None,
5319                uri: "db://users/123".to_string(),
5320                meta: None,
5321            }),
5322            extensions: Extensions::new(),
5323        };
5324
5325        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5326
5327        match resp.inner {
5328            Ok(McpResponse::ReadResource(result)) => {
5329                assert_eq!(result.contents.len(), 1);
5330                assert_eq!(result.contents[0].uri, "db://users/123");
5331                assert!(result.contents[0].text.as_ref().unwrap().contains("123"));
5332            }
5333            _ => panic!("Expected ReadResource response"),
5334        }
5335    }
5336
5337    #[tokio::test]
5338    async fn test_static_resource_takes_precedence_over_template() {
5339        use crate::resource::{ResourceBuilder, ResourceTemplateBuilder};
5340        use std::collections::HashMap;
5341
5342        // Template that would match the same URI
5343        let template = ResourceTemplateBuilder::new("file:///{path}")
5344            .name("Files Template")
5345            .handler(|uri: String, _vars: HashMap<String, String>| async move {
5346                Ok(ReadResourceResult {
5347                    contents: vec![ResourceContent {
5348                        uri,
5349                        mime_type: None,
5350                        text: Some("from template".to_string()),
5351                        blob: None,
5352                        meta: None,
5353                    }],
5354                    meta: None,
5355                    ..Default::default()
5356                })
5357            });
5358
5359        // Static resource with exact URI
5360        let static_resource = ResourceBuilder::new("file:///README.md")
5361            .name("README")
5362            .text("from static resource");
5363
5364        let mut router = McpRouter::new()
5365            .resource_template(template)
5366            .resource(static_resource);
5367
5368        // Initialize session
5369        init_router(&mut router).await;
5370
5371        // Read the static resource - should NOT go through template
5372        let req = RouterRequest {
5373            id: RequestId::Number(1),
5374            inner: McpRequest::ReadResource(ReadResourceParams {
5375                input_responses: None,
5376                request_state: None,
5377                uri: "file:///README.md".to_string(),
5378                meta: None,
5379            }),
5380            extensions: Extensions::new(),
5381        };
5382
5383        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5384
5385        match resp.inner {
5386            Ok(McpResponse::ReadResource(result)) => {
5387                // Should get static resource, not template
5388                assert_eq!(
5389                    result.contents[0].text.as_deref(),
5390                    Some("from static resource")
5391                );
5392            }
5393            _ => panic!("Expected ReadResource response"),
5394        }
5395    }
5396
5397    #[tokio::test]
5398    async fn test_resource_not_found_when_no_match() {
5399        use crate::resource::ResourceTemplateBuilder;
5400        use std::collections::HashMap;
5401
5402        let template = ResourceTemplateBuilder::new("db://users/{id}")
5403            .name("Users")
5404            .handler(|uri: String, _vars: HashMap<String, String>| async move {
5405                Ok(ReadResourceResult {
5406                    contents: vec![ResourceContent {
5407                        uri,
5408                        mime_type: None,
5409                        text: None,
5410                        blob: None,
5411                        meta: None,
5412                    }],
5413                    meta: None,
5414                    ..Default::default()
5415                })
5416            });
5417
5418        let mut router = McpRouter::new().resource_template(template);
5419
5420        // Initialize session
5421        init_router(&mut router).await;
5422
5423        // Try to read a URI that doesn't match any resource or template
5424        let req = RouterRequest {
5425            id: RequestId::Number(1),
5426            inner: McpRequest::ReadResource(ReadResourceParams {
5427                input_responses: None,
5428                request_state: None,
5429                uri: "db://posts/123".to_string(),
5430                meta: None,
5431            }),
5432            extensions: Extensions::new(),
5433        };
5434
5435        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5436
5437        match resp.inner {
5438            Err(err) => {
5439                assert!(err.message.contains("not found"));
5440            }
5441            Ok(_) => panic!("Expected error for non-matching URI"),
5442        }
5443    }
5444
5445    #[tokio::test]
5446    async fn test_capabilities_include_resources_with_only_templates() {
5447        use crate::resource::ResourceTemplateBuilder;
5448        use std::collections::HashMap;
5449
5450        let template = ResourceTemplateBuilder::new("file:///{path}")
5451            .name("Files")
5452            .handler(|uri: String, _vars: HashMap<String, String>| async move {
5453                Ok(ReadResourceResult {
5454                    contents: vec![ResourceContent {
5455                        uri,
5456                        mime_type: None,
5457                        text: None,
5458                        blob: None,
5459                        meta: None,
5460                    }],
5461                    meta: None,
5462                    ..Default::default()
5463                })
5464            });
5465
5466        let mut router = McpRouter::new().resource_template(template);
5467
5468        // Send initialize request and check capabilities
5469        let init_req = RouterRequest {
5470            id: RequestId::Number(0),
5471            inner: McpRequest::Initialize(InitializeParams {
5472                protocol_version: "2025-11-25".to_string(),
5473                capabilities: ClientCapabilities {
5474                    roots: None,
5475                    sampling: None,
5476                    elicitation: None,
5477                    tasks: None,
5478                    experimental: None,
5479                    extensions: None,
5480                },
5481                client_info: Implementation {
5482                    name: "test".to_string(),
5483                    version: "1.0".to_string(),
5484                    ..Default::default()
5485                },
5486                meta: None,
5487            }),
5488            extensions: Extensions::new(),
5489        };
5490        let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
5491
5492        match resp.inner {
5493            Ok(McpResponse::Initialize(result)) => {
5494                // Should have resources capability even though only templates registered
5495                assert!(result.capabilities.resources.is_some());
5496            }
5497            _ => panic!("Expected Initialize response"),
5498        }
5499    }
5500
5501    // =========================================================================
5502    // Logging Notification Tests
5503    // =========================================================================
5504
5505    #[tokio::test]
5506    async fn test_log_sends_notification() {
5507        use crate::context::notification_channel;
5508
5509        let (tx, mut rx) = notification_channel(10);
5510        let router = McpRouter::new().with_notification_sender(tx);
5511
5512        // Send an info log
5513        let sent = router.log_info("Test message");
5514        assert!(sent);
5515
5516        // Should receive the notification
5517        let notification = rx.try_recv().unwrap();
5518        match notification {
5519            ServerNotification::LogMessage(params) => {
5520                assert_eq!(params.level, LogLevel::Info);
5521                let data = params.data;
5522                assert_eq!(
5523                    data.get("message").unwrap().as_str().unwrap(),
5524                    "Test message"
5525                );
5526            }
5527            _ => panic!("Expected LogMessage notification"),
5528        }
5529    }
5530
5531    #[tokio::test]
5532    async fn test_log_with_custom_params() {
5533        use crate::context::notification_channel;
5534
5535        let (tx, mut rx) = notification_channel(10);
5536        let router = McpRouter::new().with_notification_sender(tx);
5537
5538        // Send a custom log message
5539        let params = LoggingMessageParams::new(
5540            LogLevel::Error,
5541            serde_json::json!({
5542                "error": "Connection failed",
5543                "host": "localhost"
5544            }),
5545        )
5546        .with_logger("database");
5547
5548        let sent = router.log(params);
5549        assert!(sent);
5550
5551        let notification = rx.try_recv().unwrap();
5552        match notification {
5553            ServerNotification::LogMessage(params) => {
5554                assert_eq!(params.level, LogLevel::Error);
5555                assert_eq!(params.logger.as_deref(), Some("database"));
5556                let data = params.data;
5557                assert_eq!(
5558                    data.get("error").unwrap().as_str().unwrap(),
5559                    "Connection failed"
5560                );
5561            }
5562            _ => panic!("Expected LogMessage notification"),
5563        }
5564    }
5565
5566    #[tokio::test]
5567    async fn test_log_without_channel_returns_false() {
5568        // Router without notification channel
5569        let router = McpRouter::new();
5570
5571        // Should return false when no channel configured
5572        assert!(!router.log_info("Test"));
5573        assert!(!router.log_warning("Test"));
5574        assert!(!router.log_error("Test"));
5575        assert!(!router.log_debug("Test"));
5576    }
5577
5578    #[tokio::test]
5579    async fn test_logging_capability_with_channel() {
5580        use crate::context::notification_channel;
5581
5582        let (tx, _rx) = notification_channel(10);
5583        let mut router = McpRouter::new().with_notification_sender(tx);
5584
5585        // Initialize and check capabilities
5586        let init_req = RouterRequest {
5587            id: RequestId::Number(0),
5588            inner: McpRequest::Initialize(InitializeParams {
5589                protocol_version: "2025-11-25".to_string(),
5590                capabilities: ClientCapabilities {
5591                    roots: None,
5592                    sampling: None,
5593                    elicitation: None,
5594                    tasks: None,
5595                    experimental: None,
5596                    extensions: None,
5597                },
5598                client_info: Implementation {
5599                    name: "test".to_string(),
5600                    version: "1.0".to_string(),
5601                    ..Default::default()
5602                },
5603                meta: None,
5604            }),
5605            extensions: Extensions::new(),
5606        };
5607        let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
5608
5609        match resp.inner {
5610            Ok(McpResponse::Initialize(result)) => {
5611                // Should have logging capability when notification channel is set
5612                assert!(result.capabilities.logging.is_some());
5613            }
5614            _ => panic!("Expected Initialize response"),
5615        }
5616    }
5617
5618    #[tokio::test]
5619    async fn test_no_logging_capability_without_channel() {
5620        let mut router = McpRouter::new();
5621
5622        // Initialize and check capabilities
5623        let init_req = RouterRequest {
5624            id: RequestId::Number(0),
5625            inner: McpRequest::Initialize(InitializeParams {
5626                protocol_version: "2025-11-25".to_string(),
5627                capabilities: ClientCapabilities {
5628                    roots: None,
5629                    sampling: None,
5630                    elicitation: None,
5631                    tasks: None,
5632                    experimental: None,
5633                    extensions: None,
5634                },
5635                client_info: Implementation {
5636                    name: "test".to_string(),
5637                    version: "1.0".to_string(),
5638                    ..Default::default()
5639                },
5640                meta: None,
5641            }),
5642            extensions: Extensions::new(),
5643        };
5644        let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
5645
5646        match resp.inner {
5647            Ok(McpResponse::Initialize(result)) => {
5648                // Should NOT have logging capability without notification channel
5649                assert!(result.capabilities.logging.is_none());
5650            }
5651            _ => panic!("Expected Initialize response"),
5652        }
5653    }
5654
5655    // =========================================================================
5656    // Task Lifecycle Tests
5657    // =========================================================================
5658
5659    #[tokio::test]
5660    async fn test_create_task_via_call_tool() {
5661        let add_tool = ToolBuilder::new("add")
5662            .description("Add two numbers")
5663            .task_support(TaskSupportMode::Optional)
5664            .handler(|input: AddInput| async move {
5665                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
5666            })
5667            .build();
5668
5669        let mut router = McpRouter::new().tool(add_tool);
5670        init_router(&mut router).await;
5671
5672        let req = RouterRequest {
5673            id: RequestId::Number(1),
5674            inner: McpRequest::CallTool(CallToolParams {
5675                input_responses: None,
5676                request_state: None,
5677                name: "add".to_string(),
5678                arguments: serde_json::json!({"a": 5, "b": 10}),
5679                meta: None,
5680                task: Some(TaskRequestParams { ttl: None }),
5681            }),
5682            extensions: Extensions::new(),
5683        };
5684
5685        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5686
5687        match resp.inner {
5688            Ok(McpResponse::CreateTask(result)) => {
5689                assert!(!result.task.task_id.is_empty());
5690                assert_eq!(result.task.status, TaskStatus::Working);
5691            }
5692            _ => panic!("Expected CreateTask response"),
5693        }
5694    }
5695
5696    /// [`TaskStore`] wrapper that counts calls, for proving dispatch goes
5697    /// through an injected store.
5698    struct CountingTaskStore {
5699        inner: MemoryTaskStore,
5700        creates: std::sync::atomic::AtomicUsize,
5701        gets: std::sync::atomic::AtomicUsize,
5702        completes: std::sync::atomic::AtomicUsize,
5703    }
5704
5705    impl CountingTaskStore {
5706        fn new() -> Self {
5707            Self {
5708                inner: MemoryTaskStore::new(),
5709                creates: std::sync::atomic::AtomicUsize::new(0),
5710                gets: std::sync::atomic::AtomicUsize::new(0),
5711                completes: std::sync::atomic::AtomicUsize::new(0),
5712            }
5713        }
5714    }
5715
5716    #[async_trait::async_trait]
5717    impl TaskStore for CountingTaskStore {
5718        async fn create_task(
5719            &self,
5720            tool_name: &str,
5721            arguments: serde_json::Value,
5722            ttl: Option<u64>,
5723            owner: crate::async_task::TaskOwner,
5724        ) -> crate::async_task::Result<(String, crate::async_task::CancellationToken)> {
5725            self.creates
5726                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5727            self.inner
5728                .create_task(tool_name, arguments, ttl, owner)
5729                .await
5730        }
5731
5732        async fn get_task(&self, task_id: &str) -> crate::async_task::Result<Option<TaskObject>> {
5733            self.gets.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5734            self.inner.get_task(task_id).await
5735        }
5736
5737        async fn task_owner(
5738            &self,
5739            task_id: &str,
5740        ) -> crate::async_task::Result<Option<crate::async_task::TaskOwner>> {
5741            self.inner.task_owner(task_id).await
5742        }
5743
5744        async fn get_task_result(
5745            &self,
5746            task_id: &str,
5747        ) -> crate::async_task::Result<Option<crate::async_task::TaskSnapshot>> {
5748            // Counted as a read: `tasks/get` dispatch fetches the snapshot so
5749            // it can inline the SEP-2663 DetailedTask terminal payload.
5750            self.gets.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5751            self.inner.get_task_result(task_id).await
5752        }
5753
5754        async fn wait_for_completion(
5755            &self,
5756            task_id: &str,
5757        ) -> crate::async_task::Result<Option<crate::async_task::TaskSnapshot>> {
5758            self.inner.wait_for_completion(task_id).await
5759        }
5760
5761        async fn list_tasks(
5762            &self,
5763            status_filter: Option<TaskStatus>,
5764        ) -> crate::async_task::Result<Vec<TaskObject>> {
5765            self.inner.list_tasks(status_filter).await
5766        }
5767
5768        async fn require_input(
5769            &self,
5770            task_id: &str,
5771            requests: crate::protocol::InputRequests,
5772            message: Option<&str>,
5773        ) -> crate::async_task::Result<bool> {
5774            self.inner.require_input(task_id, requests, message).await
5775        }
5776
5777        async fn outstanding_input_requests(
5778            &self,
5779            task_id: &str,
5780        ) -> crate::async_task::Result<Option<crate::protocol::InputRequests>> {
5781            self.inner.outstanding_input_requests(task_id).await
5782        }
5783
5784        async fn apply_input_responses(
5785            &self,
5786            task_id: &str,
5787            responses: crate::protocol::InputResponses,
5788        ) -> crate::async_task::Result<Option<crate::async_task::AppliedInputResponses>> {
5789            self.inner.apply_input_responses(task_id, responses).await
5790        }
5791
5792        async fn set_ttl(&self, task_id: &str, ttl_ms: u64) -> crate::async_task::Result<bool> {
5793            self.inner.set_ttl(task_id, ttl_ms).await
5794        }
5795
5796        async fn complete_task(
5797            &self,
5798            task_id: &str,
5799            result: CallToolResult,
5800        ) -> crate::async_task::Result<bool> {
5801            self.completes
5802                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5803            self.inner.complete_task(task_id, result).await
5804        }
5805
5806        async fn fail_task(
5807            &self,
5808            task_id: &str,
5809            error: JsonRpcError,
5810        ) -> crate::async_task::Result<bool> {
5811            self.inner.fail_task(task_id, error).await
5812        }
5813
5814        async fn cancel_task(
5815            &self,
5816            task_id: &str,
5817            reason: Option<&str>,
5818        ) -> crate::async_task::Result<Option<TaskObject>> {
5819            self.inner.cancel_task(task_id, reason).await
5820        }
5821    }
5822
5823    #[tokio::test]
5824    async fn test_injected_task_store_used_by_dispatch() {
5825        let store = Arc::new(CountingTaskStore::new());
5826
5827        let add_tool = ToolBuilder::new("add")
5828            .description("Add two numbers")
5829            .task_support(TaskSupportMode::Optional)
5830            .handler(|input: AddInput| async move {
5831                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
5832            })
5833            .build();
5834
5835        let mut router = McpRouter::new()
5836            .tool(add_tool)
5837            .task_store(store.clone() as Arc<dyn TaskStore>);
5838        init_router(&mut router).await;
5839
5840        // Task-augmented tools/call must create the task in the injected store.
5841        let req = RouterRequest {
5842            id: RequestId::Number(1),
5843            inner: McpRequest::CallTool(CallToolParams {
5844                input_responses: None,
5845                request_state: None,
5846                name: "add".to_string(),
5847                arguments: serde_json::json!({"a": 2, "b": 3}),
5848                meta: None,
5849                task: Some(TaskRequestParams { ttl: None }),
5850            }),
5851            extensions: Extensions::new(),
5852        };
5853        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5854        let task_id = match resp.inner {
5855            Ok(McpResponse::CreateTask(result)) => result.task.task_id,
5856            other => panic!("Expected CreateTask response, got {other:?}"),
5857        };
5858
5859        assert_eq!(
5860            store.creates.load(std::sync::atomic::Ordering::Relaxed),
5861            1,
5862            "create_task must go through the injected store"
5863        );
5864
5865        // Wait for the background execution to record completion.
5866        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
5867        assert_eq!(
5868            store.completes.load(std::sync::atomic::Ordering::Relaxed),
5869            1,
5870            "complete_task must go through the injected store"
5871        );
5872
5873        // tasks/get must read from the injected store.
5874        let gets_before = store.gets.load(std::sync::atomic::Ordering::Relaxed);
5875        let req = RouterRequest {
5876            id: RequestId::Number(2),
5877            inner: McpRequest::GetTaskInfo(GetTaskInfoParams {
5878                task_id: task_id.clone(),
5879                meta: None,
5880            }),
5881            extensions: Extensions::new(),
5882        };
5883        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5884        match resp.inner {
5885            Ok(McpResponse::GetTaskInfo(info)) => {
5886                assert_eq!(info.task_id, task_id);
5887                assert_eq!(info.status, TaskStatus::Completed);
5888            }
5889            other => panic!("Expected GetTaskInfo response, got {other:?}"),
5890        }
5891        assert!(
5892            store.gets.load(std::sync::atomic::Ordering::Relaxed) > gets_before,
5893            "tasks/get must go through the injected store"
5894        );
5895    }
5896
5897    #[tokio::test]
5898    async fn test_removed_tasks_methods_get_method_not_found() {
5899        // Final SEP-2663 removes tasks/list and tasks/result. They no longer
5900        // parse into typed requests, so the router sees Unknown and must
5901        // answer MethodNotFound (-32601).
5902        let mut router = McpRouter::new();
5903        init_router(&mut router).await;
5904
5905        for method in ["tasks/list", "tasks/result"] {
5906            let req = RouterRequest {
5907                id: RequestId::Number(1),
5908                inner: McpRequest::Unknown {
5909                    method: method.to_string(),
5910                    params: None,
5911                },
5912                extensions: Extensions::new(),
5913            };
5914
5915            let resp = router.ready().await.unwrap().call(req).await.unwrap();
5916
5917            match resp.inner {
5918                Err(err) => {
5919                    assert_eq!(err.code, -32601, "{method} must be MethodNotFound");
5920                }
5921                other => panic!("Expected MethodNotFound error for {method}, got {other:?}"),
5922            }
5923        }
5924    }
5925
5926    #[tokio::test]
5927    async fn test_task_lifecycle_complete() {
5928        let add_tool = ToolBuilder::new("add")
5929            .description("Add two numbers")
5930            .task_support(TaskSupportMode::Optional)
5931            .handler(|input: AddInput| async move {
5932                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
5933            })
5934            .build();
5935
5936        let mut router = McpRouter::new().tool(add_tool);
5937        init_router(&mut router).await;
5938
5939        // Create task via tools/call with task params
5940        let req = RouterRequest {
5941            id: RequestId::Number(1),
5942            inner: McpRequest::CallTool(CallToolParams {
5943                input_responses: None,
5944                request_state: None,
5945                name: "add".to_string(),
5946                arguments: serde_json::json!({"a": 7, "b": 8}),
5947                meta: None,
5948                task: Some(TaskRequestParams { ttl: None }),
5949            }),
5950            extensions: Extensions::new(),
5951        };
5952
5953        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5954        let task_id = match resp.inner {
5955            Ok(McpResponse::CreateTask(result)) => result.task.task_id,
5956            _ => panic!("Expected CreateTask response"),
5957        };
5958
5959        // Wait for task to complete
5960        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
5961
5962        // Poll task state via tasks/get (final SEP-2663 removed the blocking
5963        // tasks/result; the terminal result payload on tasks/get is the
5964        // phase 4 DetailedTask work, #951).
5965        let req = RouterRequest {
5966            id: RequestId::Number(2),
5967            inner: McpRequest::GetTaskInfo(GetTaskInfoParams {
5968                task_id: task_id.clone(),
5969                meta: None,
5970            }),
5971            extensions: Extensions::new(),
5972        };
5973
5974        let resp = router.ready().await.unwrap().call(req).await.unwrap();
5975
5976        match resp.inner {
5977            Ok(McpResponse::GetTaskInfo(info)) => {
5978                assert_eq!(info.task_id, task_id);
5979                assert_eq!(info.status, TaskStatus::Completed);
5980            }
5981            _ => panic!("Expected GetTaskInfo response"),
5982        }
5983    }
5984
5985    #[tokio::test]
5986    async fn test_task_cancellation() {
5987        // Use a slow tool to test cancellation
5988        let slow_tool = ToolBuilder::new("slow")
5989            .description("Slow tool")
5990            .task_support(TaskSupportMode::Optional)
5991            .handler(|_input: serde_json::Value| async move {
5992                tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
5993                Ok(CallToolResult::text("done"))
5994            })
5995            .build();
5996
5997        let mut router = McpRouter::new().tool(slow_tool);
5998        init_router(&mut router).await;
5999
6000        // Create task
6001        let req = RouterRequest {
6002            id: RequestId::Number(1),
6003            inner: McpRequest::CallTool(CallToolParams {
6004                input_responses: None,
6005                request_state: None,
6006                name: "slow".to_string(),
6007                arguments: serde_json::json!({}),
6008                meta: None,
6009                task: Some(TaskRequestParams { ttl: None }),
6010            }),
6011            extensions: Extensions::new(),
6012        };
6013
6014        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6015        let task_id = match resp.inner {
6016            Ok(McpResponse::CreateTask(result)) => result.task.task_id,
6017            _ => panic!("Expected CreateTask response"),
6018        };
6019
6020        // Cancel the task
6021        let req = RouterRequest {
6022            id: RequestId::Number(2),
6023            inner: McpRequest::CancelTask(CancelTaskParams {
6024                task_id: task_id.clone(),
6025                reason: Some("Test cancellation".to_string()),
6026                meta: None,
6027            }),
6028            extensions: Extensions::new(),
6029        };
6030
6031        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6032
6033        // SEP-2663 (final): cancel acknowledges with an empty result.
6034        match resp.inner {
6035            Ok(McpResponse::CancelTask(EmptyResult {})) => {}
6036            other => panic!("Expected empty CancelTask ack, got {other:?}"),
6037        }
6038
6039        // Observable status is polled via tasks/get.
6040        let req = RouterRequest {
6041            id: RequestId::Number(3),
6042            inner: McpRequest::GetTaskInfo(GetTaskInfoParams {
6043                task_id: task_id.clone(),
6044                meta: None,
6045            }),
6046            extensions: Extensions::new(),
6047        };
6048        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6049        match resp.inner {
6050            Ok(McpResponse::GetTaskInfo(info)) => {
6051                assert_eq!(info.status, TaskStatus::Cancelled);
6052            }
6053            _ => panic!("Expected GetTaskInfo response"),
6054        }
6055    }
6056
6057    #[tokio::test]
6058    async fn test_get_task_info() {
6059        let add_tool = ToolBuilder::new("add")
6060            .description("Add two numbers")
6061            .task_support(TaskSupportMode::Optional)
6062            .handler(|input: AddInput| async move {
6063                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
6064            })
6065            .build();
6066
6067        let mut router = McpRouter::new().tool(add_tool);
6068        init_router(&mut router).await;
6069
6070        // Create task with TTL
6071        let req = RouterRequest {
6072            id: RequestId::Number(1),
6073            inner: McpRequest::CallTool(CallToolParams {
6074                input_responses: None,
6075                request_state: None,
6076                name: "add".to_string(),
6077                arguments: serde_json::json!({"a": 1, "b": 2}),
6078                meta: None,
6079                task: Some(TaskRequestParams { ttl: Some(600_000) }),
6080            }),
6081            extensions: Extensions::new(),
6082        };
6083
6084        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6085        let task_id = match resp.inner {
6086            Ok(McpResponse::CreateTask(result)) => result.task.task_id,
6087            _ => panic!("Expected CreateTask response"),
6088        };
6089
6090        // Get task info
6091        let req = RouterRequest {
6092            id: RequestId::Number(2),
6093            inner: McpRequest::GetTaskInfo(GetTaskInfoParams {
6094                task_id: task_id.clone(),
6095                meta: None,
6096            }),
6097            extensions: Extensions::new(),
6098        };
6099
6100        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6101
6102        match resp.inner {
6103            Ok(McpResponse::GetTaskInfo(info)) => {
6104                assert_eq!(info.task_id, task_id);
6105                assert!(info.created_at.contains('T')); // ISO 8601
6106                assert_eq!(info.ttl, Some(600_000));
6107            }
6108            _ => panic!("Expected GetTaskInfo response"),
6109        }
6110    }
6111
6112    #[tokio::test]
6113    async fn test_task_forbidden_tool_rejects_task_params() {
6114        let tool = ToolBuilder::new("sync_only")
6115            .description("Sync only tool")
6116            .handler(|_input: serde_json::Value| async move { Ok(CallToolResult::text("ok")) })
6117            .build();
6118
6119        let mut router = McpRouter::new().tool(tool);
6120        init_router(&mut router).await;
6121
6122        // Try to create task on a tool with Forbidden task support
6123        let req = RouterRequest {
6124            id: RequestId::Number(1),
6125            inner: McpRequest::CallTool(CallToolParams {
6126                input_responses: None,
6127                request_state: None,
6128                name: "sync_only".to_string(),
6129                arguments: serde_json::json!({}),
6130                meta: None,
6131                task: Some(TaskRequestParams { ttl: None }),
6132            }),
6133            extensions: Extensions::new(),
6134        };
6135
6136        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6137
6138        match resp.inner {
6139            Err(e) => {
6140                assert!(e.message.contains("does not support async tasks"));
6141            }
6142            _ => panic!("Expected error response"),
6143        }
6144    }
6145
6146    #[tokio::test]
6147    async fn test_get_nonexistent_task() {
6148        let mut router = McpRouter::new();
6149        init_router(&mut router).await;
6150
6151        let req = RouterRequest {
6152            id: RequestId::Number(1),
6153            inner: McpRequest::GetTaskInfo(GetTaskInfoParams {
6154                task_id: "task-999".to_string(),
6155                meta: None,
6156            }),
6157            extensions: Extensions::new(),
6158        };
6159
6160        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6161
6162        match resp.inner {
6163            Err(e) => {
6164                assert!(e.message.contains("not found"));
6165            }
6166            _ => panic!("Expected error response"),
6167        }
6168    }
6169
6170    // =========================================================================
6171    // Resource Subscription Tests
6172    // =========================================================================
6173
6174    #[tokio::test]
6175    async fn test_subscribe_to_resource() {
6176        use crate::resource::ResourceBuilder;
6177
6178        let resource = ResourceBuilder::new("file:///test.txt")
6179            .name("Test File")
6180            .text("Hello");
6181
6182        let mut router = McpRouter::new().resource(resource);
6183        init_router(&mut router).await;
6184
6185        // Subscribe to the resource
6186        let req = RouterRequest {
6187            id: RequestId::Number(1),
6188            inner: McpRequest::SubscribeResource(SubscribeResourceParams {
6189                uri: "file:///test.txt".to_string(),
6190                meta: None,
6191            }),
6192            extensions: Extensions::new(),
6193        };
6194
6195        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6196
6197        match resp.inner {
6198            Ok(McpResponse::SubscribeResource(_)) => {
6199                // Should be subscribed now
6200                assert!(router.is_subscribed("file:///test.txt"));
6201            }
6202            _ => panic!("Expected SubscribeResource response"),
6203        }
6204    }
6205
6206    #[tokio::test]
6207    async fn test_unsubscribe_from_resource() {
6208        use crate::resource::ResourceBuilder;
6209
6210        let resource = ResourceBuilder::new("file:///test.txt")
6211            .name("Test File")
6212            .text("Hello");
6213
6214        let mut router = McpRouter::new().resource(resource);
6215        init_router(&mut router).await;
6216
6217        // Subscribe first
6218        let req = RouterRequest {
6219            id: RequestId::Number(1),
6220            inner: McpRequest::SubscribeResource(SubscribeResourceParams {
6221                uri: "file:///test.txt".to_string(),
6222                meta: None,
6223            }),
6224            extensions: Extensions::new(),
6225        };
6226        let _ = router.ready().await.unwrap().call(req).await.unwrap();
6227        assert!(router.is_subscribed("file:///test.txt"));
6228
6229        // Now unsubscribe
6230        let req = RouterRequest {
6231            id: RequestId::Number(2),
6232            inner: McpRequest::UnsubscribeResource(UnsubscribeResourceParams {
6233                uri: "file:///test.txt".to_string(),
6234                meta: None,
6235            }),
6236            extensions: Extensions::new(),
6237        };
6238
6239        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6240
6241        match resp.inner {
6242            Ok(McpResponse::UnsubscribeResource(_)) => {
6243                // Should no longer be subscribed
6244                assert!(!router.is_subscribed("file:///test.txt"));
6245            }
6246            _ => panic!("Expected UnsubscribeResource response"),
6247        }
6248    }
6249
6250    #[tokio::test]
6251    async fn test_subscribe_nonexistent_resource() {
6252        let mut router = McpRouter::new();
6253        init_router(&mut router).await;
6254
6255        let req = RouterRequest {
6256            id: RequestId::Number(1),
6257            inner: McpRequest::SubscribeResource(SubscribeResourceParams {
6258                uri: "file:///nonexistent.txt".to_string(),
6259                meta: None,
6260            }),
6261            extensions: Extensions::new(),
6262        };
6263
6264        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6265
6266        match resp.inner {
6267            Err(e) => {
6268                assert!(e.message.contains("not found"));
6269            }
6270            _ => panic!("Expected error response"),
6271        }
6272    }
6273
6274    #[tokio::test]
6275    async fn test_notify_resource_updated() {
6276        use crate::context::notification_channel;
6277        use crate::resource::ResourceBuilder;
6278
6279        let (tx, mut rx) = notification_channel(10);
6280
6281        let resource = ResourceBuilder::new("file:///test.txt")
6282            .name("Test File")
6283            .text("Hello");
6284
6285        let router = McpRouter::new()
6286            .resource(resource)
6287            .with_notification_sender(tx);
6288
6289        // First, manually subscribe (simulate subscription)
6290        router.subscribe("file:///test.txt");
6291
6292        // Now notify
6293        let sent = router.notify_resource_updated("file:///test.txt");
6294        assert!(sent);
6295
6296        // Check the notification was sent
6297        let notification = rx.try_recv().unwrap();
6298        match notification {
6299            ServerNotification::ResourceUpdated { uri } => {
6300                assert_eq!(uri, "file:///test.txt");
6301            }
6302            _ => panic!("Expected ResourceUpdated notification"),
6303        }
6304    }
6305
6306    #[tokio::test]
6307    async fn test_notify_resource_updated_not_subscribed() {
6308        use crate::context::notification_channel;
6309        use crate::resource::ResourceBuilder;
6310
6311        let (tx, mut rx) = notification_channel(10);
6312
6313        let resource = ResourceBuilder::new("file:///test.txt")
6314            .name("Test File")
6315            .text("Hello");
6316
6317        let router = McpRouter::new()
6318            .resource(resource)
6319            .with_notification_sender(tx);
6320
6321        // Try to notify without subscribing
6322        let sent = router.notify_resource_updated("file:///test.txt");
6323        assert!(!sent); // Should not send because not subscribed
6324
6325        // Channel should be empty
6326        assert!(rx.try_recv().is_err());
6327    }
6328
6329    #[tokio::test]
6330    async fn test_notify_resources_list_changed() {
6331        use crate::context::notification_channel;
6332
6333        let (tx, mut rx) = notification_channel(10);
6334        let router = McpRouter::new().with_notification_sender(tx);
6335
6336        let sent = router.notify_resources_list_changed();
6337        assert!(sent);
6338
6339        let notification = rx.try_recv().unwrap();
6340        match notification {
6341            ServerNotification::ResourcesListChanged => {}
6342            _ => panic!("Expected ResourcesListChanged notification"),
6343        }
6344    }
6345
6346    #[tokio::test]
6347    async fn test_subscribed_uris() {
6348        use crate::resource::ResourceBuilder;
6349
6350        let resource1 = ResourceBuilder::new("file:///a.txt").name("A").text("A");
6351
6352        let resource2 = ResourceBuilder::new("file:///b.txt").name("B").text("B");
6353
6354        let router = McpRouter::new().resource(resource1).resource(resource2);
6355
6356        // Subscribe to both
6357        router.subscribe("file:///a.txt");
6358        router.subscribe("file:///b.txt");
6359
6360        let uris = router.subscribed_uris();
6361        assert_eq!(uris.len(), 2);
6362        assert!(uris.contains(&"file:///a.txt".to_string()));
6363        assert!(uris.contains(&"file:///b.txt".to_string()));
6364    }
6365
6366    #[tokio::test]
6367    async fn test_subscription_capability_advertised() {
6368        use crate::resource::ResourceBuilder;
6369
6370        let resource = ResourceBuilder::new("file:///test.txt")
6371            .name("Test")
6372            .text("Hello");
6373
6374        let mut router = McpRouter::new().resource(resource);
6375
6376        // Initialize and check capabilities
6377        let init_req = RouterRequest {
6378            id: RequestId::Number(0),
6379            inner: McpRequest::Initialize(InitializeParams {
6380                protocol_version: "2025-11-25".to_string(),
6381                capabilities: ClientCapabilities {
6382                    roots: None,
6383                    sampling: None,
6384                    elicitation: None,
6385                    tasks: None,
6386                    experimental: None,
6387                    extensions: None,
6388                },
6389                client_info: Implementation {
6390                    name: "test".to_string(),
6391                    version: "1.0".to_string(),
6392                    ..Default::default()
6393                },
6394                meta: None,
6395            }),
6396            extensions: Extensions::new(),
6397        };
6398        let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
6399
6400        match resp.inner {
6401            Ok(McpResponse::Initialize(result)) => {
6402                // Should have resources capability with subscribe enabled
6403                let resources_cap = result.capabilities.resources.unwrap();
6404                assert!(resources_cap.subscribe);
6405            }
6406            _ => panic!("Expected Initialize response"),
6407        }
6408    }
6409
6410    #[tokio::test]
6411    async fn test_completion_handler() {
6412        let router = McpRouter::new()
6413            .server_info("test", "1.0")
6414            .completion_handler(|params: CompleteParams| async move {
6415                // Return suggestions based on the argument value
6416                let prefix = &params.argument.value;
6417                let suggestions: Vec<String> = vec!["alpha", "beta", "gamma"]
6418                    .into_iter()
6419                    .filter(|s| s.starts_with(prefix))
6420                    .map(String::from)
6421                    .collect();
6422                Ok(CompleteResult::new(suggestions))
6423            });
6424
6425        // Initialize
6426        let init_req = RouterRequest {
6427            id: RequestId::Number(0),
6428            inner: McpRequest::Initialize(InitializeParams {
6429                protocol_version: "2025-11-25".to_string(),
6430                capabilities: ClientCapabilities::default(),
6431                client_info: Implementation {
6432                    name: "test".to_string(),
6433                    version: "1.0".to_string(),
6434                    ..Default::default()
6435                },
6436                meta: None,
6437            }),
6438            extensions: Extensions::new(),
6439        };
6440        let resp = router
6441            .clone()
6442            .ready()
6443            .await
6444            .unwrap()
6445            .call(init_req)
6446            .await
6447            .unwrap();
6448
6449        // Check that completions capability is advertised
6450        match resp.inner {
6451            Ok(McpResponse::Initialize(result)) => {
6452                assert!(result.capabilities.completions.is_some());
6453            }
6454            _ => panic!("Expected Initialize response"),
6455        }
6456
6457        // Send initialized notification
6458        router.handle_notification(McpNotification::Initialized);
6459
6460        // Test completion request
6461        let complete_req = RouterRequest {
6462            id: RequestId::Number(1),
6463            inner: McpRequest::Complete(CompleteParams {
6464                reference: CompletionReference::prompt("test-prompt"),
6465                argument: CompletionArgument::new("query", "al"),
6466                context: None,
6467                meta: None,
6468            }),
6469            extensions: Extensions::new(),
6470        };
6471        let resp = router
6472            .clone()
6473            .ready()
6474            .await
6475            .unwrap()
6476            .call(complete_req)
6477            .await
6478            .unwrap();
6479
6480        match resp.inner {
6481            Ok(McpResponse::Complete(result)) => {
6482                assert_eq!(result.completion.values, vec!["alpha"]);
6483            }
6484            _ => panic!("Expected Complete response"),
6485        }
6486    }
6487
6488    #[tokio::test]
6489    async fn test_completion_without_handler_returns_empty() {
6490        let router = McpRouter::new().server_info("test", "1.0");
6491
6492        // Initialize
6493        let init_req = RouterRequest {
6494            id: RequestId::Number(0),
6495            inner: McpRequest::Initialize(InitializeParams {
6496                protocol_version: "2025-11-25".to_string(),
6497                capabilities: ClientCapabilities::default(),
6498                client_info: Implementation {
6499                    name: "test".to_string(),
6500                    version: "1.0".to_string(),
6501                    ..Default::default()
6502                },
6503                meta: None,
6504            }),
6505            extensions: Extensions::new(),
6506        };
6507        let resp = router
6508            .clone()
6509            .ready()
6510            .await
6511            .unwrap()
6512            .call(init_req)
6513            .await
6514            .unwrap();
6515
6516        // Check that completions capability is NOT advertised
6517        match resp.inner {
6518            Ok(McpResponse::Initialize(result)) => {
6519                assert!(result.capabilities.completions.is_none());
6520            }
6521            _ => panic!("Expected Initialize response"),
6522        }
6523
6524        // Send initialized notification
6525        router.handle_notification(McpNotification::Initialized);
6526
6527        // Test completion request still works but returns empty
6528        let complete_req = RouterRequest {
6529            id: RequestId::Number(1),
6530            inner: McpRequest::Complete(CompleteParams {
6531                reference: CompletionReference::prompt("test-prompt"),
6532                argument: CompletionArgument::new("query", "al"),
6533                context: None,
6534                meta: None,
6535            }),
6536            extensions: Extensions::new(),
6537        };
6538        let resp = router
6539            .clone()
6540            .ready()
6541            .await
6542            .unwrap()
6543            .call(complete_req)
6544            .await
6545            .unwrap();
6546
6547        match resp.inner {
6548            Ok(McpResponse::Complete(result)) => {
6549                assert!(result.completion.values.is_empty());
6550            }
6551            _ => panic!("Expected Complete response"),
6552        }
6553    }
6554
6555    #[tokio::test]
6556    async fn test_tool_filter_list() {
6557        use crate::filter::CapabilityFilter;
6558        use crate::tool::Tool;
6559
6560        let public_tool = ToolBuilder::new("public")
6561            .description("Public tool")
6562            .handler(|_: AddInput| async move { Ok(CallToolResult::text("public")) })
6563            .build();
6564
6565        let admin_tool = ToolBuilder::new("admin")
6566            .description("Admin tool")
6567            .handler(|_: AddInput| async move { Ok(CallToolResult::text("admin")) })
6568            .build();
6569
6570        let mut router = McpRouter::new()
6571            .tool(public_tool)
6572            .tool(admin_tool)
6573            .tool_filter(CapabilityFilter::new(|_, tool: &Tool| tool.name != "admin"));
6574
6575        // Initialize session
6576        init_router(&mut router).await;
6577
6578        let req = RouterRequest {
6579            id: RequestId::Number(1),
6580            inner: McpRequest::ListTools(ListToolsParams::default()),
6581            extensions: Extensions::new(),
6582        };
6583
6584        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6585
6586        match resp.inner {
6587            Ok(McpResponse::ListTools(result)) => {
6588                // Only public tool should be visible
6589                assert_eq!(result.tools.len(), 1);
6590                assert_eq!(result.tools[0].name, "public");
6591            }
6592            _ => panic!("Expected ListTools response"),
6593        }
6594    }
6595
6596    #[tokio::test]
6597    async fn test_tool_filter_call_denied() {
6598        use crate::filter::CapabilityFilter;
6599        use crate::tool::Tool;
6600
6601        let admin_tool = ToolBuilder::new("admin")
6602            .description("Admin tool")
6603            .handler(|_: AddInput| async move { Ok(CallToolResult::text("admin")) })
6604            .build();
6605
6606        let mut router = McpRouter::new()
6607            .tool(admin_tool)
6608            .tool_filter(CapabilityFilter::new(|_, _: &Tool| false)); // Deny all
6609
6610        // Initialize session
6611        init_router(&mut router).await;
6612
6613        let req = RouterRequest {
6614            id: RequestId::Number(1),
6615            inner: McpRequest::CallTool(CallToolParams {
6616                input_responses: None,
6617                request_state: None,
6618                name: "admin".to_string(),
6619                arguments: serde_json::json!({"a": 1, "b": 2}),
6620                meta: None,
6621                task: None,
6622            }),
6623            extensions: Extensions::new(),
6624        };
6625
6626        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6627
6628        // Should get method not found error (default denial behavior)
6629        match resp.inner {
6630            Err(e) => {
6631                assert_eq!(e.code, -32601); // Method not found
6632            }
6633            _ => panic!("Expected JsonRpc error"),
6634        }
6635    }
6636
6637    #[tokio::test]
6638    async fn test_tool_filter_call_allowed() {
6639        use crate::filter::CapabilityFilter;
6640        use crate::tool::Tool;
6641
6642        let public_tool = ToolBuilder::new("public")
6643            .description("Public tool")
6644            .handler(|input: AddInput| async move {
6645                Ok(CallToolResult::text(format!("{}", input.a + input.b)))
6646            })
6647            .build();
6648
6649        let mut router = McpRouter::new()
6650            .tool(public_tool)
6651            .tool_filter(CapabilityFilter::new(|_, _: &Tool| true)); // Allow all
6652
6653        // Initialize session
6654        init_router(&mut router).await;
6655
6656        let req = RouterRequest {
6657            id: RequestId::Number(1),
6658            inner: McpRequest::CallTool(CallToolParams {
6659                input_responses: None,
6660                request_state: None,
6661                name: "public".to_string(),
6662                arguments: serde_json::json!({"a": 1, "b": 2}),
6663                meta: None,
6664                task: None,
6665            }),
6666            extensions: Extensions::new(),
6667        };
6668
6669        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6670
6671        match resp.inner {
6672            Ok(McpResponse::CallTool(result)) => {
6673                assert!(!result.is_error);
6674            }
6675            _ => panic!("Expected CallTool response"),
6676        }
6677    }
6678
6679    #[tokio::test]
6680    async fn test_tool_filter_custom_denial() {
6681        use crate::filter::{CapabilityFilter, DenialBehavior};
6682        use crate::tool::Tool;
6683
6684        let admin_tool = ToolBuilder::new("admin")
6685            .description("Admin tool")
6686            .handler(|_: AddInput| async move { Ok(CallToolResult::text("admin")) })
6687            .build();
6688
6689        let mut router = McpRouter::new().tool(admin_tool).tool_filter(
6690            CapabilityFilter::new(|_, _: &Tool| false)
6691                .denial_behavior(DenialBehavior::Unauthorized),
6692        );
6693
6694        // Initialize session
6695        init_router(&mut router).await;
6696
6697        let req = RouterRequest {
6698            id: RequestId::Number(1),
6699            inner: McpRequest::CallTool(CallToolParams {
6700                input_responses: None,
6701                request_state: None,
6702                name: "admin".to_string(),
6703                arguments: serde_json::json!({"a": 1, "b": 2}),
6704                meta: None,
6705                task: None,
6706            }),
6707            extensions: Extensions::new(),
6708        };
6709
6710        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6711
6712        // Should get forbidden error
6713        match resp.inner {
6714            Err(e) => {
6715                assert_eq!(e.code, -32007); // Forbidden
6716                assert!(e.message.contains("Unauthorized"));
6717            }
6718            _ => panic!("Expected JsonRpc error"),
6719        }
6720    }
6721
6722    #[tokio::test]
6723    async fn test_resource_filter_list() {
6724        use crate::filter::CapabilityFilter;
6725        use crate::resource::{Resource, ResourceBuilder};
6726
6727        let public_resource = ResourceBuilder::new("file:///public.txt")
6728            .name("Public File")
6729            .text("public content");
6730
6731        let secret_resource = ResourceBuilder::new("file:///secret.txt")
6732            .name("Secret File")
6733            .text("secret content");
6734
6735        let mut router = McpRouter::new()
6736            .resource(public_resource)
6737            .resource(secret_resource)
6738            .resource_filter(CapabilityFilter::new(|_, r: &Resource| {
6739                !r.name.contains("Secret")
6740            }));
6741
6742        // Initialize session
6743        init_router(&mut router).await;
6744
6745        let req = RouterRequest {
6746            id: RequestId::Number(1),
6747            inner: McpRequest::ListResources(ListResourcesParams::default()),
6748            extensions: Extensions::new(),
6749        };
6750
6751        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6752
6753        match resp.inner {
6754            Ok(McpResponse::ListResources(result)) => {
6755                // Should only see public resource
6756                assert_eq!(result.resources.len(), 1);
6757                assert_eq!(result.resources[0].name, "Public File");
6758            }
6759            _ => panic!("Expected ListResources response"),
6760        }
6761    }
6762
6763    #[tokio::test]
6764    async fn test_resource_filter_read_denied() {
6765        use crate::filter::CapabilityFilter;
6766        use crate::resource::{Resource, ResourceBuilder};
6767
6768        let secret_resource = ResourceBuilder::new("file:///secret.txt")
6769            .name("Secret File")
6770            .text("secret content");
6771
6772        let mut router = McpRouter::new()
6773            .resource(secret_resource)
6774            .resource_filter(CapabilityFilter::new(|_, _: &Resource| false)); // Deny all
6775
6776        // Initialize session
6777        init_router(&mut router).await;
6778
6779        let req = RouterRequest {
6780            id: RequestId::Number(1),
6781            inner: McpRequest::ReadResource(ReadResourceParams {
6782                input_responses: None,
6783                request_state: None,
6784                uri: "file:///secret.txt".to_string(),
6785                meta: None,
6786            }),
6787            extensions: Extensions::new(),
6788        };
6789
6790        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6791
6792        // Should get method not found error (default denial behavior)
6793        match resp.inner {
6794            Err(e) => {
6795                assert_eq!(e.code, -32601); // Method not found
6796            }
6797            _ => panic!("Expected JsonRpc error"),
6798        }
6799    }
6800
6801    #[tokio::test]
6802    async fn test_resource_filter_read_allowed() {
6803        use crate::filter::CapabilityFilter;
6804        use crate::resource::{Resource, ResourceBuilder};
6805
6806        let public_resource = ResourceBuilder::new("file:///public.txt")
6807            .name("Public File")
6808            .text("public content");
6809
6810        let mut router = McpRouter::new()
6811            .resource(public_resource)
6812            .resource_filter(CapabilityFilter::new(|_, _: &Resource| true)); // Allow all
6813
6814        // Initialize session
6815        init_router(&mut router).await;
6816
6817        let req = RouterRequest {
6818            id: RequestId::Number(1),
6819            inner: McpRequest::ReadResource(ReadResourceParams {
6820                input_responses: None,
6821                request_state: None,
6822                uri: "file:///public.txt".to_string(),
6823                meta: None,
6824            }),
6825            extensions: Extensions::new(),
6826        };
6827
6828        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6829
6830        match resp.inner {
6831            Ok(McpResponse::ReadResource(result)) => {
6832                assert_eq!(result.contents.len(), 1);
6833                assert_eq!(result.contents[0].text.as_deref(), Some("public content"));
6834            }
6835            _ => panic!("Expected ReadResource response"),
6836        }
6837    }
6838
6839    #[tokio::test]
6840    async fn test_resource_filter_custom_denial() {
6841        use crate::filter::{CapabilityFilter, DenialBehavior};
6842        use crate::resource::{Resource, ResourceBuilder};
6843
6844        let secret_resource = ResourceBuilder::new("file:///secret.txt")
6845            .name("Secret File")
6846            .text("secret content");
6847
6848        let mut router = McpRouter::new().resource(secret_resource).resource_filter(
6849            CapabilityFilter::new(|_, _: &Resource| false)
6850                .denial_behavior(DenialBehavior::Unauthorized),
6851        );
6852
6853        // Initialize session
6854        init_router(&mut router).await;
6855
6856        let req = RouterRequest {
6857            id: RequestId::Number(1),
6858            inner: McpRequest::ReadResource(ReadResourceParams {
6859                input_responses: None,
6860                request_state: None,
6861                uri: "file:///secret.txt".to_string(),
6862                meta: None,
6863            }),
6864            extensions: Extensions::new(),
6865        };
6866
6867        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6868
6869        // Should get forbidden error
6870        match resp.inner {
6871            Err(e) => {
6872                assert_eq!(e.code, -32007); // Forbidden
6873                assert!(e.message.contains("Unauthorized"));
6874            }
6875            _ => panic!("Expected JsonRpc error"),
6876        }
6877    }
6878
6879    #[tokio::test]
6880    async fn test_prompt_filter_list() {
6881        use crate::filter::CapabilityFilter;
6882        use crate::prompt::{Prompt, PromptBuilder};
6883
6884        let public_prompt = PromptBuilder::new("greeting")
6885            .description("A greeting")
6886            .user_message("Hello!");
6887
6888        let admin_prompt = PromptBuilder::new("system_debug")
6889            .description("Admin prompt")
6890            .user_message("Debug");
6891
6892        let mut router = McpRouter::new()
6893            .prompt(public_prompt)
6894            .prompt(admin_prompt)
6895            .prompt_filter(CapabilityFilter::new(|_, p: &Prompt| {
6896                !p.name.contains("system")
6897            }));
6898
6899        // Initialize session
6900        init_router(&mut router).await;
6901
6902        let req = RouterRequest {
6903            id: RequestId::Number(1),
6904            inner: McpRequest::ListPrompts(ListPromptsParams::default()),
6905            extensions: Extensions::new(),
6906        };
6907
6908        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6909
6910        match resp.inner {
6911            Ok(McpResponse::ListPrompts(result)) => {
6912                // Should only see public prompt
6913                assert_eq!(result.prompts.len(), 1);
6914                assert_eq!(result.prompts[0].name, "greeting");
6915            }
6916            _ => panic!("Expected ListPrompts response"),
6917        }
6918    }
6919
6920    #[tokio::test]
6921    async fn test_prompt_filter_get_denied() {
6922        use crate::filter::CapabilityFilter;
6923        use crate::prompt::{Prompt, PromptBuilder};
6924        use std::collections::HashMap;
6925
6926        let admin_prompt = PromptBuilder::new("system_debug")
6927            .description("Admin prompt")
6928            .user_message("Debug");
6929
6930        let mut router = McpRouter::new()
6931            .prompt(admin_prompt)
6932            .prompt_filter(CapabilityFilter::new(|_, _: &Prompt| false)); // Deny all
6933
6934        // Initialize session
6935        init_router(&mut router).await;
6936
6937        let req = RouterRequest {
6938            id: RequestId::Number(1),
6939            inner: McpRequest::GetPrompt(GetPromptParams {
6940                input_responses: None,
6941                request_state: None,
6942                name: "system_debug".to_string(),
6943                arguments: HashMap::new(),
6944                meta: None,
6945            }),
6946            extensions: Extensions::new(),
6947        };
6948
6949        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6950
6951        // Should get method not found error (default denial behavior)
6952        match resp.inner {
6953            Err(e) => {
6954                assert_eq!(e.code, -32601); // Method not found
6955            }
6956            _ => panic!("Expected JsonRpc error"),
6957        }
6958    }
6959
6960    #[tokio::test]
6961    async fn test_prompt_filter_get_allowed() {
6962        use crate::filter::CapabilityFilter;
6963        use crate::prompt::{Prompt, PromptBuilder};
6964        use std::collections::HashMap;
6965
6966        let public_prompt = PromptBuilder::new("greeting")
6967            .description("A greeting")
6968            .user_message("Hello!");
6969
6970        let mut router = McpRouter::new()
6971            .prompt(public_prompt)
6972            .prompt_filter(CapabilityFilter::new(|_, _: &Prompt| true)); // Allow all
6973
6974        // Initialize session
6975        init_router(&mut router).await;
6976
6977        let req = RouterRequest {
6978            id: RequestId::Number(1),
6979            inner: McpRequest::GetPrompt(GetPromptParams {
6980                input_responses: None,
6981                request_state: None,
6982                name: "greeting".to_string(),
6983                arguments: HashMap::new(),
6984                meta: None,
6985            }),
6986            extensions: Extensions::new(),
6987        };
6988
6989        let resp = router.ready().await.unwrap().call(req).await.unwrap();
6990
6991        match resp.inner {
6992            Ok(McpResponse::GetPrompt(result)) => {
6993                assert_eq!(result.messages.len(), 1);
6994            }
6995            _ => panic!("Expected GetPrompt response"),
6996        }
6997    }
6998
6999    #[tokio::test]
7000    async fn test_prompt_filter_custom_denial() {
7001        use crate::filter::{CapabilityFilter, DenialBehavior};
7002        use crate::prompt::{Prompt, PromptBuilder};
7003        use std::collections::HashMap;
7004
7005        let admin_prompt = PromptBuilder::new("system_debug")
7006            .description("Admin prompt")
7007            .user_message("Debug");
7008
7009        let mut router = McpRouter::new().prompt(admin_prompt).prompt_filter(
7010            CapabilityFilter::new(|_, _: &Prompt| false)
7011                .denial_behavior(DenialBehavior::Unauthorized),
7012        );
7013
7014        // Initialize session
7015        init_router(&mut router).await;
7016
7017        let req = RouterRequest {
7018            id: RequestId::Number(1),
7019            inner: McpRequest::GetPrompt(GetPromptParams {
7020                input_responses: None,
7021                request_state: None,
7022                name: "system_debug".to_string(),
7023                arguments: HashMap::new(),
7024                meta: None,
7025            }),
7026            extensions: Extensions::new(),
7027        };
7028
7029        let resp = router.ready().await.unwrap().call(req).await.unwrap();
7030
7031        // Should get forbidden error
7032        match resp.inner {
7033            Err(e) => {
7034                assert_eq!(e.code, -32007); // Forbidden
7035                assert!(e.message.contains("Unauthorized"));
7036            }
7037            _ => panic!("Expected JsonRpc error"),
7038        }
7039    }
7040
7041    // =========================================================================
7042    // Router Composition Tests (merge/nest)
7043    // =========================================================================
7044
7045    #[derive(Debug, Deserialize, JsonSchema)]
7046    struct StringInput {
7047        value: String,
7048    }
7049
7050    #[tokio::test]
7051    async fn test_router_merge_tools() {
7052        // Create first router with a tool
7053        let tool_a = ToolBuilder::new("tool_a")
7054            .description("Tool A")
7055            .handler(|_: StringInput| async move { Ok(CallToolResult::text("A")) })
7056            .build();
7057
7058        let router_a = McpRouter::new().tool(tool_a);
7059
7060        // Create second router with different tools
7061        let tool_b = ToolBuilder::new("tool_b")
7062            .description("Tool B")
7063            .handler(|_: StringInput| async move { Ok(CallToolResult::text("B")) })
7064            .build();
7065        let tool_c = ToolBuilder::new("tool_c")
7066            .description("Tool C")
7067            .handler(|_: StringInput| async move { Ok(CallToolResult::text("C")) })
7068            .build();
7069
7070        let router_b = McpRouter::new().tool(tool_b).tool(tool_c);
7071
7072        // Merge them
7073        let mut merged = McpRouter::new()
7074            .server_info("merged", "1.0")
7075            .merge(router_a)
7076            .merge(router_b);
7077
7078        init_router(&mut merged).await;
7079
7080        // List tools
7081        let req = RouterRequest {
7082            id: RequestId::Number(1),
7083            inner: McpRequest::ListTools(ListToolsParams::default()),
7084            extensions: Extensions::new(),
7085        };
7086
7087        let resp = merged.ready().await.unwrap().call(req).await.unwrap();
7088
7089        match resp.inner {
7090            Ok(McpResponse::ListTools(result)) => {
7091                assert_eq!(result.tools.len(), 3);
7092                let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
7093                assert!(names.contains(&"tool_a"));
7094                assert!(names.contains(&"tool_b"));
7095                assert!(names.contains(&"tool_c"));
7096            }
7097            _ => panic!("Expected ListTools response"),
7098        }
7099    }
7100
7101    #[tokio::test]
7102    async fn test_router_merge_overwrites_duplicates() {
7103        // Create first router with a tool
7104        let tool_v1 = ToolBuilder::new("shared")
7105            .description("Version 1")
7106            .handler(|_: StringInput| async move { Ok(CallToolResult::text("v1")) })
7107            .build();
7108
7109        let router_a = McpRouter::new().tool(tool_v1);
7110
7111        // Create second router with same tool name but different description
7112        let tool_v2 = ToolBuilder::new("shared")
7113            .description("Version 2")
7114            .handler(|_: StringInput| async move { Ok(CallToolResult::text("v2")) })
7115            .build();
7116
7117        let router_b = McpRouter::new().tool(tool_v2);
7118
7119        // Merge - second should win
7120        let mut merged = McpRouter::new().merge(router_a).merge(router_b);
7121
7122        init_router(&mut merged).await;
7123
7124        let req = RouterRequest {
7125            id: RequestId::Number(1),
7126            inner: McpRequest::ListTools(ListToolsParams::default()),
7127            extensions: Extensions::new(),
7128        };
7129
7130        let resp = merged.ready().await.unwrap().call(req).await.unwrap();
7131
7132        match resp.inner {
7133            Ok(McpResponse::ListTools(result)) => {
7134                assert_eq!(result.tools.len(), 1);
7135                assert_eq!(result.tools[0].name, "shared");
7136                assert_eq!(result.tools[0].description.as_deref(), Some("Version 2"));
7137            }
7138            _ => panic!("Expected ListTools response"),
7139        }
7140    }
7141
7142    #[tokio::test]
7143    async fn test_router_merge_resources() {
7144        use crate::resource::ResourceBuilder;
7145
7146        // Create routers with different resources
7147        let router_a = McpRouter::new().resource(
7148            ResourceBuilder::new("file:///a.txt")
7149                .name("File A")
7150                .text("content a"),
7151        );
7152
7153        let router_b = McpRouter::new().resource(
7154            ResourceBuilder::new("file:///b.txt")
7155                .name("File B")
7156                .text("content b"),
7157        );
7158
7159        let mut merged = McpRouter::new().merge(router_a).merge(router_b);
7160
7161        init_router(&mut merged).await;
7162
7163        let req = RouterRequest {
7164            id: RequestId::Number(1),
7165            inner: McpRequest::ListResources(ListResourcesParams::default()),
7166            extensions: Extensions::new(),
7167        };
7168
7169        let resp = merged.ready().await.unwrap().call(req).await.unwrap();
7170
7171        match resp.inner {
7172            Ok(McpResponse::ListResources(result)) => {
7173                assert_eq!(result.resources.len(), 2);
7174                let uris: Vec<&str> = result.resources.iter().map(|r| r.uri.as_str()).collect();
7175                assert!(uris.contains(&"file:///a.txt"));
7176                assert!(uris.contains(&"file:///b.txt"));
7177            }
7178            _ => panic!("Expected ListResources response"),
7179        }
7180    }
7181
7182    #[tokio::test]
7183    async fn test_router_merge_prompts() {
7184        use crate::prompt::PromptBuilder;
7185
7186        let router_a =
7187            McpRouter::new().prompt(PromptBuilder::new("prompt_a").user_message("Hello A"));
7188
7189        let router_b =
7190            McpRouter::new().prompt(PromptBuilder::new("prompt_b").user_message("Hello B"));
7191
7192        let mut merged = McpRouter::new().merge(router_a).merge(router_b);
7193
7194        init_router(&mut merged).await;
7195
7196        let req = RouterRequest {
7197            id: RequestId::Number(1),
7198            inner: McpRequest::ListPrompts(ListPromptsParams::default()),
7199            extensions: Extensions::new(),
7200        };
7201
7202        let resp = merged.ready().await.unwrap().call(req).await.unwrap();
7203
7204        match resp.inner {
7205            Ok(McpResponse::ListPrompts(result)) => {
7206                assert_eq!(result.prompts.len(), 2);
7207                let names: Vec<&str> = result.prompts.iter().map(|p| p.name.as_str()).collect();
7208                assert!(names.contains(&"prompt_a"));
7209                assert!(names.contains(&"prompt_b"));
7210            }
7211            _ => panic!("Expected ListPrompts response"),
7212        }
7213    }
7214
7215    #[tokio::test]
7216    async fn test_router_nest_prefixes_tools() {
7217        // Create a router with tools
7218        let tool_query = ToolBuilder::new("query")
7219            .description("Query the database")
7220            .handler(|_: StringInput| async move { Ok(CallToolResult::text("query result")) })
7221            .build();
7222        let tool_insert = ToolBuilder::new("insert")
7223            .description("Insert into database")
7224            .handler(|_: StringInput| async move { Ok(CallToolResult::text("insert result")) })
7225            .build();
7226
7227        let db_router = McpRouter::new().tool(tool_query).tool(tool_insert);
7228
7229        // Nest under "db" prefix
7230        let mut router = McpRouter::new()
7231            .server_info("nested", "1.0")
7232            .nest("db", db_router);
7233
7234        init_router(&mut router).await;
7235
7236        let req = RouterRequest {
7237            id: RequestId::Number(1),
7238            inner: McpRequest::ListTools(ListToolsParams::default()),
7239            extensions: Extensions::new(),
7240        };
7241
7242        let resp = router.ready().await.unwrap().call(req).await.unwrap();
7243
7244        match resp.inner {
7245            Ok(McpResponse::ListTools(result)) => {
7246                assert_eq!(result.tools.len(), 2);
7247                let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
7248                assert!(names.contains(&"db.query"));
7249                assert!(names.contains(&"db.insert"));
7250            }
7251            _ => panic!("Expected ListTools response"),
7252        }
7253    }
7254
7255    #[tokio::test]
7256    async fn test_router_nest_call_prefixed_tool() {
7257        let tool = ToolBuilder::new("echo")
7258            .description("Echo input")
7259            .handler(|input: StringInput| async move { Ok(CallToolResult::text(&input.value)) })
7260            .build();
7261
7262        let nested_router = McpRouter::new().tool(tool);
7263
7264        let mut router = McpRouter::new().nest("api", nested_router);
7265
7266        init_router(&mut router).await;
7267
7268        // Call the prefixed tool
7269        let req = RouterRequest {
7270            id: RequestId::Number(1),
7271            inner: McpRequest::CallTool(CallToolParams {
7272                input_responses: None,
7273                request_state: None,
7274                name: "api.echo".to_string(),
7275                arguments: serde_json::json!({"value": "hello world"}),
7276                meta: None,
7277                task: None,
7278            }),
7279            extensions: Extensions::new(),
7280        };
7281
7282        let resp = router.ready().await.unwrap().call(req).await.unwrap();
7283
7284        match resp.inner {
7285            Ok(McpResponse::CallTool(result)) => {
7286                assert!(!result.is_error);
7287                match &result.content[0] {
7288                    Content::Text { text, .. } => assert_eq!(text, "hello world"),
7289                    _ => panic!("Expected text content"),
7290                }
7291            }
7292            _ => panic!("Expected CallTool response"),
7293        }
7294    }
7295
7296    #[tokio::test]
7297    async fn test_router_multiple_nests() {
7298        let db_tool = ToolBuilder::new("query")
7299            .description("Database query")
7300            .handler(|_: StringInput| async move { Ok(CallToolResult::text("db")) })
7301            .build();
7302
7303        let api_tool = ToolBuilder::new("fetch")
7304            .description("API fetch")
7305            .handler(|_: StringInput| async move { Ok(CallToolResult::text("api")) })
7306            .build();
7307
7308        let db_router = McpRouter::new().tool(db_tool);
7309        let api_router = McpRouter::new().tool(api_tool);
7310
7311        let mut router = McpRouter::new()
7312            .nest("db", db_router)
7313            .nest("api", api_router);
7314
7315        init_router(&mut router).await;
7316
7317        let req = RouterRequest {
7318            id: RequestId::Number(1),
7319            inner: McpRequest::ListTools(ListToolsParams::default()),
7320            extensions: Extensions::new(),
7321        };
7322
7323        let resp = router.ready().await.unwrap().call(req).await.unwrap();
7324
7325        match resp.inner {
7326            Ok(McpResponse::ListTools(result)) => {
7327                assert_eq!(result.tools.len(), 2);
7328                let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
7329                assert!(names.contains(&"db.query"));
7330                assert!(names.contains(&"api.fetch"));
7331            }
7332            _ => panic!("Expected ListTools response"),
7333        }
7334    }
7335
7336    #[tokio::test]
7337    async fn test_router_merge_and_nest_combined() {
7338        // Test combining merge and nest
7339        let tool_a = ToolBuilder::new("local")
7340            .description("Local tool")
7341            .handler(|_: StringInput| async move { Ok(CallToolResult::text("local")) })
7342            .build();
7343
7344        let nested_tool = ToolBuilder::new("remote")
7345            .description("Remote tool")
7346            .handler(|_: StringInput| async move { Ok(CallToolResult::text("remote")) })
7347            .build();
7348
7349        let nested_router = McpRouter::new().tool(nested_tool);
7350
7351        let mut router = McpRouter::new()
7352            .tool(tool_a)
7353            .nest("external", nested_router);
7354
7355        init_router(&mut router).await;
7356
7357        let req = RouterRequest {
7358            id: RequestId::Number(1),
7359            inner: McpRequest::ListTools(ListToolsParams::default()),
7360            extensions: Extensions::new(),
7361        };
7362
7363        let resp = router.ready().await.unwrap().call(req).await.unwrap();
7364
7365        match resp.inner {
7366            Ok(McpResponse::ListTools(result)) => {
7367                assert_eq!(result.tools.len(), 2);
7368                let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
7369                assert!(names.contains(&"local"));
7370                assert!(names.contains(&"external.remote"));
7371            }
7372            _ => panic!("Expected ListTools response"),
7373        }
7374    }
7375
7376    #[tokio::test]
7377    async fn test_router_merge_preserves_server_info() {
7378        let child_router = McpRouter::new()
7379            .server_info("child", "2.0")
7380            .instructions("Child instructions");
7381
7382        let mut router = McpRouter::new()
7383            .server_info("parent", "1.0")
7384            .instructions("Parent instructions")
7385            .merge(child_router);
7386
7387        init_router(&mut router).await;
7388
7389        // Initialize response should have parent's server info
7390        let init_req = RouterRequest {
7391            id: RequestId::Number(99),
7392            inner: McpRequest::Initialize(InitializeParams {
7393                protocol_version: "2025-11-25".to_string(),
7394                capabilities: ClientCapabilities::default(),
7395                client_info: Implementation {
7396                    name: "test".to_string(),
7397                    version: "1.0".to_string(),
7398                    ..Default::default()
7399                },
7400                meta: None,
7401            }),
7402            extensions: Extensions::new(),
7403        };
7404
7405        // Create fresh router for this test since we need to call initialize
7406        let child_router2 = McpRouter::new().server_info("child", "2.0");
7407        let mut fresh_router = McpRouter::new()
7408            .server_info("parent", "1.0")
7409            .merge(child_router2);
7410
7411        let resp = fresh_router
7412            .ready()
7413            .await
7414            .unwrap()
7415            .call(init_req)
7416            .await
7417            .unwrap();
7418
7419        match resp.inner {
7420            Ok(McpResponse::Initialize(result)) => {
7421                assert_eq!(result.server_info.name, "parent");
7422                assert_eq!(result.server_info.version, "1.0");
7423            }
7424            _ => panic!("Expected Initialize response"),
7425        }
7426    }
7427
7428    // =========================================================================
7429    // Auto-instructions tests
7430    // =========================================================================
7431
7432    #[tokio::test]
7433    async fn test_auto_instructions_tools_only() {
7434        let tool_a = ToolBuilder::new("alpha")
7435            .description("Alpha tool")
7436            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7437            .build();
7438        let tool_b = ToolBuilder::new("beta")
7439            .description("Beta tool")
7440            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7441            .build();
7442
7443        let mut router = McpRouter::new()
7444            .auto_instructions()
7445            .tool(tool_a)
7446            .tool(tool_b);
7447
7448        let resp = send_initialize(&mut router).await;
7449        let instructions = resp.instructions.expect("should have instructions");
7450
7451        assert!(instructions.contains("## Tools"));
7452        assert!(instructions.contains("- **alpha**: Alpha tool"));
7453        assert!(instructions.contains("- **beta**: Beta tool"));
7454        // No resources or prompts sections
7455        assert!(!instructions.contains("## Resources"));
7456        assert!(!instructions.contains("## Prompts"));
7457    }
7458
7459    #[tokio::test]
7460    async fn test_auto_instructions_with_annotations() {
7461        let read_only_tool = ToolBuilder::new("query")
7462            .description("Run a query")
7463            .read_only()
7464            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7465            .build();
7466        let destructive_tool = ToolBuilder::new("delete")
7467            .description("Delete a record")
7468            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7469            .build();
7470        let idempotent_tool = ToolBuilder::new("upsert")
7471            .description("Upsert a record")
7472            .non_destructive()
7473            .idempotent()
7474            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7475            .build();
7476
7477        let mut router = McpRouter::new()
7478            .auto_instructions()
7479            .tool(read_only_tool)
7480            .tool(destructive_tool)
7481            .tool(idempotent_tool);
7482
7483        let resp = send_initialize(&mut router).await;
7484        let instructions = resp.instructions.unwrap();
7485
7486        assert!(instructions.contains("- **query**: Run a query [read-only]"));
7487        // delete has no annotations set via builder, so no tags
7488        assert!(instructions.contains("- **delete**: Delete a record\n"));
7489        assert!(instructions.contains("- **upsert**: Upsert a record [idempotent]"));
7490    }
7491
7492    #[tokio::test]
7493    async fn test_auto_instructions_with_resources() {
7494        use crate::resource::ResourceBuilder;
7495
7496        let resource = ResourceBuilder::new("file:///schema.sql")
7497            .name("Schema")
7498            .description("Database schema")
7499            .text("CREATE TABLE ...");
7500
7501        let mut router = McpRouter::new().auto_instructions().resource(resource);
7502
7503        let resp = send_initialize(&mut router).await;
7504        let instructions = resp.instructions.unwrap();
7505
7506        assert!(instructions.contains("## Resources"));
7507        assert!(instructions.contains("- **file:///schema.sql**: Database schema"));
7508        assert!(!instructions.contains("## Tools"));
7509    }
7510
7511    #[tokio::test]
7512    async fn test_auto_instructions_with_resource_templates() {
7513        use crate::resource::ResourceTemplateBuilder;
7514
7515        let template = ResourceTemplateBuilder::new("file:///{path}")
7516            .name("File")
7517            .description("Read a file by path")
7518            .handler(
7519                |_uri: String, _vars: std::collections::HashMap<String, String>| async move {
7520                    Ok(crate::ReadResourceResult::text("content", "text/plain"))
7521                },
7522            );
7523
7524        let mut router = McpRouter::new()
7525            .auto_instructions()
7526            .resource_template(template);
7527
7528        let resp = send_initialize(&mut router).await;
7529        let instructions = resp.instructions.unwrap();
7530
7531        assert!(instructions.contains("## Resources"));
7532        assert!(instructions.contains("- **file:///{path}**: Read a file by path"));
7533    }
7534
7535    #[tokio::test]
7536    async fn test_auto_instructions_with_prompts() {
7537        use crate::prompt::PromptBuilder;
7538
7539        let prompt = PromptBuilder::new("write_query")
7540            .description("Help write a SQL query")
7541            .user_message("Write a query for: {task}");
7542
7543        let mut router = McpRouter::new().auto_instructions().prompt(prompt);
7544
7545        let resp = send_initialize(&mut router).await;
7546        let instructions = resp.instructions.unwrap();
7547
7548        assert!(instructions.contains("## Prompts"));
7549        assert!(instructions.contains("- **write_query**: Help write a SQL query"));
7550        assert!(!instructions.contains("## Tools"));
7551    }
7552
7553    #[tokio::test]
7554    async fn test_auto_instructions_all_sections() {
7555        use crate::prompt::PromptBuilder;
7556        use crate::resource::ResourceBuilder;
7557
7558        let tool = ToolBuilder::new("query")
7559            .description("Execute SQL")
7560            .read_only()
7561            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7562            .build();
7563        let resource = ResourceBuilder::new("db://schema")
7564            .name("Schema")
7565            .description("Full database schema")
7566            .text("schema");
7567        let prompt = PromptBuilder::new("write_query")
7568            .description("Help write a SQL query")
7569            .user_message("Write a query");
7570
7571        let mut router = McpRouter::new()
7572            .auto_instructions()
7573            .tool(tool)
7574            .resource(resource)
7575            .prompt(prompt);
7576
7577        let resp = send_initialize(&mut router).await;
7578        let instructions = resp.instructions.unwrap();
7579
7580        // All three sections present
7581        assert!(instructions.contains("## Tools"));
7582        assert!(instructions.contains("## Resources"));
7583        assert!(instructions.contains("## Prompts"));
7584
7585        // Sections appear in order: Tools, Resources, Prompts
7586        let tools_pos = instructions.find("## Tools").unwrap();
7587        let resources_pos = instructions.find("## Resources").unwrap();
7588        let prompts_pos = instructions.find("## Prompts").unwrap();
7589        assert!(tools_pos < resources_pos);
7590        assert!(resources_pos < prompts_pos);
7591    }
7592
7593    #[tokio::test]
7594    async fn test_auto_instructions_with_prefix_and_suffix() {
7595        let tool = ToolBuilder::new("echo")
7596            .description("Echo input")
7597            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7598            .build();
7599
7600        let mut router = McpRouter::new()
7601            .auto_instructions_with(
7602                Some("This server provides echo capabilities."),
7603                Some("Contact admin@example.com for support."),
7604            )
7605            .tool(tool);
7606
7607        let resp = send_initialize(&mut router).await;
7608        let instructions = resp.instructions.unwrap();
7609
7610        assert!(instructions.starts_with("This server provides echo capabilities."));
7611        assert!(instructions.ends_with("Contact admin@example.com for support."));
7612        assert!(instructions.contains("## Tools"));
7613        assert!(instructions.contains("- **echo**: Echo input"));
7614    }
7615
7616    #[tokio::test]
7617    async fn test_auto_instructions_prefix_only() {
7618        let tool = ToolBuilder::new("echo")
7619            .description("Echo input")
7620            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7621            .build();
7622
7623        let mut router = McpRouter::new()
7624            .auto_instructions_with(Some("My server intro."), None::<String>)
7625            .tool(tool);
7626
7627        let resp = send_initialize(&mut router).await;
7628        let instructions = resp.instructions.unwrap();
7629
7630        assert!(instructions.starts_with("My server intro."));
7631        assert!(instructions.contains("- **echo**: Echo input"));
7632    }
7633
7634    #[tokio::test]
7635    async fn test_auto_instructions_empty_router() {
7636        let mut router = McpRouter::new().auto_instructions();
7637
7638        let resp = send_initialize(&mut router).await;
7639        let instructions = resp.instructions.expect("should have instructions");
7640
7641        // No sections when nothing is registered
7642        assert!(!instructions.contains("## Tools"));
7643        assert!(!instructions.contains("## Resources"));
7644        assert!(!instructions.contains("## Prompts"));
7645        assert!(instructions.is_empty());
7646    }
7647
7648    #[tokio::test]
7649    async fn test_auto_instructions_overrides_manual() {
7650        let tool = ToolBuilder::new("echo")
7651            .description("Echo input")
7652            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7653            .build();
7654
7655        let mut router = McpRouter::new()
7656            .instructions("This will be overridden")
7657            .auto_instructions()
7658            .tool(tool);
7659
7660        let resp = send_initialize(&mut router).await;
7661        let instructions = resp.instructions.unwrap();
7662
7663        assert!(!instructions.contains("This will be overridden"));
7664        assert!(instructions.contains("- **echo**: Echo input"));
7665    }
7666
7667    #[tokio::test]
7668    async fn test_no_auto_instructions_returns_manual() {
7669        let tool = ToolBuilder::new("echo")
7670            .description("Echo input")
7671            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7672            .build();
7673
7674        let mut router = McpRouter::new()
7675            .instructions("Manual instructions here")
7676            .tool(tool);
7677
7678        let resp = send_initialize(&mut router).await;
7679        let instructions = resp.instructions.unwrap();
7680
7681        assert_eq!(instructions, "Manual instructions here");
7682    }
7683
7684    #[tokio::test]
7685    async fn test_auto_instructions_no_description_fallback() {
7686        let tool = ToolBuilder::new("mystery")
7687            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7688            .build();
7689
7690        let mut router = McpRouter::new().auto_instructions().tool(tool);
7691
7692        let resp = send_initialize(&mut router).await;
7693        let instructions = resp.instructions.unwrap();
7694
7695        assert!(instructions.contains("- **mystery**: No description"));
7696    }
7697
7698    #[tokio::test]
7699    async fn test_auto_instructions_sorted_alphabetically() {
7700        let tool_z = ToolBuilder::new("zebra")
7701            .description("Z tool")
7702            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7703            .build();
7704        let tool_a = ToolBuilder::new("alpha")
7705            .description("A tool")
7706            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7707            .build();
7708        let tool_m = ToolBuilder::new("middle")
7709            .description("M tool")
7710            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7711            .build();
7712
7713        let mut router = McpRouter::new()
7714            .auto_instructions()
7715            .tool(tool_z)
7716            .tool(tool_a)
7717            .tool(tool_m);
7718
7719        let resp = send_initialize(&mut router).await;
7720        let instructions = resp.instructions.unwrap();
7721
7722        let alpha_pos = instructions.find("**alpha**").unwrap();
7723        let middle_pos = instructions.find("**middle**").unwrap();
7724        let zebra_pos = instructions.find("**zebra**").unwrap();
7725        assert!(alpha_pos < middle_pos);
7726        assert!(middle_pos < zebra_pos);
7727    }
7728
7729    #[tokio::test]
7730    async fn test_auto_instructions_read_only_and_idempotent_tags() {
7731        let tool = ToolBuilder::new("safe_update")
7732            .description("Safe update operation")
7733            .idempotent()
7734            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7735            .build();
7736
7737        let mut router = McpRouter::new().auto_instructions().tool(tool);
7738
7739        let resp = send_initialize(&mut router).await;
7740        let instructions = resp.instructions.unwrap();
7741
7742        assert!(
7743            instructions.contains("[idempotent]"),
7744            "got: {}",
7745            instructions
7746        );
7747    }
7748
7749    #[tokio::test]
7750    async fn test_auto_instructions_lazy_generation() {
7751        // auto_instructions() is called BEFORE tools are registered
7752        // but instructions should still include tools
7753        let mut router = McpRouter::new().auto_instructions();
7754
7755        let tool = ToolBuilder::new("late_tool")
7756            .description("Added after auto_instructions")
7757            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7758            .build();
7759
7760        router = router.tool(tool);
7761
7762        let resp = send_initialize(&mut router).await;
7763        let instructions = resp.instructions.unwrap();
7764
7765        assert!(instructions.contains("- **late_tool**: Added after auto_instructions"));
7766    }
7767
7768    #[tokio::test]
7769    async fn test_auto_instructions_multiple_annotation_tags() {
7770        let tool = ToolBuilder::new("update")
7771            .description("Update a record")
7772            .annotations(ToolAnnotations {
7773                read_only_hint: true,
7774                idempotent_hint: true,
7775                ..Default::default()
7776            })
7777            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7778            .build();
7779
7780        let mut router = McpRouter::new().auto_instructions().tool(tool);
7781
7782        let resp = send_initialize(&mut router).await;
7783        let instructions = resp.instructions.unwrap();
7784
7785        assert!(
7786            instructions.contains("[read-only, idempotent]"),
7787            "got: {}",
7788            instructions
7789        );
7790    }
7791
7792    #[tokio::test]
7793    async fn test_auto_instructions_no_annotations_no_tags() {
7794        // Tools without annotations should have no tags at all
7795        let tool = ToolBuilder::new("fetch")
7796            .description("Fetch data")
7797            .handler(|_: AddInput| async move { Ok(CallToolResult::text("ok")) })
7798            .build();
7799
7800        let mut router = McpRouter::new().auto_instructions().tool(tool);
7801
7802        let resp = send_initialize(&mut router).await;
7803        let instructions = resp.instructions.unwrap();
7804
7805        // No bracket tags
7806        assert!(
7807            !instructions.contains('['),
7808            "should have no tags, got: {}",
7809            instructions
7810        );
7811        assert!(instructions.contains("- **fetch**: Fetch data"));
7812    }
7813
7814    /// Helper to send an Initialize request and return the result
7815    async fn send_initialize(router: &mut McpRouter) -> InitializeResult {
7816        let init_req = RouterRequest {
7817            id: RequestId::Number(0),
7818            inner: McpRequest::Initialize(InitializeParams {
7819                protocol_version: "2025-11-25".to_string(),
7820                capabilities: ClientCapabilities {
7821                    roots: None,
7822                    sampling: None,
7823                    elicitation: None,
7824                    tasks: None,
7825                    experimental: None,
7826                    extensions: None,
7827                },
7828                client_info: Implementation {
7829                    name: "test".to_string(),
7830                    version: "1.0".to_string(),
7831                    ..Default::default()
7832                },
7833                meta: None,
7834            }),
7835            extensions: Extensions::new(),
7836        };
7837        let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
7838        match resp.inner {
7839            Ok(McpResponse::Initialize(result)) => result,
7840            other => panic!("Expected Initialize response, got {:?}", other),
7841        }
7842    }
7843
7844    #[tokio::test]
7845    async fn test_notify_tools_list_changed() {
7846        let (tx, mut rx) = crate::context::notification_channel(16);
7847
7848        let router = McpRouter::new()
7849            .server_info("test", "1.0")
7850            .with_notification_sender(tx);
7851
7852        assert!(router.notify_tools_list_changed());
7853
7854        let notification = rx.recv().await.unwrap();
7855        assert!(matches!(notification, ServerNotification::ToolsListChanged));
7856    }
7857
7858    #[tokio::test]
7859    async fn test_notify_prompts_list_changed() {
7860        let (tx, mut rx) = crate::context::notification_channel(16);
7861
7862        let router = McpRouter::new()
7863            .server_info("test", "1.0")
7864            .with_notification_sender(tx);
7865
7866        assert!(router.notify_prompts_list_changed());
7867
7868        let notification = rx.recv().await.unwrap();
7869        assert!(matches!(
7870            notification,
7871            ServerNotification::PromptsListChanged
7872        ));
7873    }
7874
7875    #[tokio::test]
7876    async fn test_notify_without_sender_returns_false() {
7877        let router = McpRouter::new().server_info("test", "1.0");
7878
7879        assert!(!router.notify_tools_list_changed());
7880        assert!(!router.notify_prompts_list_changed());
7881        assert!(!router.notify_resources_list_changed());
7882    }
7883
7884    #[tokio::test]
7885    async fn test_list_changed_capabilities_with_notification_sender() {
7886        let (tx, _rx) = crate::context::notification_channel(16);
7887        let tool = ToolBuilder::new("test")
7888            .description("test")
7889            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
7890            .build();
7891
7892        let mut router = McpRouter::new()
7893            .server_info("test", "1.0")
7894            .tool(tool)
7895            .with_notification_sender(tx);
7896
7897        init_router(&mut router).await;
7898
7899        let caps = router.capabilities();
7900        let tools_cap = caps.tools.expect("tools capability should be present");
7901        assert!(
7902            tools_cap.list_changed,
7903            "tools.listChanged should be true when notification sender is configured"
7904        );
7905    }
7906
7907    #[tokio::test]
7908    async fn test_list_changed_capabilities_without_notification_sender() {
7909        let tool = ToolBuilder::new("test")
7910            .description("test")
7911            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
7912            .build();
7913
7914        let mut router = McpRouter::new().server_info("test", "1.0").tool(tool);
7915
7916        init_router(&mut router).await;
7917
7918        let caps = router.capabilities();
7919        let tools_cap = caps.tools.expect("tools capability should be present");
7920        assert!(
7921            !tools_cap.list_changed,
7922            "tools.listChanged should be false without notification sender"
7923        );
7924    }
7925
7926    #[tokio::test]
7927    async fn test_set_logging_level_filters_messages() {
7928        let (tx, mut rx) = crate::context::notification_channel(16);
7929
7930        let mut router = McpRouter::new()
7931            .server_info("test", "1.0")
7932            .with_notification_sender(tx);
7933
7934        init_router(&mut router).await;
7935
7936        // Set logging level to Warning
7937        let set_level_req = RouterRequest {
7938            id: RequestId::Number(99),
7939            inner: McpRequest::SetLoggingLevel(SetLogLevelParams {
7940                level: LogLevel::Warning,
7941                meta: None,
7942            }),
7943            extensions: crate::context::Extensions::new(),
7944        };
7945        let resp = router
7946            .ready()
7947            .await
7948            .unwrap()
7949            .call(set_level_req)
7950            .await
7951            .unwrap();
7952        assert!(matches!(resp.inner, Ok(McpResponse::SetLoggingLevel(_))));
7953
7954        // Create a context from the router (simulating a handler)
7955        let ctx = router.create_context(RequestId::Number(100), None);
7956
7957        // Error (more severe than Warning) should pass through
7958        ctx.send_log(LoggingMessageParams::new(
7959            LogLevel::Error,
7960            serde_json::Value::Null,
7961        ));
7962        assert!(
7963            rx.try_recv().is_ok(),
7964            "Error should pass through Warning filter"
7965        );
7966
7967        // Info (less severe than Warning) should be filtered
7968        ctx.send_log(LoggingMessageParams::new(
7969            LogLevel::Info,
7970            serde_json::Value::Null,
7971        ));
7972        assert!(
7973            rx.try_recv().is_err(),
7974            "Info should be filtered at Warning level"
7975        );
7976    }
7977
7978    #[test]
7979    fn test_paginate_no_page_size() {
7980        let items = vec![1, 2, 3, 4, 5];
7981        let (page, cursor) = paginate(items.clone(), None, None).unwrap();
7982        assert_eq!(page, items);
7983        assert!(cursor.is_none());
7984    }
7985
7986    #[test]
7987    fn test_paginate_first_page() {
7988        let items = vec![1, 2, 3, 4, 5];
7989        let (page, cursor) = paginate(items, None, Some(2)).unwrap();
7990        assert_eq!(page, vec![1, 2]);
7991        assert!(cursor.is_some());
7992    }
7993
7994    #[test]
7995    fn test_paginate_middle_page() {
7996        let items = vec![1, 2, 3, 4, 5];
7997        let (page1, cursor1) = paginate(items.clone(), None, Some(2)).unwrap();
7998        assert_eq!(page1, vec![1, 2]);
7999
8000        let (page2, cursor2) = paginate(items, cursor1.as_deref(), Some(2)).unwrap();
8001        assert_eq!(page2, vec![3, 4]);
8002        assert!(cursor2.is_some());
8003    }
8004
8005    #[test]
8006    fn test_paginate_last_page() {
8007        let items = vec![1, 2, 3, 4, 5];
8008        // Skip to offset 4 (last item)
8009        let cursor = encode_cursor(4);
8010        let (page, next) = paginate(items, Some(&cursor), Some(2)).unwrap();
8011        assert_eq!(page, vec![5]);
8012        assert!(next.is_none());
8013    }
8014
8015    #[test]
8016    fn test_paginate_exact_boundary() {
8017        let items = vec![1, 2, 3, 4];
8018        let (page, cursor) = paginate(items, None, Some(4)).unwrap();
8019        assert_eq!(page, vec![1, 2, 3, 4]);
8020        assert!(cursor.is_none());
8021    }
8022
8023    #[test]
8024    fn test_paginate_invalid_cursor() {
8025        let items = vec![1, 2, 3];
8026        let result = paginate(items, Some("not-valid-base64!@#$"), Some(2));
8027        assert!(result.is_err());
8028    }
8029
8030    #[test]
8031    fn test_cursor_round_trip() {
8032        let offset = 42;
8033        let encoded = encode_cursor(offset);
8034        let decoded = decode_cursor(&encoded).unwrap();
8035        assert_eq!(decoded, offset);
8036    }
8037
8038    #[tokio::test]
8039    async fn test_list_tools_pagination() {
8040        let tool_a = ToolBuilder::new("alpha")
8041            .description("a")
8042            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
8043            .build();
8044        let tool_b = ToolBuilder::new("beta")
8045            .description("b")
8046            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
8047            .build();
8048        let tool_c = ToolBuilder::new("gamma")
8049            .description("c")
8050            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
8051            .build();
8052
8053        let mut router = McpRouter::new()
8054            .server_info("test", "1.0")
8055            .page_size(2)
8056            .tool(tool_a)
8057            .tool(tool_b)
8058            .tool(tool_c);
8059
8060        init_router(&mut router).await;
8061
8062        // First page
8063        let req = RouterRequest {
8064            id: RequestId::Number(1),
8065            inner: McpRequest::ListTools(ListToolsParams {
8066                cursor: None,
8067                meta: None,
8068            }),
8069            extensions: Extensions::new(),
8070        };
8071        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8072        let (tools, next_cursor) = match resp.inner {
8073            Ok(McpResponse::ListTools(result)) => (result.tools, result.next_cursor),
8074            other => panic!("Expected ListTools, got {:?}", other),
8075        };
8076        assert_eq!(tools.len(), 2);
8077        assert_eq!(tools[0].name, "alpha");
8078        assert_eq!(tools[1].name, "beta");
8079        assert!(next_cursor.is_some());
8080
8081        // Second page
8082        let req = RouterRequest {
8083            id: RequestId::Number(2),
8084            inner: McpRequest::ListTools(ListToolsParams {
8085                cursor: next_cursor,
8086                meta: None,
8087            }),
8088            extensions: Extensions::new(),
8089        };
8090        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8091        let (tools, next_cursor) = match resp.inner {
8092            Ok(McpResponse::ListTools(result)) => (result.tools, result.next_cursor),
8093            other => panic!("Expected ListTools, got {:?}", other),
8094        };
8095        assert_eq!(tools.len(), 1);
8096        assert_eq!(tools[0].name, "gamma");
8097        assert!(next_cursor.is_none());
8098    }
8099
8100    #[tokio::test]
8101    async fn test_list_tools_no_pagination_by_default() {
8102        let tool_a = ToolBuilder::new("alpha")
8103            .description("a")
8104            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
8105            .build();
8106        let tool_b = ToolBuilder::new("beta")
8107            .description("b")
8108            .handler(|_input: AddInput| async { Ok(CallToolResult::text("ok")) })
8109            .build();
8110
8111        let mut router = McpRouter::new()
8112            .server_info("test", "1.0")
8113            .tool(tool_a)
8114            .tool(tool_b);
8115
8116        init_router(&mut router).await;
8117
8118        let req = RouterRequest {
8119            id: RequestId::Number(1),
8120            inner: McpRequest::ListTools(ListToolsParams {
8121                cursor: None,
8122                meta: None,
8123            }),
8124            extensions: Extensions::new(),
8125        };
8126        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8127        match resp.inner {
8128            Ok(McpResponse::ListTools(result)) => {
8129                assert_eq!(result.tools.len(), 2);
8130                assert!(result.next_cursor.is_none());
8131            }
8132            other => panic!("Expected ListTools, got {:?}", other),
8133        }
8134    }
8135
8136    // =========================================================================
8137    // Dynamic Tool Registry Tests
8138    // =========================================================================
8139
8140    #[cfg(feature = "dynamic-tools")]
8141    mod dynamic_tools_tests {
8142        use super::*;
8143
8144        #[tokio::test]
8145        async fn test_dynamic_tools_register_and_list() {
8146            let (router, registry) = McpRouter::new()
8147                .server_info("test", "1.0")
8148                .with_dynamic_tools();
8149
8150            let tool = ToolBuilder::new("dynamic_echo")
8151                .description("Dynamic echo")
8152                .handler(|input: AddInput| async move {
8153                    Ok(CallToolResult::text(format!("{}", input.a)))
8154                })
8155                .build();
8156
8157            registry.register(tool);
8158
8159            let mut router = router;
8160            init_router(&mut router).await;
8161
8162            let req = RouterRequest {
8163                id: RequestId::Number(1),
8164                inner: McpRequest::ListTools(ListToolsParams::default()),
8165                extensions: Extensions::new(),
8166            };
8167
8168            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8169            match resp.inner {
8170                Ok(McpResponse::ListTools(result)) => {
8171                    assert_eq!(result.tools.len(), 1);
8172                    assert_eq!(result.tools[0].name, "dynamic_echo");
8173                }
8174                _ => panic!("Expected ListTools response"),
8175            }
8176        }
8177
8178        #[tokio::test]
8179        async fn test_dynamic_tools_unregister() {
8180            let (router, registry) = McpRouter::new()
8181                .server_info("test", "1.0")
8182                .with_dynamic_tools();
8183
8184            let tool = ToolBuilder::new("temp")
8185                .description("Temporary")
8186                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8187                .build();
8188
8189            registry.register(tool);
8190            assert!(registry.contains("temp"));
8191
8192            let removed = registry.unregister("temp");
8193            assert!(removed);
8194            assert!(!registry.contains("temp"));
8195
8196            // Unregistering again returns false
8197            assert!(!registry.unregister("temp"));
8198
8199            let mut router = router;
8200            init_router(&mut router).await;
8201
8202            let req = RouterRequest {
8203                id: RequestId::Number(1),
8204                inner: McpRequest::ListTools(ListToolsParams::default()),
8205                extensions: Extensions::new(),
8206            };
8207
8208            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8209            match resp.inner {
8210                Ok(McpResponse::ListTools(result)) => {
8211                    assert_eq!(result.tools.len(), 0);
8212                }
8213                _ => panic!("Expected ListTools response"),
8214            }
8215        }
8216
8217        #[tokio::test]
8218        async fn test_dynamic_tools_merged_with_static() {
8219            let static_tool = ToolBuilder::new("static_tool")
8220                .description("Static")
8221                .handler(|_: AddInput| async { Ok(CallToolResult::text("static")) })
8222                .build();
8223
8224            let (router, registry) = McpRouter::new()
8225                .server_info("test", "1.0")
8226                .tool(static_tool)
8227                .with_dynamic_tools();
8228
8229            let dynamic_tool = ToolBuilder::new("dynamic_tool")
8230                .description("Dynamic")
8231                .handler(|_: AddInput| async { Ok(CallToolResult::text("dynamic")) })
8232                .build();
8233
8234            registry.register(dynamic_tool);
8235
8236            let mut router = router;
8237            init_router(&mut router).await;
8238
8239            let req = RouterRequest {
8240                id: RequestId::Number(1),
8241                inner: McpRequest::ListTools(ListToolsParams::default()),
8242                extensions: Extensions::new(),
8243            };
8244
8245            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8246            match resp.inner {
8247                Ok(McpResponse::ListTools(result)) => {
8248                    assert_eq!(result.tools.len(), 2);
8249                    let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
8250                    assert!(names.contains(&"static_tool"));
8251                    assert!(names.contains(&"dynamic_tool"));
8252                }
8253                _ => panic!("Expected ListTools response"),
8254            }
8255        }
8256
8257        #[tokio::test]
8258        async fn test_static_tools_shadow_dynamic() {
8259            let static_tool = ToolBuilder::new("shared")
8260                .description("Static version")
8261                .handler(|_: AddInput| async { Ok(CallToolResult::text("static")) })
8262                .build();
8263
8264            let (router, registry) = McpRouter::new()
8265                .server_info("test", "1.0")
8266                .tool(static_tool)
8267                .with_dynamic_tools();
8268
8269            let dynamic_tool = ToolBuilder::new("shared")
8270                .description("Dynamic version")
8271                .handler(|_: AddInput| async { Ok(CallToolResult::text("dynamic")) })
8272                .build();
8273
8274            registry.register(dynamic_tool);
8275
8276            let mut router = router;
8277            init_router(&mut router).await;
8278
8279            // List should only show the static version
8280            let req = RouterRequest {
8281                id: RequestId::Number(1),
8282                inner: McpRequest::ListTools(ListToolsParams::default()),
8283                extensions: Extensions::new(),
8284            };
8285
8286            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8287            match resp.inner {
8288                Ok(McpResponse::ListTools(result)) => {
8289                    assert_eq!(result.tools.len(), 1);
8290                    assert_eq!(result.tools[0].name, "shared");
8291                    assert_eq!(
8292                        result.tools[0].description.as_deref(),
8293                        Some("Static version")
8294                    );
8295                }
8296                _ => panic!("Expected ListTools response"),
8297            }
8298
8299            // Call should dispatch to the static tool
8300            let req = RouterRequest {
8301                id: RequestId::Number(2),
8302                inner: McpRequest::CallTool(CallToolParams {
8303                    input_responses: None,
8304                    request_state: None,
8305                    name: "shared".to_string(),
8306                    arguments: serde_json::json!({"a": 1, "b": 2}),
8307                    meta: None,
8308                    task: None,
8309                }),
8310                extensions: Extensions::new(),
8311            };
8312
8313            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8314            match resp.inner {
8315                Ok(McpResponse::CallTool(result)) => {
8316                    assert!(!result.is_error);
8317                    match &result.content[0] {
8318                        Content::Text { text, .. } => assert_eq!(text, "static"),
8319                        _ => panic!("Expected text content"),
8320                    }
8321                }
8322                _ => panic!("Expected CallTool response"),
8323            }
8324        }
8325
8326        #[tokio::test]
8327        async fn test_dynamic_tools_call() {
8328            let (router, registry) = McpRouter::new()
8329                .server_info("test", "1.0")
8330                .with_dynamic_tools();
8331
8332            let tool = ToolBuilder::new("add")
8333                .description("Add two numbers")
8334                .handler(|input: AddInput| async move {
8335                    Ok(CallToolResult::text(format!("{}", input.a + input.b)))
8336                })
8337                .build();
8338
8339            registry.register(tool);
8340
8341            let mut router = router;
8342            init_router(&mut router).await;
8343
8344            let req = RouterRequest {
8345                id: RequestId::Number(1),
8346                inner: McpRequest::CallTool(CallToolParams {
8347                    input_responses: None,
8348                    request_state: None,
8349                    name: "add".to_string(),
8350                    arguments: serde_json::json!({"a": 3, "b": 4}),
8351                    meta: None,
8352                    task: None,
8353                }),
8354                extensions: Extensions::new(),
8355            };
8356
8357            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8358            match resp.inner {
8359                Ok(McpResponse::CallTool(result)) => {
8360                    assert!(!result.is_error);
8361                    match &result.content[0] {
8362                        Content::Text { text, .. } => assert_eq!(text, "7"),
8363                        _ => panic!("Expected text content"),
8364                    }
8365                }
8366                _ => panic!("Expected CallTool response"),
8367            }
8368        }
8369
8370        #[tokio::test]
8371        async fn test_dynamic_tools_notification_on_register() {
8372            let (tx, mut rx) = crate::context::notification_channel(16);
8373            let (router, registry) = McpRouter::new()
8374                .server_info("test", "1.0")
8375                .with_dynamic_tools();
8376            let _router = router.with_notification_sender(tx);
8377
8378            let tool = ToolBuilder::new("notified")
8379                .description("Test")
8380                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8381                .build();
8382
8383            registry.register(tool);
8384
8385            let notification = rx.recv().await.unwrap();
8386            assert!(matches!(notification, ServerNotification::ToolsListChanged));
8387        }
8388
8389        #[tokio::test]
8390        async fn test_dynamic_tools_notification_on_unregister() {
8391            let (tx, mut rx) = crate::context::notification_channel(16);
8392            let (router, registry) = McpRouter::new()
8393                .server_info("test", "1.0")
8394                .with_dynamic_tools();
8395            let _router = router.with_notification_sender(tx);
8396
8397            let tool = ToolBuilder::new("notified")
8398                .description("Test")
8399                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8400                .build();
8401
8402            registry.register(tool);
8403            // Consume the register notification
8404            let _ = rx.recv().await.unwrap();
8405
8406            registry.unregister("notified");
8407            let notification = rx.recv().await.unwrap();
8408            assert!(matches!(notification, ServerNotification::ToolsListChanged));
8409        }
8410
8411        #[tokio::test]
8412        async fn test_dynamic_tools_no_notification_on_empty_unregister() {
8413            let (tx, mut rx) = crate::context::notification_channel(16);
8414            let (router, registry) = McpRouter::new()
8415                .server_info("test", "1.0")
8416                .with_dynamic_tools();
8417            let _router = router.with_notification_sender(tx);
8418
8419            // Unregister a tool that doesn't exist — should NOT send notification
8420            assert!(!registry.unregister("nonexistent"));
8421
8422            // Channel should be empty
8423            assert!(rx.try_recv().is_err());
8424        }
8425
8426        #[tokio::test]
8427        async fn test_dynamic_tools_filter_applies() {
8428            use crate::filter::CapabilityFilter;
8429
8430            let (router, registry) = McpRouter::new()
8431                .server_info("test", "1.0")
8432                .tool_filter(CapabilityFilter::new(|_, tool: &Tool| {
8433                    tool.name != "hidden"
8434                }))
8435                .with_dynamic_tools();
8436
8437            let visible = ToolBuilder::new("visible")
8438                .description("Visible")
8439                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8440                .build();
8441
8442            let hidden = ToolBuilder::new("hidden")
8443                .description("Hidden")
8444                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8445                .build();
8446
8447            registry.register(visible);
8448            registry.register(hidden);
8449
8450            let mut router = router;
8451            init_router(&mut router).await;
8452
8453            // List should only show visible tool
8454            let req = RouterRequest {
8455                id: RequestId::Number(1),
8456                inner: McpRequest::ListTools(ListToolsParams::default()),
8457                extensions: Extensions::new(),
8458            };
8459
8460            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8461            match resp.inner {
8462                Ok(McpResponse::ListTools(result)) => {
8463                    assert_eq!(result.tools.len(), 1);
8464                    assert_eq!(result.tools[0].name, "visible");
8465                }
8466                _ => panic!("Expected ListTools response"),
8467            }
8468
8469            // Call to hidden tool should be denied
8470            let req = RouterRequest {
8471                id: RequestId::Number(2),
8472                inner: McpRequest::CallTool(CallToolParams {
8473                    input_responses: None,
8474                    request_state: None,
8475                    name: "hidden".to_string(),
8476                    arguments: serde_json::json!({"a": 1, "b": 2}),
8477                    meta: None,
8478                    task: None,
8479                }),
8480                extensions: Extensions::new(),
8481            };
8482
8483            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8484            match resp.inner {
8485                Err(e) => {
8486                    assert_eq!(e.code, -32601); // Method not found
8487                }
8488                _ => panic!("Expected JsonRpc error"),
8489            }
8490        }
8491
8492        #[tokio::test]
8493        async fn test_dynamic_tools_capabilities_advertised() {
8494            // No static tools, but dynamic tools enabled — should advertise tools capability
8495            let (mut router, _registry) = McpRouter::new()
8496                .server_info("test", "1.0")
8497                .with_dynamic_tools();
8498
8499            let init_req = RouterRequest {
8500                id: RequestId::Number(1),
8501                inner: McpRequest::Initialize(InitializeParams {
8502                    protocol_version: "2025-11-25".to_string(),
8503                    capabilities: ClientCapabilities::default(),
8504                    client_info: Implementation {
8505                        name: "test".to_string(),
8506                        version: "1.0".to_string(),
8507                        ..Default::default()
8508                    },
8509                    meta: None,
8510                }),
8511                extensions: Extensions::new(),
8512            };
8513
8514            let resp = router.ready().await.unwrap().call(init_req).await.unwrap();
8515            match resp.inner {
8516                Ok(McpResponse::Initialize(result)) => {
8517                    assert!(result.capabilities.tools.is_some());
8518                }
8519                _ => panic!("Expected Initialize response"),
8520            }
8521        }
8522
8523        #[tokio::test]
8524        async fn test_dynamic_tools_multi_session_notification() {
8525            let (tx1, mut rx1) = crate::context::notification_channel(16);
8526            let (tx2, mut rx2) = crate::context::notification_channel(16);
8527
8528            let (router, registry) = McpRouter::new()
8529                .server_info("test", "1.0")
8530                .with_dynamic_tools();
8531
8532            // Simulate two sessions by calling with_notification_sender on two clones
8533            let _session1 = router.clone().with_notification_sender(tx1);
8534            let _session2 = router.clone().with_notification_sender(tx2);
8535
8536            let tool = ToolBuilder::new("broadcast")
8537                .description("Test")
8538                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8539                .build();
8540
8541            registry.register(tool);
8542
8543            // Both sessions should receive the notification
8544            let n1 = rx1.recv().await.unwrap();
8545            let n2 = rx2.recv().await.unwrap();
8546            assert!(matches!(n1, ServerNotification::ToolsListChanged));
8547            assert!(matches!(n2, ServerNotification::ToolsListChanged));
8548        }
8549
8550        #[tokio::test]
8551        async fn test_dynamic_tools_call_not_found() {
8552            let (router, _registry) = McpRouter::new()
8553                .server_info("test", "1.0")
8554                .with_dynamic_tools();
8555
8556            let mut router = router;
8557            init_router(&mut router).await;
8558
8559            let req = RouterRequest {
8560                id: RequestId::Number(1),
8561                inner: McpRequest::CallTool(CallToolParams {
8562                    input_responses: None,
8563                    request_state: None,
8564                    name: "nonexistent".to_string(),
8565                    arguments: serde_json::json!({}),
8566                    meta: None,
8567                    task: None,
8568                }),
8569                extensions: Extensions::new(),
8570            };
8571
8572            let resp = router.ready().await.unwrap().call(req).await.unwrap();
8573            match resp.inner {
8574                Err(e) => {
8575                    assert_eq!(e.code, -32601);
8576                }
8577                _ => panic!("Expected method not found error"),
8578            }
8579        }
8580
8581        #[tokio::test]
8582        async fn test_dynamic_tools_registry_list() {
8583            let (_, registry) = McpRouter::new()
8584                .server_info("test", "1.0")
8585                .with_dynamic_tools();
8586
8587            assert!(registry.list().is_empty());
8588
8589            let tool = ToolBuilder::new("tool_a")
8590                .description("A")
8591                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8592                .build();
8593            registry.register(tool);
8594
8595            let tool = ToolBuilder::new("tool_b")
8596                .description("B")
8597                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8598                .build();
8599            registry.register(tool);
8600
8601            let tools = registry.list();
8602            assert_eq!(tools.len(), 2);
8603            let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
8604            assert!(names.contains(&"tool_a"));
8605            assert!(names.contains(&"tool_b"));
8606        }
8607    } // mod dynamic_tools_tests
8608
8609    #[tokio::test]
8610    async fn test_tool_if_true_registers() {
8611        let tool = ToolBuilder::new("conditional")
8612            .description("Conditional tool")
8613            .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8614            .build();
8615
8616        let mut router = McpRouter::new().tool_if(true, tool);
8617        init_router(&mut router).await;
8618
8619        let req = RouterRequest {
8620            id: RequestId::Number(1),
8621            inner: McpRequest::ListTools(ListToolsParams::default()),
8622            extensions: Extensions::new(),
8623        };
8624        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8625        match resp.inner {
8626            Ok(McpResponse::ListTools(result)) => {
8627                assert_eq!(result.tools.len(), 1);
8628                assert_eq!(result.tools[0].name, "conditional");
8629            }
8630            _ => panic!("Expected ListTools response"),
8631        }
8632    }
8633
8634    #[tokio::test]
8635    async fn test_tool_if_false_skips() {
8636        let tool = ToolBuilder::new("conditional")
8637            .description("Conditional tool")
8638            .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8639            .build();
8640
8641        let mut router = McpRouter::new().tool_if(false, tool);
8642        init_router(&mut router).await;
8643
8644        let req = RouterRequest {
8645            id: RequestId::Number(1),
8646            inner: McpRequest::ListTools(ListToolsParams::default()),
8647            extensions: Extensions::new(),
8648        };
8649        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8650        match resp.inner {
8651            Ok(McpResponse::ListTools(result)) => {
8652                assert_eq!(result.tools.len(), 0);
8653            }
8654            _ => panic!("Expected ListTools response"),
8655        }
8656    }
8657
8658    #[tokio::test]
8659    async fn test_tools_if_batch_conditional() {
8660        let tools = vec![
8661            ToolBuilder::new("a")
8662                .description("Tool A")
8663                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8664                .build(),
8665            ToolBuilder::new("b")
8666                .description("Tool B")
8667                .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8668                .build(),
8669        ];
8670
8671        let mut router = McpRouter::new().tools_if(false, tools);
8672        init_router(&mut router).await;
8673
8674        let req = RouterRequest {
8675            id: RequestId::Number(1),
8676            inner: McpRequest::ListTools(ListToolsParams::default()),
8677            extensions: Extensions::new(),
8678        };
8679        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8680        match resp.inner {
8681            Ok(McpResponse::ListTools(result)) => {
8682                assert_eq!(result.tools.len(), 0);
8683            }
8684            _ => panic!("Expected ListTools response"),
8685        }
8686    }
8687
8688    #[test]
8689    fn test_resource_if_true_registers() {
8690        let resource = crate::resource::ResourceBuilder::new("file:///test.txt")
8691            .name("test")
8692            .text("hello");
8693
8694        let router = McpRouter::new().resource_if(true, resource);
8695        assert_eq!(router.inner.resources.len(), 1);
8696    }
8697
8698    #[test]
8699    fn test_resource_if_false_skips() {
8700        let resource = crate::resource::ResourceBuilder::new("file:///test.txt")
8701            .name("test")
8702            .text("hello");
8703
8704        let router = McpRouter::new().resource_if(false, resource);
8705        assert_eq!(router.inner.resources.len(), 0);
8706    }
8707
8708    #[test]
8709    fn test_prompt_if_true_registers() {
8710        let prompt = crate::prompt::PromptBuilder::new("greet")
8711            .description("Greeting")
8712            .user_message("Hello!");
8713
8714        let router = McpRouter::new().prompt_if(true, prompt);
8715        assert_eq!(router.inner.prompts.len(), 1);
8716    }
8717
8718    #[test]
8719    fn test_prompt_if_false_skips() {
8720        let prompt = crate::prompt::PromptBuilder::new("greet")
8721            .description("Greeting")
8722            .user_message("Hello!");
8723
8724        let router = McpRouter::new().prompt_if(false, prompt);
8725        assert_eq!(router.inner.prompts.len(), 0);
8726    }
8727
8728    #[tokio::test]
8729    async fn test_disable_tool_hides_from_list() {
8730        let safe = ToolBuilder::new("safe")
8731            .description("Safe tool")
8732            .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8733            .build();
8734        let dangerous = ToolBuilder::new("dangerous")
8735            .description("Dangerous tool")
8736            .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8737            .build();
8738        let mut router = McpRouter::new().tool(safe).tool(dangerous);
8739        init_router(&mut router).await;
8740
8741        router.disable_tool("dangerous");
8742        assert!(router.is_tool_enabled("safe"));
8743        assert!(!router.is_tool_enabled("dangerous"));
8744
8745        let req = RouterRequest {
8746            id: RequestId::Number(1),
8747            inner: McpRequest::ListTools(ListToolsParams::default()),
8748            extensions: Extensions::new(),
8749        };
8750        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8751        match resp.inner {
8752            Ok(McpResponse::ListTools(result)) => {
8753                let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
8754                assert_eq!(names, vec!["safe"]);
8755            }
8756            _ => panic!("Expected ListTools response"),
8757        }
8758    }
8759
8760    #[tokio::test]
8761    async fn test_disable_tool_blocks_call() {
8762        let dangerous = ToolBuilder::new("dangerous")
8763            .description("Dangerous tool")
8764            .handler(|_: AddInput| async { Ok(CallToolResult::text("ran")) })
8765            .build();
8766        let mut router = McpRouter::new().tool(dangerous);
8767        init_router(&mut router).await;
8768
8769        router.disable_tool("dangerous");
8770
8771        let req = RouterRequest {
8772            id: RequestId::Number(2),
8773            inner: McpRequest::CallTool(CallToolParams {
8774                input_responses: None,
8775                request_state: None,
8776                name: "dangerous".to_string(),
8777                arguments: serde_json::json!({"a": 1, "b": 2}),
8778                meta: None,
8779                task: None,
8780            }),
8781            extensions: Extensions::new(),
8782        };
8783        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8784        let err = resp.inner.expect_err("disabled tool should error");
8785        assert_eq!(err.code, crate::error::ErrorCode::MethodNotFound as i32);
8786    }
8787
8788    #[tokio::test]
8789    async fn test_enable_tool_restores_visibility() {
8790        let tool = ToolBuilder::new("flippy")
8791            .description("Toggleable tool")
8792            .handler(|_: AddInput| async { Ok(CallToolResult::text("ran")) })
8793            .build();
8794        let mut router = McpRouter::new().tool(tool);
8795        init_router(&mut router).await;
8796
8797        router.disable_tool("flippy");
8798        router.enable_tool("flippy");
8799        assert!(router.is_tool_enabled("flippy"));
8800
8801        let req = RouterRequest {
8802            id: RequestId::Number(3),
8803            inner: McpRequest::CallTool(CallToolParams {
8804                input_responses: None,
8805                request_state: None,
8806                name: "flippy".to_string(),
8807                arguments: serde_json::json!({"a": 1, "b": 2}),
8808                meta: None,
8809                task: None,
8810            }),
8811            extensions: Extensions::new(),
8812        };
8813        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8814        match resp.inner {
8815            Ok(McpResponse::CallTool(result)) => {
8816                assert_eq!(result.first_text(), Some("ran"));
8817            }
8818            _ => panic!("Expected CallTool response"),
8819        }
8820    }
8821
8822    #[tokio::test]
8823    async fn test_disable_propagates_through_fresh_session() {
8824        let tool = ToolBuilder::new("shared")
8825            .description("Shared across sessions")
8826            .handler(|_: AddInput| async { Ok(CallToolResult::text("ok")) })
8827            .build();
8828        let router = McpRouter::new().tool(tool);
8829
8830        // Disable on the parent, observe via with_fresh_session clone.
8831        router.disable_tool("shared");
8832        let mut child = router.with_fresh_session();
8833        init_router(&mut child).await;
8834        assert!(!child.is_tool_enabled("shared"));
8835
8836        let req = RouterRequest {
8837            id: RequestId::Number(4),
8838            inner: McpRequest::ListTools(ListToolsParams::default()),
8839            extensions: Extensions::new(),
8840        };
8841        let resp = child.ready().await.unwrap().call(req).await.unwrap();
8842        match resp.inner {
8843            Ok(McpResponse::ListTools(result)) => {
8844                assert!(result.tools.is_empty());
8845            }
8846            _ => panic!("Expected ListTools response"),
8847        }
8848    }
8849
8850    #[tokio::test]
8851    async fn test_disable_resource_and_prompt() {
8852        let resource = crate::resource::ResourceBuilder::new("file:///hidden.txt")
8853            .name("hidden")
8854            .text("secret");
8855        let prompt = crate::prompt::PromptBuilder::new("hidden_prompt")
8856            .description("hidden")
8857            .user_message("hello");
8858
8859        let mut router = McpRouter::new().resource(resource).prompt(prompt);
8860        init_router(&mut router).await;
8861
8862        router.disable_resource("file:///hidden.txt");
8863        router.disable_prompt("hidden_prompt");
8864        assert!(!router.is_resource_enabled("file:///hidden.txt"));
8865        assert!(!router.is_prompt_enabled("hidden_prompt"));
8866
8867        // resources/list excludes
8868        let req = RouterRequest {
8869            id: RequestId::Number(5),
8870            inner: McpRequest::ListResources(ListResourcesParams::default()),
8871            extensions: Extensions::new(),
8872        };
8873        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8874        match resp.inner {
8875            Ok(McpResponse::ListResources(result)) => {
8876                assert!(result.resources.is_empty());
8877            }
8878            _ => panic!("Expected ListResources response"),
8879        }
8880
8881        // resources/read returns not found
8882        let req = RouterRequest {
8883            id: RequestId::Number(6),
8884            inner: McpRequest::ReadResource(ReadResourceParams {
8885                input_responses: None,
8886                request_state: None,
8887                uri: "file:///hidden.txt".to_string(),
8888                meta: None,
8889            }),
8890            extensions: Extensions::new(),
8891        };
8892        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8893        let err = resp.inner.expect_err("disabled resource should error");
8894        assert_eq!(err.code, -32602); // SEP-2164: ResourceNotFound now uses InvalidParams
8895
8896        // prompts/list excludes
8897        let req = RouterRequest {
8898            id: RequestId::Number(7),
8899            inner: McpRequest::ListPrompts(ListPromptsParams::default()),
8900            extensions: Extensions::new(),
8901        };
8902        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8903        match resp.inner {
8904            Ok(McpResponse::ListPrompts(result)) => {
8905                assert!(result.prompts.is_empty());
8906            }
8907            _ => panic!("Expected ListPrompts response"),
8908        }
8909
8910        // prompts/get returns not found
8911        let req = RouterRequest {
8912            id: RequestId::Number(8),
8913            inner: McpRequest::GetPrompt(GetPromptParams {
8914                input_responses: None,
8915                request_state: None,
8916                name: "hidden_prompt".to_string(),
8917                arguments: Default::default(),
8918                meta: None,
8919            }),
8920            extensions: Extensions::new(),
8921        };
8922        let resp = router.ready().await.unwrap().call(req).await.unwrap();
8923        let err = resp.inner.expect_err("disabled prompt should error");
8924        assert_eq!(err.code, crate::error::ErrorCode::MethodNotFound as i32);
8925    }
8926
8927    #[test]
8928    fn test_router_request_new() {
8929        let req = RouterRequest::new(RequestId::Number(1), McpRequest::Ping);
8930        assert_eq!(req.id, RequestId::Number(1));
8931        assert!(req.extensions.is_empty());
8932    }
8933
8934    #[test]
8935    fn test_with_inner_preserves_extensions() {
8936        let mut req = RouterRequest::new(RequestId::Number(1), McpRequest::Ping);
8937        req.extensions.insert(42u32);
8938
8939        let rewritten = req.with_inner(McpRequest::ListTools(Default::default()));
8940        assert!(matches!(rewritten.inner, McpRequest::ListTools(_)));
8941        assert_eq!(rewritten.id, RequestId::Number(1));
8942        assert_eq!(rewritten.extensions.get::<u32>(), Some(&42));
8943    }
8944
8945    #[test]
8946    fn test_with_id_and_inner_preserves_extensions() {
8947        let mut req = RouterRequest::new(RequestId::Number(1), McpRequest::Ping);
8948        req.extensions.insert(String::from("token-abc"));
8949
8950        let rewritten = req.with_id_and_inner(
8951            RequestId::Number(99),
8952            McpRequest::ListResources(Default::default()),
8953        );
8954        assert_eq!(rewritten.id, RequestId::Number(99));
8955        assert!(matches!(rewritten.inner, McpRequest::ListResources(_)));
8956        assert_eq!(
8957            rewritten.extensions.get::<String>(),
8958            Some(&String::from("token-abc"))
8959        );
8960    }
8961
8962    #[test]
8963    fn test_clone_with_inner_preserves_extensions() {
8964        let mut req = RouterRequest::new(RequestId::Number(1), McpRequest::Ping);
8965        req.extensions.insert(true);
8966
8967        let cloned = req.clone_with_inner(McpRequest::ListTools(Default::default()));
8968
8969        // Original still intact
8970        assert!(matches!(req.inner, McpRequest::Ping));
8971        assert_eq!(req.extensions.get::<bool>(), Some(&true));
8972
8973        // Clone has new inner but same extensions
8974        assert!(matches!(cloned.inner, McpRequest::ListTools(_)));
8975        assert_eq!(cloned.extensions.get::<bool>(), Some(&true));
8976    }
8977
8978    #[test]
8979    fn test_router_response_is_error() {
8980        let ok_resp = RouterResponse {
8981            id: RequestId::Number(1),
8982            inner: Ok(McpResponse::Pong(Default::default())),
8983        };
8984        assert!(!ok_resp.is_error());
8985
8986        let err_resp = RouterResponse {
8987            id: RequestId::Number(2),
8988            inner: Err(JsonRpcError::internal_error("boom")),
8989        };
8990        assert!(err_resp.is_error());
8991    }
8992
8993    #[test]
8994    fn test_extensions_len_and_is_empty() {
8995        let mut ext = Extensions::new();
8996        assert!(ext.is_empty());
8997        assert_eq!(ext.len(), 0);
8998
8999        ext.insert(42u32);
9000        assert!(!ext.is_empty());
9001        assert_eq!(ext.len(), 1);
9002
9003        ext.insert(String::from("hello"));
9004        assert_eq!(ext.len(), 2);
9005    }
9006
9007    #[test]
9008    fn test_router_response_serde_roundtrip() {
9009        // Success response
9010        let response = RouterResponse {
9011            id: RequestId::Number(1),
9012            inner: Ok(McpResponse::Empty(EmptyResult {})),
9013        };
9014        let json = serde_json::to_string(&response).unwrap();
9015        let deserialized: RouterResponse = serde_json::from_str(&json).unwrap();
9016        assert_eq!(deserialized.id, RequestId::Number(1));
9017        assert!(!deserialized.is_error());
9018
9019        // Error response
9020        let response = RouterResponse {
9021            id: RequestId::String("req-2".into()),
9022            inner: Err(JsonRpcError::method_not_found("unknown")),
9023        };
9024        let json = serde_json::to_string(&response).unwrap();
9025        let deserialized: RouterResponse = serde_json::from_str(&json).unwrap();
9026        assert_eq!(deserialized.id, RequestId::String("req-2".into()));
9027        assert!(deserialized.is_error());
9028    }
9029
9030    // =========================================================================
9031    // Issue #872: McpRequest::Discover unit tests
9032    // Unit tests that exercise the router dispatch directly via JsonRpcService,
9033    // without going through the HTTP transport layer.
9034    // =========================================================================
9035
9036    #[tokio::test]
9037    async fn test_discover_dispatch_via_jsonrpc_service() {
9038        // server/discover must work without any prior initialize call.
9039        // The router does NOT require session initialization for this RPC.
9040        let router = McpRouter::new().server_info("unit-test-server", "4.2.0");
9041        let mut service = JsonRpcService::new(router);
9042
9043        let req = JsonRpcRequest::new(1, "server/discover");
9044        let resp = service.call_single(req).await.unwrap();
9045
9046        match resp {
9047            JsonRpcResponse::Result(r) => {
9048                // supportedVersions must be a non-empty array.
9049                let versions = r
9050                    .result
9051                    .get("supportedVersions")
9052                    .and_then(|v| v.as_array())
9053                    .expect("result.supportedVersions must be an array");
9054                assert!(!versions.is_empty(), "supportedVersions must not be empty");
9055
9056                // Server identity lives in _meta, not the result body (SEP-2575 final).
9057                assert_eq!(
9058                    r.result["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
9059                    "unit-test-server",
9060                    "serverInfo.name must match configured value"
9061                );
9062                assert_eq!(
9063                    r.result["_meta"]["io.modelcontextprotocol/serverInfo"]["version"], "4.2.0",
9064                    "serverInfo.version must match configured value"
9065                );
9066
9067                // server/discover must NOT include singular protocolVersion
9068                // (that field belongs to the initialize response shape).
9069                assert!(
9070                    r.result.get("protocolVersion").is_none(),
9071                    "server/discover must NOT include protocolVersion: {:?}",
9072                    r.result
9073                );
9074            }
9075            JsonRpcResponse::Error(e) => panic!("Expected success, got error: {:?}", e),
9076            _ => panic!("unexpected response variant"),
9077        }
9078    }
9079
9080    #[tokio::test]
9081    async fn test_discover_does_not_require_initialization() {
9082        // server/discover works on a freshly created, un-initialized router.
9083        // No prior initialize call is made -- the session state is empty.
9084        let router = McpRouter::new().server_info("fresh-router", "1.0.0");
9085        let mut service = JsonRpcService::new(router);
9086
9087        let req = JsonRpcRequest::new(2, "server/discover");
9088        let resp = service.call_single(req).await.unwrap();
9089
9090        // Must succeed -- not return an error about missing session/initialization.
9091        assert!(
9092            !matches!(resp, JsonRpcResponse::Error(_)),
9093            "server/discover must not require initialization: {:?}",
9094            resp
9095        );
9096    }
9097}
9098
9099#[cfg(test)]
9100mod cursor_property_tests {
9101    use super::{decode_cursor, encode_cursor};
9102    use proptest::prelude::*;
9103
9104    fn arb_cursor_text() -> BoxedStrategy<String> {
9105        prop_oneof![
9106            8 => prop::collection::vec(any::<char>(), 0..512)
9107                .prop_map(|chars| chars.into_iter().collect()),
9108            1 => Just("\0\r\n\t\u{001b}\u{007f}".repeat(64)),
9109            1 => Just("A".repeat(16 * 1024)),
9110        ]
9111        .boxed()
9112    }
9113
9114    proptest! {
9115        #![proptest_config(ProptestConfig::with_cases(512))]
9116
9117        /// A cursor round-trips: decode(encode(n)) == n.
9118        #[test]
9119        fn cursor_round_trips(offset in any::<usize>()) {
9120            prop_assert_eq!(decode_cursor(&encode_cursor(offset)).unwrap(), offset);
9121        }
9122
9123        /// Decoding arbitrary client input never panics; it is Ok or a clean Err.
9124        #[test]
9125        fn decode_cursor_never_panics(s in arb_cursor_text()) {
9126            let _ = decode_cursor(&s);
9127        }
9128    }
9129}