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