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