1use 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#[derive(Clone)]
34pub struct LambdaMcpHandler {
35 session_handler: SessionMcpHandler,
37
38 streamable_handler: StreamableHttpHandler,
40
41 #[allow(dead_code)]
43 sse_enabled: bool,
44
45 route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
47
48 #[cfg(feature = "dynamic-tools")]
50 tool_registry: Option<Arc<turul_mcp_server::ToolRegistry>>,
51
52 #[cfg(feature = "cors")]
54 cors_config: Option<CorsConfig>,
55}
56
57impl LambdaMcpHandler {
58 #[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 let middleware_stack = Arc::new(turul_http_mcp_server::middleware::MiddlewareStack::new());
75
76 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 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, );
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 #[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 let middleware_stack = Arc::new(turul_http_mcp_server::middleware::MiddlewareStack::new());
123
124 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 let streamable_handler = StreamableHttpHandler::new(
136 Arc::new(config),
137 dispatcher,
138 session_storage,
139 stream_manager,
140 capabilities,
141 middleware_stack,
142 None, );
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 #[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 #[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 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 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 #[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 #[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 pub fn get_stream_manager(&self) -> &Arc<StreamManager> {
248 self.session_handler.get_stream_manager()
249 }
250
251 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 #[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 #[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 let hyper_req = crate::adapter::lambda_to_hyper_request(req)?;
293
294 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) => {} 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 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 let mut lambda_resp = crate::adapter::hyper_to_lambda_response(hyper_resp).await?;
344
345 #[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 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 #[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 return Ok(self.convert_lambda_response_to_streaming(preflight_response));
392 }
393
394 #[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 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 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) => {} Err(e) => {
419 debug!("Route validation error (streaming): {}", e);
420 return Ok(e.into_response());
421 }
422 }
423 }
424
425 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 let hyper_resp = if protocol_version.supports_streamable_http() {
436 debug!(
438 "Using StreamableHttpHandler for protocol {}",
439 protocol_version.to_string()
440 );
441 self.streamable_handler.handle_request(hyper_req).await
442 } else {
443 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 let mut lambda_resp = crate::adapter::hyper_to_lambda_streaming(hyper_resp);
459
460 #[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 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 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, #[cfg(feature = "cors")]
521 None,
522 );
523
524 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 let handler = LambdaMcpHandler::new(
539 dispatcher,
540 session_storage,
541 stream_manager,
542 config,
543 StreamConfig::default(),
544 implementation,
545 capabilities,
546 true, #[cfg(feature = "cors")]
548 None,
549 );
550
551 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 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 #[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 let custom_stream_config = StreamConfig {
579 channel_buffer_size: 1024, max_replay_events: 200, keepalive_interval_seconds: 10, cors_origin: "https://custom-test.example.com".to_string(), };
584
585 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, #[cfg(feature = "cors")]
601 None,
602 );
603
604 assert!(!handler.sse_enabled);
606
607 let stream_manager = handler.get_stream_manager();
609
610 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 assert!(Arc::strong_count(stream_manager) >= 1);
633 }
634
635 #[tokio::test]
637 async fn test_full_builder_chain_stream_config() {
638 use crate::LambdaMcpServerBuilder;
639 use turul_mcp_session_storage::InMemorySessionStorage;
640
641 let custom_stream_config = turul_http_mcp_server::StreamConfig {
643 channel_buffer_size: 2048, max_replay_events: 500, keepalive_interval_seconds: 15, cors_origin: "https://full-chain-test.example.com".to_string(),
647 };
648
649 let server = LambdaMcpServerBuilder::new()
651 .name("full-chain-test")
652 .version("1.0.0")
653 .storage(Arc::new(InMemorySessionStorage::new()))
654 .sse(true) .stream_config(custom_stream_config.clone())
656 .build()
657 .await
658 .expect("Server should build successfully");
659
660 let handler = server
662 .handler()
663 .await
664 .expect("Handler should be created from server");
665
666 assert!(handler.sse_enabled, "SSE should be enabled");
668
669 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 assert!(
693 Arc::strong_count(stream_manager) >= 1,
694 "Stream manager should be properly initialized"
695 );
696
697 let test_session_id = uuid::Uuid::now_v7().as_simple().to_string();
700
701 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 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 #[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) .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 assert!(!handler.sse_enabled, "SSE should be disabled");
743
744 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 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 #[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) .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 assert!(handler.sse_enabled, "SSE should be enabled");
783
784 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 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 }
803
804 #[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) .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 assert!(!handler.sse_enabled, "SSE should be disabled");
826
827 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 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 #[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) .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 assert!(handler.sse_enabled, "SSE should be enabled");
866
867 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 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 }
886
887 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) .sse(true)
901 .build()
902 .await
903 .expect("build should succeed");
904
905 server.handler().await.expect("handler should succeed")
906 }
907
908 #[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 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 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; (status, body_str)
998 }
999
1000 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 fn parse_response_json(body: &str) -> serde_json::Value {
1015 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 #[tokio::test]
1027 async fn test_lambda_streaming_strict_handshake_succeeds() {
1028 let handler = build_strict_streaming_handler().await;
1029
1030 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 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 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 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 #[tokio::test]
1112 async fn test_lambda_streaming_strict_rejects_before_initialized() {
1113 let handler = build_strict_streaming_handler().await;
1114
1115 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 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 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 #[tokio::test]
1194 async fn test_lambda_streaming_initialized_is_effective_immediately() {
1195 let handler = build_strict_streaming_handler().await;
1196
1197 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 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 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 #[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) .sse(true)
1262 .build()
1263 .await
1264 .unwrap();
1265
1266 let handler = server.handler().await.unwrap();
1267
1268 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 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}