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