Skip to main content

fastmcp_server/
router.rs

1//! Request router for MCP servers.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use asupersync::time::wall_now;
7use asupersync::{Budget, Cx, Outcome};
8use base64::Engine as _;
9use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
10use fastmcp_core::logging::{debug, targets, trace};
11use fastmcp_core::{
12    McpContext, McpError, McpErrorCode, McpResult, OutcomeExt, SessionState, block_on,
13};
14use fastmcp_protocol::{
15    CallToolParams, CallToolResult, CancelTaskParams, CancelTaskResult, Content, GetPromptParams,
16    GetPromptResult, GetTaskParams, GetTaskResult, InitializeParams, InitializeResult,
17    JsonRpcRequest, ListPromptsParams, ListPromptsResult, ListResourceTemplatesParams,
18    ListResourceTemplatesResult, ListResourcesParams, ListResourcesResult, ListTasksParams,
19    ListTasksResult, ListToolsParams, ListToolsResult, PROTOCOL_VERSION, ProgressMarker, Prompt,
20    ReadResourceParams, ReadResourceResult, Resource, ResourceTemplate, SubmitTaskParams,
21    SubmitTaskResult, Tool, validate, validate_strict,
22};
23
24use crate::handler::{BidirectionalSenders, UriParams, create_context_with_progress_and_senders};
25use crate::tasks::SharedTaskManager;
26
27use crate::Session;
28use crate::handler::{
29    BoxedPromptHandler, BoxedResourceHandler, BoxedToolHandler, PromptHandler, ResourceHandler,
30    ToolHandler,
31};
32
33/// Type alias for a notification sender callback.
34///
35/// This callback is used to send notifications (like progress updates) back to the client
36/// during request handling. The callback receives a JSON-RPC request (notification format).
37pub type NotificationSender = Arc<dyn Fn(JsonRpcRequest) + Send + Sync>;
38
39/// Tag filtering parameters for list operations.
40#[derive(Debug, Clone, Default)]
41pub struct TagFilters<'a> {
42    /// Only include components with ALL of these tags (AND logic).
43    pub include: Option<&'a [String]>,
44    /// Exclude components with ANY of these tags (OR logic).
45    pub exclude: Option<&'a [String]>,
46}
47
48impl<'a> TagFilters<'a> {
49    /// Creates tag filters from include and exclude vectors.
50    pub fn new(include: Option<&'a Vec<String>>, exclude: Option<&'a Vec<String>>) -> Self {
51        Self {
52            include: include.map(|v| v.as_slice()),
53            exclude: exclude.map(|v| v.as_slice()),
54        }
55    }
56
57    /// Returns true if the given component tags pass the filter.
58    ///
59    /// - Include filter: component must have ALL include tags (AND logic)
60    /// - Exclude filter: component is rejected if it has ANY exclude tag (OR logic)
61    /// - Tag matching is case-insensitive
62    pub fn matches(&self, component_tags: &[String]) -> bool {
63        // Normalize component tags to lowercase for comparison
64        let component_tags_lower: Vec<String> =
65            component_tags.iter().map(|t| t.to_lowercase()).collect();
66
67        // Include filter: must have ALL specified tags
68        if let Some(include) = self.include {
69            // Empty include array means no filter (all pass)
70            if !include.is_empty() {
71                for tag in include {
72                    let tag_lower = tag.to_lowercase();
73                    if !component_tags_lower.contains(&tag_lower) {
74                        return false;
75                    }
76                }
77            }
78        }
79
80        // Exclude filter: rejected if has ANY specified tag
81        if let Some(exclude) = self.exclude {
82            for tag in exclude {
83                let tag_lower = tag.to_lowercase();
84                if component_tags_lower.contains(&tag_lower) {
85                    return false;
86                }
87            }
88        }
89
90        true
91    }
92}
93
94fn decode_cursor_offset(cursor: Option<&str>) -> McpResult<usize> {
95    let Some(cursor) = cursor else {
96        return Ok(0);
97    };
98
99    let decoded = BASE64_STANDARD.decode(cursor).map_err(|_| {
100        McpError::invalid_params("Invalid cursor (base64 decode failed)".to_string())
101    })?;
102    let v: serde_json::Value = serde_json::from_slice(&decoded)
103        .map_err(|_| McpError::invalid_params("Invalid cursor (JSON parse failed)".to_string()))?;
104    let offset = v
105        .get("offset")
106        .and_then(serde_json::Value::as_u64)
107        .ok_or_else(|| McpError::invalid_params("Invalid cursor (missing offset)".to_string()))?;
108
109    usize::try_from(offset)
110        .map_err(|_| McpError::invalid_params("Invalid cursor (offset too large)".to_string()))
111}
112
113fn encode_cursor_offset(offset: usize) -> String {
114    let payload = serde_json::json!({ "offset": offset });
115    let bytes = serde_json::to_vec(&payload).expect("cursor state must serialize");
116    BASE64_STANDARD.encode(bytes)
117}
118
119/// Routes MCP requests to the appropriate handlers.
120pub struct Router {
121    tools: HashMap<String, BoxedToolHandler>,
122    tool_order: Vec<String>,
123    resources: HashMap<String, BoxedResourceHandler>,
124    resource_order: Vec<String>,
125    prompts: HashMap<String, BoxedPromptHandler>,
126    prompt_order: Vec<String>,
127    resource_templates: HashMap<String, ResourceTemplateEntry>,
128    resource_template_order: Vec<String>,
129    /// Pre-sorted template keys by specificity (most specific first).
130    /// Updated whenever templates are added/modified.
131    sorted_template_keys: Vec<String>,
132    /// Whether to enforce strict input validation (reject extra properties).
133    strict_input_validation: bool,
134    /// Optional list page size for cursor-based pagination.
135    ///
136    /// When `None`, list methods return all items in a single response and
137    /// `nextCursor` is always omitted.
138    list_page_size: Option<usize>,
139}
140
141impl Router {
142    /// Creates a new empty router.
143    #[must_use]
144    pub fn new() -> Self {
145        Self {
146            tools: HashMap::new(),
147            tool_order: Vec::new(),
148            resources: HashMap::new(),
149            resource_order: Vec::new(),
150            prompts: HashMap::new(),
151            prompt_order: Vec::new(),
152            resource_templates: HashMap::new(),
153            resource_template_order: Vec::new(),
154            sorted_template_keys: Vec::new(),
155            strict_input_validation: false,
156            list_page_size: None,
157        }
158    }
159
160    /// Sets the list pagination page size.
161    ///
162    /// When set, list methods (`tools/list`, `resources/list`, `resources/templates/list`,
163    /// `prompts/list`, `tasks/list`) will page results using opaque base64 cursors.
164    pub fn set_list_page_size(&mut self, page_size: Option<usize>) {
165        self.list_page_size = page_size.filter(|n| *n > 0);
166    }
167
168    /// Sets whether to use strict input validation.
169    ///
170    /// When enabled, tool input validation will reject any properties not
171    /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
172    ///
173    /// When disabled (default), extra properties are allowed unless the schema
174    /// explicitly sets `additionalProperties: false`.
175    pub fn set_strict_input_validation(&mut self, strict: bool) {
176        self.strict_input_validation = strict;
177    }
178
179    /// Returns whether strict input validation is enabled.
180    #[must_use]
181    pub fn strict_input_validation(&self) -> bool {
182        self.strict_input_validation
183    }
184
185    /// Rebuilds the sorted template keys vector.
186    /// Called after any modification to resource_templates.
187    fn rebuild_sorted_template_keys(&mut self) {
188        self.sorted_template_keys = self.resource_templates.keys().cloned().collect();
189        self.sorted_template_keys.sort_by(|a, b| {
190            let entry_a = &self.resource_templates[a];
191            let entry_b = &self.resource_templates[b];
192            let (a_literals, a_literal_segments, a_segments) = entry_a.matcher.specificity();
193            let (b_literals, b_literal_segments, b_segments) = entry_b.matcher.specificity();
194            b_literals
195                .cmp(&a_literals)
196                .then(b_literal_segments.cmp(&a_literal_segments))
197                .then(b_segments.cmp(&a_segments))
198                .then_with(|| a.cmp(b))
199        });
200    }
201
202    /// Adds a tool handler.
203    ///
204    /// If a tool with the same name already exists, it will be replaced.
205    /// Use [`add_tool_with_behavior`](Self::add_tool_with_behavior) for
206    /// finer control over duplicate handling.
207    pub fn add_tool<H: ToolHandler + 'static>(&mut self, handler: H) {
208        let def = handler.definition();
209        let is_new = !self.tools.contains_key(&def.name);
210        self.tools.insert(def.name.clone(), Box::new(handler));
211        if is_new {
212            self.tool_order.push(def.name);
213        }
214    }
215
216    /// Adds a tool handler with specified duplicate behavior.
217    ///
218    /// Returns `Err` if behavior is [`DuplicateBehavior::Error`] and the
219    /// tool name already exists.
220    pub fn add_tool_with_behavior<H: ToolHandler + 'static>(
221        &mut self,
222        handler: H,
223        behavior: crate::DuplicateBehavior,
224    ) -> Result<(), McpError> {
225        let def = handler.definition();
226        let name = &def.name;
227
228        let existed = self.tools.contains_key(name);
229        if existed {
230            match behavior {
231                crate::DuplicateBehavior::Error => {
232                    return Err(McpError::invalid_request(format!(
233                        "Tool '{}' already exists",
234                        name
235                    )));
236                }
237                crate::DuplicateBehavior::Warn => {
238                    log::warn!(target: "fastmcp_rust::router", "Tool '{}' already exists, keeping original", name);
239                    return Ok(());
240                }
241                crate::DuplicateBehavior::Replace => {
242                    log::debug!(target: "fastmcp_rust::router", "Replacing tool '{}'", name);
243                    // Fall through to insert
244                }
245                crate::DuplicateBehavior::Ignore => {
246                    return Ok(());
247                }
248            }
249        }
250
251        self.tools.insert(def.name.clone(), Box::new(handler));
252        if !existed {
253            self.tool_order.push(def.name);
254        }
255        Ok(())
256    }
257
258    /// Adds a resource handler.
259    ///
260    /// If a resource with the same URI already exists, it will be replaced.
261    /// Use [`add_resource_with_behavior`](Self::add_resource_with_behavior) for
262    /// finer control over duplicate handling.
263    pub fn add_resource<H: ResourceHandler + 'static>(&mut self, handler: H) {
264        let template = handler.template();
265        let def = handler.definition();
266        let boxed: BoxedResourceHandler = Box::new(handler);
267
268        if let Some(template) = template {
269            let is_new = !self.resource_templates.contains_key(&template.uri_template);
270            let entry = ResourceTemplateEntry {
271                matcher: UriTemplate::new(&template.uri_template),
272                template: template.clone(),
273                handler: Some(boxed),
274            };
275            self.resource_templates
276                .insert(template.uri_template.clone(), entry);
277            if is_new {
278                self.resource_template_order.push(template.uri_template);
279            }
280            self.rebuild_sorted_template_keys();
281        } else {
282            let is_new = !self.resources.contains_key(&def.uri);
283            self.resources.insert(def.uri.clone(), boxed);
284            if is_new {
285                self.resource_order.push(def.uri);
286            }
287        }
288    }
289
290    /// Adds a resource handler with specified duplicate behavior.
291    ///
292    /// Returns `Err` if behavior is [`DuplicateBehavior::Error`] and the
293    /// resource URI already exists.
294    pub fn add_resource_with_behavior<H: ResourceHandler + 'static>(
295        &mut self,
296        handler: H,
297        behavior: crate::DuplicateBehavior,
298    ) -> Result<(), McpError> {
299        let template = handler.template();
300        let def = handler.definition();
301
302        // Check for duplicates
303        let key = match template.as_ref() {
304            Some(template) => template.uri_template.clone(),
305            None => def.uri.clone(),
306        };
307
308        let exists = if template.is_some() {
309            self.resource_templates.contains_key(&key)
310        } else {
311            self.resources.contains_key(&key)
312        };
313
314        if exists {
315            match behavior {
316                crate::DuplicateBehavior::Error => {
317                    return Err(McpError::invalid_request(format!(
318                        "Resource '{}' already exists",
319                        key
320                    )));
321                }
322                crate::DuplicateBehavior::Warn => {
323                    log::warn!(target: "fastmcp_rust::router", "Resource '{}' already exists, keeping original", key);
324                    return Ok(());
325                }
326                crate::DuplicateBehavior::Replace => {
327                    log::debug!(target: "fastmcp_rust::router", "Replacing resource '{}'", key);
328                    // Fall through to insert
329                }
330                crate::DuplicateBehavior::Ignore => {
331                    return Ok(());
332                }
333            }
334        }
335
336        // Actually add the resource
337        let boxed: BoxedResourceHandler = Box::new(handler);
338
339        if let Some(template) = template {
340            let is_new = !self.resource_templates.contains_key(&template.uri_template);
341            let entry = ResourceTemplateEntry {
342                matcher: UriTemplate::new(&template.uri_template),
343                template: template.clone(),
344                handler: Some(boxed),
345            };
346            self.resource_templates
347                .insert(template.uri_template.clone(), entry);
348            if is_new {
349                self.resource_template_order.push(template.uri_template);
350            }
351            self.rebuild_sorted_template_keys();
352        } else {
353            let is_new = !self.resources.contains_key(&def.uri);
354            self.resources.insert(def.uri.clone(), boxed);
355            if is_new {
356                self.resource_order.push(def.uri);
357            }
358        }
359
360        Ok(())
361    }
362
363    /// Adds a resource template definition.
364    pub fn add_resource_template(&mut self, template: ResourceTemplate) {
365        let key = template.uri_template.clone();
366        let matcher = UriTemplate::new(&key);
367        let entry = ResourceTemplateEntry {
368            matcher,
369            template: template.clone(),
370            handler: None,
371        };
372        let needs_rebuild = match self.resource_templates.get_mut(&key) {
373            Some(existing) => {
374                existing.template = template;
375                existing.matcher = entry.matcher;
376                false // Key already exists, order unchanged
377            }
378            None => {
379                self.resource_templates.insert(key.clone(), entry);
380                true // New key added, need to rebuild
381            }
382        };
383        if needs_rebuild {
384            self.resource_template_order.push(key);
385            self.rebuild_sorted_template_keys();
386        }
387    }
388
389    /// Adds a prompt handler.
390    ///
391    /// If a prompt with the same name already exists, it will be replaced.
392    /// Use [`add_prompt_with_behavior`](Self::add_prompt_with_behavior) for
393    /// finer control over duplicate handling.
394    pub fn add_prompt<H: PromptHandler + 'static>(&mut self, handler: H) {
395        let def = handler.definition();
396        let is_new = !self.prompts.contains_key(&def.name);
397        self.prompts.insert(def.name.clone(), Box::new(handler));
398        if is_new {
399            self.prompt_order.push(def.name);
400        }
401    }
402
403    /// Adds a prompt handler with specified duplicate behavior.
404    ///
405    /// Returns `Err` if behavior is [`DuplicateBehavior::Error`] and the
406    /// prompt name already exists.
407    pub fn add_prompt_with_behavior<H: PromptHandler + 'static>(
408        &mut self,
409        handler: H,
410        behavior: crate::DuplicateBehavior,
411    ) -> Result<(), McpError> {
412        let def = handler.definition();
413        let name = &def.name;
414
415        let existed = self.prompts.contains_key(name);
416        if existed {
417            match behavior {
418                crate::DuplicateBehavior::Error => {
419                    return Err(McpError::invalid_request(format!(
420                        "Prompt '{}' already exists",
421                        name
422                    )));
423                }
424                crate::DuplicateBehavior::Warn => {
425                    log::warn!(target: "fastmcp_rust::router", "Prompt '{}' already exists, keeping original", name);
426                    return Ok(());
427                }
428                crate::DuplicateBehavior::Replace => {
429                    log::debug!(target: "fastmcp_rust::router", "Replacing prompt '{}'", name);
430                    // Fall through to insert
431                }
432                crate::DuplicateBehavior::Ignore => {
433                    return Ok(());
434                }
435            }
436        }
437
438        self.prompts.insert(def.name.clone(), Box::new(handler));
439        if !existed {
440            self.prompt_order.push(def.name);
441        }
442        Ok(())
443    }
444
445    /// Returns all tool definitions.
446    #[must_use]
447    pub fn tools(&self) -> Vec<Tool> {
448        self.tool_order
449            .iter()
450            .filter_map(|name| self.tools.get(name))
451            .map(|h| h.definition())
452            .collect()
453    }
454
455    /// Returns tool definitions filtered by session state and tags.
456    ///
457    /// Tools that have been disabled in the session state will not be included.
458    /// If tag filters are provided, tools must match the include/exclude criteria.
459    #[must_use]
460    pub fn tools_filtered(
461        &self,
462        session_state: Option<&SessionState>,
463        tag_filters: Option<&TagFilters<'_>>,
464    ) -> Vec<Tool> {
465        self.tool_order
466            .iter()
467            .filter_map(|name| self.tools.get(name))
468            .filter_map(|h| {
469                let def = h.definition();
470                // Check session state filter
471                if let Some(state) = session_state {
472                    if !state.is_tool_enabled(&def.name) {
473                        return None;
474                    }
475                }
476                // Check tag filters
477                if let Some(filters) = tag_filters {
478                    if !filters.matches(&def.tags) {
479                        return None;
480                    }
481                }
482                Some(def)
483            })
484            .collect()
485    }
486
487    /// Returns all resource definitions.
488    #[must_use]
489    pub fn resources(&self) -> Vec<Resource> {
490        self.resource_order
491            .iter()
492            .filter_map(|uri| self.resources.get(uri))
493            .map(|h| h.definition())
494            .collect()
495    }
496
497    /// Returns resource definitions filtered by session state and tags.
498    ///
499    /// Resources that have been disabled in the session state will not be included.
500    /// If tag filters are provided, resources must match the include/exclude criteria.
501    #[must_use]
502    pub fn resources_filtered(
503        &self,
504        session_state: Option<&SessionState>,
505        tag_filters: Option<&TagFilters<'_>>,
506    ) -> Vec<Resource> {
507        self.resource_order
508            .iter()
509            .filter_map(|uri| self.resources.get(uri))
510            .filter_map(|h| {
511                let def = h.definition();
512                // Check session state filter
513                if let Some(state) = session_state {
514                    if !state.is_resource_enabled(&def.uri) {
515                        return None;
516                    }
517                }
518                // Check tag filters
519                if let Some(filters) = tag_filters {
520                    if !filters.matches(&def.tags) {
521                        return None;
522                    }
523                }
524                Some(def)
525            })
526            .collect()
527    }
528
529    /// Returns all resource templates.
530    #[must_use]
531    pub fn resource_templates(&self) -> Vec<ResourceTemplate> {
532        self.resource_template_order
533            .iter()
534            .filter_map(|t| self.resource_templates.get(t))
535            .map(|entry| entry.template.clone())
536            .collect()
537    }
538
539    /// Returns resource templates filtered by session state and tags.
540    ///
541    /// Templates that have been disabled in the session state will not be included.
542    /// If tag filters are provided, templates must match the include/exclude criteria.
543    #[must_use]
544    pub fn resource_templates_filtered(
545        &self,
546        session_state: Option<&SessionState>,
547        tag_filters: Option<&TagFilters<'_>>,
548    ) -> Vec<ResourceTemplate> {
549        self.resource_template_order
550            .iter()
551            .filter_map(|t| self.resource_templates.get(t))
552            .filter_map(|entry| {
553                // Check session state filter
554                if let Some(state) = session_state {
555                    if !state.is_resource_enabled(&entry.template.uri_template) {
556                        return None;
557                    }
558                }
559                // Check tag filters
560                if let Some(filters) = tag_filters {
561                    if !filters.matches(&entry.template.tags) {
562                        return None;
563                    }
564                }
565                Some(entry.template.clone())
566            })
567            .collect()
568    }
569
570    /// Returns all prompt definitions.
571    #[must_use]
572    pub fn prompts(&self) -> Vec<Prompt> {
573        self.prompt_order
574            .iter()
575            .filter_map(|name| self.prompts.get(name))
576            .map(|h| h.definition())
577            .collect()
578    }
579
580    /// Returns prompt definitions filtered by session state and tags.
581    ///
582    /// Prompts that have been disabled in the session state will not be included.
583    /// If tag filters are provided, prompts must match the include/exclude criteria.
584    #[must_use]
585    pub fn prompts_filtered(
586        &self,
587        session_state: Option<&SessionState>,
588        tag_filters: Option<&TagFilters<'_>>,
589    ) -> Vec<Prompt> {
590        self.prompt_order
591            .iter()
592            .filter_map(|name| self.prompts.get(name))
593            .filter_map(|h| {
594                let def = h.definition();
595                // Check session state filter
596                if let Some(state) = session_state {
597                    if !state.is_prompt_enabled(&def.name) {
598                        return None;
599                    }
600                }
601                // Check tag filters
602                if let Some(filters) = tag_filters {
603                    if !filters.matches(&def.tags) {
604                        return None;
605                    }
606                }
607                Some(def)
608            })
609            .collect()
610    }
611
612    /// Returns the number of registered tools.
613    #[must_use]
614    pub fn tools_count(&self) -> usize {
615        self.tools.len()
616    }
617
618    /// Returns the number of registered resources.
619    #[must_use]
620    pub fn resources_count(&self) -> usize {
621        self.resources.len()
622    }
623
624    /// Returns the number of registered resource templates.
625    #[must_use]
626    pub fn resource_templates_count(&self) -> usize {
627        self.resource_templates.len()
628    }
629
630    /// Returns the number of registered prompts.
631    #[must_use]
632    pub fn prompts_count(&self) -> usize {
633        self.prompts.len()
634    }
635
636    /// Gets a tool handler by name.
637    #[must_use]
638    pub fn get_tool(&self, name: &str) -> Option<&BoxedToolHandler> {
639        self.tools.get(name)
640    }
641
642    /// Gets a resource handler by URI.
643    #[must_use]
644    pub fn get_resource(&self, uri: &str) -> Option<&BoxedResourceHandler> {
645        self.resources.get(uri)
646    }
647
648    /// Gets a resource template by URI template.
649    #[must_use]
650    pub fn get_resource_template(&self, uri_template: &str) -> Option<&ResourceTemplate> {
651        self.resource_templates
652            .get(uri_template)
653            .map(|entry| &entry.template)
654    }
655
656    /// Returns true if a resource exists for the given URI (static or template match).
657    #[must_use]
658    pub fn resource_exists(&self, uri: &str) -> bool {
659        self.resolve_resource(uri).is_some()
660    }
661
662    fn resolve_resource(&self, uri: &str) -> Option<ResolvedResource<'_>> {
663        if let Some(handler) = self.resources.get(uri) {
664            return Some(ResolvedResource {
665                handler,
666                params: UriParams::new(),
667            });
668        }
669
670        // Use pre-sorted template keys to avoid sorting on every lookup
671        for key in &self.sorted_template_keys {
672            let entry = &self.resource_templates[key];
673            let Some(handler) = entry.handler.as_ref() else {
674                continue;
675            };
676            if let Some(params) = entry.matcher.matches(uri) {
677                return Some(ResolvedResource { handler, params });
678            }
679        }
680
681        None
682    }
683
684    /// Gets a prompt handler by name.
685    #[must_use]
686    pub fn get_prompt(&self, name: &str) -> Option<&BoxedPromptHandler> {
687        self.prompts.get(name)
688    }
689
690    // ========================================================================
691    // Request Dispatch Methods
692    // ========================================================================
693
694    /// Handles the initialize request.
695    pub fn handle_initialize(
696        &self,
697        _cx: &Cx,
698        session: &mut Session,
699        params: InitializeParams,
700        instructions: Option<&str>,
701    ) -> McpResult<InitializeResult> {
702        debug!(
703            target: targets::SESSION,
704            "Initializing session with client: {:?}",
705            params.client_info.name
706        );
707
708        // Initialize the session
709        session.initialize(
710            params.client_info,
711            params.capabilities,
712            PROTOCOL_VERSION.to_string(),
713        );
714
715        Ok(InitializeResult {
716            protocol_version: PROTOCOL_VERSION.to_string(),
717            capabilities: session.server_capabilities().clone(),
718            server_info: session.server_info().clone(),
719            instructions: instructions.map(String::from),
720        })
721    }
722
723    /// Handles the tools/list request.
724    ///
725    /// If session_state is provided, disabled tools will be filtered out.
726    /// If include_tags/exclude_tags are provided, tools are filtered by tags.
727    pub fn handle_tools_list(
728        &self,
729        _cx: &Cx,
730        params: ListToolsParams,
731        session_state: Option<&SessionState>,
732    ) -> McpResult<ListToolsResult> {
733        let tag_filters =
734            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
735        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
736            Some(&tag_filters)
737        } else {
738            None
739        };
740        let tools = self.tools_filtered(session_state, tag_filters);
741        let Some(page_size) = self.list_page_size else {
742            return Ok(ListToolsResult {
743                tools,
744                next_cursor: None,
745            });
746        };
747
748        let offset = decode_cursor_offset(params.cursor.as_deref())?;
749        let end = offset.saturating_add(page_size).min(tools.len());
750        let next_cursor = if end < tools.len() {
751            Some(encode_cursor_offset(end))
752        } else {
753            None
754        };
755        Ok(ListToolsResult {
756            tools: tools.get(offset..end).unwrap_or_default().to_vec(),
757            next_cursor,
758        })
759    }
760
761    /// Handles the tools/call request.
762    ///
763    /// # Arguments
764    ///
765    /// * `cx` - The asupersync context for cancellation and tracing
766    /// * `request_id` - Internal request ID for tracking
767    /// * `params` - The tool call parameters including tool name and arguments
768    /// * `budget` - Request budget for timeout enforcement
769    /// * `session_state` - Session state for per-session storage
770    /// * `notification_sender` - Optional callback for sending progress notifications
771    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
772    pub fn handle_tools_call(
773        &self,
774        cx: &Cx,
775        request_id: u64,
776        params: CallToolParams,
777        budget: &Budget,
778        session_state: SessionState,
779        notification_sender: Option<&NotificationSender>,
780        bidirectional_senders: Option<&BidirectionalSenders>,
781    ) -> McpResult<CallToolResult> {
782        debug!(target: targets::HANDLER, "Calling tool: {}", params.name);
783        trace!(target: targets::HANDLER, "Tool arguments: {:?}", params.arguments);
784
785        // Check cancellation
786        if cx.is_cancel_requested() {
787            return Err(McpError::request_cancelled());
788        }
789
790        // Check budget exhaustion
791        if budget.is_exhausted() {
792            return Err(McpError::new(
793                McpErrorCode::RequestCancelled,
794                "Request budget exhausted",
795            ));
796        }
797        if budget.is_past_deadline(wall_now()) {
798            return Err(McpError::new(
799                McpErrorCode::RequestCancelled,
800                "Request timeout exceeded",
801            ));
802        }
803
804        // Check if tool is disabled for this session
805        if !session_state.is_tool_enabled(&params.name) {
806            return Err(McpError::new(
807                McpErrorCode::MethodNotFound,
808                format!("Tool '{}' is disabled for this session", params.name),
809            ));
810        }
811
812        // Find the tool handler
813        let handler = self
814            .tools
815            .get(&params.name)
816            .ok_or_else(|| McpError::method_not_found(&format!("tool: {}", params.name)))?;
817
818        // Validate arguments against the tool's input schema
819        // Default to empty object since MCP tool arguments are always objects
820        let arguments = params.arguments.unwrap_or_else(|| serde_json::json!({}));
821        let tool_def = handler.definition();
822
823        // Use strict or lenient validation based on configuration
824        let validation_result = if self.strict_input_validation {
825            validate_strict(&tool_def.input_schema, &arguments)
826        } else {
827            validate(&tool_def.input_schema, &arguments)
828        };
829
830        if let Err(validation_errors) = validation_result {
831            let error_messages: Vec<String> = validation_errors
832                .iter()
833                .map(|e| format!("{}: {}", e.path, e.message))
834                .collect();
835            return Err(McpError::invalid_params(format!(
836                "Input validation failed: {}",
837                error_messages.join("; ")
838            )));
839        }
840
841        // Extract progress marker from request metadata
842        let progress_marker: Option<ProgressMarker> =
843            params.meta.as_ref().and_then(|m| m.progress_marker.clone());
844
845        // Create context for the handler with progress reporting, session state, and bidirectional senders
846        let ctx = match (progress_marker, notification_sender) {
847            (Some(marker), Some(sender)) => {
848                let sender = sender.clone();
849                create_context_with_progress_and_senders(
850                    cx.clone(),
851                    request_id,
852                    Some(marker),
853                    Some(session_state),
854                    move |req| {
855                        sender(req);
856                    },
857                    bidirectional_senders,
858                )
859            }
860            _ => {
861                let mut ctx = McpContext::with_state(cx.clone(), request_id, session_state);
862                // Attach bidirectional senders even without progress
863                if let Some(senders) = bidirectional_senders {
864                    if let Some(ref sampling) = senders.sampling {
865                        ctx = ctx.with_sampling(sampling.clone());
866                    }
867                    if let Some(ref elicitation) = senders.elicitation {
868                        ctx = ctx.with_elicitation(elicitation.clone());
869                    }
870                }
871                ctx
872            }
873        };
874
875        // Call the handler asynchronously - returns McpOutcome (4-valued)
876        let outcome = block_on(handler.call_async(&ctx, arguments));
877        match outcome {
878            Outcome::Ok(content) => Ok(CallToolResult {
879                content,
880                is_error: false,
881            }),
882            Outcome::Err(e) => {
883                // If the request was cancelled, propagate the error as a JSON-RPC error.
884                if matches!(e.code, McpErrorCode::RequestCancelled) {
885                    return Err(e);
886                }
887
888                // Tool errors are returned as content with is_error=true
889                Ok(CallToolResult {
890                    content: vec![Content::Text { text: e.message }],
891                    is_error: true,
892                })
893            }
894            Outcome::Cancelled(_) => {
895                // Cancelled requests are reported as JSON-RPC errors
896                Err(McpError::request_cancelled())
897            }
898            Outcome::Panicked(payload) => {
899                // Panics become internal errors
900                Err(McpError::internal_error(format!(
901                    "Handler panic: {}",
902                    payload.message()
903                )))
904            }
905        }
906    }
907
908    /// Handles the resources/list request.
909    ///
910    /// If session_state is provided, disabled resources will be filtered out.
911    /// If include_tags/exclude_tags are provided, resources are filtered by tags.
912    pub fn handle_resources_list(
913        &self,
914        _cx: &Cx,
915        params: ListResourcesParams,
916        session_state: Option<&SessionState>,
917    ) -> McpResult<ListResourcesResult> {
918        let tag_filters =
919            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
920        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
921            Some(&tag_filters)
922        } else {
923            None
924        };
925        let resources = self.resources_filtered(session_state, tag_filters);
926        let Some(page_size) = self.list_page_size else {
927            return Ok(ListResourcesResult {
928                resources,
929                next_cursor: None,
930            });
931        };
932
933        let offset = decode_cursor_offset(params.cursor.as_deref())?;
934        let end = offset.saturating_add(page_size).min(resources.len());
935        let next_cursor = if end < resources.len() {
936            Some(encode_cursor_offset(end))
937        } else {
938            None
939        };
940        Ok(ListResourcesResult {
941            resources: resources.get(offset..end).unwrap_or_default().to_vec(),
942            next_cursor,
943        })
944    }
945
946    /// Handles the resources/templates/list request.
947    ///
948    /// If session_state is provided, disabled resource templates will be filtered out.
949    /// If include_tags/exclude_tags are provided, templates are filtered by tags.
950    pub fn handle_resource_templates_list(
951        &self,
952        _cx: &Cx,
953        params: ListResourceTemplatesParams,
954        session_state: Option<&SessionState>,
955    ) -> McpResult<ListResourceTemplatesResult> {
956        let tag_filters =
957            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
958        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
959            Some(&tag_filters)
960        } else {
961            None
962        };
963        let templates = self.resource_templates_filtered(session_state, tag_filters);
964        let Some(page_size) = self.list_page_size else {
965            return Ok(ListResourceTemplatesResult {
966                resource_templates: templates,
967                next_cursor: None,
968            });
969        };
970
971        let offset = decode_cursor_offset(params.cursor.as_deref())?;
972        let end = offset.saturating_add(page_size).min(templates.len());
973        let next_cursor = if end < templates.len() {
974            Some(encode_cursor_offset(end))
975        } else {
976            None
977        };
978        Ok(ListResourceTemplatesResult {
979            resource_templates: templates.get(offset..end).unwrap_or_default().to_vec(),
980            next_cursor,
981        })
982    }
983
984    /// Handles the resources/read request.
985    ///
986    /// # Arguments
987    ///
988    /// * `cx` - The asupersync context for cancellation and tracing
989    /// * `request_id` - Internal request ID for tracking
990    /// * `params` - The resource read parameters including URI
991    /// * `budget` - Request budget for timeout enforcement
992    /// * `session_state` - Session state for per-session storage
993    /// * `notification_sender` - Optional callback for sending progress notifications
994    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
995    pub fn handle_resources_read(
996        &self,
997        cx: &Cx,
998        request_id: u64,
999        params: &ReadResourceParams,
1000        budget: &Budget,
1001        session_state: SessionState,
1002        notification_sender: Option<&NotificationSender>,
1003        bidirectional_senders: Option<&BidirectionalSenders>,
1004    ) -> McpResult<ReadResourceResult> {
1005        debug!(target: targets::HANDLER, "Reading resource: {}", params.uri);
1006
1007        // Check cancellation
1008        if cx.is_cancel_requested() {
1009            return Err(McpError::request_cancelled());
1010        }
1011
1012        // Check budget exhaustion
1013        if budget.is_exhausted() {
1014            return Err(McpError::new(
1015                McpErrorCode::RequestCancelled,
1016                "Request budget exhausted",
1017            ));
1018        }
1019        if budget.is_past_deadline(wall_now()) {
1020            return Err(McpError::new(
1021                McpErrorCode::RequestCancelled,
1022                "Request timeout exceeded",
1023            ));
1024        }
1025
1026        // Check if resource is disabled for this session
1027        if !session_state.is_resource_enabled(&params.uri) {
1028            return Err(McpError::new(
1029                McpErrorCode::ResourceNotFound,
1030                format!("Resource '{}' is disabled for this session", params.uri),
1031            ));
1032        }
1033
1034        let resolved = self
1035            .resolve_resource(&params.uri)
1036            .ok_or_else(|| McpError::resource_not_found(&params.uri))?;
1037
1038        // Extract progress marker from request metadata
1039        let progress_marker: Option<ProgressMarker> =
1040            params.meta.as_ref().and_then(|m| m.progress_marker.clone());
1041
1042        // Create context for the handler with progress reporting, session state, and bidirectional senders
1043        let ctx = match (progress_marker, notification_sender) {
1044            (Some(marker), Some(sender)) => {
1045                let sender = sender.clone();
1046                create_context_with_progress_and_senders(
1047                    cx.clone(),
1048                    request_id,
1049                    Some(marker),
1050                    Some(session_state),
1051                    move |req| {
1052                        sender(req);
1053                    },
1054                    bidirectional_senders,
1055                )
1056            }
1057            _ => {
1058                let mut ctx = McpContext::with_state(cx.clone(), request_id, session_state);
1059                // Attach bidirectional senders even without progress
1060                if let Some(senders) = bidirectional_senders {
1061                    if let Some(ref sampling) = senders.sampling {
1062                        ctx = ctx.with_sampling(sampling.clone());
1063                    }
1064                    if let Some(ref elicitation) = senders.elicitation {
1065                        ctx = ctx.with_elicitation(elicitation.clone());
1066                    }
1067                }
1068                ctx
1069            }
1070        };
1071
1072        // Read the resource asynchronously - returns McpOutcome (4-valued)
1073        let outcome = block_on(resolved.handler.read_async_with_uri(
1074            &ctx,
1075            &params.uri,
1076            &resolved.params,
1077        ));
1078
1079        // Convert 4-valued Outcome to McpResult for JSON-RPC response
1080        let contents = outcome.into_mcp_result()?;
1081
1082        Ok(ReadResourceResult { contents })
1083    }
1084
1085    /// Handles the prompts/list request.
1086    ///
1087    /// If session_state is provided, disabled prompts will be filtered out.
1088    /// If include_tags/exclude_tags are provided, prompts are filtered by tags.
1089    pub fn handle_prompts_list(
1090        &self,
1091        _cx: &Cx,
1092        params: ListPromptsParams,
1093        session_state: Option<&SessionState>,
1094    ) -> McpResult<ListPromptsResult> {
1095        let tag_filters =
1096            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
1097        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
1098            Some(&tag_filters)
1099        } else {
1100            None
1101        };
1102        let prompts = self.prompts_filtered(session_state, tag_filters);
1103        let Some(page_size) = self.list_page_size else {
1104            return Ok(ListPromptsResult {
1105                prompts,
1106                next_cursor: None,
1107            });
1108        };
1109
1110        let offset = decode_cursor_offset(params.cursor.as_deref())?;
1111        let end = offset.saturating_add(page_size).min(prompts.len());
1112        let next_cursor = if end < prompts.len() {
1113            Some(encode_cursor_offset(end))
1114        } else {
1115            None
1116        };
1117        Ok(ListPromptsResult {
1118            prompts: prompts.get(offset..end).unwrap_or_default().to_vec(),
1119            next_cursor,
1120        })
1121    }
1122
1123    /// Handles the prompts/get request.
1124    ///
1125    /// # Arguments
1126    ///
1127    /// * `cx` - The asupersync context for cancellation and tracing
1128    /// * `request_id` - Internal request ID for tracking
1129    /// * `params` - The prompt get parameters including name and arguments
1130    /// * `budget` - Request budget for timeout enforcement
1131    /// * `session_state` - Session state for per-session storage
1132    /// * `notification_sender` - Optional callback for sending progress notifications
1133    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
1134    pub fn handle_prompts_get(
1135        &self,
1136        cx: &Cx,
1137        request_id: u64,
1138        params: GetPromptParams,
1139        budget: &Budget,
1140        session_state: SessionState,
1141        notification_sender: Option<&NotificationSender>,
1142        bidirectional_senders: Option<&BidirectionalSenders>,
1143    ) -> McpResult<GetPromptResult> {
1144        debug!(target: targets::HANDLER, "Getting prompt: {}", params.name);
1145        trace!(target: targets::HANDLER, "Prompt arguments: {:?}", params.arguments);
1146
1147        // Check cancellation
1148        if cx.is_cancel_requested() {
1149            return Err(McpError::request_cancelled());
1150        }
1151
1152        // Check budget exhaustion
1153        if budget.is_exhausted() {
1154            return Err(McpError::new(
1155                McpErrorCode::RequestCancelled,
1156                "Request budget exhausted",
1157            ));
1158        }
1159        if budget.is_past_deadline(wall_now()) {
1160            return Err(McpError::new(
1161                McpErrorCode::RequestCancelled,
1162                "Request timeout exceeded",
1163            ));
1164        }
1165
1166        // Check if prompt is disabled for this session
1167        if !session_state.is_prompt_enabled(&params.name) {
1168            return Err(McpError::new(
1169                McpErrorCode::PromptNotFound,
1170                format!("Prompt '{}' is disabled for this session", params.name),
1171            ));
1172        }
1173
1174        // Find the prompt handler
1175        let handler = self.prompts.get(&params.name).ok_or_else(|| {
1176            McpError::new(
1177                fastmcp_core::McpErrorCode::PromptNotFound,
1178                format!("Prompt not found: {}", params.name),
1179            )
1180        })?;
1181
1182        // Extract progress marker from request metadata
1183        let progress_marker: Option<ProgressMarker> =
1184            params.meta.as_ref().and_then(|m| m.progress_marker.clone());
1185
1186        // Create context for the handler with progress reporting, session state, and bidirectional senders
1187        let ctx = match (progress_marker, notification_sender) {
1188            (Some(marker), Some(sender)) => {
1189                let sender = sender.clone();
1190                create_context_with_progress_and_senders(
1191                    cx.clone(),
1192                    request_id,
1193                    Some(marker),
1194                    Some(session_state),
1195                    move |req| {
1196                        sender(req);
1197                    },
1198                    bidirectional_senders,
1199                )
1200            }
1201            _ => {
1202                let mut ctx = McpContext::with_state(cx.clone(), request_id, session_state);
1203                // Attach bidirectional senders even without progress
1204                if let Some(senders) = bidirectional_senders {
1205                    if let Some(ref sampling) = senders.sampling {
1206                        ctx = ctx.with_sampling(sampling.clone());
1207                    }
1208                    if let Some(ref elicitation) = senders.elicitation {
1209                        ctx = ctx.with_elicitation(elicitation.clone());
1210                    }
1211                }
1212                ctx
1213            }
1214        };
1215
1216        // Get the prompt asynchronously - returns McpOutcome (4-valued)
1217        let arguments = params.arguments.unwrap_or_default();
1218        let outcome = block_on(handler.get_async(&ctx, arguments));
1219
1220        // Convert 4-valued Outcome to McpResult for JSON-RPC response
1221        let messages = outcome.into_mcp_result()?;
1222
1223        Ok(GetPromptResult {
1224            description: handler.definition().description,
1225            messages,
1226        })
1227    }
1228
1229    // ========================================================================
1230    // Task Dispatch Methods (Docket/SEP-1686)
1231    // ========================================================================
1232
1233    /// Handles the tasks/list request.
1234    ///
1235    /// Lists all background tasks, optionally filtered by status.
1236    pub fn handle_tasks_list(
1237        &self,
1238        _cx: &Cx,
1239        params: ListTasksParams,
1240        task_manager: Option<&SharedTaskManager>,
1241    ) -> McpResult<ListTasksResult> {
1242        let task_manager = task_manager.ok_or_else(|| {
1243            McpError::new(
1244                McpErrorCode::MethodNotFound,
1245                "Background tasks not enabled on this server",
1246            )
1247        })?;
1248
1249        debug!(target: targets::HANDLER, "Listing tasks (status filter: {:?})", params.status);
1250
1251        let mut tasks = task_manager.list_tasks(params.status);
1252        // Stable ordering for pagination: created_at then id.
1253        tasks.sort_by(|a, b| {
1254            a.created_at
1255                .cmp(&b.created_at)
1256                .then_with(|| a.id.0.cmp(&b.id.0))
1257        });
1258
1259        let limit = params.limit.unwrap_or(50).max(1) as usize;
1260        let offset = decode_cursor_offset(params.cursor.as_deref())?;
1261        let end = offset.saturating_add(limit).min(tasks.len());
1262        let next_cursor = if end < tasks.len() {
1263            Some(encode_cursor_offset(end))
1264        } else {
1265            None
1266        };
1267
1268        Ok(ListTasksResult {
1269            tasks: tasks.get(offset..end).unwrap_or_default().to_vec(),
1270            next_cursor,
1271        })
1272    }
1273
1274    /// Handles the tasks/get request.
1275    ///
1276    /// Gets information about a specific task, including its result if completed.
1277    pub fn handle_tasks_get(
1278        &self,
1279        _cx: &Cx,
1280        params: GetTaskParams,
1281        task_manager: Option<&SharedTaskManager>,
1282    ) -> McpResult<GetTaskResult> {
1283        let task_manager = task_manager.ok_or_else(|| {
1284            McpError::new(
1285                McpErrorCode::MethodNotFound,
1286                "Background tasks not enabled on this server",
1287            )
1288        })?;
1289
1290        debug!(target: targets::HANDLER, "Getting task: {}", params.id);
1291
1292        let task = task_manager
1293            .get_info(&params.id)
1294            .ok_or_else(|| McpError::invalid_params(format!("Task not found: {}", params.id)))?;
1295
1296        let result = task_manager.get_result(&params.id);
1297
1298        Ok(GetTaskResult { task, result })
1299    }
1300
1301    /// Handles the tasks/cancel request.
1302    ///
1303    /// Requests cancellation of a running or pending task.
1304    pub fn handle_tasks_cancel(
1305        &self,
1306        _cx: &Cx,
1307        params: CancelTaskParams,
1308        task_manager: Option<&SharedTaskManager>,
1309    ) -> McpResult<CancelTaskResult> {
1310        let task_manager = task_manager.ok_or_else(|| {
1311            McpError::new(
1312                McpErrorCode::MethodNotFound,
1313                "Background tasks not enabled on this server",
1314            )
1315        })?;
1316
1317        debug!(target: targets::HANDLER, "Cancelling task: {}", params.id);
1318
1319        let task = task_manager.cancel(&params.id, params.reason)?;
1320
1321        Ok(CancelTaskResult {
1322            cancelled: true,
1323            task,
1324        })
1325    }
1326
1327    /// Handles the tasks/submit request.
1328    ///
1329    /// Submits a new background task for execution.
1330    pub fn handle_tasks_submit(
1331        &self,
1332        cx: &Cx,
1333        params: SubmitTaskParams,
1334        task_manager: Option<&SharedTaskManager>,
1335    ) -> McpResult<SubmitTaskResult> {
1336        let task_manager = task_manager.ok_or_else(|| {
1337            McpError::new(
1338                McpErrorCode::MethodNotFound,
1339                "Background tasks not enabled on this server",
1340            )
1341        })?;
1342
1343        debug!(target: targets::HANDLER, "Submitting task: {}", params.task_type);
1344
1345        let task_id = task_manager.submit(cx, &params.task_type, params.params)?;
1346        let task = task_manager
1347            .get_info(&task_id)
1348            .ok_or_else(|| McpError::internal_error("Task created but not found"))?;
1349
1350        Ok(SubmitTaskResult { task })
1351    }
1352}
1353
1354impl Default for Router {
1355    fn default() -> Self {
1356        Self::new()
1357    }
1358}
1359
1360// ============================================================================
1361// Mount/Composition Support
1362// ============================================================================
1363
1364/// Result of a mount operation.
1365#[derive(Debug, Default)]
1366pub struct MountResult {
1367    /// Number of tools mounted.
1368    pub tools: usize,
1369    /// Number of resources mounted.
1370    pub resources: usize,
1371    /// Number of resource templates mounted.
1372    pub resource_templates: usize,
1373    /// Number of prompts mounted.
1374    pub prompts: usize,
1375    /// Any warnings generated during mounting (e.g., name conflicts).
1376    pub warnings: Vec<String>,
1377}
1378
1379impl MountResult {
1380    /// Returns true if any components were mounted.
1381    #[must_use]
1382    pub fn has_components(&self) -> bool {
1383        self.tools > 0 || self.resources > 0 || self.resource_templates > 0 || self.prompts > 0
1384    }
1385
1386    /// Returns true if mounting was successful (currently always true).
1387    #[must_use]
1388    pub fn is_success(&self) -> bool {
1389        true
1390    }
1391}
1392
1393impl Router {
1394    /// Applies a prefix to a name or URI.
1395    fn apply_prefix(name: &str, prefix: Option<&str>) -> String {
1396        match prefix {
1397            Some(p) if !p.is_empty() => format!("{}/{}", p, name),
1398            _ => name.to_string(),
1399        }
1400    }
1401
1402    /// Validates a prefix string.
1403    ///
1404    /// Prefixes must be alphanumeric plus underscores and hyphens,
1405    /// and cannot contain slashes.
1406    fn validate_prefix(prefix: &str) -> Result<(), String> {
1407        if prefix.is_empty() {
1408            return Ok(());
1409        }
1410        if prefix.contains('/') {
1411            return Err(format!("Prefix cannot contain slashes: '{}'", prefix));
1412        }
1413        // Allow alphanumeric, underscore, hyphen
1414        for ch in prefix.chars() {
1415            if !ch.is_alphanumeric() && ch != '_' && ch != '-' {
1416                return Err(format!(
1417                    "Prefix contains invalid character '{}': '{}'",
1418                    ch, prefix
1419                ));
1420            }
1421        }
1422        Ok(())
1423    }
1424
1425    /// Mounts all handlers from another router with an optional prefix.
1426    ///
1427    /// This consumes the source router and moves its handlers into this router.
1428    /// Names/URIs are prefixed with `prefix/` if a prefix is provided.
1429    ///
1430    /// # Example
1431    ///
1432    /// ```ignore
1433    /// let mut main_router = Router::new();
1434    /// let db_router = Router::new();
1435    /// // ... add handlers to db_router ...
1436    ///
1437    /// main_router.mount(db_router, Some("db"));
1438    /// // Tool "query" becomes "db/query"
1439    /// ```
1440    pub fn mount(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
1441        let mut result = MountResult::default();
1442
1443        let Router {
1444            tools,
1445            tool_order,
1446            resources,
1447            resource_order,
1448            prompts,
1449            prompt_order,
1450            resource_templates,
1451            resource_template_order,
1452            ..
1453        } = other;
1454
1455        // Validate prefix
1456        if let Some(p) = prefix {
1457            if let Err(e) = Self::validate_prefix(p) {
1458                result.warnings.push(e);
1459                // Continue anyway, but log the warning
1460            }
1461        }
1462
1463        // Mount tools
1464        let tool_result = self.mount_tools_from(tools, tool_order, prefix);
1465        result.tools = tool_result.tools;
1466        result.warnings.extend(tool_result.warnings);
1467
1468        // Mount resources
1469        let resource_result = self.mount_resources_from(resources, resource_order, prefix);
1470        result.resources = resource_result.resources;
1471        result.warnings.extend(resource_result.warnings);
1472
1473        // Mount resource templates
1474        let template_result =
1475            self.mount_resource_templates_from(resource_templates, resource_template_order, prefix);
1476        result.resource_templates = template_result.resource_templates;
1477        result.warnings.extend(template_result.warnings);
1478
1479        // Mount prompts
1480        let prompt_result = self.mount_prompts_from(prompts, prompt_order, prefix);
1481        result.prompts = prompt_result.prompts;
1482        result.warnings.extend(prompt_result.warnings);
1483
1484        // Log mount result
1485        if result.has_components() {
1486            debug!(
1487                target: targets::HANDLER,
1488                "Mounted {} tools, {} resources, {} templates, {} prompts (prefix: {:?})",
1489                result.tools,
1490                result.resources,
1491                result.resource_templates,
1492                result.prompts,
1493                prefix
1494            );
1495        }
1496
1497        result
1498    }
1499
1500    /// Mounts only tools from a router.
1501    pub fn mount_tools(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
1502        self.mount_tools_from(other.tools, other.tool_order, prefix)
1503    }
1504
1505    /// Internal: mount tools from a HashMap.
1506    fn mount_tools_from(
1507        &mut self,
1508        mut tools: HashMap<String, BoxedToolHandler>,
1509        tool_order: Vec<String>,
1510        prefix: Option<&str>,
1511    ) -> MountResult {
1512        use crate::handler::MountedToolHandler;
1513
1514        let mut result = MountResult::default();
1515
1516        for name in tool_order {
1517            let Some(handler) = tools.remove(&name) else {
1518                continue;
1519            };
1520            let mounted_name = Self::apply_prefix(&name, prefix);
1521            trace!(
1522                target: targets::HANDLER,
1523                "Mounting tool '{}' as '{}'",
1524                name,
1525                mounted_name
1526            );
1527
1528            // Check for conflicts
1529            let existed = self.tools.contains_key(&mounted_name);
1530            if existed {
1531                result.warnings.push(format!(
1532                    "Tool '{}' already exists, will be overwritten",
1533                    mounted_name
1534                ));
1535            }
1536
1537            // Wrap with mounted name and insert
1538            let mounted = MountedToolHandler::new(handler, mounted_name.clone());
1539            let needs_order_push = !existed && !self.tool_order.iter().any(|n| n == &mounted_name);
1540            self.tools.insert(mounted_name.clone(), Box::new(mounted));
1541            if needs_order_push {
1542                self.tool_order.push(mounted_name);
1543            }
1544            result.tools += 1;
1545        }
1546
1547        if !tools.is_empty() {
1548            // Defensive: older Routers or unusual construction could leave items untracked by
1549            // tool_order. Mount them deterministically to avoid HashMap iteration order leaks.
1550            let mut remaining: Vec<(String, BoxedToolHandler)> = tools.into_iter().collect();
1551            remaining.sort_by(|a, b| a.0.cmp(&b.0));
1552            for (name, handler) in remaining {
1553                let mounted_name = Self::apply_prefix(&name, prefix);
1554
1555                let existed = self.tools.contains_key(&mounted_name);
1556                if existed {
1557                    result.warnings.push(format!(
1558                        "Tool '{}' already exists, will be overwritten",
1559                        mounted_name
1560                    ));
1561                }
1562
1563                let mounted = MountedToolHandler::new(handler, mounted_name.clone());
1564                self.tools.insert(mounted_name.clone(), Box::new(mounted));
1565                if !existed && !self.tool_order.iter().any(|n| n == &mounted_name) {
1566                    self.tool_order.push(mounted_name);
1567                }
1568                result.tools += 1;
1569            }
1570        }
1571
1572        result
1573    }
1574
1575    /// Mounts only resources from a router.
1576    pub fn mount_resources(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
1577        let mut result = self.mount_resources_from(other.resources, other.resource_order, prefix);
1578        let template_result = self.mount_resource_templates_from(
1579            other.resource_templates,
1580            other.resource_template_order,
1581            prefix,
1582        );
1583        result.resource_templates = template_result.resource_templates;
1584        result.warnings.extend(template_result.warnings);
1585        result
1586    }
1587
1588    /// Internal: mount resources from a HashMap.
1589    fn mount_resources_from(
1590        &mut self,
1591        mut resources: HashMap<String, BoxedResourceHandler>,
1592        resource_order: Vec<String>,
1593        prefix: Option<&str>,
1594    ) -> MountResult {
1595        use crate::handler::MountedResourceHandler;
1596
1597        let mut result = MountResult::default();
1598
1599        for uri in resource_order {
1600            let Some(handler) = resources.remove(&uri) else {
1601                continue;
1602            };
1603            let mounted_uri = Self::apply_prefix(&uri, prefix);
1604            trace!(
1605                target: targets::HANDLER,
1606                "Mounting resource '{}' as '{}'",
1607                uri,
1608                mounted_uri
1609            );
1610
1611            // Check for conflicts
1612            let existed = self.resources.contains_key(&mounted_uri);
1613            if existed {
1614                result.warnings.push(format!(
1615                    "Resource '{}' already exists, will be overwritten",
1616                    mounted_uri
1617                ));
1618            }
1619
1620            // Wrap with mounted URI and insert
1621            let mounted = MountedResourceHandler::new(handler, mounted_uri.clone());
1622            let needs_order_push =
1623                !existed && !self.resource_order.iter().any(|u| u == &mounted_uri);
1624            self.resources
1625                .insert(mounted_uri.clone(), Box::new(mounted));
1626            if needs_order_push {
1627                self.resource_order.push(mounted_uri);
1628            }
1629            result.resources += 1;
1630        }
1631
1632        if !resources.is_empty() {
1633            let mut remaining: Vec<(String, BoxedResourceHandler)> =
1634                resources.into_iter().collect();
1635            remaining.sort_by(|a, b| a.0.cmp(&b.0));
1636            for (uri, handler) in remaining {
1637                let mounted_uri = Self::apply_prefix(&uri, prefix);
1638
1639                let existed = self.resources.contains_key(&mounted_uri);
1640                if existed {
1641                    result.warnings.push(format!(
1642                        "Resource '{}' already exists, will be overwritten",
1643                        mounted_uri
1644                    ));
1645                }
1646
1647                let mounted = MountedResourceHandler::new(handler, mounted_uri.clone());
1648                self.resources
1649                    .insert(mounted_uri.clone(), Box::new(mounted));
1650                if !existed && !self.resource_order.iter().any(|u| u == &mounted_uri) {
1651                    self.resource_order.push(mounted_uri);
1652                }
1653                result.resources += 1;
1654            }
1655        }
1656
1657        result
1658    }
1659
1660    /// Internal: mount resource templates from a HashMap.
1661    fn mount_resource_templates_from(
1662        &mut self,
1663        mut templates: HashMap<String, ResourceTemplateEntry>,
1664        resource_template_order: Vec<String>,
1665        prefix: Option<&str>,
1666    ) -> MountResult {
1667        use crate::handler::MountedResourceHandler;
1668
1669        let mut result = MountResult::default();
1670
1671        for uri_template in resource_template_order {
1672            let Some(entry) = templates.remove(&uri_template) else {
1673                continue;
1674            };
1675            let mounted_uri_template = Self::apply_prefix(&uri_template, prefix);
1676            trace!(
1677                target: targets::HANDLER,
1678                "Mounting resource template '{}' as '{}'",
1679                uri_template,
1680                mounted_uri_template
1681            );
1682
1683            // Check for conflicts
1684            let existed = self.resource_templates.contains_key(&mounted_uri_template);
1685            if existed {
1686                result.warnings.push(format!(
1687                    "Resource template '{}' already exists, will be overwritten",
1688                    mounted_uri_template
1689                ));
1690            }
1691
1692            // Create new template with mounted URI
1693            let mut mounted_template = entry.template.clone();
1694            mounted_template.uri_template = mounted_uri_template.clone();
1695
1696            // Wrap handler if present
1697            let mounted_handler = entry.handler.map(|h| {
1698                let wrapped: BoxedResourceHandler =
1699                    Box::new(MountedResourceHandler::with_template(
1700                        h,
1701                        mounted_uri_template.clone(),
1702                        mounted_template.clone(),
1703                    ));
1704                wrapped
1705            });
1706
1707            // Create new entry with mounted template
1708            let mounted_entry = ResourceTemplateEntry {
1709                matcher: UriTemplate::new(&mounted_uri_template),
1710                template: mounted_template,
1711                handler: mounted_handler,
1712            };
1713
1714            let needs_order_push = !existed
1715                && !self
1716                    .resource_template_order
1717                    .iter()
1718                    .any(|t| t == &mounted_uri_template);
1719            self.resource_templates
1720                .insert(mounted_uri_template.clone(), mounted_entry);
1721            if needs_order_push {
1722                self.resource_template_order.push(mounted_uri_template);
1723            }
1724            result.resource_templates += 1;
1725        }
1726
1727        if !templates.is_empty() {
1728            let mut remaining: Vec<(String, ResourceTemplateEntry)> =
1729                templates.into_iter().collect();
1730            remaining.sort_by(|a, b| a.0.cmp(&b.0));
1731            for (uri_template, entry) in remaining {
1732                let mounted_uri_template = Self::apply_prefix(&uri_template, prefix);
1733
1734                let existed = self.resource_templates.contains_key(&mounted_uri_template);
1735                if existed {
1736                    result.warnings.push(format!(
1737                        "Resource template '{}' already exists, will be overwritten",
1738                        mounted_uri_template
1739                    ));
1740                }
1741
1742                let mut mounted_template = entry.template.clone();
1743                mounted_template.uri_template = mounted_uri_template.clone();
1744
1745                let mounted_handler = entry.handler.map(|h| {
1746                    let wrapped: BoxedResourceHandler =
1747                        Box::new(MountedResourceHandler::with_template(
1748                            h,
1749                            mounted_uri_template.clone(),
1750                            mounted_template.clone(),
1751                        ));
1752                    wrapped
1753                });
1754
1755                let mounted_entry = ResourceTemplateEntry {
1756                    matcher: UriTemplate::new(&mounted_uri_template),
1757                    template: mounted_template,
1758                    handler: mounted_handler,
1759                };
1760
1761                self.resource_templates
1762                    .insert(mounted_uri_template.clone(), mounted_entry);
1763                if !existed
1764                    && !self
1765                        .resource_template_order
1766                        .iter()
1767                        .any(|t| t == &mounted_uri_template)
1768                {
1769                    self.resource_template_order
1770                        .push(mounted_uri_template.clone());
1771                }
1772                result.resource_templates += 1;
1773            }
1774        }
1775
1776        // Rebuild sorted keys if we added templates
1777        if result.resource_templates > 0 {
1778            self.rebuild_sorted_template_keys();
1779        }
1780
1781        result
1782    }
1783
1784    /// Mounts only prompts from a router.
1785    pub fn mount_prompts(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
1786        self.mount_prompts_from(other.prompts, other.prompt_order, prefix)
1787    }
1788
1789    /// Internal: mount prompts from a HashMap.
1790    fn mount_prompts_from(
1791        &mut self,
1792        mut prompts: HashMap<String, BoxedPromptHandler>,
1793        prompt_order: Vec<String>,
1794        prefix: Option<&str>,
1795    ) -> MountResult {
1796        use crate::handler::MountedPromptHandler;
1797
1798        let mut result = MountResult::default();
1799
1800        for name in prompt_order {
1801            let Some(handler) = prompts.remove(&name) else {
1802                continue;
1803            };
1804            let mounted_name = Self::apply_prefix(&name, prefix);
1805            trace!(
1806                target: targets::HANDLER,
1807                "Mounting prompt '{}' as '{}'",
1808                name,
1809                mounted_name
1810            );
1811
1812            // Check for conflicts
1813            let existed = self.prompts.contains_key(&mounted_name);
1814            if existed {
1815                result.warnings.push(format!(
1816                    "Prompt '{}' already exists, will be overwritten",
1817                    mounted_name
1818                ));
1819            }
1820
1821            // Wrap with mounted name and insert
1822            let mounted = MountedPromptHandler::new(handler, mounted_name.clone());
1823            let needs_order_push =
1824                !existed && !self.prompt_order.iter().any(|n| n == &mounted_name);
1825            self.prompts.insert(mounted_name.clone(), Box::new(mounted));
1826            if needs_order_push {
1827                self.prompt_order.push(mounted_name);
1828            }
1829            result.prompts += 1;
1830        }
1831
1832        if !prompts.is_empty() {
1833            let mut remaining: Vec<(String, BoxedPromptHandler)> = prompts.into_iter().collect();
1834            remaining.sort_by(|a, b| a.0.cmp(&b.0));
1835            for (name, handler) in remaining {
1836                let mounted_name = Self::apply_prefix(&name, prefix);
1837
1838                let existed = self.prompts.contains_key(&mounted_name);
1839                if existed {
1840                    result.warnings.push(format!(
1841                        "Prompt '{}' already exists, will be overwritten",
1842                        mounted_name
1843                    ));
1844                }
1845
1846                let mounted = MountedPromptHandler::new(handler, mounted_name.clone());
1847                self.prompts.insert(mounted_name.clone(), Box::new(mounted));
1848                if !existed && !self.prompt_order.iter().any(|n| n == &mounted_name) {
1849                    self.prompt_order.push(mounted_name);
1850                }
1851                result.prompts += 1;
1852            }
1853        }
1854
1855        result
1856    }
1857
1858    /// Consumes the router and returns its internal handlers.
1859    ///
1860    /// This is used internally for mounting operations.
1861    #[must_use]
1862    #[allow(dead_code)]
1863    pub(crate) fn into_parts(
1864        self,
1865    ) -> (
1866        HashMap<String, BoxedToolHandler>,
1867        HashMap<String, BoxedResourceHandler>,
1868        HashMap<String, ResourceTemplateEntry>,
1869        HashMap<String, BoxedPromptHandler>,
1870    ) {
1871        (
1872            self.tools,
1873            self.resources,
1874            self.resource_templates,
1875            self.prompts,
1876        )
1877    }
1878}
1879
1880struct ResolvedResource<'a> {
1881    handler: &'a BoxedResourceHandler,
1882    params: UriParams,
1883}
1884
1885/// Entry for a resource template with its matcher and optional handler.
1886pub(crate) struct ResourceTemplateEntry {
1887    pub(crate) matcher: UriTemplate,
1888    pub(crate) template: ResourceTemplate,
1889    pub(crate) handler: Option<BoxedResourceHandler>,
1890}
1891
1892/// A parsed URI template for matching resource URIs.
1893#[derive(Debug, Clone)]
1894pub(crate) struct UriTemplate {
1895    pattern: String,
1896    segments: Vec<UriSegment>,
1897}
1898
1899#[derive(Debug, Clone, PartialEq, Eq)]
1900enum UriTemplateError {
1901    UnclosedParam,
1902    UnmatchedClose,
1903    EmptyParam,
1904    DuplicateParam(String),
1905}
1906
1907#[derive(Debug, Clone)]
1908enum UriSegment {
1909    Literal(String),
1910    Param(String),
1911}
1912
1913impl UriTemplate {
1914    /// Creates a new URI template from a pattern.
1915    ///
1916    /// If the pattern is invalid, logs a warning and returns a template
1917    /// that will never match any URI (fail-safe behavior).
1918    fn new(pattern: &str) -> Self {
1919        Self::try_new(pattern).unwrap_or_else(|err| {
1920            fastmcp_core::logging::warn!(
1921                target: targets::HANDLER,
1922                "Invalid URI template '{}': {:?}, using non-matching fallback",
1923                pattern,
1924                err
1925            );
1926            // Return a template with no segments that can never match
1927            Self {
1928                pattern: pattern.to_string(),
1929                segments: vec![UriSegment::Literal("\0INVALID\0".to_string())],
1930            }
1931        })
1932    }
1933
1934    /// Attempts to create a URI template, returning an error if invalid.
1935    fn try_new(pattern: &str) -> Result<Self, UriTemplateError> {
1936        Self::parse(pattern)
1937    }
1938
1939    fn parse(pattern: &str) -> Result<Self, UriTemplateError> {
1940        let mut segments = Vec::new();
1941        let mut literal = String::new();
1942        let mut chars = pattern.chars().peekable();
1943        let mut seen = std::collections::HashSet::new();
1944
1945        while let Some(ch) = chars.next() {
1946            match ch {
1947                '{' => {
1948                    if matches!(chars.peek(), Some('{')) {
1949                        let _ = chars.next();
1950                        literal.push('{');
1951                        continue;
1952                    }
1953
1954                    if !literal.is_empty() {
1955                        segments.push(UriSegment::Literal(std::mem::take(&mut literal)));
1956                    }
1957
1958                    let mut name = String::new();
1959                    let mut closed = false;
1960                    for next in chars.by_ref() {
1961                        if next == '}' {
1962                            closed = true;
1963                            break;
1964                        }
1965                        name.push(next);
1966                    }
1967
1968                    if !closed {
1969                        return Err(UriTemplateError::UnclosedParam);
1970                    }
1971
1972                    if name.is_empty() {
1973                        return Err(UriTemplateError::EmptyParam);
1974                    }
1975                    if !seen.insert(name.clone()) {
1976                        return Err(UriTemplateError::DuplicateParam(name));
1977                    }
1978                    segments.push(UriSegment::Param(name));
1979                }
1980                '}' => {
1981                    if matches!(chars.peek(), Some('}')) {
1982                        let _ = chars.next();
1983                        literal.push('}');
1984                        continue;
1985                    }
1986                    return Err(UriTemplateError::UnmatchedClose);
1987                }
1988                _ => literal.push(ch),
1989            }
1990        }
1991
1992        if !literal.is_empty() {
1993            segments.push(UriSegment::Literal(literal));
1994        }
1995
1996        Ok(Self {
1997            pattern: pattern.to_string(),
1998            segments,
1999        })
2000    }
2001
2002    fn specificity(&self) -> (usize, usize, usize) {
2003        let mut literal_len = 0usize;
2004        let mut literal_segments = 0usize;
2005        for segment in &self.segments {
2006            if let UriSegment::Literal(lit) = segment {
2007                literal_len += lit.len();
2008                literal_segments += 1;
2009            }
2010        }
2011        (literal_len, literal_segments, self.segments.len())
2012    }
2013
2014    fn matches(&self, uri: &str) -> Option<UriParams> {
2015        let mut params = UriParams::new();
2016        let mut remainder = uri;
2017        let mut iter = self.segments.iter().peekable();
2018
2019        while let Some(segment) = iter.next() {
2020            match segment {
2021                UriSegment::Literal(lit) => {
2022                    remainder = remainder.strip_prefix(lit)?;
2023                }
2024                UriSegment::Param(name) => {
2025                    let next_literal = iter.peek().and_then(|next| match next {
2026                        UriSegment::Literal(lit) => Some(lit.as_str()),
2027                        UriSegment::Param(_) => None,
2028                    });
2029
2030                    if next_literal.is_none() && iter.peek().is_some() {
2031                        return None;
2032                    }
2033
2034                    if let Some(literal) = next_literal {
2035                        let idx = remainder.find(literal)?;
2036                        let value = &remainder[..idx];
2037                        if value.is_empty() {
2038                            return None;
2039                        }
2040                        let value = percent_decode(value)?;
2041                        params.insert(name.clone(), value);
2042                        remainder = &remainder[idx..];
2043                    } else {
2044                        // Last param: only allow "/" when this is the sole param.
2045                        // Multi-param templates should not let the tail param
2046                        // consume extra path segments.
2047                        if remainder.is_empty() {
2048                            return None;
2049                        }
2050
2051                        let allow_slash_in_last_param = self
2052                            .segments
2053                            .iter()
2054                            .filter(|seg| matches!(seg, UriSegment::Param(_)))
2055                            .count()
2056                            == 1;
2057
2058                        let end_idx = if allow_slash_in_last_param {
2059                            remainder.len()
2060                        } else {
2061                            remainder.find('/').unwrap_or(remainder.len())
2062                        };
2063
2064                        let value = &remainder[..end_idx];
2065                        if value.is_empty() {
2066                            return None;
2067                        }
2068                        let value = percent_decode(value)?;
2069                        params.insert(name.clone(), value);
2070                        remainder = &remainder[end_idx..];
2071                    }
2072                }
2073            }
2074        }
2075
2076        if remainder.is_empty() {
2077            Some(params)
2078        } else {
2079            None
2080        }
2081    }
2082}
2083
2084fn percent_decode(input: &str) -> Option<String> {
2085    if !input.as_bytes().contains(&b'%') {
2086        return Some(input.to_string());
2087    }
2088    let bytes = input.as_bytes();
2089    let mut out = Vec::with_capacity(bytes.len());
2090    let mut i = 0usize;
2091    while i < bytes.len() {
2092        match bytes[i] {
2093            b'%' => {
2094                if i + 2 >= bytes.len() {
2095                    return None;
2096                }
2097                let hi = bytes[i + 1];
2098                let lo = bytes[i + 2];
2099                let value = (from_hex(hi)? << 4) | from_hex(lo)?;
2100                out.push(value);
2101                i += 3;
2102            }
2103            b => {
2104                out.push(b);
2105                i += 1;
2106            }
2107        }
2108    }
2109    String::from_utf8(out).ok()
2110}
2111
2112fn from_hex(byte: u8) -> Option<u8> {
2113    match byte {
2114        b'0'..=b'9' => Some(byte - b'0'),
2115        b'a'..=b'f' => Some(byte - b'a' + 10),
2116        b'A'..=b'F' => Some(byte - b'A' + 10),
2117        _ => None,
2118    }
2119}
2120
2121// ============================================================================
2122// Resource Reader Implementation
2123// ============================================================================
2124
2125use fastmcp_core::{
2126    MAX_RESOURCE_READ_DEPTH, ResourceContentItem, ResourceReadResult, ResourceReader,
2127};
2128use std::pin::Pin;
2129
2130/// A wrapper that implements `ResourceReader` for a shared `Router`.
2131///
2132/// This allows handlers to read resources from within tool/resource/prompt
2133/// handlers, enabling cross-component access.
2134pub struct RouterResourceReader {
2135    /// The shared router.
2136    router: Arc<Router>,
2137    /// Session state for handlers.
2138    session_state: SessionState,
2139}
2140
2141impl RouterResourceReader {
2142    /// Creates a new resource reader with the given router and session state.
2143    #[must_use]
2144    pub fn new(router: Arc<Router>, session_state: SessionState) -> Self {
2145        Self {
2146            router,
2147            session_state,
2148        }
2149    }
2150}
2151
2152impl ResourceReader for RouterResourceReader {
2153    fn read_resource(
2154        &self,
2155        cx: &Cx,
2156        uri: &str,
2157        depth: u32,
2158    ) -> Pin<
2159        Box<
2160            dyn std::future::Future<Output = fastmcp_core::McpResult<ResourceReadResult>>
2161                + Send
2162                + '_,
2163        >,
2164    > {
2165        // Check recursion depth
2166        if depth > MAX_RESOURCE_READ_DEPTH {
2167            return Box::pin(async move {
2168                Err(McpError::new(
2169                    McpErrorCode::InternalError,
2170                    format!(
2171                        "Maximum resource read depth ({}) exceeded",
2172                        MAX_RESOURCE_READ_DEPTH
2173                    ),
2174                ))
2175            });
2176        }
2177
2178        // Clone what we need for the async block
2179        let cx = cx.clone();
2180        let uri = uri.to_string();
2181        let router = self.router.clone();
2182        let session_state = self.session_state.clone();
2183
2184        Box::pin(async move {
2185            debug!(target: targets::HANDLER, "Cross-component resource read: {} (depth: {})", uri, depth);
2186
2187            // Resolve the resource
2188            let resolved = router.resolve_resource(&uri).ok_or_else(|| {
2189                McpError::new(
2190                    McpErrorCode::ResourceNotFound,
2191                    format!("Resource not found: {}", uri),
2192                )
2193            })?;
2194
2195            // Create a child context with incremented depth
2196            // Clone router again for the nested reader (the original is borrowed by resolved)
2197            let nested_router = router.clone();
2198            let nested_state = session_state.clone();
2199            let child_ctx = McpContext::with_state(cx.clone(), 0, session_state)
2200                .with_resource_read_depth(depth)
2201                .with_resource_reader(Arc::new(RouterResourceReader::new(
2202                    nested_router,
2203                    nested_state,
2204                )));
2205
2206            // Read the resource
2207            let outcome = block_on(resolved.handler.read_async_with_uri(
2208                &child_ctx,
2209                &uri,
2210                &resolved.params,
2211            ));
2212
2213            // Convert outcome to result
2214            let contents = outcome.into_mcp_result()?;
2215
2216            // Convert protocol ResourceContent to core ResourceContentItem
2217            let items: Vec<ResourceContentItem> = contents
2218                .into_iter()
2219                .map(|c| ResourceContentItem {
2220                    uri: c.uri,
2221                    mime_type: c.mime_type,
2222                    text: c.text,
2223                    blob: c.blob,
2224                })
2225                .collect();
2226
2227            Ok(ResourceReadResult::new(items))
2228        })
2229    }
2230}
2231
2232// ============================================================================
2233// Tool Caller Implementation
2234// ============================================================================
2235
2236use fastmcp_core::{MAX_TOOL_CALL_DEPTH, ToolCallResult, ToolCaller, ToolContentItem};
2237
2238/// A wrapper that implements `ToolCaller` for a shared `Router`.
2239///
2240/// This allows handlers to call other tools from within tool/resource/prompt
2241/// handlers, enabling cross-component access.
2242pub struct RouterToolCaller {
2243    /// The shared router.
2244    router: Arc<Router>,
2245    /// Session state for handlers.
2246    session_state: SessionState,
2247}
2248
2249impl RouterToolCaller {
2250    /// Creates a new tool caller with the given router and session state.
2251    #[must_use]
2252    pub fn new(router: Arc<Router>, session_state: SessionState) -> Self {
2253        Self {
2254            router,
2255            session_state,
2256        }
2257    }
2258}
2259
2260impl ToolCaller for RouterToolCaller {
2261    fn call_tool(
2262        &self,
2263        cx: &Cx,
2264        name: &str,
2265        args: serde_json::Value,
2266        depth: u32,
2267    ) -> Pin<
2268        Box<dyn std::future::Future<Output = fastmcp_core::McpResult<ToolCallResult>> + Send + '_>,
2269    > {
2270        // Check recursion depth
2271        if depth > MAX_TOOL_CALL_DEPTH {
2272            return Box::pin(async move {
2273                Err(McpError::new(
2274                    McpErrorCode::InternalError,
2275                    format!("Maximum tool call depth ({}) exceeded", MAX_TOOL_CALL_DEPTH),
2276                ))
2277            });
2278        }
2279
2280        // Clone what we need for the async block
2281        let cx = cx.clone();
2282        let name = name.to_string();
2283        let router = self.router.clone();
2284        let session_state = self.session_state.clone();
2285
2286        Box::pin(async move {
2287            debug!(target: targets::HANDLER, "Cross-component tool call: {} (depth: {})", name, depth);
2288
2289            // Find the tool handler
2290            let handler = router
2291                .tools
2292                .get(&name)
2293                .ok_or_else(|| McpError::method_not_found(&format!("tool: {}", name)))?;
2294
2295            // Validate arguments against the tool's input schema
2296            let tool_def = handler.definition();
2297
2298            // Use strict or lenient validation based on router configuration
2299            let validation_result = if router.strict_input_validation {
2300                validate_strict(&tool_def.input_schema, &args)
2301            } else {
2302                validate(&tool_def.input_schema, &args)
2303            };
2304
2305            if let Err(validation_errors) = validation_result {
2306                let error_messages: Vec<String> = validation_errors
2307                    .iter()
2308                    .map(|e| format!("{}: {}", e.path, e.message))
2309                    .collect();
2310                return Err(McpError::invalid_params(format!(
2311                    "Input validation failed: {}",
2312                    error_messages.join("; ")
2313                )));
2314            }
2315
2316            // Create a child context with incremented depth
2317            // Clone router again for nested calls
2318            let nested_router = router.clone();
2319            let nested_state = session_state.clone();
2320            let child_ctx = McpContext::with_state(cx.clone(), 0, session_state)
2321                .with_tool_call_depth(depth)
2322                .with_tool_caller(Arc::new(RouterToolCaller::new(
2323                    nested_router.clone(),
2324                    nested_state.clone(),
2325                )))
2326                .with_resource_reader(Arc::new(RouterResourceReader::new(
2327                    nested_router,
2328                    nested_state,
2329                )));
2330
2331            // Call the tool
2332            let outcome = block_on(handler.call_async(&child_ctx, args));
2333
2334            // Convert outcome to result
2335            match outcome {
2336                Outcome::Ok(content) => {
2337                    // Convert protocol Content to core ToolContentItem
2338                    let items: Vec<ToolContentItem> = content
2339                        .into_iter()
2340                        .map(|c| match c {
2341                            Content::Text { text } => ToolContentItem::Text { text },
2342                            Content::Image { data, mime_type } => {
2343                                ToolContentItem::Image { data, mime_type }
2344                            }
2345                            Content::Audio { data, mime_type } => {
2346                                ToolContentItem::Audio { data, mime_type }
2347                            }
2348                            Content::Resource { resource } => ToolContentItem::Resource {
2349                                uri: resource.uri,
2350                                mime_type: resource.mime_type,
2351                                text: resource.text,
2352                                blob: resource.blob,
2353                            },
2354                        })
2355                        .collect();
2356
2357                    Ok(ToolCallResult::success(items))
2358                }
2359                Outcome::Err(e) => {
2360                    // Tool errors become error results, not failures
2361                    Ok(ToolCallResult::error(e.message))
2362                }
2363                Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
2364                Outcome::Panicked(payload) => Err(McpError::internal_error(format!(
2365                    "Handler panic: {}",
2366                    payload.message()
2367                ))),
2368            }
2369        })
2370    }
2371}
2372
2373#[cfg(test)]
2374mod uri_template_tests {
2375    use super::{UriTemplate, UriTemplateError};
2376
2377    #[test]
2378    fn uri_template_matches_simple_param() {
2379        let matcher = UriTemplate::new("file://{path}");
2380        let params = matcher.matches("file://foo").expect("match");
2381        assert_eq!(params.get("path").map(String::as_str), Some("foo"));
2382    }
2383
2384    #[test]
2385    fn uri_template_allows_slash_in_trailing_param() {
2386        let matcher = UriTemplate::new("file://{path}");
2387        let params = matcher.matches("file://foo/bar").expect("match");
2388        assert_eq!(params.get("path").map(String::as_str), Some("foo/bar"));
2389    }
2390
2391    #[test]
2392    fn uri_template_matches_multiple_params() {
2393        let matcher = UriTemplate::new("db://{table}/{id}");
2394        let params = matcher.matches("db://users/42").expect("match");
2395        assert_eq!(params.get("table").map(String::as_str), Some("users"));
2396        assert_eq!(params.get("id").map(String::as_str), Some("42"));
2397    }
2398
2399    #[test]
2400    fn uri_template_rejects_extra_segments() {
2401        let matcher = UriTemplate::new("db://{table}/{id}");
2402        assert!(matcher.matches("db://users/42/extra").is_none());
2403    }
2404
2405    #[test]
2406    fn uri_template_rejects_extra_segments_with_literal_path() {
2407        let matcher = UriTemplate::new("db://{table}/items/{id}");
2408        let params = matcher.matches("db://users/items/42").expect("match");
2409        assert_eq!(params.get("table").map(String::as_str), Some("users"));
2410        assert_eq!(params.get("id").map(String::as_str), Some("42"));
2411        assert!(matcher.matches("db://users/items/42/extra").is_none());
2412    }
2413
2414    #[test]
2415    fn uri_template_decodes_percent_encoded_values() {
2416        let matcher = UriTemplate::new("file://{path}");
2417        let params = matcher.matches("file://foo%2Fbar").expect("match");
2418        assert_eq!(params.get("path").map(String::as_str), Some("foo/bar"));
2419    }
2420
2421    #[test]
2422    fn uri_template_supports_escaped_braces() {
2423        let matcher = UriTemplate::new("file://{{literal}}/{id}");
2424        let params = matcher.matches("file://{literal}/123").expect("match");
2425        assert_eq!(params.get("id").map(String::as_str), Some("123"));
2426    }
2427
2428    #[test]
2429    fn uri_template_rejects_empty_param() {
2430        let err = UriTemplate::parse("file://{}/x").unwrap_err();
2431        assert_eq!(err, UriTemplateError::EmptyParam);
2432    }
2433
2434    #[test]
2435    fn uri_template_rejects_unmatched_close() {
2436        let err = UriTemplate::parse("file://}x").unwrap_err();
2437        assert_eq!(err, UriTemplateError::UnmatchedClose);
2438    }
2439
2440    #[test]
2441    fn uri_template_rejects_duplicate_params() {
2442        let err = UriTemplate::parse("db://{id}/{id}").unwrap_err();
2443        assert_eq!(err, UriTemplateError::DuplicateParam("id".to_string()));
2444    }
2445
2446    #[test]
2447    fn uri_template_rejects_unclosed_param() {
2448        let err = UriTemplate::parse("file://{path").unwrap_err();
2449        assert_eq!(err, UriTemplateError::UnclosedParam);
2450    }
2451
2452    #[test]
2453    fn uri_template_specificity_literal_only() {
2454        let t = UriTemplate::new("file://exact/path");
2455        let (lit_len, lit_segs, total_segs) = t.specificity();
2456        assert_eq!(lit_len, "file://exact/path".len());
2457        assert_eq!(lit_segs, 1);
2458        assert_eq!(total_segs, 1);
2459    }
2460
2461    #[test]
2462    fn uri_template_specificity_with_params() {
2463        let t = UriTemplate::new("db://{table}/items/{id}");
2464        let (lit_len, lit_segs, total_segs) = t.specificity();
2465        assert_eq!(lit_len, "db://".len() + "/items/".len());
2466        assert_eq!(lit_segs, 2);
2467        assert_eq!(total_segs, 4); // "db://", {table}, "/items/", {id}
2468    }
2469
2470    #[test]
2471    fn uri_template_no_match_on_literal_mismatch() {
2472        let t = UriTemplate::new("file://exact");
2473        assert!(t.matches("file://other").is_none());
2474    }
2475
2476    #[test]
2477    fn uri_template_rejects_empty_param_value() {
2478        let t = UriTemplate::new("db://{table}/items/{id}");
2479        // table would be empty
2480        assert!(t.matches("db:///items/42").is_none());
2481    }
2482
2483    #[test]
2484    fn uri_template_debug_and_clone() {
2485        let t = UriTemplate::new("file://{path}");
2486        let debug = format!("{:?}", t);
2487        assert!(debug.contains("file://{path}"));
2488        let cloned = t.clone();
2489        assert!(cloned.matches("file://test").is_some());
2490    }
2491
2492    #[test]
2493    fn uri_template_escaped_close_brace() {
2494        let t = UriTemplate::new("file://{{a}}/{id}");
2495        let params = t.matches("file://{a}/42").expect("match");
2496        assert_eq!(params.get("id").map(String::as_str), Some("42"));
2497    }
2498
2499    #[test]
2500    fn uri_template_try_new_ok() {
2501        let t = UriTemplate::try_new("file://{path}");
2502        assert!(t.is_ok());
2503    }
2504
2505    #[test]
2506    fn uri_template_try_new_err() {
2507        let t = UriTemplate::try_new("file://{");
2508        assert!(t.is_err());
2509    }
2510
2511    #[test]
2512    fn uri_template_new_invalid_returns_non_matching() {
2513        // Invalid template: UriTemplate::new should log a warning and return
2514        // a template that never matches any URI (fail-safe).
2515        let t = UriTemplate::new("file://{");
2516        assert!(t.matches("file://anything").is_none());
2517        assert!(t.matches("").is_none());
2518    }
2519
2520    #[test]
2521    fn uri_template_literal_only_no_match_empty() {
2522        let t = UriTemplate::new("file://exact");
2523        assert!(t.matches("").is_none());
2524        assert!(t.matches("file://exact").is_some());
2525    }
2526
2527    #[test]
2528    fn uri_template_multiple_params_empty_last() {
2529        // Last param must not be empty
2530        let t = UriTemplate::new("db://{table}/{id}");
2531        assert!(t.matches("db://users/").is_none());
2532    }
2533
2534    #[test]
2535    fn uri_template_adjacent_params_not_supported() {
2536        // Two adjacent params (no literal between them) should fail to match
2537        let t = UriTemplate::new("{a}{b}");
2538        assert!(t.matches("xy").is_none());
2539    }
2540
2541    #[test]
2542    fn uri_template_escaped_double_close_brace() {
2543        // Escaped closing braces: }} -> }
2544        let t = UriTemplate::new("a}}b/{id}");
2545        let params = t.matches("a}b/42").expect("match");
2546        assert_eq!(params.get("id").map(String::as_str), Some("42"));
2547    }
2548
2549    #[test]
2550    fn uri_template_specificity_param_only() {
2551        let t = UriTemplate::new("{all}");
2552        let (lit_len, lit_segs, total_segs) = t.specificity();
2553        assert_eq!(lit_len, 0);
2554        assert_eq!(lit_segs, 0);
2555        assert_eq!(total_segs, 1);
2556    }
2557}
2558
2559#[cfg(test)]
2560mod percent_decode_tests {
2561    use super::{from_hex, percent_decode};
2562
2563    #[test]
2564    fn no_percent_passthrough() {
2565        assert_eq!(percent_decode("hello"), Some("hello".to_string()));
2566    }
2567
2568    #[test]
2569    fn basic_percent_decode() {
2570        assert_eq!(percent_decode("foo%20bar"), Some("foo bar".to_string()));
2571    }
2572
2573    #[test]
2574    fn truncated_percent_returns_none() {
2575        assert!(percent_decode("foo%2").is_none());
2576    }
2577
2578    #[test]
2579    fn invalid_hex_returns_none() {
2580        assert!(percent_decode("foo%GG").is_none());
2581    }
2582
2583    #[test]
2584    fn from_hex_digits() {
2585        assert_eq!(from_hex(b'0'), Some(0));
2586        assert_eq!(from_hex(b'9'), Some(9));
2587        assert_eq!(from_hex(b'a'), Some(10));
2588        assert_eq!(from_hex(b'f'), Some(15));
2589        assert_eq!(from_hex(b'A'), Some(10));
2590        assert_eq!(from_hex(b'F'), Some(15));
2591        assert_eq!(from_hex(b'G'), None);
2592    }
2593}
2594
2595#[cfg(test)]
2596mod cursor_tests {
2597    use super::{decode_cursor_offset, encode_cursor_offset};
2598
2599    #[test]
2600    fn roundtrip_zero() {
2601        let encoded = encode_cursor_offset(0);
2602        let decoded = decode_cursor_offset(Some(&encoded)).unwrap();
2603        assert_eq!(decoded, 0);
2604    }
2605
2606    #[test]
2607    fn roundtrip_large_offset() {
2608        let encoded = encode_cursor_offset(12345);
2609        let decoded = decode_cursor_offset(Some(&encoded)).unwrap();
2610        assert_eq!(decoded, 12345);
2611    }
2612
2613    #[test]
2614    fn none_cursor_returns_zero() {
2615        assert_eq!(decode_cursor_offset(None).unwrap(), 0);
2616    }
2617
2618    #[test]
2619    fn invalid_base64_returns_error() {
2620        let err = decode_cursor_offset(Some("not-valid-base64!!!")).unwrap_err();
2621        assert!(err.message.contains("base64"));
2622    }
2623
2624    #[test]
2625    fn valid_base64_but_not_json_returns_error() {
2626        let encoded =
2627            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b"not json");
2628        let err = decode_cursor_offset(Some(&encoded)).unwrap_err();
2629        assert!(err.message.contains("JSON"));
2630    }
2631
2632    #[test]
2633    fn valid_json_but_no_offset_returns_error() {
2634        let payload = serde_json::json!({"other": 1});
2635        let bytes = serde_json::to_vec(&payload).unwrap();
2636        let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
2637        let err = decode_cursor_offset(Some(&encoded)).unwrap_err();
2638        assert!(err.message.contains("offset"));
2639    }
2640}
2641
2642#[cfg(test)]
2643mod tag_filter_tests {
2644    use super::TagFilters;
2645
2646    #[test]
2647    fn no_filters_matches_anything() {
2648        let f = TagFilters::default();
2649        assert!(f.matches(&[]));
2650        assert!(f.matches(&["a".to_string()]));
2651    }
2652
2653    #[test]
2654    fn include_filter_requires_all_tags() {
2655        let include = vec!["a".to_string(), "b".to_string()];
2656        let f = TagFilters::new(Some(&include), None);
2657        assert!(f.matches(&["a".to_string(), "b".to_string(), "c".to_string()]));
2658        assert!(!f.matches(&["a".to_string()])); // missing "b"
2659    }
2660
2661    #[test]
2662    fn exclude_filter_rejects_any_tag() {
2663        let exclude = vec!["x".to_string()];
2664        let f = TagFilters::new(None, Some(&exclude));
2665        assert!(f.matches(&["a".to_string(), "b".to_string()]));
2666        assert!(!f.matches(&["a".to_string(), "x".to_string()]));
2667    }
2668
2669    #[test]
2670    fn include_and_exclude_combined() {
2671        let include = vec!["a".to_string()];
2672        let exclude = vec!["b".to_string()];
2673        let f = TagFilters::new(Some(&include), Some(&exclude));
2674        assert!(f.matches(&["a".to_string()]));
2675        assert!(!f.matches(&["a".to_string(), "b".to_string()])); // excluded
2676        assert!(!f.matches(&["c".to_string()])); // missing "a"
2677    }
2678
2679    #[test]
2680    fn case_insensitive_matching() {
2681        let include = vec!["Alpha".to_string()];
2682        let f = TagFilters::new(Some(&include), None);
2683        assert!(f.matches(&["alpha".to_string()]));
2684        assert!(f.matches(&["ALPHA".to_string()]));
2685    }
2686
2687    #[test]
2688    fn empty_include_array_passes_all() {
2689        let include: Vec<String> = vec![];
2690        let f = TagFilters::new(Some(&include), None);
2691        assert!(f.matches(&[]));
2692        assert!(f.matches(&["anything".to_string()]));
2693    }
2694
2695    #[test]
2696    fn tag_filters_debug() {
2697        let f = TagFilters::default();
2698        let debug = format!("{:?}", f);
2699        assert!(debug.contains("TagFilters"));
2700    }
2701}
2702
2703#[cfg(test)]
2704mod router_tests {
2705    use super::*;
2706    use crate::handler::{PromptHandler, ResourceHandler, ToolHandler};
2707    use fastmcp_core::{McpContext, McpResult, SessionState};
2708    use fastmcp_protocol::{Content, Prompt, PromptMessage, Resource, ResourceContent, Tool};
2709
2710    // ── Stub handlers ──────────────────────────────────────────────────
2711
2712    struct NamedTool {
2713        name: String,
2714        tags: Vec<String>,
2715    }
2716
2717    impl NamedTool {
2718        fn new(name: &str) -> Self {
2719            Self {
2720                name: name.to_string(),
2721                tags: vec![],
2722            }
2723        }
2724        fn with_tags(name: &str, tags: Vec<String>) -> Self {
2725            Self {
2726                name: name.to_string(),
2727                tags,
2728            }
2729        }
2730    }
2731
2732    impl ToolHandler for NamedTool {
2733        fn definition(&self) -> Tool {
2734            Tool {
2735                name: self.name.clone(),
2736                description: Some(format!("Tool {}", self.name)),
2737                input_schema: serde_json::json!({"type": "object"}),
2738                output_schema: None,
2739                icon: None,
2740                version: None,
2741                tags: self.tags.clone(),
2742                annotations: None,
2743            }
2744        }
2745        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2746            Ok(vec![Content::text(format!("called {}", self.name))])
2747        }
2748    }
2749
2750    struct NamedResource {
2751        uri: String,
2752        tags: Vec<String>,
2753    }
2754
2755    impl NamedResource {
2756        fn new(uri: &str) -> Self {
2757            Self {
2758                uri: uri.to_string(),
2759                tags: vec![],
2760            }
2761        }
2762        fn with_tags(uri: &str, tags: Vec<String>) -> Self {
2763            Self {
2764                uri: uri.to_string(),
2765                tags,
2766            }
2767        }
2768    }
2769
2770    impl ResourceHandler for NamedResource {
2771        fn definition(&self) -> Resource {
2772            Resource {
2773                uri: self.uri.clone(),
2774                name: self.uri.clone(),
2775                description: None,
2776                mime_type: Some("text/plain".to_string()),
2777                icon: None,
2778                version: None,
2779                tags: self.tags.clone(),
2780            }
2781        }
2782        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
2783            Ok(vec![ResourceContent {
2784                uri: self.uri.clone(),
2785                mime_type: Some("text/plain".to_string()),
2786                text: Some("content".to_string()),
2787                blob: None,
2788            }])
2789        }
2790    }
2791
2792    struct NamedPrompt {
2793        name: String,
2794        tags: Vec<String>,
2795    }
2796
2797    impl NamedPrompt {
2798        fn new(name: &str) -> Self {
2799            Self {
2800                name: name.to_string(),
2801                tags: vec![],
2802            }
2803        }
2804        fn with_tags(name: &str, tags: Vec<String>) -> Self {
2805            Self {
2806                name: name.to_string(),
2807                tags,
2808            }
2809        }
2810    }
2811
2812    impl PromptHandler for NamedPrompt {
2813        fn definition(&self) -> Prompt {
2814            Prompt {
2815                name: self.name.clone(),
2816                description: Some(format!("Prompt {}", self.name)),
2817                arguments: vec![],
2818                icon: None,
2819                version: None,
2820                tags: self.tags.clone(),
2821            }
2822        }
2823        fn get(
2824            &self,
2825            _ctx: &McpContext,
2826            _args: std::collections::HashMap<String, String>,
2827        ) -> McpResult<Vec<PromptMessage>> {
2828            Ok(vec![])
2829        }
2830    }
2831
2832    // ── Router::new ────────────────────────────────────────────────────
2833
2834    #[test]
2835    fn new_router_is_empty() {
2836        let r = Router::new();
2837        assert_eq!(r.tools_count(), 0);
2838        assert_eq!(r.resources_count(), 0);
2839        assert_eq!(r.resource_templates_count(), 0);
2840        assert_eq!(r.prompts_count(), 0);
2841        assert!(r.tools().is_empty());
2842        assert!(r.resources().is_empty());
2843        assert!(r.resource_templates().is_empty());
2844        assert!(r.prompts().is_empty());
2845    }
2846
2847    #[test]
2848    fn default_router_is_empty() {
2849        let r = Router::default();
2850        assert_eq!(r.tools_count(), 0);
2851    }
2852
2853    // ── add_tool / get_tool ────────────────────────────────────────────
2854
2855    #[test]
2856    fn add_and_get_tool() {
2857        let mut r = Router::new();
2858        r.add_tool(NamedTool::new("my_tool"));
2859        assert_eq!(r.tools_count(), 1);
2860        assert!(r.get_tool("my_tool").is_some());
2861        assert!(r.get_tool("other").is_none());
2862    }
2863
2864    #[test]
2865    fn add_tool_replace_on_duplicate() {
2866        let mut r = Router::new();
2867        r.add_tool(NamedTool::new("t"));
2868        r.add_tool(NamedTool::new("t"));
2869        assert_eq!(r.tools_count(), 1);
2870        // Order preserved (only one entry)
2871        assert_eq!(r.tools().len(), 1);
2872    }
2873
2874    #[test]
2875    fn tools_returns_definitions_in_order() {
2876        let mut r = Router::new();
2877        r.add_tool(NamedTool::new("b"));
2878        r.add_tool(NamedTool::new("a"));
2879        let names: Vec<_> = r.tools().iter().map(|t| t.name.clone()).collect();
2880        assert_eq!(names, vec!["b", "a"]); // insertion order
2881    }
2882
2883    // ── add_tool_with_behavior ─────────────────────────────────────────
2884
2885    #[test]
2886    fn add_tool_behavior_error_on_duplicate() {
2887        let mut r = Router::new();
2888        r.add_tool(NamedTool::new("t"));
2889        let err = r
2890            .add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Error)
2891            .unwrap_err();
2892        assert!(err.message.contains("already exists"));
2893    }
2894
2895    #[test]
2896    fn add_tool_behavior_warn_keeps_original() {
2897        let mut r = Router::new();
2898        r.add_tool(NamedTool::new("t"));
2899        r.add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Warn)
2900            .unwrap();
2901        assert_eq!(r.tools_count(), 1);
2902    }
2903
2904    #[test]
2905    fn add_tool_behavior_replace() {
2906        let mut r = Router::new();
2907        r.add_tool(NamedTool::new("t"));
2908        r.add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Replace)
2909            .unwrap();
2910        assert_eq!(r.tools_count(), 1);
2911    }
2912
2913    #[test]
2914    fn add_tool_behavior_ignore() {
2915        let mut r = Router::new();
2916        r.add_tool(NamedTool::new("t"));
2917        r.add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Ignore)
2918            .unwrap();
2919        assert_eq!(r.tools_count(), 1);
2920    }
2921
2922    #[test]
2923    fn add_tool_behavior_new_tool_ok() {
2924        let mut r = Router::new();
2925        r.add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Error)
2926            .unwrap();
2927        assert_eq!(r.tools_count(), 1);
2928    }
2929
2930    // ── add_resource / get_resource ────────────────────────────────────
2931
2932    #[test]
2933    fn add_and_get_resource() {
2934        let mut r = Router::new();
2935        r.add_resource(NamedResource::new("file:///a.txt"));
2936        assert_eq!(r.resources_count(), 1);
2937        assert!(r.get_resource("file:///a.txt").is_some());
2938        assert!(r.get_resource("file:///b.txt").is_none());
2939    }
2940
2941    #[test]
2942    fn resources_returns_definitions_in_order() {
2943        let mut r = Router::new();
2944        r.add_resource(NamedResource::new("file:///b"));
2945        r.add_resource(NamedResource::new("file:///a"));
2946        let uris: Vec<_> = r.resources().iter().map(|res| res.uri.clone()).collect();
2947        assert_eq!(uris, vec!["file:///b", "file:///a"]);
2948    }
2949
2950    // ── add_resource_with_behavior ─────────────────────────────────────
2951
2952    #[test]
2953    fn add_resource_behavior_error_on_duplicate() {
2954        let mut r = Router::new();
2955        r.add_resource(NamedResource::new("file:///a"));
2956        let err = r
2957            .add_resource_with_behavior(
2958                NamedResource::new("file:///a"),
2959                crate::DuplicateBehavior::Error,
2960            )
2961            .unwrap_err();
2962        assert!(err.message.contains("already exists"));
2963    }
2964
2965    #[test]
2966    fn add_resource_behavior_ignore() {
2967        let mut r = Router::new();
2968        r.add_resource(NamedResource::new("file:///a"));
2969        r.add_resource_with_behavior(
2970            NamedResource::new("file:///a"),
2971            crate::DuplicateBehavior::Ignore,
2972        )
2973        .unwrap();
2974        assert_eq!(r.resources_count(), 1);
2975    }
2976
2977    // ── add_prompt / get_prompt ────────────────────────────────────────
2978
2979    #[test]
2980    fn add_and_get_prompt() {
2981        let mut r = Router::new();
2982        r.add_prompt(NamedPrompt::new("greet"));
2983        assert_eq!(r.prompts_count(), 1);
2984        assert!(r.get_prompt("greet").is_some());
2985        assert!(r.get_prompt("other").is_none());
2986    }
2987
2988    #[test]
2989    fn prompts_returns_definitions_in_order() {
2990        let mut r = Router::new();
2991        r.add_prompt(NamedPrompt::new("z"));
2992        r.add_prompt(NamedPrompt::new("a"));
2993        let names: Vec<_> = r.prompts().iter().map(|p| p.name.clone()).collect();
2994        assert_eq!(names, vec!["z", "a"]);
2995    }
2996
2997    // ── add_prompt_with_behavior ───────────────────────────────────────
2998
2999    #[test]
3000    fn add_prompt_behavior_error_on_duplicate() {
3001        let mut r = Router::new();
3002        r.add_prompt(NamedPrompt::new("p"));
3003        let err = r
3004            .add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Error)
3005            .unwrap_err();
3006        assert!(err.message.contains("already exists"));
3007    }
3008
3009    #[test]
3010    fn add_prompt_behavior_warn_keeps_original() {
3011        let mut r = Router::new();
3012        r.add_prompt(NamedPrompt::new("p"));
3013        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Warn)
3014            .unwrap();
3015        assert_eq!(r.prompts_count(), 1);
3016    }
3017
3018    // ── add_resource_template ──────────────────────────────────────────
3019
3020    #[test]
3021    fn add_resource_template_and_list() {
3022        let mut r = Router::new();
3023        let tmpl = ResourceTemplate {
3024            uri_template: "db://{table}".to_string(),
3025            name: "db".to_string(),
3026            description: None,
3027            mime_type: None,
3028            icon: None,
3029            version: None,
3030            tags: vec![],
3031        };
3032        r.add_resource_template(tmpl);
3033        assert_eq!(r.resource_templates_count(), 1);
3034        assert!(r.get_resource_template("db://{table}").is_some());
3035        assert!(r.get_resource_template("db://{other}").is_none());
3036    }
3037
3038    #[test]
3039    fn add_resource_template_replaces_existing() {
3040        let mut r = Router::new();
3041        let tmpl1 = ResourceTemplate {
3042            uri_template: "db://{table}".to_string(),
3043            name: "db1".to_string(),
3044            description: None,
3045            mime_type: None,
3046            icon: None,
3047            version: None,
3048            tags: vec![],
3049        };
3050        let tmpl2 = ResourceTemplate {
3051            uri_template: "db://{table}".to_string(),
3052            name: "db2".to_string(),
3053            description: None,
3054            mime_type: None,
3055            icon: None,
3056            version: None,
3057            tags: vec![],
3058        };
3059        r.add_resource_template(tmpl1);
3060        r.add_resource_template(tmpl2);
3061        assert_eq!(r.resource_templates_count(), 1);
3062        let tmpl = r.get_resource_template("db://{table}").unwrap();
3063        assert_eq!(tmpl.name, "db2");
3064    }
3065
3066    // ── resource_exists / resolve_resource ──────────────────────────────
3067
3068    #[test]
3069    fn resource_exists_for_static_resource() {
3070        let mut r = Router::new();
3071        r.add_resource(NamedResource::new("file:///a.txt"));
3072        assert!(r.resource_exists("file:///a.txt"));
3073        assert!(!r.resource_exists("file:///b.txt"));
3074    }
3075
3076    // ── strict_input_validation ────────────────────────────────────────
3077
3078    #[test]
3079    fn strict_input_validation_default_off() {
3080        let r = Router::new();
3081        assert!(!r.strict_input_validation());
3082    }
3083
3084    #[test]
3085    fn set_strict_input_validation() {
3086        let mut r = Router::new();
3087        r.set_strict_input_validation(true);
3088        assert!(r.strict_input_validation());
3089        r.set_strict_input_validation(false);
3090        assert!(!r.strict_input_validation());
3091    }
3092
3093    // ── set_list_page_size ─────────────────────────────────────────────
3094
3095    #[test]
3096    fn set_list_page_size_zero_treated_as_none() {
3097        let mut r = Router::new();
3098        r.set_list_page_size(Some(0));
3099        // Zero page size is filtered to None
3100        assert!(r.list_page_size.is_none());
3101    }
3102
3103    #[test]
3104    fn set_list_page_size_positive() {
3105        let mut r = Router::new();
3106        r.set_list_page_size(Some(10));
3107        assert_eq!(r.list_page_size, Some(10));
3108    }
3109
3110    #[test]
3111    fn set_list_page_size_none() {
3112        let mut r = Router::new();
3113        r.set_list_page_size(Some(10));
3114        r.set_list_page_size(None);
3115        assert!(r.list_page_size.is_none());
3116    }
3117
3118    // ── tools_filtered ─────────────────────────────────────────────────
3119
3120    #[test]
3121    fn tools_filtered_no_filters_returns_all() {
3122        let mut r = Router::new();
3123        r.add_tool(NamedTool::new("a"));
3124        r.add_tool(NamedTool::new("b"));
3125        let tools = r.tools_filtered(None, None);
3126        assert_eq!(tools.len(), 2);
3127    }
3128
3129    #[test]
3130    fn tools_filtered_by_session_state_disables() {
3131        let mut r = Router::new();
3132        r.add_tool(NamedTool::new("a"));
3133        r.add_tool(NamedTool::new("b"));
3134        let state = SessionState::new();
3135        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
3136        state.set("fastmcp.disabled_tools", &disabled);
3137        let tools = r.tools_filtered(Some(&state), None);
3138        assert_eq!(tools.len(), 1);
3139        assert_eq!(tools[0].name, "b");
3140    }
3141
3142    #[test]
3143    fn tools_filtered_by_tags() {
3144        let mut r = Router::new();
3145        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]));
3146        r.add_tool(NamedTool::with_tags("b", vec!["web".to_string()]));
3147        let include = vec!["db".to_string()];
3148        let filters = TagFilters::new(Some(&include), None);
3149        let tools = r.tools_filtered(None, Some(&filters));
3150        assert_eq!(tools.len(), 1);
3151        assert_eq!(tools[0].name, "a");
3152    }
3153
3154    // ── resources_filtered ─────────────────────────────────────────────
3155
3156    #[test]
3157    fn resources_filtered_by_session_state() {
3158        let mut r = Router::new();
3159        r.add_resource(NamedResource::new("file:///a"));
3160        r.add_resource(NamedResource::new("file:///b"));
3161        let state = SessionState::new();
3162        let disabled: std::collections::HashSet<String> =
3163            ["file:///a".to_string()].into_iter().collect();
3164        state.set("fastmcp.disabled_resources", &disabled);
3165        let res = r.resources_filtered(Some(&state), None);
3166        assert_eq!(res.len(), 1);
3167        assert_eq!(res[0].uri, "file:///b");
3168    }
3169
3170    // ── prompts_filtered ───────────────────────────────────────────────
3171
3172    #[test]
3173    fn prompts_filtered_by_session_state() {
3174        let mut r = Router::new();
3175        r.add_prompt(NamedPrompt::new("a"));
3176        r.add_prompt(NamedPrompt::new("b"));
3177        let state = SessionState::new();
3178        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
3179        state.set("fastmcp.disabled_prompts", &disabled);
3180        let prompts = r.prompts_filtered(Some(&state), None);
3181        assert_eq!(prompts.len(), 1);
3182        assert_eq!(prompts[0].name, "b");
3183    }
3184
3185    #[test]
3186    fn prompts_filtered_by_tags() {
3187        let mut r = Router::new();
3188        r.add_prompt(NamedPrompt::with_tags("a", vec!["internal".to_string()]));
3189        r.add_prompt(NamedPrompt::with_tags("b", vec!["public".to_string()]));
3190        let exclude = vec!["internal".to_string()];
3191        let filters = TagFilters::new(None, Some(&exclude));
3192        let prompts = r.prompts_filtered(None, Some(&filters));
3193        assert_eq!(prompts.len(), 1);
3194        assert_eq!(prompts[0].name, "b");
3195    }
3196
3197    // ── resource_templates_filtered ────────────────────────────────────
3198
3199    #[test]
3200    fn resource_templates_filtered_by_session_state() {
3201        let mut r = Router::new();
3202        r.add_resource_template(ResourceTemplate {
3203            uri_template: "db://{table}".to_string(),
3204            name: "db".to_string(),
3205            description: None,
3206            mime_type: None,
3207            icon: None,
3208            version: None,
3209            tags: vec!["admin".to_string()],
3210        });
3211        r.add_resource_template(ResourceTemplate {
3212            uri_template: "cache://{key}".to_string(),
3213            name: "cache".to_string(),
3214            description: None,
3215            mime_type: None,
3216            icon: None,
3217            version: None,
3218            tags: vec![],
3219        });
3220        let state = SessionState::new();
3221        let disabled: std::collections::HashSet<String> =
3222            ["db://{table}".to_string()].into_iter().collect();
3223        state.set("fastmcp.disabled_resources", &disabled);
3224        let tmpls = r.resource_templates_filtered(Some(&state), None);
3225        assert_eq!(tmpls.len(), 1);
3226        assert_eq!(tmpls[0].name, "cache");
3227    }
3228
3229    // ── apply_prefix / validate_prefix ─────────────────────────────────
3230
3231    #[test]
3232    fn apply_prefix_with_prefix() {
3233        assert_eq!(Router::apply_prefix("tool", Some("ns")), "ns/tool");
3234    }
3235
3236    #[test]
3237    fn apply_prefix_no_prefix() {
3238        assert_eq!(Router::apply_prefix("tool", None), "tool");
3239    }
3240
3241    #[test]
3242    fn apply_prefix_empty_prefix() {
3243        assert_eq!(Router::apply_prefix("tool", Some("")), "tool");
3244    }
3245
3246    #[test]
3247    fn validate_prefix_valid() {
3248        assert!(Router::validate_prefix("my-prefix_1").is_ok());
3249    }
3250
3251    #[test]
3252    fn validate_prefix_empty_is_ok() {
3253        assert!(Router::validate_prefix("").is_ok());
3254    }
3255
3256    #[test]
3257    fn validate_prefix_rejects_slashes() {
3258        let err = Router::validate_prefix("a/b").unwrap_err();
3259        assert!(err.contains("slashes"));
3260    }
3261
3262    #[test]
3263    fn validate_prefix_rejects_special_chars() {
3264        let err = Router::validate_prefix("a@b").unwrap_err();
3265        assert!(err.contains("invalid character"));
3266    }
3267
3268    // ── MountResult ────────────────────────────────────────────────────
3269
3270    #[test]
3271    fn mount_result_default_has_no_components() {
3272        let r = MountResult::default();
3273        assert!(!r.has_components());
3274        assert!(r.is_success());
3275    }
3276
3277    #[test]
3278    fn mount_result_with_tools_has_components() {
3279        let mut r = MountResult::default();
3280        r.tools = 1;
3281        assert!(r.has_components());
3282    }
3283
3284    #[test]
3285    fn mount_result_debug() {
3286        let r = MountResult::default();
3287        let debug = format!("{:?}", r);
3288        assert!(debug.contains("MountResult"));
3289    }
3290
3291    // ── mount ──────────────────────────────────────────────────────────
3292
3293    #[test]
3294    fn mount_tools_with_prefix() {
3295        let mut main = Router::new();
3296        let mut sub = Router::new();
3297        sub.add_tool(NamedTool::new("query"));
3298        let result = main.mount(sub, Some("db"));
3299        assert_eq!(result.tools, 1);
3300        assert!(main.get_tool("db/query").is_some());
3301        assert!(main.get_tool("query").is_none());
3302    }
3303
3304    #[test]
3305    fn mount_without_prefix() {
3306        let mut main = Router::new();
3307        let mut sub = Router::new();
3308        sub.add_tool(NamedTool::new("query"));
3309        let result = main.mount(sub, None);
3310        assert_eq!(result.tools, 1);
3311        assert!(main.get_tool("query").is_some());
3312    }
3313
3314    #[test]
3315    fn mount_resources_with_prefix() {
3316        let mut main = Router::new();
3317        let mut sub = Router::new();
3318        sub.add_resource(NamedResource::new("file:///a"));
3319        let result = main.mount(sub, Some("ns"));
3320        assert_eq!(result.resources, 1);
3321        assert!(main.get_resource("ns/file:///a").is_some());
3322    }
3323
3324    #[test]
3325    fn mount_prompts_with_prefix() {
3326        let mut main = Router::new();
3327        let mut sub = Router::new();
3328        sub.add_prompt(NamedPrompt::new("greet"));
3329        let result = main.mount(sub, Some("ns"));
3330        assert_eq!(result.prompts, 1);
3331        assert!(main.get_prompt("ns/greet").is_some());
3332    }
3333
3334    #[test]
3335    fn mount_warns_on_conflict() {
3336        let mut main = Router::new();
3337        main.add_tool(NamedTool::new("t"));
3338        let mut sub = Router::new();
3339        sub.add_tool(NamedTool::new("t"));
3340        let result = main.mount(sub, None);
3341        assert_eq!(result.tools, 1);
3342        assert!(!result.warnings.is_empty());
3343        assert!(result.warnings[0].contains("already exists"));
3344    }
3345
3346    #[test]
3347    fn mount_warns_on_invalid_prefix() {
3348        let mut main = Router::new();
3349        let sub = Router::new();
3350        let result = main.mount(sub, Some("bad/prefix"));
3351        assert!(!result.warnings.is_empty());
3352        assert!(result.warnings[0].contains("slashes"));
3353    }
3354
3355    // ── mount_tools / mount_resources / mount_prompts ──────────────────
3356
3357    #[test]
3358    fn mount_tools_only() {
3359        let mut main = Router::new();
3360        let mut sub = Router::new();
3361        sub.add_tool(NamedTool::new("t1"));
3362        sub.add_prompt(NamedPrompt::new("p1"));
3363        let result = main.mount_tools(sub, Some("ns"));
3364        assert_eq!(result.tools, 1);
3365        assert!(main.get_tool("ns/t1").is_some());
3366        assert_eq!(main.prompts_count(), 0); // prompts not mounted
3367    }
3368
3369    #[test]
3370    fn mount_prompts_only() {
3371        let mut main = Router::new();
3372        let mut sub = Router::new();
3373        sub.add_tool(NamedTool::new("t1"));
3374        sub.add_prompt(NamedPrompt::new("p1"));
3375        let result = main.mount_prompts(sub, Some("ns"));
3376        assert_eq!(result.prompts, 1);
3377        assert!(main.get_prompt("ns/p1").is_some());
3378        assert_eq!(main.tools_count(), 0); // tools not mounted
3379    }
3380
3381    // ── handle_tools_list pagination ───────────────────────────────────
3382
3383    #[test]
3384    fn handle_tools_list_no_pagination() {
3385        let mut r = Router::new();
3386        r.add_tool(NamedTool::new("a"));
3387        r.add_tool(NamedTool::new("b"));
3388        let cx = Cx::for_testing();
3389        let params = ListToolsParams {
3390            cursor: None,
3391            include_tags: None,
3392            exclude_tags: None,
3393        };
3394        let result = r.handle_tools_list(&cx, params, None).unwrap();
3395        assert_eq!(result.tools.len(), 2);
3396        assert!(result.next_cursor.is_none());
3397    }
3398
3399    #[test]
3400    fn handle_tools_list_with_pagination() {
3401        let mut r = Router::new();
3402        r.set_list_page_size(Some(1));
3403        r.add_tool(NamedTool::new("a"));
3404        r.add_tool(NamedTool::new("b"));
3405        let cx = Cx::for_testing();
3406
3407        // First page
3408        let params = ListToolsParams {
3409            cursor: None,
3410            include_tags: None,
3411            exclude_tags: None,
3412        };
3413        let result = r.handle_tools_list(&cx, params, None).unwrap();
3414        assert_eq!(result.tools.len(), 1);
3415        assert_eq!(result.tools[0].name, "a");
3416        assert!(result.next_cursor.is_some());
3417
3418        // Second page
3419        let params = ListToolsParams {
3420            cursor: result.next_cursor,
3421            include_tags: None,
3422            exclude_tags: None,
3423        };
3424        let result = r.handle_tools_list(&cx, params, None).unwrap();
3425        assert_eq!(result.tools.len(), 1);
3426        assert_eq!(result.tools[0].name, "b");
3427        assert!(result.next_cursor.is_none());
3428    }
3429
3430    #[test]
3431    fn handle_tools_list_with_tag_filter() {
3432        let mut r = Router::new();
3433        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]));
3434        r.add_tool(NamedTool::with_tags("b", vec!["web".to_string()]));
3435        let cx = Cx::for_testing();
3436        let params = ListToolsParams {
3437            cursor: None,
3438            include_tags: Some(vec!["db".to_string()]),
3439            exclude_tags: None,
3440        };
3441        let result = r.handle_tools_list(&cx, params, None).unwrap();
3442        assert_eq!(result.tools.len(), 1);
3443        assert_eq!(result.tools[0].name, "a");
3444    }
3445
3446    // ── handle_resources_list pagination ───────────────────────────────
3447
3448    #[test]
3449    fn handle_resources_list_no_pagination() {
3450        let mut r = Router::new();
3451        r.add_resource(NamedResource::new("file:///a"));
3452        let cx = Cx::for_testing();
3453        let params = ListResourcesParams {
3454            cursor: None,
3455            include_tags: None,
3456            exclude_tags: None,
3457        };
3458        let result = r.handle_resources_list(&cx, params, None).unwrap();
3459        assert_eq!(result.resources.len(), 1);
3460        assert!(result.next_cursor.is_none());
3461    }
3462
3463    #[test]
3464    fn handle_resources_list_with_pagination() {
3465        let mut r = Router::new();
3466        r.set_list_page_size(Some(1));
3467        r.add_resource(NamedResource::new("file:///a"));
3468        r.add_resource(NamedResource::new("file:///b"));
3469        let cx = Cx::for_testing();
3470        let params = ListResourcesParams {
3471            cursor: None,
3472            include_tags: None,
3473            exclude_tags: None,
3474        };
3475        let result = r.handle_resources_list(&cx, params, None).unwrap();
3476        assert_eq!(result.resources.len(), 1);
3477        assert!(result.next_cursor.is_some());
3478    }
3479
3480    // ── handle_prompts_list pagination ─────────────────────────────────
3481
3482    #[test]
3483    fn handle_prompts_list_no_pagination() {
3484        let mut r = Router::new();
3485        r.add_prompt(NamedPrompt::new("greet"));
3486        let cx = Cx::for_testing();
3487        let params = ListPromptsParams {
3488            cursor: None,
3489            include_tags: None,
3490            exclude_tags: None,
3491        };
3492        let result = r.handle_prompts_list(&cx, params, None).unwrap();
3493        assert_eq!(result.prompts.len(), 1);
3494        assert!(result.next_cursor.is_none());
3495    }
3496
3497    // ── handle_resource_templates_list ──────────────────────────────────
3498
3499    #[test]
3500    fn handle_resource_templates_list_no_pagination() {
3501        let mut r = Router::new();
3502        r.add_resource_template(ResourceTemplate {
3503            uri_template: "db://{table}".to_string(),
3504            name: "db".to_string(),
3505            description: None,
3506            mime_type: None,
3507            icon: None,
3508            version: None,
3509            tags: vec![],
3510        });
3511        let cx = Cx::for_testing();
3512        let params = ListResourceTemplatesParams {
3513            cursor: None,
3514            include_tags: None,
3515            exclude_tags: None,
3516        };
3517        let result = r.handle_resource_templates_list(&cx, params, None).unwrap();
3518        assert_eq!(result.resource_templates.len(), 1);
3519        assert!(result.next_cursor.is_none());
3520    }
3521
3522    // ── handle_initialize ──────────────────────────────────────────────
3523
3524    #[test]
3525    fn handle_initialize_returns_protocol_version() {
3526        let r = Router::new();
3527        let cx = Cx::for_testing();
3528        let mut session = Session::new(
3529            fastmcp_protocol::ServerInfo {
3530                name: "test".to_string(),
3531                version: "1.0".to_string(),
3532            },
3533            fastmcp_protocol::ServerCapabilities::default(),
3534        );
3535        let params = InitializeParams {
3536            protocol_version: PROTOCOL_VERSION.to_string(),
3537            capabilities: fastmcp_protocol::ClientCapabilities::default(),
3538            client_info: fastmcp_protocol::ClientInfo {
3539                name: "test-client".to_string(),
3540                version: "1.0".to_string(),
3541            },
3542        };
3543        let result = r
3544            .handle_initialize(&cx, &mut session, params, Some("test instructions"))
3545            .unwrap();
3546        assert_eq!(result.protocol_version, PROTOCOL_VERSION);
3547        assert_eq!(result.server_info.name, "test");
3548        assert_eq!(result.instructions.as_deref(), Some("test instructions"));
3549    }
3550
3551    #[test]
3552    fn handle_initialize_no_instructions() {
3553        let r = Router::new();
3554        let cx = Cx::for_testing();
3555        let mut session = Session::new(
3556            fastmcp_protocol::ServerInfo {
3557                name: "srv".to_string(),
3558                version: "0.1".to_string(),
3559            },
3560            fastmcp_protocol::ServerCapabilities::default(),
3561        );
3562        let params = InitializeParams {
3563            protocol_version: PROTOCOL_VERSION.to_string(),
3564            capabilities: fastmcp_protocol::ClientCapabilities::default(),
3565            client_info: fastmcp_protocol::ClientInfo {
3566                name: "c".to_string(),
3567                version: "0.1".to_string(),
3568            },
3569        };
3570        let result = r
3571            .handle_initialize(&cx, &mut session, params, None)
3572            .unwrap();
3573        assert!(result.instructions.is_none());
3574    }
3575
3576    // ── handle_tasks_list/get/cancel/submit without manager ────────────
3577
3578    #[test]
3579    fn handle_tasks_list_no_manager_errors() {
3580        let r = Router::new();
3581        let cx = Cx::for_testing();
3582        let params = ListTasksParams {
3583            cursor: None,
3584            status: None,
3585            limit: None,
3586        };
3587        let err = r.handle_tasks_list(&cx, params, None).unwrap_err();
3588        assert!(err.message.contains("not enabled"));
3589    }
3590
3591    #[test]
3592    fn handle_tasks_get_no_manager_errors() {
3593        let r = Router::new();
3594        let cx = Cx::for_testing();
3595        let params = GetTaskParams {
3596            id: fastmcp_protocol::TaskId("test-id".to_string()),
3597        };
3598        let err = r.handle_tasks_get(&cx, params, None).unwrap_err();
3599        assert!(err.message.contains("not enabled"));
3600    }
3601
3602    #[test]
3603    fn handle_tasks_cancel_no_manager_errors() {
3604        let r = Router::new();
3605        let cx = Cx::for_testing();
3606        let params = CancelTaskParams {
3607            id: fastmcp_protocol::TaskId("test-id".to_string()),
3608            reason: None,
3609        };
3610        let err = r.handle_tasks_cancel(&cx, params, None).unwrap_err();
3611        assert!(err.message.contains("not enabled"));
3612    }
3613
3614    #[test]
3615    fn handle_tasks_submit_no_manager_errors() {
3616        let r = Router::new();
3617        let cx = Cx::for_testing();
3618        let params = SubmitTaskParams {
3619            task_type: "test".to_string(),
3620            params: None,
3621        };
3622        let err = r.handle_tasks_submit(&cx, params, None).unwrap_err();
3623        assert!(err.message.contains("not enabled"));
3624    }
3625
3626    // ── add_resource_with_behavior (Warn / Replace) ─────────────────────
3627
3628    #[test]
3629    fn add_resource_behavior_warn_keeps_original() {
3630        let mut r = Router::new();
3631        r.add_resource(NamedResource::new("file:///a"));
3632        r.add_resource_with_behavior(
3633            NamedResource::new("file:///a"),
3634            crate::DuplicateBehavior::Warn,
3635        )
3636        .unwrap();
3637        assert_eq!(r.resources_count(), 1);
3638    }
3639
3640    #[test]
3641    fn add_resource_behavior_replace() {
3642        let mut r = Router::new();
3643        r.add_resource(NamedResource::new("file:///a"));
3644        r.add_resource_with_behavior(
3645            NamedResource::new("file:///a"),
3646            crate::DuplicateBehavior::Replace,
3647        )
3648        .unwrap();
3649        assert_eq!(r.resources_count(), 1);
3650    }
3651
3652    #[test]
3653    fn add_resource_behavior_new_resource_ok() {
3654        let mut r = Router::new();
3655        r.add_resource_with_behavior(
3656            NamedResource::new("file:///a"),
3657            crate::DuplicateBehavior::Error,
3658        )
3659        .unwrap();
3660        assert_eq!(r.resources_count(), 1);
3661    }
3662
3663    // ── add_prompt_with_behavior (Replace / Ignore / new) ───────────────
3664
3665    #[test]
3666    fn add_prompt_behavior_replace() {
3667        let mut r = Router::new();
3668        r.add_prompt(NamedPrompt::new("p"));
3669        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Replace)
3670            .unwrap();
3671        assert_eq!(r.prompts_count(), 1);
3672    }
3673
3674    #[test]
3675    fn add_prompt_behavior_ignore() {
3676        let mut r = Router::new();
3677        r.add_prompt(NamedPrompt::new("p"));
3678        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Ignore)
3679            .unwrap();
3680        assert_eq!(r.prompts_count(), 1);
3681    }
3682
3683    #[test]
3684    fn add_prompt_behavior_new_prompt_ok() {
3685        let mut r = Router::new();
3686        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Error)
3687            .unwrap();
3688        assert_eq!(r.prompts_count(), 1);
3689    }
3690
3691    // ── add_resource / add_prompt duplicate replace ─────────────────────
3692
3693    #[test]
3694    fn add_resource_replaces_on_duplicate() {
3695        let mut r = Router::new();
3696        r.add_resource(NamedResource::new("file:///a"));
3697        r.add_resource(NamedResource::new("file:///a"));
3698        assert_eq!(r.resources_count(), 1);
3699        assert_eq!(r.resources().len(), 1);
3700    }
3701
3702    #[test]
3703    fn add_prompt_replaces_on_duplicate() {
3704        let mut r = Router::new();
3705        r.add_prompt(NamedPrompt::new("p"));
3706        r.add_prompt(NamedPrompt::new("p"));
3707        assert_eq!(r.prompts_count(), 1);
3708        assert_eq!(r.prompts().len(), 1);
3709    }
3710
3711    // ── resource_exists for template match ──────────────────────────────
3712
3713    #[test]
3714    fn resource_exists_for_template_match() {
3715        struct DbResource;
3716        impl ResourceHandler for DbResource {
3717            fn definition(&self) -> Resource {
3718                Resource {
3719                    uri: "db://placeholder".to_string(),
3720                    name: "db".to_string(),
3721                    description: None,
3722                    mime_type: Some("text/plain".to_string()),
3723                    icon: None,
3724                    version: None,
3725                    tags: vec![],
3726                }
3727            }
3728            fn template(&self) -> Option<ResourceTemplate> {
3729                Some(ResourceTemplate {
3730                    uri_template: "db://{table}".to_string(),
3731                    name: "db".to_string(),
3732                    description: None,
3733                    mime_type: None,
3734                    icon: None,
3735                    version: None,
3736                    tags: vec![],
3737                })
3738            }
3739            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<fastmcp_protocol::ResourceContent>> {
3740                Ok(vec![])
3741            }
3742        }
3743        let mut r = Router::new();
3744        r.add_resource(DbResource);
3745        assert!(r.resource_exists("db://users"));
3746        assert!(!r.resource_exists("file://other"));
3747    }
3748
3749    // ── resources_filtered by tags ──────────────────────────────────────
3750
3751    #[test]
3752    fn resources_filtered_by_tags() {
3753        let mut r = Router::new();
3754        r.add_resource(NamedResource::with_tags(
3755            "file:///a",
3756            vec!["internal".to_string()],
3757        ));
3758        r.add_resource(NamedResource::with_tags(
3759            "file:///b",
3760            vec!["public".to_string()],
3761        ));
3762        let include = vec!["public".to_string()];
3763        let filters = TagFilters::new(Some(&include), None);
3764        let res = r.resources_filtered(None, Some(&filters));
3765        assert_eq!(res.len(), 1);
3766        assert_eq!(res[0].uri, "file:///b");
3767    }
3768
3769    // ── resource_templates_filtered by tags ─────────────────────────────
3770
3771    #[test]
3772    fn resource_templates_filtered_by_tags() {
3773        let mut r = Router::new();
3774        r.add_resource_template(ResourceTemplate {
3775            uri_template: "db://{table}".to_string(),
3776            name: "db".to_string(),
3777            description: None,
3778            mime_type: None,
3779            icon: None,
3780            version: None,
3781            tags: vec!["admin".to_string()],
3782        });
3783        r.add_resource_template(ResourceTemplate {
3784            uri_template: "cache://{key}".to_string(),
3785            name: "cache".to_string(),
3786            description: None,
3787            mime_type: None,
3788            icon: None,
3789            version: None,
3790            tags: vec!["public".to_string()],
3791        });
3792        let exclude = vec!["admin".to_string()];
3793        let filters = TagFilters::new(None, Some(&exclude));
3794        let tmpls = r.resource_templates_filtered(None, Some(&filters));
3795        assert_eq!(tmpls.len(), 1);
3796        assert_eq!(tmpls[0].name, "cache");
3797    }
3798
3799    // ── handle_tools_list with session state ────────────────────────────
3800
3801    #[test]
3802    fn handle_tools_list_with_session_state_filter() {
3803        let mut r = Router::new();
3804        r.add_tool(NamedTool::new("a"));
3805        r.add_tool(NamedTool::new("b"));
3806        let cx = Cx::for_testing();
3807        let state = SessionState::new();
3808        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
3809        state.set("fastmcp.disabled_tools", &disabled);
3810        let params = ListToolsParams {
3811            cursor: None,
3812            include_tags: None,
3813            exclude_tags: None,
3814        };
3815        let result = r.handle_tools_list(&cx, params, Some(&state)).unwrap();
3816        assert_eq!(result.tools.len(), 1);
3817        assert_eq!(result.tools[0].name, "b");
3818    }
3819
3820    // ── handle_resources_list with tag filter ────────────────────────────
3821
3822    #[test]
3823    fn handle_resources_list_with_tag_filter() {
3824        let mut r = Router::new();
3825        r.add_resource(NamedResource::with_tags(
3826            "file:///a",
3827            vec!["db".to_string()],
3828        ));
3829        r.add_resource(NamedResource::with_tags(
3830            "file:///b",
3831            vec!["web".to_string()],
3832        ));
3833        let cx = Cx::for_testing();
3834        let params = ListResourcesParams {
3835            cursor: None,
3836            include_tags: Some(vec!["web".to_string()]),
3837            exclude_tags: None,
3838        };
3839        let result = r.handle_resources_list(&cx, params, None).unwrap();
3840        assert_eq!(result.resources.len(), 1);
3841        assert_eq!(result.resources[0].uri, "file:///b");
3842    }
3843
3844    // ── handle_prompts_list with pagination ──────────────────────────────
3845
3846    #[test]
3847    fn handle_prompts_list_with_pagination() {
3848        let mut r = Router::new();
3849        r.set_list_page_size(Some(1));
3850        r.add_prompt(NamedPrompt::new("a"));
3851        r.add_prompt(NamedPrompt::new("b"));
3852        let cx = Cx::for_testing();
3853        let params = ListPromptsParams {
3854            cursor: None,
3855            include_tags: None,
3856            exclude_tags: None,
3857        };
3858        let result = r.handle_prompts_list(&cx, params, None).unwrap();
3859        assert_eq!(result.prompts.len(), 1);
3860        assert_eq!(result.prompts[0].name, "a");
3861        assert!(result.next_cursor.is_some());
3862
3863        let params = ListPromptsParams {
3864            cursor: result.next_cursor,
3865            include_tags: None,
3866            exclude_tags: None,
3867        };
3868        let result = r.handle_prompts_list(&cx, params, None).unwrap();
3869        assert_eq!(result.prompts.len(), 1);
3870        assert_eq!(result.prompts[0].name, "b");
3871        assert!(result.next_cursor.is_none());
3872    }
3873
3874    // ── handle_prompts_list with tag filter ──────────────────────────────
3875
3876    #[test]
3877    fn handle_prompts_list_with_tag_filter() {
3878        let mut r = Router::new();
3879        r.add_prompt(NamedPrompt::with_tags("a", vec!["internal".to_string()]));
3880        r.add_prompt(NamedPrompt::with_tags("b", vec!["public".to_string()]));
3881        let cx = Cx::for_testing();
3882        let params = ListPromptsParams {
3883            cursor: None,
3884            include_tags: None,
3885            exclude_tags: Some(vec!["internal".to_string()]),
3886        };
3887        let result = r.handle_prompts_list(&cx, params, None).unwrap();
3888        assert_eq!(result.prompts.len(), 1);
3889        assert_eq!(result.prompts[0].name, "b");
3890    }
3891
3892    // ── handle_resource_templates_list with pagination ───────────────────
3893
3894    #[test]
3895    fn handle_resource_templates_list_with_pagination() {
3896        let mut r = Router::new();
3897        r.set_list_page_size(Some(1));
3898        r.add_resource_template(ResourceTemplate {
3899            uri_template: "db://{table}".to_string(),
3900            name: "db".to_string(),
3901            description: None,
3902            mime_type: None,
3903            icon: None,
3904            version: None,
3905            tags: vec![],
3906        });
3907        r.add_resource_template(ResourceTemplate {
3908            uri_template: "cache://{key}".to_string(),
3909            name: "cache".to_string(),
3910            description: None,
3911            mime_type: None,
3912            icon: None,
3913            version: None,
3914            tags: vec![],
3915        });
3916        let cx = Cx::for_testing();
3917        let params = ListResourceTemplatesParams {
3918            cursor: None,
3919            include_tags: None,
3920            exclude_tags: None,
3921        };
3922        let result = r.handle_resource_templates_list(&cx, params, None).unwrap();
3923        assert_eq!(result.resource_templates.len(), 1);
3924        assert!(result.next_cursor.is_some());
3925
3926        let params = ListResourceTemplatesParams {
3927            cursor: result.next_cursor,
3928            include_tags: None,
3929            exclude_tags: None,
3930        };
3931        let result = r.handle_resource_templates_list(&cx, params, None).unwrap();
3932        assert_eq!(result.resource_templates.len(), 1);
3933        assert!(result.next_cursor.is_none());
3934    }
3935
3936    // ── handle_resource_templates_list with tag filter ───────────────────
3937
3938    #[test]
3939    fn handle_resource_templates_list_with_tag_filter() {
3940        let mut r = Router::new();
3941        r.add_resource_template(ResourceTemplate {
3942            uri_template: "db://{table}".to_string(),
3943            name: "db".to_string(),
3944            description: None,
3945            mime_type: None,
3946            icon: None,
3947            version: None,
3948            tags: vec!["admin".to_string()],
3949        });
3950        r.add_resource_template(ResourceTemplate {
3951            uri_template: "cache://{key}".to_string(),
3952            name: "cache".to_string(),
3953            description: None,
3954            mime_type: None,
3955            icon: None,
3956            version: None,
3957            tags: vec!["public".to_string()],
3958        });
3959        let cx = Cx::for_testing();
3960        let params = ListResourceTemplatesParams {
3961            cursor: None,
3962            include_tags: Some(vec!["public".to_string()]),
3963            exclude_tags: None,
3964        };
3965        let result = r.handle_resource_templates_list(&cx, params, None).unwrap();
3966        assert_eq!(result.resource_templates.len(), 1);
3967        assert_eq!(result.resource_templates[0].name, "cache");
3968    }
3969
3970    // ── mount_resources (selective method) ───────────────────────────────
3971
3972    #[test]
3973    fn mount_resources_only() {
3974        let mut main = Router::new();
3975        let mut sub = Router::new();
3976        sub.add_resource(NamedResource::new("file:///a"));
3977        sub.add_tool(NamedTool::new("t1"));
3978        sub.add_resource_template(ResourceTemplate {
3979            uri_template: "db://{t}".to_string(),
3980            name: "db".to_string(),
3981            description: None,
3982            mime_type: None,
3983            icon: None,
3984            version: None,
3985            tags: vec![],
3986        });
3987        let result = main.mount_resources(sub, Some("ns"));
3988        assert_eq!(result.resources, 1);
3989        assert_eq!(result.resource_templates, 1);
3990        assert!(main.get_resource("ns/file:///a").is_some());
3991        assert_eq!(main.tools_count(), 0); // tools not mounted
3992    }
3993
3994    // ── MountResult has_components with all fields ──────────────────────
3995
3996    #[test]
3997    fn mount_result_with_resources_has_components() {
3998        let mut r = MountResult::default();
3999        r.resources = 1;
4000        assert!(r.has_components());
4001    }
4002
4003    #[test]
4004    fn mount_result_with_templates_has_components() {
4005        let mut r = MountResult::default();
4006        r.resource_templates = 1;
4007        assert!(r.has_components());
4008    }
4009
4010    #[test]
4011    fn mount_result_with_prompts_has_components() {
4012        let mut r = MountResult::default();
4013        r.prompts = 1;
4014        assert!(r.has_components());
4015    }
4016
4017    #[test]
4018    fn mount_result_is_success_with_warnings() {
4019        let mut r = MountResult::default();
4020        r.warnings.push("something".to_string());
4021        assert!(r.is_success()); // always true
4022    }
4023
4024    // ── mount with all component types ──────────────────────────────────
4025
4026    #[test]
4027    fn mount_all_component_types() {
4028        let mut main = Router::new();
4029        let mut sub = Router::new();
4030        sub.add_tool(NamedTool::new("t1"));
4031        sub.add_resource(NamedResource::new("file:///r1"));
4032        sub.add_prompt(NamedPrompt::new("p1"));
4033        sub.add_resource_template(ResourceTemplate {
4034            uri_template: "db://{table}".to_string(),
4035            name: "db".to_string(),
4036            description: None,
4037            mime_type: None,
4038            icon: None,
4039            version: None,
4040            tags: vec![],
4041        });
4042        let result = main.mount(sub, Some("ns"));
4043        assert_eq!(result.tools, 1);
4044        assert_eq!(result.resources, 1);
4045        assert_eq!(result.prompts, 1);
4046        assert_eq!(result.resource_templates, 1);
4047        assert!(result.has_components());
4048        assert!(main.get_tool("ns/t1").is_some());
4049        assert!(main.get_resource("ns/file:///r1").is_some());
4050        assert!(main.get_prompt("ns/p1").is_some());
4051    }
4052
4053    // ── mount resource conflict warnings ────────────────────────────────
4054
4055    #[test]
4056    fn mount_warns_on_resource_conflict() {
4057        let mut main = Router::new();
4058        main.add_resource(NamedResource::new("file:///a"));
4059        let mut sub = Router::new();
4060        sub.add_resource(NamedResource::new("file:///a"));
4061        let result = main.mount(sub, None);
4062        assert!(!result.warnings.is_empty());
4063        assert!(result.warnings[0].contains("Resource"));
4064    }
4065
4066    #[test]
4067    fn mount_warns_on_prompt_conflict() {
4068        let mut main = Router::new();
4069        main.add_prompt(NamedPrompt::new("p"));
4070        let mut sub = Router::new();
4071        sub.add_prompt(NamedPrompt::new("p"));
4072        let result = main.mount(sub, None);
4073        assert!(!result.warnings.is_empty());
4074        assert!(result.warnings[0].contains("Prompt"));
4075    }
4076
4077    // ── TagFilters::clone ───────────────────────────────────────────────
4078
4079    #[test]
4080    fn tag_filters_clone() {
4081        let include = vec!["a".to_string()];
4082        let f = TagFilters::new(Some(&include), None);
4083        let cloned = f.clone();
4084        assert!(cloned.matches(&["a".to_string()]));
4085        assert!(!cloned.matches(&["b".to_string()]));
4086    }
4087
4088    // ── handle_tools_list with pagination AND tags ───────────────────────
4089
4090    #[test]
4091    fn handle_tools_list_pagination_with_tags() {
4092        let mut r = Router::new();
4093        r.set_list_page_size(Some(1));
4094        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]));
4095        r.add_tool(NamedTool::with_tags("b", vec!["db".to_string()]));
4096        r.add_tool(NamedTool::with_tags("c", vec!["web".to_string()]));
4097        let cx = Cx::for_testing();
4098
4099        // Only "db" tagged tools, page 1
4100        let params = ListToolsParams {
4101            cursor: None,
4102            include_tags: Some(vec!["db".to_string()]),
4103            exclude_tags: None,
4104        };
4105        let result = r.handle_tools_list(&cx, params, None).unwrap();
4106        assert_eq!(result.tools.len(), 1);
4107        assert_eq!(result.tools[0].name, "a");
4108        assert!(result.next_cursor.is_some());
4109
4110        // Page 2
4111        let params = ListToolsParams {
4112            cursor: result.next_cursor,
4113            include_tags: Some(vec!["db".to_string()]),
4114            exclude_tags: None,
4115        };
4116        let result = r.handle_tools_list(&cx, params, None).unwrap();
4117        assert_eq!(result.tools.len(), 1);
4118        assert_eq!(result.tools[0].name, "b");
4119        assert!(result.next_cursor.is_none());
4120    }
4121
4122    // ── handle_resources_list with session state filter ──────────────────
4123
4124    #[test]
4125    fn handle_resources_list_with_session_state_filter() {
4126        let mut r = Router::new();
4127        r.add_resource(NamedResource::new("file:///a"));
4128        r.add_resource(NamedResource::new("file:///b"));
4129        let cx = Cx::for_testing();
4130        let state = SessionState::new();
4131        let disabled: std::collections::HashSet<String> =
4132            ["file:///a".to_string()].into_iter().collect();
4133        state.set("fastmcp.disabled_resources", &disabled);
4134        let params = ListResourcesParams {
4135            cursor: None,
4136            include_tags: None,
4137            exclude_tags: None,
4138        };
4139        let result = r.handle_resources_list(&cx, params, Some(&state)).unwrap();
4140        assert_eq!(result.resources.len(), 1);
4141        assert_eq!(result.resources[0].uri, "file:///b");
4142    }
4143
4144    // ── handle_prompts_list with session state filter ────────────────────
4145
4146    #[test]
4147    fn handle_prompts_list_with_session_state_filter() {
4148        let mut r = Router::new();
4149        r.add_prompt(NamedPrompt::new("a"));
4150        r.add_prompt(NamedPrompt::new("b"));
4151        let cx = Cx::for_testing();
4152        let state = SessionState::new();
4153        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
4154        state.set("fastmcp.disabled_prompts", &disabled);
4155        let params = ListPromptsParams {
4156            cursor: None,
4157            include_tags: None,
4158            exclude_tags: None,
4159        };
4160        let result = r.handle_prompts_list(&cx, params, Some(&state)).unwrap();
4161        assert_eq!(result.prompts.len(), 1);
4162        assert_eq!(result.prompts[0].name, "b");
4163    }
4164
4165    // ── resource_templates_filtered by session + tags combined ───────────
4166
4167    #[test]
4168    fn resource_templates_filtered_session_and_tags_combined() {
4169        let mut r = Router::new();
4170        r.add_resource_template(ResourceTemplate {
4171            uri_template: "db://{table}".to_string(),
4172            name: "db".to_string(),
4173            description: None,
4174            mime_type: None,
4175            icon: None,
4176            version: None,
4177            tags: vec!["admin".to_string()],
4178        });
4179        r.add_resource_template(ResourceTemplate {
4180            uri_template: "cache://{key}".to_string(),
4181            name: "cache".to_string(),
4182            description: None,
4183            mime_type: None,
4184            icon: None,
4185            version: None,
4186            tags: vec!["admin".to_string()],
4187        });
4188        r.add_resource_template(ResourceTemplate {
4189            uri_template: "log://{entry}".to_string(),
4190            name: "log".to_string(),
4191            description: None,
4192            mime_type: None,
4193            icon: None,
4194            version: None,
4195            tags: vec!["public".to_string()],
4196        });
4197        // Disable db template via session state
4198        let state = SessionState::new();
4199        let disabled: std::collections::HashSet<String> =
4200            ["db://{table}".to_string()].into_iter().collect();
4201        state.set("fastmcp.disabled_resources", &disabled);
4202        // Also filter by admin tag
4203        let include = vec!["admin".to_string()];
4204        let filters = TagFilters::new(Some(&include), None);
4205        let tmpls = r.resource_templates_filtered(Some(&state), Some(&filters));
4206        // db is disabled, log doesn't have admin tag => only cache
4207        assert_eq!(tmpls.len(), 1);
4208        assert_eq!(tmpls[0].name, "cache");
4209    }
4210
4211    // ── mount_tools warns on template conflict ──────────────────────────
4212
4213    #[test]
4214    fn mount_resource_template_warns_on_conflict() {
4215        let mut main = Router::new();
4216        main.add_resource_template(ResourceTemplate {
4217            uri_template: "db://{table}".to_string(),
4218            name: "db".to_string(),
4219            description: None,
4220            mime_type: None,
4221            icon: None,
4222            version: None,
4223            tags: vec![],
4224        });
4225        let mut sub = Router::new();
4226        sub.add_resource_template(ResourceTemplate {
4227            uri_template: "db://{table}".to_string(),
4228            name: "db2".to_string(),
4229            description: None,
4230            mime_type: None,
4231            icon: None,
4232            version: None,
4233            tags: vec![],
4234        });
4235        let result = main.mount(sub, None);
4236        assert!(!result.warnings.is_empty());
4237        assert!(result.warnings[0].contains("Resource template"));
4238    }
4239
4240    // ── handle_tools_call: tool disabled via session ─────────────────────
4241
4242    #[test]
4243    fn handle_tools_call_disabled_tool_returns_error() {
4244        let mut r = Router::new();
4245        r.add_tool(NamedTool::new("my_tool"));
4246        let cx = Cx::for_testing();
4247        let budget = Budget::INFINITE;
4248        let state = SessionState::new();
4249        let disabled: std::collections::HashSet<String> =
4250            ["my_tool".to_string()].into_iter().collect();
4251        state.set("fastmcp.disabled_tools", &disabled);
4252        let params = CallToolParams {
4253            name: "my_tool".to_string(),
4254            arguments: None,
4255            meta: None,
4256        };
4257        let err = r
4258            .handle_tools_call(&cx, 1, params, &budget, state, None, None)
4259            .unwrap_err();
4260        assert!(err.message.contains("disabled"));
4261    }
4262
4263    // ── handle_tools_call: success path ──────────────────────────────────
4264
4265    #[test]
4266    fn handle_tools_call_success() {
4267        let mut r = Router::new();
4268        r.add_tool(NamedTool::new("echo"));
4269        let cx = Cx::for_testing();
4270        let budget = Budget::INFINITE;
4271        let params = CallToolParams {
4272            name: "echo".to_string(),
4273            arguments: None,
4274            meta: None,
4275        };
4276        let result = r
4277            .handle_tools_call(&cx, 1, params, &budget, SessionState::new(), None, None)
4278            .unwrap();
4279        assert!(!result.is_error);
4280        assert!(!result.content.is_empty());
4281    }
4282
4283    // ── handle_tools_call: not found ─────────────────────────────────────
4284
4285    #[test]
4286    fn handle_tools_call_not_found() {
4287        let r = Router::new();
4288        let cx = Cx::for_testing();
4289        let budget = Budget::INFINITE;
4290        let params = CallToolParams {
4291            name: "missing".to_string(),
4292            arguments: None,
4293            meta: None,
4294        };
4295        let err = r
4296            .handle_tools_call(&cx, 1, params, &budget, SessionState::new(), None, None)
4297            .unwrap_err();
4298        assert!(err.message.contains("missing"));
4299    }
4300
4301    // ── handle_tools_call: budget exhausted ──────────────────────────────
4302
4303    #[test]
4304    fn handle_tools_call_budget_exhausted() {
4305        let mut r = Router::new();
4306        r.add_tool(NamedTool::new("t"));
4307        let cx = Cx::for_testing();
4308        let budget = Budget::unlimited().with_poll_quota(0);
4309        let params = CallToolParams {
4310            name: "t".to_string(),
4311            arguments: None,
4312            meta: None,
4313        };
4314        let err = r
4315            .handle_tools_call(&cx, 1, params, &budget, SessionState::new(), None, None)
4316            .unwrap_err();
4317        assert!(
4318            err.message.contains("budget") || err.message.contains("exhausted"),
4319            "unexpected error: {}",
4320            err.message
4321        );
4322    }
4323
4324    // ── handle_resources_read: resource disabled via session ──────────────
4325
4326    #[test]
4327    fn handle_resources_read_disabled_resource_returns_error() {
4328        let mut r = Router::new();
4329        r.add_resource(NamedResource::new("file:///secret"));
4330        let cx = Cx::for_testing();
4331        let budget = Budget::INFINITE;
4332        let state = SessionState::new();
4333        let disabled: std::collections::HashSet<String> =
4334            ["file:///secret".to_string()].into_iter().collect();
4335        state.set("fastmcp.disabled_resources", &disabled);
4336        let params = ReadResourceParams {
4337            uri: "file:///secret".to_string(),
4338            meta: None,
4339        };
4340        let err = r
4341            .handle_resources_read(&cx, 1, &params, &budget, state, None, None)
4342            .unwrap_err();
4343        assert!(err.message.contains("disabled"));
4344    }
4345
4346    // ── handle_resources_read: success path ──────────────────────────────
4347
4348    #[test]
4349    fn handle_resources_read_success() {
4350        let mut r = Router::new();
4351        r.add_resource(NamedResource::new("file:///a"));
4352        let cx = Cx::for_testing();
4353        let budget = Budget::INFINITE;
4354        let params = ReadResourceParams {
4355            uri: "file:///a".to_string(),
4356            meta: None,
4357        };
4358        let result = r
4359            .handle_resources_read(&cx, 1, &params, &budget, SessionState::new(), None, None)
4360            .unwrap();
4361        assert_eq!(result.contents.len(), 1);
4362        assert_eq!(result.contents[0].uri, "file:///a");
4363    }
4364
4365    // ── handle_resources_read: not found ─────────────────────────────────
4366
4367    #[test]
4368    fn handle_resources_read_not_found() {
4369        let r = Router::new();
4370        let cx = Cx::for_testing();
4371        let budget = Budget::INFINITE;
4372        let params = ReadResourceParams {
4373            uri: "file:///nonexistent".to_string(),
4374            meta: None,
4375        };
4376        let err = r
4377            .handle_resources_read(&cx, 1, &params, &budget, SessionState::new(), None, None)
4378            .unwrap_err();
4379        assert!(err.message.contains("nonexistent") || err.message.contains("not found"));
4380    }
4381
4382    // ── handle_resources_read: budget exhausted ──────────────────────────
4383
4384    #[test]
4385    fn handle_resources_read_budget_exhausted() {
4386        let mut r = Router::new();
4387        r.add_resource(NamedResource::new("file:///a"));
4388        let cx = Cx::for_testing();
4389        let budget = Budget::unlimited().with_poll_quota(0);
4390        let params = ReadResourceParams {
4391            uri: "file:///a".to_string(),
4392            meta: None,
4393        };
4394        let err = r
4395            .handle_resources_read(&cx, 1, &params, &budget, SessionState::new(), None, None)
4396            .unwrap_err();
4397        assert!(
4398            err.message.contains("budget") || err.message.contains("exhausted"),
4399            "unexpected error: {}",
4400            err.message
4401        );
4402    }
4403
4404    // ── handle_prompts_get: prompt disabled via session ───────────────────
4405
4406    #[test]
4407    fn handle_prompts_get_disabled_prompt_returns_error() {
4408        let mut r = Router::new();
4409        r.add_prompt(NamedPrompt::new("secret_prompt"));
4410        let cx = Cx::for_testing();
4411        let budget = Budget::INFINITE;
4412        let state = SessionState::new();
4413        let disabled: std::collections::HashSet<String> =
4414            ["secret_prompt".to_string()].into_iter().collect();
4415        state.set("fastmcp.disabled_prompts", &disabled);
4416        let params = GetPromptParams {
4417            name: "secret_prompt".to_string(),
4418            arguments: None,
4419            meta: None,
4420        };
4421        let err = r
4422            .handle_prompts_get(&cx, 1, params, &budget, state, None, None)
4423            .unwrap_err();
4424        assert!(err.message.contains("disabled"));
4425    }
4426
4427    // ── handle_prompts_get: success path ─────────────────────────────────
4428
4429    #[test]
4430    fn handle_prompts_get_success() {
4431        let mut r = Router::new();
4432        r.add_prompt(NamedPrompt::new("greet"));
4433        let cx = Cx::for_testing();
4434        let budget = Budget::INFINITE;
4435        let params = GetPromptParams {
4436            name: "greet".to_string(),
4437            arguments: None,
4438            meta: None,
4439        };
4440        let result = r
4441            .handle_prompts_get(&cx, 1, params, &budget, SessionState::new(), None, None)
4442            .unwrap();
4443        assert!(result.description.is_some());
4444    }
4445
4446    // ── handle_prompts_get: not found ────────────────────────────────────
4447
4448    #[test]
4449    fn handle_prompts_get_not_found() {
4450        let r = Router::new();
4451        let cx = Cx::for_testing();
4452        let budget = Budget::INFINITE;
4453        let params = GetPromptParams {
4454            name: "missing".to_string(),
4455            arguments: None,
4456            meta: None,
4457        };
4458        let err = r
4459            .handle_prompts_get(&cx, 1, params, &budget, SessionState::new(), None, None)
4460            .unwrap_err();
4461        assert!(err.message.contains("missing") || err.message.contains("not found"));
4462    }
4463
4464    // ── handle_prompts_get: budget exhausted ─────────────────────────────
4465
4466    #[test]
4467    fn handle_prompts_get_budget_exhausted() {
4468        let mut r = Router::new();
4469        r.add_prompt(NamedPrompt::new("p"));
4470        let cx = Cx::for_testing();
4471        let budget = Budget::unlimited().with_poll_quota(0);
4472        let params = GetPromptParams {
4473            name: "p".to_string(),
4474            arguments: None,
4475            meta: None,
4476        };
4477        let err = r
4478            .handle_prompts_get(&cx, 1, params, &budget, SessionState::new(), None, None)
4479            .unwrap_err();
4480        assert!(
4481            err.message.contains("budget") || err.message.contains("exhausted"),
4482            "unexpected error: {}",
4483            err.message
4484        );
4485    }
4486
4487    // ── add_resource_with_behavior: template resource Error ───────────────
4488
4489    #[test]
4490    fn add_resource_with_behavior_template_error_on_duplicate() {
4491        struct TmplResource;
4492        impl ResourceHandler for TmplResource {
4493            fn definition(&self) -> Resource {
4494                Resource {
4495                    uri: "db://placeholder".to_string(),
4496                    name: "db".to_string(),
4497                    description: None,
4498                    mime_type: None,
4499                    icon: None,
4500                    version: None,
4501                    tags: vec![],
4502                }
4503            }
4504            fn template(&self) -> Option<ResourceTemplate> {
4505                Some(ResourceTemplate {
4506                    uri_template: "db://{table}".to_string(),
4507                    name: "db".to_string(),
4508                    description: None,
4509                    mime_type: None,
4510                    icon: None,
4511                    version: None,
4512                    tags: vec![],
4513                })
4514            }
4515            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
4516                Ok(vec![])
4517            }
4518        }
4519        let mut r = Router::new();
4520        r.add_resource(TmplResource);
4521        let err = r
4522            .add_resource_with_behavior(TmplResource, crate::DuplicateBehavior::Error)
4523            .unwrap_err();
4524        assert!(err.message.contains("already exists"));
4525    }
4526
4527    // ── add_resource_with_behavior: template resource Ignore ─────────────
4528
4529    #[test]
4530    fn add_resource_with_behavior_template_ignore_on_duplicate() {
4531        struct TmplResource2;
4532        impl ResourceHandler for TmplResource2 {
4533            fn definition(&self) -> Resource {
4534                Resource {
4535                    uri: "cache://placeholder".to_string(),
4536                    name: "cache".to_string(),
4537                    description: None,
4538                    mime_type: None,
4539                    icon: None,
4540                    version: None,
4541                    tags: vec![],
4542                }
4543            }
4544            fn template(&self) -> Option<ResourceTemplate> {
4545                Some(ResourceTemplate {
4546                    uri_template: "cache://{key}".to_string(),
4547                    name: "cache".to_string(),
4548                    description: None,
4549                    mime_type: None,
4550                    icon: None,
4551                    version: None,
4552                    tags: vec![],
4553                })
4554            }
4555            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
4556                Ok(vec![])
4557            }
4558        }
4559        let mut r = Router::new();
4560        r.add_resource(TmplResource2);
4561        r.add_resource_with_behavior(TmplResource2, crate::DuplicateBehavior::Ignore)
4562            .unwrap();
4563        assert_eq!(r.resource_templates_count(), 1);
4564    }
4565
4566    // ── add_resource_with_behavior: template resource Warn ───────────────
4567
4568    #[test]
4569    fn add_resource_with_behavior_template_warn_on_duplicate() {
4570        struct TmplResource3;
4571        impl ResourceHandler for TmplResource3 {
4572            fn definition(&self) -> Resource {
4573                Resource {
4574                    uri: "log://placeholder".to_string(),
4575                    name: "log".to_string(),
4576                    description: None,
4577                    mime_type: None,
4578                    icon: None,
4579                    version: None,
4580                    tags: vec![],
4581                }
4582            }
4583            fn template(&self) -> Option<ResourceTemplate> {
4584                Some(ResourceTemplate {
4585                    uri_template: "log://{entry}".to_string(),
4586                    name: "log".to_string(),
4587                    description: None,
4588                    mime_type: None,
4589                    icon: None,
4590                    version: None,
4591                    tags: vec![],
4592                })
4593            }
4594            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
4595                Ok(vec![])
4596            }
4597        }
4598        let mut r = Router::new();
4599        r.add_resource(TmplResource3);
4600        r.add_resource_with_behavior(TmplResource3, crate::DuplicateBehavior::Warn)
4601            .unwrap();
4602        assert_eq!(r.resource_templates_count(), 1);
4603    }
4604
4605    // ── mount_tools warns on conflict ────────────────────────────────
4606
4607    #[test]
4608    fn mount_tools_warns_on_tool_conflict() {
4609        let mut main = Router::new();
4610        main.add_tool(NamedTool::new("t"));
4611        let mut sub = Router::new();
4612        sub.add_tool(NamedTool::new("t"));
4613        let result = main.mount_tools(sub, None);
4614        assert!(!result.warnings.is_empty());
4615        assert!(result.warnings[0].contains("Tool"));
4616    }
4617
4618    // ── mount_prompts warns on conflict ──────────────────────────────────
4619
4620    #[test]
4621    fn mount_prompts_warns_on_prompt_conflict() {
4622        let mut main = Router::new();
4623        main.add_prompt(NamedPrompt::new("p"));
4624        let mut sub = Router::new();
4625        sub.add_prompt(NamedPrompt::new("p"));
4626        let result = main.mount_prompts(sub, None);
4627        assert!(!result.warnings.is_empty());
4628        assert!(result.warnings[0].contains("Prompt"));
4629    }
4630
4631    // ── invalid cursor returns error ─────────────────────────────────────
4632
4633    #[test]
4634    fn invalid_cursor_returns_error() {
4635        let mut r = Router::new();
4636        r.set_list_page_size(Some(1));
4637        r.add_tool(NamedTool::new("a"));
4638        let cx = Cx::for_testing();
4639        let params = ListToolsParams {
4640            cursor: Some("not-valid-base64!!!".to_string()),
4641            include_tags: None,
4642            exclude_tags: None,
4643        };
4644        let err = r.handle_tools_list(&cx, params, None).unwrap_err();
4645        assert!(err.message.contains("cursor") || err.message.contains("Invalid"));
4646    }
4647
4648    // ── set_list_page_size zero is treated as None ───────────────────────
4649
4650    #[test]
4651    fn set_list_page_size_zero_disables_pagination() {
4652        let mut r = Router::new();
4653        r.set_list_page_size(Some(0));
4654        r.add_tool(NamedTool::new("a"));
4655        r.add_tool(NamedTool::new("b"));
4656        let cx = Cx::for_testing();
4657        let params = ListToolsParams {
4658            cursor: None,
4659            include_tags: None,
4660            exclude_tags: None,
4661        };
4662        let result = r.handle_tools_list(&cx, params, None).unwrap();
4663        // With page_size = 0, all items returned (no pagination)
4664        assert_eq!(result.tools.len(), 2);
4665        assert!(result.next_cursor.is_none());
4666    }
4667
4668    // ── strict_input_validation getter ───────────────────────────────────
4669
4670    #[test]
4671    fn strict_input_validation_toggle() {
4672        let mut r = Router::new();
4673        assert!(!r.strict_input_validation());
4674        r.set_strict_input_validation(true);
4675        assert!(r.strict_input_validation());
4676        r.set_strict_input_validation(false);
4677        assert!(!r.strict_input_validation());
4678    }
4679
4680    // ── cx-cancelled early return paths ──────────────────────────────────
4681
4682    #[test]
4683    fn handle_tools_call_cancelled_cx_returns_error() {
4684        let mut r = Router::new();
4685        r.add_tool(NamedTool::new("t"));
4686        let cx = Cx::for_testing();
4687        cx.set_cancel_requested(true);
4688        let budget = Budget::INFINITE;
4689        let params = CallToolParams {
4690            name: "t".to_string(),
4691            arguments: None,
4692            meta: None,
4693        };
4694        let err = r
4695            .handle_tools_call(&cx, 1, params, &budget, SessionState::new(), None, None)
4696            .unwrap_err();
4697        assert_eq!(err.code, McpErrorCode::RequestCancelled);
4698    }
4699
4700    #[test]
4701    fn handle_resources_read_cancelled_cx_returns_error() {
4702        let mut r = Router::new();
4703        r.add_resource(NamedResource::new("file:///a.txt"));
4704        let cx = Cx::for_testing();
4705        cx.set_cancel_requested(true);
4706        let budget = Budget::INFINITE;
4707        let params = ReadResourceParams {
4708            uri: "file:///a.txt".to_string(),
4709            meta: None,
4710        };
4711        let err = r
4712            .handle_resources_read(&cx, 1, &params, &budget, SessionState::new(), None, None)
4713            .unwrap_err();
4714        assert_eq!(err.code, McpErrorCode::RequestCancelled);
4715    }
4716
4717    #[test]
4718    fn handle_prompts_get_cancelled_cx_returns_error() {
4719        let mut r = Router::new();
4720        r.add_prompt(NamedPrompt::new("p"));
4721        let cx = Cx::for_testing();
4722        cx.set_cancel_requested(true);
4723        let budget = Budget::INFINITE;
4724        let params = GetPromptParams {
4725            name: "p".to_string(),
4726            arguments: None,
4727            meta: None,
4728        };
4729        let err = r
4730            .handle_prompts_get(&cx, 1, params, &budget, SessionState::new(), None, None)
4731            .unwrap_err();
4732        assert_eq!(err.code, McpErrorCode::RequestCancelled);
4733    }
4734
4735    // ── handle_tasks with real task manager ──────────────────────────────
4736
4737    #[test]
4738    fn handle_tasks_list_with_manager_returns_tasks() {
4739        use crate::tasks::TaskManager;
4740        let r = Router::new();
4741        let cx = Cx::for_testing();
4742        let tm = TaskManager::new_for_testing();
4743        tm.register_handler("analyze", |_cx, _params| async {
4744            Ok(serde_json::json!({}))
4745        });
4746        let _ = tm.submit(&cx, "analyze", None).unwrap();
4747        let _ = tm.submit(&cx, "analyze", None).unwrap();
4748        let shared = tm.into_shared();
4749        let params = ListTasksParams {
4750            cursor: None,
4751            status: None,
4752            limit: None,
4753        };
4754        let result = r.handle_tasks_list(&cx, params, Some(&shared)).unwrap();
4755        assert_eq!(result.tasks.len(), 2);
4756    }
4757
4758    #[test]
4759    fn handle_tasks_get_with_manager_returns_task() {
4760        use crate::tasks::TaskManager;
4761        let r = Router::new();
4762        let cx = Cx::for_testing();
4763        let tm = TaskManager::new_for_testing();
4764        tm.register_handler("t", |_cx, _params| async { Ok(serde_json::json!({})) });
4765        let id = tm.submit(&cx, "t", None).unwrap();
4766        let shared = tm.into_shared();
4767        let params = GetTaskParams { id: id.clone() };
4768        let result = r.handle_tasks_get(&cx, params, Some(&shared)).unwrap();
4769        assert_eq!(result.task.id, id);
4770        assert!(result.result.is_none());
4771    }
4772
4773    #[test]
4774    fn handle_tasks_get_task_not_found() {
4775        use crate::tasks::TaskManager;
4776        use fastmcp_protocol::TaskId;
4777        let r = Router::new();
4778        let cx = Cx::for_testing();
4779        let tm = TaskManager::new_for_testing();
4780        let shared = tm.into_shared();
4781        let params = GetTaskParams {
4782            id: TaskId::from_string("nonexistent".to_string()),
4783        };
4784        let err = r.handle_tasks_get(&cx, params, Some(&shared)).unwrap_err();
4785        assert!(err.message.contains("not found"));
4786    }
4787
4788    #[test]
4789    fn mount_result_is_success_always_true() {
4790        let result = MountResult {
4791            tools: 0,
4792            resources: 0,
4793            resource_templates: 0,
4794            prompts: 0,
4795            warnings: vec!["something".to_string()],
4796        };
4797        assert!(result.is_success());
4798        assert!(!result.has_components());
4799    }
4800}