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