1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9use fastmcp_client::Client;
10use fastmcp_core::{McpContext, McpError, McpResult};
11use fastmcp_protocol::{
12 Content, Prompt, PromptMessage, Resource, ResourceContent, ResourceTemplate, Tool,
13};
14
15use crate::handler::{PromptHandler, ResourceHandler, ToolHandler, UriParams};
16
17pub type ProgressCallback<'a> = &'a mut dyn FnMut(f64, Option<f64>, Option<String>);
19
20pub trait ProxyBackend: Send {
22 fn list_tools(&mut self) -> McpResult<Vec<Tool>>;
24 fn list_resources(&mut self) -> McpResult<Vec<Resource>>;
26 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>>;
28 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>>;
30 fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> McpResult<Vec<Content>>;
32 fn call_tool_with_progress(
34 &mut self,
35 name: &str,
36 arguments: serde_json::Value,
37 on_progress: ProgressCallback<'_>,
38 ) -> McpResult<Vec<Content>>;
39 fn read_resource(&mut self, uri: &str) -> McpResult<Vec<ResourceContent>>;
41 fn get_prompt(
43 &mut self,
44 name: &str,
45 arguments: HashMap<String, String>,
46 ) -> McpResult<Vec<PromptMessage>>;
47}
48
49impl ProxyBackend for Client {
50 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
51 if self.server_capabilities().tools.is_none() {
52 return Ok(Vec::new());
53 }
54 Client::list_tools(self)
55 }
56
57 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
58 if self.server_capabilities().resources.is_none() {
59 return Ok(Vec::new());
60 }
61 Client::list_resources(self)
62 }
63
64 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
65 if self.server_capabilities().resources.is_none() {
66 return Ok(Vec::new());
67 }
68 Client::list_resource_templates(self)
69 }
70
71 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
72 if self.server_capabilities().prompts.is_none() {
73 return Ok(Vec::new());
74 }
75 Client::list_prompts(self)
76 }
77
78 fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
79 Client::call_tool(self, name, arguments)
80 }
81
82 fn call_tool_with_progress(
83 &mut self,
84 name: &str,
85 arguments: serde_json::Value,
86 on_progress: ProgressCallback<'_>,
87 ) -> McpResult<Vec<Content>> {
88 let mut wrapper = |progress, total, message: Option<&str>| {
89 on_progress(progress, total, message.map(ToString::to_string));
90 };
91 Client::call_tool_with_progress(self, name, arguments, &mut wrapper)
92 }
93
94 fn read_resource(&mut self, uri: &str) -> McpResult<Vec<ResourceContent>> {
95 Client::read_resource(self, uri)
96 }
97
98 fn get_prompt(
99 &mut self,
100 name: &str,
101 arguments: HashMap<String, String>,
102 ) -> McpResult<Vec<PromptMessage>> {
103 Client::get_prompt(self, name, arguments)
104 }
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct ProxyCatalog {
110 pub tools: Vec<Tool>,
112 pub resources: Vec<Resource>,
114 pub resource_templates: Vec<ResourceTemplate>,
116 pub prompts: Vec<Prompt>,
118}
119
120impl ProxyCatalog {
121 pub fn from_backend<B: ProxyBackend + ?Sized>(backend: &mut B) -> McpResult<Self> {
123 Ok(Self {
124 tools: backend.list_tools()?,
125 resources: backend.list_resources()?,
126 resource_templates: backend.list_resource_templates()?,
127 prompts: backend.list_prompts()?,
128 })
129 }
130
131 pub fn from_client(client: &mut Client) -> McpResult<Self> {
133 Self::from_backend(client)
134 }
135}
136
137#[derive(Clone)]
139pub struct ProxyClient {
140 inner: Arc<Mutex<dyn ProxyBackend>>,
141}
142
143impl ProxyClient {
144 #[must_use]
146 pub fn from_client(client: Client) -> Self {
147 Self::from_backend(client)
148 }
149
150 #[must_use]
152 pub fn from_backend<B: ProxyBackend + 'static>(backend: B) -> Self {
153 Self {
154 inner: Arc::new(Mutex::new(backend)),
155 }
156 }
157
158 pub fn catalog(&self) -> McpResult<ProxyCatalog> {
160 self.with_backend(|backend| ProxyCatalog::from_backend(backend))
161 }
162
163 fn with_backend<F, R>(&self, f: F) -> McpResult<R>
164 where
165 F: FnOnce(&mut dyn ProxyBackend) -> McpResult<R>,
166 {
167 let mut guard = self
168 .inner
169 .lock()
170 .map_err(|_| McpError::internal_error("Proxy backend lock poisoned"))?;
171 f(&mut *guard)
172 }
173
174 fn call_tool(
175 &self,
176 ctx: &McpContext,
177 name: &str,
178 arguments: serde_json::Value,
179 ) -> McpResult<Vec<Content>> {
180 ctx.checkpoint()?;
181 self.with_backend(|backend| {
182 if ctx.has_progress_reporter() {
183 let mut callback = |progress, total, message: Option<String>| {
184 if let Some(total) = total {
185 ctx.report_progress_with_total(progress, total, message.as_deref());
186 } else {
187 ctx.report_progress(progress, message.as_deref());
188 }
189 };
190 backend.call_tool_with_progress(name, arguments, &mut callback)
191 } else {
192 backend.call_tool(name, arguments)
193 }
194 })
195 }
196
197 fn read_resource(&self, ctx: &McpContext, uri: &str) -> McpResult<Vec<ResourceContent>> {
198 ctx.checkpoint()?;
199 self.with_backend(|backend| backend.read_resource(uri))
200 }
201
202 fn get_prompt(
203 &self,
204 ctx: &McpContext,
205 name: &str,
206 arguments: HashMap<String, String>,
207 ) -> McpResult<Vec<PromptMessage>> {
208 ctx.checkpoint()?;
209 self.with_backend(|backend| backend.get_prompt(name, arguments))
210 }
211}
212
213pub(crate) struct ProxyToolHandler {
214 tool: Tool,
216 external_name: String,
218 client: ProxyClient,
219}
220
221impl ProxyToolHandler {
222 pub(crate) fn new(tool: Tool, client: ProxyClient) -> Self {
223 let external_name = tool.name.clone();
224 Self {
225 tool,
226 external_name,
227 client,
228 }
229 }
230
231 pub(crate) fn with_prefix(mut tool: Tool, prefix: &str, client: ProxyClient) -> Self {
236 let external_name = tool.name.clone();
237 tool.name = format!("{}/{}", prefix, tool.name);
238 Self {
239 tool,
240 external_name,
241 client,
242 }
243 }
244}
245
246impl ToolHandler for ProxyToolHandler {
247 fn definition(&self) -> Tool {
248 self.tool.clone()
249 }
250
251 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
252 self.client.call_tool(ctx, &self.external_name, arguments)
254 }
255}
256
257pub(crate) struct ProxyResourceHandler {
258 resource: Resource,
260 external_uri: String,
262 template: Option<ResourceTemplate>,
263 client: ProxyClient,
264}
265
266impl ProxyResourceHandler {
267 pub(crate) fn new(resource: Resource, client: ProxyClient) -> Self {
268 let external_uri = resource.uri.clone();
269 Self {
270 resource,
271 external_uri,
272 template: None,
273 client,
274 }
275 }
276
277 pub(crate) fn with_prefix(mut resource: Resource, prefix: &str, client: ProxyClient) -> Self {
279 let external_uri = resource.uri.clone();
280 resource.uri = format!("{}/{}", prefix, resource.uri);
281 Self {
282 resource,
283 external_uri,
284 template: None,
285 client,
286 }
287 }
288
289 pub(crate) fn from_template(template: ResourceTemplate, client: ProxyClient) -> Self {
290 let external_uri = template.uri_template.clone();
291 Self {
292 resource: resource_from_template(&template),
293 external_uri,
294 template: Some(template),
295 client,
296 }
297 }
298
299 pub(crate) fn from_template_with_prefix(
301 mut template: ResourceTemplate,
302 prefix: &str,
303 client: ProxyClient,
304 ) -> Self {
305 let external_uri = template.uri_template.clone();
306 template.uri_template = format!("{}/{}", prefix, template.uri_template);
307 Self {
308 resource: resource_from_template(&template),
309 external_uri,
310 template: Some(template),
311 client,
312 }
313 }
314}
315
316impl ResourceHandler for ProxyResourceHandler {
317 fn definition(&self) -> Resource {
318 self.resource.clone()
319 }
320
321 fn template(&self) -> Option<ResourceTemplate> {
322 self.template.clone()
323 }
324
325 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
326 self.client.read_resource(ctx, &self.external_uri)
328 }
329
330 fn read_with_uri(
331 &self,
332 ctx: &McpContext,
333 uri: &str,
334 _params: &UriParams,
335 ) -> McpResult<Vec<ResourceContent>> {
336 let external_uri = if uri.starts_with(&format!(
342 "{}/",
343 self.resource.uri.split('/').next().unwrap_or("")
344 )) {
345 uri.splitn(2, '/').nth(1).unwrap_or(uri)
347 } else {
348 uri
350 };
351 self.client.read_resource(ctx, external_uri)
352 }
353}
354
355pub(crate) struct ProxyPromptHandler {
356 prompt: Prompt,
358 external_name: String,
360 client: ProxyClient,
361}
362
363impl ProxyPromptHandler {
364 pub(crate) fn new(prompt: Prompt, client: ProxyClient) -> Self {
365 let external_name = prompt.name.clone();
366 Self {
367 prompt,
368 external_name,
369 client,
370 }
371 }
372
373 pub(crate) fn with_prefix(mut prompt: Prompt, prefix: &str, client: ProxyClient) -> Self {
375 let external_name = prompt.name.clone();
376 prompt.name = format!("{}/{}", prefix, prompt.name);
377 Self {
378 prompt,
379 external_name,
380 client,
381 }
382 }
383}
384
385impl PromptHandler for ProxyPromptHandler {
386 fn definition(&self) -> Prompt {
387 self.prompt.clone()
388 }
389
390 fn get(
391 &self,
392 ctx: &McpContext,
393 arguments: HashMap<String, String>,
394 ) -> McpResult<Vec<PromptMessage>> {
395 self.client.get_prompt(ctx, &self.external_name, arguments)
397 }
398}
399
400fn resource_from_template(template: &ResourceTemplate) -> Resource {
401 Resource {
402 uri: template.uri_template.clone(),
403 name: template.name.clone(),
404 description: template.description.clone(),
405 mime_type: template.mime_type.clone(),
406 icon: template.icon.clone(),
407 version: template.version.clone(),
408 tags: template.tags.clone(),
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use std::collections::HashMap;
415 use std::sync::{Arc, Mutex};
416
417 use asupersync::Cx;
418 use fastmcp_core::McpContext;
419 use fastmcp_protocol::{Content, Prompt, PromptMessage, Resource, ResourceContent, Tool};
420
421 use super::{ProxyBackend, ProxyCatalog, ProxyClient, ProxyPromptHandler, ProxyToolHandler};
422 use crate::handler::{PromptHandler, ToolHandler};
423
424 #[derive(Default)]
425 struct TestState {
426 last_tool: Option<(String, serde_json::Value)>,
427 last_prompt: Option<(String, HashMap<String, String>)>,
428 }
429
430 #[derive(Clone, Default)]
431 struct TestBackend {
432 tools: Vec<Tool>,
433 resources: Vec<Resource>,
434 prompts: Vec<Prompt>,
435 state: Arc<Mutex<TestState>>,
436 }
437
438 impl ProxyBackend for TestBackend {
439 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
440 Ok(self.tools.clone())
441 }
442
443 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
444 Ok(self.resources.clone())
445 }
446
447 fn list_resource_templates(
448 &mut self,
449 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
450 Ok(Vec::new())
451 }
452
453 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
454 Ok(self.prompts.clone())
455 }
456
457 fn call_tool(
458 &mut self,
459 name: &str,
460 arguments: serde_json::Value,
461 ) -> fastmcp_core::McpResult<Vec<Content>> {
462 let mut guard = self.state.lock().expect("state lock poisoned");
463 guard.last_tool.replace((name.to_string(), arguments));
464 Ok(vec![Content::Text {
465 text: "ok".to_string(),
466 }])
467 }
468
469 fn call_tool_with_progress(
470 &mut self,
471 name: &str,
472 arguments: serde_json::Value,
473 on_progress: super::ProgressCallback<'_>,
474 ) -> fastmcp_core::McpResult<Vec<Content>> {
475 on_progress(0.5, Some(1.0), Some("half".to_string()));
476 self.call_tool(name, arguments)
477 }
478
479 fn read_resource(&mut self, _uri: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
480 Ok(vec![ResourceContent {
481 uri: "test://resource".to_string(),
482 text: Some("resource".to_string()),
483 mime_type: None,
484 blob: None,
485 }])
486 }
487
488 fn get_prompt(
489 &mut self,
490 name: &str,
491 arguments: HashMap<String, String>,
492 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
493 let mut guard = self.state.lock().expect("state lock poisoned");
494 guard.last_prompt.replace((name.to_string(), arguments));
495 Ok(vec![PromptMessage {
496 role: fastmcp_protocol::Role::Assistant,
497 content: Content::Text {
498 text: "ok".to_string(),
499 },
500 }])
501 }
502 }
503
504 #[test]
505 fn proxy_catalog_collects_definitions() {
506 let backend = TestBackend {
507 tools: vec![Tool {
508 name: "tool".to_string(),
509 description: None,
510 input_schema: serde_json::json!({}),
511 output_schema: None,
512 icon: None,
513 version: None,
514 tags: vec![],
515 annotations: None,
516 }],
517 resources: vec![Resource {
518 uri: "test://resource".to_string(),
519 name: "resource".to_string(),
520 description: None,
521 mime_type: None,
522 icon: None,
523 version: None,
524 tags: vec![],
525 }],
526 prompts: vec![Prompt {
527 name: "prompt".to_string(),
528 description: None,
529 arguments: Vec::new(),
530 icon: None,
531 version: None,
532 tags: vec![],
533 }],
534 ..TestBackend::default()
535 };
536 let mut backend = backend;
537 let catalog = ProxyCatalog::from_backend(&mut backend).expect("catalog");
538 assert_eq!(catalog.tools.len(), 1);
539 assert_eq!(catalog.resources.len(), 1);
540 assert_eq!(catalog.prompts.len(), 1);
541 }
542
543 #[test]
544 fn proxy_tool_handler_forwards_calls() {
545 let state = Arc::new(Mutex::new(TestState::default()));
546 let backend = TestBackend {
547 tools: vec![Tool {
548 name: "tool".to_string(),
549 description: None,
550 input_schema: serde_json::json!({}),
551 output_schema: None,
552 icon: None,
553 version: None,
554 tags: vec![],
555 annotations: None,
556 }],
557 state: Arc::clone(&state),
558 ..TestBackend::default()
559 };
560 let proxy = ProxyClient::from_backend(backend);
561 let handler = ProxyToolHandler::new(
562 Tool {
563 name: "tool".to_string(),
564 description: None,
565 input_schema: serde_json::json!({}),
566 output_schema: None,
567 icon: None,
568 version: None,
569 tags: vec![],
570 annotations: None,
571 },
572 proxy,
573 );
574
575 let ctx = McpContext::new(Cx::for_testing(), 1);
576 let args = serde_json::json!({"value": 1});
577 let result = handler.call(&ctx, args.clone()).expect("call ok");
578 assert_eq!(result.len(), 1);
579
580 let guard = state.lock().expect("state lock poisoned");
581 let (name, recorded_args) = guard
582 .last_tool
583 .as_ref()
584 .expect("tool call recorded")
585 .clone();
586 assert_eq!(name, "tool");
587 assert_eq!(recorded_args, args);
588 }
589
590 #[test]
591 fn proxy_prompt_handler_forwards_calls() {
592 let state = Arc::new(Mutex::new(TestState::default()));
593 let backend = TestBackend {
594 prompts: vec![Prompt {
595 name: "prompt".to_string(),
596 description: None,
597 arguments: Vec::new(),
598 icon: None,
599 version: None,
600 tags: vec![],
601 }],
602 state: Arc::clone(&state),
603 ..TestBackend::default()
604 };
605 let proxy = ProxyClient::from_backend(backend);
606 let handler = ProxyPromptHandler::new(
607 Prompt {
608 name: "prompt".to_string(),
609 description: None,
610 arguments: Vec::new(),
611 icon: None,
612 version: None,
613 tags: vec![],
614 },
615 proxy,
616 );
617
618 let ctx = McpContext::new(Cx::for_testing(), 1);
619 let mut args = HashMap::new();
620 args.insert("key".to_string(), "value".to_string());
621 let result = handler.get(&ctx, args.clone()).expect("get ok");
622 assert_eq!(result.len(), 1);
623
624 let guard = state.lock().expect("state lock poisoned");
625 let (name, recorded_args) = guard
626 .last_prompt
627 .as_ref()
628 .expect("prompt call recorded")
629 .clone();
630 assert_eq!(name, "prompt");
631 assert_eq!(recorded_args, args);
632 }
633
634 #[test]
639 fn prefixed_tool_handler_uses_correct_names() {
640 let state = Arc::new(Mutex::new(TestState::default()));
641 let backend = TestBackend {
642 tools: vec![Tool {
643 name: "query".to_string(),
644 description: Some("Execute a query".to_string()),
645 input_schema: serde_json::json!({}),
646 output_schema: None,
647 icon: None,
648 version: None,
649 tags: vec![],
650 annotations: None,
651 }],
652 state: Arc::clone(&state),
653 ..TestBackend::default()
654 };
655 let proxy = ProxyClient::from_backend(backend);
656
657 let handler = ProxyToolHandler::with_prefix(
659 Tool {
660 name: "query".to_string(),
661 description: Some("Execute a query".to_string()),
662 input_schema: serde_json::json!({}),
663 output_schema: None,
664 icon: None,
665 version: None,
666 tags: vec![],
667 annotations: None,
668 },
669 "db",
670 proxy,
671 );
672
673 let def = handler.definition();
675 assert_eq!(def.name, "db/query");
676 assert_eq!(def.description, Some("Execute a query".to_string()));
677
678 let ctx = McpContext::new(Cx::for_testing(), 1);
680 let args = serde_json::json!({"sql": "SELECT 1"});
681 handler.call(&ctx, args.clone()).expect("call ok");
682
683 let guard = state.lock().expect("state lock poisoned");
684 let (forwarded_name, _) = guard.last_tool.as_ref().expect("tool called").clone();
685 assert_eq!(forwarded_name, "query"); }
687
688 #[test]
689 fn prefixed_prompt_handler_uses_correct_names() {
690 let state = Arc::new(Mutex::new(TestState::default()));
691 let backend = TestBackend {
692 prompts: vec![Prompt {
693 name: "greeting".to_string(),
694 description: Some("A greeting prompt".to_string()),
695 arguments: Vec::new(),
696 icon: None,
697 version: None,
698 tags: vec![],
699 }],
700 state: Arc::clone(&state),
701 ..TestBackend::default()
702 };
703 let proxy = ProxyClient::from_backend(backend);
704
705 let handler = ProxyPromptHandler::with_prefix(
707 Prompt {
708 name: "greeting".to_string(),
709 description: Some("A greeting prompt".to_string()),
710 arguments: Vec::new(),
711 icon: None,
712 version: None,
713 tags: vec![],
714 },
715 "templates",
716 proxy,
717 );
718
719 let def = handler.definition();
721 assert_eq!(def.name, "templates/greeting");
722 assert_eq!(def.description, Some("A greeting prompt".to_string()));
723
724 let ctx = McpContext::new(Cx::for_testing(), 1);
726 let args = HashMap::new();
727 handler.get(&ctx, args).expect("get ok");
728
729 let guard = state.lock().expect("state lock poisoned");
730 let (forwarded_name, _) = guard.last_prompt.as_ref().expect("prompt called").clone();
731 assert_eq!(forwarded_name, "greeting"); }
733
734 #[test]
735 fn prefixed_resource_handler_uses_correct_uri() {
736 use super::ProxyResourceHandler;
737 use crate::handler::ResourceHandler;
738
739 let backend = TestBackend {
740 resources: vec![Resource {
741 uri: "file://data".to_string(),
742 name: "Data File".to_string(),
743 description: None,
744 mime_type: None,
745 icon: None,
746 version: None,
747 tags: vec![],
748 }],
749 ..TestBackend::default()
750 };
751 let proxy = ProxyClient::from_backend(backend);
752
753 let handler = ProxyResourceHandler::with_prefix(
755 Resource {
756 uri: "file://data".to_string(),
757 name: "Data File".to_string(),
758 description: None,
759 mime_type: None,
760 icon: None,
761 version: None,
762 tags: vec![],
763 },
764 "storage",
765 proxy,
766 );
767
768 let def = handler.definition();
770 assert_eq!(def.uri, "storage/file://data");
771 assert_eq!(def.name, "Data File");
772 }
773
774 #[test]
779 fn proxy_catalog_empty_backend() {
780 let mut backend = TestBackend::default();
781 let catalog = ProxyCatalog::from_backend(&mut backend).expect("catalog");
782 assert!(catalog.tools.is_empty());
783 assert!(catalog.resources.is_empty());
784 assert!(catalog.resource_templates.is_empty());
785 assert!(catalog.prompts.is_empty());
786 }
787
788 #[test]
789 fn proxy_catalog_default_is_empty() {
790 let catalog = ProxyCatalog::default();
791 assert!(catalog.tools.is_empty());
792 assert!(catalog.resources.is_empty());
793 assert!(catalog.resource_templates.is_empty());
794 assert!(catalog.prompts.is_empty());
795 }
796
797 #[test]
798 fn proxy_catalog_multiple_items() {
799 let mut backend = TestBackend {
800 tools: vec![
801 Tool {
802 name: "t1".to_string(),
803 description: None,
804 input_schema: serde_json::json!({}),
805 output_schema: None,
806 icon: None,
807 version: None,
808 tags: vec![],
809 annotations: None,
810 },
811 Tool {
812 name: "t2".to_string(),
813 description: None,
814 input_schema: serde_json::json!({}),
815 output_schema: None,
816 icon: None,
817 version: None,
818 tags: vec![],
819 annotations: None,
820 },
821 ],
822 prompts: vec![
823 Prompt {
824 name: "p1".to_string(),
825 description: None,
826 arguments: Vec::new(),
827 icon: None,
828 version: None,
829 tags: vec![],
830 },
831 Prompt {
832 name: "p2".to_string(),
833 description: None,
834 arguments: Vec::new(),
835 icon: None,
836 version: None,
837 tags: vec![],
838 },
839 ],
840 ..TestBackend::default()
841 };
842 let catalog = ProxyCatalog::from_backend(&mut backend).expect("catalog");
843 assert_eq!(catalog.tools.len(), 2);
844 assert_eq!(catalog.prompts.len(), 2);
845 }
846
847 #[test]
852 fn proxy_client_clone_shares_backend() {
853 let state = Arc::new(Mutex::new(TestState::default()));
854 let backend = TestBackend {
855 tools: vec![Tool {
856 name: "shared".to_string(),
857 description: None,
858 input_schema: serde_json::json!({}),
859 output_schema: None,
860 icon: None,
861 version: None,
862 tags: vec![],
863 annotations: None,
864 }],
865 state: Arc::clone(&state),
866 ..TestBackend::default()
867 };
868 let proxy1 = ProxyClient::from_backend(backend);
869 let proxy2 = proxy1.clone();
870
871 let catalog1 = proxy1.catalog().expect("catalog1");
873 let catalog2 = proxy2.catalog().expect("catalog2");
874 assert_eq!(catalog1.tools.len(), catalog2.tools.len());
875 }
876
877 #[test]
878 fn proxy_client_catalog_fetches_all() {
879 let backend = TestBackend {
880 tools: vec![Tool {
881 name: "t".to_string(),
882 description: None,
883 input_schema: serde_json::json!({}),
884 output_schema: None,
885 icon: None,
886 version: None,
887 tags: vec![],
888 annotations: None,
889 }],
890 resources: vec![Resource {
891 uri: "test://r".to_string(),
892 name: "r".to_string(),
893 description: None,
894 mime_type: None,
895 icon: None,
896 version: None,
897 tags: vec![],
898 }],
899 prompts: vec![Prompt {
900 name: "p".to_string(),
901 description: None,
902 arguments: Vec::new(),
903 icon: None,
904 version: None,
905 tags: vec![],
906 }],
907 ..TestBackend::default()
908 };
909 let proxy = ProxyClient::from_backend(backend);
910 let catalog = proxy.catalog().expect("catalog");
911 assert_eq!(catalog.tools.len(), 1);
912 assert_eq!(catalog.resources.len(), 1);
913 assert_eq!(catalog.prompts.len(), 1);
914 }
915
916 #[test]
921 fn proxy_resource_handler_read_forwards_to_backend() {
922 use super::ProxyResourceHandler;
923 use crate::handler::ResourceHandler;
924
925 let backend = TestBackend::default();
926 let proxy = ProxyClient::from_backend(backend);
927 let handler = ProxyResourceHandler::new(
928 Resource {
929 uri: "test://resource".to_string(),
930 name: "Test".to_string(),
931 description: None,
932 mime_type: None,
933 icon: None,
934 version: None,
935 tags: vec![],
936 },
937 proxy,
938 );
939
940 let ctx = McpContext::new(Cx::for_testing(), 1);
941 let result = handler.read(&ctx).expect("read ok");
942 assert_eq!(result.len(), 1);
943 assert_eq!(result[0].text, Some("resource".to_string()));
944 }
945
946 #[test]
947 fn proxy_resource_handler_no_template_by_default() {
948 use super::ProxyResourceHandler;
949 use crate::handler::ResourceHandler;
950
951 let backend = TestBackend::default();
952 let proxy = ProxyClient::from_backend(backend);
953 let handler = ProxyResourceHandler::new(
954 Resource {
955 uri: "test://x".to_string(),
956 name: "x".to_string(),
957 description: None,
958 mime_type: None,
959 icon: None,
960 version: None,
961 tags: vec![],
962 },
963 proxy,
964 );
965 assert!(handler.template().is_none());
966 }
967
968 #[test]
969 fn proxy_resource_handler_from_template() {
970 use super::ProxyResourceHandler;
971 use crate::handler::ResourceHandler;
972 use fastmcp_protocol::ResourceTemplate;
973
974 let backend = TestBackend::default();
975 let proxy = ProxyClient::from_backend(backend);
976 let template = ResourceTemplate {
977 uri_template: "file://{path}".to_string(),
978 name: "File".to_string(),
979 description: Some("A file resource".to_string()),
980 mime_type: Some("text/plain".to_string()),
981 icon: None,
982 version: None,
983 tags: vec![],
984 };
985 let handler = ProxyResourceHandler::from_template(template.clone(), proxy);
986
987 let def = handler.definition();
989 assert_eq!(def.uri, "file://{path}");
990 assert_eq!(def.name, "File");
991 assert_eq!(def.description, Some("A file resource".to_string()));
992 assert_eq!(def.mime_type, Some("text/plain".to_string()));
993
994 let tmpl = handler.template().expect("has template");
996 assert_eq!(tmpl.uri_template, "file://{path}");
997 }
998
999 #[test]
1000 fn proxy_resource_handler_from_template_with_prefix() {
1001 use super::ProxyResourceHandler;
1002 use crate::handler::ResourceHandler;
1003 use fastmcp_protocol::ResourceTemplate;
1004
1005 let backend = TestBackend::default();
1006 let proxy = ProxyClient::from_backend(backend);
1007 let template = ResourceTemplate {
1008 uri_template: "file://{path}".to_string(),
1009 name: "File".to_string(),
1010 description: None,
1011 mime_type: None,
1012 icon: None,
1013 version: None,
1014 tags: vec![],
1015 };
1016 let handler = ProxyResourceHandler::from_template_with_prefix(template, "storage", proxy);
1017
1018 let def = handler.definition();
1020 assert_eq!(def.uri, "storage/file://{path}");
1021
1022 let tmpl = handler.template().expect("has template");
1024 assert_eq!(tmpl.uri_template, "storage/file://{path}");
1025 }
1026
1027 struct FailingBackend;
1033
1034 impl ProxyBackend for FailingBackend {
1035 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
1036 Err(fastmcp_core::McpError::internal_error("tool list failed"))
1037 }
1038
1039 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
1040 Err(fastmcp_core::McpError::internal_error(
1041 "resource list failed",
1042 ))
1043 }
1044
1045 fn list_resource_templates(
1046 &mut self,
1047 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
1048 Err(fastmcp_core::McpError::internal_error(
1049 "template list failed",
1050 ))
1051 }
1052
1053 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
1054 Err(fastmcp_core::McpError::internal_error("prompt list failed"))
1055 }
1056
1057 fn call_tool(
1058 &mut self,
1059 _name: &str,
1060 _arguments: serde_json::Value,
1061 ) -> fastmcp_core::McpResult<Vec<Content>> {
1062 Err(fastmcp_core::McpError::internal_error("tool call failed"))
1063 }
1064
1065 fn call_tool_with_progress(
1066 &mut self,
1067 _name: &str,
1068 _arguments: serde_json::Value,
1069 _on_progress: super::ProgressCallback<'_>,
1070 ) -> fastmcp_core::McpResult<Vec<Content>> {
1071 Err(fastmcp_core::McpError::internal_error("tool call failed"))
1072 }
1073
1074 fn read_resource(&mut self, _uri: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
1075 Err(fastmcp_core::McpError::internal_error(
1076 "resource read failed",
1077 ))
1078 }
1079
1080 fn get_prompt(
1081 &mut self,
1082 _name: &str,
1083 _arguments: HashMap<String, String>,
1084 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
1085 Err(fastmcp_core::McpError::internal_error("prompt get failed"))
1086 }
1087 }
1088
1089 #[test]
1090 fn proxy_catalog_propagates_tool_list_error() {
1091 let mut backend = FailingBackend;
1092 let result = ProxyCatalog::from_backend(&mut backend);
1093 assert!(result.is_err());
1094 let err = result.unwrap_err();
1095 assert!(err.message.contains("tool list failed"));
1096 }
1097
1098 #[test]
1099 fn proxy_tool_handler_propagates_call_error() {
1100 let proxy = ProxyClient::from_backend(FailingBackend);
1101 let handler = ProxyToolHandler::new(
1102 Tool {
1103 name: "fail".to_string(),
1104 description: None,
1105 input_schema: serde_json::json!({}),
1106 output_schema: None,
1107 icon: None,
1108 version: None,
1109 tags: vec![],
1110 annotations: None,
1111 },
1112 proxy,
1113 );
1114
1115 let ctx = McpContext::new(Cx::for_testing(), 1);
1116 let result = handler.call(&ctx, serde_json::json!({}));
1117 assert!(result.is_err());
1118 assert!(result.unwrap_err().message.contains("tool call failed"));
1119 }
1120
1121 #[test]
1122 fn proxy_resource_handler_propagates_read_error() {
1123 use super::ProxyResourceHandler;
1124 use crate::handler::ResourceHandler;
1125
1126 let proxy = ProxyClient::from_backend(FailingBackend);
1127 let handler = ProxyResourceHandler::new(
1128 Resource {
1129 uri: "test://fail".to_string(),
1130 name: "Fail".to_string(),
1131 description: None,
1132 mime_type: None,
1133 icon: None,
1134 version: None,
1135 tags: vec![],
1136 },
1137 proxy,
1138 );
1139
1140 let ctx = McpContext::new(Cx::for_testing(), 1);
1141 let result = handler.read(&ctx);
1142 assert!(result.is_err());
1143 assert!(result.unwrap_err().message.contains("resource read failed"));
1144 }
1145
1146 #[test]
1147 fn proxy_prompt_handler_propagates_get_error() {
1148 let proxy = ProxyClient::from_backend(FailingBackend);
1149 let handler = ProxyPromptHandler::new(
1150 Prompt {
1151 name: "fail".to_string(),
1152 description: None,
1153 arguments: Vec::new(),
1154 icon: None,
1155 version: None,
1156 tags: vec![],
1157 },
1158 proxy,
1159 );
1160
1161 let ctx = McpContext::new(Cx::for_testing(), 1);
1162 let result = handler.get(&ctx, HashMap::new());
1163 assert!(result.is_err());
1164 assert!(result.unwrap_err().message.contains("prompt get failed"));
1165 }
1166
1167 #[test]
1172 fn resource_from_template_copies_all_fields() {
1173 use fastmcp_protocol::ResourceTemplate;
1174
1175 let template = ResourceTemplate {
1176 uri_template: "db://{table}/{id}".to_string(),
1177 name: "Database Record".to_string(),
1178 description: Some("A database record".to_string()),
1179 mime_type: Some("application/json".to_string()),
1180 icon: None,
1181 version: Some("1.0.0".to_string()),
1182 tags: vec!["db".to_string()],
1183 };
1184 let resource = super::resource_from_template(&template);
1185 assert_eq!(resource.uri, "db://{table}/{id}");
1186 assert_eq!(resource.name, "Database Record");
1187 assert_eq!(resource.description, Some("A database record".to_string()));
1188 assert_eq!(resource.mime_type, Some("application/json".to_string()));
1189 assert_eq!(resource.version, Some("1.0.0".to_string()));
1190 assert_eq!(resource.tags, vec!["db".to_string()]);
1191 }
1192
1193 #[test]
1198 fn proxy_catalog_debug() {
1199 let catalog = ProxyCatalog {
1200 tools: vec![Tool {
1201 name: "dbg-tool".to_string(),
1202 description: None,
1203 input_schema: serde_json::json!({}),
1204 output_schema: None,
1205 icon: None,
1206 version: None,
1207 tags: vec![],
1208 annotations: None,
1209 }],
1210 ..ProxyCatalog::default()
1211 };
1212 let debug = format!("{:?}", catalog);
1213 assert!(debug.contains("ProxyCatalog"));
1214 assert!(debug.contains("dbg-tool"));
1215 }
1216
1217 #[test]
1218 fn proxy_catalog_clone() {
1219 let catalog = ProxyCatalog {
1220 tools: vec![Tool {
1221 name: "cloned".to_string(),
1222 description: None,
1223 input_schema: serde_json::json!({}),
1224 output_schema: None,
1225 icon: None,
1226 version: None,
1227 tags: vec![],
1228 annotations: None,
1229 }],
1230 ..ProxyCatalog::default()
1231 };
1232 let cloned = catalog.clone();
1233 assert_eq!(cloned.tools.len(), 1);
1234 assert_eq!(cloned.tools[0].name, "cloned");
1235 }
1236
1237 #[test]
1242 fn proxy_resource_handler_read_with_uri_uses_params() {
1243 use super::ProxyResourceHandler;
1244 use crate::handler::ResourceHandler;
1245
1246 let backend = TestBackend::default();
1247 let proxy = ProxyClient::from_backend(backend);
1248 let handler = ProxyResourceHandler::new(
1249 Resource {
1250 uri: "test://r".to_string(),
1251 name: "R".to_string(),
1252 description: None,
1253 mime_type: None,
1254 icon: None,
1255 version: None,
1256 tags: vec![],
1257 },
1258 proxy,
1259 );
1260
1261 let ctx = McpContext::new(Cx::for_testing(), 1);
1262 let params = HashMap::new();
1263 let result = handler
1264 .read_with_uri(&ctx, "test://r", ¶ms)
1265 .expect("read ok");
1266 assert_eq!(result.len(), 1);
1267 }
1268
1269 #[test]
1270 fn proxy_resource_handler_read_with_uri_strips_prefix() {
1271 use super::ProxyResourceHandler;
1272 use crate::handler::ResourceHandler;
1273
1274 let backend = TestBackend::default();
1275 let proxy = ProxyClient::from_backend(backend);
1276 let handler = ProxyResourceHandler::with_prefix(
1277 Resource {
1278 uri: "file://data".to_string(),
1279 name: "Data".to_string(),
1280 description: None,
1281 mime_type: None,
1282 icon: None,
1283 version: None,
1284 tags: vec![],
1285 },
1286 "ext",
1287 proxy,
1288 );
1289
1290 let ctx = McpContext::new(Cx::for_testing(), 1);
1291 let params = HashMap::new();
1292 let result = handler
1294 .read_with_uri(&ctx, "ext/file://data", ¶ms)
1295 .expect("read ok");
1296 assert_eq!(result.len(), 1);
1297 }
1298
1299 #[test]
1300 fn proxy_resource_handler_read_with_uri_no_prefix_match() {
1301 use super::ProxyResourceHandler;
1302 use crate::handler::ResourceHandler;
1303
1304 let backend = TestBackend::default();
1305 let proxy = ProxyClient::from_backend(backend);
1306 let handler = ProxyResourceHandler::new(
1307 Resource {
1308 uri: "test://r".to_string(),
1309 name: "R".to_string(),
1310 description: None,
1311 mime_type: None,
1312 icon: None,
1313 version: None,
1314 tags: vec![],
1315 },
1316 proxy,
1317 );
1318
1319 let ctx = McpContext::new(Cx::for_testing(), 1);
1320 let params = HashMap::new();
1321 let result = handler
1323 .read_with_uri(&ctx, "other://uri", ¶ms)
1324 .expect("read ok");
1325 assert_eq!(result.len(), 1);
1326 }
1327
1328 #[test]
1333 fn proxy_tool_handler_definition_returns_clone() {
1334 let backend = TestBackend::default();
1335 let proxy = ProxyClient::from_backend(backend);
1336 let handler = ProxyToolHandler::new(
1337 Tool {
1338 name: "def-tool".to_string(),
1339 description: Some("desc".to_string()),
1340 input_schema: serde_json::json!({"type": "object"}),
1341 output_schema: None,
1342 icon: None,
1343 version: None,
1344 tags: vec!["tag1".to_string()],
1345 annotations: None,
1346 },
1347 proxy,
1348 );
1349
1350 let def = handler.definition();
1351 assert_eq!(def.name, "def-tool");
1352 assert_eq!(def.description, Some("desc".to_string()));
1353 assert_eq!(def.tags, vec!["tag1".to_string()]);
1354 }
1355
1356 #[test]
1361 fn proxy_prompt_handler_definition_returns_clone() {
1362 let backend = TestBackend::default();
1363 let proxy = ProxyClient::from_backend(backend);
1364 let handler = ProxyPromptHandler::new(
1365 Prompt {
1366 name: "def-prompt".to_string(),
1367 description: Some("A prompt".to_string()),
1368 arguments: Vec::new(),
1369 icon: None,
1370 version: None,
1371 tags: vec!["tag2".to_string()],
1372 },
1373 proxy,
1374 );
1375
1376 let def = handler.definition();
1377 assert_eq!(def.name, "def-prompt");
1378 assert_eq!(def.description, Some("A prompt".to_string()));
1379 assert_eq!(def.tags, vec!["tag2".to_string()]);
1380 }
1381
1382 #[test]
1387 fn proxy_client_read_resource() {
1388 let backend = TestBackend::default();
1389 let proxy = ProxyClient::from_backend(backend);
1390 let ctx = McpContext::new(Cx::for_testing(), 1);
1391 let result = proxy.read_resource(&ctx, "test://r").expect("read ok");
1392 assert_eq!(result.len(), 1);
1393 assert_eq!(result[0].text, Some("resource".to_string()));
1394 }
1395
1396 #[test]
1397 fn proxy_client_get_prompt() {
1398 let state = Arc::new(Mutex::new(TestState::default()));
1399 let backend = TestBackend {
1400 state: Arc::clone(&state),
1401 ..TestBackend::default()
1402 };
1403 let proxy = ProxyClient::from_backend(backend);
1404 let ctx = McpContext::new(Cx::for_testing(), 1);
1405 let mut args = HashMap::new();
1406 args.insert("k".to_string(), "v".to_string());
1407 let result = proxy
1408 .get_prompt(&ctx, "test-prompt", args.clone())
1409 .expect("get ok");
1410 assert_eq!(result.len(), 1);
1411
1412 let guard = state.lock().unwrap();
1413 let (name, recorded) = guard.last_prompt.as_ref().unwrap();
1414 assert_eq!(name, "test-prompt");
1415 assert_eq!(recorded, &args);
1416 }
1417
1418 #[test]
1419 fn proxy_client_call_tool() {
1420 let state = Arc::new(Mutex::new(TestState::default()));
1421 let backend = TestBackend {
1422 state: Arc::clone(&state),
1423 ..TestBackend::default()
1424 };
1425 let proxy = ProxyClient::from_backend(backend);
1426 let ctx = McpContext::new(Cx::for_testing(), 1);
1427 let args = serde_json::json!({"x": 42});
1428 let result = proxy
1429 .call_tool(&ctx, "my-tool", args.clone())
1430 .expect("call ok");
1431 assert_eq!(result.len(), 1);
1432
1433 let guard = state.lock().unwrap();
1434 let (name, recorded) = guard.last_tool.as_ref().unwrap();
1435 assert_eq!(name, "my-tool");
1436 assert_eq!(recorded, &args);
1437 }
1438
1439 #[test]
1444 fn proxy_resource_handler_new_stores_external_uri() {
1445 use super::ProxyResourceHandler;
1446
1447 let backend = TestBackend::default();
1448 let proxy = ProxyClient::from_backend(backend);
1449 let handler = ProxyResourceHandler::new(
1450 Resource {
1451 uri: "original://uri".to_string(),
1452 name: "Orig".to_string(),
1453 description: None,
1454 mime_type: None,
1455 icon: None,
1456 version: None,
1457 tags: vec![],
1458 },
1459 proxy,
1460 );
1461 assert_eq!(handler.external_uri, "original://uri");
1462 }
1463
1464 #[test]
1465 fn proxy_resource_handler_with_prefix_stores_external_uri() {
1466 use super::ProxyResourceHandler;
1467
1468 let backend = TestBackend::default();
1469 let proxy = ProxyClient::from_backend(backend);
1470 let handler = ProxyResourceHandler::with_prefix(
1471 Resource {
1472 uri: "original://uri".to_string(),
1473 name: "Orig".to_string(),
1474 description: None,
1475 mime_type: None,
1476 icon: None,
1477 version: None,
1478 tags: vec![],
1479 },
1480 "pfx",
1481 proxy,
1482 );
1483 assert_eq!(handler.external_uri, "original://uri");
1485 assert_eq!(handler.resource.uri, "pfx/original://uri");
1487 }
1488
1489 #[test]
1494 fn proxy_tool_handler_new_stores_external_name() {
1495 let backend = TestBackend::default();
1496 let proxy = ProxyClient::from_backend(backend);
1497 let handler = ProxyToolHandler::new(
1498 Tool {
1499 name: "orig-name".to_string(),
1500 description: None,
1501 input_schema: serde_json::json!({}),
1502 output_schema: None,
1503 icon: None,
1504 version: None,
1505 tags: vec![],
1506 annotations: None,
1507 },
1508 proxy,
1509 );
1510 assert_eq!(handler.external_name, "orig-name");
1511 assert_eq!(handler.tool.name, "orig-name");
1512 }
1513
1514 #[test]
1515 fn proxy_tool_handler_with_prefix_stores_external_name() {
1516 let backend = TestBackend::default();
1517 let proxy = ProxyClient::from_backend(backend);
1518 let handler = ProxyToolHandler::with_prefix(
1519 Tool {
1520 name: "orig".to_string(),
1521 description: None,
1522 input_schema: serde_json::json!({}),
1523 output_schema: None,
1524 icon: None,
1525 version: None,
1526 tags: vec![],
1527 annotations: None,
1528 },
1529 "ns",
1530 proxy,
1531 );
1532 assert_eq!(handler.external_name, "orig");
1533 assert_eq!(handler.tool.name, "ns/orig");
1534 }
1535
1536 #[test]
1541 fn proxy_prompt_handler_new_stores_external_name() {
1542 let backend = TestBackend::default();
1543 let proxy = ProxyClient::from_backend(backend);
1544 let handler = ProxyPromptHandler::new(
1545 Prompt {
1546 name: "orig-prompt".to_string(),
1547 description: None,
1548 arguments: Vec::new(),
1549 icon: None,
1550 version: None,
1551 tags: vec![],
1552 },
1553 proxy,
1554 );
1555 assert_eq!(handler.external_name, "orig-prompt");
1556 }
1557
1558 #[test]
1559 fn proxy_prompt_handler_with_prefix_stores_external_name() {
1560 let backend = TestBackend::default();
1561 let proxy = ProxyClient::from_backend(backend);
1562 let handler = ProxyPromptHandler::with_prefix(
1563 Prompt {
1564 name: "prompt1".to_string(),
1565 description: None,
1566 arguments: Vec::new(),
1567 icon: None,
1568 version: None,
1569 tags: vec![],
1570 },
1571 "scope",
1572 proxy,
1573 );
1574 assert_eq!(handler.external_name, "prompt1");
1575 assert_eq!(handler.prompt.name, "scope/prompt1");
1576 }
1577
1578 #[test]
1583 fn resource_from_template_minimal_fields() {
1584 use fastmcp_protocol::ResourceTemplate;
1585
1586 let template = ResourceTemplate {
1587 uri_template: "test://{id}".to_string(),
1588 name: "Minimal".to_string(),
1589 description: None,
1590 mime_type: None,
1591 icon: None,
1592 version: None,
1593 tags: vec![],
1594 };
1595 let resource = super::resource_from_template(&template);
1596 assert_eq!(resource.uri, "test://{id}");
1597 assert_eq!(resource.name, "Minimal");
1598 assert!(resource.description.is_none());
1599 assert!(resource.mime_type.is_none());
1600 assert!(resource.icon.is_none());
1601 assert!(resource.version.is_none());
1602 assert!(resource.tags.is_empty());
1603 }
1604
1605 #[test]
1610 fn proxy_client_read_resource_propagates_error() {
1611 let proxy = ProxyClient::from_backend(FailingBackend);
1612 let ctx = McpContext::new(Cx::for_testing(), 1);
1613 let result = proxy.read_resource(&ctx, "test://x");
1614 assert!(result.is_err());
1615 assert!(result.unwrap_err().message.contains("resource read failed"));
1616 }
1617
1618 #[test]
1619 fn proxy_client_get_prompt_propagates_error() {
1620 let proxy = ProxyClient::from_backend(FailingBackend);
1621 let ctx = McpContext::new(Cx::for_testing(), 1);
1622 let result = proxy.get_prompt(&ctx, "fail", HashMap::new());
1623 assert!(result.is_err());
1624 assert!(result.unwrap_err().message.contains("prompt get failed"));
1625 }
1626
1627 #[test]
1628 fn proxy_client_call_tool_propagates_error() {
1629 let proxy = ProxyClient::from_backend(FailingBackend);
1630 let ctx = McpContext::new(Cx::for_testing(), 1);
1631 let result = proxy.call_tool(&ctx, "fail", serde_json::json!({}));
1632 assert!(result.is_err());
1633 assert!(result.unwrap_err().message.contains("tool call failed"));
1634 }
1635
1636 #[test]
1641 fn proxy_client_lock_poison_returns_error() {
1642 let backend = TestBackend::default();
1643 let proxy = ProxyClient::from_backend(backend);
1644
1645 let proxy2 = proxy.clone();
1647 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1648 let _guard = proxy2.inner.lock().unwrap();
1649 panic!("intentional poison");
1650 }));
1651
1652 let result = proxy.catalog();
1654 assert!(result.is_err());
1655 assert!(
1656 result
1657 .unwrap_err()
1658 .message
1659 .contains("Proxy backend lock poisoned")
1660 );
1661 }
1662
1663 #[test]
1668 fn proxy_resource_handler_from_template_stores_external_uri() {
1669 use super::ProxyResourceHandler;
1670 use fastmcp_protocol::ResourceTemplate;
1671
1672 let backend = TestBackend::default();
1673 let proxy = ProxyClient::from_backend(backend);
1674 let template = ResourceTemplate {
1675 uri_template: "file://{path}".to_string(),
1676 name: "File".to_string(),
1677 description: None,
1678 mime_type: None,
1679 icon: None,
1680 version: None,
1681 tags: vec![],
1682 };
1683 let handler = ProxyResourceHandler::from_template(template, proxy);
1684 assert_eq!(handler.external_uri, "file://{path}");
1685 }
1686
1687 #[test]
1688 fn proxy_resource_handler_from_template_with_prefix_stores_external_uri() {
1689 use super::ProxyResourceHandler;
1690 use fastmcp_protocol::ResourceTemplate;
1691
1692 let backend = TestBackend::default();
1693 let proxy = ProxyClient::from_backend(backend);
1694 let template = ResourceTemplate {
1695 uri_template: "db://{table}".to_string(),
1696 name: "DB".to_string(),
1697 description: None,
1698 mime_type: None,
1699 icon: None,
1700 version: None,
1701 tags: vec![],
1702 };
1703 let handler = ProxyResourceHandler::from_template_with_prefix(template, "remote", proxy);
1704 assert_eq!(handler.external_uri, "db://{table}");
1706 assert_eq!(handler.resource.uri, "remote/db://{table}");
1708 let tmpl = handler.template.unwrap();
1710 assert_eq!(tmpl.uri_template, "remote/db://{table}");
1711 }
1712
1713 struct TestNotificationSender {
1718 calls: Mutex<Vec<(f64, Option<f64>, Option<String>)>>,
1719 }
1720
1721 impl fastmcp_core::NotificationSender for TestNotificationSender {
1722 fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
1723 self.calls
1724 .lock()
1725 .unwrap()
1726 .push((progress, total, message.map(|s| s.to_string())));
1727 }
1728 }
1729
1730 #[test]
1731 fn proxy_client_call_tool_with_progress_reporter() {
1732 use fastmcp_core::ProgressReporter;
1733
1734 let state = Arc::new(Mutex::new(TestState::default()));
1735 let backend = TestBackend {
1736 state: Arc::clone(&state),
1737 ..TestBackend::default()
1738 };
1739 let proxy = ProxyClient::from_backend(backend);
1740
1741 let sender = Arc::new(TestNotificationSender {
1742 calls: Mutex::new(Vec::new()),
1743 });
1744 let reporter =
1745 ProgressReporter::new(Arc::clone(&sender) as Arc<dyn fastmcp_core::NotificationSender>);
1746 let ctx = McpContext::with_progress(Cx::for_testing(), 1, reporter);
1747
1748 let result = proxy
1749 .call_tool(&ctx, "progress-tool", serde_json::json!({"x": 1}))
1750 .expect("call ok");
1751 assert_eq!(result.len(), 1);
1752
1753 let calls = sender.calls.lock().unwrap();
1756 assert!(!calls.is_empty());
1757 assert!((calls[0].0 - 0.5).abs() < f64::EPSILON);
1758 assert!(calls[0].1.is_some_and(|v| (v - 1.0).abs() < f64::EPSILON));
1759 }
1760
1761 #[test]
1766 fn proxy_resource_handler_read_with_uri_resource_uri_no_slash() {
1767 use super::ProxyResourceHandler;
1768 use crate::handler::ResourceHandler;
1769
1770 let backend = TestBackend::default();
1771 let proxy = ProxyClient::from_backend(backend);
1772 let handler = ProxyResourceHandler::new(
1774 Resource {
1775 uri: "noslash".to_string(),
1776 name: "NoSlash".to_string(),
1777 description: None,
1778 mime_type: None,
1779 icon: None,
1780 version: None,
1781 tags: vec![],
1782 },
1783 proxy,
1784 );
1785
1786 let ctx = McpContext::new(Cx::for_testing(), 1);
1787 let params = HashMap::new();
1788 let result = handler
1790 .read_with_uri(&ctx, "noslash/rest", ¶ms)
1791 .expect("read ok");
1792 assert_eq!(result.len(), 1);
1793 }
1794
1795 #[test]
1800 fn proxy_catalog_collects_resource_templates() {
1801 use fastmcp_protocol::ResourceTemplate;
1802
1803 struct TemplateBackend;
1804 impl ProxyBackend for TemplateBackend {
1805 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
1806 Ok(vec![])
1807 }
1808 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
1809 Ok(vec![])
1810 }
1811 fn list_resource_templates(
1812 &mut self,
1813 ) -> fastmcp_core::McpResult<Vec<ResourceTemplate>> {
1814 Ok(vec![ResourceTemplate {
1815 uri_template: "tmpl://{id}".to_string(),
1816 name: "Template".to_string(),
1817 description: None,
1818 mime_type: None,
1819 icon: None,
1820 version: None,
1821 tags: vec![],
1822 }])
1823 }
1824 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
1825 Ok(vec![])
1826 }
1827 fn call_tool(
1828 &mut self,
1829 _: &str,
1830 _: serde_json::Value,
1831 ) -> fastmcp_core::McpResult<Vec<Content>> {
1832 Ok(vec![])
1833 }
1834 fn call_tool_with_progress(
1835 &mut self,
1836 _: &str,
1837 _: serde_json::Value,
1838 _: super::ProgressCallback<'_>,
1839 ) -> fastmcp_core::McpResult<Vec<Content>> {
1840 Ok(vec![])
1841 }
1842 fn read_resource(&mut self, _: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
1843 Ok(vec![])
1844 }
1845 fn get_prompt(
1846 &mut self,
1847 _: &str,
1848 _: HashMap<String, String>,
1849 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
1850 Ok(vec![])
1851 }
1852 }
1853
1854 let mut backend = TemplateBackend;
1855 let catalog = ProxyCatalog::from_backend(&mut backend).expect("catalog");
1856 assert_eq!(catalog.resource_templates.len(), 1);
1857 assert_eq!(catalog.resource_templates[0].uri_template, "tmpl://{id}");
1858 }
1859
1860 #[test]
1865 fn proxy_catalog_propagates_resource_list_error() {
1866 let mut backend = FailingBackend;
1868 let result = ProxyCatalog::from_backend(&mut backend);
1869 assert!(result.is_err());
1870 assert!(result.unwrap_err().message.contains("tool list failed"));
1872 }
1873
1874 #[test]
1879 fn proxy_client_call_tool_no_progress_uses_plain_call() {
1880 let state = Arc::new(Mutex::new(TestState::default()));
1881 let backend = TestBackend {
1882 state: Arc::clone(&state),
1883 ..TestBackend::default()
1884 };
1885 let proxy = ProxyClient::from_backend(backend);
1886
1887 let ctx = McpContext::new(Cx::for_testing(), 1);
1889 assert!(!ctx.has_progress_reporter());
1890
1891 let result = proxy
1892 .call_tool(&ctx, "plain-tool", serde_json::json!({"y": 2}))
1893 .expect("call ok");
1894 assert_eq!(result.len(), 1);
1895
1896 let guard = state.lock().unwrap();
1897 let (name, _) = guard.last_tool.as_ref().unwrap();
1898 assert_eq!(name, "plain-tool");
1899 }
1900
1901 #[test]
1906 fn resource_from_template_copies_icon() {
1907 use fastmcp_protocol::{Icon, ResourceTemplate};
1908
1909 let icon = Icon {
1910 src: Some("https://example.com/star.png".to_string()),
1911 mime_type: None,
1912 sizes: None,
1913 };
1914 let template = ResourceTemplate {
1915 uri_template: "icon://{x}".to_string(),
1916 name: "WithIcon".to_string(),
1917 description: None,
1918 mime_type: None,
1919 icon: Some(icon.clone()),
1920 version: None,
1921 tags: vec![],
1922 };
1923 let resource = super::resource_from_template(&template);
1924 assert_eq!(resource.icon, Some(icon));
1925 }
1926
1927 struct NoTotalProgressBackend {
1934 state: Arc<Mutex<TestState>>,
1935 }
1936
1937 impl ProxyBackend for NoTotalProgressBackend {
1938 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
1939 Ok(vec![])
1940 }
1941 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
1942 Ok(vec![])
1943 }
1944 fn list_resource_templates(
1945 &mut self,
1946 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
1947 Ok(vec![])
1948 }
1949 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
1950 Ok(vec![])
1951 }
1952 fn call_tool(
1953 &mut self,
1954 name: &str,
1955 arguments: serde_json::Value,
1956 ) -> fastmcp_core::McpResult<Vec<Content>> {
1957 let mut guard = self.state.lock().expect("state lock poisoned");
1958 guard.last_tool.replace((name.to_string(), arguments));
1959 Ok(vec![Content::Text {
1960 text: "ok".to_string(),
1961 }])
1962 }
1963 fn call_tool_with_progress(
1964 &mut self,
1965 name: &str,
1966 arguments: serde_json::Value,
1967 on_progress: super::ProgressCallback<'_>,
1968 ) -> fastmcp_core::McpResult<Vec<Content>> {
1969 on_progress(0.3, None, Some("partial".to_string()));
1971 self.call_tool(name, arguments)
1972 }
1973 fn read_resource(&mut self, _uri: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
1974 Ok(vec![])
1975 }
1976 fn get_prompt(
1977 &mut self,
1978 _name: &str,
1979 _arguments: HashMap<String, String>,
1980 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
1981 Ok(vec![])
1982 }
1983 }
1984
1985 #[test]
1986 fn proxy_client_call_tool_with_progress_none_total() {
1987 use fastmcp_core::ProgressReporter;
1988
1989 let state = Arc::new(Mutex::new(TestState::default()));
1990 let backend = NoTotalProgressBackend {
1991 state: Arc::clone(&state),
1992 };
1993 let proxy = ProxyClient::from_backend(backend);
1994
1995 let sender = Arc::new(TestNotificationSender {
1996 calls: Mutex::new(Vec::new()),
1997 });
1998 let reporter =
1999 ProgressReporter::new(Arc::clone(&sender) as Arc<dyn fastmcp_core::NotificationSender>);
2000 let ctx = McpContext::with_progress(Cx::for_testing(), 1, reporter);
2001
2002 let result = proxy
2003 .call_tool(&ctx, "no-total", serde_json::json!({}))
2004 .expect("call ok");
2005 assert_eq!(result.len(), 1);
2006
2007 let calls = sender.calls.lock().unwrap();
2008 assert!(!calls.is_empty());
2009 assert!(calls[0].1.is_none());
2011 }
2012
2013 struct FailAtResourcesBackend;
2019
2020 impl ProxyBackend for FailAtResourcesBackend {
2021 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
2022 Ok(vec![])
2023 }
2024 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
2025 Err(fastmcp_core::McpError::internal_error(
2026 "resource list failed",
2027 ))
2028 }
2029 fn list_resource_templates(
2030 &mut self,
2031 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
2032 Ok(vec![])
2033 }
2034 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
2035 Ok(vec![])
2036 }
2037 fn call_tool(
2038 &mut self,
2039 _: &str,
2040 _: serde_json::Value,
2041 ) -> fastmcp_core::McpResult<Vec<Content>> {
2042 Ok(vec![])
2043 }
2044 fn call_tool_with_progress(
2045 &mut self,
2046 _: &str,
2047 _: serde_json::Value,
2048 _: super::ProgressCallback<'_>,
2049 ) -> fastmcp_core::McpResult<Vec<Content>> {
2050 Ok(vec![])
2051 }
2052 fn read_resource(&mut self, _: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
2053 Ok(vec![])
2054 }
2055 fn get_prompt(
2056 &mut self,
2057 _: &str,
2058 _: HashMap<String, String>,
2059 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
2060 Ok(vec![])
2061 }
2062 }
2063
2064 #[test]
2065 fn proxy_catalog_propagates_resource_list_error_directly() {
2066 let mut backend = FailAtResourcesBackend;
2067 let result = ProxyCatalog::from_backend(&mut backend);
2068 assert!(result.is_err());
2069 assert!(result.unwrap_err().message.contains("resource list failed"));
2070 }
2071
2072 struct FailAtTemplatesBackend;
2074
2075 impl ProxyBackend for FailAtTemplatesBackend {
2076 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
2077 Ok(vec![])
2078 }
2079 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
2080 Ok(vec![])
2081 }
2082 fn list_resource_templates(
2083 &mut self,
2084 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
2085 Err(fastmcp_core::McpError::internal_error(
2086 "template list failed",
2087 ))
2088 }
2089 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
2090 Ok(vec![])
2091 }
2092 fn call_tool(
2093 &mut self,
2094 _: &str,
2095 _: serde_json::Value,
2096 ) -> fastmcp_core::McpResult<Vec<Content>> {
2097 Ok(vec![])
2098 }
2099 fn call_tool_with_progress(
2100 &mut self,
2101 _: &str,
2102 _: serde_json::Value,
2103 _: super::ProgressCallback<'_>,
2104 ) -> fastmcp_core::McpResult<Vec<Content>> {
2105 Ok(vec![])
2106 }
2107 fn read_resource(&mut self, _: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
2108 Ok(vec![])
2109 }
2110 fn get_prompt(
2111 &mut self,
2112 _: &str,
2113 _: HashMap<String, String>,
2114 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
2115 Ok(vec![])
2116 }
2117 }
2118
2119 #[test]
2120 fn proxy_catalog_propagates_template_list_error() {
2121 let mut backend = FailAtTemplatesBackend;
2122 let result = ProxyCatalog::from_backend(&mut backend);
2123 assert!(result.is_err());
2124 assert!(result.unwrap_err().message.contains("template list failed"));
2125 }
2126
2127 struct FailAtPromptsBackend;
2129
2130 impl ProxyBackend for FailAtPromptsBackend {
2131 fn list_tools(&mut self) -> fastmcp_core::McpResult<Vec<Tool>> {
2132 Ok(vec![])
2133 }
2134 fn list_resources(&mut self) -> fastmcp_core::McpResult<Vec<Resource>> {
2135 Ok(vec![])
2136 }
2137 fn list_resource_templates(
2138 &mut self,
2139 ) -> fastmcp_core::McpResult<Vec<fastmcp_protocol::ResourceTemplate>> {
2140 Ok(vec![])
2141 }
2142 fn list_prompts(&mut self) -> fastmcp_core::McpResult<Vec<Prompt>> {
2143 Err(fastmcp_core::McpError::internal_error("prompt list failed"))
2144 }
2145 fn call_tool(
2146 &mut self,
2147 _: &str,
2148 _: serde_json::Value,
2149 ) -> fastmcp_core::McpResult<Vec<Content>> {
2150 Ok(vec![])
2151 }
2152 fn call_tool_with_progress(
2153 &mut self,
2154 _: &str,
2155 _: serde_json::Value,
2156 _: super::ProgressCallback<'_>,
2157 ) -> fastmcp_core::McpResult<Vec<Content>> {
2158 Ok(vec![])
2159 }
2160 fn read_resource(&mut self, _: &str) -> fastmcp_core::McpResult<Vec<ResourceContent>> {
2161 Ok(vec![])
2162 }
2163 fn get_prompt(
2164 &mut self,
2165 _: &str,
2166 _: HashMap<String, String>,
2167 ) -> fastmcp_core::McpResult<Vec<PromptMessage>> {
2168 Ok(vec![])
2169 }
2170 }
2171
2172 #[test]
2173 fn proxy_catalog_propagates_prompt_list_error() {
2174 let mut backend = FailAtPromptsBackend;
2175 let result = ProxyCatalog::from_backend(&mut backend);
2176 assert!(result.is_err());
2177 assert!(result.unwrap_err().message.contains("prompt list failed"));
2178 }
2179
2180 #[test]
2185 fn proxy_resource_handler_from_template_read_with_uri() {
2186 use super::ProxyResourceHandler;
2187 use crate::handler::ResourceHandler;
2188 use fastmcp_protocol::ResourceTemplate;
2189
2190 let backend = TestBackend::default();
2191 let proxy = ProxyClient::from_backend(backend);
2192 let template = ResourceTemplate {
2193 uri_template: "db://{table}".to_string(),
2194 name: "DB".to_string(),
2195 description: None,
2196 mime_type: None,
2197 icon: None,
2198 version: None,
2199 tags: vec![],
2200 };
2201 let handler = ProxyResourceHandler::from_template(template, proxy);
2202
2203 let ctx = McpContext::new(Cx::for_testing(), 1);
2204 let mut params = HashMap::new();
2205 params.insert("table".to_string(), "users".to_string());
2206 let result = handler
2207 .read_with_uri(&ctx, "db://users", ¶ms)
2208 .expect("read ok");
2209 assert_eq!(result.len(), 1);
2210 }
2211
2212 #[test]
2213 fn proxy_resource_handler_from_template_with_prefix_read_with_uri() {
2214 use super::ProxyResourceHandler;
2215 use crate::handler::ResourceHandler;
2216 use fastmcp_protocol::ResourceTemplate;
2217
2218 let backend = TestBackend::default();
2219 let proxy = ProxyClient::from_backend(backend);
2220 let template = ResourceTemplate {
2221 uri_template: "db://{table}".to_string(),
2222 name: "DB".to_string(),
2223 description: None,
2224 mime_type: None,
2225 icon: None,
2226 version: None,
2227 tags: vec![],
2228 };
2229 let handler = ProxyResourceHandler::from_template_with_prefix(template, "remote", proxy);
2230
2231 let ctx = McpContext::new(Cx::for_testing(), 1);
2232 let mut params = HashMap::new();
2233 params.insert("table".to_string(), "orders".to_string());
2234 let result = handler
2236 .read_with_uri(&ctx, "remote/db://orders", ¶ms)
2237 .expect("read ok");
2238 assert_eq!(result.len(), 1);
2239 }
2240
2241 #[test]
2246 fn proxy_prompt_handler_definition_preserves_arguments() {
2247 use fastmcp_protocol::PromptArgument;
2248
2249 let backend = TestBackend::default();
2250 let proxy = ProxyClient::from_backend(backend);
2251 let handler = ProxyPromptHandler::new(
2252 Prompt {
2253 name: "templated".to_string(),
2254 description: Some("prompt with args".to_string()),
2255 arguments: vec![
2256 PromptArgument {
2257 name: "name".to_string(),
2258 description: Some("User name".to_string()),
2259 required: true,
2260 },
2261 PromptArgument {
2262 name: "lang".to_string(),
2263 description: None,
2264 required: false,
2265 },
2266 ],
2267 icon: None,
2268 version: None,
2269 tags: vec![],
2270 },
2271 proxy,
2272 );
2273
2274 let def = handler.definition();
2275 assert_eq!(def.arguments.len(), 2);
2276 assert_eq!(def.arguments[0].name, "name");
2277 assert!(def.arguments[0].required);
2278 assert_eq!(def.arguments[1].name, "lang");
2279 assert!(!def.arguments[1].required);
2280 }
2281
2282 #[test]
2287 fn prefixed_prompt_handler_definition_preserves_arguments() {
2288 use fastmcp_protocol::PromptArgument;
2289
2290 let backend = TestBackend::default();
2291 let proxy = ProxyClient::from_backend(backend);
2292 let handler = ProxyPromptHandler::with_prefix(
2293 Prompt {
2294 name: "greet".to_string(),
2295 description: None,
2296 arguments: vec![PromptArgument {
2297 name: "user".to_string(),
2298 description: None,
2299 required: true,
2300 }],
2301 icon: None,
2302 version: None,
2303 tags: vec![],
2304 },
2305 "ns",
2306 proxy,
2307 );
2308
2309 let def = handler.definition();
2310 assert_eq!(def.name, "ns/greet");
2311 assert_eq!(def.arguments.len(), 1);
2312 assert_eq!(def.arguments[0].name, "user");
2313 }
2314}