Skip to main content

fastmcp_server/
proxy.rs

1//! Proxy/composition support for MCP servers.
2//!
3//! This module provides lightweight proxy handlers that forward tool/resource/prompt
4//! calls to another MCP server via a backend client.
5
6use 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
17/// Progress callback signature used by proxy backends.
18pub type ProgressCallback<'a> = &'a mut dyn FnMut(f64, Option<f64>, Option<String>);
19
20/// Backend interface used by proxy handlers.
21pub trait ProxyBackend: Send {
22    /// Lists available tools.
23    fn list_tools(&mut self) -> McpResult<Vec<Tool>>;
24    /// Lists available resources.
25    fn list_resources(&mut self) -> McpResult<Vec<Resource>>;
26    /// Lists available resource templates.
27    fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>>;
28    /// Lists available prompts.
29    fn list_prompts(&mut self) -> McpResult<Vec<Prompt>>;
30    /// Calls a tool.
31    fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> McpResult<Vec<Content>>;
32    /// Calls a tool with progress callback support.
33    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    /// Reads a resource by URI.
40    fn read_resource(&mut self, uri: &str) -> McpResult<Vec<ResourceContent>>;
41    /// Fetches a prompt by name.
42    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/// Catalog of remote definitions used to register proxy handlers.
108#[derive(Debug, Clone, Default)]
109pub struct ProxyCatalog {
110    /// Remote tool definitions.
111    pub tools: Vec<Tool>,
112    /// Remote resource definitions.
113    pub resources: Vec<Resource>,
114    /// Remote resource templates.
115    pub resource_templates: Vec<ResourceTemplate>,
116    /// Remote prompt definitions.
117    pub prompts: Vec<Prompt>,
118}
119
120impl ProxyCatalog {
121    /// Builds a catalog by querying a proxy backend.
122    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    /// Builds a catalog by querying a client.
132    pub fn from_client(client: &mut Client) -> McpResult<Self> {
133        Self::from_backend(client)
134    }
135}
136
137/// Shared proxy client wrapper for handler reuse.
138#[derive(Clone)]
139pub struct ProxyClient {
140    inner: Arc<Mutex<dyn ProxyBackend>>,
141}
142
143impl ProxyClient {
144    /// Creates a proxy client from an MCP client.
145    #[must_use]
146    pub fn from_client(client: Client) -> Self {
147        Self::from_backend(client)
148    }
149
150    /// Creates a proxy client from a backend implementation.
151    #[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    /// Fetches a catalog by querying the backend.
159    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    /// The tool definition as exposed to clients (may have prefixed name).
215    tool: Tool,
216    /// The original tool name on the remote server (for forwarding).
217    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    /// Creates a proxy handler with a prefixed name.
232    ///
233    /// The tool will be exposed with `prefix/original_name` but calls will be
234    /// forwarded using the original name.
235    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        // Forward using the original external name
253        self.client.call_tool(ctx, &self.external_name, arguments)
254    }
255}
256
257pub(crate) struct ProxyResourceHandler {
258    /// The resource definition as exposed to clients (may have prefixed URI).
259    resource: Resource,
260    /// The original URI on the remote server (for forwarding).
261    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    /// Creates a proxy handler with a prefixed URI.
278    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    /// Creates a proxy handler from a template with a prefixed URI.
300    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        // Forward using the original external URI
327        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        // For templated resources with a prefix, we need to strip the prefix
337        // to forward the correct URI to the external server.
338        //
339        // If the incoming URI matches our prefixed pattern (e.g., "ext/file://..."),
340        // strip the prefix to get the original URI (e.g., "file://...").
341        let external_uri = if uri.starts_with(&format!(
342            "{}/",
343            self.resource.uri.split('/').next().unwrap_or("")
344        )) {
345            // Strip the prefix (everything before and including the first '/')
346            uri.splitn(2, '/').nth(1).unwrap_or(uri)
347        } else {
348            // No prefix match, use as-is
349            uri
350        };
351        self.client.read_resource(ctx, external_uri)
352    }
353}
354
355pub(crate) struct ProxyPromptHandler {
356    /// The prompt definition as exposed to clients (may have prefixed name).
357    prompt: Prompt,
358    /// The original prompt name on the remote server (for forwarding).
359    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    /// Creates a proxy handler with a prefixed name.
374    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        // Forward using the original external name
396        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    // =========================================================================
635    // Prefixed Proxy Handler Tests (for as_proxy)
636    // =========================================================================
637
638    #[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        // Create handler with prefix "db"
658        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        // Definition should have prefixed name
674        let def = handler.definition();
675        assert_eq!(def.name, "db/query");
676        assert_eq!(def.description, Some("Execute a query".to_string()));
677
678        // Call should forward with original name
679        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"); // Original name, not prefixed
686    }
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        // Create handler with prefix "templates"
706        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        // Definition should have prefixed name
720        let def = handler.definition();
721        assert_eq!(def.name, "templates/greeting");
722        assert_eq!(def.description, Some("A greeting prompt".to_string()));
723
724        // Call should forward with original name
725        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"); // Original name, not prefixed
732    }
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        // Create handler with prefix "storage"
754        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        // Definition should have prefixed URI
769        let def = handler.definition();
770        assert_eq!(def.uri, "storage/file://data");
771        assert_eq!(def.name, "Data File");
772    }
773
774    // =========================================================================
775    // ProxyCatalog Edge Cases
776    // =========================================================================
777
778    #[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    // =========================================================================
848    // ProxyClient Tests
849    // =========================================================================
850
851    #[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        // Both clones should reach the same backend
872        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    // =========================================================================
917    // ProxyResourceHandler Tests
918    // =========================================================================
919
920    #[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        // Definition should mirror the template
988        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        // Template should be available
995        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        // Definition should have prefixed URI template
1019        let def = handler.definition();
1020        assert_eq!(def.uri, "storage/file://{path}");
1021
1022        // Template should also be prefixed
1023        let tmpl = handler.template().expect("has template");
1024        assert_eq!(tmpl.uri_template, "storage/file://{path}");
1025    }
1026
1027    // =========================================================================
1028    // Error Propagation Tests
1029    // =========================================================================
1030
1031    /// A backend that always returns errors.
1032    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    // =========================================================================
1168    // resource_from_template Helper
1169    // =========================================================================
1170
1171    #[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    // =========================================================================
1194    // ProxyCatalog trait derives
1195    // =========================================================================
1196
1197    #[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    // =========================================================================
1238    // ProxyResourceHandler.read_with_uri
1239    // =========================================================================
1240
1241    #[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", &params)
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        // URI with prefix should still work (prefix gets stripped)
1293        let result = handler
1294            .read_with_uri(&ctx, "ext/file://data", &params)
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        // URI without prefix match - used as-is
1322        let result = handler
1323            .read_with_uri(&ctx, "other://uri", &params)
1324            .expect("read ok");
1325        assert_eq!(result.len(), 1);
1326    }
1327
1328    // =========================================================================
1329    // ProxyToolHandler.definition
1330    // =========================================================================
1331
1332    #[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    // =========================================================================
1357    // ProxyPromptHandler.definition
1358    // =========================================================================
1359
1360    #[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    // =========================================================================
1383    // ProxyClient.read_resource and get_prompt
1384    // =========================================================================
1385
1386    #[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    // =========================================================================
1440    // ProxyResourceHandler new/with_prefix stores external_uri
1441    // =========================================================================
1442
1443    #[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        // External URI is the original, not the prefixed one
1484        assert_eq!(handler.external_uri, "original://uri");
1485        // But the resource URI is prefixed
1486        assert_eq!(handler.resource.uri, "pfx/original://uri");
1487    }
1488
1489    // =========================================================================
1490    // ProxyToolHandler stores external_name
1491    // =========================================================================
1492
1493    #[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    // =========================================================================
1537    // ProxyPromptHandler stores external_name
1538    // =========================================================================
1539
1540    #[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    // =========================================================================
1579    // resource_from_template with minimal fields
1580    // =========================================================================
1581
1582    #[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    // =========================================================================
1606    // Error propagation for resource read and prompt get
1607    // =========================================================================
1608
1609    #[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    // =========================================================================
1637    // ProxyClient — lock poison error
1638    // =========================================================================
1639
1640    #[test]
1641    fn proxy_client_lock_poison_returns_error() {
1642        let backend = TestBackend::default();
1643        let proxy = ProxyClient::from_backend(backend);
1644
1645        // Poison the mutex by panicking inside a lock
1646        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        // Now the lock is poisoned — catalog should return an error
1653        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    // =========================================================================
1664    // ProxyResourceHandler — from_template stores external_uri
1665    // =========================================================================
1666
1667    #[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        // External URI is the original template URI
1705        assert_eq!(handler.external_uri, "db://{table}");
1706        // Resource URI is prefixed
1707        assert_eq!(handler.resource.uri, "remote/db://{table}");
1708        // Template is also prefixed
1709        let tmpl = handler.template.unwrap();
1710        assert_eq!(tmpl.uri_template, "remote/db://{table}");
1711    }
1712
1713    // =========================================================================
1714    // ProxyClient — call_tool with progress reporter
1715    // =========================================================================
1716
1717    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        // The TestBackend's call_tool_with_progress calls on_progress(0.5, Some(1.0), ...)
1754        // which triggers ctx.report_progress_with_total
1755        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    // =========================================================================
1762    // read_with_uri — URI without slash in resource URI
1763    // =========================================================================
1764
1765    #[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        // Resource URI without any slash — split('/').next() returns the whole string
1773        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        // URI that starts with "noslash/" — prefix will match
1789        let result = handler
1790            .read_with_uri(&ctx, "noslash/rest", &params)
1791            .expect("read ok");
1792        assert_eq!(result.len(), 1);
1793    }
1794
1795    // =========================================================================
1796    // ProxyCatalog — resource_templates populated
1797    // =========================================================================
1798
1799    #[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    // =========================================================================
1861    // FailingBackend — catalog errors propagate from resource list
1862    // =========================================================================
1863
1864    #[test]
1865    fn proxy_catalog_propagates_resource_list_error() {
1866        // FailingBackend.list_tools fails first, but let's verify the error message
1867        let mut backend = FailingBackend;
1868        let result = ProxyCatalog::from_backend(&mut backend);
1869        assert!(result.is_err());
1870        // The first error encountered is from list_tools
1871        assert!(result.unwrap_err().message.contains("tool list failed"));
1872    }
1873
1874    // =========================================================================
1875    // ProxyClient — call_tool without progress (no_progress path)
1876    // =========================================================================
1877
1878    #[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        // McpContext::new has no progress reporter
1888        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    // =========================================================================
1902    // resource_from_template — icon field
1903    // =========================================================================
1904
1905    #[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    // =========================================================================
1928    // Progress callback — None total branch
1929    // =========================================================================
1930
1931    /// Backend that invokes the progress callback with `None` total,
1932    /// exercising the `report_progress` (no total) path in `ProxyClient::call_tool`.
1933    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            // Call with None total to exercise the else branch
1970            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        // Total should be None since the backend passes None
2010        assert!(calls[0].1.is_none());
2011    }
2012
2013    // =========================================================================
2014    // Partial catalog failures — list_resources, list_templates, list_prompts
2015    // =========================================================================
2016
2017    /// A backend where list_tools succeeds but list_resources fails.
2018    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    /// A backend where list_tools and list_resources succeed but list_resource_templates fails.
2073    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    /// A backend where everything succeeds except list_prompts.
2128    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    // =========================================================================
2181    // read_with_uri on template-based handlers
2182    // =========================================================================
2183
2184    #[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", &params)
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        // Prefixed URI
2235        let result = handler
2236            .read_with_uri(&ctx, "remote/db://orders", &params)
2237            .expect("read ok");
2238        assert_eq!(result.len(), 1);
2239    }
2240
2241    // =========================================================================
2242    // Prompt definition preserves arguments
2243    // =========================================================================
2244
2245    #[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    // =========================================================================
2283    // Prefixed prompt definition preserves arguments
2284    // =========================================================================
2285
2286    #[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}