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