Skip to main content

turul_mcp_aws_lambda/
handler.rs

1//! Lambda MCP handler that delegates to SessionMcpHandler
2//!
3//! This module provides the LambdaMcpHandler that processes Lambda HTTP
4//! requests by delegating to SessionMcpHandler, eliminating code duplication.
5
6use std::sync::Arc;
7
8use lambda_http::{Body as LambdaBody, Request as LambdaRequest, Response as LambdaResponse};
9use tracing::{debug, info};
10
11use turul_http_mcp_server::{
12    ServerConfig, SessionMcpHandler, StreamConfig, StreamManager, StreamableHttpHandler,
13};
14use turul_mcp_json_rpc_server::JsonRpcDispatcher;
15use turul_mcp_protocol::{McpError, ServerCapabilities};
16use turul_mcp_session_storage::BoxedSessionStorage;
17
18use crate::error::Result;
19
20#[cfg(feature = "cors")]
21use crate::cors::{CorsConfig, create_preflight_response, inject_cors_headers};
22
23/// Main handler for Lambda MCP requests
24///
25/// This handler processes MCP requests in Lambda by delegating to SessionMcpHandler,
26/// eliminating 600+ lines of duplicate business logic code.
27///
28/// Features:
29/// 1. Type conversion between lambda_http and hyper
30/// 2. Delegation to SessionMcpHandler for all business logic
31/// 3. CORS support for browser clients
32/// 4. SSE validation to prevent silent failures
33#[derive(Clone)]
34pub struct LambdaMcpHandler {
35    /// SessionMcpHandler for legacy protocol support
36    session_handler: SessionMcpHandler,
37
38    /// StreamableHttpHandler for MCP 2025-11-25 with proper headers
39    streamable_handler: StreamableHttpHandler,
40
41    /// Whether SSE is enabled (used for testing and debugging)
42    #[allow(dead_code)]
43    sse_enabled: bool,
44
45    /// Custom route registry (e.g., .well-known endpoints)
46    route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
47
48    /// Dynamic tool registry for request-time change detection
49    #[cfg(feature = "dynamic-tools")]
50    tool_registry: Option<Arc<turul_mcp_server::ToolRegistry>>,
51
52    /// CORS configuration (if enabled)
53    #[cfg(feature = "cors")]
54    cors_config: Option<CorsConfig>,
55}
56
57impl LambdaMcpHandler {
58    /// Create a new Lambda MCP handler with the framework components
59    #[allow(clippy::too_many_arguments)]
60    pub fn new(
61        dispatcher: JsonRpcDispatcher<McpError>,
62        session_storage: Arc<BoxedSessionStorage>,
63        stream_manager: Arc<StreamManager>,
64        config: ServerConfig,
65        stream_config: StreamConfig,
66        _implementation: turul_mcp_protocol::Implementation,
67        capabilities: ServerCapabilities,
68        sse_enabled: bool,
69        #[cfg(feature = "cors")] cors_config: Option<CorsConfig>,
70    ) -> Self {
71        let dispatcher = Arc::new(dispatcher);
72
73        // Create empty middleware stack (shared by both handlers)
74        let middleware_stack = Arc::new(turul_http_mcp_server::middleware::MiddlewareStack::new());
75
76        // Create SessionMcpHandler for legacy protocol support
77        let session_handler = SessionMcpHandler::with_shared_stream_manager(
78            config.clone(),
79            dispatcher.clone(),
80            session_storage.clone(),
81            stream_config.clone(),
82            stream_manager.clone(),
83            middleware_stack.clone(),
84        );
85
86        // Create StreamableHttpHandler for MCP 2025-11-25 support
87        let streamable_handler = StreamableHttpHandler::new(
88            Arc::new(config.clone()),
89            dispatcher.clone(),
90            session_storage.clone(),
91            stream_manager.clone(),
92            capabilities.clone(),
93            middleware_stack,
94            None, // No fingerprint in legacy constructor
95        );
96
97        Self {
98            session_handler,
99            streamable_handler,
100            sse_enabled,
101            route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
102            #[cfg(feature = "dynamic-tools")]
103            tool_registry: None,
104            #[cfg(feature = "cors")]
105            cors_config,
106        }
107    }
108
109    /// Create with shared stream manager (for advanced use cases)
110    #[allow(clippy::too_many_arguments)]
111    pub fn with_shared_stream_manager(
112        config: ServerConfig,
113        dispatcher: Arc<JsonRpcDispatcher<McpError>>,
114        session_storage: Arc<BoxedSessionStorage>,
115        stream_manager: Arc<StreamManager>,
116        stream_config: StreamConfig,
117        _implementation: turul_mcp_protocol::Implementation,
118        capabilities: ServerCapabilities,
119        sse_enabled: bool,
120    ) -> Self {
121        // Create empty middleware stack (shared by both handlers)
122        let middleware_stack = Arc::new(turul_http_mcp_server::middleware::MiddlewareStack::new());
123
124        // Create SessionMcpHandler for legacy protocol support
125        let session_handler = SessionMcpHandler::with_shared_stream_manager(
126            config.clone(),
127            dispatcher.clone(),
128            session_storage.clone(),
129            stream_config.clone(),
130            stream_manager.clone(),
131            middleware_stack.clone(),
132        );
133
134        // Create StreamableHttpHandler for MCP 2025-11-25 support
135        let streamable_handler = StreamableHttpHandler::new(
136            Arc::new(config),
137            dispatcher,
138            session_storage,
139            stream_manager,
140            capabilities,
141            middleware_stack,
142            None, // No fingerprint in legacy constructor
143        );
144
145        Self {
146            session_handler,
147            streamable_handler,
148            sse_enabled,
149            route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
150            #[cfg(feature = "dynamic-tools")]
151            tool_registry: None,
152            #[cfg(feature = "cors")]
153            cors_config: None,
154        }
155    }
156
157    /// Create with custom middleware stack (for testing and examples)
158    #[allow(clippy::too_many_arguments)]
159    pub fn with_middleware(
160        config: ServerConfig,
161        dispatcher: Arc<JsonRpcDispatcher<McpError>>,
162        session_storage: Arc<BoxedSessionStorage>,
163        stream_manager: Arc<StreamManager>,
164        stream_config: StreamConfig,
165        capabilities: ServerCapabilities,
166        middleware_stack: Arc<turul_http_mcp_server::middleware::MiddlewareStack>,
167        sse_enabled: bool,
168        route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
169    ) -> Self {
170        Self::with_middleware_and_fingerprint(
171            config,
172            dispatcher,
173            session_storage,
174            stream_manager,
175            stream_config,
176            capabilities,
177            middleware_stack,
178            sse_enabled,
179            route_registry,
180            None,
181        )
182    }
183
184    /// Create with custom middleware stack and tool fingerprint for session versioning
185    #[allow(clippy::too_many_arguments)]
186    pub fn with_middleware_and_fingerprint(
187        config: ServerConfig,
188        dispatcher: Arc<JsonRpcDispatcher<McpError>>,
189        session_storage: Arc<BoxedSessionStorage>,
190        stream_manager: Arc<StreamManager>,
191        stream_config: StreamConfig,
192        capabilities: ServerCapabilities,
193        middleware_stack: Arc<turul_http_mcp_server::middleware::MiddlewareStack>,
194        sse_enabled: bool,
195        route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
196        tool_fingerprint: Option<String>,
197    ) -> Self {
198        // Create SessionMcpHandler with custom middleware and fingerprint
199        let session_handler = SessionMcpHandler::with_shared_stream_manager(
200            config.clone(),
201            dispatcher.clone(),
202            session_storage.clone(),
203            stream_config.clone(),
204            stream_manager.clone(),
205            middleware_stack.clone(),
206        )
207        .with_tool_fingerprint(tool_fingerprint.clone());
208
209        // Create StreamableHttpHandler with custom middleware and fingerprint
210        let streamable_handler = StreamableHttpHandler::new(
211            Arc::new(config),
212            dispatcher,
213            session_storage,
214            stream_manager,
215            capabilities,
216            middleware_stack,
217            tool_fingerprint,
218        );
219
220        Self {
221            session_handler,
222            streamable_handler,
223            sse_enabled,
224            route_registry,
225            #[cfg(feature = "dynamic-tools")]
226            tool_registry: None,
227            #[cfg(feature = "cors")]
228            cors_config: None,
229        }
230    }
231
232    /// Set the tool change notifier for restart/redeploy fingerprint mismatch notifications.
233    pub fn with_tool_notifier(mut self, notifier: Arc<dyn turul_http_mcp_server::ToolChangeNotifier>) -> Self {
234        self.session_handler = self.session_handler.with_tool_notifier(Arc::clone(&notifier));
235        self.streamable_handler = self.streamable_handler.with_tool_notifier(notifier);
236        self
237    }
238
239    /// Set a dynamic tool registry for request-time change detection.
240    #[cfg(feature = "dynamic-tools")]
241    pub fn with_tool_registry(mut self, registry: Arc<turul_mcp_server::ToolRegistry>) -> Self {
242        self.tool_registry = Some(registry);
243        self
244    }
245
246    /// Set CORS configuration
247    #[cfg(feature = "cors")]
248    pub fn with_cors(mut self, cors_config: CorsConfig) -> Self {
249        self.cors_config = Some(cors_config);
250        self
251    }
252
253    /// Get access to the underlying stream manager for notifications
254    pub fn get_stream_manager(&self) -> &Arc<StreamManager> {
255        self.session_handler.get_stream_manager()
256    }
257
258    /// Handle a Lambda HTTP request (snapshot mode - no real-time SSE)
259    ///
260    /// This method performs delegation to SessionMcpHandler for all business logic.
261    /// It only handles Lambda-specific concerns: CORS and type conversion.
262    ///
263    /// Note: If SSE is enabled (.sse(true)), SSE responses may not stream properly
264    /// with regular Lambda runtime. For proper SSE streaming, use handle_streaming()
265    /// with run_with_streaming_response().
266    pub async fn handle(&self, req: LambdaRequest) -> Result<LambdaResponse<LambdaBody>> {
267        let method = req.method().clone();
268        let uri = req.uri().clone();
269
270        let request_origin = req
271            .headers()
272            .get("origin")
273            .and_then(|v| v.to_str().ok())
274            .map(|s| s.to_string());
275
276        info!(
277            "🌐 Lambda MCP request: {} {} (origin: {:?})",
278            method, uri, request_origin
279        );
280
281        // Handle CORS preflight requests first (Lambda-specific logic)
282        #[cfg(feature = "cors")]
283        if method == http::Method::OPTIONS
284            && let Some(ref cors_config) = self.cors_config
285        {
286            debug!("Handling CORS preflight request");
287            return create_preflight_response(cors_config, request_origin.as_deref());
288        }
289
290        // Check for remote tool changes (Dynamic mode with coordination)
291        #[cfg(feature = "dynamic-tools")]
292        if let Some(ref registry) = self.tool_registry {
293            if let Err(e) = registry.check_for_changes().await {
294                tracing::warn!(error = %e, "Failed to check for tool changes");
295            }
296        }
297
298        // 🚀 DELEGATION: Convert Lambda request to hyper request
299        let hyper_req = crate::adapter::lambda_to_hyper_request(req)?;
300
301        // Check custom routes (e.g., .well-known) before MCP delegation
302        let path = hyper_req.uri().path().to_string();
303        if !self.route_registry.is_empty() {
304            match self.route_registry.match_route(&path) {
305                Ok(Some(route_handler)) => {
306                    debug!("Custom route matched: {}", path);
307                    use http_body_util::BodyExt;
308                    let (parts, body) = hyper_req.into_parts();
309                    let boxed_req = hyper::Request::from_parts(parts, body.boxed_unsync());
310                    let route_resp = route_handler.handle(boxed_req).await;
311                    let mut lambda_resp =
312                        crate::adapter::hyper_to_lambda_response(route_resp).await?;
313                    #[cfg(feature = "cors")]
314                    if let Some(ref cors_config) = self.cors_config {
315                        inject_cors_headers(
316                            &mut lambda_resp,
317                            cors_config,
318                            request_origin.as_deref(),
319                        )?;
320                    }
321                    return Ok(lambda_resp);
322                }
323                Ok(None) => {} // No match, continue to MCP handler
324                Err(e) => {
325                    debug!("Route validation error: {}", e);
326                    let route_resp = e.into_response();
327                    let mut lambda_resp =
328                        crate::adapter::hyper_to_lambda_response(route_resp).await?;
329                    #[cfg(feature = "cors")]
330                    if let Some(ref cors_config) = self.cors_config {
331                        inject_cors_headers(
332                            &mut lambda_resp,
333                            cors_config,
334                            request_origin.as_deref(),
335                        )?;
336                    }
337                    return Ok(lambda_resp);
338                }
339            }
340        }
341
342        // 🚀 DELEGATION: Use SessionMcpHandler for all business logic
343        let hyper_resp = self
344            .session_handler
345            .handle_mcp_request(hyper_req)
346            .await
347            .map_err(|e| crate::error::LambdaError::McpFramework(e.to_string()))?;
348
349        // 🚀 DELEGATION: Convert hyper response back to Lambda response
350        let mut lambda_resp = crate::adapter::hyper_to_lambda_response(hyper_resp).await?;
351
352        // Apply CORS headers if configured (Lambda-specific logic)
353        #[cfg(feature = "cors")]
354        if let Some(ref cors_config) = self.cors_config {
355            inject_cors_headers(&mut lambda_resp, cors_config, request_origin.as_deref())?;
356        }
357
358        Ok(lambda_resp)
359    }
360
361    /// Handle Lambda streaming request (real SSE streaming)
362    ///
363    /// This method enables real-time SSE streaming using Lambda's streaming response capability.
364    /// It delegates all business logic to SessionMcpHandler.
365    pub async fn handle_streaming(
366        &self,
367        req: LambdaRequest,
368    ) -> std::result::Result<
369        lambda_http::Response<
370            http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>,
371        >,
372        Box<dyn std::error::Error + Send + Sync>,
373    > {
374        let method = req.method().clone();
375        let uri = req.uri().clone();
376        let request_origin = req
377            .headers()
378            .get("origin")
379            .and_then(|v| v.to_str().ok())
380            .map(|s| s.to_string());
381
382        debug!(
383            "🌊 Lambda streaming MCP request: {} {} (origin: {:?})",
384            method, uri, request_origin
385        );
386
387        // Handle CORS preflight requests first (Lambda-specific logic)
388        #[cfg(feature = "cors")]
389        if method == http::Method::OPTIONS
390            && let Some(ref cors_config) = self.cors_config
391        {
392            debug!("Handling CORS preflight request (streaming)");
393            let preflight_response =
394                create_preflight_response(cors_config, request_origin.as_deref())
395                    .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
396
397            // Convert LambdaResponse<LambdaBody> to streaming response
398            return Ok(self.convert_lambda_response_to_streaming(preflight_response));
399        }
400
401        // Check for remote tool changes (Dynamic mode with coordination)
402        #[cfg(feature = "dynamic-tools")]
403        if let Some(ref registry) = self.tool_registry {
404            if let Err(e) = registry.check_for_changes().await {
405                tracing::warn!(error = %e, "Failed to check for tool changes (streaming)");
406            }
407        }
408
409        // 🚀 DELEGATION: Convert Lambda request to hyper request
410        let hyper_req = crate::adapter::lambda_to_hyper_request(req)
411            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
412
413        // Check custom routes (e.g., .well-known) before MCP delegation
414        let path = hyper_req.uri().path().to_string();
415        if !self.route_registry.is_empty() {
416            match self.route_registry.match_route(&path) {
417                Ok(Some(route_handler)) => {
418                    debug!("Custom route matched (streaming): {}", path);
419                    use http_body_util::BodyExt;
420                    let (parts, body) = hyper_req.into_parts();
421                    let boxed_req = hyper::Request::from_parts(parts, body.boxed_unsync());
422                    return Ok(route_handler.handle(boxed_req).await);
423                }
424                Ok(None) => {} // No match, continue to MCP handler
425                Err(e) => {
426                    debug!("Route validation error (streaming): {}", e);
427                    return Ok(e.into_response());
428                }
429            }
430        }
431
432        // 🚀 PROTOCOL ROUTING: Check protocol version and route to appropriate handler
433        use turul_http_mcp_server::protocol::McpProtocolVersion;
434        let protocol_version = hyper_req
435            .headers()
436            .get("MCP-Protocol-Version")
437            .and_then(|h| h.to_str().ok())
438            .and_then(McpProtocolVersion::parse_version)
439            .unwrap_or(McpProtocolVersion::V2025_06_18);
440
441        // Route based on protocol version
442        let hyper_resp = if protocol_version.supports_streamable_http() {
443            // Use StreamableHttpHandler for MCP 2025-11-25 (proper headers, chunked SSE)
444            debug!(
445                "Using StreamableHttpHandler for protocol {}",
446                protocol_version.to_string()
447            );
448            self.streamable_handler.handle_request(hyper_req).await
449        } else {
450            // Legacy protocol: use SessionMcpHandler
451            debug!(
452                "Using SessionMcpHandler for legacy protocol {}",
453                protocol_version.to_string()
454            );
455            self.session_handler
456                .handle_mcp_request(hyper_req)
457                .await
458                .map_err(|e| {
459                    Box::new(crate::error::LambdaError::McpFramework(e.to_string()))
460                        as Box<dyn std::error::Error + Send + Sync>
461                })?
462        };
463
464        // 🚀 DELEGATION: Convert hyper response to Lambda streaming response (preserves streaming!)
465        let mut lambda_resp = crate::adapter::hyper_to_lambda_streaming(hyper_resp);
466
467        // Apply CORS headers if configured (Lambda-specific logic)
468        #[cfg(feature = "cors")]
469        if let Some(ref cors_config) = self.cors_config {
470            inject_cors_headers(&mut lambda_resp, cors_config, request_origin.as_deref())
471                .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
472        }
473
474        Ok(lambda_resp)
475    }
476
477    /// Convert Lambda response to streaming format (helper for CORS preflight)
478    fn convert_lambda_response_to_streaming(
479        &self,
480        lambda_response: LambdaResponse<LambdaBody>,
481    ) -> lambda_http::Response<http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>>
482    {
483        use bytes::Bytes;
484        use http_body_util::{BodyExt, Full};
485
486        let (parts, body) = lambda_response.into_parts();
487        let body_bytes = match body {
488            LambdaBody::Empty => Bytes::new(),
489            LambdaBody::Text(text) => Bytes::from(text),
490            LambdaBody::Binary(bytes) => Bytes::from(bytes),
491            _ => Bytes::new(),
492        };
493
494        // Map error type from Infallible to hyper::Error
495        let streaming_body = Full::new(body_bytes)
496            .map_err(|e: std::convert::Infallible| match e {})
497            .boxed_unsync();
498
499        lambda_http::Response::from_parts(parts, streaming_body)
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use http::Request;
507    use turul_mcp_session_storage::InMemorySessionStorage;
508
509    #[tokio::test]
510    async fn test_handler_creation() {
511        let session_storage = Arc::new(InMemorySessionStorage::new());
512        let stream_manager = Arc::new(StreamManager::new(session_storage.clone()));
513        let dispatcher = JsonRpcDispatcher::new();
514        let config = ServerConfig::default();
515        let implementation = turul_mcp_protocol::Implementation::new("test", "1.0.0");
516        let capabilities = ServerCapabilities::default();
517
518        let handler = LambdaMcpHandler::new(
519            dispatcher,
520            session_storage,
521            stream_manager,
522            config,
523            StreamConfig::default(),
524            implementation,
525            capabilities,
526            false, // SSE disabled for test
527            #[cfg(feature = "cors")]
528            None,
529        );
530
531        // Test that handler was created successfully
532        assert!(!handler.sse_enabled);
533    }
534
535    #[tokio::test]
536    async fn test_sse_enabled_with_handle_works() {
537        let session_storage = Arc::new(InMemorySessionStorage::new());
538        let stream_manager = Arc::new(StreamManager::new(session_storage.clone()));
539        let dispatcher = JsonRpcDispatcher::new();
540        let config = ServerConfig::default();
541        let implementation = turul_mcp_protocol::Implementation::new("test", "1.0.0");
542        let capabilities = ServerCapabilities::default();
543
544        // Create handler with SSE enabled
545        let handler = LambdaMcpHandler::new(
546            dispatcher,
547            session_storage,
548            stream_manager,
549            config,
550            StreamConfig::default(),
551            implementation,
552            capabilities,
553            true, // SSE enabled - should work with handle() for snapshot-based SSE
554            #[cfg(feature = "cors")]
555            None,
556        );
557
558        // Create a test Lambda request
559        let lambda_req = Request::builder()
560            .method("POST")
561            .uri("/mcp")
562            .body(LambdaBody::Text(
563                r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
564            ))
565            .unwrap();
566
567        // handle() should work (provides snapshot-based SSE rather than real-time streaming)
568        let result = handler.handle(lambda_req).await;
569        assert!(
570            result.is_ok(),
571            "handle() should work with SSE enabled for snapshot-based responses"
572        );
573    }
574
575    /// Test that verifies StreamConfig is properly threaded through the delegation
576    #[tokio::test]
577    async fn test_stream_config_preservation() {
578        let session_storage = Arc::new(InMemorySessionStorage::new());
579        let dispatcher = JsonRpcDispatcher::new();
580        let config = ServerConfig::default();
581        let implementation = turul_mcp_protocol::Implementation::new("test", "1.0.0");
582        let capabilities = ServerCapabilities::default();
583
584        // Create a custom StreamConfig with non-default values
585        let custom_stream_config = StreamConfig {
586            channel_buffer_size: 1024,      // Non-default value (default is 1000)
587            max_replay_events: 200,         // Non-default value (default is 100)
588            keepalive_interval_seconds: 10, // Non-default value (default is 30)
589            cors_origin: "https://custom-test.example.com".to_string(), // Non-default value
590        };
591
592        // Create stream manager with the custom config
593        let stream_manager = Arc::new(StreamManager::with_config(
594            session_storage.clone(),
595            custom_stream_config.clone(),
596        ));
597
598        let handler = LambdaMcpHandler::new(
599            dispatcher,
600            session_storage,
601            stream_manager,
602            config,
603            custom_stream_config.clone(),
604            implementation,
605            capabilities,
606            false, // SSE disabled for test
607            #[cfg(feature = "cors")]
608            None,
609        );
610
611        // The handler should be created successfully, proving the StreamConfig was accepted
612        assert!(!handler.sse_enabled);
613
614        // Verify that the stream manager has the custom configuration
615        let stream_manager = handler.get_stream_manager();
616
617        // Verify the StreamConfig values were propagated correctly
618        let actual_config = stream_manager.get_config();
619
620        assert_eq!(
621            actual_config.channel_buffer_size, custom_stream_config.channel_buffer_size,
622            "Custom channel_buffer_size was not propagated correctly"
623        );
624        assert_eq!(
625            actual_config.max_replay_events, custom_stream_config.max_replay_events,
626            "Custom max_replay_events was not propagated correctly"
627        );
628        assert_eq!(
629            actual_config.keepalive_interval_seconds,
630            custom_stream_config.keepalive_interval_seconds,
631            "Custom keepalive_interval_seconds was not propagated correctly"
632        );
633        assert_eq!(
634            actual_config.cors_origin, custom_stream_config.cors_origin,
635            "Custom cors_origin was not propagated correctly"
636        );
637
638        // Verify the stream manager is accessible (proves delegation worked)
639        assert!(Arc::strong_count(stream_manager) >= 1);
640    }
641
642    /// Test the full builder → server → handler chain with StreamConfig
643    #[tokio::test]
644    async fn test_full_builder_chain_stream_config() {
645        use crate::LambdaMcpServerBuilder;
646        use turul_mcp_session_storage::InMemorySessionStorage;
647
648        // Create a custom StreamConfig with non-default values
649        let custom_stream_config = turul_http_mcp_server::StreamConfig {
650            channel_buffer_size: 2048,      // Non-default value
651            max_replay_events: 500,         // Non-default value
652            keepalive_interval_seconds: 15, // Non-default value
653            cors_origin: "https://full-chain-test.example.com".to_string(),
654        };
655
656        // Test the complete builder → server → handler chain
657        let server = LambdaMcpServerBuilder::new()
658            .name("full-chain-test")
659            .version("1.0.0")
660            .storage(Arc::new(InMemorySessionStorage::new()))
661            .sse(true) // Enable SSE to test streaming functionality
662            .stream_config(custom_stream_config.clone())
663            .build()
664            .await
665            .expect("Server should build successfully");
666
667        // Create handler from server (this is the critical chain step)
668        let handler = server
669            .handler()
670            .await
671            .expect("Handler should be created from server");
672
673        // Verify the handler was created successfully
674        assert!(handler.sse_enabled, "SSE should be enabled");
675
676        // Verify that the custom StreamConfig was preserved through the entire chain
677        let stream_manager = handler.get_stream_manager();
678        let actual_config = stream_manager.get_config();
679
680        assert_eq!(
681            actual_config.channel_buffer_size, custom_stream_config.channel_buffer_size,
682            "Custom channel_buffer_size should be preserved through builder → server → handler chain"
683        );
684        assert_eq!(
685            actual_config.max_replay_events, custom_stream_config.max_replay_events,
686            "Custom max_replay_events should be preserved through builder → server → handler chain"
687        );
688        assert_eq!(
689            actual_config.keepalive_interval_seconds,
690            custom_stream_config.keepalive_interval_seconds,
691            "Custom keepalive_interval_seconds should be preserved through builder → server → handler chain"
692        );
693        assert_eq!(
694            actual_config.cors_origin, custom_stream_config.cors_origin,
695            "Custom cors_origin should be preserved through builder → server → handler chain"
696        );
697
698        // Verify the stream manager is functional
699        assert!(
700            Arc::strong_count(stream_manager) >= 1,
701            "Stream manager should be properly initialized"
702        );
703
704        // Additional verification: Test that the configuration is actually used functionally
705        // by verifying the stream manager can be used with the custom configuration
706        let test_session_id = uuid::Uuid::now_v7().as_simple().to_string();
707
708        // The stream manager should be able to handle session operations with the custom config
709        // This verifies the config isn't just preserved but actually used
710        let subscriptions = stream_manager.get_subscriptions(&test_session_id).await;
711        assert!(
712            subscriptions.is_empty(),
713            "New session should have no subscriptions initially"
714        );
715
716        // Verify the stream manager was constructed with our custom config values
717        // This confirms the config propagated through the entire builder → server → handler chain
718        assert_eq!(
719            stream_manager.get_config().channel_buffer_size,
720            2048,
721            "Stream manager should be using the custom buffer size functionally"
722        );
723    }
724
725    /// Test matrix: 4 combinations of streaming runtime vs SSE configuration
726    /// This ensures we don't have runtime hangs or configuration conflicts
727    ///
728    /// Test 1: Non-streaming runtime + sse(false) - This should work (snapshot mode)
729    #[tokio::test]
730    async fn test_non_streaming_runtime_sse_false() {
731        use crate::LambdaMcpServerBuilder;
732        use turul_mcp_session_storage::InMemorySessionStorage;
733
734        let server = LambdaMcpServerBuilder::new()
735            .name("test-non-streaming-sse-false")
736            .version("1.0.0")
737            .storage(Arc::new(InMemorySessionStorage::new()))
738            .sse(false) // Disable SSE for non-streaming runtime
739            .build()
740            .await
741            .expect("Server should build successfully");
742
743        let handler = server
744            .handler()
745            .await
746            .expect("Handler should be created from server");
747
748        // Verify configuration
749        assert!(!handler.sse_enabled, "SSE should be disabled");
750
751        // Create a test request (POST /mcp works in all configs)
752        let lambda_req = Request::builder()
753            .method("POST")
754            .uri("/mcp")
755            .body(LambdaBody::Text(
756                r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
757            ))
758            .unwrap();
759
760        // This should work without hanging
761        let result = handler.handle(lambda_req).await;
762        assert!(
763            result.is_ok(),
764            "POST /mcp should work with non-streaming + sse(false)"
765        );
766    }
767
768    /// Test 2: Non-streaming runtime + sse(true) - This should work (snapshot-based SSE)
769    #[tokio::test]
770    async fn test_non_streaming_runtime_sse_true() {
771        use crate::LambdaMcpServerBuilder;
772        use turul_mcp_session_storage::InMemorySessionStorage;
773
774        let server = LambdaMcpServerBuilder::new()
775            .name("test-non-streaming-sse-true")
776            .version("1.0.0")
777            .storage(Arc::new(InMemorySessionStorage::new()))
778            .sse(true) // Enable SSE for snapshot-based responses
779            .build()
780            .await
781            .expect("Server should build successfully");
782
783        let handler = server
784            .handler()
785            .await
786            .expect("Handler should be created from server");
787
788        // Verify configuration
789        assert!(handler.sse_enabled, "SSE should be enabled");
790
791        // Create a test request (POST /mcp works in all configs)
792        let lambda_req = Request::builder()
793            .method("POST")
794            .uri("/mcp")
795            .body(LambdaBody::Text(
796                r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
797            ))
798            .unwrap();
799
800        // This should work without hanging (provides snapshot-based SSE)
801        let result = handler.handle(lambda_req).await;
802        assert!(
803            result.is_ok(),
804            "POST /mcp should work with non-streaming + sse(true)"
805        );
806
807        // Note: GET /mcp would provide snapshot events, not real-time streaming
808        // This is the key difference from handle_streaming()
809    }
810
811    /// Test 3: Streaming runtime + sse(false) - This should work (SSE disabled)
812    #[tokio::test]
813    async fn test_streaming_runtime_sse_false() {
814        use crate::LambdaMcpServerBuilder;
815        use turul_mcp_session_storage::InMemorySessionStorage;
816
817        let server = LambdaMcpServerBuilder::new()
818            .name("test-streaming-sse-false")
819            .version("1.0.0")
820            .storage(Arc::new(InMemorySessionStorage::new()))
821            .sse(false) // Disable SSE even with streaming runtime
822            .build()
823            .await
824            .expect("Server should build successfully");
825
826        let handler = server
827            .handler()
828            .await
829            .expect("Handler should be created from server");
830
831        // Verify configuration
832        assert!(!handler.sse_enabled, "SSE should be disabled");
833
834        // Create a test request for streaming handler
835        let lambda_req = Request::builder()
836            .method("POST")
837            .uri("/mcp")
838            .body(LambdaBody::Text(
839                r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
840            ))
841            .unwrap();
842
843        // This should work with streaming runtime even when SSE is disabled
844        let result = handler.handle_streaming(lambda_req).await;
845        assert!(
846            result.is_ok(),
847            "Streaming runtime should work with sse(false)"
848        );
849    }
850
851    /// Test 4: Streaming runtime + sse(true) - This should work (real-time SSE streaming)
852    #[tokio::test]
853    async fn test_streaming_runtime_sse_true() {
854        use crate::LambdaMcpServerBuilder;
855        use turul_mcp_session_storage::InMemorySessionStorage;
856
857        let server = LambdaMcpServerBuilder::new()
858            .name("test-streaming-sse-true")
859            .version("1.0.0")
860            .storage(Arc::new(InMemorySessionStorage::new()))
861            .sse(true) // Enable SSE with streaming runtime for real-time streaming
862            .build()
863            .await
864            .expect("Server should build successfully");
865
866        let handler = server
867            .handler()
868            .await
869            .expect("Handler should be created from server");
870
871        // Verify configuration
872        assert!(handler.sse_enabled, "SSE should be enabled");
873
874        // Create a test request for streaming handler
875        let lambda_req = Request::builder()
876            .method("POST")
877            .uri("/mcp")
878            .body(LambdaBody::Text(
879                r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
880            ))
881            .unwrap();
882
883        // This should work and provide real-time SSE streaming
884        let result = handler.handle_streaming(lambda_req).await;
885        assert!(
886            result.is_ok(),
887            "Streaming runtime should work with sse(true) for real-time streaming"
888        );
889
890        // Note: GET /mcp would provide real-time streaming events
891        // This is the optimal configuration for real-time notifications
892    }
893
894    // ── Strict lifecycle tests over handle_streaming() ────────────────
895
896    /// Helper: build a Lambda handler with strict lifecycle and a test tool via the builder.
897    async fn build_strict_streaming_handler() -> LambdaMcpHandler {
898        use crate::LambdaMcpServerBuilder;
899        use turul_mcp_session_storage::InMemorySessionStorage;
900
901        let server = LambdaMcpServerBuilder::new()
902            .name("lifecycle-test")
903            .version("1.0.0")
904            .tool(LifecycleTestTool)
905            .storage(Arc::new(InMemorySessionStorage::new()))
906            .strict_lifecycle(true) // explicit — survives default changes
907            .sse(true)
908            .build()
909            .await
910            .expect("build should succeed");
911
912        server.handler().await.expect("handler should succeed")
913    }
914
915    // Test tool for lifecycle tests — satisfies all required traits
916    #[derive(Clone, Default)]
917    struct LifecycleTestTool;
918
919    impl turul_mcp_builders::traits::HasBaseMetadata for LifecycleTestTool {
920        fn name(&self) -> &str {
921            "ping_tool"
922        }
923    }
924    impl turul_mcp_builders::traits::HasDescription for LifecycleTestTool {
925        fn description(&self) -> Option<&str> {
926            Some("test tool")
927        }
928    }
929    impl turul_mcp_builders::traits::HasInputSchema for LifecycleTestTool {
930        fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
931            static SCHEMA: std::sync::OnceLock<turul_mcp_protocol::ToolSchema> =
932                std::sync::OnceLock::new();
933            SCHEMA.get_or_init(turul_mcp_protocol::ToolSchema::object)
934        }
935    }
936    impl turul_mcp_builders::traits::HasOutputSchema for LifecycleTestTool {
937        fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
938            None
939        }
940    }
941    impl turul_mcp_builders::traits::HasAnnotations for LifecycleTestTool {
942        fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
943            None
944        }
945    }
946    impl turul_mcp_builders::traits::HasToolMeta for LifecycleTestTool {
947        fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
948            None
949        }
950    }
951    impl turul_mcp_builders::traits::HasIcons for LifecycleTestTool {}
952    impl turul_mcp_builders::traits::HasExecution for LifecycleTestTool {}
953
954    #[async_trait::async_trait]
955    impl turul_mcp_server::McpTool for LifecycleTestTool {
956        async fn call(
957            &self,
958            _args: serde_json::Value,
959            _session: Option<turul_mcp_server::SessionContext>,
960        ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
961            Ok(turul_mcp_protocol::tools::CallToolResult::success(vec![
962                turul_mcp_protocol::tools::ToolResult::text("pong"),
963            ]))
964        }
965    }
966
967    /// Helper: create a Lambda POST request for handle_streaming()
968    fn streaming_mcp_request(body: &str, session_id: Option<&str>) -> LambdaRequest {
969        let mut builder = Request::builder()
970            .method("POST")
971            .uri("/mcp")
972            .header("Content-Type", "application/json")
973            .header("Accept", "application/json, text/event-stream")
974            .header("MCP-Protocol-Version", "2025-11-25");
975
976        if let Some(sid) = session_id {
977            builder = builder.header("Mcp-Session-Id", sid);
978        }
979
980        builder.body(LambdaBody::Text(body.to_string())).unwrap()
981    }
982
983    /// Helper: collect streaming response body into a string
984    async fn collect_streaming_body(
985        response: lambda_http::Response<
986            http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>,
987        >,
988    ) -> (http::StatusCode, String) {
989        use http_body_util::BodyExt;
990        let status = response.status();
991        let session_id = response
992            .headers()
993            .get("Mcp-Session-Id")
994            .and_then(|v| v.to_str().ok())
995            .map(String::from);
996        let body_bytes = response
997            .into_body()
998            .collect()
999            .await
1000            .map(|c| c.to_bytes())
1001            .unwrap_or_default();
1002        let body_str = String::from_utf8_lossy(&body_bytes).to_string();
1003        let _ = session_id; // available if needed
1004        (status, body_str)
1005    }
1006
1007    /// Helper: extract session ID from a streaming response
1008    fn extract_session_id(
1009        response: &lambda_http::Response<
1010            http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>,
1011        >,
1012    ) -> Option<String> {
1013        response
1014            .headers()
1015            .get("Mcp-Session-Id")
1016            .and_then(|v| v.to_str().ok())
1017            .map(String::from)
1018    }
1019
1020    /// Helper: parse JSON from a response body (handles SSE "data: " prefix)
1021    fn parse_response_json(body: &str) -> serde_json::Value {
1022        // Strip SSE framing if present
1023        let json_str = body
1024            .lines()
1025            .find(|line| line.starts_with("data: "))
1026            .map(|line| &line[6..])
1027            .unwrap_or(body.trim());
1028        serde_json::from_str(json_str)
1029            .unwrap_or_else(|e| panic!("Failed to parse JSON from body: {e}\nBody: {body}"))
1030    }
1031
1032    /// P0: Full strict lifecycle handshake succeeds on handle_streaming()
1033    #[tokio::test]
1034    async fn test_lambda_streaming_strict_handshake_succeeds() {
1035        let handler = build_strict_streaming_handler().await;
1036
1037        // Step 1: initialize
1038        let init_req = streaming_mcp_request(
1039            &serde_json::json!({
1040                "jsonrpc": "2.0", "method": "initialize", "id": 1,
1041                "params": {
1042                    "protocolVersion": "2025-11-25",
1043                    "capabilities": {},
1044                    "clientInfo": { "name": "test", "version": "1.0.0" }
1045                }
1046            })
1047            .to_string(),
1048            None,
1049        );
1050        let init_resp = handler
1051            .handle_streaming(init_req)
1052            .await
1053            .expect("initialize should succeed");
1054        let session_id = extract_session_id(&init_resp).expect("must return session ID");
1055        let (status, _body) = collect_streaming_body(init_resp).await;
1056        assert_eq!(status, 200, "initialize should return 200");
1057
1058        // Step 2: notifications/initialized
1059        let notif_req = streaming_mcp_request(
1060            &serde_json::json!({
1061                "jsonrpc": "2.0",
1062                "method": "notifications/initialized",
1063                "params": {}
1064            })
1065            .to_string(),
1066            Some(&session_id),
1067        );
1068        let notif_resp = handler
1069            .handle_streaming(notif_req)
1070            .await
1071            .expect("notification should succeed");
1072        let (status, _) = collect_streaming_body(notif_resp).await;
1073        assert_eq!(status, 202, "notifications/initialized should return 202");
1074
1075        // Step 3: tools/list
1076        let list_req = streaming_mcp_request(
1077            &serde_json::json!({
1078                "jsonrpc": "2.0", "method": "tools/list", "id": 2
1079            })
1080            .to_string(),
1081            Some(&session_id),
1082        );
1083        let list_resp = handler
1084            .handle_streaming(list_req)
1085            .await
1086            .expect("tools/list should succeed");
1087        let (status, body) = collect_streaming_body(list_resp).await;
1088        assert_eq!(status, 200, "tools/list should return 200");
1089        let json = parse_response_json(&body);
1090        assert!(
1091            json["result"]["tools"].is_array(),
1092            "tools/list should return tools array: {json}"
1093        );
1094
1095        // Step 4: tools/call
1096        let call_req = streaming_mcp_request(
1097            &serde_json::json!({
1098                "jsonrpc": "2.0", "method": "tools/call", "id": 3,
1099                "params": { "name": "ping_tool", "arguments": {} }
1100            })
1101            .to_string(),
1102            Some(&session_id),
1103        );
1104        let call_resp = handler
1105            .handle_streaming(call_req)
1106            .await
1107            .expect("tools/call should succeed");
1108        let (status, body) = collect_streaming_body(call_resp).await;
1109        assert_eq!(status, 200, "tools/call should return 200");
1110        let json = parse_response_json(&body);
1111        assert!(
1112            json["result"].is_object(),
1113            "tools/call should return result: {json}"
1114        );
1115    }
1116
1117    /// P0: Strict lifecycle rejects both tools/list and tools/call before notifications/initialized
1118    #[tokio::test]
1119    async fn test_lambda_streaming_strict_rejects_before_initialized() {
1120        let handler = build_strict_streaming_handler().await;
1121
1122        // Initialize to get session
1123        let init_req = streaming_mcp_request(
1124            &serde_json::json!({
1125                "jsonrpc": "2.0", "method": "initialize", "id": 1,
1126                "params": {
1127                    "protocolVersion": "2025-11-25",
1128                    "capabilities": {},
1129                    "clientInfo": { "name": "test", "version": "1.0.0" }
1130                }
1131            })
1132            .to_string(),
1133            None,
1134        );
1135        let init_resp = handler.handle_streaming(init_req).await.unwrap();
1136        let session_id = extract_session_id(&init_resp).unwrap();
1137        let _ = collect_streaming_body(init_resp).await;
1138
1139        // tools/list without notifications/initialized — must fail
1140        let list_req = streaming_mcp_request(
1141            &serde_json::json!({
1142                "jsonrpc": "2.0", "method": "tools/list", "id": 2
1143            })
1144            .to_string(),
1145            Some(&session_id),
1146        );
1147        let list_resp = handler.handle_streaming(list_req).await.unwrap();
1148        let (_, body) = collect_streaming_body(list_resp).await;
1149        let json = parse_response_json(&body);
1150        assert!(
1151            json["error"].is_object(),
1152            "tools/list should return JSON-RPC error: {json}"
1153        );
1154        assert_eq!(
1155            json["error"]["code"].as_i64().unwrap(),
1156            -32031,
1157            "tools/list must return SessionError code -32031, got: {json}"
1158        );
1159        assert!(
1160            json["error"]["message"]
1161                .as_str()
1162                .unwrap()
1163                .contains("notifications/initialized"),
1164            "Error must mention notifications/initialized: {}",
1165            json["error"]["message"]
1166        );
1167
1168        // tools/call without notifications/initialized — must also fail
1169        let call_req = streaming_mcp_request(
1170            &serde_json::json!({
1171                "jsonrpc": "2.0", "method": "tools/call", "id": 3,
1172                "params": { "name": "ping_tool", "arguments": {} }
1173            })
1174            .to_string(),
1175            Some(&session_id),
1176        );
1177        let call_resp = handler.handle_streaming(call_req).await.unwrap();
1178        let (_, body) = collect_streaming_body(call_resp).await;
1179        let json = parse_response_json(&body);
1180        assert!(
1181            json["error"].is_object(),
1182            "tools/call should return JSON-RPC error: {json}"
1183        );
1184        assert_eq!(
1185            json["error"]["code"].as_i64().unwrap(),
1186            -32031,
1187            "tools/call must return SessionError code -32031, got: {json}"
1188        );
1189        assert!(
1190            json["error"]["message"]
1191                .as_str()
1192                .unwrap()
1193                .contains("notifications/initialized"),
1194            "Error must mention notifications/initialized: {}",
1195            json["error"]["message"]
1196        );
1197    }
1198
1199    /// P0: tools/list succeeds immediately after notifications/initialized (race fix proof)
1200    #[tokio::test]
1201    async fn test_lambda_streaming_initialized_is_effective_immediately() {
1202        let handler = build_strict_streaming_handler().await;
1203
1204        // Initialize
1205        let init_req = streaming_mcp_request(
1206            &serde_json::json!({
1207                "jsonrpc": "2.0", "method": "initialize", "id": 1,
1208                "params": {
1209                    "protocolVersion": "2025-11-25",
1210                    "capabilities": {},
1211                    "clientInfo": { "name": "test", "version": "1.0.0" }
1212                }
1213            })
1214            .to_string(),
1215            None,
1216        );
1217        let init_resp = handler.handle_streaming(init_req).await.unwrap();
1218        let session_id = extract_session_id(&init_resp).unwrap();
1219        let _ = collect_streaming_body(init_resp).await;
1220
1221        // notifications/initialized
1222        let notif_req = streaming_mcp_request(
1223            &serde_json::json!({
1224                "jsonrpc": "2.0",
1225                "method": "notifications/initialized",
1226                "params": {}
1227            })
1228            .to_string(),
1229            Some(&session_id),
1230        );
1231        let notif_resp = handler.handle_streaming(notif_req).await.unwrap();
1232        let (status, _) = collect_streaming_body(notif_resp).await;
1233        assert_eq!(status, 202);
1234
1235        // Immediately — no delay — send tools/list
1236        let list_req = streaming_mcp_request(
1237            &serde_json::json!({
1238                "jsonrpc": "2.0", "method": "tools/list", "id": 2
1239            })
1240            .to_string(),
1241            Some(&session_id),
1242        );
1243        let list_resp = handler.handle_streaming(list_req).await.unwrap();
1244        let (status, body) = collect_streaming_body(list_resp).await;
1245        assert_eq!(
1246            status, 200,
1247            "tools/list must succeed immediately after initialized"
1248        );
1249        let json = parse_response_json(&body);
1250        assert!(
1251            json["result"]["tools"].is_array(),
1252            "Must return tools list, not error: {json}"
1253        );
1254    }
1255
1256    /// P1: Lenient mode allows operations without notifications/initialized
1257    #[tokio::test]
1258    async fn test_lambda_streaming_lenient_mode_allows_without_initialized() {
1259        use crate::LambdaMcpServerBuilder;
1260        use turul_mcp_session_storage::InMemorySessionStorage;
1261
1262        let server = LambdaMcpServerBuilder::new()
1263            .name("lenient-test")
1264            .version("1.0.0")
1265            .tool(LifecycleTestTool)
1266            .storage(Arc::new(InMemorySessionStorage::new()))
1267            .strict_lifecycle(false) // lenient mode
1268            .sse(true)
1269            .build()
1270            .await
1271            .unwrap();
1272
1273        let handler = server.handler().await.unwrap();
1274
1275        // Initialize (no notifications/initialized)
1276        let init_req = streaming_mcp_request(
1277            &serde_json::json!({
1278                "jsonrpc": "2.0", "method": "initialize", "id": 1,
1279                "params": {
1280                    "protocolVersion": "2025-11-25",
1281                    "capabilities": {},
1282                    "clientInfo": { "name": "test", "version": "1.0.0" }
1283                }
1284            })
1285            .to_string(),
1286            None,
1287        );
1288        let init_resp = handler.handle_streaming(init_req).await.unwrap();
1289        let session_id = extract_session_id(&init_resp).unwrap();
1290        let _ = collect_streaming_body(init_resp).await;
1291
1292        // Skip notifications/initialized — go straight to tools/list
1293        let list_req = streaming_mcp_request(
1294            &serde_json::json!({
1295                "jsonrpc": "2.0", "method": "tools/list", "id": 2
1296            })
1297            .to_string(),
1298            Some(&session_id),
1299        );
1300        let list_resp = handler.handle_streaming(list_req).await.unwrap();
1301        let (status, body) = collect_streaming_body(list_resp).await;
1302        assert_eq!(
1303            status, 200,
1304            "Lenient mode should allow tools/list without initialized"
1305        );
1306        let json = parse_response_json(&body);
1307        assert!(
1308            json["result"]["tools"].is_array(),
1309            "Must return tools list in lenient mode: {json}"
1310        );
1311    }
1312}