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
75 capabilities: ServerCapabilities,
77
78 tools: HashMap<String, Arc<dyn McpTool>>,
80
81 resources: HashMap<String, Arc<dyn McpResource>>,
83
84 template_resources: Vec<(turul_mcp_server::uri_template::UriTemplate, Arc<dyn McpResource>)>,
86
87 prompts: HashMap<String, Arc<dyn McpPrompt>>,
89
90 elicitations: HashMap<String, Arc<dyn McpElicitation>>,
92
93 sampling: HashMap<String, Arc<dyn McpSampling>>,
95
96 completions: HashMap<String, Arc<dyn McpCompletion>>,
98
99 loggers: HashMap<String, Arc<dyn McpLogger>>,
101
102 root_providers: HashMap<String, Arc<dyn McpRoot>>,
104
105 notifications: HashMap<String, Arc<dyn McpNotification>>,
107
108 handlers: HashMap<String, Arc<dyn McpHandler>>,
110
111 roots: Vec<turul_mcp_protocol::roots::Root>,
113
114 instructions: Option<String>,
116
117 session_timeout_minutes: Option<u64>,
119 session_cleanup_interval_seconds: Option<u64>,
120
121 session_storage: Option<Arc<BoxedSessionStorage>>,
123
124 strict_lifecycle: bool,
126
127 enable_sse: bool,
129 server_config: ServerConfig,
131 stream_config: StreamConfig,
132
133 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
135
136 task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
138 task_recovery_timeout_ms: u64,
140
141 #[cfg(feature = "cors")]
143 cors_config: Option<CorsConfig>,
144}
145
146impl LambdaMcpServerBuilder {
147 pub fn new() -> Self {
149 let capabilities = ServerCapabilities::default();
152
153 let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
155 handlers.insert("ping".to_string(), Arc::new(PingHandler));
156 handlers.insert(
157 "completion/complete".to_string(),
158 Arc::new(CompletionHandler),
159 );
160 handlers.insert(
161 "resources/list".to_string(),
162 Arc::new(ResourcesHandler::new()),
163 );
164 handlers.insert(
165 "prompts/list".to_string(),
166 Arc::new(PromptsListHandler::new()),
167 );
168 handlers.insert(
169 "prompts/get".to_string(),
170 Arc::new(PromptsGetHandler::new()),
171 );
172 handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
173 handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
174 handlers.insert(
175 "sampling/createMessage".to_string(),
176 Arc::new(SamplingHandler),
177 );
178 handlers.insert(
179 "resources/templates/list".to_string(),
180 Arc::new(ResourceTemplatesHandler::new()),
181 );
182 handlers.insert(
183 "elicitation/create".to_string(),
184 Arc::new(ElicitationHandler::with_mock_provider()),
185 );
186
187 let notifications_handler = Arc::new(NotificationsHandler);
189 handlers.insert(
190 "notifications/message".to_string(),
191 notifications_handler.clone(),
192 );
193 handlers.insert(
194 "notifications/progress".to_string(),
195 notifications_handler.clone(),
196 );
197 handlers.insert(
199 "notifications/resources/list_changed".to_string(),
200 notifications_handler.clone(),
201 );
202 handlers.insert(
203 "notifications/resources/updated".to_string(),
204 notifications_handler.clone(),
205 );
206 handlers.insert(
207 "notifications/tools/list_changed".to_string(),
208 notifications_handler.clone(),
209 );
210 handlers.insert(
211 "notifications/prompts/list_changed".to_string(),
212 notifications_handler.clone(),
213 );
214 handlers.insert(
215 "notifications/roots/list_changed".to_string(),
216 notifications_handler.clone(),
217 );
218 handlers.insert(
220 "notifications/resources/listChanged".to_string(),
221 notifications_handler.clone(),
222 );
223 handlers.insert(
224 "notifications/tools/listChanged".to_string(),
225 notifications_handler.clone(),
226 );
227 handlers.insert(
228 "notifications/prompts/listChanged".to_string(),
229 notifications_handler.clone(),
230 );
231 handlers.insert(
232 "notifications/roots/listChanged".to_string(),
233 notifications_handler,
234 );
235
236 Self {
237 name: "turul-mcp-aws-lambda".to_string(),
238 version: env!("CARGO_PKG_VERSION").to_string(),
239 title: None,
240 capabilities,
241 tools: HashMap::new(),
242 resources: HashMap::new(),
243 template_resources: Vec::new(),
244 prompts: HashMap::new(),
245 elicitations: HashMap::new(),
246 sampling: HashMap::new(),
247 completions: HashMap::new(),
248 loggers: HashMap::new(),
249 root_providers: HashMap::new(),
250 notifications: HashMap::new(),
251 handlers,
252 roots: Vec::new(),
253 instructions: None,
254 session_timeout_minutes: None,
255 session_cleanup_interval_seconds: None,
256 session_storage: None,
257 strict_lifecycle: false,
258 enable_sse: cfg!(feature = "sse"),
259 server_config: ServerConfig::default(),
260 stream_config: StreamConfig::default(),
261 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
262 task_runtime: None,
263 task_recovery_timeout_ms: 300_000, #[cfg(feature = "cors")]
265 cors_config: None,
266 }
267 }
268
269 pub fn name(mut self, name: impl Into<String>) -> Self {
271 self.name = name.into();
272 self
273 }
274
275 pub fn version(mut self, version: impl Into<String>) -> Self {
277 self.version = version.into();
278 self
279 }
280
281 pub fn title(mut self, title: impl Into<String>) -> Self {
283 self.title = Some(title.into());
284 self
285 }
286
287 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
289 self.instructions = Some(instructions.into());
290 self
291 }
292
293 pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
305 let name = tool.name().to_string();
306 self.tools.insert(name, Arc::new(tool));
307 self
308 }
309
310 pub fn tool_fn<F, T>(self, func: F) -> Self
312 where
313 F: Fn() -> T,
314 T: McpTool + 'static,
315 {
316 self.tool(func())
317 }
318
319 pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
321 for tool in tools {
322 self = self.tool(tool);
323 }
324 self
325 }
326
327 pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
333 let uri = resource.uri().to_string();
334
335 if uri.contains('{') && uri.contains('}') {
336 match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
338 Ok(template) => {
339 self.template_resources.push((template, Arc::new(resource)));
340 }
341 Err(e) => {
342 tracing::warn!(
343 "Failed to parse template resource URI '{}': {}. Registering as static.",
344 uri,
345 e
346 );
347 self.resources.insert(uri, Arc::new(resource));
348 }
349 }
350 } else {
351 self.resources.insert(uri, Arc::new(resource));
353 }
354 self
355 }
356
357 pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
359 mut self,
360 resources: I,
361 ) -> Self {
362 for resource in resources {
363 self = self.resource(resource);
364 }
365 self
366 }
367
368 pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
370 let name = prompt.name().to_string();
371 self.prompts.insert(name, Arc::new(prompt));
372 self
373 }
374
375 pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
377 mut self,
378 prompts: I,
379 ) -> Self {
380 for prompt in prompts {
381 self = self.prompt(prompt);
382 }
383 self
384 }
385
386 pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
388 let key = format!("elicitation_{}", self.elicitations.len());
389 self.elicitations.insert(key, Arc::new(elicitation));
390 self
391 }
392
393 pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
395 mut self,
396 elicitations: I,
397 ) -> Self {
398 for elicitation in elicitations {
399 self = self.elicitation(elicitation);
400 }
401 self
402 }
403
404 pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
406 let key = format!("sampling_{}", self.sampling.len());
407 self.sampling.insert(key, Arc::new(sampling));
408 self
409 }
410
411 pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
413 mut self,
414 sampling: I,
415 ) -> Self {
416 for s in sampling {
417 self = self.sampling_provider(s);
418 }
419 self
420 }
421
422 pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
424 let key = format!("completion_{}", self.completions.len());
425 self.completions.insert(key, Arc::new(completion));
426 self
427 }
428
429 pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
431 mut self,
432 completions: I,
433 ) -> Self {
434 for completion in completions {
435 self = self.completion_provider(completion);
436 }
437 self
438 }
439
440 pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
442 let key = format!("logger_{}", self.loggers.len());
443 self.loggers.insert(key, Arc::new(logger));
444 self
445 }
446
447 pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
449 mut self,
450 loggers: I,
451 ) -> Self {
452 for logger in loggers {
453 self = self.logger(logger);
454 }
455 self
456 }
457
458 pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
460 let key = format!("root_{}", self.root_providers.len());
461 self.root_providers.insert(key, Arc::new(root));
462 self
463 }
464
465 pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
467 mut self,
468 roots: I,
469 ) -> Self {
470 for root in roots {
471 self = self.root_provider(root);
472 }
473 self
474 }
475
476 pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
478 let key = format!("notification_{}", self.notifications.len());
479 self.notifications.insert(key, Arc::new(notification));
480 self
481 }
482
483 pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
485 mut self,
486 notifications: I,
487 ) -> Self {
488 for notification in notifications {
489 self = self.notification_provider(notification);
490 }
491 self
492 }
493
494 pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
500 self.sampling_provider(sampling)
501 }
502
503 pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
505 self.completion_provider(completion)
506 }
507
508 pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
510 let notification = N::default();
511 self.notification_provider(notification)
512 }
513
514 pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
516 let handler_arc = Arc::new(handler);
517 for method in handler_arc.supported_methods() {
518 self.handlers.insert(method, handler_arc.clone());
519 }
520 self
521 }
522
523 pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
525 mut self,
526 handlers: I,
527 ) -> Self {
528 for handler in handlers {
529 self = self.handler(handler);
530 }
531 self
532 }
533
534 pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
536 self.roots.push(root);
537 self
538 }
539
540 pub fn with_completion(mut self) -> Self {
546 use turul_mcp_protocol::initialize::CompletionsCapabilities;
547 self.capabilities.completions = Some(CompletionsCapabilities {
548 enabled: Some(true),
549 });
550 self.handler(CompletionHandler)
551 }
552
553 pub fn with_prompts(mut self) -> Self {
555 use turul_mcp_protocol::initialize::PromptsCapabilities;
556 self.capabilities.prompts = Some(PromptsCapabilities {
557 list_changed: Some(false),
558 });
559
560 self
563 }
564
565 pub fn with_resources(mut self) -> Self {
567 use turul_mcp_protocol::initialize::ResourcesCapabilities;
568 self.capabilities.resources = Some(ResourcesCapabilities {
569 subscribe: Some(false),
570 list_changed: Some(false),
571 });
572
573 let mut list_handler = ResourcesHandler::new();
575 for resource in self.resources.values() {
576 list_handler = list_handler.add_resource_arc(resource.clone());
577 }
578 self = self.handler(list_handler);
579
580 if !self.template_resources.is_empty() {
582 let templates_handler =
583 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
584 self = self.handler(templates_handler);
585 }
586
587 let mut read_handler = ResourcesReadHandler::new().without_security();
589 for resource in self.resources.values() {
590 read_handler = read_handler.add_resource_arc(resource.clone());
591 }
592 for (template, resource) in &self.template_resources {
593 read_handler =
594 read_handler.add_template_resource_arc(template.clone(), resource.clone());
595 }
596 self.handler(read_handler)
597 }
598
599 pub fn with_logging(mut self) -> Self {
601 use turul_mcp_protocol::initialize::LoggingCapabilities;
602 self.capabilities.logging = Some(LoggingCapabilities::default());
603 self.handler(LoggingHandler)
604 }
605
606 pub fn with_roots(self) -> Self {
608 self.handler(RootsHandler::new())
609 }
610
611 pub fn with_sampling(self) -> Self {
613 self.handler(SamplingHandler)
614 }
615
616 pub fn with_elicitation(self) -> Self {
618 self.handler(ElicitationHandler::with_mock_provider())
621 }
622
623 pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
625 self.handler(ElicitationHandler::new(Arc::new(provider)))
627 }
628
629 pub fn with_notifications(self) -> Self {
631 self.handler(NotificationsHandler)
632 }
633
634 pub fn with_task_storage(
649 mut self,
650 storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
651 ) -> Self {
652 let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
653 .with_recovery_timeout(self.task_recovery_timeout_ms);
654 self.task_runtime = Some(Arc::new(runtime));
655 self
656 }
657
658 pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
662 self.task_runtime = Some(runtime);
663 self
664 }
665
666 pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
671 self.task_recovery_timeout_ms = timeout_ms;
672 self
673 }
674
675 pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
681 self.session_timeout_minutes = Some(minutes);
682 self
683 }
684
685 pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
687 self.session_cleanup_interval_seconds = Some(seconds);
688 self
689 }
690
691 pub fn strict_lifecycle(mut self, strict: bool) -> Self {
693 self.strict_lifecycle = strict;
694 self
695 }
696
697 pub fn with_strict_lifecycle(self) -> Self {
699 self.strict_lifecycle(true)
700 }
701
702 pub fn sse(mut self, enable: bool) -> Self {
704 self.enable_sse = enable;
705
706 if enable {
708 self.server_config.enable_get_sse = true;
709 self.server_config.enable_post_sse = true;
710 } else {
711 self.server_config.enable_get_sse = false;
714 self.server_config.enable_post_sse = false;
715 }
716
717 self
718 }
719
720 pub fn with_long_sessions(mut self) -> Self {
722 self.session_timeout_minutes = Some(120); self.session_cleanup_interval_seconds = Some(300); self
725 }
726
727 pub fn with_short_sessions(mut self) -> Self {
729 self.session_timeout_minutes = Some(5); self.session_cleanup_interval_seconds = Some(30); self
732 }
733
734 pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
742 self.session_storage = Some(storage);
743 self
744 }
745
746 #[cfg(feature = "dynamodb")]
753 pub async fn dynamodb_storage(self) -> Result<Self> {
754 use turul_mcp_session_storage::DynamoDbSessionStorage;
755
756 let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
757 LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
758 })?;
759
760 Ok(self.storage(Arc::new(storage)))
761 }
762
763 pub fn middleware(
799 mut self,
800 middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
801 ) -> Self {
802 self.middleware_stack.push(middleware);
803 self
804 }
805
806 pub fn server_config(mut self, config: ServerConfig) -> Self {
808 self.server_config = config;
809 self
810 }
811
812 pub fn stream_config(mut self, config: StreamConfig) -> Self {
814 self.stream_config = config;
815 self
816 }
817
818 #[cfg(feature = "cors")]
822 pub fn cors(mut self, config: CorsConfig) -> Self {
823 self.cors_config = Some(config);
824 self
825 }
826
827 #[cfg(feature = "cors")]
829 pub fn cors_allow_all_origins(mut self) -> Self {
830 self.cors_config = Some(CorsConfig::allow_all());
831 self
832 }
833
834 #[cfg(feature = "cors")]
836 pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
837 self.cors_config = Some(CorsConfig::for_origins(origins));
838 self
839 }
840
841 #[cfg(feature = "cors")]
848 pub fn cors_from_env(mut self) -> Self {
849 self.cors_config = Some(CorsConfig::from_env());
850 self
851 }
852
853 #[cfg(feature = "cors")]
855 pub fn cors_disabled(self) -> Self {
856 self
858 }
859
860 #[cfg(all(feature = "dynamodb", feature = "cors"))]
866 pub async fn production_config(self) -> Result<Self> {
867 Ok(self.dynamodb_storage().await?.cors_from_env())
868 }
869
870 #[cfg(feature = "cors")]
874 pub fn development_config(self) -> Self {
875 use turul_mcp_session_storage::InMemorySessionStorage;
876
877 self.storage(Arc::new(InMemorySessionStorage::new()))
878 .cors_allow_all_origins()
879 }
880
881 pub async fn build(self) -> Result<LambdaMcpServer> {
885 use turul_mcp_session_storage::InMemorySessionStorage;
886
887 if self.name.is_empty() {
889 return Err(crate::error::LambdaError::Configuration(
890 "Server name cannot be empty".to_string(),
891 ));
892 }
893 if self.version.is_empty() {
894 return Err(crate::error::LambdaError::Configuration(
895 "Server version cannot be empty".to_string(),
896 ));
897 }
898
899 let session_storage = self
905 .session_storage
906 .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
907
908 let implementation = if let Some(title) = self.title {
910 Implementation::new(&self.name, &self.version).with_title(title)
911 } else {
912 Implementation::new(&self.name, &self.version)
913 };
914
915 let mut capabilities = self.capabilities.clone();
917 let has_tools = !self.tools.is_empty();
918 let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
919 let has_prompts = !self.prompts.is_empty();
920 let has_elicitations = !self.elicitations.is_empty();
921 let has_completions = !self.completions.is_empty();
922 let has_logging = !self.loggers.is_empty();
923 tracing::debug!("🔧 Has logging configured: {}", has_logging);
924
925 if has_tools {
927 capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
928 list_changed: Some(false), });
930 }
931
932 if has_resources {
934 capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
935 subscribe: Some(false), list_changed: Some(false), });
938 }
939
940 if has_prompts {
942 capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
943 list_changed: Some(false), });
945 }
946
947 let _ = has_elicitations; if has_completions {
953 capabilities.completions =
954 Some(turul_mcp_protocol::initialize::CompletionsCapabilities {
955 enabled: Some(true),
956 });
957 }
958
959 capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities {
962 enabled: Some(true),
963 levels: Some(vec![
964 "debug".to_string(),
965 "info".to_string(),
966 "warning".to_string(),
967 "error".to_string(),
968 ]),
969 });
970
971 if self.task_runtime.is_some() {
973 use turul_mcp_protocol::initialize::*;
974 capabilities.tasks = Some(TasksCapabilities {
975 list: Some(TasksListCapabilities::default()),
976 cancel: Some(TasksCancelCapabilities::default()),
977 requests: Some(TasksRequestCapabilities {
978 tools: Some(TasksToolCapabilities {
979 call: Some(TasksToolCallCapabilities::default()),
980 extra: Default::default(),
981 }),
982 extra: Default::default(),
983 }),
984 extra: Default::default(),
985 });
986 }
987
988 let mut handlers = self.handlers;
990 if !self.roots.is_empty() {
991 let mut roots_handler = RootsHandler::new();
992 for root in &self.roots {
993 roots_handler = roots_handler.add_root(root.clone());
994 }
995 handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
996 }
997
998 if let Some(ref runtime) = self.task_runtime {
1000 use turul_mcp_server::{
1001 TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1002 };
1003 handlers.insert(
1004 "tasks/get".to_string(),
1005 Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1006 );
1007 handlers.insert(
1008 "tasks/list".to_string(),
1009 Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1010 );
1011 handlers.insert(
1012 "tasks/cancel".to_string(),
1013 Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1014 );
1015 handlers.insert(
1016 "tasks/result".to_string(),
1017 Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1018 );
1019 }
1020
1021 if has_resources {
1023 let mut list_handler = ResourcesHandler::new();
1025 for resource in self.resources.values() {
1026 list_handler = list_handler.add_resource_arc(resource.clone());
1027 }
1028 handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1029
1030 if !self.template_resources.is_empty() {
1032 let templates_handler = ResourceTemplatesHandler::new()
1033 .with_templates(self.template_resources.clone());
1034 handlers.insert(
1035 "resources/templates/list".to_string(),
1036 Arc::new(templates_handler),
1037 );
1038 }
1039
1040 let mut read_handler = ResourcesReadHandler::new().without_security();
1042 for resource in self.resources.values() {
1043 read_handler = read_handler.add_resource_arc(resource.clone());
1044 }
1045 for (template, resource) in &self.template_resources {
1046 read_handler =
1047 read_handler.add_template_resource_arc(template.clone(), resource.clone());
1048 }
1049 handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1050 }
1051
1052 Ok(LambdaMcpServer::new(
1054 implementation,
1055 capabilities,
1056 self.tools,
1057 self.resources,
1058 self.prompts,
1059 self.elicitations,
1060 self.sampling,
1061 self.completions,
1062 self.loggers,
1063 self.root_providers,
1064 self.notifications,
1065 handlers,
1066 self.roots,
1067 self.instructions,
1068 session_storage,
1069 self.strict_lifecycle,
1070 self.server_config,
1071 self.enable_sse,
1072 self.stream_config,
1073 #[cfg(feature = "cors")]
1074 self.cors_config,
1075 self.middleware_stack,
1076 self.task_runtime,
1077 ))
1078 }
1079}
1080
1081impl Default for LambdaMcpServerBuilder {
1082 fn default() -> Self {
1083 Self::new()
1084 }
1085}
1086
1087pub trait LambdaMcpServerBuilderExt {
1089 fn tools<I, T>(self, tools: I) -> Self
1091 where
1092 I: IntoIterator<Item = T>,
1093 T: McpTool + 'static;
1094}
1095
1096impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1097 fn tools<I, T>(mut self, tools: I) -> Self
1098 where
1099 I: IntoIterator<Item = T>,
1100 T: McpTool + 'static,
1101 {
1102 for tool in tools {
1103 self = self.tool(tool);
1104 }
1105 self
1106 }
1107}
1108
1109pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1114where
1115 I: IntoIterator<Item = T>,
1116 T: McpTool + 'static,
1117{
1118 let mut builder = LambdaMcpServerBuilder::new();
1119
1120 for tool in tools {
1121 builder = builder.tool(tool);
1122 }
1123
1124 #[cfg(feature = "cors")]
1125 {
1126 builder = builder.cors_allow_all_origins();
1127 }
1128
1129 builder.sse(false).build().await
1130}
1131
1132#[cfg(all(feature = "dynamodb", feature = "cors"))]
1136pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1137where
1138 I: IntoIterator<Item = T>,
1139 T: McpTool + 'static,
1140{
1141 let mut builder = LambdaMcpServerBuilder::new();
1142
1143 for tool in tools {
1144 builder = builder.tool(tool);
1145 }
1146
1147 builder.production_config().await?.build().await
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152 use super::*;
1153 use turul_mcp_builders::prelude::*;
1154 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1158 struct TestTool;
1159
1160 impl HasBaseMetadata for TestTool {
1161 fn name(&self) -> &str {
1162 "test_tool"
1163 }
1164 }
1165
1166 impl HasDescription for TestTool {
1167 fn description(&self) -> Option<&str> {
1168 Some("Test tool")
1169 }
1170 }
1171
1172 impl HasInputSchema for TestTool {
1173 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1174 use turul_mcp_protocol::ToolSchema;
1175 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1176 SCHEMA.get_or_init(ToolSchema::object)
1177 }
1178 }
1179
1180 impl HasOutputSchema for TestTool {
1181 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1182 None
1183 }
1184 }
1185
1186 impl HasAnnotations for TestTool {
1187 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1188 None
1189 }
1190 }
1191
1192 impl HasToolMeta for TestTool {
1193 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1194 None
1195 }
1196 }
1197
1198 impl HasIcons for TestTool {}
1199 impl HasExecution for TestTool {}
1200
1201 #[async_trait::async_trait]
1202 impl McpTool for TestTool {
1203 async fn call(
1204 &self,
1205 _args: serde_json::Value,
1206 _session: Option<turul_mcp_server::SessionContext>,
1207 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1208 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1209 Ok(CallToolResult::success(vec![ToolResult::text(
1210 "test result",
1211 )]))
1212 }
1213 }
1214
1215 #[tokio::test]
1216 async fn test_builder_basic() {
1217 let server = LambdaMcpServerBuilder::new()
1218 .name("test-server")
1219 .version("1.0.0")
1220 .tool(TestTool)
1221 .storage(Arc::new(InMemorySessionStorage::new()))
1222 .sse(false) .build()
1224 .await
1225 .unwrap();
1226
1227 let handler = server.handler().await.unwrap();
1229 assert!(
1231 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1232 "Stream manager must be initialized"
1233 );
1234 }
1235
1236 #[tokio::test]
1237 async fn test_simple_lambda_server() {
1238 let tools = vec![TestTool];
1239 let server = simple_lambda_server(tools).await.unwrap();
1240
1241 let handler = server.handler().await.unwrap();
1243 assert!(
1246 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1247 "Stream manager must be initialized"
1248 );
1249 }
1250
1251 #[tokio::test]
1252 async fn test_builder_extension_trait() {
1253 let tools = vec![TestTool, TestTool];
1254
1255 let server = LambdaMcpServerBuilder::new()
1256 .tools(tools)
1257 .storage(Arc::new(InMemorySessionStorage::new()))
1258 .sse(false) .build()
1260 .await
1261 .unwrap();
1262
1263 let handler = server.handler().await.unwrap();
1264 assert!(
1267 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1268 "Stream manager must be initialized"
1269 );
1270 }
1271
1272 #[cfg(feature = "cors")]
1273 #[tokio::test]
1274 async fn test_cors_configuration() {
1275 let server = LambdaMcpServerBuilder::new()
1276 .cors_allow_all_origins()
1277 .storage(Arc::new(InMemorySessionStorage::new()))
1278 .sse(false) .build()
1280 .await
1281 .unwrap();
1282
1283 let handler = server.handler().await.unwrap();
1284 assert!(
1287 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1288 "Stream manager must be initialized"
1289 );
1290 }
1291
1292 #[tokio::test]
1293 async fn test_sse_toggle_functionality() {
1294 let mut builder =
1296 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1297
1298 builder = builder.sse(true);
1300 assert!(builder.enable_sse, "SSE should be enabled");
1301 assert!(
1302 builder.server_config.enable_get_sse,
1303 "GET SSE endpoint should be enabled"
1304 );
1305 assert!(
1306 builder.server_config.enable_post_sse,
1307 "POST SSE endpoint should be enabled"
1308 );
1309
1310 builder = builder.sse(false);
1312 assert!(!builder.enable_sse, "SSE should be disabled");
1313 assert!(
1314 !builder.server_config.enable_get_sse,
1315 "GET SSE endpoint should be disabled"
1316 );
1317 assert!(
1318 !builder.server_config.enable_post_sse,
1319 "POST SSE endpoint should be disabled"
1320 );
1321
1322 builder = builder.sse(true);
1324 assert!(builder.enable_sse, "SSE should be re-enabled");
1325 assert!(
1326 builder.server_config.enable_get_sse,
1327 "GET SSE endpoint should be re-enabled"
1328 );
1329 assert!(
1330 builder.server_config.enable_post_sse,
1331 "POST SSE endpoint should be re-enabled"
1332 );
1333
1334 let server = builder.build().await.unwrap();
1336 let handler = server.handler().await.unwrap();
1337 assert!(
1338 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1339 "Stream manager must be initialized"
1340 );
1341 }
1342
1343 #[tokio::test]
1348 async fn test_builder_without_tasks_no_capability() {
1349 let server = LambdaMcpServerBuilder::new()
1350 .name("no-tasks")
1351 .tool(TestTool)
1352 .storage(Arc::new(InMemorySessionStorage::new()))
1353 .sse(false)
1354 .build()
1355 .await
1356 .unwrap();
1357
1358 assert!(
1359 server.capabilities().tasks.is_none(),
1360 "Tasks capability should not be advertised without task storage"
1361 );
1362 }
1363
1364 #[tokio::test]
1365 async fn test_builder_with_task_storage_advertises_capability() {
1366 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1367
1368 let server = LambdaMcpServerBuilder::new()
1369 .name("with-tasks")
1370 .tool(TestTool)
1371 .storage(Arc::new(InMemorySessionStorage::new()))
1372 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1373 .sse(false)
1374 .build()
1375 .await
1376 .unwrap();
1377
1378 let tasks_cap = server
1379 .capabilities()
1380 .tasks
1381 .as_ref()
1382 .expect("Tasks capability should be advertised");
1383 assert!(tasks_cap.list.is_some(), "list capability should be set");
1384 assert!(
1385 tasks_cap.cancel.is_some(),
1386 "cancel capability should be set"
1387 );
1388 let requests = tasks_cap
1389 .requests
1390 .as_ref()
1391 .expect("requests capability should be set");
1392 let tools = requests
1393 .tools
1394 .as_ref()
1395 .expect("tools capability should be set");
1396 assert!(tools.call.is_some(), "tools.call capability should be set");
1397 }
1398
1399 #[tokio::test]
1400 async fn test_builder_with_task_runtime_advertises_capability() {
1401 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1402
1403 let server = LambdaMcpServerBuilder::new()
1404 .name("with-runtime")
1405 .tool(TestTool)
1406 .storage(Arc::new(InMemorySessionStorage::new()))
1407 .with_task_runtime(runtime)
1408 .sse(false)
1409 .build()
1410 .await
1411 .unwrap();
1412
1413 assert!(
1414 server.capabilities().tasks.is_some(),
1415 "Tasks capability should be advertised with task runtime"
1416 );
1417 }
1418
1419 #[tokio::test]
1420 async fn test_task_recovery_timeout_configuration() {
1421 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1422
1423 let server = LambdaMcpServerBuilder::new()
1424 .name("custom-timeout")
1425 .tool(TestTool)
1426 .storage(Arc::new(InMemorySessionStorage::new()))
1427 .task_recovery_timeout_ms(60_000)
1428 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1429 .sse(false)
1430 .build()
1431 .await
1432 .unwrap();
1433
1434 assert!(
1435 server.capabilities().tasks.is_some(),
1436 "Tasks should be enabled with custom timeout"
1437 );
1438 }
1439
1440 #[tokio::test]
1441 async fn test_backward_compatibility_no_tasks() {
1442 let server = LambdaMcpServerBuilder::new()
1444 .name("backward-compat")
1445 .version("1.0.0")
1446 .tool(TestTool)
1447 .storage(Arc::new(InMemorySessionStorage::new()))
1448 .sse(false)
1449 .build()
1450 .await
1451 .unwrap();
1452
1453 let handler = server.handler().await.unwrap();
1454 assert!(
1455 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1456 "Stream manager must be initialized"
1457 );
1458 assert!(server.capabilities().tasks.is_none());
1459 }
1460
1461 #[derive(Clone, Default)]
1463 struct SlowTool;
1464
1465 impl HasBaseMetadata for SlowTool {
1466 fn name(&self) -> &str {
1467 "slow_tool"
1468 }
1469 }
1470
1471 impl HasDescription for SlowTool {
1472 fn description(&self) -> Option<&str> {
1473 Some("A slow tool for testing")
1474 }
1475 }
1476
1477 impl HasInputSchema for SlowTool {
1478 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1479 use turul_mcp_protocol::ToolSchema;
1480 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1481 SCHEMA.get_or_init(ToolSchema::object)
1482 }
1483 }
1484
1485 impl HasOutputSchema for SlowTool {
1486 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1487 None
1488 }
1489 }
1490
1491 impl HasAnnotations for SlowTool {
1492 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1493 None
1494 }
1495 }
1496
1497 impl HasToolMeta for SlowTool {
1498 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1499 None
1500 }
1501 }
1502
1503 impl HasIcons for SlowTool {}
1504 impl HasExecution for SlowTool {
1505 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1506 Some(turul_mcp_protocol::tools::ToolExecution {
1507 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1508 })
1509 }
1510 }
1511
1512 #[async_trait::async_trait]
1513 impl McpTool for SlowTool {
1514 async fn call(
1515 &self,
1516 _args: serde_json::Value,
1517 _session: Option<turul_mcp_server::SessionContext>,
1518 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1519 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1520 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1522 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1523 }
1524 }
1525
1526 #[tokio::test]
1527 async fn test_nonblocking_tools_call_with_task() {
1528 use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1529 use turul_mcp_server::SessionAwareToolHandler;
1530 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1531
1532 let task_storage = Arc::new(InMemoryTaskStorage::new());
1533 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1534 task_storage,
1535 ));
1536
1537 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1539 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1540
1541 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1543 Arc::new(InMemorySessionStorage::new());
1544 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1545 session_storage,
1546 turul_mcp_protocol::ServerCapabilities::default(),
1547 ));
1548
1549 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1551 .with_task_runtime(Arc::clone(&runtime));
1552
1553 let params = serde_json::json!({
1555 "name": "slow_tool",
1556 "arguments": {},
1557 "task": {}
1558 });
1559 let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1560 params
1561 .as_object()
1562 .unwrap()
1563 .iter()
1564 .map(|(k, v)| (k.clone(), v.clone()))
1565 .collect(),
1566 );
1567
1568 let start = std::time::Instant::now();
1570 let result = tool_handler
1571 .handle("tools/call", Some(request_params), None)
1572 .await;
1573 let elapsed = start.elapsed();
1574
1575 let value = result.expect("tools/call with task should succeed");
1577 assert!(
1578 value.get("task").is_some(),
1579 "Response should contain 'task' field (CreateTaskResult shape)"
1580 );
1581 let task = value.get("task").unwrap();
1582 assert!(
1583 task.get("taskId").is_some(),
1584 "Task should have taskId field"
1585 );
1586 assert_eq!(
1587 task.get("status")
1588 .and_then(|v| v.as_str())
1589 .unwrap_or_default(),
1590 "working",
1591 "Task status should be 'working'"
1592 );
1593
1594 assert!(
1598 elapsed < std::time::Duration::from_secs(1),
1599 "tools/call with task should return immediately (took {:?}, expected < 1s)",
1600 elapsed
1601 );
1602 }
1603
1604 #[derive(Clone)]
1610 struct StaticTestResource;
1611
1612 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1613 fn name(&self) -> &str {
1614 "static_test"
1615 }
1616 }
1617
1618 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1619 fn description(&self) -> Option<&str> {
1620 Some("Static test resource")
1621 }
1622 }
1623
1624 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1625 fn uri(&self) -> &str {
1626 "file:///test.txt"
1627 }
1628 }
1629
1630 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1631 fn mime_type(&self) -> Option<&str> {
1632 Some("text/plain")
1633 }
1634 }
1635
1636 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1637 fn size(&self) -> Option<u64> {
1638 None
1639 }
1640 }
1641
1642 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1643 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1644 None
1645 }
1646 }
1647
1648 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1649 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1650 None
1651 }
1652 }
1653
1654 impl HasIcons for StaticTestResource {}
1655
1656 #[async_trait::async_trait]
1657 impl McpResource for StaticTestResource {
1658 async fn read(
1659 &self,
1660 _params: Option<serde_json::Value>,
1661 _session: Option<&turul_mcp_server::SessionContext>,
1662 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>> {
1663 use turul_mcp_protocol::resources::ResourceContent;
1664 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1665 }
1666 }
1667
1668 #[derive(Clone)]
1670 struct TemplateTestResource;
1671
1672 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1673 fn name(&self) -> &str {
1674 "template_test"
1675 }
1676 }
1677
1678 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1679 fn description(&self) -> Option<&str> {
1680 Some("Template test resource")
1681 }
1682 }
1683
1684 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1685 fn uri(&self) -> &str {
1686 "agent://agents/{agent_id}"
1687 }
1688 }
1689
1690 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1691 fn mime_type(&self) -> Option<&str> {
1692 Some("application/json")
1693 }
1694 }
1695
1696 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1697 fn size(&self) -> Option<u64> {
1698 None
1699 }
1700 }
1701
1702 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1703 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1704 None
1705 }
1706 }
1707
1708 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1709 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1710 None
1711 }
1712 }
1713
1714 impl HasIcons for TemplateTestResource {}
1715
1716 #[async_trait::async_trait]
1717 impl McpResource for TemplateTestResource {
1718 async fn read(
1719 &self,
1720 _params: Option<serde_json::Value>,
1721 _session: Option<&turul_mcp_server::SessionContext>,
1722 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>> {
1723 use turul_mcp_protocol::resources::ResourceContent;
1724 Ok(vec![ResourceContent::text(
1725 "agent://agents/test",
1726 "{}",
1727 )])
1728 }
1729 }
1730
1731 #[test]
1732 fn test_resource_auto_detection_static() {
1733 let builder = LambdaMcpServerBuilder::new()
1734 .name("test")
1735 .resource(StaticTestResource);
1736
1737 assert_eq!(builder.resources.len(), 1);
1738 assert!(builder.resources.contains_key("file:///test.txt"));
1739 assert_eq!(builder.template_resources.len(), 0);
1740 }
1741
1742 #[test]
1743 fn test_resource_auto_detection_template() {
1744 let builder = LambdaMcpServerBuilder::new()
1745 .name("test")
1746 .resource(TemplateTestResource);
1747
1748 assert_eq!(builder.resources.len(), 0);
1749 assert_eq!(builder.template_resources.len(), 1);
1750
1751 let (template, _) = &builder.template_resources[0];
1752 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1753 }
1754
1755 #[test]
1756 fn test_resource_auto_detection_mixed() {
1757 let builder = LambdaMcpServerBuilder::new()
1758 .name("test")
1759 .resource(StaticTestResource)
1760 .resource(TemplateTestResource);
1761
1762 assert_eq!(builder.resources.len(), 1);
1763 assert!(builder.resources.contains_key("file:///test.txt"));
1764 assert_eq!(builder.template_resources.len(), 1);
1765
1766 let (template, _) = &builder.template_resources[0];
1767 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1768 }
1769
1770 #[tokio::test]
1771 async fn test_build_advertises_resources_capability_for_templates_only() {
1772 let server = LambdaMcpServerBuilder::new()
1773 .name("template-only")
1774 .resource(TemplateTestResource)
1775 .storage(Arc::new(InMemorySessionStorage::new()))
1776 .sse(false)
1777 .build()
1778 .await
1779 .unwrap();
1780
1781 assert!(
1782 server.capabilities().resources.is_some(),
1783 "Resources capability should be advertised when template resources are registered"
1784 );
1785 }
1786
1787 #[tokio::test]
1788 async fn test_build_advertises_resources_capability_for_static_only() {
1789 let server = LambdaMcpServerBuilder::new()
1790 .name("static-only")
1791 .resource(StaticTestResource)
1792 .storage(Arc::new(InMemorySessionStorage::new()))
1793 .sse(false)
1794 .build()
1795 .await
1796 .unwrap();
1797
1798 assert!(
1799 server.capabilities().resources.is_some(),
1800 "Resources capability should be advertised when static resources are registered"
1801 );
1802 }
1803
1804 #[tokio::test]
1805 async fn test_build_no_resources_no_capability() {
1806 let server = LambdaMcpServerBuilder::new()
1807 .name("no-resources")
1808 .tool(TestTool)
1809 .storage(Arc::new(InMemorySessionStorage::new()))
1810 .sse(false)
1811 .build()
1812 .await
1813 .unwrap();
1814
1815 assert!(
1816 server.capabilities().resources.is_none(),
1817 "Resources capability should NOT be advertised when no resources are registered"
1818 );
1819 }
1820
1821 #[tokio::test]
1822 async fn test_lambda_builder_templates_list_returns_template() {
1823 use turul_mcp_server::handlers::McpHandler;
1824
1825 let builder = LambdaMcpServerBuilder::new()
1827 .name("template-test")
1828 .resource(TemplateTestResource);
1829
1830 assert_eq!(builder.template_resources.len(), 1);
1832
1833 let handler =
1835 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1836
1837 let result = handler.handle(None).await.expect("should succeed");
1839
1840 let templates = result["resourceTemplates"]
1841 .as_array()
1842 .expect("resourceTemplates should be an array");
1843 assert_eq!(
1844 templates.len(),
1845 1,
1846 "Should have exactly 1 template resource"
1847 );
1848 assert_eq!(
1849 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1850 "Template URI should match"
1851 );
1852 assert_eq!(templates[0]["name"], "template_test");
1853 }
1854
1855 #[tokio::test]
1856 async fn test_lambda_builder_resources_list_returns_static() {
1857 use turul_mcp_server::handlers::McpHandler;
1858
1859 let builder = LambdaMcpServerBuilder::new()
1861 .name("static-test")
1862 .resource(StaticTestResource);
1863
1864 assert_eq!(builder.resources.len(), 1);
1865
1866 let mut handler = ResourcesHandler::new();
1867 for resource in builder.resources.values() {
1868 handler = handler.add_resource_arc(resource.clone());
1869 }
1870
1871 let result = handler.handle(None).await.expect("should succeed");
1872
1873 let resources = result["resources"]
1874 .as_array()
1875 .expect("resources should be an array");
1876 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1877 assert_eq!(resources[0]["uri"], "file:///test.txt");
1878 assert_eq!(resources[0]["name"], "static_test");
1879 }
1880
1881 #[tokio::test]
1882 async fn test_lambda_builder_mixed_resources_separation() {
1883 use turul_mcp_server::handlers::McpHandler;
1884
1885 let builder = LambdaMcpServerBuilder::new()
1887 .name("mixed-test")
1888 .resource(StaticTestResource)
1889 .resource(TemplateTestResource);
1890
1891 assert_eq!(builder.resources.len(), 1);
1892 assert_eq!(builder.template_resources.len(), 1);
1893
1894 let mut list_handler = ResourcesHandler::new();
1896 for resource in builder.resources.values() {
1897 list_handler = list_handler.add_resource_arc(resource.clone());
1898 }
1899
1900 let templates_handler =
1901 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1902
1903 let list_result = list_handler.handle(None).await.expect("should succeed");
1905 let resources = list_result["resources"]
1906 .as_array()
1907 .expect("resources should be an array");
1908 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1909 assert_eq!(resources[0]["uri"], "file:///test.txt");
1910
1911 let templates_result = templates_handler
1913 .handle(None)
1914 .await
1915 .expect("should succeed");
1916 let templates = templates_result["resourceTemplates"]
1917 .as_array()
1918 .expect("resourceTemplates should be an array");
1919 assert_eq!(
1920 templates.len(),
1921 1,
1922 "Only template resource in resources/templates/list"
1923 );
1924 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
1925 }
1926
1927 #[tokio::test]
1928 async fn test_tasks_get_route_registered() {
1929 use turul_mcp_server::TasksGetHandler;
1930 use turul_mcp_server::handlers::McpHandler;
1931 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1932
1933 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1934 Arc::new(InMemoryTaskStorage::new()),
1935 ));
1936 let handler = TasksGetHandler::new(runtime);
1937
1938 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
1941
1942 let result = handler.handle(Some(params)).await;
1943
1944 assert!(
1946 result.is_err(),
1947 "tasks/get with unknown task should return error"
1948 );
1949 let err = result.unwrap_err();
1950 let err_str = err.to_string();
1951 assert!(
1952 !err_str.contains("method not found"),
1953 "Error should not be 'method not found' — handler should respond to tasks/get"
1954 );
1955 }
1956}