Skip to main content

turul_mcp_aws_lambda/
builder.rs

1//! High-level builder API for Lambda MCP servers
2//!
3//! This module provides a fluent builder API similar to McpServer::builder()
4//! but specifically designed for AWS Lambda deployment.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use turul_http_mcp_server::{ServerConfig, StreamConfig};
10use turul_mcp_protocol::{Implementation, ServerCapabilities};
11use turul_mcp_server::handlers::{McpHandler, *};
12use turul_mcp_server::{
13    McpCompletion, McpElicitation, McpLogger, McpNotification, McpPrompt, McpResource, McpRoot,
14    McpSampling, McpTool,
15};
16use turul_mcp_session_storage::BoxedSessionStorage;
17
18use crate::error::Result;
19
20#[cfg(feature = "dynamodb")]
21use crate::error::LambdaError;
22use crate::server::LambdaMcpServer;
23
24#[cfg(feature = "cors")]
25use crate::cors::CorsConfig;
26
27/// High-level builder for Lambda MCP servers
28///
29/// This provides a clean, fluent API for building Lambda MCP servers
30/// similar to the framework's McpServer::builder() pattern.
31///
32/// ## Example
33///
34/// ```rust,no_run
35/// use std::sync::Arc;
36/// use turul_mcp_aws_lambda::LambdaMcpServerBuilder;
37/// use turul_mcp_session_storage::InMemorySessionStorage;
38/// use turul_mcp_derive::McpTool;
39/// use turul_mcp_server::{McpResult, SessionContext};
40///
41/// #[derive(McpTool, Clone, Default)]
42/// #[tool(name = "example", description = "Example tool")]
43/// struct ExampleTool {
44///     #[param(description = "Example parameter")]
45///     value: String,
46/// }
47///
48/// impl ExampleTool {
49///     async fn execute(&self, _session: Option<SessionContext>) -> McpResult<String> {
50///         Ok(format!("Got: {}", self.value))
51///     }
52/// }
53///
54/// #[tokio::main]
55/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
56///     let server = LambdaMcpServerBuilder::new()
57///         .name("my-lambda-server")
58///         .version("1.0.0")
59///         .tool(ExampleTool::default())
60///         .storage(Arc::new(InMemorySessionStorage::new()))
61///         .cors_allow_all_origins()
62///         .build()
63///         .await?;
64///
65///     // Use with Lambda runtime...
66///     Ok(())
67/// }
68/// ```
69pub struct LambdaMcpServerBuilder {
70    /// Server implementation info
71    name: String,
72    version: String,
73    title: Option<String>,
74
75    /// Server capabilities
76    capabilities: ServerCapabilities,
77
78    /// Tools registered with the server
79    tools: HashMap<String, Arc<dyn McpTool>>,
80
81    /// Static resources registered with the server
82    resources: HashMap<String, Arc<dyn McpResource>>,
83
84    /// Template resources registered with the server (auto-detected from URI)
85    template_resources: Vec<(
86        turul_mcp_server::uri_template::UriTemplate,
87        Arc<dyn McpResource>,
88    )>,
89
90    /// Prompts registered with the server
91    prompts: HashMap<String, Arc<dyn McpPrompt>>,
92
93    /// Elicitations registered with the server
94    elicitations: HashMap<String, Arc<dyn McpElicitation>>,
95
96    /// Sampling providers registered with the server
97    sampling: HashMap<String, Arc<dyn McpSampling>>,
98
99    /// Completion providers registered with the server
100    completions: HashMap<String, Arc<dyn McpCompletion>>,
101
102    /// Loggers registered with the server
103    loggers: HashMap<String, Arc<dyn McpLogger>>,
104
105    /// Root providers registered with the server
106    root_providers: HashMap<String, Arc<dyn McpRoot>>,
107
108    /// Notification providers registered with the server
109    notifications: HashMap<String, Arc<dyn McpNotification>>,
110
111    /// Handlers registered with the server
112    handlers: HashMap<String, Arc<dyn McpHandler>>,
113
114    /// Roots configured for the server
115    roots: Vec<turul_mcp_protocol::roots::Root>,
116
117    /// Optional instructions for clients
118    instructions: Option<String>,
119
120    /// Session configuration
121    session_timeout_minutes: Option<u64>,
122    session_cleanup_interval_seconds: Option<u64>,
123
124    /// Session storage backend (defaults to InMemory if None)
125    session_storage: Option<Arc<BoxedSessionStorage>>,
126
127    /// MCP Lifecycle enforcement configuration
128    strict_lifecycle: bool,
129
130    /// Enable SSE streaming
131    enable_sse: bool,
132    /// Server and stream configuration
133    server_config: ServerConfig,
134    stream_config: StreamConfig,
135
136    /// Middleware stack for request/response interception
137    middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
138
139    /// Custom route registry (e.g., .well-known endpoints)
140    route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
141
142    /// Optional task runtime for MCP task support
143    task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
144    /// Recovery timeout for stuck tasks (milliseconds)
145    task_recovery_timeout_ms: u64,
146
147    /// CORS configuration (if enabled)
148    #[cfg(feature = "cors")]
149    cors_config: Option<CorsConfig>,
150}
151
152impl LambdaMcpServerBuilder {
153    /// Create a new Lambda MCP server builder
154    pub fn new() -> Self {
155        // Initialize with default capabilities (same as McpServer)
156        // Capabilities will be set truthfully in build() based on registered components
157        let capabilities = ServerCapabilities::default();
158
159        // Initialize handlers with defaults (same as McpServerBuilder)
160        let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
161        handlers.insert("ping".to_string(), Arc::new(PingHandler));
162        handlers.insert(
163            "completion/complete".to_string(),
164            Arc::new(CompletionHandler),
165        );
166        handlers.insert(
167            "resources/list".to_string(),
168            Arc::new(ResourcesHandler::new()),
169        );
170        handlers.insert(
171            "prompts/list".to_string(),
172            Arc::new(PromptsListHandler::new()),
173        );
174        handlers.insert(
175            "prompts/get".to_string(),
176            Arc::new(PromptsGetHandler::new()),
177        );
178        handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
179        handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
180        handlers.insert(
181            "sampling/createMessage".to_string(),
182            Arc::new(SamplingHandler),
183        );
184        handlers.insert(
185            "resources/templates/list".to_string(),
186            Arc::new(ResourceTemplatesHandler::new()),
187        );
188        handlers.insert(
189            "elicitation/create".to_string(),
190            Arc::new(ElicitationHandler::with_mock_provider()),
191        );
192
193        // Add notification handlers
194        let notifications_handler = Arc::new(NotificationsHandler);
195        handlers.insert(
196            "notifications/message".to_string(),
197            notifications_handler.clone(),
198        );
199        handlers.insert(
200            "notifications/progress".to_string(),
201            notifications_handler.clone(),
202        );
203        // MCP 2025-11-25 spec-correct underscore form
204        handlers.insert(
205            "notifications/resources/list_changed".to_string(),
206            notifications_handler.clone(),
207        );
208        handlers.insert(
209            "notifications/resources/updated".to_string(),
210            notifications_handler.clone(),
211        );
212        handlers.insert(
213            "notifications/tools/list_changed".to_string(),
214            notifications_handler.clone(),
215        );
216        handlers.insert(
217            "notifications/prompts/list_changed".to_string(),
218            notifications_handler.clone(),
219        );
220        handlers.insert(
221            "notifications/roots/list_changed".to_string(),
222            notifications_handler.clone(),
223        );
224        // Legacy compat: accept camelCase from older clients
225        handlers.insert(
226            "notifications/resources/listChanged".to_string(),
227            notifications_handler.clone(),
228        );
229        handlers.insert(
230            "notifications/tools/listChanged".to_string(),
231            notifications_handler.clone(),
232        );
233        handlers.insert(
234            "notifications/prompts/listChanged".to_string(),
235            notifications_handler.clone(),
236        );
237        handlers.insert(
238            "notifications/roots/listChanged".to_string(),
239            notifications_handler,
240        );
241
242        Self {
243            name: "turul-mcp-aws-lambda".to_string(),
244            version: env!("CARGO_PKG_VERSION").to_string(),
245            title: None,
246            capabilities,
247            tools: HashMap::new(),
248            resources: HashMap::new(),
249            template_resources: Vec::new(),
250            prompts: HashMap::new(),
251            elicitations: HashMap::new(),
252            sampling: HashMap::new(),
253            completions: HashMap::new(),
254            loggers: HashMap::new(),
255            root_providers: HashMap::new(),
256            notifications: HashMap::new(),
257            handlers,
258            roots: Vec::new(),
259            instructions: None,
260            session_timeout_minutes: None,
261            session_cleanup_interval_seconds: None,
262            session_storage: None,
263            strict_lifecycle: false,
264            enable_sse: cfg!(feature = "sse"),
265            server_config: ServerConfig::default(),
266            stream_config: StreamConfig::default(),
267            middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
268            route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
269            task_runtime: None,
270            task_recovery_timeout_ms: 300_000, // 5 minutes
271            #[cfg(feature = "cors")]
272            cors_config: None,
273        }
274    }
275
276    /// Set the server name
277    pub fn name(mut self, name: impl Into<String>) -> Self {
278        self.name = name.into();
279        self
280    }
281
282    /// Set the server version
283    pub fn version(mut self, version: impl Into<String>) -> Self {
284        self.version = version.into();
285        self
286    }
287
288    /// Set the server title
289    pub fn title(mut self, title: impl Into<String>) -> Self {
290        self.title = Some(title.into());
291        self
292    }
293
294    /// Set optional instructions for clients
295    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
296        self.instructions = Some(instructions.into());
297        self
298    }
299
300    // =============================================================================
301    // PROVIDER REGISTRATION METHODS (same as McpServerBuilder)
302    // =============================================================================
303
304    /// Register a tool with the server
305    ///
306    /// Tools can be created using any of the framework's 4 creation levels:
307    /// - Function macros: `#[mcp_tool]`
308    /// - Derive macros: `#[derive(McpTool)]`
309    /// - Builder pattern: `ToolBuilder::new(...).build()`
310    /// - Manual implementation: Custom struct implementing `McpTool`
311    pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
312        let name = tool.name().to_string();
313        self.tools.insert(name, Arc::new(tool));
314        self
315    }
316
317    /// Register a function tool created with `#[mcp_tool]` macro
318    pub fn tool_fn<F, T>(self, func: F) -> Self
319    where
320        F: Fn() -> T,
321        T: McpTool + 'static,
322    {
323        self.tool(func())
324    }
325
326    /// Register multiple tools
327    pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
328        for tool in tools {
329            self = self.tool(tool);
330        }
331        self
332    }
333
334    /// Register a resource with the server
335    ///
336    /// Automatically detects template resources (URIs containing `{variables}`)
337    /// and routes them to the template resource list. Template resources appear
338    /// in `resources/templates/list`, not `resources/list`.
339    pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
340        let uri = resource.uri().to_string();
341
342        if uri.contains('{') && uri.contains('}') {
343            // Template resource — parse URI as UriTemplate
344            match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
345                Ok(template) => {
346                    self.template_resources.push((template, Arc::new(resource)));
347                }
348                Err(e) => {
349                    tracing::warn!(
350                        "Failed to parse template resource URI '{}': {}. Registering as static.",
351                        uri,
352                        e
353                    );
354                    self.resources.insert(uri, Arc::new(resource));
355                }
356            }
357        } else {
358            // Static resource
359            self.resources.insert(uri, Arc::new(resource));
360        }
361        self
362    }
363
364    /// Register multiple resources
365    pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
366        mut self,
367        resources: I,
368    ) -> Self {
369        for resource in resources {
370            self = self.resource(resource);
371        }
372        self
373    }
374
375    /// Register a prompt with the server
376    pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
377        let name = prompt.name().to_string();
378        self.prompts.insert(name, Arc::new(prompt));
379        self
380    }
381
382    /// Register multiple prompts
383    pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
384        mut self,
385        prompts: I,
386    ) -> Self {
387        for prompt in prompts {
388            self = self.prompt(prompt);
389        }
390        self
391    }
392
393    /// Register an elicitation provider with the server
394    pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
395        let key = format!("elicitation_{}", self.elicitations.len());
396        self.elicitations.insert(key, Arc::new(elicitation));
397        self
398    }
399
400    /// Register multiple elicitation providers
401    pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
402        mut self,
403        elicitations: I,
404    ) -> Self {
405        for elicitation in elicitations {
406            self = self.elicitation(elicitation);
407        }
408        self
409    }
410
411    /// Register a sampling provider with the server
412    pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
413        let key = format!("sampling_{}", self.sampling.len());
414        self.sampling.insert(key, Arc::new(sampling));
415        self
416    }
417
418    /// Register multiple sampling providers
419    pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
420        mut self,
421        sampling: I,
422    ) -> Self {
423        for s in sampling {
424            self = self.sampling_provider(s);
425        }
426        self
427    }
428
429    /// Register a completion provider with the server
430    pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
431        let key = format!("completion_{}", self.completions.len());
432        self.completions.insert(key, Arc::new(completion));
433        self
434    }
435
436    /// Register multiple completion providers
437    pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
438        mut self,
439        completions: I,
440    ) -> Self {
441        for completion in completions {
442            self = self.completion_provider(completion);
443        }
444        self
445    }
446
447    /// Register a logger with the server
448    pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
449        let key = format!("logger_{}", self.loggers.len());
450        self.loggers.insert(key, Arc::new(logger));
451        self
452    }
453
454    /// Register multiple loggers
455    pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
456        mut self,
457        loggers: I,
458    ) -> Self {
459        for logger in loggers {
460            self = self.logger(logger);
461        }
462        self
463    }
464
465    /// Register a root provider with the server
466    pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
467        let key = format!("root_{}", self.root_providers.len());
468        self.root_providers.insert(key, Arc::new(root));
469        self
470    }
471
472    /// Register multiple root providers
473    pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
474        mut self,
475        roots: I,
476    ) -> Self {
477        for root in roots {
478            self = self.root_provider(root);
479        }
480        self
481    }
482
483    /// Register a notification provider with the server
484    pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
485        let key = format!("notification_{}", self.notifications.len());
486        self.notifications.insert(key, Arc::new(notification));
487        self
488    }
489
490    /// Register multiple notification providers
491    pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
492        mut self,
493        notifications: I,
494    ) -> Self {
495        for notification in notifications {
496            self = self.notification_provider(notification);
497        }
498        self
499    }
500
501    // =============================================================================
502    // ZERO-CONFIGURATION CONVENIENCE METHODS (same as McpServerBuilder)
503    // =============================================================================
504
505    /// Register a sampler - convenient alias for sampling_provider
506    pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
507        self.sampling_provider(sampling)
508    }
509
510    /// Register a completer - convenient alias for completion_provider
511    pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
512        self.completion_provider(completion)
513    }
514
515    /// Register a notification by type - type determines method automatically
516    pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
517        let notification = N::default();
518        self.notification_provider(notification)
519    }
520
521    /// Register a handler with the server
522    pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
523        let handler_arc = Arc::new(handler);
524        for method in handler_arc.supported_methods() {
525            self.handlers.insert(method, handler_arc.clone());
526        }
527        self
528    }
529
530    /// Register multiple handlers
531    pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
532        mut self,
533        handlers: I,
534    ) -> Self {
535        for handler in handlers {
536            self = self.handler(handler);
537        }
538        self
539    }
540
541    /// Add a single root directory
542    pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
543        self.roots.push(root);
544        self
545    }
546
547    // =============================================================================
548    // CAPABILITY CONFIGURATION METHODS (same as McpServerBuilder)
549    // =============================================================================
550
551    /// Add completion support
552    pub fn with_completion(mut self) -> Self {
553        use turul_mcp_protocol::initialize::CompletionsCapabilities;
554        self.capabilities.completions = Some(CompletionsCapabilities {
555            enabled: Some(true),
556        });
557        self.handler(CompletionHandler)
558    }
559
560    /// Add prompts support
561    pub fn with_prompts(mut self) -> Self {
562        use turul_mcp_protocol::initialize::PromptsCapabilities;
563        self.capabilities.prompts = Some(PromptsCapabilities {
564            list_changed: Some(false),
565        });
566
567        // Prompts handlers are automatically registered when prompts are added via .prompt()
568        // This method now just enables the capability
569        self
570    }
571
572    /// Add resources support
573    pub fn with_resources(mut self) -> Self {
574        use turul_mcp_protocol::initialize::ResourcesCapabilities;
575        self.capabilities.resources = Some(ResourcesCapabilities {
576            subscribe: Some(false),
577            list_changed: Some(false),
578        });
579
580        // Create ResourcesHandler (resources/list) — static resources only
581        let mut list_handler = ResourcesHandler::new();
582        for resource in self.resources.values() {
583            list_handler = list_handler.add_resource_arc(resource.clone());
584        }
585        self = self.handler(list_handler);
586
587        // Create ResourceTemplatesHandler (resources/templates/list) — template resources
588        if !self.template_resources.is_empty() {
589            let templates_handler =
590                ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
591            self = self.handler(templates_handler);
592        }
593
594        // Create ResourcesReadHandler (resources/read) — both static and template resources
595        let mut read_handler = ResourcesReadHandler::new().without_security();
596        for resource in self.resources.values() {
597            read_handler = read_handler.add_resource_arc(resource.clone());
598        }
599        for (template, resource) in &self.template_resources {
600            read_handler =
601                read_handler.add_template_resource_arc(template.clone(), resource.clone());
602        }
603        self.handler(read_handler)
604    }
605
606    /// Add logging support
607    pub fn with_logging(mut self) -> Self {
608        use turul_mcp_protocol::initialize::LoggingCapabilities;
609        self.capabilities.logging = Some(LoggingCapabilities::default());
610        self.handler(LoggingHandler)
611    }
612
613    /// Add roots support
614    pub fn with_roots(self) -> Self {
615        self.handler(RootsHandler::new())
616    }
617
618    /// Add sampling support
619    pub fn with_sampling(self) -> Self {
620        self.handler(SamplingHandler)
621    }
622
623    /// Add elicitation support with default mock provider
624    pub fn with_elicitation(self) -> Self {
625        // Elicitation is a client-side capability per MCP 2025-11-25
626        // Server just registers the handler, no capability advertisement needed
627        self.handler(ElicitationHandler::with_mock_provider())
628    }
629
630    /// Add elicitation support with custom provider
631    pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
632        // Elicitation is a client-side capability per MCP 2025-11-25
633        self.handler(ElicitationHandler::new(Arc::new(provider)))
634    }
635
636    /// Add notifications support
637    pub fn with_notifications(self) -> Self {
638        self.handler(NotificationsHandler)
639    }
640
641    // =============================================================================
642    // TASK SUPPORT METHODS
643    // =============================================================================
644
645    /// Configure task storage to enable MCP task support for long-running operations.
646    ///
647    /// When task storage is configured, the server will:
648    /// - Advertise `tasks` capabilities in the initialize response
649    /// - Register handlers for `tasks/get`, `tasks/list`, `tasks/cancel`, `tasks/result`
650    /// - Wire task-augmented `tools/call` for `CreateTaskResult` returns
651    /// - Recover stuck tasks on cold start
652    ///
653    /// **Lambda note**: Use a durable backend (DynamoDB recommended) since Lambda
654    /// invocations are stateless. `InMemoryTaskStorage` will lose state between invocations.
655    pub fn with_task_storage(
656        mut self,
657        storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
658    ) -> Self {
659        let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
660            .with_recovery_timeout(self.task_recovery_timeout_ms);
661        self.task_runtime = Some(Arc::new(runtime));
662        self
663    }
664
665    /// Configure task support with a pre-built `TaskRuntime`.
666    ///
667    /// Use this when you need fine-grained control over the task runtime configuration.
668    pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
669        self.task_runtime = Some(runtime);
670        self
671    }
672
673    /// Set the recovery timeout for stuck tasks (in milliseconds).
674    ///
675    /// On Lambda cold start, tasks in non-terminal states older than this timeout
676    /// will be marked as `Failed`. Default: 300,000 ms (5 minutes).
677    pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
678        self.task_recovery_timeout_ms = timeout_ms;
679        self
680    }
681
682    // =============================================================================
683    // SESSION AND CONFIGURATION METHODS
684    // =============================================================================
685
686    /// Configure session timeout (in minutes, default: 30)
687    pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
688        self.session_timeout_minutes = Some(minutes);
689        self
690    }
691
692    /// Configure session cleanup interval (in seconds, default: 60)
693    pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
694        self.session_cleanup_interval_seconds = Some(seconds);
695        self
696    }
697
698    /// Enable strict MCP lifecycle enforcement
699    pub fn strict_lifecycle(mut self, strict: bool) -> Self {
700        self.strict_lifecycle = strict;
701        self
702    }
703
704    /// Enable strict MCP lifecycle enforcement (convenience method)
705    pub fn with_strict_lifecycle(self) -> Self {
706        self.strict_lifecycle(true)
707    }
708
709    /// Enable or disable SSE streaming support
710    pub fn sse(mut self, enable: bool) -> Self {
711        self.enable_sse = enable;
712
713        // Update SSE endpoints in ServerConfig based on enable flag
714        if enable {
715            self.server_config.enable_get_sse = true;
716            self.server_config.enable_post_sse = true;
717        } else {
718            // When SSE is disabled, also disable SSE endpoints in ServerConfig
719            // This prevents GET /mcp from hanging by returning 405 instead
720            self.server_config.enable_get_sse = false;
721            self.server_config.enable_post_sse = false;
722        }
723
724        self
725    }
726
727    /// Configure sessions with recommended defaults for long-running sessions
728    pub fn with_long_sessions(mut self) -> Self {
729        self.session_timeout_minutes = Some(120); // 2 hours
730        self.session_cleanup_interval_seconds = Some(300); // 5 minutes
731        self
732    }
733
734    /// Configure sessions with recommended defaults for short-lived sessions
735    pub fn with_short_sessions(mut self) -> Self {
736        self.session_timeout_minutes = Some(5); // 5 minutes
737        self.session_cleanup_interval_seconds = Some(30); // 30 seconds
738        self
739    }
740
741    /// Set the session storage backend
742    ///
743    /// Supports all framework storage backends:
744    /// - `InMemorySessionStorage` - For development and testing
745    /// - `SqliteSessionStorage` - For single-instance persistence
746    /// - `PostgreSqlSessionStorage` - For multi-instance deployments
747    /// - `DynamoDbSessionStorage` - For serverless AWS deployments
748    pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
749        self.session_storage = Some(storage);
750        self
751    }
752
753    /// Create DynamoDB storage from environment variables
754    ///
755    /// Uses these environment variables:
756    /// - `SESSION_TABLE_NAME` or `MCP_SESSION_TABLE` - DynamoDB table name
757    /// - `AWS_REGION` - AWS region
758    /// - AWS credentials from standard AWS credential chain
759    #[cfg(feature = "dynamodb")]
760    pub async fn dynamodb_storage(self) -> Result<Self> {
761        use turul_mcp_session_storage::DynamoDbSessionStorage;
762
763        let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
764            LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
765        })?;
766
767        Ok(self.storage(Arc::new(storage)))
768    }
769
770    /// Register middleware for request/response interception
771    ///
772    /// Middleware can inspect and modify requests before they reach handlers,
773    /// inject data into sessions, and transform responses. Multiple middleware
774    /// can be registered and will execute in FIFO order for before_dispatch
775    /// and LIFO order for after_dispatch.
776    ///
777    /// # Example
778    ///
779    /// ```rust,no_run
780    /// use std::sync::Arc;
781    /// use turul_mcp_aws_lambda::LambdaMcpServerBuilder;
782    /// use turul_http_mcp_server::middleware::McpMiddleware;
783    /// # use turul_mcp_session_storage::SessionView;
784    /// # use turul_http_mcp_server::middleware::{RequestContext, SessionInjection, MiddlewareError};
785    /// # use async_trait::async_trait;
786    /// # struct AuthMiddleware;
787    /// # #[async_trait]
788    /// # impl McpMiddleware for AuthMiddleware {
789    /// #     async fn before_dispatch(&self, _: &mut RequestContext<'_>, _: Option<&dyn SessionView>, _: &mut SessionInjection) -> Result<(), MiddlewareError> { Ok(()) }
790    /// # }
791    /// # struct RateLimitMiddleware;
792    /// # #[async_trait]
793    /// # impl McpMiddleware for RateLimitMiddleware {
794    /// #     async fn before_dispatch(&self, _: &mut RequestContext<'_>, _: Option<&dyn SessionView>, _: &mut SessionInjection) -> Result<(), MiddlewareError> { Ok(()) }
795    /// # }
796    ///
797    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
798    /// let builder = LambdaMcpServerBuilder::new()
799    ///     .name("my-server")
800    ///     .middleware(Arc::new(AuthMiddleware))
801    ///     .middleware(Arc::new(RateLimitMiddleware));
802    /// # Ok(())
803    /// # }
804    /// ```
805    pub fn middleware(
806        mut self,
807        middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
808    ) -> Self {
809        self.middleware_stack.push(middleware);
810        self
811    }
812
813    /// Register a custom HTTP route (e.g., `.well-known` endpoints)
814    pub fn route(
815        mut self,
816        path: &str,
817        handler: Arc<dyn turul_http_mcp_server::RouteHandler>,
818    ) -> Self {
819        Arc::get_mut(&mut self.route_registry)
820            .expect("route_registry must not be shared during build")
821            .add_route(path, handler);
822        self
823    }
824
825    /// Configure server settings
826    pub fn server_config(mut self, config: ServerConfig) -> Self {
827        self.server_config = config;
828        self
829    }
830
831    /// Configure streaming/SSE settings
832    pub fn stream_config(mut self, config: StreamConfig) -> Self {
833        self.stream_config = config;
834        self
835    }
836
837    // CORS Configuration Methods
838
839    /// Set custom CORS configuration
840    #[cfg(feature = "cors")]
841    pub fn cors(mut self, config: CorsConfig) -> Self {
842        self.cors_config = Some(config);
843        self
844    }
845
846    /// Allow all origins for CORS (development only)
847    #[cfg(feature = "cors")]
848    pub fn cors_allow_all_origins(mut self) -> Self {
849        self.cors_config = Some(CorsConfig::allow_all());
850        self
851    }
852
853    /// Set specific allowed origins for CORS
854    #[cfg(feature = "cors")]
855    pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
856        self.cors_config = Some(CorsConfig::for_origins(origins));
857        self
858    }
859
860    /// Configure CORS from environment variables
861    ///
862    /// Uses these environment variables:
863    /// - `MCP_CORS_ORIGINS` - Comma-separated list of allowed origins
864    /// - `MCP_CORS_CREDENTIALS` - Whether to allow credentials (true/false)
865    /// - `MCP_CORS_MAX_AGE` - Preflight cache max age in seconds
866    #[cfg(feature = "cors")]
867    pub fn cors_from_env(mut self) -> Self {
868        self.cors_config = Some(CorsConfig::from_env());
869        self
870    }
871
872    /// Disable CORS (headers will not be added)
873    #[cfg(feature = "cors")]
874    pub fn cors_disabled(self) -> Self {
875        // Don't set any CORS config - builder will not add headers
876        self
877    }
878
879    // Convenience Methods
880
881    /// Create with DynamoDB storage and environment-based CORS
882    ///
883    /// This is the recommended configuration for production Lambda deployments.
884    #[cfg(all(feature = "dynamodb", feature = "cors"))]
885    pub async fn production_config(self) -> Result<Self> {
886        Ok(self.dynamodb_storage().await?.cors_from_env())
887    }
888
889    /// Create with in-memory storage and permissive CORS
890    ///
891    /// This is the recommended configuration for development and testing.
892    #[cfg(feature = "cors")]
893    pub fn development_config(self) -> Self {
894        use turul_mcp_session_storage::InMemorySessionStorage;
895
896        self.storage(Arc::new(InMemorySessionStorage::new()))
897            .cors_allow_all_origins()
898    }
899
900    /// Build the Lambda MCP server
901    ///
902    /// Returns a server that can create handlers when needed.
903    pub async fn build(self) -> Result<LambdaMcpServer> {
904        use turul_mcp_session_storage::InMemorySessionStorage;
905
906        // Validate configuration (same as MCP server)
907        if self.name.is_empty() {
908            return Err(crate::error::LambdaError::Configuration(
909                "Server name cannot be empty".to_string(),
910            ));
911        }
912        if self.version.is_empty() {
913            return Err(crate::error::LambdaError::Configuration(
914                "Server version cannot be empty".to_string(),
915            ));
916        }
917
918        // Note: SSE behavior depends on which handler method is used:
919        // - handle(): Works with run(), but SSE responses may not stream properly
920        // - handle_streaming(): Works with run_with_streaming_response() for real SSE streaming
921
922        // Create session storage (use in-memory if none provided)
923        let session_storage = self
924            .session_storage
925            .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
926
927        // Create implementation info
928        let implementation = if let Some(title) = self.title {
929            Implementation::new(&self.name, &self.version).with_title(title)
930        } else {
931            Implementation::new(&self.name, &self.version)
932        };
933
934        // Auto-detect and configure server capabilities based on registered components (same as McpServer)
935        let mut capabilities = self.capabilities.clone();
936        let has_tools = !self.tools.is_empty();
937        let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
938        let has_prompts = !self.prompts.is_empty();
939        let has_elicitations = !self.elicitations.is_empty();
940        let has_completions = !self.completions.is_empty();
941        let has_logging = !self.loggers.is_empty();
942        tracing::debug!("🔧 Has logging configured: {}", has_logging);
943
944        // Tools capabilities - truthful reporting (only set if tools are registered)
945        if has_tools {
946            capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
947                list_changed: Some(false), // Static framework: no dynamic change sources
948            });
949        }
950
951        // Resources capabilities - truthful reporting (only set if resources are registered)
952        if has_resources {
953            capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
954                subscribe: Some(false),    // TODO: Implement resource subscriptions
955                list_changed: Some(false), // Static framework: no dynamic change sources
956            });
957        }
958
959        // Prompts capabilities - truthful reporting (only set if prompts are registered)
960        if has_prompts {
961            capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
962                list_changed: Some(false), // Static framework: no dynamic change sources
963            });
964        }
965
966        // Elicitation is a client-side capability per MCP 2025-11-25
967        // Server does NOT advertise elicitation capabilities
968        let _ = has_elicitations; // Acknowledge the variable without using it
969
970        // Completion capabilities - truthful reporting (only set if completions are registered)
971        if has_completions {
972            capabilities.completions =
973                Some(turul_mcp_protocol::initialize::CompletionsCapabilities {
974                    enabled: Some(true),
975                });
976        }
977
978        // Logging capabilities - always enabled for debugging/monitoring (same as McpServer)
979        // Always enable logging for debugging/monitoring
980        capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities {
981            enabled: Some(true),
982            levels: Some(vec![
983                "debug".to_string(),
984                "info".to_string(),
985                "warning".to_string(),
986                "error".to_string(),
987            ]),
988        });
989
990        // Tasks capabilities — auto-configure when task runtime is set
991        if self.task_runtime.is_some() {
992            use turul_mcp_protocol::initialize::*;
993            capabilities.tasks = Some(TasksCapabilities {
994                list: Some(TasksListCapabilities::default()),
995                cancel: Some(TasksCancelCapabilities::default()),
996                requests: Some(TasksRequestCapabilities {
997                    tools: Some(TasksToolCapabilities {
998                        call: Some(TasksToolCallCapabilities::default()),
999                        extra: Default::default(),
1000                    }),
1001                    extra: Default::default(),
1002                }),
1003                extra: Default::default(),
1004            });
1005        }
1006
1007        // Add RootsHandler if roots were configured (same pattern as MCP server)
1008        let mut handlers = self.handlers;
1009        if !self.roots.is_empty() {
1010            let mut roots_handler = RootsHandler::new();
1011            for root in &self.roots {
1012                roots_handler = roots_handler.add_root(root.clone());
1013            }
1014            handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1015        }
1016
1017        // Add task handlers if task runtime is configured
1018        if let Some(ref runtime) = self.task_runtime {
1019            use turul_mcp_server::{
1020                TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1021            };
1022            handlers.insert(
1023                "tasks/get".to_string(),
1024                Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1025            );
1026            handlers.insert(
1027                "tasks/list".to_string(),
1028                Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1029            );
1030            handlers.insert(
1031                "tasks/cancel".to_string(),
1032                Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1033            );
1034            handlers.insert(
1035                "tasks/result".to_string(),
1036                Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1037            );
1038        }
1039
1040        // Auto-populate resource handlers (same as McpServer build() auto-setup)
1041        if has_resources {
1042            // Populate resources/list handler with static resources
1043            let mut list_handler = ResourcesHandler::new();
1044            for resource in self.resources.values() {
1045                list_handler = list_handler.add_resource_arc(resource.clone());
1046            }
1047            handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1048
1049            // Populate resources/templates/list handler with template resources
1050            if !self.template_resources.is_empty() {
1051                let templates_handler =
1052                    ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1053                handlers.insert(
1054                    "resources/templates/list".to_string(),
1055                    Arc::new(templates_handler),
1056                );
1057            }
1058
1059            // Create resources/read handler with both static and template resources
1060            let mut read_handler = ResourcesReadHandler::new().without_security();
1061            for resource in self.resources.values() {
1062                read_handler = read_handler.add_resource_arc(resource.clone());
1063            }
1064            for (template, resource) in &self.template_resources {
1065                read_handler =
1066                    read_handler.add_template_resource_arc(template.clone(), resource.clone());
1067            }
1068            handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1069        }
1070
1071        // Create the Lambda server (stores all configuration like MCP server does)
1072        Ok(LambdaMcpServer::new(
1073            implementation,
1074            capabilities,
1075            self.tools,
1076            self.resources,
1077            self.prompts,
1078            self.elicitations,
1079            self.sampling,
1080            self.completions,
1081            self.loggers,
1082            self.root_providers,
1083            self.notifications,
1084            handlers,
1085            self.roots,
1086            self.instructions,
1087            session_storage,
1088            self.strict_lifecycle,
1089            self.server_config,
1090            self.enable_sse,
1091            self.stream_config,
1092            #[cfg(feature = "cors")]
1093            self.cors_config,
1094            self.middleware_stack,
1095            self.route_registry,
1096            self.task_runtime,
1097        ))
1098    }
1099}
1100
1101impl Default for LambdaMcpServerBuilder {
1102    fn default() -> Self {
1103        Self::new()
1104    }
1105}
1106
1107// Extension trait for cleaner chaining
1108pub trait LambdaMcpServerBuilderExt {
1109    /// Add multiple tools at once
1110    fn tools<I, T>(self, tools: I) -> Self
1111    where
1112        I: IntoIterator<Item = T>,
1113        T: McpTool + 'static;
1114}
1115
1116impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1117    fn tools<I, T>(mut self, tools: I) -> Self
1118    where
1119        I: IntoIterator<Item = T>,
1120        T: McpTool + 'static,
1121    {
1122        for tool in tools {
1123            self = self.tool(tool);
1124        }
1125        self
1126    }
1127}
1128
1129/// Create a Lambda MCP server with minimal configuration
1130///
1131/// This is a convenience function for simple use cases where you just
1132/// want to register some tools and get a working handler.
1133pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1134where
1135    I: IntoIterator<Item = T>,
1136    T: McpTool + 'static,
1137{
1138    let mut builder = LambdaMcpServerBuilder::new();
1139
1140    for tool in tools {
1141        builder = builder.tool(tool);
1142    }
1143
1144    #[cfg(feature = "cors")]
1145    {
1146        builder = builder.cors_allow_all_origins();
1147    }
1148
1149    builder.sse(false).build().await
1150}
1151
1152/// Create a Lambda MCP server configured for production
1153///
1154/// Uses DynamoDB for session storage and environment-based CORS configuration.
1155#[cfg(all(feature = "dynamodb", feature = "cors"))]
1156pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1157where
1158    I: IntoIterator<Item = T>,
1159    T: McpTool + 'static,
1160{
1161    let mut builder = LambdaMcpServerBuilder::new();
1162
1163    for tool in tools {
1164        builder = builder.tool(tool);
1165    }
1166
1167    builder.production_config().await?.build().await
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173    use turul_mcp_builders::prelude::*;
1174    use turul_mcp_session_storage::InMemorySessionStorage; // HasBaseMetadata, HasDescription, etc.
1175
1176    // Mock tool for testing
1177    #[derive(Clone, Default)]
1178    struct TestTool;
1179
1180    impl HasBaseMetadata for TestTool {
1181        fn name(&self) -> &str {
1182            "test_tool"
1183        }
1184    }
1185
1186    impl HasDescription for TestTool {
1187        fn description(&self) -> Option<&str> {
1188            Some("Test tool")
1189        }
1190    }
1191
1192    impl HasInputSchema for TestTool {
1193        fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1194            use turul_mcp_protocol::ToolSchema;
1195            static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1196            SCHEMA.get_or_init(ToolSchema::object)
1197        }
1198    }
1199
1200    impl HasOutputSchema for TestTool {
1201        fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1202            None
1203        }
1204    }
1205
1206    impl HasAnnotations for TestTool {
1207        fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1208            None
1209        }
1210    }
1211
1212    impl HasToolMeta for TestTool {
1213        fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1214            None
1215        }
1216    }
1217
1218    impl HasIcons for TestTool {}
1219    impl HasExecution for TestTool {}
1220
1221    #[async_trait::async_trait]
1222    impl McpTool for TestTool {
1223        async fn call(
1224            &self,
1225            _args: serde_json::Value,
1226            _session: Option<turul_mcp_server::SessionContext>,
1227        ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1228            use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1229            Ok(CallToolResult::success(vec![ToolResult::text(
1230                "test result",
1231            )]))
1232        }
1233    }
1234
1235    #[tokio::test]
1236    async fn test_builder_basic() {
1237        let server = LambdaMcpServerBuilder::new()
1238            .name("test-server")
1239            .version("1.0.0")
1240            .tool(TestTool)
1241            .storage(Arc::new(InMemorySessionStorage::new()))
1242            .sse(false) // Disable SSE for tests since streaming feature not enabled
1243            .build()
1244            .await
1245            .unwrap();
1246
1247        // Create handler from server and verify it has stream_manager
1248        let handler = server.handler().await.unwrap();
1249        // Verify handler has stream_manager (critical invariant)
1250        assert!(
1251            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1252            "Stream manager must be initialized"
1253        );
1254    }
1255
1256    #[tokio::test]
1257    async fn test_simple_lambda_server() {
1258        let tools = vec![TestTool];
1259        let server = simple_lambda_server(tools).await.unwrap();
1260
1261        // Create handler and verify it was created with default configuration
1262        let handler = server.handler().await.unwrap();
1263        // Verify handler has stream_manager
1264        // Verify handler has stream_manager (critical invariant)
1265        assert!(
1266            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1267            "Stream manager must be initialized"
1268        );
1269    }
1270
1271    #[tokio::test]
1272    async fn test_builder_extension_trait() {
1273        let tools = vec![TestTool, TestTool];
1274
1275        let server = LambdaMcpServerBuilder::new()
1276            .tools(tools)
1277            .storage(Arc::new(InMemorySessionStorage::new()))
1278            .sse(false) // Disable SSE for tests since streaming feature not enabled
1279            .build()
1280            .await
1281            .unwrap();
1282
1283        let handler = server.handler().await.unwrap();
1284        // Verify handler has stream_manager
1285        // Verify handler has stream_manager (critical invariant)
1286        assert!(
1287            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1288            "Stream manager must be initialized"
1289        );
1290    }
1291
1292    #[cfg(feature = "cors")]
1293    #[tokio::test]
1294    async fn test_cors_configuration() {
1295        let server = LambdaMcpServerBuilder::new()
1296            .cors_allow_all_origins()
1297            .storage(Arc::new(InMemorySessionStorage::new()))
1298            .sse(false) // Disable SSE for tests since streaming feature not enabled
1299            .build()
1300            .await
1301            .unwrap();
1302
1303        let handler = server.handler().await.unwrap();
1304        // Verify handler has stream_manager
1305        // Verify handler has stream_manager (critical invariant)
1306        assert!(
1307            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1308            "Stream manager must be initialized"
1309        );
1310    }
1311
1312    #[tokio::test]
1313    async fn test_sse_toggle_functionality() {
1314        // Test that SSE can be toggled on/off/on correctly
1315        let mut builder =
1316            LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1317
1318        // Initially enable SSE
1319        builder = builder.sse(true);
1320        assert!(builder.enable_sse, "SSE should be enabled");
1321        assert!(
1322            builder.server_config.enable_get_sse,
1323            "GET SSE endpoint should be enabled"
1324        );
1325        assert!(
1326            builder.server_config.enable_post_sse,
1327            "POST SSE endpoint should be enabled"
1328        );
1329
1330        // Disable SSE
1331        builder = builder.sse(false);
1332        assert!(!builder.enable_sse, "SSE should be disabled");
1333        assert!(
1334            !builder.server_config.enable_get_sse,
1335            "GET SSE endpoint should be disabled"
1336        );
1337        assert!(
1338            !builder.server_config.enable_post_sse,
1339            "POST SSE endpoint should be disabled"
1340        );
1341
1342        // Re-enable SSE (this was broken before the fix)
1343        builder = builder.sse(true);
1344        assert!(builder.enable_sse, "SSE should be re-enabled");
1345        assert!(
1346            builder.server_config.enable_get_sse,
1347            "GET SSE endpoint should be re-enabled"
1348        );
1349        assert!(
1350            builder.server_config.enable_post_sse,
1351            "POST SSE endpoint should be re-enabled"
1352        );
1353
1354        // Verify the server can be built with SSE enabled
1355        let server = builder.build().await.unwrap();
1356        let handler = server.handler().await.unwrap();
1357        assert!(
1358            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1359            "Stream manager must be initialized"
1360        );
1361    }
1362
1363    // =========================================================================
1364    // Task support tests
1365    // =========================================================================
1366
1367    #[tokio::test]
1368    async fn test_builder_without_tasks_no_capability() {
1369        let server = LambdaMcpServerBuilder::new()
1370            .name("no-tasks")
1371            .tool(TestTool)
1372            .storage(Arc::new(InMemorySessionStorage::new()))
1373            .sse(false)
1374            .build()
1375            .await
1376            .unwrap();
1377
1378        assert!(
1379            server.capabilities().tasks.is_none(),
1380            "Tasks capability should not be advertised without task storage"
1381        );
1382    }
1383
1384    #[tokio::test]
1385    async fn test_builder_with_task_storage_advertises_capability() {
1386        use turul_mcp_server::task_storage::InMemoryTaskStorage;
1387
1388        let server = LambdaMcpServerBuilder::new()
1389            .name("with-tasks")
1390            .tool(TestTool)
1391            .storage(Arc::new(InMemorySessionStorage::new()))
1392            .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1393            .sse(false)
1394            .build()
1395            .await
1396            .unwrap();
1397
1398        let tasks_cap = server
1399            .capabilities()
1400            .tasks
1401            .as_ref()
1402            .expect("Tasks capability should be advertised");
1403        assert!(tasks_cap.list.is_some(), "list capability should be set");
1404        assert!(
1405            tasks_cap.cancel.is_some(),
1406            "cancel capability should be set"
1407        );
1408        let requests = tasks_cap
1409            .requests
1410            .as_ref()
1411            .expect("requests capability should be set");
1412        let tools = requests
1413            .tools
1414            .as_ref()
1415            .expect("tools capability should be set");
1416        assert!(tools.call.is_some(), "tools.call capability should be set");
1417    }
1418
1419    #[tokio::test]
1420    async fn test_builder_with_task_runtime_advertises_capability() {
1421        let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1422
1423        let server = LambdaMcpServerBuilder::new()
1424            .name("with-runtime")
1425            .tool(TestTool)
1426            .storage(Arc::new(InMemorySessionStorage::new()))
1427            .with_task_runtime(runtime)
1428            .sse(false)
1429            .build()
1430            .await
1431            .unwrap();
1432
1433        assert!(
1434            server.capabilities().tasks.is_some(),
1435            "Tasks capability should be advertised with task runtime"
1436        );
1437    }
1438
1439    #[tokio::test]
1440    async fn test_task_recovery_timeout_configuration() {
1441        use turul_mcp_server::task_storage::InMemoryTaskStorage;
1442
1443        let server = LambdaMcpServerBuilder::new()
1444            .name("custom-timeout")
1445            .tool(TestTool)
1446            .storage(Arc::new(InMemorySessionStorage::new()))
1447            .task_recovery_timeout_ms(60_000)
1448            .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1449            .sse(false)
1450            .build()
1451            .await
1452            .unwrap();
1453
1454        assert!(
1455            server.capabilities().tasks.is_some(),
1456            "Tasks should be enabled with custom timeout"
1457        );
1458    }
1459
1460    #[tokio::test]
1461    async fn test_backward_compatibility_no_tasks() {
1462        // Existing builder pattern still works unchanged
1463        let server = LambdaMcpServerBuilder::new()
1464            .name("backward-compat")
1465            .version("1.0.0")
1466            .tool(TestTool)
1467            .storage(Arc::new(InMemorySessionStorage::new()))
1468            .sse(false)
1469            .build()
1470            .await
1471            .unwrap();
1472
1473        let handler = server.handler().await.unwrap();
1474        assert!(
1475            handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1476            "Stream manager must be initialized"
1477        );
1478        assert!(server.capabilities().tasks.is_none());
1479    }
1480
1481    /// Slow tool that sleeps for 2 seconds — used to prove non-blocking behavior.
1482    #[derive(Clone, Default)]
1483    struct SlowTool;
1484
1485    impl HasBaseMetadata for SlowTool {
1486        fn name(&self) -> &str {
1487            "slow_tool"
1488        }
1489    }
1490
1491    impl HasDescription for SlowTool {
1492        fn description(&self) -> Option<&str> {
1493            Some("A slow tool for testing")
1494        }
1495    }
1496
1497    impl HasInputSchema for SlowTool {
1498        fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1499            use turul_mcp_protocol::ToolSchema;
1500            static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1501            SCHEMA.get_or_init(ToolSchema::object)
1502        }
1503    }
1504
1505    impl HasOutputSchema for SlowTool {
1506        fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1507            None
1508        }
1509    }
1510
1511    impl HasAnnotations for SlowTool {
1512        fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1513            None
1514        }
1515    }
1516
1517    impl HasToolMeta for SlowTool {
1518        fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1519            None
1520        }
1521    }
1522
1523    impl HasIcons for SlowTool {}
1524    impl HasExecution for SlowTool {
1525        fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1526            Some(turul_mcp_protocol::tools::ToolExecution {
1527                task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1528            })
1529        }
1530    }
1531
1532    #[async_trait::async_trait]
1533    impl McpTool for SlowTool {
1534        async fn call(
1535            &self,
1536            _args: serde_json::Value,
1537            _session: Option<turul_mcp_server::SessionContext>,
1538        ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1539            use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1540            // Sleep 2 seconds to prove the task path is non-blocking
1541            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1542            Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1543        }
1544    }
1545
1546    #[tokio::test]
1547    async fn test_nonblocking_tools_call_with_task() {
1548        use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1549        use turul_mcp_server::SessionAwareToolHandler;
1550        use turul_mcp_server::task_storage::InMemoryTaskStorage;
1551
1552        let task_storage = Arc::new(InMemoryTaskStorage::new());
1553        let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1554            task_storage,
1555        ));
1556
1557        // Build tools map
1558        let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1559        tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1560
1561        // Create session manager
1562        let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1563            Arc::new(InMemorySessionStorage::new());
1564        let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1565            session_storage,
1566            turul_mcp_protocol::ServerCapabilities::default(),
1567        ));
1568
1569        // Create tool handler with task runtime
1570        let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1571            .with_task_runtime(Arc::clone(&runtime));
1572
1573        // Build a tools/call request with task parameter
1574        let params = serde_json::json!({
1575            "name": "slow_tool",
1576            "arguments": {},
1577            "task": {}
1578        });
1579        let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1580            params
1581                .as_object()
1582                .unwrap()
1583                .iter()
1584                .map(|(k, v)| (k.clone(), v.clone()))
1585                .collect(),
1586        );
1587
1588        // Time the call
1589        let start = std::time::Instant::now();
1590        let result = tool_handler
1591            .handle("tools/call", Some(request_params), None)
1592            .await;
1593        let elapsed = start.elapsed();
1594
1595        // Should succeed with CreateTaskResult
1596        let value = result.expect("tools/call with task should succeed");
1597        assert!(
1598            value.get("task").is_some(),
1599            "Response should contain 'task' field (CreateTaskResult shape)"
1600        );
1601        let task = value.get("task").unwrap();
1602        assert!(
1603            task.get("taskId").is_some(),
1604            "Task should have taskId field"
1605        );
1606        assert_eq!(
1607            task.get("status")
1608                .and_then(|v| v.as_str())
1609                .unwrap_or_default(),
1610            "working",
1611            "Task status should be 'working'"
1612        );
1613
1614        // Non-blocking proof: should return well under the 2s tool sleep.
1615        // Threshold is 1s (not 500ms) to avoid flakes on slow CI runners —
1616        // the 2s tool sleep vs 1s threshold still proves a clear 2x gap.
1617        assert!(
1618            elapsed < std::time::Duration::from_secs(1),
1619            "tools/call with task should return immediately (took {:?}, expected < 1s)",
1620            elapsed
1621        );
1622    }
1623
1624    // =========================================================================
1625    // Resource and template resource tests
1626    // =========================================================================
1627
1628    // Mock static resource for testing
1629    #[derive(Clone)]
1630    struct StaticTestResource;
1631
1632    impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1633        fn name(&self) -> &str {
1634            "static_test"
1635        }
1636    }
1637
1638    impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1639        fn description(&self) -> Option<&str> {
1640            Some("Static test resource")
1641        }
1642    }
1643
1644    impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1645        fn uri(&self) -> &str {
1646            "file:///test.txt"
1647        }
1648    }
1649
1650    impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1651        fn mime_type(&self) -> Option<&str> {
1652            Some("text/plain")
1653        }
1654    }
1655
1656    impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1657        fn size(&self) -> Option<u64> {
1658            None
1659        }
1660    }
1661
1662    impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1663        fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1664            None
1665        }
1666    }
1667
1668    impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1669        fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1670            None
1671        }
1672    }
1673
1674    impl HasIcons for StaticTestResource {}
1675
1676    #[async_trait::async_trait]
1677    impl McpResource for StaticTestResource {
1678        async fn read(
1679            &self,
1680            _params: Option<serde_json::Value>,
1681            _session: Option<&turul_mcp_server::SessionContext>,
1682        ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1683        {
1684            use turul_mcp_protocol::resources::ResourceContent;
1685            Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1686        }
1687    }
1688
1689    // Mock template resource for testing
1690    #[derive(Clone)]
1691    struct TemplateTestResource;
1692
1693    impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1694        fn name(&self) -> &str {
1695            "template_test"
1696        }
1697    }
1698
1699    impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1700        fn description(&self) -> Option<&str> {
1701            Some("Template test resource")
1702        }
1703    }
1704
1705    impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1706        fn uri(&self) -> &str {
1707            "agent://agents/{agent_id}"
1708        }
1709    }
1710
1711    impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1712        fn mime_type(&self) -> Option<&str> {
1713            Some("application/json")
1714        }
1715    }
1716
1717    impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1718        fn size(&self) -> Option<u64> {
1719            None
1720        }
1721    }
1722
1723    impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1724        fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1725            None
1726        }
1727    }
1728
1729    impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1730        fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1731            None
1732        }
1733    }
1734
1735    impl HasIcons for TemplateTestResource {}
1736
1737    #[async_trait::async_trait]
1738    impl McpResource for TemplateTestResource {
1739        async fn read(
1740            &self,
1741            _params: Option<serde_json::Value>,
1742            _session: Option<&turul_mcp_server::SessionContext>,
1743        ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1744        {
1745            use turul_mcp_protocol::resources::ResourceContent;
1746            Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
1747        }
1748    }
1749
1750    #[test]
1751    fn test_resource_auto_detection_static() {
1752        let builder = LambdaMcpServerBuilder::new()
1753            .name("test")
1754            .resource(StaticTestResource);
1755
1756        assert_eq!(builder.resources.len(), 1);
1757        assert!(builder.resources.contains_key("file:///test.txt"));
1758        assert_eq!(builder.template_resources.len(), 0);
1759    }
1760
1761    #[test]
1762    fn test_resource_auto_detection_template() {
1763        let builder = LambdaMcpServerBuilder::new()
1764            .name("test")
1765            .resource(TemplateTestResource);
1766
1767        assert_eq!(builder.resources.len(), 0);
1768        assert_eq!(builder.template_resources.len(), 1);
1769
1770        let (template, _) = &builder.template_resources[0];
1771        assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1772    }
1773
1774    #[test]
1775    fn test_resource_auto_detection_mixed() {
1776        let builder = LambdaMcpServerBuilder::new()
1777            .name("test")
1778            .resource(StaticTestResource)
1779            .resource(TemplateTestResource);
1780
1781        assert_eq!(builder.resources.len(), 1);
1782        assert!(builder.resources.contains_key("file:///test.txt"));
1783        assert_eq!(builder.template_resources.len(), 1);
1784
1785        let (template, _) = &builder.template_resources[0];
1786        assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1787    }
1788
1789    #[tokio::test]
1790    async fn test_build_advertises_resources_capability_for_templates_only() {
1791        let server = LambdaMcpServerBuilder::new()
1792            .name("template-only")
1793            .resource(TemplateTestResource)
1794            .storage(Arc::new(InMemorySessionStorage::new()))
1795            .sse(false)
1796            .build()
1797            .await
1798            .unwrap();
1799
1800        assert!(
1801            server.capabilities().resources.is_some(),
1802            "Resources capability should be advertised when template resources are registered"
1803        );
1804    }
1805
1806    #[tokio::test]
1807    async fn test_build_advertises_resources_capability_for_static_only() {
1808        let server = LambdaMcpServerBuilder::new()
1809            .name("static-only")
1810            .resource(StaticTestResource)
1811            .storage(Arc::new(InMemorySessionStorage::new()))
1812            .sse(false)
1813            .build()
1814            .await
1815            .unwrap();
1816
1817        assert!(
1818            server.capabilities().resources.is_some(),
1819            "Resources capability should be advertised when static resources are registered"
1820        );
1821    }
1822
1823    #[tokio::test]
1824    async fn test_build_no_resources_no_capability() {
1825        let server = LambdaMcpServerBuilder::new()
1826            .name("no-resources")
1827            .tool(TestTool)
1828            .storage(Arc::new(InMemorySessionStorage::new()))
1829            .sse(false)
1830            .build()
1831            .await
1832            .unwrap();
1833
1834        assert!(
1835            server.capabilities().resources.is_none(),
1836            "Resources capability should NOT be advertised when no resources are registered"
1837        );
1838    }
1839
1840    #[tokio::test]
1841    async fn test_lambda_builder_templates_list_returns_template() {
1842        use turul_mcp_server::handlers::McpHandler;
1843
1844        // Build a ResourceTemplatesHandler the same way build() does — with the template resource
1845        let builder = LambdaMcpServerBuilder::new()
1846            .name("template-test")
1847            .resource(TemplateTestResource);
1848
1849        // Verify the template is registered
1850        assert_eq!(builder.template_resources.len(), 1);
1851
1852        // Build the handler the same way build() does
1853        let handler =
1854            ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1855
1856        // Invoke the handler directly (same as JSON-RPC dispatch)
1857        let result = handler.handle(None).await.expect("should succeed");
1858
1859        let templates = result["resourceTemplates"]
1860            .as_array()
1861            .expect("resourceTemplates should be an array");
1862        assert_eq!(
1863            templates.len(),
1864            1,
1865            "Should have exactly 1 template resource"
1866        );
1867        assert_eq!(
1868            templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1869            "Template URI should match"
1870        );
1871        assert_eq!(templates[0]["name"], "template_test");
1872    }
1873
1874    #[tokio::test]
1875    async fn test_lambda_builder_resources_list_returns_static() {
1876        use turul_mcp_server::handlers::McpHandler;
1877
1878        // Build a ResourcesHandler the same way build() does — with the static resource
1879        let builder = LambdaMcpServerBuilder::new()
1880            .name("static-test")
1881            .resource(StaticTestResource);
1882
1883        assert_eq!(builder.resources.len(), 1);
1884
1885        let mut handler = ResourcesHandler::new();
1886        for resource in builder.resources.values() {
1887            handler = handler.add_resource_arc(resource.clone());
1888        }
1889
1890        let result = handler.handle(None).await.expect("should succeed");
1891
1892        let resources = result["resources"]
1893            .as_array()
1894            .expect("resources should be an array");
1895        assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1896        assert_eq!(resources[0]["uri"], "file:///test.txt");
1897        assert_eq!(resources[0]["name"], "static_test");
1898    }
1899
1900    #[tokio::test]
1901    async fn test_lambda_builder_mixed_resources_separation() {
1902        use turul_mcp_server::handlers::McpHandler;
1903
1904        // Build with both static and template resources
1905        let builder = LambdaMcpServerBuilder::new()
1906            .name("mixed-test")
1907            .resource(StaticTestResource)
1908            .resource(TemplateTestResource);
1909
1910        assert_eq!(builder.resources.len(), 1);
1911        assert_eq!(builder.template_resources.len(), 1);
1912
1913        // Build handlers the same way build() does
1914        let mut list_handler = ResourcesHandler::new();
1915        for resource in builder.resources.values() {
1916            list_handler = list_handler.add_resource_arc(resource.clone());
1917        }
1918
1919        let templates_handler =
1920            ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1921
1922        // resources/list should return only the static resource
1923        let list_result = list_handler.handle(None).await.expect("should succeed");
1924        let resources = list_result["resources"]
1925            .as_array()
1926            .expect("resources should be an array");
1927        assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1928        assert_eq!(resources[0]["uri"], "file:///test.txt");
1929
1930        // resources/templates/list should return only the template resource
1931        let templates_result = templates_handler
1932            .handle(None)
1933            .await
1934            .expect("should succeed");
1935        let templates = templates_result["resourceTemplates"]
1936            .as_array()
1937            .expect("resourceTemplates should be an array");
1938        assert_eq!(
1939            templates.len(),
1940            1,
1941            "Only template resource in resources/templates/list"
1942        );
1943        assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
1944    }
1945
1946    #[tokio::test]
1947    async fn test_tasks_get_route_registered() {
1948        use turul_mcp_server::TasksGetHandler;
1949        use turul_mcp_server::handlers::McpHandler;
1950        use turul_mcp_server::task_storage::InMemoryTaskStorage;
1951
1952        let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1953            Arc::new(InMemoryTaskStorage::new()),
1954        ));
1955        let handler = TasksGetHandler::new(runtime);
1956
1957        // Dispatch tasks/get with a non-existent task_id — should return MCP error
1958        // (not "method not found"), proving the route is registered and responds
1959        let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
1960
1961        let result = handler.handle(Some(params)).await;
1962
1963        // Should be an error (task not found) — NOT a "method not found" error
1964        assert!(
1965            result.is_err(),
1966            "tasks/get with unknown task should return error"
1967        );
1968        let err = result.unwrap_err();
1969        let err_str = err.to_string();
1970        assert!(
1971            !err_str.contains("method not found"),
1972            "Error should not be 'method not found' — handler should respond to tasks/get"
1973        );
1974    }
1975}