1use std::collections::HashMap;
7use std::sync::Arc;
8
9use turul_http_mcp_server::{ServerConfig, StreamConfig};
10use turul_mcp_protocol::{Implementation, ServerCapabilities};
11use turul_mcp_server::handlers::{McpHandler, *};
12use turul_mcp_server::{
13 McpCompletion, McpElicitation, McpLogger, McpNotification, McpPrompt, McpResource, McpRoot,
14 McpSampling, McpTool,
15};
16use turul_mcp_session_storage::BoxedSessionStorage;
17
18use crate::error::Result;
19
20#[cfg(feature = "dynamodb")]
21use crate::error::LambdaError;
22use crate::server::LambdaMcpServer;
23
24#[cfg(feature = "cors")]
25use crate::cors::CorsConfig;
26
27pub struct LambdaMcpServerBuilder {
70 name: String,
72 version: String,
73 title: Option<String>,
74 icons: Option<Vec<turul_mcp_protocol::Icon>>,
75
76 capabilities: ServerCapabilities,
78
79 tools: HashMap<String, Arc<dyn McpTool>>,
81
82 resources: HashMap<String, Arc<dyn McpResource>>,
84
85 template_resources: Vec<(
87 turul_mcp_server::uri_template::UriTemplate,
88 Arc<dyn McpResource>,
89 )>,
90
91 prompts: HashMap<String, Arc<dyn McpPrompt>>,
93
94 elicitations: HashMap<String, Arc<dyn McpElicitation>>,
96
97 sampling: HashMap<String, Arc<dyn McpSampling>>,
99
100 completions: HashMap<String, Arc<dyn McpCompletion>>,
102
103 loggers: HashMap<String, Arc<dyn McpLogger>>,
105
106 root_providers: HashMap<String, Arc<dyn McpRoot>>,
108
109 notifications: HashMap<String, Arc<dyn McpNotification>>,
111
112 handlers: HashMap<String, Arc<dyn McpHandler>>,
114
115 roots: Vec<turul_mcp_protocol::roots::Root>,
117
118 instructions: Option<String>,
120
121 session_timeout_minutes: Option<u64>,
123 session_cleanup_interval_seconds: Option<u64>,
124
125 session_storage: Option<Arc<BoxedSessionStorage>>,
127
128 strict_lifecycle: bool,
130
131 enable_sse: bool,
133 server_config: ServerConfig,
135 stream_config: StreamConfig,
136
137 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
139
140 route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
142
143 task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
145 task_recovery_timeout_ms: u64,
147
148 tool_change_mode: turul_mcp_server::ToolChangeMode,
150
151 #[cfg(feature = "dynamic-tools")]
153 server_state_storage: Option<Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>>,
154
155 #[cfg(feature = "cors")]
157 cors_config: Option<CorsConfig>,
158}
159
160impl LambdaMcpServerBuilder {
161 pub fn new() -> Self {
163 let capabilities = ServerCapabilities::default();
166
167 let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
169 handlers.insert("ping".to_string(), Arc::new(PingHandler));
170 handlers.insert(
171 "completion/complete".to_string(),
172 Arc::new(CompletionHandler),
173 );
174 handlers.insert(
175 "resources/list".to_string(),
176 Arc::new(ResourcesHandler::new()),
177 );
178 handlers.insert(
179 "resources/read".to_string(),
180 Arc::new(ResourcesReadHandler::new().without_security()),
181 );
182 handlers.insert(
183 "prompts/list".to_string(),
184 Arc::new(PromptsListHandler::new()),
185 );
186 handlers.insert(
187 "prompts/get".to_string(),
188 Arc::new(PromptsGetHandler::new()),
189 );
190 handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
191 handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
192 handlers.insert(
193 "sampling/createMessage".to_string(),
194 Arc::new(SamplingHandler),
195 );
196 handlers.insert(
199 "elicitation/create".to_string(),
200 Arc::new(ElicitationHandler::with_mock_provider()),
201 );
202
203 let notifications_handler = Arc::new(NotificationsHandler);
205 handlers.insert(
206 "notifications/message".to_string(),
207 notifications_handler.clone(),
208 );
209 handlers.insert(
210 "notifications/progress".to_string(),
211 notifications_handler.clone(),
212 );
213 handlers.insert(
215 "notifications/resources/list_changed".to_string(),
216 notifications_handler.clone(),
217 );
218 handlers.insert(
219 "notifications/resources/updated".to_string(),
220 notifications_handler.clone(),
221 );
222 handlers.insert(
223 "notifications/tools/list_changed".to_string(),
224 notifications_handler.clone(),
225 );
226 handlers.insert(
227 "notifications/prompts/list_changed".to_string(),
228 notifications_handler.clone(),
229 );
230 handlers.insert(
231 "notifications/roots/list_changed".to_string(),
232 notifications_handler.clone(),
233 );
234 handlers.insert(
236 "notifications/resources/listChanged".to_string(),
237 notifications_handler.clone(),
238 );
239 handlers.insert(
240 "notifications/tools/listChanged".to_string(),
241 notifications_handler.clone(),
242 );
243 handlers.insert(
244 "notifications/prompts/listChanged".to_string(),
245 notifications_handler.clone(),
246 );
247 handlers.insert(
248 "notifications/roots/listChanged".to_string(),
249 notifications_handler,
250 );
251
252 Self {
253 name: "turul-mcp-aws-lambda".to_string(),
254 version: env!("CARGO_PKG_VERSION").to_string(),
255 title: None,
256 icons: None,
257 capabilities,
258 tools: HashMap::new(),
259 resources: HashMap::new(),
260 template_resources: Vec::new(),
261 prompts: HashMap::new(),
262 elicitations: HashMap::new(),
263 sampling: HashMap::new(),
264 completions: HashMap::new(),
265 loggers: HashMap::new(),
266 root_providers: HashMap::new(),
267 notifications: HashMap::new(),
268 handlers,
269 roots: Vec::new(),
270 instructions: None,
271 session_timeout_minutes: None,
272 session_cleanup_interval_seconds: None,
273 session_storage: None,
274 strict_lifecycle: true, enable_sse: cfg!(feature = "sse"),
276 server_config: ServerConfig::default(),
277 stream_config: StreamConfig::default(),
278 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
279 route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
280 task_runtime: None,
281 task_recovery_timeout_ms: 300_000, tool_change_mode: turul_mcp_server::ToolChangeMode::Static,
283 #[cfg(feature = "dynamic-tools")]
284 server_state_storage: None,
285 #[cfg(feature = "cors")]
286 cors_config: None,
287 }
288 }
289
290 pub fn name(mut self, name: impl Into<String>) -> Self {
292 self.name = name.into();
293 self
294 }
295
296 pub fn version(mut self, version: impl Into<String>) -> Self {
298 self.version = version.into();
299 self
300 }
301
302 pub fn title(mut self, title: impl Into<String>) -> Self {
304 self.title = Some(title.into());
305 self
306 }
307
308 pub fn icons(mut self, icons: Vec<turul_mcp_protocol::Icon>) -> Self {
310 self.icons = Some(icons);
311 self
312 }
313
314 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
316 self.instructions = Some(instructions.into());
317 self
318 }
319
320 pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
332 let name = tool.name().to_string();
333 self.tools.insert(name, Arc::new(tool));
334 self
335 }
336
337 pub fn tool_fn<F, T>(self, func: F) -> Self
339 where
340 F: Fn() -> T,
341 T: McpTool + 'static,
342 {
343 self.tool(func())
344 }
345
346 pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
348 for tool in tools {
349 self = self.tool(tool);
350 }
351 self
352 }
353
354 pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
360 let uri = resource.uri().to_string();
361
362 if uri.contains('{') && uri.contains('}') {
363 match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
365 Ok(template) => {
366 self.template_resources.push((template, Arc::new(resource)));
367 }
368 Err(e) => {
369 tracing::warn!(
370 "Failed to parse template resource URI '{}': {}. Registering as static.",
371 uri,
372 e
373 );
374 self.resources.insert(uri, Arc::new(resource));
375 }
376 }
377 } else {
378 self.resources.insert(uri, Arc::new(resource));
380 }
381 self
382 }
383
384 pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
386 mut self,
387 resources: I,
388 ) -> Self {
389 for resource in resources {
390 self = self.resource(resource);
391 }
392 self
393 }
394
395 pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
397 let name = prompt.name().to_string();
398 self.prompts.insert(name, Arc::new(prompt));
399 self
400 }
401
402 pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
404 mut self,
405 prompts: I,
406 ) -> Self {
407 for prompt in prompts {
408 self = self.prompt(prompt);
409 }
410 self
411 }
412
413 pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
415 let key = format!("elicitation_{}", self.elicitations.len());
416 self.elicitations.insert(key, Arc::new(elicitation));
417 self
418 }
419
420 pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
422 mut self,
423 elicitations: I,
424 ) -> Self {
425 for elicitation in elicitations {
426 self = self.elicitation(elicitation);
427 }
428 self
429 }
430
431 pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
433 let key = format!("sampling_{}", self.sampling.len());
434 self.sampling.insert(key, Arc::new(sampling));
435 self
436 }
437
438 pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
440 mut self,
441 sampling: I,
442 ) -> Self {
443 for s in sampling {
444 self = self.sampling_provider(s);
445 }
446 self
447 }
448
449 pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
451 let key = format!("completion_{}", self.completions.len());
452 self.completions.insert(key, Arc::new(completion));
453 self
454 }
455
456 pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
458 mut self,
459 completions: I,
460 ) -> Self {
461 for completion in completions {
462 self = self.completion_provider(completion);
463 }
464 self
465 }
466
467 pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
469 let key = format!("logger_{}", self.loggers.len());
470 self.loggers.insert(key, Arc::new(logger));
471 self
472 }
473
474 pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
476 mut self,
477 loggers: I,
478 ) -> Self {
479 for logger in loggers {
480 self = self.logger(logger);
481 }
482 self
483 }
484
485 pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
487 let key = format!("root_{}", self.root_providers.len());
488 self.root_providers.insert(key, Arc::new(root));
489 self
490 }
491
492 pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
494 mut self,
495 roots: I,
496 ) -> Self {
497 for root in roots {
498 self = self.root_provider(root);
499 }
500 self
501 }
502
503 pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
505 let key = format!("notification_{}", self.notifications.len());
506 self.notifications.insert(key, Arc::new(notification));
507 self
508 }
509
510 pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
512 mut self,
513 notifications: I,
514 ) -> Self {
515 for notification in notifications {
516 self = self.notification_provider(notification);
517 }
518 self
519 }
520
521 pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
527 self.sampling_provider(sampling)
528 }
529
530 pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
532 self.completion_provider(completion)
533 }
534
535 pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
537 let notification = N::default();
538 self.notification_provider(notification)
539 }
540
541 pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
543 let handler_arc = Arc::new(handler);
544 for method in handler_arc.supported_methods() {
545 self.handlers.insert(method, handler_arc.clone());
546 }
547 self
548 }
549
550 pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
552 mut self,
553 handlers: I,
554 ) -> Self {
555 for handler in handlers {
556 self = self.handler(handler);
557 }
558 self
559 }
560
561 pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
563 self.roots.push(root);
564 self
565 }
566
567 pub fn with_completion(mut self) -> Self {
573 use turul_mcp_protocol::initialize::CompletionsCapabilities;
574 self.capabilities.completions = Some(CompletionsCapabilities {
575 enabled: Some(true),
576 });
577 self.handler(CompletionHandler)
578 }
579
580 pub fn with_prompts(mut self) -> Self {
582 use turul_mcp_protocol::initialize::PromptsCapabilities;
583 self.capabilities.prompts = Some(PromptsCapabilities {
584 list_changed: Some(false),
585 });
586
587 self
590 }
591
592 pub fn with_resources(mut self) -> Self {
594 use turul_mcp_protocol::initialize::ResourcesCapabilities;
595 self.capabilities.resources = Some(ResourcesCapabilities {
596 subscribe: Some(false),
597 list_changed: Some(false),
598 });
599
600 let mut list_handler = ResourcesHandler::new();
602 for resource in self.resources.values() {
603 list_handler = list_handler.add_resource_arc(resource.clone());
604 }
605 self = self.handler(list_handler);
606
607 if !self.template_resources.is_empty() {
609 let templates_handler =
610 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
611 self = self.handler(templates_handler);
612 }
613
614 let mut read_handler = ResourcesReadHandler::new().without_security();
616 for resource in self.resources.values() {
617 read_handler = read_handler.add_resource_arc(resource.clone());
618 }
619 for (template, resource) in &self.template_resources {
620 read_handler =
621 read_handler.add_template_resource_arc(template.clone(), resource.clone());
622 }
623 self.handler(read_handler)
624 }
625
626 pub fn with_logging(mut self) -> Self {
628 use turul_mcp_protocol::initialize::LoggingCapabilities;
629 self.capabilities.logging = Some(LoggingCapabilities::default());
630 self.handler(LoggingHandler)
631 }
632
633 pub fn with_roots(self) -> Self {
635 self.handler(RootsHandler::new())
636 }
637
638 pub fn with_sampling(self) -> Self {
640 self.handler(SamplingHandler)
641 }
642
643 pub fn with_elicitation(self) -> Self {
645 self.handler(ElicitationHandler::with_mock_provider())
648 }
649
650 pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
652 self.handler(ElicitationHandler::new(Arc::new(provider)))
654 }
655
656 pub fn with_notifications(self) -> Self {
658 self.handler(NotificationsHandler)
659 }
660
661 pub fn with_task_storage(
676 mut self,
677 storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
678 ) -> Self {
679 let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
680 .with_recovery_timeout(self.task_recovery_timeout_ms);
681 self.task_runtime = Some(Arc::new(runtime));
682 self
683 }
684
685 pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
689 self.task_runtime = Some(runtime);
690 self
691 }
692
693 pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
698 self.task_recovery_timeout_ms = timeout_ms;
699 self
700 }
701
702 pub fn tool_change_mode(mut self, mode: turul_mcp_server::ToolChangeMode) -> Self {
713 self.tool_change_mode = mode;
714 self
715 }
716
717 #[cfg(feature = "dynamic-tools")]
724 pub fn server_state_storage(
725 mut self,
726 storage: Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>,
727 ) -> Self {
728 self.server_state_storage = Some(storage);
729 self
730 }
731
732 pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
738 self.session_timeout_minutes = Some(minutes);
739 self
740 }
741
742 pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
744 self.session_cleanup_interval_seconds = Some(seconds);
745 self
746 }
747
748 pub fn strict_lifecycle(mut self, strict: bool) -> Self {
750 self.strict_lifecycle = strict;
751 self
752 }
753
754 pub fn with_strict_lifecycle(self) -> Self {
756 self.strict_lifecycle(true)
757 }
758
759 pub fn sse(mut self, enable: bool) -> Self {
761 self.enable_sse = enable;
762
763 if enable {
765 self.server_config.enable_get_sse = true;
766 self.server_config.enable_post_sse = true;
767 } else {
768 self.server_config.enable_get_sse = false;
771 self.server_config.enable_post_sse = false;
772 }
773
774 self
775 }
776
777 pub fn with_long_sessions(mut self) -> Self {
779 self.session_timeout_minutes = Some(120); self.session_cleanup_interval_seconds = Some(300); self
782 }
783
784 pub fn with_short_sessions(mut self) -> Self {
786 self.session_timeout_minutes = Some(5); self.session_cleanup_interval_seconds = Some(30); self
789 }
790
791 pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
799 self.session_storage = Some(storage);
800 self
801 }
802
803 #[cfg(feature = "dynamodb")]
810 pub async fn dynamodb_storage(self) -> Result<Self> {
811 use turul_mcp_session_storage::DynamoDbSessionStorage;
812
813 let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
814 LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
815 })?;
816
817 Ok(self.storage(Arc::new(storage)))
818 }
819
820 pub fn middleware(
856 mut self,
857 middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
858 ) -> Self {
859 self.middleware_stack.push(middleware);
860 self
861 }
862
863 pub fn route(
865 mut self,
866 path: &str,
867 handler: Arc<dyn turul_http_mcp_server::RouteHandler>,
868 ) -> Self {
869 Arc::get_mut(&mut self.route_registry)
870 .expect("route_registry must not be shared during build")
871 .add_route(path, handler);
872 self
873 }
874
875 pub fn server_config(mut self, config: ServerConfig) -> Self {
877 self.server_config = config;
878 self
879 }
880
881 pub fn stream_config(mut self, config: StreamConfig) -> Self {
883 self.stream_config = config;
884 self
885 }
886
887 #[cfg(feature = "cors")]
891 pub fn cors(mut self, config: CorsConfig) -> Self {
892 self.cors_config = Some(config);
893 self
894 }
895
896 #[cfg(feature = "cors")]
898 pub fn cors_allow_all_origins(mut self) -> Self {
899 self.cors_config = Some(CorsConfig::allow_all());
900 self
901 }
902
903 #[cfg(feature = "cors")]
905 pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
906 self.cors_config = Some(CorsConfig::for_origins(origins));
907 self
908 }
909
910 #[cfg(feature = "cors")]
917 pub fn cors_from_env(mut self) -> Self {
918 self.cors_config = Some(CorsConfig::from_env());
919 self
920 }
921
922 #[cfg(feature = "cors")]
924 pub fn cors_disabled(self) -> Self {
925 self
927 }
928
929 #[cfg(all(feature = "dynamodb", feature = "cors"))]
935 pub async fn production_config(self) -> Result<Self> {
936 Ok(self.dynamodb_storage().await?.cors_from_env())
937 }
938
939 #[cfg(feature = "cors")]
943 pub fn development_config(self) -> Self {
944 use turul_mcp_session_storage::InMemorySessionStorage;
945
946 self.storage(Arc::new(InMemorySessionStorage::new()))
947 .cors_allow_all_origins()
948 }
949
950 pub async fn build(self) -> Result<LambdaMcpServer> {
954 use turul_mcp_session_storage::InMemorySessionStorage;
955
956 if self.name.is_empty() {
958 return Err(crate::error::LambdaError::Configuration(
959 "Server name cannot be empty".to_string(),
960 ));
961 }
962 if self.version.is_empty() {
963 return Err(crate::error::LambdaError::Configuration(
964 "Server version cannot be empty".to_string(),
965 ));
966 }
967
968 let session_storage = self
977 .session_storage
978 .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
979
980 let mut implementation = Implementation::new(&self.name, &self.version);
982 if let Some(title) = self.title {
983 implementation = implementation.with_title(title);
984 }
985 if let Some(icons) = self.icons {
986 implementation = implementation.with_icons(icons);
987 }
988
989 let mut capabilities = self.capabilities.clone();
991 let has_tools = !self.tools.is_empty();
992 let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
993 let has_prompts = !self.prompts.is_empty();
994 let has_elicitations = !self.elicitations.is_empty();
995 let has_completions = !self.completions.is_empty();
996 let has_logging = !self.loggers.is_empty();
997 tracing::debug!("🔧 Has logging configured: {}", has_logging);
998
999 if has_tools {
1001 let list_changed = !matches!(
1002 self.tool_change_mode,
1003 turul_mcp_server::ToolChangeMode::Static
1004 );
1005 capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
1006 list_changed: Some(list_changed),
1007 });
1008 }
1009
1010 if has_resources {
1012 capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
1013 subscribe: Some(false), list_changed: Some(false), });
1016 }
1017
1018 if has_prompts {
1020 capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
1021 list_changed: Some(false), });
1023 }
1024
1025 let _ = has_elicitations; if has_completions {
1031 capabilities.completions =
1032 Some(turul_mcp_protocol::initialize::CompletionsCapabilities {
1033 enabled: Some(true),
1034 });
1035 }
1036
1037 capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities {
1040 enabled: Some(true),
1041 levels: Some(vec![
1042 "debug".to_string(),
1043 "info".to_string(),
1044 "warning".to_string(),
1045 "error".to_string(),
1046 ]),
1047 });
1048
1049 if self.task_runtime.is_some() {
1051 use turul_mcp_protocol::initialize::*;
1052 capabilities.tasks = Some(TasksCapabilities {
1053 list: Some(TasksListCapabilities::default()),
1054 cancel: Some(TasksCancelCapabilities::default()),
1055 requests: Some(TasksRequestCapabilities {
1056 tools: Some(TasksToolCapabilities {
1057 call: Some(TasksToolCallCapabilities::default()),
1058 extra: Default::default(),
1059 }),
1060 extra: Default::default(),
1061 }),
1062 extra: Default::default(),
1063 });
1064 }
1065
1066 let mut handlers = self.handlers;
1068 if !self.roots.is_empty() {
1069 let mut roots_handler = RootsHandler::new();
1070 for root in &self.roots {
1071 roots_handler = roots_handler.add_root(root.clone());
1072 }
1073 handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1074 }
1075
1076 if let Some(ref runtime) = self.task_runtime {
1078 use turul_mcp_server::{
1079 TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1080 };
1081 handlers.insert(
1082 "tasks/get".to_string(),
1083 Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1084 );
1085 handlers.insert(
1086 "tasks/list".to_string(),
1087 Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1088 );
1089 handlers.insert(
1090 "tasks/cancel".to_string(),
1091 Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1092 );
1093 handlers.insert(
1094 "tasks/result".to_string(),
1095 Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1096 );
1097 }
1098
1099 if has_resources {
1101 let mut list_handler = ResourcesHandler::new();
1103 for resource in self.resources.values() {
1104 list_handler = list_handler.add_resource_arc(resource.clone());
1105 }
1106 handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1107
1108 if !self.template_resources.is_empty() {
1110 let templates_handler =
1111 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1112 handlers.insert(
1113 "resources/templates/list".to_string(),
1114 Arc::new(templates_handler),
1115 );
1116 }
1117
1118 let mut read_handler = ResourcesReadHandler::new().without_security();
1120 for resource in self.resources.values() {
1121 read_handler = read_handler.add_resource_arc(resource.clone());
1122 }
1123 for (template, resource) in &self.template_resources {
1124 read_handler =
1125 read_handler.add_template_resource_arc(template.clone(), resource.clone());
1126 }
1127 handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1128 }
1129
1130 let tool_fingerprint = turul_mcp_server::compute_tool_fingerprint(&self.tools);
1132
1133 Ok(LambdaMcpServer::new(
1135 implementation,
1136 capabilities,
1137 self.tools,
1138 self.resources,
1139 self.prompts,
1140 self.elicitations,
1141 self.sampling,
1142 self.completions,
1143 self.loggers,
1144 self.root_providers,
1145 self.notifications,
1146 handlers,
1147 self.roots,
1148 self.instructions,
1149 session_storage,
1150 self.strict_lifecycle,
1151 self.server_config,
1152 self.enable_sse,
1153 self.stream_config,
1154 #[cfg(feature = "cors")]
1155 self.cors_config,
1156 self.middleware_stack,
1157 self.route_registry,
1158 self.task_runtime,
1159 tool_fingerprint,
1160 #[cfg(feature = "dynamic-tools")]
1161 !matches!(self.tool_change_mode, turul_mcp_server::ToolChangeMode::Static),
1162 #[cfg(feature = "dynamic-tools")]
1163 self.server_state_storage,
1164 ))
1165 }
1166}
1167
1168impl Default for LambdaMcpServerBuilder {
1169 fn default() -> Self {
1170 Self::new()
1171 }
1172}
1173
1174pub trait LambdaMcpServerBuilderExt {
1176 fn tools<I, T>(self, tools: I) -> Self
1178 where
1179 I: IntoIterator<Item = T>,
1180 T: McpTool + 'static;
1181}
1182
1183impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1184 fn tools<I, T>(mut self, tools: I) -> Self
1185 where
1186 I: IntoIterator<Item = T>,
1187 T: McpTool + 'static,
1188 {
1189 for tool in tools {
1190 self = self.tool(tool);
1191 }
1192 self
1193 }
1194}
1195
1196pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1201where
1202 I: IntoIterator<Item = T>,
1203 T: McpTool + 'static,
1204{
1205 let mut builder = LambdaMcpServerBuilder::new();
1206
1207 for tool in tools {
1208 builder = builder.tool(tool);
1209 }
1210
1211 #[cfg(feature = "cors")]
1212 {
1213 builder = builder.cors_allow_all_origins();
1214 }
1215
1216 builder.sse(false).build().await
1217}
1218
1219#[cfg(all(feature = "dynamodb", feature = "cors"))]
1223pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1224where
1225 I: IntoIterator<Item = T>,
1226 T: McpTool + 'static,
1227{
1228 let mut builder = LambdaMcpServerBuilder::new();
1229
1230 for tool in tools {
1231 builder = builder.tool(tool);
1232 }
1233
1234 builder.production_config().await?.build().await
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239 use super::*;
1240 use turul_mcp_builders::prelude::*;
1241 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1245 struct TestTool;
1246
1247 impl HasBaseMetadata for TestTool {
1248 fn name(&self) -> &str {
1249 "test_tool"
1250 }
1251 }
1252
1253 impl HasDescription for TestTool {
1254 fn description(&self) -> Option<&str> {
1255 Some("Test tool")
1256 }
1257 }
1258
1259 impl HasInputSchema for TestTool {
1260 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1261 use turul_mcp_protocol::ToolSchema;
1262 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1263 SCHEMA.get_or_init(ToolSchema::object)
1264 }
1265 }
1266
1267 impl HasOutputSchema for TestTool {
1268 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1269 None
1270 }
1271 }
1272
1273 impl HasAnnotations for TestTool {
1274 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1275 None
1276 }
1277 }
1278
1279 impl HasToolMeta for TestTool {
1280 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1281 None
1282 }
1283 }
1284
1285 impl HasIcons for TestTool {}
1286 impl HasExecution for TestTool {}
1287
1288 #[async_trait::async_trait]
1289 impl McpTool for TestTool {
1290 async fn call(
1291 &self,
1292 _args: serde_json::Value,
1293 _session: Option<turul_mcp_server::SessionContext>,
1294 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1295 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1296 Ok(CallToolResult::success(vec![ToolResult::text(
1297 "test result",
1298 )]))
1299 }
1300 }
1301
1302 #[tokio::test]
1303 async fn test_builder_basic() {
1304 let server = LambdaMcpServerBuilder::new()
1305 .name("test-server")
1306 .version("1.0.0")
1307 .tool(TestTool)
1308 .storage(Arc::new(InMemorySessionStorage::new()))
1309 .sse(false) .build()
1311 .await
1312 .unwrap();
1313
1314 let handler = server.handler().await.unwrap();
1316 assert!(
1318 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1319 "Stream manager must be initialized"
1320 );
1321 }
1322
1323 #[tokio::test]
1324 async fn test_simple_lambda_server() {
1325 let tools = vec![TestTool];
1326 let server = simple_lambda_server(tools).await.unwrap();
1327
1328 let handler = server.handler().await.unwrap();
1330 assert!(
1333 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1334 "Stream manager must be initialized"
1335 );
1336 }
1337
1338 #[tokio::test]
1339 async fn test_builder_extension_trait() {
1340 let tools = vec![TestTool, TestTool];
1341
1342 let server = LambdaMcpServerBuilder::new()
1343 .tools(tools)
1344 .storage(Arc::new(InMemorySessionStorage::new()))
1345 .sse(false) .build()
1347 .await
1348 .unwrap();
1349
1350 let handler = server.handler().await.unwrap();
1351 assert!(
1354 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1355 "Stream manager must be initialized"
1356 );
1357 }
1358
1359 #[cfg(feature = "cors")]
1360 #[tokio::test]
1361 async fn test_cors_configuration() {
1362 let server = LambdaMcpServerBuilder::new()
1363 .cors_allow_all_origins()
1364 .storage(Arc::new(InMemorySessionStorage::new()))
1365 .sse(false) .build()
1367 .await
1368 .unwrap();
1369
1370 let handler = server.handler().await.unwrap();
1371 assert!(
1374 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1375 "Stream manager must be initialized"
1376 );
1377 }
1378
1379 #[tokio::test]
1380 async fn test_sse_toggle_functionality() {
1381 let mut builder =
1383 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1384
1385 builder = builder.sse(true);
1387 assert!(builder.enable_sse, "SSE should be enabled");
1388 assert!(
1389 builder.server_config.enable_get_sse,
1390 "GET SSE endpoint should be enabled"
1391 );
1392 assert!(
1393 builder.server_config.enable_post_sse,
1394 "POST SSE endpoint should be enabled"
1395 );
1396
1397 builder = builder.sse(false);
1399 assert!(!builder.enable_sse, "SSE should be disabled");
1400 assert!(
1401 !builder.server_config.enable_get_sse,
1402 "GET SSE endpoint should be disabled"
1403 );
1404 assert!(
1405 !builder.server_config.enable_post_sse,
1406 "POST SSE endpoint should be disabled"
1407 );
1408
1409 builder = builder.sse(true);
1411 assert!(builder.enable_sse, "SSE should be re-enabled");
1412 assert!(
1413 builder.server_config.enable_get_sse,
1414 "GET SSE endpoint should be re-enabled"
1415 );
1416 assert!(
1417 builder.server_config.enable_post_sse,
1418 "POST SSE endpoint should be re-enabled"
1419 );
1420
1421 let server = builder.build().await.unwrap();
1423 let handler = server.handler().await.unwrap();
1424 assert!(
1425 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1426 "Stream manager must be initialized"
1427 );
1428 }
1429
1430 #[tokio::test]
1435 async fn test_builder_without_tasks_no_capability() {
1436 let server = LambdaMcpServerBuilder::new()
1437 .name("no-tasks")
1438 .tool(TestTool)
1439 .storage(Arc::new(InMemorySessionStorage::new()))
1440 .sse(false)
1441 .build()
1442 .await
1443 .unwrap();
1444
1445 assert!(
1446 server.capabilities().tasks.is_none(),
1447 "Tasks capability should not be advertised without task storage"
1448 );
1449 }
1450
1451 #[tokio::test]
1452 async fn test_builder_with_task_storage_advertises_capability() {
1453 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1454
1455 let server = LambdaMcpServerBuilder::new()
1456 .name("with-tasks")
1457 .tool(TestTool)
1458 .storage(Arc::new(InMemorySessionStorage::new()))
1459 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1460 .sse(false)
1461 .build()
1462 .await
1463 .unwrap();
1464
1465 let tasks_cap = server
1466 .capabilities()
1467 .tasks
1468 .as_ref()
1469 .expect("Tasks capability should be advertised");
1470 assert!(tasks_cap.list.is_some(), "list capability should be set");
1471 assert!(
1472 tasks_cap.cancel.is_some(),
1473 "cancel capability should be set"
1474 );
1475 let requests = tasks_cap
1476 .requests
1477 .as_ref()
1478 .expect("requests capability should be set");
1479 let tools = requests
1480 .tools
1481 .as_ref()
1482 .expect("tools capability should be set");
1483 assert!(tools.call.is_some(), "tools.call capability should be set");
1484 }
1485
1486 #[tokio::test]
1487 async fn test_builder_with_task_runtime_advertises_capability() {
1488 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1489
1490 let server = LambdaMcpServerBuilder::new()
1491 .name("with-runtime")
1492 .tool(TestTool)
1493 .storage(Arc::new(InMemorySessionStorage::new()))
1494 .with_task_runtime(runtime)
1495 .sse(false)
1496 .build()
1497 .await
1498 .unwrap();
1499
1500 assert!(
1501 server.capabilities().tasks.is_some(),
1502 "Tasks capability should be advertised with task runtime"
1503 );
1504 }
1505
1506 #[tokio::test]
1507 async fn test_task_recovery_timeout_configuration() {
1508 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1509
1510 let server = LambdaMcpServerBuilder::new()
1511 .name("custom-timeout")
1512 .tool(TestTool)
1513 .storage(Arc::new(InMemorySessionStorage::new()))
1514 .task_recovery_timeout_ms(60_000)
1515 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1516 .sse(false)
1517 .build()
1518 .await
1519 .unwrap();
1520
1521 assert!(
1522 server.capabilities().tasks.is_some(),
1523 "Tasks should be enabled with custom timeout"
1524 );
1525 }
1526
1527 #[tokio::test]
1528 async fn test_backward_compatibility_no_tasks() {
1529 let server = LambdaMcpServerBuilder::new()
1531 .name("backward-compat")
1532 .version("1.0.0")
1533 .tool(TestTool)
1534 .storage(Arc::new(InMemorySessionStorage::new()))
1535 .sse(false)
1536 .build()
1537 .await
1538 .unwrap();
1539
1540 let handler = server.handler().await.unwrap();
1541 assert!(
1542 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1543 "Stream manager must be initialized"
1544 );
1545 assert!(server.capabilities().tasks.is_none());
1546 }
1547
1548 #[derive(Clone, Default)]
1550 struct SlowTool;
1551
1552 impl HasBaseMetadata for SlowTool {
1553 fn name(&self) -> &str {
1554 "slow_tool"
1555 }
1556 }
1557
1558 impl HasDescription for SlowTool {
1559 fn description(&self) -> Option<&str> {
1560 Some("A slow tool for testing")
1561 }
1562 }
1563
1564 impl HasInputSchema for SlowTool {
1565 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1566 use turul_mcp_protocol::ToolSchema;
1567 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1568 SCHEMA.get_or_init(ToolSchema::object)
1569 }
1570 }
1571
1572 impl HasOutputSchema for SlowTool {
1573 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1574 None
1575 }
1576 }
1577
1578 impl HasAnnotations for SlowTool {
1579 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1580 None
1581 }
1582 }
1583
1584 impl HasToolMeta for SlowTool {
1585 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1586 None
1587 }
1588 }
1589
1590 impl HasIcons for SlowTool {}
1591 impl HasExecution for SlowTool {
1592 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1593 Some(turul_mcp_protocol::tools::ToolExecution {
1594 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1595 })
1596 }
1597 }
1598
1599 #[async_trait::async_trait]
1600 impl McpTool for SlowTool {
1601 async fn call(
1602 &self,
1603 _args: serde_json::Value,
1604 _session: Option<turul_mcp_server::SessionContext>,
1605 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1606 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1607 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1609 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1610 }
1611 }
1612
1613 #[tokio::test]
1614 async fn test_nonblocking_tools_call_with_task() {
1615 use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1616 use turul_mcp_server::SessionAwareToolHandler;
1617 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1618
1619 let task_storage = Arc::new(InMemoryTaskStorage::new());
1620 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1621 task_storage,
1622 ));
1623
1624 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1626 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1627
1628 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1630 Arc::new(InMemorySessionStorage::new());
1631 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1632 session_storage,
1633 turul_mcp_protocol::ServerCapabilities::default(),
1634 ));
1635
1636 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1638 .with_task_runtime(Arc::clone(&runtime));
1639
1640 let params = serde_json::json!({
1642 "name": "slow_tool",
1643 "arguments": {},
1644 "task": {}
1645 });
1646 let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1647 params
1648 .as_object()
1649 .unwrap()
1650 .iter()
1651 .map(|(k, v)| (k.clone(), v.clone()))
1652 .collect(),
1653 );
1654
1655 let start = std::time::Instant::now();
1657 let result = tool_handler
1658 .handle("tools/call", Some(request_params), None)
1659 .await;
1660 let elapsed = start.elapsed();
1661
1662 let value = result.expect("tools/call with task should succeed");
1664 assert!(
1665 value.get("task").is_some(),
1666 "Response should contain 'task' field (CreateTaskResult shape)"
1667 );
1668 let task = value.get("task").unwrap();
1669 assert!(
1670 task.get("taskId").is_some(),
1671 "Task should have taskId field"
1672 );
1673 assert_eq!(
1674 task.get("status")
1675 .and_then(|v| v.as_str())
1676 .unwrap_or_default(),
1677 "working",
1678 "Task status should be 'working'"
1679 );
1680
1681 assert!(
1685 elapsed < std::time::Duration::from_secs(1),
1686 "tools/call with task should return immediately (took {:?}, expected < 1s)",
1687 elapsed
1688 );
1689 }
1690
1691 #[derive(Clone)]
1697 struct StaticTestResource;
1698
1699 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1700 fn name(&self) -> &str {
1701 "static_test"
1702 }
1703 }
1704
1705 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1706 fn description(&self) -> Option<&str> {
1707 Some("Static test resource")
1708 }
1709 }
1710
1711 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1712 fn uri(&self) -> &str {
1713 "file:///test.txt"
1714 }
1715 }
1716
1717 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1718 fn mime_type(&self) -> Option<&str> {
1719 Some("text/plain")
1720 }
1721 }
1722
1723 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1724 fn size(&self) -> Option<u64> {
1725 None
1726 }
1727 }
1728
1729 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1730 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1731 None
1732 }
1733 }
1734
1735 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1736 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1737 None
1738 }
1739 }
1740
1741 impl HasIcons for StaticTestResource {}
1742
1743 #[async_trait::async_trait]
1744 impl McpResource for StaticTestResource {
1745 async fn read(
1746 &self,
1747 _params: Option<serde_json::Value>,
1748 _session: Option<&turul_mcp_server::SessionContext>,
1749 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1750 {
1751 use turul_mcp_protocol::resources::ResourceContent;
1752 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1753 }
1754 }
1755
1756 #[derive(Clone)]
1758 struct TemplateTestResource;
1759
1760 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1761 fn name(&self) -> &str {
1762 "template_test"
1763 }
1764 }
1765
1766 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1767 fn description(&self) -> Option<&str> {
1768 Some("Template test resource")
1769 }
1770 }
1771
1772 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1773 fn uri(&self) -> &str {
1774 "agent://agents/{agent_id}"
1775 }
1776 }
1777
1778 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1779 fn mime_type(&self) -> Option<&str> {
1780 Some("application/json")
1781 }
1782 }
1783
1784 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1785 fn size(&self) -> Option<u64> {
1786 None
1787 }
1788 }
1789
1790 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1791 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1792 None
1793 }
1794 }
1795
1796 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1797 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1798 None
1799 }
1800 }
1801
1802 impl HasIcons for TemplateTestResource {}
1803
1804 #[async_trait::async_trait]
1805 impl McpResource for TemplateTestResource {
1806 async fn read(
1807 &self,
1808 _params: Option<serde_json::Value>,
1809 _session: Option<&turul_mcp_server::SessionContext>,
1810 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1811 {
1812 use turul_mcp_protocol::resources::ResourceContent;
1813 Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
1814 }
1815 }
1816
1817 #[test]
1818 fn test_resource_auto_detection_static() {
1819 let builder = LambdaMcpServerBuilder::new()
1820 .name("test")
1821 .resource(StaticTestResource);
1822
1823 assert_eq!(builder.resources.len(), 1);
1824 assert!(builder.resources.contains_key("file:///test.txt"));
1825 assert_eq!(builder.template_resources.len(), 0);
1826 }
1827
1828 #[test]
1829 fn test_resource_auto_detection_template() {
1830 let builder = LambdaMcpServerBuilder::new()
1831 .name("test")
1832 .resource(TemplateTestResource);
1833
1834 assert_eq!(builder.resources.len(), 0);
1835 assert_eq!(builder.template_resources.len(), 1);
1836
1837 let (template, _) = &builder.template_resources[0];
1838 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1839 }
1840
1841 #[test]
1842 fn test_resource_auto_detection_mixed() {
1843 let builder = LambdaMcpServerBuilder::new()
1844 .name("test")
1845 .resource(StaticTestResource)
1846 .resource(TemplateTestResource);
1847
1848 assert_eq!(builder.resources.len(), 1);
1849 assert!(builder.resources.contains_key("file:///test.txt"));
1850 assert_eq!(builder.template_resources.len(), 1);
1851
1852 let (template, _) = &builder.template_resources[0];
1853 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1854 }
1855
1856 #[tokio::test]
1857 async fn test_build_advertises_resources_capability_for_templates_only() {
1858 let server = LambdaMcpServerBuilder::new()
1859 .name("template-only")
1860 .resource(TemplateTestResource)
1861 .storage(Arc::new(InMemorySessionStorage::new()))
1862 .sse(false)
1863 .build()
1864 .await
1865 .unwrap();
1866
1867 assert!(
1868 server.capabilities().resources.is_some(),
1869 "Resources capability should be advertised when template resources are registered"
1870 );
1871 }
1872
1873 #[tokio::test]
1874 async fn test_build_advertises_resources_capability_for_static_only() {
1875 let server = LambdaMcpServerBuilder::new()
1876 .name("static-only")
1877 .resource(StaticTestResource)
1878 .storage(Arc::new(InMemorySessionStorage::new()))
1879 .sse(false)
1880 .build()
1881 .await
1882 .unwrap();
1883
1884 assert!(
1885 server.capabilities().resources.is_some(),
1886 "Resources capability should be advertised when static resources are registered"
1887 );
1888 }
1889
1890 #[tokio::test]
1891 async fn test_build_no_resources_no_capability() {
1892 let server = LambdaMcpServerBuilder::new()
1893 .name("no-resources")
1894 .tool(TestTool)
1895 .storage(Arc::new(InMemorySessionStorage::new()))
1896 .sse(false)
1897 .build()
1898 .await
1899 .unwrap();
1900
1901 assert!(
1902 server.capabilities().resources.is_none(),
1903 "Resources capability should NOT be advertised when no resources are registered"
1904 );
1905 }
1906
1907 #[tokio::test]
1908 async fn test_lambda_builder_templates_list_returns_template() {
1909 use turul_mcp_server::handlers::McpHandler;
1910
1911 let builder = LambdaMcpServerBuilder::new()
1913 .name("template-test")
1914 .resource(TemplateTestResource);
1915
1916 assert_eq!(builder.template_resources.len(), 1);
1918
1919 let handler =
1921 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1922
1923 let result = handler.handle(None).await.expect("should succeed");
1925
1926 let templates = result["resourceTemplates"]
1927 .as_array()
1928 .expect("resourceTemplates should be an array");
1929 assert_eq!(
1930 templates.len(),
1931 1,
1932 "Should have exactly 1 template resource"
1933 );
1934 assert_eq!(
1935 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1936 "Template URI should match"
1937 );
1938 assert_eq!(templates[0]["name"], "template_test");
1939 }
1940
1941 #[tokio::test]
1942 async fn test_lambda_builder_resources_list_returns_static() {
1943 use turul_mcp_server::handlers::McpHandler;
1944
1945 let builder = LambdaMcpServerBuilder::new()
1947 .name("static-test")
1948 .resource(StaticTestResource);
1949
1950 assert_eq!(builder.resources.len(), 1);
1951
1952 let mut handler = ResourcesHandler::new();
1953 for resource in builder.resources.values() {
1954 handler = handler.add_resource_arc(resource.clone());
1955 }
1956
1957 let result = handler.handle(None).await.expect("should succeed");
1958
1959 let resources = result["resources"]
1960 .as_array()
1961 .expect("resources should be an array");
1962 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1963 assert_eq!(resources[0]["uri"], "file:///test.txt");
1964 assert_eq!(resources[0]["name"], "static_test");
1965 }
1966
1967 #[tokio::test]
1968 async fn test_lambda_builder_mixed_resources_separation() {
1969 use turul_mcp_server::handlers::McpHandler;
1970
1971 let builder = LambdaMcpServerBuilder::new()
1973 .name("mixed-test")
1974 .resource(StaticTestResource)
1975 .resource(TemplateTestResource);
1976
1977 assert_eq!(builder.resources.len(), 1);
1978 assert_eq!(builder.template_resources.len(), 1);
1979
1980 let mut list_handler = ResourcesHandler::new();
1982 for resource in builder.resources.values() {
1983 list_handler = list_handler.add_resource_arc(resource.clone());
1984 }
1985
1986 let templates_handler =
1987 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1988
1989 let list_result = list_handler.handle(None).await.expect("should succeed");
1991 let resources = list_result["resources"]
1992 .as_array()
1993 .expect("resources should be an array");
1994 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1995 assert_eq!(resources[0]["uri"], "file:///test.txt");
1996
1997 let templates_result = templates_handler
1999 .handle(None)
2000 .await
2001 .expect("should succeed");
2002 let templates = templates_result["resourceTemplates"]
2003 .as_array()
2004 .expect("resourceTemplates should be an array");
2005 assert_eq!(
2006 templates.len(),
2007 1,
2008 "Only template resource in resources/templates/list"
2009 );
2010 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
2011 }
2012
2013 #[tokio::test]
2014 async fn test_tasks_get_route_registered() {
2015 use turul_mcp_server::TasksGetHandler;
2016 use turul_mcp_server::handlers::McpHandler;
2017 use turul_mcp_server::task_storage::InMemoryTaskStorage;
2018
2019 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
2020 Arc::new(InMemoryTaskStorage::new()),
2021 ));
2022 let handler = TasksGetHandler::new(runtime);
2023
2024 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
2027
2028 let result = handler.handle(Some(params)).await;
2029
2030 assert!(
2032 result.is_err(),
2033 "tasks/get with unknown task should return error"
2034 );
2035 let err = result.unwrap_err();
2036 let err_str = err.to_string();
2037 assert!(
2038 !err_str.contains("method not found"),
2039 "Error should not be 'method not found' — handler should respond to tasks/get"
2040 );
2041 }
2042
2043 #[tokio::test]
2050 async fn test_resources_read_registered_by_default() {
2051 use lambda_http::Body as LambdaBody;
2052
2053 let server = LambdaMcpServerBuilder::new()
2054 .name("parity-test")
2055 .version("1.0.0")
2056 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2058 .strict_lifecycle(false) .sse(false)
2060 .build()
2061 .await
2062 .unwrap();
2063
2064 let handler = server.handler().await.unwrap();
2065
2066 let init_req = http::Request::builder()
2068 .method("POST")
2069 .uri("/mcp")
2070 .header("Content-Type", "application/json")
2071 .header("MCP-Protocol-Version", "2025-11-25")
2072 .body(LambdaBody::Text(
2073 serde_json::json!({
2074 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2075 "params": {
2076 "protocolVersion": "2025-11-25",
2077 "capabilities": {},
2078 "clientInfo": { "name": "test", "version": "1.0.0" }
2079 }
2080 })
2081 .to_string(),
2082 ))
2083 .unwrap();
2084 let init_resp = handler.handle(init_req).await.unwrap();
2085 let session_id = init_resp
2086 .headers()
2087 .get("Mcp-Session-Id")
2088 .unwrap()
2089 .to_str()
2090 .unwrap()
2091 .to_string();
2092
2093 let read_req = http::Request::builder()
2096 .method("POST")
2097 .uri("/mcp")
2098 .header("Content-Type", "application/json")
2099 .header("MCP-Protocol-Version", "2025-11-25")
2100 .header("Mcp-Session-Id", &session_id)
2101 .body(LambdaBody::Text(
2102 serde_json::json!({
2103 "jsonrpc": "2.0", "method": "resources/read", "id": 2,
2104 "params": { "uri": "file:///nonexistent" }
2105 })
2106 .to_string(),
2107 ))
2108 .unwrap();
2109 let read_resp = handler.handle(read_req).await.unwrap();
2110 let body = String::from_utf8_lossy(read_resp.body().as_ref()).to_string();
2111 let json: serde_json::Value = serde_json::from_str(&body)
2112 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2113
2114 assert!(
2116 json["error"].is_object(),
2117 "resources/read must return JSON-RPC error, got: {json}"
2118 );
2119 let error_code = json["error"]["code"].as_i64().unwrap();
2123 assert_ne!(
2124 error_code, -32601,
2125 "resources/read must be registered (got method-not-found -32601): {json}"
2126 );
2127 }
2128
2129 #[tokio::test]
2133 async fn test_resources_templates_list_absent_without_templates() {
2134 use lambda_http::Body as LambdaBody;
2135
2136 let server = LambdaMcpServerBuilder::new()
2137 .name("parity-test")
2138 .version("1.0.0")
2139 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2141 .strict_lifecycle(false) .sse(false)
2143 .build()
2144 .await
2145 .unwrap();
2146
2147 let handler = server.handler().await.unwrap();
2148
2149 let init_req = http::Request::builder()
2151 .method("POST")
2152 .uri("/mcp")
2153 .header("Content-Type", "application/json")
2154 .header("MCP-Protocol-Version", "2025-11-25")
2155 .body(LambdaBody::Text(
2156 serde_json::json!({
2157 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2158 "params": {
2159 "protocolVersion": "2025-11-25",
2160 "capabilities": {},
2161 "clientInfo": { "name": "test", "version": "1.0.0" }
2162 }
2163 })
2164 .to_string(),
2165 ))
2166 .unwrap();
2167 let init_resp = handler.handle(init_req).await.unwrap();
2168 let session_id = init_resp
2169 .headers()
2170 .get("Mcp-Session-Id")
2171 .unwrap()
2172 .to_str()
2173 .unwrap()
2174 .to_string();
2175
2176 let tmpl_req = http::Request::builder()
2179 .method("POST")
2180 .uri("/mcp")
2181 .header("Content-Type", "application/json")
2182 .header("MCP-Protocol-Version", "2025-11-25")
2183 .header("Mcp-Session-Id", &session_id)
2184 .body(LambdaBody::Text(
2185 serde_json::json!({
2186 "jsonrpc": "2.0", "method": "resources/templates/list", "id": 2
2187 })
2188 .to_string(),
2189 ))
2190 .unwrap();
2191 let tmpl_resp = handler.handle(tmpl_req).await.unwrap();
2192 let body = String::from_utf8_lossy(tmpl_resp.body().as_ref()).to_string();
2193 let json: serde_json::Value = serde_json::from_str(&body)
2194 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2195
2196 assert!(
2198 json["error"].is_object(),
2199 "resources/templates/list should return error without templates: {json}"
2200 );
2201 assert_eq!(
2202 json["error"]["code"].as_i64().unwrap(),
2203 -32601,
2204 "resources/templates/list must be method-not-found (-32601) without templates: {json}"
2205 );
2206 }
2207}