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!(
1162 self.tool_change_mode,
1163 turul_mcp_server::ToolChangeMode::Static
1164 ),
1165 #[cfg(feature = "dynamic-tools")]
1166 self.server_state_storage,
1167 ))
1168 }
1169}
1170
1171impl Default for LambdaMcpServerBuilder {
1172 fn default() -> Self {
1173 Self::new()
1174 }
1175}
1176
1177pub trait LambdaMcpServerBuilderExt {
1179 fn tools<I, T>(self, tools: I) -> Self
1181 where
1182 I: IntoIterator<Item = T>,
1183 T: McpTool + 'static;
1184}
1185
1186impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1187 fn tools<I, T>(mut self, tools: I) -> Self
1188 where
1189 I: IntoIterator<Item = T>,
1190 T: McpTool + 'static,
1191 {
1192 for tool in tools {
1193 self = self.tool(tool);
1194 }
1195 self
1196 }
1197}
1198
1199pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1204where
1205 I: IntoIterator<Item = T>,
1206 T: McpTool + 'static,
1207{
1208 let mut builder = LambdaMcpServerBuilder::new();
1209
1210 for tool in tools {
1211 builder = builder.tool(tool);
1212 }
1213
1214 #[cfg(feature = "cors")]
1215 {
1216 builder = builder.cors_allow_all_origins();
1217 }
1218
1219 builder.sse(false).build().await
1220}
1221
1222#[cfg(all(feature = "dynamodb", feature = "cors"))]
1226pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1227where
1228 I: IntoIterator<Item = T>,
1229 T: McpTool + 'static,
1230{
1231 let mut builder = LambdaMcpServerBuilder::new();
1232
1233 for tool in tools {
1234 builder = builder.tool(tool);
1235 }
1236
1237 builder.production_config().await?.build().await
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243 use turul_mcp_builders::prelude::*;
1244 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1248 struct TestTool;
1249
1250 impl HasBaseMetadata for TestTool {
1251 fn name(&self) -> &str {
1252 "test_tool"
1253 }
1254 }
1255
1256 impl HasDescription for TestTool {
1257 fn description(&self) -> Option<&str> {
1258 Some("Test tool")
1259 }
1260 }
1261
1262 impl HasInputSchema for TestTool {
1263 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1264 use turul_mcp_protocol::ToolSchema;
1265 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1266 SCHEMA.get_or_init(ToolSchema::object)
1267 }
1268 }
1269
1270 impl HasOutputSchema for TestTool {
1271 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1272 None
1273 }
1274 }
1275
1276 impl HasAnnotations for TestTool {
1277 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1278 None
1279 }
1280 }
1281
1282 impl HasToolMeta for TestTool {
1283 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1284 None
1285 }
1286 }
1287
1288 impl HasIcons for TestTool {}
1289 impl HasExecution for TestTool {}
1290
1291 #[async_trait::async_trait]
1292 impl McpTool for TestTool {
1293 async fn call(
1294 &self,
1295 _args: serde_json::Value,
1296 _session: Option<turul_mcp_server::SessionContext>,
1297 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1298 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1299 Ok(CallToolResult::success(vec![ToolResult::text(
1300 "test result",
1301 )]))
1302 }
1303 }
1304
1305 #[tokio::test]
1306 async fn test_builder_basic() {
1307 let server = LambdaMcpServerBuilder::new()
1308 .name("test-server")
1309 .version("1.0.0")
1310 .tool(TestTool)
1311 .storage(Arc::new(InMemorySessionStorage::new()))
1312 .sse(false) .build()
1314 .await
1315 .unwrap();
1316
1317 let handler = server.handler().await.unwrap();
1319 assert!(
1321 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1322 "Stream manager must be initialized"
1323 );
1324 }
1325
1326 #[tokio::test]
1327 async fn test_simple_lambda_server() {
1328 let tools = vec![TestTool];
1329 let server = simple_lambda_server(tools).await.unwrap();
1330
1331 let handler = server.handler().await.unwrap();
1333 assert!(
1336 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1337 "Stream manager must be initialized"
1338 );
1339 }
1340
1341 #[tokio::test]
1342 async fn test_builder_extension_trait() {
1343 let tools = vec![TestTool, TestTool];
1344
1345 let server = LambdaMcpServerBuilder::new()
1346 .tools(tools)
1347 .storage(Arc::new(InMemorySessionStorage::new()))
1348 .sse(false) .build()
1350 .await
1351 .unwrap();
1352
1353 let handler = server.handler().await.unwrap();
1354 assert!(
1357 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1358 "Stream manager must be initialized"
1359 );
1360 }
1361
1362 #[cfg(feature = "cors")]
1363 #[tokio::test]
1364 async fn test_cors_configuration() {
1365 let server = LambdaMcpServerBuilder::new()
1366 .cors_allow_all_origins()
1367 .storage(Arc::new(InMemorySessionStorage::new()))
1368 .sse(false) .build()
1370 .await
1371 .unwrap();
1372
1373 let handler = server.handler().await.unwrap();
1374 assert!(
1377 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1378 "Stream manager must be initialized"
1379 );
1380 }
1381
1382 #[tokio::test]
1383 async fn test_sse_toggle_functionality() {
1384 let mut builder =
1386 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1387
1388 builder = builder.sse(true);
1390 assert!(builder.enable_sse, "SSE should be enabled");
1391 assert!(
1392 builder.server_config.enable_get_sse,
1393 "GET SSE endpoint should be enabled"
1394 );
1395 assert!(
1396 builder.server_config.enable_post_sse,
1397 "POST SSE endpoint should be enabled"
1398 );
1399
1400 builder = builder.sse(false);
1402 assert!(!builder.enable_sse, "SSE should be disabled");
1403 assert!(
1404 !builder.server_config.enable_get_sse,
1405 "GET SSE endpoint should be disabled"
1406 );
1407 assert!(
1408 !builder.server_config.enable_post_sse,
1409 "POST SSE endpoint should be disabled"
1410 );
1411
1412 builder = builder.sse(true);
1414 assert!(builder.enable_sse, "SSE should be re-enabled");
1415 assert!(
1416 builder.server_config.enable_get_sse,
1417 "GET SSE endpoint should be re-enabled"
1418 );
1419 assert!(
1420 builder.server_config.enable_post_sse,
1421 "POST SSE endpoint should be re-enabled"
1422 );
1423
1424 let server = builder.build().await.unwrap();
1426 let handler = server.handler().await.unwrap();
1427 assert!(
1428 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1429 "Stream manager must be initialized"
1430 );
1431 }
1432
1433 #[tokio::test]
1438 async fn test_builder_without_tasks_no_capability() {
1439 let server = LambdaMcpServerBuilder::new()
1440 .name("no-tasks")
1441 .tool(TestTool)
1442 .storage(Arc::new(InMemorySessionStorage::new()))
1443 .sse(false)
1444 .build()
1445 .await
1446 .unwrap();
1447
1448 assert!(
1449 server.capabilities().tasks.is_none(),
1450 "Tasks capability should not be advertised without task storage"
1451 );
1452 }
1453
1454 #[tokio::test]
1455 async fn test_builder_with_task_storage_advertises_capability() {
1456 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1457
1458 let server = LambdaMcpServerBuilder::new()
1459 .name("with-tasks")
1460 .tool(TestTool)
1461 .storage(Arc::new(InMemorySessionStorage::new()))
1462 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1463 .sse(false)
1464 .build()
1465 .await
1466 .unwrap();
1467
1468 let tasks_cap = server
1469 .capabilities()
1470 .tasks
1471 .as_ref()
1472 .expect("Tasks capability should be advertised");
1473 assert!(tasks_cap.list.is_some(), "list capability should be set");
1474 assert!(
1475 tasks_cap.cancel.is_some(),
1476 "cancel capability should be set"
1477 );
1478 let requests = tasks_cap
1479 .requests
1480 .as_ref()
1481 .expect("requests capability should be set");
1482 let tools = requests
1483 .tools
1484 .as_ref()
1485 .expect("tools capability should be set");
1486 assert!(tools.call.is_some(), "tools.call capability should be set");
1487 }
1488
1489 #[tokio::test]
1490 async fn test_builder_with_task_runtime_advertises_capability() {
1491 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1492
1493 let server = LambdaMcpServerBuilder::new()
1494 .name("with-runtime")
1495 .tool(TestTool)
1496 .storage(Arc::new(InMemorySessionStorage::new()))
1497 .with_task_runtime(runtime)
1498 .sse(false)
1499 .build()
1500 .await
1501 .unwrap();
1502
1503 assert!(
1504 server.capabilities().tasks.is_some(),
1505 "Tasks capability should be advertised with task runtime"
1506 );
1507 }
1508
1509 #[tokio::test]
1510 async fn test_task_recovery_timeout_configuration() {
1511 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1512
1513 let server = LambdaMcpServerBuilder::new()
1514 .name("custom-timeout")
1515 .tool(TestTool)
1516 .storage(Arc::new(InMemorySessionStorage::new()))
1517 .task_recovery_timeout_ms(60_000)
1518 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1519 .sse(false)
1520 .build()
1521 .await
1522 .unwrap();
1523
1524 assert!(
1525 server.capabilities().tasks.is_some(),
1526 "Tasks should be enabled with custom timeout"
1527 );
1528 }
1529
1530 #[tokio::test]
1531 async fn test_backward_compatibility_no_tasks() {
1532 let server = LambdaMcpServerBuilder::new()
1534 .name("backward-compat")
1535 .version("1.0.0")
1536 .tool(TestTool)
1537 .storage(Arc::new(InMemorySessionStorage::new()))
1538 .sse(false)
1539 .build()
1540 .await
1541 .unwrap();
1542
1543 let handler = server.handler().await.unwrap();
1544 assert!(
1545 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1546 "Stream manager must be initialized"
1547 );
1548 assert!(server.capabilities().tasks.is_none());
1549 }
1550
1551 #[derive(Clone, Default)]
1553 struct SlowTool;
1554
1555 impl HasBaseMetadata for SlowTool {
1556 fn name(&self) -> &str {
1557 "slow_tool"
1558 }
1559 }
1560
1561 impl HasDescription for SlowTool {
1562 fn description(&self) -> Option<&str> {
1563 Some("A slow tool for testing")
1564 }
1565 }
1566
1567 impl HasInputSchema for SlowTool {
1568 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1569 use turul_mcp_protocol::ToolSchema;
1570 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1571 SCHEMA.get_or_init(ToolSchema::object)
1572 }
1573 }
1574
1575 impl HasOutputSchema for SlowTool {
1576 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1577 None
1578 }
1579 }
1580
1581 impl HasAnnotations for SlowTool {
1582 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1583 None
1584 }
1585 }
1586
1587 impl HasToolMeta for SlowTool {
1588 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1589 None
1590 }
1591 }
1592
1593 impl HasIcons for SlowTool {}
1594 impl HasExecution for SlowTool {
1595 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1596 Some(turul_mcp_protocol::tools::ToolExecution {
1597 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1598 })
1599 }
1600 }
1601
1602 #[async_trait::async_trait]
1603 impl McpTool for SlowTool {
1604 async fn call(
1605 &self,
1606 _args: serde_json::Value,
1607 _session: Option<turul_mcp_server::SessionContext>,
1608 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1609 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1610 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1612 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1613 }
1614 }
1615
1616 #[tokio::test]
1617 async fn test_nonblocking_tools_call_with_task() {
1618 use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1619 use turul_mcp_server::SessionAwareToolHandler;
1620 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1621
1622 let task_storage = Arc::new(InMemoryTaskStorage::new());
1623 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1624 task_storage,
1625 ));
1626
1627 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1629 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1630
1631 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1633 Arc::new(InMemorySessionStorage::new());
1634 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1635 session_storage,
1636 turul_mcp_protocol::ServerCapabilities::default(),
1637 ));
1638
1639 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1641 .with_task_runtime(Arc::clone(&runtime));
1642
1643 let params = serde_json::json!({
1645 "name": "slow_tool",
1646 "arguments": {},
1647 "task": {}
1648 });
1649 let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1650 params
1651 .as_object()
1652 .unwrap()
1653 .iter()
1654 .map(|(k, v)| (k.clone(), v.clone()))
1655 .collect(),
1656 );
1657
1658 let start = std::time::Instant::now();
1660 let result = tool_handler
1661 .handle("tools/call", Some(request_params), None)
1662 .await;
1663 let elapsed = start.elapsed();
1664
1665 let value = result.expect("tools/call with task should succeed");
1667 assert!(
1668 value.get("task").is_some(),
1669 "Response should contain 'task' field (CreateTaskResult shape)"
1670 );
1671 let task = value.get("task").unwrap();
1672 assert!(
1673 task.get("taskId").is_some(),
1674 "Task should have taskId field"
1675 );
1676 assert_eq!(
1677 task.get("status")
1678 .and_then(|v| v.as_str())
1679 .unwrap_or_default(),
1680 "working",
1681 "Task status should be 'working'"
1682 );
1683
1684 assert!(
1688 elapsed < std::time::Duration::from_secs(1),
1689 "tools/call with task should return immediately (took {:?}, expected < 1s)",
1690 elapsed
1691 );
1692 }
1693
1694 #[derive(Clone)]
1700 struct StaticTestResource;
1701
1702 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1703 fn name(&self) -> &str {
1704 "static_test"
1705 }
1706 }
1707
1708 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1709 fn description(&self) -> Option<&str> {
1710 Some("Static test resource")
1711 }
1712 }
1713
1714 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1715 fn uri(&self) -> &str {
1716 "file:///test.txt"
1717 }
1718 }
1719
1720 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1721 fn mime_type(&self) -> Option<&str> {
1722 Some("text/plain")
1723 }
1724 }
1725
1726 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1727 fn size(&self) -> Option<u64> {
1728 None
1729 }
1730 }
1731
1732 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1733 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1734 None
1735 }
1736 }
1737
1738 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1739 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1740 None
1741 }
1742 }
1743
1744 impl HasIcons for StaticTestResource {}
1745
1746 #[async_trait::async_trait]
1747 impl McpResource for StaticTestResource {
1748 async fn read(
1749 &self,
1750 _params: Option<serde_json::Value>,
1751 _session: Option<&turul_mcp_server::SessionContext>,
1752 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1753 {
1754 use turul_mcp_protocol::resources::ResourceContent;
1755 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1756 }
1757 }
1758
1759 #[derive(Clone)]
1761 struct TemplateTestResource;
1762
1763 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1764 fn name(&self) -> &str {
1765 "template_test"
1766 }
1767 }
1768
1769 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1770 fn description(&self) -> Option<&str> {
1771 Some("Template test resource")
1772 }
1773 }
1774
1775 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1776 fn uri(&self) -> &str {
1777 "agent://agents/{agent_id}"
1778 }
1779 }
1780
1781 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1782 fn mime_type(&self) -> Option<&str> {
1783 Some("application/json")
1784 }
1785 }
1786
1787 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1788 fn size(&self) -> Option<u64> {
1789 None
1790 }
1791 }
1792
1793 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1794 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1795 None
1796 }
1797 }
1798
1799 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1800 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1801 None
1802 }
1803 }
1804
1805 impl HasIcons for TemplateTestResource {}
1806
1807 #[async_trait::async_trait]
1808 impl McpResource for TemplateTestResource {
1809 async fn read(
1810 &self,
1811 _params: Option<serde_json::Value>,
1812 _session: Option<&turul_mcp_server::SessionContext>,
1813 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1814 {
1815 use turul_mcp_protocol::resources::ResourceContent;
1816 Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
1817 }
1818 }
1819
1820 #[test]
1821 fn test_resource_auto_detection_static() {
1822 let builder = LambdaMcpServerBuilder::new()
1823 .name("test")
1824 .resource(StaticTestResource);
1825
1826 assert_eq!(builder.resources.len(), 1);
1827 assert!(builder.resources.contains_key("file:///test.txt"));
1828 assert_eq!(builder.template_resources.len(), 0);
1829 }
1830
1831 #[test]
1832 fn test_resource_auto_detection_template() {
1833 let builder = LambdaMcpServerBuilder::new()
1834 .name("test")
1835 .resource(TemplateTestResource);
1836
1837 assert_eq!(builder.resources.len(), 0);
1838 assert_eq!(builder.template_resources.len(), 1);
1839
1840 let (template, _) = &builder.template_resources[0];
1841 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1842 }
1843
1844 #[test]
1845 fn test_resource_auto_detection_mixed() {
1846 let builder = LambdaMcpServerBuilder::new()
1847 .name("test")
1848 .resource(StaticTestResource)
1849 .resource(TemplateTestResource);
1850
1851 assert_eq!(builder.resources.len(), 1);
1852 assert!(builder.resources.contains_key("file:///test.txt"));
1853 assert_eq!(builder.template_resources.len(), 1);
1854
1855 let (template, _) = &builder.template_resources[0];
1856 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1857 }
1858
1859 #[tokio::test]
1860 async fn test_build_advertises_resources_capability_for_templates_only() {
1861 let server = LambdaMcpServerBuilder::new()
1862 .name("template-only")
1863 .resource(TemplateTestResource)
1864 .storage(Arc::new(InMemorySessionStorage::new()))
1865 .sse(false)
1866 .build()
1867 .await
1868 .unwrap();
1869
1870 assert!(
1871 server.capabilities().resources.is_some(),
1872 "Resources capability should be advertised when template resources are registered"
1873 );
1874 }
1875
1876 #[tokio::test]
1877 async fn test_build_advertises_resources_capability_for_static_only() {
1878 let server = LambdaMcpServerBuilder::new()
1879 .name("static-only")
1880 .resource(StaticTestResource)
1881 .storage(Arc::new(InMemorySessionStorage::new()))
1882 .sse(false)
1883 .build()
1884 .await
1885 .unwrap();
1886
1887 assert!(
1888 server.capabilities().resources.is_some(),
1889 "Resources capability should be advertised when static resources are registered"
1890 );
1891 }
1892
1893 #[tokio::test]
1894 async fn test_build_no_resources_no_capability() {
1895 let server = LambdaMcpServerBuilder::new()
1896 .name("no-resources")
1897 .tool(TestTool)
1898 .storage(Arc::new(InMemorySessionStorage::new()))
1899 .sse(false)
1900 .build()
1901 .await
1902 .unwrap();
1903
1904 assert!(
1905 server.capabilities().resources.is_none(),
1906 "Resources capability should NOT be advertised when no resources are registered"
1907 );
1908 }
1909
1910 #[tokio::test]
1911 async fn test_lambda_builder_templates_list_returns_template() {
1912 use turul_mcp_server::handlers::McpHandler;
1913
1914 let builder = LambdaMcpServerBuilder::new()
1916 .name("template-test")
1917 .resource(TemplateTestResource);
1918
1919 assert_eq!(builder.template_resources.len(), 1);
1921
1922 let handler =
1924 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1925
1926 let result = handler.handle(None).await.expect("should succeed");
1928
1929 let templates = result["resourceTemplates"]
1930 .as_array()
1931 .expect("resourceTemplates should be an array");
1932 assert_eq!(
1933 templates.len(),
1934 1,
1935 "Should have exactly 1 template resource"
1936 );
1937 assert_eq!(
1938 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1939 "Template URI should match"
1940 );
1941 assert_eq!(templates[0]["name"], "template_test");
1942 }
1943
1944 #[tokio::test]
1945 async fn test_lambda_builder_resources_list_returns_static() {
1946 use turul_mcp_server::handlers::McpHandler;
1947
1948 let builder = LambdaMcpServerBuilder::new()
1950 .name("static-test")
1951 .resource(StaticTestResource);
1952
1953 assert_eq!(builder.resources.len(), 1);
1954
1955 let mut handler = ResourcesHandler::new();
1956 for resource in builder.resources.values() {
1957 handler = handler.add_resource_arc(resource.clone());
1958 }
1959
1960 let result = handler.handle(None).await.expect("should succeed");
1961
1962 let resources = result["resources"]
1963 .as_array()
1964 .expect("resources should be an array");
1965 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1966 assert_eq!(resources[0]["uri"], "file:///test.txt");
1967 assert_eq!(resources[0]["name"], "static_test");
1968 }
1969
1970 #[tokio::test]
1971 async fn test_lambda_builder_mixed_resources_separation() {
1972 use turul_mcp_server::handlers::McpHandler;
1973
1974 let builder = LambdaMcpServerBuilder::new()
1976 .name("mixed-test")
1977 .resource(StaticTestResource)
1978 .resource(TemplateTestResource);
1979
1980 assert_eq!(builder.resources.len(), 1);
1981 assert_eq!(builder.template_resources.len(), 1);
1982
1983 let mut list_handler = ResourcesHandler::new();
1985 for resource in builder.resources.values() {
1986 list_handler = list_handler.add_resource_arc(resource.clone());
1987 }
1988
1989 let templates_handler =
1990 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1991
1992 let list_result = list_handler.handle(None).await.expect("should succeed");
1994 let resources = list_result["resources"]
1995 .as_array()
1996 .expect("resources should be an array");
1997 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1998 assert_eq!(resources[0]["uri"], "file:///test.txt");
1999
2000 let templates_result = templates_handler
2002 .handle(None)
2003 .await
2004 .expect("should succeed");
2005 let templates = templates_result["resourceTemplates"]
2006 .as_array()
2007 .expect("resourceTemplates should be an array");
2008 assert_eq!(
2009 templates.len(),
2010 1,
2011 "Only template resource in resources/templates/list"
2012 );
2013 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
2014 }
2015
2016 #[tokio::test]
2017 async fn test_tasks_get_route_registered() {
2018 use turul_mcp_server::TasksGetHandler;
2019 use turul_mcp_server::handlers::McpHandler;
2020 use turul_mcp_server::task_storage::InMemoryTaskStorage;
2021
2022 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
2023 Arc::new(InMemoryTaskStorage::new()),
2024 ));
2025 let handler = TasksGetHandler::new(runtime);
2026
2027 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
2030
2031 let result = handler.handle(Some(params)).await;
2032
2033 assert!(
2035 result.is_err(),
2036 "tasks/get with unknown task should return error"
2037 );
2038 let err = result.unwrap_err();
2039 let err_str = err.to_string();
2040 assert!(
2041 !err_str.contains("method not found"),
2042 "Error should not be 'method not found' — handler should respond to tasks/get"
2043 );
2044 }
2045
2046 #[tokio::test]
2053 async fn test_resources_read_registered_by_default() {
2054 use lambda_http::Body as LambdaBody;
2055
2056 let server = LambdaMcpServerBuilder::new()
2057 .name("parity-test")
2058 .version("1.0.0")
2059 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2061 .strict_lifecycle(false) .sse(false)
2063 .build()
2064 .await
2065 .unwrap();
2066
2067 let handler = server.handler().await.unwrap();
2068
2069 let init_req = http::Request::builder()
2071 .method("POST")
2072 .uri("/mcp")
2073 .header("Content-Type", "application/json")
2074 .header("MCP-Protocol-Version", "2025-11-25")
2075 .body(LambdaBody::Text(
2076 serde_json::json!({
2077 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2078 "params": {
2079 "protocolVersion": "2025-11-25",
2080 "capabilities": {},
2081 "clientInfo": { "name": "test", "version": "1.0.0" }
2082 }
2083 })
2084 .to_string(),
2085 ))
2086 .unwrap();
2087 let init_resp = handler.handle(init_req).await.unwrap();
2088 let session_id = init_resp
2089 .headers()
2090 .get("Mcp-Session-Id")
2091 .unwrap()
2092 .to_str()
2093 .unwrap()
2094 .to_string();
2095
2096 let read_req = http::Request::builder()
2099 .method("POST")
2100 .uri("/mcp")
2101 .header("Content-Type", "application/json")
2102 .header("MCP-Protocol-Version", "2025-11-25")
2103 .header("Mcp-Session-Id", &session_id)
2104 .body(LambdaBody::Text(
2105 serde_json::json!({
2106 "jsonrpc": "2.0", "method": "resources/read", "id": 2,
2107 "params": { "uri": "file:///nonexistent" }
2108 })
2109 .to_string(),
2110 ))
2111 .unwrap();
2112 let read_resp = handler.handle(read_req).await.unwrap();
2113 let body = String::from_utf8_lossy(read_resp.body().as_ref()).to_string();
2114 let json: serde_json::Value = serde_json::from_str(&body)
2115 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2116
2117 assert!(
2119 json["error"].is_object(),
2120 "resources/read must return JSON-RPC error, got: {json}"
2121 );
2122 let error_code = json["error"]["code"].as_i64().unwrap();
2126 assert_ne!(
2127 error_code, -32601,
2128 "resources/read must be registered (got method-not-found -32601): {json}"
2129 );
2130 }
2131
2132 #[tokio::test]
2136 async fn test_resources_templates_list_absent_without_templates() {
2137 use lambda_http::Body as LambdaBody;
2138
2139 let server = LambdaMcpServerBuilder::new()
2140 .name("parity-test")
2141 .version("1.0.0")
2142 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2144 .strict_lifecycle(false) .sse(false)
2146 .build()
2147 .await
2148 .unwrap();
2149
2150 let handler = server.handler().await.unwrap();
2151
2152 let init_req = http::Request::builder()
2154 .method("POST")
2155 .uri("/mcp")
2156 .header("Content-Type", "application/json")
2157 .header("MCP-Protocol-Version", "2025-11-25")
2158 .body(LambdaBody::Text(
2159 serde_json::json!({
2160 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2161 "params": {
2162 "protocolVersion": "2025-11-25",
2163 "capabilities": {},
2164 "clientInfo": { "name": "test", "version": "1.0.0" }
2165 }
2166 })
2167 .to_string(),
2168 ))
2169 .unwrap();
2170 let init_resp = handler.handle(init_req).await.unwrap();
2171 let session_id = init_resp
2172 .headers()
2173 .get("Mcp-Session-Id")
2174 .unwrap()
2175 .to_str()
2176 .unwrap()
2177 .to_string();
2178
2179 let tmpl_req = http::Request::builder()
2182 .method("POST")
2183 .uri("/mcp")
2184 .header("Content-Type", "application/json")
2185 .header("MCP-Protocol-Version", "2025-11-25")
2186 .header("Mcp-Session-Id", &session_id)
2187 .body(LambdaBody::Text(
2188 serde_json::json!({
2189 "jsonrpc": "2.0", "method": "resources/templates/list", "id": 2
2190 })
2191 .to_string(),
2192 ))
2193 .unwrap();
2194 let tmpl_resp = handler.handle(tmpl_req).await.unwrap();
2195 let body = String::from_utf8_lossy(tmpl_resp.body().as_ref()).to_string();
2196 let json: serde_json::Value = serde_json::from_str(&body)
2197 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2198
2199 assert!(
2201 json["error"].is_object(),
2202 "resources/templates/list should return error without templates: {json}"
2203 );
2204 assert_eq!(
2205 json["error"]["code"].as_i64().unwrap(),
2206 -32601,
2207 "resources/templates/list must be method-not-found (-32601) without templates: {json}"
2208 );
2209 }
2210}