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