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 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(¬ifier));
235 self.streamable_handler = self.streamable_handler.with_tool_notifier(notifier);
236 self
237 }
238
239 #[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 #[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 pub fn get_stream_manager(&self) -> &Arc<StreamManager> {
255 self.session_handler.get_stream_manager()
256 }
257
258 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 #[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 #[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 let hyper_req = crate::adapter::lambda_to_hyper_request(req)?;
300
301 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) => {} 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 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 let mut lambda_resp = crate::adapter::hyper_to_lambda_response(hyper_resp).await?;
351
352 #[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 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 #[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 return Ok(self.convert_lambda_response_to_streaming(preflight_response));
399 }
400
401 #[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 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 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) => {} Err(e) => {
426 debug!("Route validation error (streaming): {}", e);
427 return Ok(e.into_response());
428 }
429 }
430 }
431
432 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 let hyper_resp = if protocol_version.supports_streamable_http() {
443 debug!(
445 "Using StreamableHttpHandler for protocol {}",
446 protocol_version.to_string()
447 );
448 self.streamable_handler.handle_request(hyper_req).await
449 } else {
450 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 let mut lambda_resp = crate::adapter::hyper_to_lambda_streaming(hyper_resp);
466
467 #[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 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 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, #[cfg(feature = "cors")]
528 None,
529 );
530
531 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 let handler = LambdaMcpHandler::new(
546 dispatcher,
547 session_storage,
548 stream_manager,
549 config,
550 StreamConfig::default(),
551 implementation,
552 capabilities,
553 true, #[cfg(feature = "cors")]
555 None,
556 );
557
558 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 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 #[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 let custom_stream_config = StreamConfig {
586 channel_buffer_size: 1024, max_replay_events: 200, keepalive_interval_seconds: 10, cors_origin: "https://custom-test.example.com".to_string(), };
591
592 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, #[cfg(feature = "cors")]
608 None,
609 );
610
611 assert!(!handler.sse_enabled);
613
614 let stream_manager = handler.get_stream_manager();
616
617 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 assert!(Arc::strong_count(stream_manager) >= 1);
640 }
641
642 #[tokio::test]
644 async fn test_full_builder_chain_stream_config() {
645 use crate::LambdaMcpServerBuilder;
646 use turul_mcp_session_storage::InMemorySessionStorage;
647
648 let custom_stream_config = turul_http_mcp_server::StreamConfig {
650 channel_buffer_size: 2048, max_replay_events: 500, keepalive_interval_seconds: 15, cors_origin: "https://full-chain-test.example.com".to_string(),
654 };
655
656 let server = LambdaMcpServerBuilder::new()
658 .name("full-chain-test")
659 .version("1.0.0")
660 .storage(Arc::new(InMemorySessionStorage::new()))
661 .sse(true) .stream_config(custom_stream_config.clone())
663 .build()
664 .await
665 .expect("Server should build successfully");
666
667 let handler = server
669 .handler()
670 .await
671 .expect("Handler should be created from server");
672
673 assert!(handler.sse_enabled, "SSE should be enabled");
675
676 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 assert!(
700 Arc::strong_count(stream_manager) >= 1,
701 "Stream manager should be properly initialized"
702 );
703
704 let test_session_id = uuid::Uuid::now_v7().as_simple().to_string();
707
708 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 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 #[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) .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 assert!(!handler.sse_enabled, "SSE should be disabled");
750
751 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 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 #[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) .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 assert!(handler.sse_enabled, "SSE should be enabled");
790
791 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 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 }
810
811 #[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) .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 assert!(!handler.sse_enabled, "SSE should be disabled");
833
834 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 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 #[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) .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 assert!(handler.sse_enabled, "SSE should be enabled");
873
874 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 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 }
893
894 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) .sse(true)
908 .build()
909 .await
910 .expect("build should succeed");
911
912 server.handler().await.expect("handler should succeed")
913 }
914
915 #[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 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 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; (status, body_str)
1005 }
1006
1007 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 fn parse_response_json(body: &str) -> serde_json::Value {
1022 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 #[tokio::test]
1034 async fn test_lambda_streaming_strict_handshake_succeeds() {
1035 let handler = build_strict_streaming_handler().await;
1036
1037 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 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 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 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 #[tokio::test]
1119 async fn test_lambda_streaming_strict_rejects_before_initialized() {
1120 let handler = build_strict_streaming_handler().await;
1121
1122 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 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 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 #[tokio::test]
1201 async fn test_lambda_streaming_initialized_is_effective_immediately() {
1202 let handler = build_strict_streaming_handler().await;
1203
1204 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 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 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 #[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) .sse(true)
1269 .build()
1270 .await
1271 .unwrap();
1272
1273 let handler = server.handler().await.unwrap();
1274
1275 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 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}