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}