1use std::collections::HashMap;
7use std::sync::Arc;
8
9use turul_http_mcp_server::{ServerConfig, StreamConfig};
10use turul_mcp_protocol::{Implementation, ServerCapabilities};
11use turul_mcp_server::handlers::{McpHandler, *};
12use turul_mcp_server::{
13 McpCompletion, McpElicitation, McpLogger, McpNotification, McpPrompt, McpResource, McpRoot,
14 McpSampling, McpTool,
15};
16use turul_mcp_session_storage::BoxedSessionStorage;
17
18use crate::error::Result;
19
20#[cfg(feature = "dynamodb")]
21use crate::error::LambdaError;
22use crate::server::LambdaMcpServer;
23
24#[cfg(feature = "cors")]
25use crate::cors::CorsConfig;
26
27pub struct LambdaMcpServerBuilder {
70 name: String,
72 version: String,
73 title: Option<String>,
74 icons: Option<Vec<turul_mcp_protocol::Icon>>,
75
76 capabilities: ServerCapabilities,
78
79 tools: HashMap<String, Arc<dyn McpTool>>,
81
82 resources: HashMap<String, Arc<dyn McpResource>>,
84
85 template_resources: Vec<(
87 turul_mcp_server::uri_template::UriTemplate,
88 Arc<dyn McpResource>,
89 )>,
90
91 prompts: HashMap<String, Arc<dyn McpPrompt>>,
93
94 elicitations: HashMap<String, Arc<dyn McpElicitation>>,
96
97 sampling: HashMap<String, Arc<dyn McpSampling>>,
99
100 completions: HashMap<String, Arc<dyn McpCompletion>>,
102
103 loggers: HashMap<String, Arc<dyn McpLogger>>,
105
106 root_providers: HashMap<String, Arc<dyn McpRoot>>,
108
109 notifications: HashMap<String, Arc<dyn McpNotification>>,
111
112 handlers: HashMap<String, Arc<dyn McpHandler>>,
114
115 roots: Vec<turul_mcp_protocol::roots::Root>,
117
118 instructions: Option<String>,
120
121 session_timeout_minutes: Option<u64>,
123 session_cleanup_interval_seconds: Option<u64>,
124
125 session_storage: Option<Arc<BoxedSessionStorage>>,
127
128 strict_lifecycle: bool,
130
131 enable_sse: bool,
133 server_config: ServerConfig,
135 stream_config: StreamConfig,
136
137 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
139
140 route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
142
143 task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
145 task_recovery_timeout_ms: u64,
147
148 #[cfg(feature = "cors")]
150 cors_config: Option<CorsConfig>,
151}
152
153impl LambdaMcpServerBuilder {
154 pub fn new() -> Self {
156 let capabilities = ServerCapabilities::default();
159
160 let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
162 handlers.insert("ping".to_string(), Arc::new(PingHandler));
163 handlers.insert(
164 "completion/complete".to_string(),
165 Arc::new(CompletionHandler),
166 );
167 handlers.insert(
168 "resources/list".to_string(),
169 Arc::new(ResourcesHandler::new()),
170 );
171 handlers.insert(
172 "prompts/list".to_string(),
173 Arc::new(PromptsListHandler::new()),
174 );
175 handlers.insert(
176 "prompts/get".to_string(),
177 Arc::new(PromptsGetHandler::new()),
178 );
179 handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
180 handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
181 handlers.insert(
182 "sampling/createMessage".to_string(),
183 Arc::new(SamplingHandler),
184 );
185 handlers.insert(
186 "resources/templates/list".to_string(),
187 Arc::new(ResourceTemplatesHandler::new()),
188 );
189 handlers.insert(
190 "elicitation/create".to_string(),
191 Arc::new(ElicitationHandler::with_mock_provider()),
192 );
193
194 let notifications_handler = Arc::new(NotificationsHandler);
196 handlers.insert(
197 "notifications/message".to_string(),
198 notifications_handler.clone(),
199 );
200 handlers.insert(
201 "notifications/progress".to_string(),
202 notifications_handler.clone(),
203 );
204 handlers.insert(
206 "notifications/resources/list_changed".to_string(),
207 notifications_handler.clone(),
208 );
209 handlers.insert(
210 "notifications/resources/updated".to_string(),
211 notifications_handler.clone(),
212 );
213 handlers.insert(
214 "notifications/tools/list_changed".to_string(),
215 notifications_handler.clone(),
216 );
217 handlers.insert(
218 "notifications/prompts/list_changed".to_string(),
219 notifications_handler.clone(),
220 );
221 handlers.insert(
222 "notifications/roots/list_changed".to_string(),
223 notifications_handler.clone(),
224 );
225 handlers.insert(
227 "notifications/resources/listChanged".to_string(),
228 notifications_handler.clone(),
229 );
230 handlers.insert(
231 "notifications/tools/listChanged".to_string(),
232 notifications_handler.clone(),
233 );
234 handlers.insert(
235 "notifications/prompts/listChanged".to_string(),
236 notifications_handler.clone(),
237 );
238 handlers.insert(
239 "notifications/roots/listChanged".to_string(),
240 notifications_handler,
241 );
242
243 Self {
244 name: "turul-mcp-aws-lambda".to_string(),
245 version: env!("CARGO_PKG_VERSION").to_string(),
246 title: None,
247 icons: None,
248 capabilities,
249 tools: HashMap::new(),
250 resources: HashMap::new(),
251 template_resources: Vec::new(),
252 prompts: HashMap::new(),
253 elicitations: HashMap::new(),
254 sampling: HashMap::new(),
255 completions: HashMap::new(),
256 loggers: HashMap::new(),
257 root_providers: HashMap::new(),
258 notifications: HashMap::new(),
259 handlers,
260 roots: Vec::new(),
261 instructions: None,
262 session_timeout_minutes: None,
263 session_cleanup_interval_seconds: None,
264 session_storage: None,
265 strict_lifecycle: true, enable_sse: cfg!(feature = "sse"),
267 server_config: ServerConfig::default(),
268 stream_config: StreamConfig::default(),
269 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
270 route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
271 task_runtime: None,
272 task_recovery_timeout_ms: 300_000, #[cfg(feature = "cors")]
274 cors_config: None,
275 }
276 }
277
278 pub fn name(mut self, name: impl Into<String>) -> Self {
280 self.name = name.into();
281 self
282 }
283
284 pub fn version(mut self, version: impl Into<String>) -> Self {
286 self.version = version.into();
287 self
288 }
289
290 pub fn title(mut self, title: impl Into<String>) -> Self {
292 self.title = Some(title.into());
293 self
294 }
295
296 pub fn icons(mut self, icons: Vec<turul_mcp_protocol::Icon>) -> Self {
298 self.icons = Some(icons);
299 self
300 }
301
302 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
304 self.instructions = Some(instructions.into());
305 self
306 }
307
308 pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
320 let name = tool.name().to_string();
321 self.tools.insert(name, Arc::new(tool));
322 self
323 }
324
325 pub fn tool_fn<F, T>(self, func: F) -> Self
327 where
328 F: Fn() -> T,
329 T: McpTool + 'static,
330 {
331 self.tool(func())
332 }
333
334 pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
336 for tool in tools {
337 self = self.tool(tool);
338 }
339 self
340 }
341
342 pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
348 let uri = resource.uri().to_string();
349
350 if uri.contains('{') && uri.contains('}') {
351 match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
353 Ok(template) => {
354 self.template_resources.push((template, Arc::new(resource)));
355 }
356 Err(e) => {
357 tracing::warn!(
358 "Failed to parse template resource URI '{}': {}. Registering as static.",
359 uri,
360 e
361 );
362 self.resources.insert(uri, Arc::new(resource));
363 }
364 }
365 } else {
366 self.resources.insert(uri, Arc::new(resource));
368 }
369 self
370 }
371
372 pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
374 mut self,
375 resources: I,
376 ) -> Self {
377 for resource in resources {
378 self = self.resource(resource);
379 }
380 self
381 }
382
383 pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
385 let name = prompt.name().to_string();
386 self.prompts.insert(name, Arc::new(prompt));
387 self
388 }
389
390 pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
392 mut self,
393 prompts: I,
394 ) -> Self {
395 for prompt in prompts {
396 self = self.prompt(prompt);
397 }
398 self
399 }
400
401 pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
403 let key = format!("elicitation_{}", self.elicitations.len());
404 self.elicitations.insert(key, Arc::new(elicitation));
405 self
406 }
407
408 pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
410 mut self,
411 elicitations: I,
412 ) -> Self {
413 for elicitation in elicitations {
414 self = self.elicitation(elicitation);
415 }
416 self
417 }
418
419 pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
421 let key = format!("sampling_{}", self.sampling.len());
422 self.sampling.insert(key, Arc::new(sampling));
423 self
424 }
425
426 pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
428 mut self,
429 sampling: I,
430 ) -> Self {
431 for s in sampling {
432 self = self.sampling_provider(s);
433 }
434 self
435 }
436
437 pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
439 let key = format!("completion_{}", self.completions.len());
440 self.completions.insert(key, Arc::new(completion));
441 self
442 }
443
444 pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
446 mut self,
447 completions: I,
448 ) -> Self {
449 for completion in completions {
450 self = self.completion_provider(completion);
451 }
452 self
453 }
454
455 pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
457 let key = format!("logger_{}", self.loggers.len());
458 self.loggers.insert(key, Arc::new(logger));
459 self
460 }
461
462 pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
464 mut self,
465 loggers: I,
466 ) -> Self {
467 for logger in loggers {
468 self = self.logger(logger);
469 }
470 self
471 }
472
473 pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
475 let key = format!("root_{}", self.root_providers.len());
476 self.root_providers.insert(key, Arc::new(root));
477 self
478 }
479
480 pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
482 mut self,
483 roots: I,
484 ) -> Self {
485 for root in roots {
486 self = self.root_provider(root);
487 }
488 self
489 }
490
491 pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
493 let key = format!("notification_{}", self.notifications.len());
494 self.notifications.insert(key, Arc::new(notification));
495 self
496 }
497
498 pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
500 mut self,
501 notifications: I,
502 ) -> Self {
503 for notification in notifications {
504 self = self.notification_provider(notification);
505 }
506 self
507 }
508
509 pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
515 self.sampling_provider(sampling)
516 }
517
518 pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
520 self.completion_provider(completion)
521 }
522
523 pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
525 let notification = N::default();
526 self.notification_provider(notification)
527 }
528
529 pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
531 let handler_arc = Arc::new(handler);
532 for method in handler_arc.supported_methods() {
533 self.handlers.insert(method, handler_arc.clone());
534 }
535 self
536 }
537
538 pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
540 mut self,
541 handlers: I,
542 ) -> Self {
543 for handler in handlers {
544 self = self.handler(handler);
545 }
546 self
547 }
548
549 pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
551 self.roots.push(root);
552 self
553 }
554
555 pub fn with_completion(mut self) -> Self {
561 use turul_mcp_protocol::initialize::CompletionsCapabilities;
562 self.capabilities.completions = Some(CompletionsCapabilities {
563 enabled: Some(true),
564 });
565 self.handler(CompletionHandler)
566 }
567
568 pub fn with_prompts(mut self) -> Self {
570 use turul_mcp_protocol::initialize::PromptsCapabilities;
571 self.capabilities.prompts = Some(PromptsCapabilities {
572 list_changed: Some(false),
573 });
574
575 self
578 }
579
580 pub fn with_resources(mut self) -> Self {
582 use turul_mcp_protocol::initialize::ResourcesCapabilities;
583 self.capabilities.resources = Some(ResourcesCapabilities {
584 subscribe: Some(false),
585 list_changed: Some(false),
586 });
587
588 let mut list_handler = ResourcesHandler::new();
590 for resource in self.resources.values() {
591 list_handler = list_handler.add_resource_arc(resource.clone());
592 }
593 self = self.handler(list_handler);
594
595 if !self.template_resources.is_empty() {
597 let templates_handler =
598 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
599 self = self.handler(templates_handler);
600 }
601
602 let mut read_handler = ResourcesReadHandler::new().without_security();
604 for resource in self.resources.values() {
605 read_handler = read_handler.add_resource_arc(resource.clone());
606 }
607 for (template, resource) in &self.template_resources {
608 read_handler =
609 read_handler.add_template_resource_arc(template.clone(), resource.clone());
610 }
611 self.handler(read_handler)
612 }
613
614 pub fn with_logging(mut self) -> Self {
616 use turul_mcp_protocol::initialize::LoggingCapabilities;
617 self.capabilities.logging = Some(LoggingCapabilities::default());
618 self.handler(LoggingHandler)
619 }
620
621 pub fn with_roots(self) -> Self {
623 self.handler(RootsHandler::new())
624 }
625
626 pub fn with_sampling(self) -> Self {
628 self.handler(SamplingHandler)
629 }
630
631 pub fn with_elicitation(self) -> Self {
633 self.handler(ElicitationHandler::with_mock_provider())
636 }
637
638 pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
640 self.handler(ElicitationHandler::new(Arc::new(provider)))
642 }
643
644 pub fn with_notifications(self) -> Self {
646 self.handler(NotificationsHandler)
647 }
648
649 pub fn with_task_storage(
664 mut self,
665 storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
666 ) -> Self {
667 let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
668 .with_recovery_timeout(self.task_recovery_timeout_ms);
669 self.task_runtime = Some(Arc::new(runtime));
670 self
671 }
672
673 pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
677 self.task_runtime = Some(runtime);
678 self
679 }
680
681 pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
686 self.task_recovery_timeout_ms = timeout_ms;
687 self
688 }
689
690 pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
696 self.session_timeout_minutes = Some(minutes);
697 self
698 }
699
700 pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
702 self.session_cleanup_interval_seconds = Some(seconds);
703 self
704 }
705
706 pub fn strict_lifecycle(mut self, strict: bool) -> Self {
708 self.strict_lifecycle = strict;
709 self
710 }
711
712 pub fn with_strict_lifecycle(self) -> Self {
714 self.strict_lifecycle(true)
715 }
716
717 pub fn sse(mut self, enable: bool) -> Self {
719 self.enable_sse = enable;
720
721 if enable {
723 self.server_config.enable_get_sse = true;
724 self.server_config.enable_post_sse = true;
725 } else {
726 self.server_config.enable_get_sse = false;
729 self.server_config.enable_post_sse = false;
730 }
731
732 self
733 }
734
735 pub fn with_long_sessions(mut self) -> Self {
737 self.session_timeout_minutes = Some(120); self.session_cleanup_interval_seconds = Some(300); self
740 }
741
742 pub fn with_short_sessions(mut self) -> Self {
744 self.session_timeout_minutes = Some(5); self.session_cleanup_interval_seconds = Some(30); self
747 }
748
749 pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
757 self.session_storage = Some(storage);
758 self
759 }
760
761 #[cfg(feature = "dynamodb")]
768 pub async fn dynamodb_storage(self) -> Result<Self> {
769 use turul_mcp_session_storage::DynamoDbSessionStorage;
770
771 let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
772 LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
773 })?;
774
775 Ok(self.storage(Arc::new(storage)))
776 }
777
778 pub fn middleware(
814 mut self,
815 middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
816 ) -> Self {
817 self.middleware_stack.push(middleware);
818 self
819 }
820
821 pub fn route(
823 mut self,
824 path: &str,
825 handler: Arc<dyn turul_http_mcp_server::RouteHandler>,
826 ) -> Self {
827 Arc::get_mut(&mut self.route_registry)
828 .expect("route_registry must not be shared during build")
829 .add_route(path, handler);
830 self
831 }
832
833 pub fn server_config(mut self, config: ServerConfig) -> Self {
835 self.server_config = config;
836 self
837 }
838
839 pub fn stream_config(mut self, config: StreamConfig) -> Self {
841 self.stream_config = config;
842 self
843 }
844
845 #[cfg(feature = "cors")]
849 pub fn cors(mut self, config: CorsConfig) -> Self {
850 self.cors_config = Some(config);
851 self
852 }
853
854 #[cfg(feature = "cors")]
856 pub fn cors_allow_all_origins(mut self) -> Self {
857 self.cors_config = Some(CorsConfig::allow_all());
858 self
859 }
860
861 #[cfg(feature = "cors")]
863 pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
864 self.cors_config = Some(CorsConfig::for_origins(origins));
865 self
866 }
867
868 #[cfg(feature = "cors")]
875 pub fn cors_from_env(mut self) -> Self {
876 self.cors_config = Some(CorsConfig::from_env());
877 self
878 }
879
880 #[cfg(feature = "cors")]
882 pub fn cors_disabled(self) -> Self {
883 self
885 }
886
887 #[cfg(all(feature = "dynamodb", feature = "cors"))]
893 pub async fn production_config(self) -> Result<Self> {
894 Ok(self.dynamodb_storage().await?.cors_from_env())
895 }
896
897 #[cfg(feature = "cors")]
901 pub fn development_config(self) -> Self {
902 use turul_mcp_session_storage::InMemorySessionStorage;
903
904 self.storage(Arc::new(InMemorySessionStorage::new()))
905 .cors_allow_all_origins()
906 }
907
908 pub async fn build(self) -> Result<LambdaMcpServer> {
912 use turul_mcp_session_storage::InMemorySessionStorage;
913
914 if self.name.is_empty() {
916 return Err(crate::error::LambdaError::Configuration(
917 "Server name cannot be empty".to_string(),
918 ));
919 }
920 if self.version.is_empty() {
921 return Err(crate::error::LambdaError::Configuration(
922 "Server version cannot be empty".to_string(),
923 ));
924 }
925
926 let session_storage = self
932 .session_storage
933 .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
934
935 let mut implementation = Implementation::new(&self.name, &self.version);
937 if let Some(title) = self.title {
938 implementation = implementation.with_title(title);
939 }
940 if let Some(icons) = self.icons {
941 implementation = implementation.with_icons(icons);
942 }
943
944 let mut capabilities = self.capabilities.clone();
946 let has_tools = !self.tools.is_empty();
947 let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
948 let has_prompts = !self.prompts.is_empty();
949 let has_elicitations = !self.elicitations.is_empty();
950 let has_completions = !self.completions.is_empty();
951 let has_logging = !self.loggers.is_empty();
952 tracing::debug!("🔧 Has logging configured: {}", has_logging);
953
954 if has_tools {
956 capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
957 list_changed: Some(false), });
959 }
960
961 if has_resources {
963 capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
964 subscribe: Some(false), list_changed: Some(false), });
967 }
968
969 if has_prompts {
971 capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
972 list_changed: Some(false), });
974 }
975
976 let _ = has_elicitations; if has_completions {
982 capabilities.completions =
983 Some(turul_mcp_protocol::initialize::CompletionsCapabilities {
984 enabled: Some(true),
985 });
986 }
987
988 capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities {
991 enabled: Some(true),
992 levels: Some(vec![
993 "debug".to_string(),
994 "info".to_string(),
995 "warning".to_string(),
996 "error".to_string(),
997 ]),
998 });
999
1000 if self.task_runtime.is_some() {
1002 use turul_mcp_protocol::initialize::*;
1003 capabilities.tasks = Some(TasksCapabilities {
1004 list: Some(TasksListCapabilities::default()),
1005 cancel: Some(TasksCancelCapabilities::default()),
1006 requests: Some(TasksRequestCapabilities {
1007 tools: Some(TasksToolCapabilities {
1008 call: Some(TasksToolCallCapabilities::default()),
1009 extra: Default::default(),
1010 }),
1011 extra: Default::default(),
1012 }),
1013 extra: Default::default(),
1014 });
1015 }
1016
1017 let mut handlers = self.handlers;
1019 if !self.roots.is_empty() {
1020 let mut roots_handler = RootsHandler::new();
1021 for root in &self.roots {
1022 roots_handler = roots_handler.add_root(root.clone());
1023 }
1024 handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1025 }
1026
1027 if let Some(ref runtime) = self.task_runtime {
1029 use turul_mcp_server::{
1030 TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1031 };
1032 handlers.insert(
1033 "tasks/get".to_string(),
1034 Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1035 );
1036 handlers.insert(
1037 "tasks/list".to_string(),
1038 Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1039 );
1040 handlers.insert(
1041 "tasks/cancel".to_string(),
1042 Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1043 );
1044 handlers.insert(
1045 "tasks/result".to_string(),
1046 Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1047 );
1048 }
1049
1050 if has_resources {
1052 let mut list_handler = ResourcesHandler::new();
1054 for resource in self.resources.values() {
1055 list_handler = list_handler.add_resource_arc(resource.clone());
1056 }
1057 handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1058
1059 if !self.template_resources.is_empty() {
1061 let templates_handler =
1062 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1063 handlers.insert(
1064 "resources/templates/list".to_string(),
1065 Arc::new(templates_handler),
1066 );
1067 }
1068
1069 let mut read_handler = ResourcesReadHandler::new().without_security();
1071 for resource in self.resources.values() {
1072 read_handler = read_handler.add_resource_arc(resource.clone());
1073 }
1074 for (template, resource) in &self.template_resources {
1075 read_handler =
1076 read_handler.add_template_resource_arc(template.clone(), resource.clone());
1077 }
1078 handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1079 }
1080
1081 Ok(LambdaMcpServer::new(
1083 implementation,
1084 capabilities,
1085 self.tools,
1086 self.resources,
1087 self.prompts,
1088 self.elicitations,
1089 self.sampling,
1090 self.completions,
1091 self.loggers,
1092 self.root_providers,
1093 self.notifications,
1094 handlers,
1095 self.roots,
1096 self.instructions,
1097 session_storage,
1098 self.strict_lifecycle,
1099 self.server_config,
1100 self.enable_sse,
1101 self.stream_config,
1102 #[cfg(feature = "cors")]
1103 self.cors_config,
1104 self.middleware_stack,
1105 self.route_registry,
1106 self.task_runtime,
1107 ))
1108 }
1109}
1110
1111impl Default for LambdaMcpServerBuilder {
1112 fn default() -> Self {
1113 Self::new()
1114 }
1115}
1116
1117pub trait LambdaMcpServerBuilderExt {
1119 fn tools<I, T>(self, tools: I) -> Self
1121 where
1122 I: IntoIterator<Item = T>,
1123 T: McpTool + 'static;
1124}
1125
1126impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1127 fn tools<I, T>(mut self, tools: I) -> Self
1128 where
1129 I: IntoIterator<Item = T>,
1130 T: McpTool + 'static,
1131 {
1132 for tool in tools {
1133 self = self.tool(tool);
1134 }
1135 self
1136 }
1137}
1138
1139pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1144where
1145 I: IntoIterator<Item = T>,
1146 T: McpTool + 'static,
1147{
1148 let mut builder = LambdaMcpServerBuilder::new();
1149
1150 for tool in tools {
1151 builder = builder.tool(tool);
1152 }
1153
1154 #[cfg(feature = "cors")]
1155 {
1156 builder = builder.cors_allow_all_origins();
1157 }
1158
1159 builder.sse(false).build().await
1160}
1161
1162#[cfg(all(feature = "dynamodb", feature = "cors"))]
1166pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1167where
1168 I: IntoIterator<Item = T>,
1169 T: McpTool + 'static,
1170{
1171 let mut builder = LambdaMcpServerBuilder::new();
1172
1173 for tool in tools {
1174 builder = builder.tool(tool);
1175 }
1176
1177 builder.production_config().await?.build().await
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183 use turul_mcp_builders::prelude::*;
1184 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1188 struct TestTool;
1189
1190 impl HasBaseMetadata for TestTool {
1191 fn name(&self) -> &str {
1192 "test_tool"
1193 }
1194 }
1195
1196 impl HasDescription for TestTool {
1197 fn description(&self) -> Option<&str> {
1198 Some("Test tool")
1199 }
1200 }
1201
1202 impl HasInputSchema for TestTool {
1203 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1204 use turul_mcp_protocol::ToolSchema;
1205 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1206 SCHEMA.get_or_init(ToolSchema::object)
1207 }
1208 }
1209
1210 impl HasOutputSchema for TestTool {
1211 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1212 None
1213 }
1214 }
1215
1216 impl HasAnnotations for TestTool {
1217 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1218 None
1219 }
1220 }
1221
1222 impl HasToolMeta for TestTool {
1223 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1224 None
1225 }
1226 }
1227
1228 impl HasIcons for TestTool {}
1229 impl HasExecution for TestTool {}
1230
1231 #[async_trait::async_trait]
1232 impl McpTool for TestTool {
1233 async fn call(
1234 &self,
1235 _args: serde_json::Value,
1236 _session: Option<turul_mcp_server::SessionContext>,
1237 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1238 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1239 Ok(CallToolResult::success(vec![ToolResult::text(
1240 "test result",
1241 )]))
1242 }
1243 }
1244
1245 #[tokio::test]
1246 async fn test_builder_basic() {
1247 let server = LambdaMcpServerBuilder::new()
1248 .name("test-server")
1249 .version("1.0.0")
1250 .tool(TestTool)
1251 .storage(Arc::new(InMemorySessionStorage::new()))
1252 .sse(false) .build()
1254 .await
1255 .unwrap();
1256
1257 let handler = server.handler().await.unwrap();
1259 assert!(
1261 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1262 "Stream manager must be initialized"
1263 );
1264 }
1265
1266 #[tokio::test]
1267 async fn test_simple_lambda_server() {
1268 let tools = vec![TestTool];
1269 let server = simple_lambda_server(tools).await.unwrap();
1270
1271 let handler = server.handler().await.unwrap();
1273 assert!(
1276 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1277 "Stream manager must be initialized"
1278 );
1279 }
1280
1281 #[tokio::test]
1282 async fn test_builder_extension_trait() {
1283 let tools = vec![TestTool, TestTool];
1284
1285 let server = LambdaMcpServerBuilder::new()
1286 .tools(tools)
1287 .storage(Arc::new(InMemorySessionStorage::new()))
1288 .sse(false) .build()
1290 .await
1291 .unwrap();
1292
1293 let handler = server.handler().await.unwrap();
1294 assert!(
1297 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1298 "Stream manager must be initialized"
1299 );
1300 }
1301
1302 #[cfg(feature = "cors")]
1303 #[tokio::test]
1304 async fn test_cors_configuration() {
1305 let server = LambdaMcpServerBuilder::new()
1306 .cors_allow_all_origins()
1307 .storage(Arc::new(InMemorySessionStorage::new()))
1308 .sse(false) .build()
1310 .await
1311 .unwrap();
1312
1313 let handler = server.handler().await.unwrap();
1314 assert!(
1317 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1318 "Stream manager must be initialized"
1319 );
1320 }
1321
1322 #[tokio::test]
1323 async fn test_sse_toggle_functionality() {
1324 let mut builder =
1326 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1327
1328 builder = builder.sse(true);
1330 assert!(builder.enable_sse, "SSE should be enabled");
1331 assert!(
1332 builder.server_config.enable_get_sse,
1333 "GET SSE endpoint should be enabled"
1334 );
1335 assert!(
1336 builder.server_config.enable_post_sse,
1337 "POST SSE endpoint should be enabled"
1338 );
1339
1340 builder = builder.sse(false);
1342 assert!(!builder.enable_sse, "SSE should be disabled");
1343 assert!(
1344 !builder.server_config.enable_get_sse,
1345 "GET SSE endpoint should be disabled"
1346 );
1347 assert!(
1348 !builder.server_config.enable_post_sse,
1349 "POST SSE endpoint should be disabled"
1350 );
1351
1352 builder = builder.sse(true);
1354 assert!(builder.enable_sse, "SSE should be re-enabled");
1355 assert!(
1356 builder.server_config.enable_get_sse,
1357 "GET SSE endpoint should be re-enabled"
1358 );
1359 assert!(
1360 builder.server_config.enable_post_sse,
1361 "POST SSE endpoint should be re-enabled"
1362 );
1363
1364 let server = builder.build().await.unwrap();
1366 let handler = server.handler().await.unwrap();
1367 assert!(
1368 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1369 "Stream manager must be initialized"
1370 );
1371 }
1372
1373 #[tokio::test]
1378 async fn test_builder_without_tasks_no_capability() {
1379 let server = LambdaMcpServerBuilder::new()
1380 .name("no-tasks")
1381 .tool(TestTool)
1382 .storage(Arc::new(InMemorySessionStorage::new()))
1383 .sse(false)
1384 .build()
1385 .await
1386 .unwrap();
1387
1388 assert!(
1389 server.capabilities().tasks.is_none(),
1390 "Tasks capability should not be advertised without task storage"
1391 );
1392 }
1393
1394 #[tokio::test]
1395 async fn test_builder_with_task_storage_advertises_capability() {
1396 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1397
1398 let server = LambdaMcpServerBuilder::new()
1399 .name("with-tasks")
1400 .tool(TestTool)
1401 .storage(Arc::new(InMemorySessionStorage::new()))
1402 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1403 .sse(false)
1404 .build()
1405 .await
1406 .unwrap();
1407
1408 let tasks_cap = server
1409 .capabilities()
1410 .tasks
1411 .as_ref()
1412 .expect("Tasks capability should be advertised");
1413 assert!(tasks_cap.list.is_some(), "list capability should be set");
1414 assert!(
1415 tasks_cap.cancel.is_some(),
1416 "cancel capability should be set"
1417 );
1418 let requests = tasks_cap
1419 .requests
1420 .as_ref()
1421 .expect("requests capability should be set");
1422 let tools = requests
1423 .tools
1424 .as_ref()
1425 .expect("tools capability should be set");
1426 assert!(tools.call.is_some(), "tools.call capability should be set");
1427 }
1428
1429 #[tokio::test]
1430 async fn test_builder_with_task_runtime_advertises_capability() {
1431 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1432
1433 let server = LambdaMcpServerBuilder::new()
1434 .name("with-runtime")
1435 .tool(TestTool)
1436 .storage(Arc::new(InMemorySessionStorage::new()))
1437 .with_task_runtime(runtime)
1438 .sse(false)
1439 .build()
1440 .await
1441 .unwrap();
1442
1443 assert!(
1444 server.capabilities().tasks.is_some(),
1445 "Tasks capability should be advertised with task runtime"
1446 );
1447 }
1448
1449 #[tokio::test]
1450 async fn test_task_recovery_timeout_configuration() {
1451 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1452
1453 let server = LambdaMcpServerBuilder::new()
1454 .name("custom-timeout")
1455 .tool(TestTool)
1456 .storage(Arc::new(InMemorySessionStorage::new()))
1457 .task_recovery_timeout_ms(60_000)
1458 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1459 .sse(false)
1460 .build()
1461 .await
1462 .unwrap();
1463
1464 assert!(
1465 server.capabilities().tasks.is_some(),
1466 "Tasks should be enabled with custom timeout"
1467 );
1468 }
1469
1470 #[tokio::test]
1471 async fn test_backward_compatibility_no_tasks() {
1472 let server = LambdaMcpServerBuilder::new()
1474 .name("backward-compat")
1475 .version("1.0.0")
1476 .tool(TestTool)
1477 .storage(Arc::new(InMemorySessionStorage::new()))
1478 .sse(false)
1479 .build()
1480 .await
1481 .unwrap();
1482
1483 let handler = server.handler().await.unwrap();
1484 assert!(
1485 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1486 "Stream manager must be initialized"
1487 );
1488 assert!(server.capabilities().tasks.is_none());
1489 }
1490
1491 #[derive(Clone, Default)]
1493 struct SlowTool;
1494
1495 impl HasBaseMetadata for SlowTool {
1496 fn name(&self) -> &str {
1497 "slow_tool"
1498 }
1499 }
1500
1501 impl HasDescription for SlowTool {
1502 fn description(&self) -> Option<&str> {
1503 Some("A slow tool for testing")
1504 }
1505 }
1506
1507 impl HasInputSchema for SlowTool {
1508 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1509 use turul_mcp_protocol::ToolSchema;
1510 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1511 SCHEMA.get_or_init(ToolSchema::object)
1512 }
1513 }
1514
1515 impl HasOutputSchema for SlowTool {
1516 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1517 None
1518 }
1519 }
1520
1521 impl HasAnnotations for SlowTool {
1522 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1523 None
1524 }
1525 }
1526
1527 impl HasToolMeta for SlowTool {
1528 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1529 None
1530 }
1531 }
1532
1533 impl HasIcons for SlowTool {}
1534 impl HasExecution for SlowTool {
1535 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1536 Some(turul_mcp_protocol::tools::ToolExecution {
1537 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1538 })
1539 }
1540 }
1541
1542 #[async_trait::async_trait]
1543 impl McpTool for SlowTool {
1544 async fn call(
1545 &self,
1546 _args: serde_json::Value,
1547 _session: Option<turul_mcp_server::SessionContext>,
1548 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1549 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1550 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1552 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1553 }
1554 }
1555
1556 #[tokio::test]
1557 async fn test_nonblocking_tools_call_with_task() {
1558 use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1559 use turul_mcp_server::SessionAwareToolHandler;
1560 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1561
1562 let task_storage = Arc::new(InMemoryTaskStorage::new());
1563 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1564 task_storage,
1565 ));
1566
1567 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1569 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1570
1571 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1573 Arc::new(InMemorySessionStorage::new());
1574 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1575 session_storage,
1576 turul_mcp_protocol::ServerCapabilities::default(),
1577 ));
1578
1579 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1581 .with_task_runtime(Arc::clone(&runtime));
1582
1583 let params = serde_json::json!({
1585 "name": "slow_tool",
1586 "arguments": {},
1587 "task": {}
1588 });
1589 let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1590 params
1591 .as_object()
1592 .unwrap()
1593 .iter()
1594 .map(|(k, v)| (k.clone(), v.clone()))
1595 .collect(),
1596 );
1597
1598 let start = std::time::Instant::now();
1600 let result = tool_handler
1601 .handle("tools/call", Some(request_params), None)
1602 .await;
1603 let elapsed = start.elapsed();
1604
1605 let value = result.expect("tools/call with task should succeed");
1607 assert!(
1608 value.get("task").is_some(),
1609 "Response should contain 'task' field (CreateTaskResult shape)"
1610 );
1611 let task = value.get("task").unwrap();
1612 assert!(
1613 task.get("taskId").is_some(),
1614 "Task should have taskId field"
1615 );
1616 assert_eq!(
1617 task.get("status")
1618 .and_then(|v| v.as_str())
1619 .unwrap_or_default(),
1620 "working",
1621 "Task status should be 'working'"
1622 );
1623
1624 assert!(
1628 elapsed < std::time::Duration::from_secs(1),
1629 "tools/call with task should return immediately (took {:?}, expected < 1s)",
1630 elapsed
1631 );
1632 }
1633
1634 #[derive(Clone)]
1640 struct StaticTestResource;
1641
1642 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1643 fn name(&self) -> &str {
1644 "static_test"
1645 }
1646 }
1647
1648 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1649 fn description(&self) -> Option<&str> {
1650 Some("Static test resource")
1651 }
1652 }
1653
1654 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1655 fn uri(&self) -> &str {
1656 "file:///test.txt"
1657 }
1658 }
1659
1660 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1661 fn mime_type(&self) -> Option<&str> {
1662 Some("text/plain")
1663 }
1664 }
1665
1666 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1667 fn size(&self) -> Option<u64> {
1668 None
1669 }
1670 }
1671
1672 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1673 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1674 None
1675 }
1676 }
1677
1678 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1679 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1680 None
1681 }
1682 }
1683
1684 impl HasIcons for StaticTestResource {}
1685
1686 #[async_trait::async_trait]
1687 impl McpResource for StaticTestResource {
1688 async fn read(
1689 &self,
1690 _params: Option<serde_json::Value>,
1691 _session: Option<&turul_mcp_server::SessionContext>,
1692 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1693 {
1694 use turul_mcp_protocol::resources::ResourceContent;
1695 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1696 }
1697 }
1698
1699 #[derive(Clone)]
1701 struct TemplateTestResource;
1702
1703 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1704 fn name(&self) -> &str {
1705 "template_test"
1706 }
1707 }
1708
1709 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1710 fn description(&self) -> Option<&str> {
1711 Some("Template test resource")
1712 }
1713 }
1714
1715 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1716 fn uri(&self) -> &str {
1717 "agent://agents/{agent_id}"
1718 }
1719 }
1720
1721 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1722 fn mime_type(&self) -> Option<&str> {
1723 Some("application/json")
1724 }
1725 }
1726
1727 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1728 fn size(&self) -> Option<u64> {
1729 None
1730 }
1731 }
1732
1733 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1734 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1735 None
1736 }
1737 }
1738
1739 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1740 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1741 None
1742 }
1743 }
1744
1745 impl HasIcons for TemplateTestResource {}
1746
1747 #[async_trait::async_trait]
1748 impl McpResource for TemplateTestResource {
1749 async fn read(
1750 &self,
1751 _params: Option<serde_json::Value>,
1752 _session: Option<&turul_mcp_server::SessionContext>,
1753 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1754 {
1755 use turul_mcp_protocol::resources::ResourceContent;
1756 Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
1757 }
1758 }
1759
1760 #[test]
1761 fn test_resource_auto_detection_static() {
1762 let builder = LambdaMcpServerBuilder::new()
1763 .name("test")
1764 .resource(StaticTestResource);
1765
1766 assert_eq!(builder.resources.len(), 1);
1767 assert!(builder.resources.contains_key("file:///test.txt"));
1768 assert_eq!(builder.template_resources.len(), 0);
1769 }
1770
1771 #[test]
1772 fn test_resource_auto_detection_template() {
1773 let builder = LambdaMcpServerBuilder::new()
1774 .name("test")
1775 .resource(TemplateTestResource);
1776
1777 assert_eq!(builder.resources.len(), 0);
1778 assert_eq!(builder.template_resources.len(), 1);
1779
1780 let (template, _) = &builder.template_resources[0];
1781 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1782 }
1783
1784 #[test]
1785 fn test_resource_auto_detection_mixed() {
1786 let builder = LambdaMcpServerBuilder::new()
1787 .name("test")
1788 .resource(StaticTestResource)
1789 .resource(TemplateTestResource);
1790
1791 assert_eq!(builder.resources.len(), 1);
1792 assert!(builder.resources.contains_key("file:///test.txt"));
1793 assert_eq!(builder.template_resources.len(), 1);
1794
1795 let (template, _) = &builder.template_resources[0];
1796 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1797 }
1798
1799 #[tokio::test]
1800 async fn test_build_advertises_resources_capability_for_templates_only() {
1801 let server = LambdaMcpServerBuilder::new()
1802 .name("template-only")
1803 .resource(TemplateTestResource)
1804 .storage(Arc::new(InMemorySessionStorage::new()))
1805 .sse(false)
1806 .build()
1807 .await
1808 .unwrap();
1809
1810 assert!(
1811 server.capabilities().resources.is_some(),
1812 "Resources capability should be advertised when template resources are registered"
1813 );
1814 }
1815
1816 #[tokio::test]
1817 async fn test_build_advertises_resources_capability_for_static_only() {
1818 let server = LambdaMcpServerBuilder::new()
1819 .name("static-only")
1820 .resource(StaticTestResource)
1821 .storage(Arc::new(InMemorySessionStorage::new()))
1822 .sse(false)
1823 .build()
1824 .await
1825 .unwrap();
1826
1827 assert!(
1828 server.capabilities().resources.is_some(),
1829 "Resources capability should be advertised when static resources are registered"
1830 );
1831 }
1832
1833 #[tokio::test]
1834 async fn test_build_no_resources_no_capability() {
1835 let server = LambdaMcpServerBuilder::new()
1836 .name("no-resources")
1837 .tool(TestTool)
1838 .storage(Arc::new(InMemorySessionStorage::new()))
1839 .sse(false)
1840 .build()
1841 .await
1842 .unwrap();
1843
1844 assert!(
1845 server.capabilities().resources.is_none(),
1846 "Resources capability should NOT be advertised when no resources are registered"
1847 );
1848 }
1849
1850 #[tokio::test]
1851 async fn test_lambda_builder_templates_list_returns_template() {
1852 use turul_mcp_server::handlers::McpHandler;
1853
1854 let builder = LambdaMcpServerBuilder::new()
1856 .name("template-test")
1857 .resource(TemplateTestResource);
1858
1859 assert_eq!(builder.template_resources.len(), 1);
1861
1862 let handler =
1864 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1865
1866 let result = handler.handle(None).await.expect("should succeed");
1868
1869 let templates = result["resourceTemplates"]
1870 .as_array()
1871 .expect("resourceTemplates should be an array");
1872 assert_eq!(
1873 templates.len(),
1874 1,
1875 "Should have exactly 1 template resource"
1876 );
1877 assert_eq!(
1878 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1879 "Template URI should match"
1880 );
1881 assert_eq!(templates[0]["name"], "template_test");
1882 }
1883
1884 #[tokio::test]
1885 async fn test_lambda_builder_resources_list_returns_static() {
1886 use turul_mcp_server::handlers::McpHandler;
1887
1888 let builder = LambdaMcpServerBuilder::new()
1890 .name("static-test")
1891 .resource(StaticTestResource);
1892
1893 assert_eq!(builder.resources.len(), 1);
1894
1895 let mut handler = ResourcesHandler::new();
1896 for resource in builder.resources.values() {
1897 handler = handler.add_resource_arc(resource.clone());
1898 }
1899
1900 let result = handler.handle(None).await.expect("should succeed");
1901
1902 let resources = result["resources"]
1903 .as_array()
1904 .expect("resources should be an array");
1905 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1906 assert_eq!(resources[0]["uri"], "file:///test.txt");
1907 assert_eq!(resources[0]["name"], "static_test");
1908 }
1909
1910 #[tokio::test]
1911 async fn test_lambda_builder_mixed_resources_separation() {
1912 use turul_mcp_server::handlers::McpHandler;
1913
1914 let builder = LambdaMcpServerBuilder::new()
1916 .name("mixed-test")
1917 .resource(StaticTestResource)
1918 .resource(TemplateTestResource);
1919
1920 assert_eq!(builder.resources.len(), 1);
1921 assert_eq!(builder.template_resources.len(), 1);
1922
1923 let mut list_handler = ResourcesHandler::new();
1925 for resource in builder.resources.values() {
1926 list_handler = list_handler.add_resource_arc(resource.clone());
1927 }
1928
1929 let templates_handler =
1930 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1931
1932 let list_result = list_handler.handle(None).await.expect("should succeed");
1934 let resources = list_result["resources"]
1935 .as_array()
1936 .expect("resources should be an array");
1937 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1938 assert_eq!(resources[0]["uri"], "file:///test.txt");
1939
1940 let templates_result = templates_handler
1942 .handle(None)
1943 .await
1944 .expect("should succeed");
1945 let templates = templates_result["resourceTemplates"]
1946 .as_array()
1947 .expect("resourceTemplates should be an array");
1948 assert_eq!(
1949 templates.len(),
1950 1,
1951 "Only template resource in resources/templates/list"
1952 );
1953 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
1954 }
1955
1956 #[tokio::test]
1957 async fn test_tasks_get_route_registered() {
1958 use turul_mcp_server::TasksGetHandler;
1959 use turul_mcp_server::handlers::McpHandler;
1960 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1961
1962 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1963 Arc::new(InMemoryTaskStorage::new()),
1964 ));
1965 let handler = TasksGetHandler::new(runtime);
1966
1967 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
1970
1971 let result = handler.handle(Some(params)).await;
1972
1973 assert!(
1975 result.is_err(),
1976 "tasks/get with unknown task should return error"
1977 );
1978 let err = result.unwrap_err();
1979 let err_str = err.to_string();
1980 assert!(
1981 !err_str.contains("method not found"),
1982 "Error should not be 'method not found' — handler should respond to tasks/get"
1983 );
1984 }
1985}