turul_mcp_server/
builder.rs

1//! MCP Server Builder
2//!
3//! This module provides a builder pattern for creating MCP servers.
4
5use std::collections::{HashMap, HashSet};
6use std::net::SocketAddr;
7use std::sync::Arc;
8
9use crate::handlers::*;
10use crate::resource::McpResource;
11use crate::{
12    McpCompletion, McpElicitation, McpLogger, McpNotification, McpPrompt, McpRoot, McpSampling,
13};
14use crate::{McpServer, McpTool, Result};
15use turul_mcp_protocol::McpError;
16use turul_mcp_protocol::initialize::*;
17use turul_mcp_protocol::{Implementation, ServerCapabilities};
18
19/// Builder for MCP servers
20pub struct McpServerBuilder {
21    /// Server implementation info
22    name: String,
23    version: String,
24    title: Option<String>,
25
26    /// Server capabilities
27    capabilities: ServerCapabilities,
28
29    /// Tools registered with the server
30    tools: HashMap<String, Arc<dyn McpTool>>,
31
32    /// Resources registered with the server
33    resources: HashMap<String, Arc<dyn McpResource>>,
34
35    /// Template resources (URI template -> resource)
36    template_resources: Vec<(crate::uri_template::UriTemplate, Arc<dyn McpResource>)>,
37
38    /// Prompts registered with the server
39    prompts: HashMap<String, Arc<dyn McpPrompt>>,
40
41    /// Elicitations registered with the server
42    elicitations: HashMap<String, Arc<dyn McpElicitation>>,
43
44    /// Sampling providers registered with the server
45    sampling: HashMap<String, Arc<dyn McpSampling>>,
46
47    /// Completion providers registered with the server
48    completions: HashMap<String, Arc<dyn McpCompletion>>,
49
50    /// Loggers registered with the server
51    loggers: HashMap<String, Arc<dyn McpLogger>>,
52
53    /// Root providers registered with the server
54    root_providers: HashMap<String, Arc<dyn McpRoot>>,
55
56    /// Notification providers registered with the server
57    notifications: HashMap<String, Arc<dyn McpNotification>>,
58
59    /// Handlers registered with the server
60    handlers: HashMap<String, Arc<dyn McpHandler>>,
61
62    /// Roots configured for the server
63    roots: Vec<turul_mcp_protocol::roots::Root>,
64
65    /// Optional instructions for clients
66    instructions: Option<String>,
67
68    /// Session configuration
69    session_timeout_minutes: Option<u64>,
70    session_cleanup_interval_seconds: Option<u64>,
71
72    /// Session storage backend (defaults to InMemory if None)
73    session_storage: Option<Arc<turul_mcp_session_storage::BoxedSessionStorage>>,
74
75    /// MCP Lifecycle enforcement configuration
76    strict_lifecycle: bool,
77
78    /// Test mode - disables security middleware for test servers
79    test_mode: bool,
80
81    /// Middleware stack for request/response interception
82    middleware_stack: crate::middleware::MiddlewareStack,
83
84    /// HTTP configuration (if enabled)
85    #[cfg(feature = "http")]
86    bind_address: SocketAddr,
87    #[cfg(feature = "http")]
88    mcp_path: String,
89    #[cfg(feature = "http")]
90    enable_cors: bool,
91    #[cfg(feature = "http")]
92    enable_sse: bool,
93
94    /// Validation errors collected during builder configuration
95    validation_errors: Vec<String>,
96}
97
98impl McpServerBuilder {
99    /// Create a new builder
100    pub fn new() -> Self {
101        let tools = HashMap::new();
102        let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
103
104        // Add all standard MCP 2025-06-18 handlers by default
105        handlers.insert("ping".to_string(), Arc::new(PingHandler));
106        handlers.insert(
107            "completion/complete".to_string(),
108            Arc::new(CompletionHandler),
109        );
110        handlers.insert(
111            "resources/list".to_string(),
112            Arc::new(ResourcesListHandler::new()),
113        );
114        handlers.insert(
115            "resources/read".to_string(),
116            Arc::new(ResourcesReadHandler::new()),
117        );
118        handlers.insert(
119            "prompts/list".to_string(),
120            Arc::new(PromptsListHandler::new()),
121        );
122        handlers.insert(
123            "prompts/get".to_string(),
124            Arc::new(PromptsGetHandler::new()),
125        );
126        handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
127        handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
128        handlers.insert(
129            "sampling/createMessage".to_string(),
130            Arc::new(SamplingHandler),
131        );
132        // Note: resources/templates/list is only registered if template resources are configured (see build method)
133        handlers.insert(
134            "elicitation/create".to_string(),
135            Arc::new(ElicitationHandler::with_mock_provider()),
136        );
137
138        // Add all notification handlers (except notifications/initialized which is handled specially)
139        let notifications_handler = Arc::new(NotificationsHandler);
140        handlers.insert(
141            "notifications/message".to_string(),
142            notifications_handler.clone(),
143        );
144        handlers.insert(
145            "notifications/progress".to_string(),
146            notifications_handler.clone(),
147        );
148        handlers.insert(
149            "notifications/resources/listChanged".to_string(),
150            notifications_handler.clone(),
151        );
152        handlers.insert(
153            "notifications/resources/updated".to_string(),
154            notifications_handler.clone(),
155        );
156        handlers.insert(
157            "notifications/tools/listChanged".to_string(),
158            notifications_handler.clone(),
159        );
160        handlers.insert(
161            "notifications/prompts/listChanged".to_string(),
162            notifications_handler.clone(),
163        );
164        handlers.insert(
165            "notifications/roots/listChanged".to_string(),
166            notifications_handler,
167        );
168
169        // Note: notifications/initialized is handled by InitializedNotificationHandler in server.rs
170
171        Self {
172            name: "turul-mcp-server".to_string(),
173            version: "1.0.0".to_string(),
174            title: None,
175            capabilities: ServerCapabilities::default(),
176            tools,
177            resources: HashMap::new(),
178            template_resources: Vec::new(),
179            prompts: HashMap::new(),
180            elicitations: HashMap::new(),
181            sampling: HashMap::new(),
182            completions: HashMap::new(),
183            loggers: HashMap::new(),
184            root_providers: HashMap::new(),
185            notifications: HashMap::new(),
186            handlers,
187            roots: Vec::new(),
188            instructions: None,
189            session_timeout_minutes: None,
190            session_cleanup_interval_seconds: None,
191            session_storage: None,   // Default: InMemory storage
192            strict_lifecycle: false, // Default: lenient mode for compatibility
193            test_mode: false,        // Default: production mode with security
194            middleware_stack: crate::middleware::MiddlewareStack::new(), // Default: empty middleware stack
195            #[cfg(feature = "http")]
196            bind_address: "127.0.0.1:8000".parse().unwrap(),
197            #[cfg(feature = "http")]
198            mcp_path: "/mcp".to_string(),
199            #[cfg(feature = "http")]
200            enable_cors: true,
201            #[cfg(feature = "http")]
202            enable_sse: cfg!(feature = "sse"),
203            validation_errors: Vec::new(),
204        }
205    }
206
207    /// Sets the server name for identification
208    pub fn name(mut self, name: impl Into<String>) -> Self {
209        self.name = name.into();
210        self
211    }
212
213    /// Sets the server version string
214    pub fn version(mut self, version: impl Into<String>) -> Self {
215        self.version = version.into();
216        self
217    }
218
219    /// Sets the human-readable server title
220    pub fn title(mut self, title: impl Into<String>) -> Self {
221        self.title = Some(title.into());
222        self
223    }
224
225    /// Sets usage instructions for MCP clients
226    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
227        self.instructions = Some(instructions.into());
228        self
229    }
230
231    /// Registers a tool that clients can execute
232    pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
233        let name = tool.name().to_string();
234        self.tools.insert(name, Arc::new(tool));
235        self
236    }
237
238    /// Register a function tool created with `#[mcp_tool]` macro
239    ///
240    /// This method provides a more intuitive way to register function tools.
241    /// The `#[mcp_tool]` macro generates a constructor function with the same name
242    /// as your async function, so you can use the function name directly.
243    ///
244    /// # Example
245    /// ```rust,no_run
246    /// use turul_mcp_server::prelude::*;
247    /// use std::collections::HashMap;
248    ///
249    /// // Manual tool implementation without derive macros
250    /// #[derive(Clone, Default)]
251    /// struct AddTool;
252    ///
253    /// // Implement all required traits for ToolDefinition
254    /// impl turul_mcp_builders::traits::HasBaseMetadata for AddTool {
255    ///     fn name(&self) -> &str { "add" }
256    ///     fn title(&self) -> Option<&str> { Some("Add Numbers") }
257    /// }
258    ///
259    /// impl turul_mcp_builders::traits::HasDescription for AddTool {
260    ///     fn description(&self) -> Option<&str> {
261    ///         Some("Add two numbers together")
262    ///     }
263    /// }
264    ///
265    /// impl turul_mcp_builders::traits::HasInputSchema for AddTool {
266    ///     fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
267    ///         use turul_mcp_protocol::schema::JsonSchema;
268    ///         static SCHEMA: std::sync::OnceLock<turul_mcp_protocol::ToolSchema> = std::sync::OnceLock::new();
269    ///         SCHEMA.get_or_init(|| {
270    ///             let mut props = HashMap::new();
271    ///             props.insert("a".to_string(), JsonSchema::number().with_description("First number"));
272    ///             props.insert("b".to_string(), JsonSchema::number().with_description("Second number"));
273    ///             turul_mcp_protocol::ToolSchema::object()
274    ///                 .with_properties(props)
275    ///                 .with_required(vec!["a".to_string(), "b".to_string()])
276    ///         })
277    ///     }
278    /// }
279    ///
280    /// impl turul_mcp_builders::traits::HasOutputSchema for AddTool {
281    ///     fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> { None }
282    /// }
283    ///
284    /// impl turul_mcp_builders::traits::HasAnnotations for AddTool {
285    ///     fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> { None }
286    /// }
287    ///
288    /// impl turul_mcp_builders::traits::HasToolMeta for AddTool {
289    ///     fn tool_meta(&self) -> Option<&HashMap<String, serde_json::Value>> { None }
290    /// }
291    ///
292    /// #[async_trait]
293    /// impl McpTool for AddTool {
294    ///     async fn call(&self, args: serde_json::Value, _session: Option<SessionContext>)
295    ///         -> McpResult<turul_mcp_protocol::tools::CallToolResult> {
296    ///         let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
297    ///         let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
298    ///         let result = a + b;
299    ///
300    ///         Ok(turul_mcp_protocol::tools::CallToolResult::success(vec![
301    ///             turul_mcp_protocol::ToolResult::text(format!("{} + {} = {}", a, b, result))
302    ///         ]))
303    ///     }
304    /// }
305    ///
306    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
307    /// let server = McpServer::builder()
308    ///     .name("math-server")
309    ///     .tool_fn(|| AddTool::default()) // Function returns working tool instance
310    ///     .build()?;
311    /// # Ok(())
312    /// # }
313    /// ```
314    pub fn tool_fn<F, T>(self, func: F) -> Self
315    where
316        F: Fn() -> T,
317        T: McpTool + 'static,
318    {
319        // Call the helper function to get the tool instance
320        self.tool(func())
321    }
322
323    /// Registers multiple tools in a batch
324    pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
325        for tool in tools {
326            self = self.tool(tool);
327        }
328        self
329    }
330
331    /// Add middleware to the request/response processing chain
332    ///
333    /// **This method is additive** - each call adds a new middleware to the stack.
334    /// Middleware execute in the order they are registered (FIFO):
335    /// - **Before dispatch**: First registered executes first (FIFO order)
336    /// - **After dispatch**: First registered executes last (LIFO/reverse order)
337    ///
338    /// Multiple middleware can be composed by calling this method multiple times.
339    /// Middleware works identically across all transports (HTTP, Lambda, etc.).
340    ///
341    /// # Behavior with Other Builder Methods
342    ///
343    /// - **`.test_mode()`**: Does NOT affect middleware - middleware always executes
344    /// - **Non-HTTP builds**: Middleware is available but requires manual wiring
345    ///
346    /// # Examples
347    ///
348    /// ## Single Middleware
349    ///
350    /// ```rust,no_run
351    /// use turul_mcp_server::prelude::*;
352    /// use async_trait::async_trait;
353    /// use std::sync::Arc;
354    ///
355    /// struct LoggingMiddleware;
356    ///
357    /// #[async_trait]
358    /// impl McpMiddleware for LoggingMiddleware {
359    ///     async fn before_dispatch(
360    ///         &self,
361    ///         ctx: &mut RequestContext<'_>,
362    ///         _session: Option<&dyn turul_mcp_session_storage::SessionView>,
363    ///         _injection: &mut SessionInjection,
364    ///     ) -> Result<(), MiddlewareError> {
365    ///         println!("Request: {}", ctx.method());
366    ///         Ok(())
367    ///     }
368    /// }
369    ///
370    /// # async fn example() -> McpResult<()> {
371    /// let server = McpServer::builder()
372    ///     .name("my-server")
373    ///     .middleware(Arc::new(LoggingMiddleware))
374    ///     .build()?;
375    /// # Ok(())
376    /// # }
377    /// ```
378    ///
379    /// ## Multiple Middleware Composition
380    ///
381    /// ```rust,no_run
382    /// use turul_mcp_server::prelude::*;
383    /// use async_trait::async_trait;
384    /// use std::sync::Arc;
385    /// use serde_json::json;
386    /// # struct AuthMiddleware;
387    /// # struct LoggingMiddleware;
388    /// # struct RateLimitMiddleware;
389    /// # #[async_trait]
390    /// # impl McpMiddleware for AuthMiddleware {
391    /// #     async fn before_dispatch(&self, _ctx: &mut RequestContext<'_>, _session: Option<&dyn turul_mcp_session_storage::SessionView>, _injection: &mut SessionInjection) -> Result<(), MiddlewareError> { Ok(()) }
392    /// # }
393    /// # #[async_trait]
394    /// # impl McpMiddleware for LoggingMiddleware {
395    /// #     async fn before_dispatch(&self, _ctx: &mut RequestContext<'_>, _session: Option<&dyn turul_mcp_session_storage::SessionView>, _injection: &mut SessionInjection) -> Result<(), MiddlewareError> { Ok(()) }
396    /// # }
397    /// # #[async_trait]
398    /// # impl McpMiddleware for RateLimitMiddleware {
399    /// #     async fn before_dispatch(&self, _ctx: &mut RequestContext<'_>, _session: Option<&dyn turul_mcp_session_storage::SessionView>, _injection: &mut SessionInjection) -> Result<(), MiddlewareError> { Ok(()) }
400    /// # }
401    ///
402    /// # async fn example() -> McpResult<()> {
403    /// // Execution order:
404    /// // Before dispatch: Auth → Logging → RateLimit
405    /// // After dispatch: RateLimit → Logging → Auth (reverse)
406    /// let server = McpServer::builder()
407    ///     .name("my-server")
408    ///     .middleware(Arc::new(AuthMiddleware))      // 1st before, 3rd after
409    ///     .middleware(Arc::new(LoggingMiddleware))   // 2nd before, 2nd after
410    ///     .middleware(Arc::new(RateLimitMiddleware)) // 3rd before, 1st after
411    ///     .build()?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    pub fn middleware(mut self, middleware: Arc<dyn crate::middleware::McpMiddleware>) -> Self {
416        self.middleware_stack.push(middleware);
417        self
418    }
419
420    /// Register a resource with the server
421    ///
422    /// Automatically detects if the resource URI contains template variables (e.g., `{ticker}`, `{id}`)
423    /// and registers it as either a static resource or template resource accordingly.
424    /// This eliminates the need to manually call `.template_resource()` for templated URIs.
425    ///
426    /// # Examples
427    ///
428    /// ```rust,no_run
429    /// use turul_mcp_server::prelude::*;
430    /// use std::collections::HashMap;
431    ///
432    /// // Manual resource implementation without derive macros
433    /// #[derive(Clone)]
434    /// struct ConfigResource {
435    ///     data: String,
436    /// }
437    ///
438    /// // Implement all required traits for ResourceDefinition
439    /// impl turul_mcp_builders::traits::HasResourceMetadata for ConfigResource {
440    ///     fn name(&self) -> &str { "config" }
441    ///     fn title(&self) -> Option<&str> { Some("Configuration") }
442    /// }
443    ///
444    /// impl turul_mcp_builders::traits::HasResourceDescription for ConfigResource {
445    ///     fn description(&self) -> Option<&str> {
446    ///         Some("Application configuration file")
447    ///     }
448    /// }
449    ///
450    /// impl turul_mcp_builders::traits::HasResourceUri for ConfigResource {
451    ///     fn uri(&self) -> &str { "file:///config.json" }
452    /// }
453    ///
454    /// impl turul_mcp_builders::traits::HasResourceMimeType for ConfigResource {
455    ///     fn mime_type(&self) -> Option<&str> { Some("application/json") }
456    /// }
457    ///
458    /// impl turul_mcp_builders::traits::HasResourceSize for ConfigResource {
459    ///     fn size(&self) -> Option<u64> { Some(self.data.len() as u64) }
460    /// }
461    ///
462    /// impl turul_mcp_builders::traits::HasResourceAnnotations for ConfigResource {
463    ///     fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> { None }
464    /// }
465    ///
466    /// impl turul_mcp_builders::traits::HasResourceMeta for ConfigResource {
467    ///     fn resource_meta(&self) -> Option<&HashMap<String, serde_json::Value>> { None }
468    /// }
469    ///
470    /// #[async_trait]
471    /// impl McpResource for ConfigResource {
472    ///     async fn read(&self, _params: Option<serde_json::Value>, _session: Option<&SessionContext>)
473    ///         -> McpResult<Vec<turul_mcp_protocol::ResourceContent>> {
474    ///         Ok(vec![turul_mcp_protocol::ResourceContent::text(
475    ///             self.uri(),
476    ///             &self.data
477    ///         )])
478    ///     }
479    /// }
480    ///
481    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
482    /// let config = ConfigResource {
483    ///     data: r#"{"debug": true, "port": 8080}"#.to_string(),
484    /// };
485    ///
486    /// let server = McpServer::builder()
487    ///     .name("resource-server")
488    ///     .resource(config) // Working resource with actual data
489    ///     .build()?;
490    /// # Ok(())
491    /// # }
492    /// ```
493    pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
494        let uri = resource.uri().to_string();
495
496        // Auto-detect if URI contains template variables
497        if self.is_template_uri(&uri) {
498            // It's a template resource - parse as UriTemplate
499            tracing::debug!("Detected template resource: {}", uri);
500            match crate::uri_template::UriTemplate::new(&uri) {
501                Ok(template) => {
502                    // Validate template pattern
503                    if let Err(e) = self.validate_uri_template(template.pattern()) {
504                        self.validation_errors
505                            .push(format!("Invalid template resource URI '{}': {}", uri, e));
506                    }
507                    self.template_resources.push((template, Arc::new(resource)));
508                }
509                Err(e) => {
510                    self.validation_errors.push(format!(
511                        "Failed to parse template resource URI '{}': {}",
512                        uri, e
513                    ));
514                }
515            }
516        } else {
517            // It's a static resource
518            tracing::debug!("Detected static resource: {}", uri);
519            if let Err(e) = self.validate_uri(&uri) {
520                tracing::warn!("Static resource validation failed for '{}': {}", uri, e);
521                self.validation_errors
522                    .push(format!("Invalid resource URI '{}': {}", uri, e));
523            } else {
524                tracing::debug!("Successfully added static resource: {}", uri);
525                self.resources.insert(uri, Arc::new(resource));
526            }
527        }
528
529        self
530    }
531
532    /// Register a function resource created with `#[mcp_resource]` macro
533    ///
534    /// This method provides a more intuitive way to register function resources.
535    /// The `#[mcp_resource]` macro generates a constructor function with the same name
536    /// as your async function, so you can use the function name directly.
537    ///
538    /// # Example
539    /// ```rust,no_run
540    /// use turul_mcp_server::prelude::*;
541    /// use std::collections::HashMap;
542    ///
543    /// // Manual resource implementation without derive macros
544    /// #[derive(Clone)]
545    /// struct DataResource {
546    ///     content: String,
547    /// }
548    ///
549    /// // Implement all required traits for ResourceDefinition (same as resource() example)
550    /// impl turul_mcp_builders::traits::HasResourceMetadata for DataResource {
551    ///     fn name(&self) -> &str { "data" }
552    ///     fn title(&self) -> Option<&str> { Some("Data File") }
553    /// }
554    ///
555    /// impl turul_mcp_builders::traits::HasResourceDescription for DataResource {
556    ///     fn description(&self) -> Option<&str> { Some("Sample data file") }
557    /// }
558    ///
559    /// impl turul_mcp_builders::traits::HasResourceUri for DataResource {
560    ///     fn uri(&self) -> &str { "file:///data/sample.json" }
561    /// }
562    ///
563    /// impl turul_mcp_builders::traits::HasResourceMimeType for DataResource {
564    ///     fn mime_type(&self) -> Option<&str> { Some("application/json") }
565    /// }
566    ///
567    /// impl turul_mcp_builders::traits::HasResourceSize for DataResource {
568    ///     fn size(&self) -> Option<u64> { Some(self.content.len() as u64) }
569    /// }
570    ///
571    /// impl turul_mcp_builders::traits::HasResourceAnnotations for DataResource {
572    ///     fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> { None }
573    /// }
574    ///
575    /// impl turul_mcp_builders::traits::HasResourceMeta for DataResource {
576    ///     fn resource_meta(&self) -> Option<&HashMap<String, serde_json::Value>> { None }
577    /// }
578    ///
579    /// #[async_trait]
580    /// impl McpResource for DataResource {
581    ///     async fn read(&self, _params: Option<serde_json::Value>, _session: Option<&SessionContext>)
582    ///         -> McpResult<Vec<turul_mcp_protocol::ResourceContent>> {
583    ///         Ok(vec![turul_mcp_protocol::ResourceContent::text(
584    ///             self.uri(),
585    ///             &self.content
586    ///         )])
587    ///     }
588    /// }
589    ///
590    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
591    /// let server = McpServer::builder()
592    ///     .name("data-server")
593    ///     .resource_fn(|| DataResource {
594    ///         content: r#"{"items": [1, 2, 3]}"#.to_string()
595    ///     }) // Function returns working resource instance
596    ///     .build()?;
597    /// # Ok(())
598    /// # }
599    /// ```
600    pub fn resource_fn<F, R>(self, func: F) -> Self
601    where
602        F: Fn() -> R,
603        R: McpResource + 'static,
604    {
605        // Call the helper function to get the resource instance
606        self.resource(func())
607    }
608
609    /// Register multiple resources
610    pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
611        mut self,
612        resources: I,
613    ) -> Self {
614        for resource in resources {
615            self = self.resource(resource);
616        }
617        self
618    }
619
620    /// Register a resource with explicit URI template support
621    ///
622    /// **Note**: This method is now optional. The `.resource()` method automatically detects
623    /// template URIs and handles them appropriately. Use this method only when you need
624    /// explicit control over template parsing or want to add custom validators.
625    ///
626    /// # Example
627    /// ```rust,no_run
628    /// use turul_mcp_server::prelude::*;
629    /// use turul_mcp_server::uri_template::{UriTemplate, VariableValidator};
630    /// use std::collections::HashMap;
631    ///
632    /// // Manual template resource implementation
633    /// #[derive(Clone)]
634    /// struct TemplateResource {
635    ///     base_path: String,
636    /// }
637    ///
638    /// // Implement all required traits for ResourceDefinition
639    /// impl turul_mcp_builders::traits::HasResourceMetadata for TemplateResource {
640    ///     fn name(&self) -> &str { "template-data" }
641    ///     fn title(&self) -> Option<&str> { Some("Template Data") }
642    /// }
643    ///
644    /// impl turul_mcp_builders::traits::HasResourceDescription for TemplateResource {
645    ///     fn description(&self) -> Option<&str> { Some("Template-based data resource") }
646    /// }
647    ///
648    /// impl turul_mcp_builders::traits::HasResourceUri for TemplateResource {
649    ///     fn uri(&self) -> &str { "file:///data/{id}.json" }
650    /// }
651    ///
652    /// impl turul_mcp_builders::traits::HasResourceMimeType for TemplateResource {
653    ///     fn mime_type(&self) -> Option<&str> { Some("application/json") }
654    /// }
655    ///
656    /// impl turul_mcp_builders::traits::HasResourceSize for TemplateResource {
657    ///     fn size(&self) -> Option<u64> { None } // Size varies by template
658    /// }
659    ///
660    /// impl turul_mcp_builders::traits::HasResourceAnnotations for TemplateResource {
661    ///     fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> { None }
662    /// }
663    ///
664    /// impl turul_mcp_builders::traits::HasResourceMeta for TemplateResource {
665    ///     fn resource_meta(&self) -> Option<&HashMap<String, serde_json::Value>> { None }
666    /// }
667    ///
668    /// #[async_trait]
669    /// impl McpResource for TemplateResource {
670    ///     async fn read(&self, params: Option<serde_json::Value>, _session: Option<&SessionContext>)
671    ///         -> McpResult<Vec<turul_mcp_protocol::ResourceContent>> {
672    ///         let id = params
673    ///             .as_ref()
674    ///             .and_then(|p| p.get("id"))
675    ///             .and_then(|v| v.as_str())
676    ///             .unwrap_or("default");
677    ///
678    ///         let content = format!(r#"{{"id": "{}", "data": "sample content for {}"}}"#, id, id);
679    ///         Ok(vec![turul_mcp_protocol::ResourceContent::text(
680    ///             &format!("file:///data/{}.json", id),
681    ///             &content
682    ///         )])
683    ///     }
684    /// }
685    ///
686    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
687    /// let template = UriTemplate::new("file:///data/{id}.json")?
688    ///     .with_validator("id", VariableValidator::user_id());
689    ///
690    /// let resource = TemplateResource {
691    ///     base_path: "/data".to_string(),
692    /// };
693    ///
694    /// let server = McpServer::builder()
695    ///     .name("template-server")
696    ///     .template_resource(template, resource) // Working template resource
697    ///     .build()?;
698    /// # Ok(())
699    /// # }
700    /// ```
701    pub fn template_resource<R: McpResource + 'static>(
702        mut self,
703        template: crate::uri_template::UriTemplate,
704        resource: R,
705    ) -> Self {
706        // Validate template pattern is well-formed (MCP 2025-06-18 compliance)
707        if let Err(e) = self.validate_uri_template(template.pattern()) {
708            self.validation_errors.push(format!(
709                "Invalid resource template URI '{}': {}",
710                template.pattern(),
711                e
712            ));
713        }
714
715        self.template_resources.push((template, Arc::new(resource)));
716        self
717    }
718
719    /// Registers a prompt template for conversation generation
720    pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
721        let name = prompt.name().to_string();
722        self.prompts.insert(name, Arc::new(prompt));
723        self
724    }
725
726    /// Register multiple prompts
727    pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
728        mut self,
729        prompts: I,
730    ) -> Self {
731        for prompt in prompts {
732            self = self.prompt(prompt);
733        }
734        self
735    }
736
737    /// Register an elicitation provider with the server
738    pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
739        let key = format!("elicitation_{}", self.elicitations.len());
740        self.elicitations.insert(key, Arc::new(elicitation));
741        self
742    }
743
744    /// Register multiple elicitation providers
745    pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
746        mut self,
747        elicitations: I,
748    ) -> Self {
749        for elicitation in elicitations {
750            self = self.elicitation(elicitation);
751        }
752        self
753    }
754
755    /// Register a sampling provider with the server
756    pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
757        let key = format!("sampling_{}", self.sampling.len());
758        self.sampling.insert(key, Arc::new(sampling));
759        self
760    }
761
762    /// Register multiple sampling providers
763    pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
764        mut self,
765        sampling: I,
766    ) -> Self {
767        for s in sampling {
768            self = self.sampling_provider(s);
769        }
770        self
771    }
772
773    /// Register a completion provider with the server
774    pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
775        let key = format!("completion_{}", self.completions.len());
776        self.completions.insert(key, Arc::new(completion));
777        self
778    }
779
780    /// Register multiple completion providers
781    pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
782        mut self,
783        completions: I,
784    ) -> Self {
785        for completion in completions {
786            self = self.completion_provider(completion);
787        }
788        self
789    }
790
791    /// Register a logger with the server
792    pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
793        let key = format!("logger_{}", self.loggers.len());
794        self.loggers.insert(key, Arc::new(logger));
795        self
796    }
797
798    /// Register multiple loggers
799    pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
800        mut self,
801        loggers: I,
802    ) -> Self {
803        for logger in loggers {
804            self = self.logger(logger);
805        }
806        self
807    }
808
809    /// Register a root provider with the server
810    pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
811        let key = format!("root_{}", self.root_providers.len());
812        self.root_providers.insert(key, Arc::new(root));
813        self
814    }
815
816    /// Register multiple root providers
817    pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
818        mut self,
819        roots: I,
820    ) -> Self {
821        for root in roots {
822            self = self.root_provider(root);
823        }
824        self
825    }
826
827    /// Register a notification provider with the server
828    pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
829        let key = format!("notification_{}", self.notifications.len());
830        self.notifications.insert(key, Arc::new(notification));
831        self
832    }
833
834    /// Register multiple notification providers
835    pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
836        mut self,
837        notifications: I,
838    ) -> Self {
839        for notification in notifications {
840            self = self.notification_provider(notification);
841        }
842        self
843    }
844
845    /// Check if URI contains template variables (e.g., {ticker}, {id})
846    fn is_template_uri(&self, uri: &str) -> bool {
847        uri.contains('{') && uri.contains('}')
848    }
849
850    /// Validate URI is absolute and well-formed (reusing SecurityMiddleware logic)
851    fn validate_uri(&self, uri: &str) -> std::result::Result<(), String> {
852        // Check basic URI format - must have scheme
853        if !uri.contains("://") {
854            return Err(
855                "URI must be absolute with scheme (e.g., file://, http://, memory://)".to_string(),
856            );
857        }
858
859        // Check for whitespace and control characters
860        if uri.chars().any(|c| c.is_whitespace() || c.is_control()) {
861            return Err("URI must not contain whitespace or control characters".to_string());
862        }
863
864        // For file URIs, require absolute paths
865        if let Some(path_part) = uri.strip_prefix("file://") {
866            // Skip "file://"
867            if !path_part.starts_with('/') {
868                return Err("file:// URIs must use absolute paths".to_string());
869            }
870        }
871
872        Ok(())
873    }
874
875    /// Validate URI template pattern (basic validation for template syntax)
876    fn validate_uri_template(&self, template: &str) -> std::result::Result<(), String> {
877        // First validate the base URI structure (ignoring template variables)
878        let base_uri = template.replace(['{', '}'], "");
879        self.validate_uri(&base_uri)?;
880
881        // Check template variable syntax is balanced
882        let open_braces = template.matches('{').count();
883        let close_braces = template.matches('}').count();
884        if open_braces != close_braces {
885            return Err("URI template has unbalanced braces".to_string());
886        }
887
888        Ok(())
889    }
890
891    // =============================================================================
892    // ZERO-CONFIGURATION CONVENIENCE METHODS
893    // These aliases make the API more intuitive and match the zero-config vision
894    // =============================================================================
895
896    /// Register a sampler - convenient alias for sampling_provider
897    /// Automatically uses "sampling/createMessage" method
898    pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
899        self.sampling_provider(sampling)
900    }
901
902    /// Register a completer - convenient alias for completion_provider
903    /// Automatically uses "completion/complete" method
904    pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
905        self.completion_provider(completion)
906    }
907
908    /// Register a notification by type - type determines method automatically
909    /// This enables the `.notification::<T>()` pattern from universal-turul-mcp-server
910    pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
911        let notification = N::default();
912        self.notification_provider(notification)
913    }
914
915    /// Register a handler with the server
916    pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
917        let handler_arc = Arc::new(handler);
918        for method in handler_arc.supported_methods() {
919            self.handlers.insert(method, handler_arc.clone());
920        }
921        self
922    }
923
924    /// Register multiple handlers
925    pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
926        mut self,
927        handlers: I,
928    ) -> Self {
929        for handler in handlers {
930            self = self.handler(handler);
931        }
932        self
933    }
934
935    /// Add completion support
936    pub fn with_completion(mut self) -> Self {
937        self.capabilities.completions = Some(CompletionsCapabilities {
938            enabled: Some(true),
939        });
940        self.handler(CompletionHandler)
941    }
942
943    /// Add prompts support
944    pub fn with_prompts(mut self) -> Self {
945        self.capabilities.prompts = Some(PromptsCapabilities {
946            list_changed: Some(false),
947        });
948
949        // Prompts handlers are automatically registered when prompts are added via .prompt()
950        // This method now just enables the capability
951        self
952    }
953
954    /// Add resources support
955    ///
956    /// **Note**: This method is now optional. The framework automatically calls this
957    /// when resources are registered via `.resource()` or `.template_resource()`.
958    /// You only need to call this explicitly if you want to enable resource capabilities
959    /// without registering any resources.
960    pub fn with_resources(mut self) -> Self {
961        // Enable notifications if we have resources
962        let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
963
964        self.capabilities.resources = Some(ResourcesCapabilities {
965            subscribe: Some(false), // TODO: Implement resource subscriptions
966            list_changed: Some(has_resources),
967        });
968
969        // Create ResourcesListHandler and add all registered resources
970        let mut list_handler = ResourcesListHandler::new();
971        tracing::debug!(
972            "with_resources() - adding {} static resources to list handler",
973            self.resources.len()
974        );
975        for (uri, resource) in &self.resources {
976            tracing::debug!("Adding static resource to list handler: {}", uri);
977            list_handler = list_handler.add_resource_arc(resource.clone());
978        }
979
980        // Template resources should NOT be added to ResourcesListHandler
981        // They only appear in ResourceTemplatesHandler (resources/templates/list)
982        // NOT in resources/list
983
984        // Create ResourcesReadHandler and add all registered resources
985        // Auto-configure security based on registered resources
986        let mut read_handler = if self.test_mode {
987            ResourcesReadHandler::new().without_security()
988        } else if has_resources {
989            // Auto-generate security configuration from registered resources
990            let security_middleware = self.build_resource_security();
991            ResourcesReadHandler::new().with_security(Arc::new(security_middleware))
992        } else {
993            ResourcesReadHandler::new()
994        };
995        for resource in self.resources.values() {
996            read_handler = read_handler.add_resource_arc(resource.clone());
997        }
998
999        // Add template resources to read handler with template support
1000        for (template, resource) in &self.template_resources {
1001            read_handler =
1002                read_handler.add_template_resource_arc(template.clone(), resource.clone());
1003        }
1004
1005        // Update notification manager
1006
1007        // Register both handlers
1008        self.handler(list_handler).handler(read_handler)
1009    }
1010
1011    /// Add logging support
1012    pub fn with_logging(mut self) -> Self {
1013        self.capabilities.logging = Some(LoggingCapabilities::default());
1014        self.handler(LoggingHandler)
1015    }
1016
1017    /// Add roots support
1018    pub fn with_roots(self) -> Self {
1019        // Note: roots is not part of standard server capabilities yet
1020        // Could be added to experimental if needed
1021        self.handler(RootsHandler::new())
1022    }
1023
1024    /// Add a single root directory
1025    pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
1026        self.roots.push(root);
1027        self
1028    }
1029
1030    /// Add sampling support
1031    pub fn with_sampling(self) -> Self {
1032        self.handler(SamplingHandler)
1033    }
1034
1035    /// Add elicitation support with default mock provider
1036    pub fn with_elicitation(mut self) -> Self {
1037        self.capabilities.elicitation = Some(ElicitationCapabilities {
1038            enabled: Some(true),
1039        });
1040        self.handler(ElicitationHandler::with_mock_provider())
1041    }
1042
1043    /// Add elicitation support with custom provider
1044    pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(
1045        mut self,
1046        provider: P,
1047    ) -> Self {
1048        self.capabilities.elicitation = Some(ElicitationCapabilities {
1049            enabled: Some(true),
1050        });
1051        self.handler(ElicitationHandler::new(Arc::new(provider)))
1052    }
1053
1054    /// Add notifications support
1055    pub fn with_notifications(self) -> Self {
1056        self.handler(NotificationsHandler)
1057    }
1058
1059    /// Configure session timeout (in minutes, default: 30)
1060    pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
1061        self.session_timeout_minutes = Some(minutes);
1062        self
1063    }
1064
1065    /// Configure session cleanup interval (in seconds, default: 60)
1066    pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
1067        self.session_cleanup_interval_seconds = Some(seconds);
1068        self
1069    }
1070
1071    /// Enable strict MCP lifecycle enforcement
1072    ///
1073    /// When enabled, the server will reject all operations (tools, resources, etc.)
1074    /// until the client sends `notifications/initialized` after receiving the
1075    /// initialize response.
1076    ///
1077    /// **Default: false (lenient mode)** - for compatibility with existing clients
1078    /// **Production: consider true** - for strict MCP spec compliance
1079    ///
1080    /// # Example
1081    /// ```rust,no_run
1082    /// use turul_mcp_server::McpServer;
1083    ///
1084    /// let server = McpServer::builder()
1085    ///     .name("strict-server")
1086    ///     .version("1.0.0")
1087    ///     .strict_lifecycle(true)  // Enable strict enforcement
1088    ///     .build()?;
1089    /// # Ok::<(), Box<dyn std::error::Error>>(())
1090    /// ```
1091    pub fn strict_lifecycle(mut self, strict: bool) -> Self {
1092        self.strict_lifecycle = strict;
1093        self
1094    }
1095
1096    /// Enable strict MCP lifecycle enforcement (convenience method)
1097    ///
1098    /// Equivalent to `.strict_lifecycle(true)`. Enables strict enforcement where
1099    /// all operations are rejected until `notifications/initialized` is received.
1100    pub fn with_strict_lifecycle(self) -> Self {
1101        self.strict_lifecycle(true)
1102    }
1103
1104    /// Enable test mode - disables security middleware for test servers
1105    ///
1106    /// In test mode, ResourcesReadHandler is created without security middleware,
1107    /// allowing custom URI schemes for testing (binary://, memory://, error://, etc.).
1108    /// Production servers should NOT use test mode as it bypasses security controls.
1109    ///
1110    /// # Example
1111    /// ```rust,no_run
1112    /// use turul_mcp_server::McpServer;
1113    ///
1114    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1115    /// let server = McpServer::builder()
1116    ///     .name("test-server")
1117    ///     .version("1.0.0")
1118    ///     .test_mode()  // Disable security for testing
1119    ///     .with_resources()
1120    ///     .build()?;
1121    /// # Ok(())
1122    /// # }
1123    /// ```
1124    pub fn test_mode(mut self) -> Self {
1125        self.test_mode = true;
1126        self
1127    }
1128
1129    /// Configure sessions with recommended defaults for long-running sessions
1130    pub fn with_long_sessions(mut self) -> Self {
1131        self.session_timeout_minutes = Some(120); // 2 hours
1132        self.session_cleanup_interval_seconds = Some(300); // 5 minutes
1133        self
1134    }
1135
1136    /// Configure sessions with recommended defaults for short-lived sessions
1137    pub fn with_short_sessions(mut self) -> Self {
1138        self.session_timeout_minutes = Some(15); // 15 minutes
1139        self.session_cleanup_interval_seconds = Some(30); // 30 seconds
1140        self
1141    }
1142
1143    /// Configure session storage backend (defaults to InMemory if not specified)
1144    pub fn with_session_storage<
1145        S: turul_mcp_session_storage::SessionStorage<
1146                Error = turul_mcp_session_storage::SessionStorageError,
1147            > + 'static,
1148    >(
1149        mut self,
1150        storage: Arc<S>,
1151    ) -> Self {
1152        // Convert concrete storage type to trait object
1153        let boxed_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> = storage;
1154        self.session_storage = Some(boxed_storage);
1155        self
1156    }
1157
1158    /// Set HTTP bind address (requires "http" feature)
1159    #[cfg(feature = "http")]
1160    pub fn bind_address(mut self, addr: SocketAddr) -> Self {
1161        self.bind_address = addr;
1162        self
1163    }
1164
1165    /// Set MCP endpoint path (requires "http" feature)
1166    #[cfg(feature = "http")]
1167    pub fn mcp_path(mut self, path: impl Into<String>) -> Self {
1168        self.mcp_path = path.into();
1169        self
1170    }
1171
1172    /// Enable/disable CORS (requires "http" feature)
1173    #[cfg(feature = "http")]
1174    pub fn cors(mut self, enable: bool) -> Self {
1175        self.enable_cors = enable;
1176        self
1177    }
1178
1179    /// Enable/disable SSE (requires "sse" feature)
1180    #[cfg(feature = "http")]
1181    pub fn sse(mut self, enable: bool) -> Self {
1182        self.enable_sse = enable;
1183        self
1184    }
1185
1186    /// Auto-generate security configuration based on registered resources
1187    fn build_resource_security(&self) -> crate::security::SecurityMiddleware {
1188        use crate::security::{AccessLevel, ResourceAccessControl, SecurityMiddleware};
1189        use regex::Regex;
1190        use std::collections::HashSet;
1191
1192        let mut allowed_patterns = Vec::new();
1193        let mut allowed_extensions = HashSet::new();
1194
1195        // Extract patterns from static resources
1196        for uri in self.resources.keys() {
1197            // Extract file extension
1198            if let Some(extension) = Self::extract_extension(uri) {
1199                allowed_extensions.insert(extension);
1200            }
1201
1202            // Generate regex pattern for this URI's base path
1203            if let Some(base_pattern) = Self::uri_to_base_pattern(uri) {
1204                allowed_patterns.push(base_pattern);
1205            }
1206        }
1207
1208        // Extract patterns from template resources
1209        for (template, _) in &self.template_resources {
1210            if let Some(pattern) = Self::template_to_regex_pattern(template.pattern()) {
1211                allowed_patterns.push(pattern);
1212            }
1213
1214            // Extract extension from template if present
1215            if let Some(extension) = Self::extract_extension(template.pattern()) {
1216                allowed_extensions.insert(extension);
1217            }
1218        }
1219
1220        // Build allowed MIME types from file extensions
1221        let allowed_mime_types = Self::extensions_to_mime_types(&allowed_extensions);
1222
1223        // Convert pattern strings to Regex objects
1224        let regex_patterns: Vec<Regex> = allowed_patterns
1225            .into_iter()
1226            .filter_map(|pattern| Regex::new(&pattern).ok())
1227            .collect();
1228
1229        tracing::debug!(
1230            "Auto-generated resource security: {} patterns, {} mime types",
1231            regex_patterns.len(),
1232            allowed_mime_types.len()
1233        );
1234
1235        SecurityMiddleware::new().with_resource_access_control(ResourceAccessControl {
1236            access_level: AccessLevel::Public, // Allow access without session for auto-detected resources
1237            allowed_patterns: regex_patterns,
1238            blocked_patterns: vec![
1239                Regex::new(r"\.\.").unwrap(),  // Still prevent directory traversal
1240                Regex::new(r"/etc/").unwrap(), // Block system directories
1241                Regex::new(r"/proc/").unwrap(),
1242            ],
1243            max_size: Some(50 * 1024 * 1024), // 50MB limit for auto-detected resources
1244            allowed_mime_types: Some(allowed_mime_types),
1245        })
1246    }
1247
1248    /// Extract file extension from URI
1249    fn extract_extension(uri: &str) -> Option<String> {
1250        uri.split('.')
1251            .next_back()
1252            .filter(|ext| !ext.is_empty() && ext.len() <= 10)
1253            .map(|ext| ext.to_lowercase())
1254    }
1255
1256    /// Convert URI to base regex pattern that allows files in the same directory
1257    fn uri_to_base_pattern(uri: &str) -> Option<String> {
1258        if let Some(last_slash) = uri.rfind('/') {
1259            let base_path = &uri[..last_slash];
1260            Some(format!("^{}/[^/]+$", regex::escape(base_path)))
1261        } else {
1262            None
1263        }
1264    }
1265
1266    /// Convert URI template to regex pattern
1267    fn template_to_regex_pattern(template: &str) -> Option<String> {
1268        use regex::Regex;
1269
1270        // Create a regex to find template variables in the original template
1271        let template_var_regex = Regex::new(r"\{[^}]+\}").ok()?;
1272
1273        let mut result = String::new();
1274        let mut last_end = 0;
1275
1276        // Process each template variable
1277        for mat in template_var_regex.find_iter(template) {
1278            // Add the escaped literal part before this match
1279            result.push_str(&regex::escape(&template[last_end..mat.start()]));
1280
1281            // Add the regex pattern for the template variable
1282            result.push_str("[a-zA-Z0-9_.-]+"); // Allow dots for IDs like announcement_id
1283
1284            last_end = mat.end();
1285        }
1286
1287        // Add any remaining literal part
1288        result.push_str(&regex::escape(&template[last_end..]));
1289
1290        Some(format!("^{}$", result))
1291    }
1292
1293    /// Map file extensions to MIME types
1294    fn extensions_to_mime_types(extensions: &HashSet<String>) -> Vec<String> {
1295        let mut mime_types = Vec::new();
1296
1297        for ext in extensions {
1298            match ext.as_str() {
1299                "json" => mime_types.push("application/json".to_string()),
1300                "csv" => mime_types.push("text/csv".to_string()),
1301                "txt" => mime_types.push("text/plain".to_string()),
1302                "html" => mime_types.push("text/html".to_string()),
1303                "md" => mime_types.push("text/markdown".to_string()),
1304                "xml" => mime_types.push("application/xml".to_string()),
1305                "pdf" => mime_types.push("application/pdf".to_string()),
1306                "png" => mime_types.push("image/png".to_string()),
1307                "jpg" | "jpeg" => mime_types.push("image/jpeg".to_string()),
1308                _ => {} // Unknown extensions not explicitly allowed
1309            }
1310        }
1311
1312        // Always allow basic text types
1313        mime_types.extend_from_slice(&["text/plain".to_string(), "application/json".to_string()]);
1314
1315        mime_types.sort();
1316        mime_types.dedup();
1317        mime_types
1318    }
1319
1320    /// Build the MCP server
1321    pub fn build(mut self) -> Result<McpServer> {
1322        // Validate configuration
1323        if self.name.is_empty() {
1324            return Err(McpError::configuration("Server name cannot be empty"));
1325        }
1326        if self.version.is_empty() {
1327            return Err(McpError::configuration("Server version cannot be empty"));
1328        }
1329
1330        // Check for validation errors collected during registration
1331        if !self.validation_errors.is_empty() {
1332            return Err(McpError::configuration(&format!(
1333                "Resource validation errors:\n{}",
1334                self.validation_errors.join("\n")
1335            )));
1336        }
1337
1338        // Auto-register resource handlers if resources were registered
1339        // This eliminates the need for manual .with_resources() calls
1340        let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
1341        if has_resources {
1342            // Automatically configure resource handlers - this will replace the empty defaults
1343            self = self.with_resources();
1344        }
1345
1346        // Auto-detect and configure server capabilities based on registered components
1347        let has_tools = !self.tools.is_empty();
1348        let has_prompts = !self.prompts.is_empty();
1349        let has_roots = !self.roots.is_empty();
1350        let has_elicitations = !self.elicitations.is_empty();
1351        let has_completions = !self.completions.is_empty();
1352        let has_samplings = !self.sampling.is_empty();
1353        tracing::debug!("🔧 Has sampling configured: {}", has_samplings);
1354        let has_logging = !self.loggers.is_empty();
1355        tracing::debug!("🔧 Has logging configured: {}", has_logging);
1356
1357        // Tools capabilities - support notifications only if tools are registered AND we have dynamic change sources
1358        // Note: In current static framework, tool set is fixed at build time and doesn't change
1359        // list_changed should only be true when dynamic change sources are wired, such as:
1360        // - Hot-reload configuration systems
1361        // - Admin APIs for runtime tool registration
1362        // - Plugin systems with dynamic tool loading
1363        if has_tools {
1364            self.capabilities.tools = Some(ToolsCapabilities {
1365                // Static framework: no dynamic change sources = no list changes
1366                list_changed: Some(false),
1367            });
1368        }
1369
1370        // Prompts capabilities - support notifications only when dynamic change sources are wired
1371        // Note: In current static framework, prompt set is fixed at build time and doesn't change
1372        // list_changed should only be true when dynamic change sources are wired, such as:
1373        // - Hot-reload configuration systems
1374        // - Admin APIs for runtime prompt registration
1375        // - Plugin systems with dynamic prompt loading
1376        if has_prompts {
1377            self.capabilities.prompts = Some(PromptsCapabilities {
1378                // Static framework: no dynamic change sources = no list changes
1379                list_changed: Some(false),
1380            });
1381        }
1382
1383        // Resources capabilities - support notifications only when dynamic change sources are wired
1384        // Note: In current static framework, resource set is fixed at build time and doesn't change
1385        // list_changed should only be true when dynamic change sources are wired, such as:
1386        // - Hot-reload configuration systems
1387        // - Admin APIs for runtime resource registration
1388        // - File system watchers that update resource availability
1389        if has_resources {
1390            self.capabilities.resources = Some(ResourcesCapabilities {
1391                subscribe: Some(false), // TODO: Implement resource subscriptions in Phase 5
1392                // Static framework: no dynamic change sources = no list changes
1393                list_changed: Some(false),
1394            });
1395        }
1396
1397        // Elicitation capabilities - enable if elicitation handlers are registered
1398        if has_elicitations {
1399            self.capabilities.elicitation = Some(ElicitationCapabilities {
1400                enabled: Some(true),
1401            });
1402        }
1403
1404        // Completion capabilities - enable if completion handlers are registered
1405        if has_completions {
1406            self.capabilities.completions = Some(CompletionsCapabilities {
1407                enabled: Some(true),
1408            });
1409        }
1410
1411        // Logging capabilities - always enabled with comprehensive level support
1412        // Always enable logging for debugging/monitoring
1413        self.capabilities.logging = Some(LoggingCapabilities {
1414            enabled: Some(true),
1415            levels: Some(vec![
1416                "debug".to_string(),
1417                "info".to_string(),
1418                "warning".to_string(),
1419                "error".to_string(),
1420            ]),
1421        });
1422
1423        tracing::debug!("🔧 Auto-configured server capabilities:");
1424        tracing::debug!("   - Tools: {}", has_tools);
1425        tracing::debug!("   - Resources: {}", has_resources);
1426        tracing::debug!("   - Prompts: {}", has_prompts);
1427        tracing::debug!("   - Roots: {}", has_roots);
1428        tracing::debug!("   - Elicitation: {}", has_elicitations);
1429        tracing::debug!("   - Completions: {}", has_completions);
1430        tracing::debug!("   - Logging: enabled");
1431
1432        // Create implementation info
1433        let mut implementation = Implementation::new(&self.name, &self.version);
1434        if let Some(title) = self.title {
1435            implementation = implementation.with_title(title);
1436        }
1437
1438        // Add RootsHandler if roots were configured
1439        let mut handlers = self.handlers;
1440        if !self.roots.is_empty() {
1441            let mut roots_handler = RootsHandler::new();
1442            for root in self.roots {
1443                roots_handler = roots_handler.add_root(root);
1444            }
1445            handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1446        }
1447
1448        // Add PromptsHandlers if prompts were configured
1449        if !self.prompts.is_empty() {
1450            let mut prompts_list_handler = PromptsListHandler::new();
1451            let mut prompts_get_handler = PromptsGetHandler::new();
1452
1453            for prompt in self.prompts.values() {
1454                prompts_list_handler = prompts_list_handler.add_prompt_arc(prompt.clone());
1455                prompts_get_handler = prompts_get_handler.add_prompt_arc(prompt.clone());
1456            }
1457
1458            handlers.insert("prompts/list".to_string(), Arc::new(prompts_list_handler));
1459            handlers.insert("prompts/get".to_string(), Arc::new(prompts_get_handler));
1460        }
1461
1462        // Add ResourceTemplatesHandler if template resources were configured
1463        if !self.template_resources.is_empty() {
1464            let resource_templates_handler =
1465                ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1466            handlers.insert(
1467                "resources/templates/list".to_string(),
1468                Arc::new(resource_templates_handler),
1469            );
1470        }
1471
1472        // Add ProvidedSamplingHandler if sampling providers were configured
1473        // This replaces the default SamplingHandler with one that actually calls
1474        // the registered providers' validate_request() and sample() methods
1475        if !self.sampling.is_empty() {
1476            handlers.insert(
1477                "sampling/createMessage".to_string(),
1478                Arc::new(ProvidedSamplingHandler::new(self.sampling)),
1479            );
1480        }
1481
1482        // Create server
1483        Ok(McpServer::new(
1484            implementation,
1485            self.capabilities,
1486            self.tools,
1487            handlers,
1488            self.instructions,
1489            self.session_timeout_minutes,
1490            self.session_cleanup_interval_seconds,
1491            self.session_storage,
1492            self.strict_lifecycle,
1493            self.middleware_stack,
1494            #[cfg(feature = "http")]
1495            self.bind_address,
1496            #[cfg(feature = "http")]
1497            self.mcp_path,
1498            #[cfg(feature = "http")]
1499            self.enable_cors,
1500            #[cfg(feature = "http")]
1501            self.enable_sse,
1502        ))
1503    }
1504}
1505
1506impl Default for McpServerBuilder {
1507    fn default() -> Self {
1508        Self::new()
1509    }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use super::*;
1515    use crate::{McpTool, SessionContext};
1516    use async_trait::async_trait;
1517    use serde_json::Value;
1518    use std::collections::HashMap;
1519    use turul_mcp_protocol::tools::ToolAnnotations;
1520    use turul_mcp_builders::prelude::*;  // HasBaseMetadata, HasDescription, etc.
1521    use turul_mcp_protocol::{CallToolResult, ToolSchema};
1522
1523    struct TestTool {
1524        input_schema: ToolSchema,
1525    }
1526
1527    impl TestTool {
1528        fn new() -> Self {
1529            Self {
1530                input_schema: ToolSchema::object(),
1531            }
1532        }
1533    }
1534
1535    // Implement fine-grained traits
1536    impl HasBaseMetadata for TestTool {
1537        fn name(&self) -> &str {
1538            "test"
1539        }
1540        fn title(&self) -> Option<&str> {
1541            None
1542        }
1543    }
1544
1545    impl HasDescription for TestTool {
1546        fn description(&self) -> Option<&str> {
1547            Some("Test tool")
1548        }
1549    }
1550
1551    impl HasInputSchema for TestTool {
1552        fn input_schema(&self) -> &ToolSchema {
1553            &self.input_schema
1554        }
1555    }
1556
1557    impl HasOutputSchema for TestTool {
1558        fn output_schema(&self) -> Option<&ToolSchema> {
1559            None
1560        }
1561    }
1562
1563    impl HasAnnotations for TestTool {
1564        fn annotations(&self) -> Option<&ToolAnnotations> {
1565            None
1566        }
1567    }
1568
1569    impl HasToolMeta for TestTool {
1570        fn tool_meta(&self) -> Option<&HashMap<String, Value>> {
1571            None
1572        }
1573    }
1574
1575    #[async_trait]
1576    impl McpTool for TestTool {
1577        async fn call(
1578            &self,
1579            _args: Value,
1580            _session: Option<SessionContext>,
1581        ) -> crate::McpResult<CallToolResult> {
1582            Ok(CallToolResult::success(vec![
1583                turul_mcp_protocol::ToolResult::text("test"),
1584            ]))
1585        }
1586    }
1587
1588    #[test]
1589    fn test_builder_defaults() {
1590        let builder = McpServerBuilder::new();
1591        assert_eq!(builder.name, "turul-mcp-server");
1592        assert_eq!(builder.version, "1.0.0");
1593        assert!(builder.title.is_none());
1594        assert!(builder.instructions.is_none());
1595        assert!(builder.tools.is_empty());
1596        assert_eq!(builder.handlers.len(), 17); // Default MCP 2025-06-18 handlers
1597        assert!(builder.handlers.contains_key("ping"));
1598    }
1599
1600    #[test]
1601    fn test_builder_configuration() {
1602        let builder = McpServerBuilder::new()
1603            .name("test-server")
1604            .version("2.0.0")
1605            .title("Test Server")
1606            .instructions("This is a test server");
1607
1608        assert_eq!(builder.name, "test-server");
1609        assert_eq!(builder.version, "2.0.0");
1610        assert_eq!(builder.title, Some("Test Server".to_string()));
1611        assert_eq!(
1612            builder.instructions,
1613            Some("This is a test server".to_string())
1614        );
1615    }
1616
1617    #[test]
1618    fn test_builder_build() {
1619        let server = McpServerBuilder::new()
1620            .name("test-server")
1621            .version("1.0.0")
1622            .tool(TestTool::new())
1623            .build()
1624            .unwrap();
1625
1626        assert_eq!(server.implementation.name, "test-server");
1627        assert_eq!(server.implementation.version, "1.0.0");
1628    }
1629
1630    #[test]
1631    fn test_builder_validation() {
1632        let result = McpServerBuilder::new().name("").build();
1633
1634        assert!(result.is_err());
1635        assert!(matches!(
1636            result.unwrap_err(),
1637            McpError::ConfigurationError(_)
1638        ));
1639    }
1640
1641    // Test resources for auto-detection testing
1642    use turul_mcp_protocol::resources::ResourceContent;
1643    // Resource traits now in builders crate (already imported via prelude above)
1644
1645    struct StaticTestResource;
1646
1647    impl HasResourceMetadata for StaticTestResource {
1648        fn name(&self) -> &str {
1649            "static_test"
1650        }
1651    }
1652
1653    impl HasResourceDescription for StaticTestResource {
1654        fn description(&self) -> Option<&str> {
1655            Some("Static test resource")
1656        }
1657    }
1658
1659    impl HasResourceUri for StaticTestResource {
1660        fn uri(&self) -> &str {
1661            "file:///test.txt"
1662        }
1663    }
1664
1665    impl HasResourceMimeType for StaticTestResource {
1666        fn mime_type(&self) -> Option<&str> {
1667            Some("text/plain")
1668        }
1669    }
1670
1671    impl HasResourceSize for StaticTestResource {
1672        fn size(&self) -> Option<u64> {
1673            None
1674        }
1675    }
1676
1677    impl HasResourceAnnotations for StaticTestResource {
1678        fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1679            None
1680        }
1681    }
1682
1683    impl HasResourceMeta for StaticTestResource {
1684        fn resource_meta(&self) -> Option<&HashMap<String, Value>> {
1685            None
1686        }
1687    }
1688
1689    #[async_trait]
1690    impl crate::McpResource for StaticTestResource {
1691        async fn read(&self, _params: Option<Value>, _session: Option<&crate::SessionContext>) -> crate::McpResult<Vec<ResourceContent>> {
1692            Ok(vec![ResourceContent::text(
1693                "file:///test.txt",
1694                "test content",
1695            )])
1696        }
1697    }
1698
1699    struct TemplateTestResource;
1700
1701    impl HasResourceMetadata for TemplateTestResource {
1702        fn name(&self) -> &str {
1703            "template_test"
1704        }
1705    }
1706
1707    impl HasResourceDescription for TemplateTestResource {
1708        fn description(&self) -> Option<&str> {
1709            Some("Template test resource")
1710        }
1711    }
1712
1713    impl HasResourceUri for TemplateTestResource {
1714        fn uri(&self) -> &str {
1715            "template://data/{id}.json"
1716        }
1717    }
1718
1719    impl HasResourceMimeType for TemplateTestResource {
1720        fn mime_type(&self) -> Option<&str> {
1721            Some("application/json")
1722        }
1723    }
1724
1725    impl HasResourceSize for TemplateTestResource {
1726        fn size(&self) -> Option<u64> {
1727            None
1728        }
1729    }
1730
1731    impl HasResourceAnnotations for TemplateTestResource {
1732        fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1733            None
1734        }
1735    }
1736
1737    impl HasResourceMeta for TemplateTestResource {
1738        fn resource_meta(&self) -> Option<&HashMap<String, Value>> {
1739            None
1740        }
1741    }
1742
1743    #[async_trait]
1744    impl crate::McpResource for TemplateTestResource {
1745        async fn read(&self, _params: Option<Value>, _session: Option<&crate::SessionContext>) -> crate::McpResult<Vec<ResourceContent>> {
1746            Ok(vec![ResourceContent::text(
1747                "template://data/123.json",
1748                "test content",
1749            )])
1750        }
1751    }
1752
1753    #[test]
1754    fn test_is_template_uri() {
1755        let builder = McpServerBuilder::new();
1756
1757        // Test static URIs
1758        assert!(!builder.is_template_uri("file:///test.txt"));
1759        assert!(!builder.is_template_uri("http://example.com/api"));
1760        assert!(!builder.is_template_uri("memory://cache"));
1761
1762        // Test template URIs
1763        assert!(builder.is_template_uri("file:///data/{id}.json"));
1764        assert!(builder.is_template_uri("template://users/{user_id}"));
1765        assert!(builder.is_template_uri("api://v1/{resource}/{id}"));
1766
1767        // Test edge cases
1768        assert!(!builder.is_template_uri("file:///no-braces.txt"));
1769        assert!(!builder.is_template_uri("file:///missing-close.txt{"));
1770        assert!(!builder.is_template_uri("file:///missing-open.txt}"));
1771        assert!(builder.is_template_uri("file:///multiple/{id}/{type}.json"));
1772    }
1773
1774    #[test]
1775    fn test_auto_detection_static_resource() {
1776        let builder = McpServerBuilder::new()
1777            .name("test-server")
1778            .resource(StaticTestResource);
1779
1780        // Verify static resource was added to resources collection
1781        assert_eq!(builder.resources.len(), 1);
1782        assert!(builder.resources.contains_key("file:///test.txt"));
1783        assert_eq!(builder.template_resources.len(), 0);
1784        assert_eq!(builder.validation_errors.len(), 0);
1785    }
1786
1787    #[test]
1788    fn test_auto_detection_template_resource() {
1789        let builder = McpServerBuilder::new()
1790            .name("test-server")
1791            .resource(TemplateTestResource);
1792
1793        // Verify template resource was added to template_resources collection
1794        assert_eq!(builder.resources.len(), 0);
1795        assert_eq!(builder.template_resources.len(), 1);
1796        assert_eq!(builder.validation_errors.len(), 0);
1797
1798        // Verify the template pattern is correct
1799        let (template, _) = &builder.template_resources[0];
1800        assert_eq!(template.pattern(), "template://data/{id}.json");
1801    }
1802
1803    #[test]
1804    fn test_auto_detection_mixed_resources() {
1805        let builder = McpServerBuilder::new()
1806            .name("test-server")
1807            .resource(StaticTestResource)
1808            .resource(TemplateTestResource);
1809
1810        // Verify both resources were categorized correctly
1811        assert_eq!(builder.resources.len(), 1);
1812        assert!(builder.resources.contains_key("file:///test.txt"));
1813        assert_eq!(builder.template_resources.len(), 1);
1814
1815        let (template, _) = &builder.template_resources[0];
1816        assert_eq!(template.pattern(), "template://data/{id}.json");
1817    }
1818
1819    #[test]
1820    fn test_template_resource_explicit_still_works() {
1821        let template = crate::uri_template::UriTemplate::new("template://explicit/{id}")
1822            .expect("Failed to create template");
1823
1824        let builder = McpServerBuilder::new()
1825            .name("test-server")
1826            .template_resource(template, TemplateTestResource);
1827
1828        // Verify explicit template_resource still works
1829        assert_eq!(builder.resources.len(), 0);
1830        assert_eq!(builder.template_resources.len(), 1);
1831
1832        let (template, _) = &builder.template_resources[0];
1833        assert_eq!(template.pattern(), "template://explicit/{id}");
1834    }
1835
1836    #[test]
1837    fn test_invalid_template_uri_error_handling() {
1838        struct InvalidTemplateResource;
1839
1840        impl HasResourceMetadata for InvalidTemplateResource {
1841            fn name(&self) -> &str {
1842                "invalid_template"
1843            }
1844        }
1845
1846        impl HasResourceDescription for InvalidTemplateResource {
1847            fn description(&self) -> Option<&str> {
1848                Some("Invalid template resource")
1849            }
1850        }
1851
1852        impl HasResourceUri for InvalidTemplateResource {
1853            fn uri(&self) -> &str {
1854                "not-a-valid-uri-{id}"
1855            } // Invalid base URI without scheme
1856        }
1857
1858        impl HasResourceMimeType for InvalidTemplateResource {
1859            fn mime_type(&self) -> Option<&str> {
1860                None
1861            }
1862        }
1863
1864        impl HasResourceSize for InvalidTemplateResource {
1865            fn size(&self) -> Option<u64> {
1866                None
1867            }
1868        }
1869
1870        impl HasResourceAnnotations for InvalidTemplateResource {
1871            fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1872                None
1873            }
1874        }
1875
1876        impl HasResourceMeta for InvalidTemplateResource {
1877            fn resource_meta(&self) -> Option<&HashMap<String, Value>> {
1878                None
1879            }
1880        }
1881
1882        #[async_trait]
1883        impl crate::McpResource for InvalidTemplateResource {
1884            async fn read(&self, _params: Option<Value>, _session: Option<&crate::SessionContext>) -> crate::McpResult<Vec<ResourceContent>> {
1885                Ok(vec![])
1886            }
1887        }
1888
1889        let builder = McpServerBuilder::new()
1890            .name("test-server")
1891            .resource(InvalidTemplateResource);
1892
1893        // The URI "not-a-valid-uri-{id}" has braces but lacks a scheme
1894        // So it will be detected as a template but fail validation during template creation
1895        assert!(!builder.validation_errors.is_empty());
1896        assert!(builder.validation_errors[0].contains("URI must be absolute with scheme"));
1897
1898        // Resource is still added to template collection but validation error is captured
1899        // The error will be reported during build() to prevent invalid servers
1900        assert_eq!(builder.resources.len(), 0);
1901        assert_eq!(builder.template_resources.len(), 1);
1902    }
1903
1904    #[test]
1905    fn test_automatic_resource_handler_registration() {
1906        // Test that resources automatically register handlers without needing .with_resources()
1907        let server_result = McpServerBuilder::new()
1908            .name("auto-resources-server")
1909            .resource(StaticTestResource)
1910            .resource(TemplateTestResource)
1911            .build();
1912
1913        // Server should build successfully with automatic resource handler registration
1914        assert!(server_result.is_ok());
1915    }
1916
1917    #[test]
1918    fn test_no_resources_builds_successfully() {
1919        // Test that servers without resources build successfully
1920        let server_result = McpServerBuilder::new().name("no-resources-server").build();
1921
1922        assert!(server_result.is_ok());
1923    }
1924
1925    #[test]
1926    fn test_explicit_with_resources_still_works() {
1927        // Test that explicit .with_resources() still works (no double registration)
1928        let server_result = McpServerBuilder::new()
1929            .name("explicit-resources-server")
1930            .resource(StaticTestResource)
1931            .with_resources() // Explicit call should not cause issues
1932            .build();
1933
1934        // Should build successfully even with explicit .with_resources() call
1935        assert!(server_result.is_ok());
1936    }
1937
1938    // Function resource constructor for testing resource_fn method
1939    fn create_static_test_resource() -> StaticTestResource {
1940        StaticTestResource
1941    }
1942
1943    fn create_template_test_resource() -> TemplateTestResource {
1944        TemplateTestResource
1945    }
1946
1947    #[test]
1948    fn test_resource_fn_static_resource() {
1949        // Test resource_fn with static resource (no template variables)
1950        let builder = McpServerBuilder::new()
1951            .name("resource-fn-static-server")
1952            .resource_fn(create_static_test_resource);
1953
1954        // Verify static resource was registered correctly via resource_fn
1955        assert_eq!(builder.resources.len(), 1);
1956        assert!(builder.resources.contains_key("file:///test.txt"));
1957        assert_eq!(builder.template_resources.len(), 0);
1958        assert_eq!(builder.validation_errors.len(), 0);
1959    }
1960
1961    #[test]
1962    fn test_resource_fn_template_resource() {
1963        // Test resource_fn with template resource (has template variables)
1964        let builder = McpServerBuilder::new()
1965            .name("resource-fn-template-server")
1966            .resource_fn(create_template_test_resource);
1967
1968        // Verify template resource was auto-detected and registered correctly via resource_fn
1969        assert_eq!(builder.resources.len(), 0);
1970        assert_eq!(builder.template_resources.len(), 1);
1971        assert_eq!(builder.validation_errors.len(), 0);
1972
1973        // Verify the template pattern is correct
1974        let (template, _) = &builder.template_resources[0];
1975        assert_eq!(template.pattern(), "template://data/{id}.json");
1976    }
1977
1978    #[test]
1979    fn test_resource_fn_mixed_with_direct_registration() {
1980        // Test that resource_fn works alongside direct .resource() calls
1981        let builder = McpServerBuilder::new()
1982            .name("mixed-registration-server")
1983            .resource(StaticTestResource) // Direct registration
1984            .resource_fn(create_template_test_resource); // Function registration
1985
1986        // Verify both registration methods work together
1987        assert_eq!(builder.resources.len(), 1);
1988        assert!(builder.resources.contains_key("file:///test.txt"));
1989        assert_eq!(builder.template_resources.len(), 1);
1990
1991        let (template, _) = &builder.template_resources[0];
1992        assert_eq!(template.pattern(), "template://data/{id}.json");
1993    }
1994
1995    #[test]
1996    fn test_resource_fn_multiple_resources() {
1997        // Test registering multiple resources via resource_fn
1998        let builder = McpServerBuilder::new()
1999            .name("multi-resource-fn-server")
2000            .resource_fn(create_static_test_resource)
2001            .resource_fn(create_template_test_resource);
2002
2003        // Verify both resources were registered correctly
2004        assert_eq!(builder.resources.len(), 1);
2005        assert!(builder.resources.contains_key("file:///test.txt"));
2006        assert_eq!(builder.template_resources.len(), 1);
2007
2008        let (template, _) = &builder.template_resources[0];
2009        assert_eq!(template.pattern(), "template://data/{id}.json");
2010    }
2011
2012    #[test]
2013    fn test_resource_fn_builds_successfully() {
2014        // Test that server builds successfully with resource_fn registrations
2015        let server_result = McpServerBuilder::new()
2016            .name("resource-fn-build-server")
2017            .resource_fn(create_static_test_resource)
2018            .resource_fn(create_template_test_resource)
2019            .build();
2020
2021        // Server should build successfully with automatic resource handler registration
2022        assert!(server_result.is_ok());
2023
2024        let server = server_result.unwrap();
2025        assert_eq!(server.implementation.name, "resource-fn-build-server");
2026
2027        // Verify capabilities were auto-configured for resources
2028        assert!(server.capabilities.resources.is_some());
2029        let resources_caps = server.capabilities.resources.as_ref().unwrap();
2030        assert_eq!(resources_caps.list_changed, Some(false)); // Static framework
2031    }
2032}