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(®ex::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(®ex::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}