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 "resources/read".to_string(),
173 Arc::new(ResourcesReadHandler::new().without_security()),
174 );
175 handlers.insert(
176 "prompts/list".to_string(),
177 Arc::new(PromptsListHandler::new()),
178 );
179 handlers.insert(
180 "prompts/get".to_string(),
181 Arc::new(PromptsGetHandler::new()),
182 );
183 handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
184 handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
185 handlers.insert(
186 "sampling/createMessage".to_string(),
187 Arc::new(SamplingHandler),
188 );
189 handlers.insert(
192 "elicitation/create".to_string(),
193 Arc::new(ElicitationHandler::with_mock_provider()),
194 );
195
196 let notifications_handler = Arc::new(NotificationsHandler);
198 handlers.insert(
199 "notifications/message".to_string(),
200 notifications_handler.clone(),
201 );
202 handlers.insert(
203 "notifications/progress".to_string(),
204 notifications_handler.clone(),
205 );
206 handlers.insert(
208 "notifications/resources/list_changed".to_string(),
209 notifications_handler.clone(),
210 );
211 handlers.insert(
212 "notifications/resources/updated".to_string(),
213 notifications_handler.clone(),
214 );
215 handlers.insert(
216 "notifications/tools/list_changed".to_string(),
217 notifications_handler.clone(),
218 );
219 handlers.insert(
220 "notifications/prompts/list_changed".to_string(),
221 notifications_handler.clone(),
222 );
223 handlers.insert(
224 "notifications/roots/list_changed".to_string(),
225 notifications_handler.clone(),
226 );
227 handlers.insert(
229 "notifications/resources/listChanged".to_string(),
230 notifications_handler.clone(),
231 );
232 handlers.insert(
233 "notifications/tools/listChanged".to_string(),
234 notifications_handler.clone(),
235 );
236 handlers.insert(
237 "notifications/prompts/listChanged".to_string(),
238 notifications_handler.clone(),
239 );
240 handlers.insert(
241 "notifications/roots/listChanged".to_string(),
242 notifications_handler,
243 );
244
245 Self {
246 name: "turul-mcp-aws-lambda".to_string(),
247 version: env!("CARGO_PKG_VERSION").to_string(),
248 title: None,
249 icons: None,
250 capabilities,
251 tools: HashMap::new(),
252 resources: HashMap::new(),
253 template_resources: Vec::new(),
254 prompts: HashMap::new(),
255 elicitations: HashMap::new(),
256 sampling: HashMap::new(),
257 completions: HashMap::new(),
258 loggers: HashMap::new(),
259 root_providers: HashMap::new(),
260 notifications: HashMap::new(),
261 handlers,
262 roots: Vec::new(),
263 instructions: None,
264 session_timeout_minutes: None,
265 session_cleanup_interval_seconds: None,
266 session_storage: None,
267 strict_lifecycle: true, enable_sse: cfg!(feature = "sse"),
269 server_config: ServerConfig::default(),
270 stream_config: StreamConfig::default(),
271 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
272 route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
273 task_runtime: None,
274 task_recovery_timeout_ms: 300_000, #[cfg(feature = "cors")]
276 cors_config: None,
277 }
278 }
279
280 pub fn name(mut self, name: impl Into<String>) -> Self {
282 self.name = name.into();
283 self
284 }
285
286 pub fn version(mut self, version: impl Into<String>) -> Self {
288 self.version = version.into();
289 self
290 }
291
292 pub fn title(mut self, title: impl Into<String>) -> Self {
294 self.title = Some(title.into());
295 self
296 }
297
298 pub fn icons(mut self, icons: Vec<turul_mcp_protocol::Icon>) -> Self {
300 self.icons = Some(icons);
301 self
302 }
303
304 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
306 self.instructions = Some(instructions.into());
307 self
308 }
309
310 pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
322 let name = tool.name().to_string();
323 self.tools.insert(name, Arc::new(tool));
324 self
325 }
326
327 pub fn tool_fn<F, T>(self, func: F) -> Self
329 where
330 F: Fn() -> T,
331 T: McpTool + 'static,
332 {
333 self.tool(func())
334 }
335
336 pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
338 for tool in tools {
339 self = self.tool(tool);
340 }
341 self
342 }
343
344 pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
350 let uri = resource.uri().to_string();
351
352 if uri.contains('{') && uri.contains('}') {
353 match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
355 Ok(template) => {
356 self.template_resources.push((template, Arc::new(resource)));
357 }
358 Err(e) => {
359 tracing::warn!(
360 "Failed to parse template resource URI '{}': {}. Registering as static.",
361 uri,
362 e
363 );
364 self.resources.insert(uri, Arc::new(resource));
365 }
366 }
367 } else {
368 self.resources.insert(uri, Arc::new(resource));
370 }
371 self
372 }
373
374 pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
376 mut self,
377 resources: I,
378 ) -> Self {
379 for resource in resources {
380 self = self.resource(resource);
381 }
382 self
383 }
384
385 pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
387 let name = prompt.name().to_string();
388 self.prompts.insert(name, Arc::new(prompt));
389 self
390 }
391
392 pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
394 mut self,
395 prompts: I,
396 ) -> Self {
397 for prompt in prompts {
398 self = self.prompt(prompt);
399 }
400 self
401 }
402
403 pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
405 let key = format!("elicitation_{}", self.elicitations.len());
406 self.elicitations.insert(key, Arc::new(elicitation));
407 self
408 }
409
410 pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
412 mut self,
413 elicitations: I,
414 ) -> Self {
415 for elicitation in elicitations {
416 self = self.elicitation(elicitation);
417 }
418 self
419 }
420
421 pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
423 let key = format!("sampling_{}", self.sampling.len());
424 self.sampling.insert(key, Arc::new(sampling));
425 self
426 }
427
428 pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
430 mut self,
431 sampling: I,
432 ) -> Self {
433 for s in sampling {
434 self = self.sampling_provider(s);
435 }
436 self
437 }
438
439 pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
441 let key = format!("completion_{}", self.completions.len());
442 self.completions.insert(key, Arc::new(completion));
443 self
444 }
445
446 pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
448 mut self,
449 completions: I,
450 ) -> Self {
451 for completion in completions {
452 self = self.completion_provider(completion);
453 }
454 self
455 }
456
457 pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
459 let key = format!("logger_{}", self.loggers.len());
460 self.loggers.insert(key, Arc::new(logger));
461 self
462 }
463
464 pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
466 mut self,
467 loggers: I,
468 ) -> Self {
469 for logger in loggers {
470 self = self.logger(logger);
471 }
472 self
473 }
474
475 pub fn root_provider<R: McpRoot + 'static>(mut self, root: R) -> Self {
477 let key = format!("root_{}", self.root_providers.len());
478 self.root_providers.insert(key, Arc::new(root));
479 self
480 }
481
482 pub fn root_providers<R: McpRoot + 'static, I: IntoIterator<Item = R>>(
484 mut self,
485 roots: I,
486 ) -> Self {
487 for root in roots {
488 self = self.root_provider(root);
489 }
490 self
491 }
492
493 pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
495 let key = format!("notification_{}", self.notifications.len());
496 self.notifications.insert(key, Arc::new(notification));
497 self
498 }
499
500 pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
502 mut self,
503 notifications: I,
504 ) -> Self {
505 for notification in notifications {
506 self = self.notification_provider(notification);
507 }
508 self
509 }
510
511 pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
517 self.sampling_provider(sampling)
518 }
519
520 pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
522 self.completion_provider(completion)
523 }
524
525 pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
527 let notification = N::default();
528 self.notification_provider(notification)
529 }
530
531 pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
533 let handler_arc = Arc::new(handler);
534 for method in handler_arc.supported_methods() {
535 self.handlers.insert(method, handler_arc.clone());
536 }
537 self
538 }
539
540 pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
542 mut self,
543 handlers: I,
544 ) -> Self {
545 for handler in handlers {
546 self = self.handler(handler);
547 }
548 self
549 }
550
551 pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
553 self.roots.push(root);
554 self
555 }
556
557 pub fn with_completion(mut self) -> Self {
563 use turul_mcp_protocol::initialize::CompletionsCapabilities;
564 self.capabilities.completions = Some(CompletionsCapabilities {
565 enabled: Some(true),
566 });
567 self.handler(CompletionHandler)
568 }
569
570 pub fn with_prompts(mut self) -> Self {
572 use turul_mcp_protocol::initialize::PromptsCapabilities;
573 self.capabilities.prompts = Some(PromptsCapabilities {
574 list_changed: Some(false),
575 });
576
577 self
580 }
581
582 pub fn with_resources(mut self) -> Self {
584 use turul_mcp_protocol::initialize::ResourcesCapabilities;
585 self.capabilities.resources = Some(ResourcesCapabilities {
586 subscribe: Some(false),
587 list_changed: Some(false),
588 });
589
590 let mut list_handler = ResourcesHandler::new();
592 for resource in self.resources.values() {
593 list_handler = list_handler.add_resource_arc(resource.clone());
594 }
595 self = self.handler(list_handler);
596
597 if !self.template_resources.is_empty() {
599 let templates_handler =
600 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
601 self = self.handler(templates_handler);
602 }
603
604 let mut read_handler = ResourcesReadHandler::new().without_security();
606 for resource in self.resources.values() {
607 read_handler = read_handler.add_resource_arc(resource.clone());
608 }
609 for (template, resource) in &self.template_resources {
610 read_handler =
611 read_handler.add_template_resource_arc(template.clone(), resource.clone());
612 }
613 self.handler(read_handler)
614 }
615
616 pub fn with_logging(mut self) -> Self {
618 use turul_mcp_protocol::initialize::LoggingCapabilities;
619 self.capabilities.logging = Some(LoggingCapabilities::default());
620 self.handler(LoggingHandler)
621 }
622
623 pub fn with_roots(self) -> Self {
625 self.handler(RootsHandler::new())
626 }
627
628 pub fn with_sampling(self) -> Self {
630 self.handler(SamplingHandler)
631 }
632
633 pub fn with_elicitation(self) -> Self {
635 self.handler(ElicitationHandler::with_mock_provider())
638 }
639
640 pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
642 self.handler(ElicitationHandler::new(Arc::new(provider)))
644 }
645
646 pub fn with_notifications(self) -> Self {
648 self.handler(NotificationsHandler)
649 }
650
651 pub fn with_task_storage(
666 mut self,
667 storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
668 ) -> Self {
669 let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
670 .with_recovery_timeout(self.task_recovery_timeout_ms);
671 self.task_runtime = Some(Arc::new(runtime));
672 self
673 }
674
675 pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
679 self.task_runtime = Some(runtime);
680 self
681 }
682
683 pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
688 self.task_recovery_timeout_ms = timeout_ms;
689 self
690 }
691
692 pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
698 self.session_timeout_minutes = Some(minutes);
699 self
700 }
701
702 pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
704 self.session_cleanup_interval_seconds = Some(seconds);
705 self
706 }
707
708 pub fn strict_lifecycle(mut self, strict: bool) -> Self {
710 self.strict_lifecycle = strict;
711 self
712 }
713
714 pub fn with_strict_lifecycle(self) -> Self {
716 self.strict_lifecycle(true)
717 }
718
719 pub fn sse(mut self, enable: bool) -> Self {
721 self.enable_sse = enable;
722
723 if enable {
725 self.server_config.enable_get_sse = true;
726 self.server_config.enable_post_sse = true;
727 } else {
728 self.server_config.enable_get_sse = false;
731 self.server_config.enable_post_sse = false;
732 }
733
734 self
735 }
736
737 pub fn with_long_sessions(mut self) -> Self {
739 self.session_timeout_minutes = Some(120); self.session_cleanup_interval_seconds = Some(300); self
742 }
743
744 pub fn with_short_sessions(mut self) -> Self {
746 self.session_timeout_minutes = Some(5); self.session_cleanup_interval_seconds = Some(30); self
749 }
750
751 pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
759 self.session_storage = Some(storage);
760 self
761 }
762
763 #[cfg(feature = "dynamodb")]
770 pub async fn dynamodb_storage(self) -> Result<Self> {
771 use turul_mcp_session_storage::DynamoDbSessionStorage;
772
773 let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
774 LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
775 })?;
776
777 Ok(self.storage(Arc::new(storage)))
778 }
779
780 pub fn middleware(
816 mut self,
817 middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
818 ) -> Self {
819 self.middleware_stack.push(middleware);
820 self
821 }
822
823 pub fn route(
825 mut self,
826 path: &str,
827 handler: Arc<dyn turul_http_mcp_server::RouteHandler>,
828 ) -> Self {
829 Arc::get_mut(&mut self.route_registry)
830 .expect("route_registry must not be shared during build")
831 .add_route(path, handler);
832 self
833 }
834
835 pub fn server_config(mut self, config: ServerConfig) -> Self {
837 self.server_config = config;
838 self
839 }
840
841 pub fn stream_config(mut self, config: StreamConfig) -> Self {
843 self.stream_config = config;
844 self
845 }
846
847 #[cfg(feature = "cors")]
851 pub fn cors(mut self, config: CorsConfig) -> Self {
852 self.cors_config = Some(config);
853 self
854 }
855
856 #[cfg(feature = "cors")]
858 pub fn cors_allow_all_origins(mut self) -> Self {
859 self.cors_config = Some(CorsConfig::allow_all());
860 self
861 }
862
863 #[cfg(feature = "cors")]
865 pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
866 self.cors_config = Some(CorsConfig::for_origins(origins));
867 self
868 }
869
870 #[cfg(feature = "cors")]
877 pub fn cors_from_env(mut self) -> Self {
878 self.cors_config = Some(CorsConfig::from_env());
879 self
880 }
881
882 #[cfg(feature = "cors")]
884 pub fn cors_disabled(self) -> Self {
885 self
887 }
888
889 #[cfg(all(feature = "dynamodb", feature = "cors"))]
895 pub async fn production_config(self) -> Result<Self> {
896 Ok(self.dynamodb_storage().await?.cors_from_env())
897 }
898
899 #[cfg(feature = "cors")]
903 pub fn development_config(self) -> Self {
904 use turul_mcp_session_storage::InMemorySessionStorage;
905
906 self.storage(Arc::new(InMemorySessionStorage::new()))
907 .cors_allow_all_origins()
908 }
909
910 pub async fn build(self) -> Result<LambdaMcpServer> {
914 use turul_mcp_session_storage::InMemorySessionStorage;
915
916 if self.name.is_empty() {
918 return Err(crate::error::LambdaError::Configuration(
919 "Server name cannot be empty".to_string(),
920 ));
921 }
922 if self.version.is_empty() {
923 return Err(crate::error::LambdaError::Configuration(
924 "Server version cannot be empty".to_string(),
925 ));
926 }
927
928 let session_storage = self
934 .session_storage
935 .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
936
937 let mut implementation = Implementation::new(&self.name, &self.version);
939 if let Some(title) = self.title {
940 implementation = implementation.with_title(title);
941 }
942 if let Some(icons) = self.icons {
943 implementation = implementation.with_icons(icons);
944 }
945
946 let mut capabilities = self.capabilities.clone();
948 let has_tools = !self.tools.is_empty();
949 let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
950 let has_prompts = !self.prompts.is_empty();
951 let has_elicitations = !self.elicitations.is_empty();
952 let has_completions = !self.completions.is_empty();
953 let has_logging = !self.loggers.is_empty();
954 tracing::debug!("🔧 Has logging configured: {}", has_logging);
955
956 if has_tools {
958 capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
959 list_changed: Some(false), });
961 }
962
963 if has_resources {
965 capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
966 subscribe: Some(false), list_changed: Some(false), });
969 }
970
971 if has_prompts {
973 capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
974 list_changed: Some(false), });
976 }
977
978 let _ = has_elicitations; if has_completions {
984 capabilities.completions =
985 Some(turul_mcp_protocol::initialize::CompletionsCapabilities {
986 enabled: Some(true),
987 });
988 }
989
990 capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities {
993 enabled: Some(true),
994 levels: Some(vec![
995 "debug".to_string(),
996 "info".to_string(),
997 "warning".to_string(),
998 "error".to_string(),
999 ]),
1000 });
1001
1002 if self.task_runtime.is_some() {
1004 use turul_mcp_protocol::initialize::*;
1005 capabilities.tasks = Some(TasksCapabilities {
1006 list: Some(TasksListCapabilities::default()),
1007 cancel: Some(TasksCancelCapabilities::default()),
1008 requests: Some(TasksRequestCapabilities {
1009 tools: Some(TasksToolCapabilities {
1010 call: Some(TasksToolCallCapabilities::default()),
1011 extra: Default::default(),
1012 }),
1013 extra: Default::default(),
1014 }),
1015 extra: Default::default(),
1016 });
1017 }
1018
1019 let mut handlers = self.handlers;
1021 if !self.roots.is_empty() {
1022 let mut roots_handler = RootsHandler::new();
1023 for root in &self.roots {
1024 roots_handler = roots_handler.add_root(root.clone());
1025 }
1026 handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1027 }
1028
1029 if let Some(ref runtime) = self.task_runtime {
1031 use turul_mcp_server::{
1032 TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1033 };
1034 handlers.insert(
1035 "tasks/get".to_string(),
1036 Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1037 );
1038 handlers.insert(
1039 "tasks/list".to_string(),
1040 Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1041 );
1042 handlers.insert(
1043 "tasks/cancel".to_string(),
1044 Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1045 );
1046 handlers.insert(
1047 "tasks/result".to_string(),
1048 Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1049 );
1050 }
1051
1052 if has_resources {
1054 let mut list_handler = ResourcesHandler::new();
1056 for resource in self.resources.values() {
1057 list_handler = list_handler.add_resource_arc(resource.clone());
1058 }
1059 handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1060
1061 if !self.template_resources.is_empty() {
1063 let templates_handler =
1064 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1065 handlers.insert(
1066 "resources/templates/list".to_string(),
1067 Arc::new(templates_handler),
1068 );
1069 }
1070
1071 let mut read_handler = ResourcesReadHandler::new().without_security();
1073 for resource in self.resources.values() {
1074 read_handler = read_handler.add_resource_arc(resource.clone());
1075 }
1076 for (template, resource) in &self.template_resources {
1077 read_handler =
1078 read_handler.add_template_resource_arc(template.clone(), resource.clone());
1079 }
1080 handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1081 }
1082
1083 Ok(LambdaMcpServer::new(
1085 implementation,
1086 capabilities,
1087 self.tools,
1088 self.resources,
1089 self.prompts,
1090 self.elicitations,
1091 self.sampling,
1092 self.completions,
1093 self.loggers,
1094 self.root_providers,
1095 self.notifications,
1096 handlers,
1097 self.roots,
1098 self.instructions,
1099 session_storage,
1100 self.strict_lifecycle,
1101 self.server_config,
1102 self.enable_sse,
1103 self.stream_config,
1104 #[cfg(feature = "cors")]
1105 self.cors_config,
1106 self.middleware_stack,
1107 self.route_registry,
1108 self.task_runtime,
1109 ))
1110 }
1111}
1112
1113impl Default for LambdaMcpServerBuilder {
1114 fn default() -> Self {
1115 Self::new()
1116 }
1117}
1118
1119pub trait LambdaMcpServerBuilderExt {
1121 fn tools<I, T>(self, tools: I) -> Self
1123 where
1124 I: IntoIterator<Item = T>,
1125 T: McpTool + 'static;
1126}
1127
1128impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1129 fn tools<I, T>(mut self, tools: I) -> Self
1130 where
1131 I: IntoIterator<Item = T>,
1132 T: McpTool + 'static,
1133 {
1134 for tool in tools {
1135 self = self.tool(tool);
1136 }
1137 self
1138 }
1139}
1140
1141pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1146where
1147 I: IntoIterator<Item = T>,
1148 T: McpTool + 'static,
1149{
1150 let mut builder = LambdaMcpServerBuilder::new();
1151
1152 for tool in tools {
1153 builder = builder.tool(tool);
1154 }
1155
1156 #[cfg(feature = "cors")]
1157 {
1158 builder = builder.cors_allow_all_origins();
1159 }
1160
1161 builder.sse(false).build().await
1162}
1163
1164#[cfg(all(feature = "dynamodb", feature = "cors"))]
1168pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1169where
1170 I: IntoIterator<Item = T>,
1171 T: McpTool + 'static,
1172{
1173 let mut builder = LambdaMcpServerBuilder::new();
1174
1175 for tool in tools {
1176 builder = builder.tool(tool);
1177 }
1178
1179 builder.production_config().await?.build().await
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use super::*;
1185 use turul_mcp_builders::prelude::*;
1186 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1190 struct TestTool;
1191
1192 impl HasBaseMetadata for TestTool {
1193 fn name(&self) -> &str {
1194 "test_tool"
1195 }
1196 }
1197
1198 impl HasDescription for TestTool {
1199 fn description(&self) -> Option<&str> {
1200 Some("Test tool")
1201 }
1202 }
1203
1204 impl HasInputSchema for TestTool {
1205 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1206 use turul_mcp_protocol::ToolSchema;
1207 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1208 SCHEMA.get_or_init(ToolSchema::object)
1209 }
1210 }
1211
1212 impl HasOutputSchema for TestTool {
1213 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1214 None
1215 }
1216 }
1217
1218 impl HasAnnotations for TestTool {
1219 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1220 None
1221 }
1222 }
1223
1224 impl HasToolMeta for TestTool {
1225 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1226 None
1227 }
1228 }
1229
1230 impl HasIcons for TestTool {}
1231 impl HasExecution for TestTool {}
1232
1233 #[async_trait::async_trait]
1234 impl McpTool for TestTool {
1235 async fn call(
1236 &self,
1237 _args: serde_json::Value,
1238 _session: Option<turul_mcp_server::SessionContext>,
1239 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1240 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1241 Ok(CallToolResult::success(vec![ToolResult::text(
1242 "test result",
1243 )]))
1244 }
1245 }
1246
1247 #[tokio::test]
1248 async fn test_builder_basic() {
1249 let server = LambdaMcpServerBuilder::new()
1250 .name("test-server")
1251 .version("1.0.0")
1252 .tool(TestTool)
1253 .storage(Arc::new(InMemorySessionStorage::new()))
1254 .sse(false) .build()
1256 .await
1257 .unwrap();
1258
1259 let handler = server.handler().await.unwrap();
1261 assert!(
1263 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1264 "Stream manager must be initialized"
1265 );
1266 }
1267
1268 #[tokio::test]
1269 async fn test_simple_lambda_server() {
1270 let tools = vec![TestTool];
1271 let server = simple_lambda_server(tools).await.unwrap();
1272
1273 let handler = server.handler().await.unwrap();
1275 assert!(
1278 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1279 "Stream manager must be initialized"
1280 );
1281 }
1282
1283 #[tokio::test]
1284 async fn test_builder_extension_trait() {
1285 let tools = vec![TestTool, TestTool];
1286
1287 let server = LambdaMcpServerBuilder::new()
1288 .tools(tools)
1289 .storage(Arc::new(InMemorySessionStorage::new()))
1290 .sse(false) .build()
1292 .await
1293 .unwrap();
1294
1295 let handler = server.handler().await.unwrap();
1296 assert!(
1299 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1300 "Stream manager must be initialized"
1301 );
1302 }
1303
1304 #[cfg(feature = "cors")]
1305 #[tokio::test]
1306 async fn test_cors_configuration() {
1307 let server = LambdaMcpServerBuilder::new()
1308 .cors_allow_all_origins()
1309 .storage(Arc::new(InMemorySessionStorage::new()))
1310 .sse(false) .build()
1312 .await
1313 .unwrap();
1314
1315 let handler = server.handler().await.unwrap();
1316 assert!(
1319 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1320 "Stream manager must be initialized"
1321 );
1322 }
1323
1324 #[tokio::test]
1325 async fn test_sse_toggle_functionality() {
1326 let mut builder =
1328 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1329
1330 builder = builder.sse(true);
1332 assert!(builder.enable_sse, "SSE should be enabled");
1333 assert!(
1334 builder.server_config.enable_get_sse,
1335 "GET SSE endpoint should be enabled"
1336 );
1337 assert!(
1338 builder.server_config.enable_post_sse,
1339 "POST SSE endpoint should be enabled"
1340 );
1341
1342 builder = builder.sse(false);
1344 assert!(!builder.enable_sse, "SSE should be disabled");
1345 assert!(
1346 !builder.server_config.enable_get_sse,
1347 "GET SSE endpoint should be disabled"
1348 );
1349 assert!(
1350 !builder.server_config.enable_post_sse,
1351 "POST SSE endpoint should be disabled"
1352 );
1353
1354 builder = builder.sse(true);
1356 assert!(builder.enable_sse, "SSE should be re-enabled");
1357 assert!(
1358 builder.server_config.enable_get_sse,
1359 "GET SSE endpoint should be re-enabled"
1360 );
1361 assert!(
1362 builder.server_config.enable_post_sse,
1363 "POST SSE endpoint should be re-enabled"
1364 );
1365
1366 let server = builder.build().await.unwrap();
1368 let handler = server.handler().await.unwrap();
1369 assert!(
1370 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1371 "Stream manager must be initialized"
1372 );
1373 }
1374
1375 #[tokio::test]
1380 async fn test_builder_without_tasks_no_capability() {
1381 let server = LambdaMcpServerBuilder::new()
1382 .name("no-tasks")
1383 .tool(TestTool)
1384 .storage(Arc::new(InMemorySessionStorage::new()))
1385 .sse(false)
1386 .build()
1387 .await
1388 .unwrap();
1389
1390 assert!(
1391 server.capabilities().tasks.is_none(),
1392 "Tasks capability should not be advertised without task storage"
1393 );
1394 }
1395
1396 #[tokio::test]
1397 async fn test_builder_with_task_storage_advertises_capability() {
1398 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1399
1400 let server = LambdaMcpServerBuilder::new()
1401 .name("with-tasks")
1402 .tool(TestTool)
1403 .storage(Arc::new(InMemorySessionStorage::new()))
1404 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1405 .sse(false)
1406 .build()
1407 .await
1408 .unwrap();
1409
1410 let tasks_cap = server
1411 .capabilities()
1412 .tasks
1413 .as_ref()
1414 .expect("Tasks capability should be advertised");
1415 assert!(tasks_cap.list.is_some(), "list capability should be set");
1416 assert!(
1417 tasks_cap.cancel.is_some(),
1418 "cancel capability should be set"
1419 );
1420 let requests = tasks_cap
1421 .requests
1422 .as_ref()
1423 .expect("requests capability should be set");
1424 let tools = requests
1425 .tools
1426 .as_ref()
1427 .expect("tools capability should be set");
1428 assert!(tools.call.is_some(), "tools.call capability should be set");
1429 }
1430
1431 #[tokio::test]
1432 async fn test_builder_with_task_runtime_advertises_capability() {
1433 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1434
1435 let server = LambdaMcpServerBuilder::new()
1436 .name("with-runtime")
1437 .tool(TestTool)
1438 .storage(Arc::new(InMemorySessionStorage::new()))
1439 .with_task_runtime(runtime)
1440 .sse(false)
1441 .build()
1442 .await
1443 .unwrap();
1444
1445 assert!(
1446 server.capabilities().tasks.is_some(),
1447 "Tasks capability should be advertised with task runtime"
1448 );
1449 }
1450
1451 #[tokio::test]
1452 async fn test_task_recovery_timeout_configuration() {
1453 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1454
1455 let server = LambdaMcpServerBuilder::new()
1456 .name("custom-timeout")
1457 .tool(TestTool)
1458 .storage(Arc::new(InMemorySessionStorage::new()))
1459 .task_recovery_timeout_ms(60_000)
1460 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1461 .sse(false)
1462 .build()
1463 .await
1464 .unwrap();
1465
1466 assert!(
1467 server.capabilities().tasks.is_some(),
1468 "Tasks should be enabled with custom timeout"
1469 );
1470 }
1471
1472 #[tokio::test]
1473 async fn test_backward_compatibility_no_tasks() {
1474 let server = LambdaMcpServerBuilder::new()
1476 .name("backward-compat")
1477 .version("1.0.0")
1478 .tool(TestTool)
1479 .storage(Arc::new(InMemorySessionStorage::new()))
1480 .sse(false)
1481 .build()
1482 .await
1483 .unwrap();
1484
1485 let handler = server.handler().await.unwrap();
1486 assert!(
1487 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1488 "Stream manager must be initialized"
1489 );
1490 assert!(server.capabilities().tasks.is_none());
1491 }
1492
1493 #[derive(Clone, Default)]
1495 struct SlowTool;
1496
1497 impl HasBaseMetadata for SlowTool {
1498 fn name(&self) -> &str {
1499 "slow_tool"
1500 }
1501 }
1502
1503 impl HasDescription for SlowTool {
1504 fn description(&self) -> Option<&str> {
1505 Some("A slow tool for testing")
1506 }
1507 }
1508
1509 impl HasInputSchema for SlowTool {
1510 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1511 use turul_mcp_protocol::ToolSchema;
1512 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1513 SCHEMA.get_or_init(ToolSchema::object)
1514 }
1515 }
1516
1517 impl HasOutputSchema for SlowTool {
1518 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1519 None
1520 }
1521 }
1522
1523 impl HasAnnotations for SlowTool {
1524 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1525 None
1526 }
1527 }
1528
1529 impl HasToolMeta for SlowTool {
1530 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1531 None
1532 }
1533 }
1534
1535 impl HasIcons for SlowTool {}
1536 impl HasExecution for SlowTool {
1537 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
1538 Some(turul_mcp_protocol::tools::ToolExecution {
1539 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
1540 })
1541 }
1542 }
1543
1544 #[async_trait::async_trait]
1545 impl McpTool for SlowTool {
1546 async fn call(
1547 &self,
1548 _args: serde_json::Value,
1549 _session: Option<turul_mcp_server::SessionContext>,
1550 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1551 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1552 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1554 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
1555 }
1556 }
1557
1558 #[tokio::test]
1559 async fn test_nonblocking_tools_call_with_task() {
1560 use turul_mcp_json_rpc_server::r#async::JsonRpcHandler;
1561 use turul_mcp_server::SessionAwareToolHandler;
1562 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1563
1564 let task_storage = Arc::new(InMemoryTaskStorage::new());
1565 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1566 task_storage,
1567 ));
1568
1569 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
1571 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
1572
1573 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
1575 Arc::new(InMemorySessionStorage::new());
1576 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
1577 session_storage,
1578 turul_mcp_protocol::ServerCapabilities::default(),
1579 ));
1580
1581 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
1583 .with_task_runtime(Arc::clone(&runtime));
1584
1585 let params = serde_json::json!({
1587 "name": "slow_tool",
1588 "arguments": {},
1589 "task": {}
1590 });
1591 let request_params = turul_mcp_json_rpc_server::RequestParams::Object(
1592 params
1593 .as_object()
1594 .unwrap()
1595 .iter()
1596 .map(|(k, v)| (k.clone(), v.clone()))
1597 .collect(),
1598 );
1599
1600 let start = std::time::Instant::now();
1602 let result = tool_handler
1603 .handle("tools/call", Some(request_params), None)
1604 .await;
1605 let elapsed = start.elapsed();
1606
1607 let value = result.expect("tools/call with task should succeed");
1609 assert!(
1610 value.get("task").is_some(),
1611 "Response should contain 'task' field (CreateTaskResult shape)"
1612 );
1613 let task = value.get("task").unwrap();
1614 assert!(
1615 task.get("taskId").is_some(),
1616 "Task should have taskId field"
1617 );
1618 assert_eq!(
1619 task.get("status")
1620 .and_then(|v| v.as_str())
1621 .unwrap_or_default(),
1622 "working",
1623 "Task status should be 'working'"
1624 );
1625
1626 assert!(
1630 elapsed < std::time::Duration::from_secs(1),
1631 "tools/call with task should return immediately (took {:?}, expected < 1s)",
1632 elapsed
1633 );
1634 }
1635
1636 #[derive(Clone)]
1642 struct StaticTestResource;
1643
1644 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
1645 fn name(&self) -> &str {
1646 "static_test"
1647 }
1648 }
1649
1650 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
1651 fn description(&self) -> Option<&str> {
1652 Some("Static test resource")
1653 }
1654 }
1655
1656 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
1657 fn uri(&self) -> &str {
1658 "file:///test.txt"
1659 }
1660 }
1661
1662 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
1663 fn mime_type(&self) -> Option<&str> {
1664 Some("text/plain")
1665 }
1666 }
1667
1668 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
1669 fn size(&self) -> Option<u64> {
1670 None
1671 }
1672 }
1673
1674 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
1675 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1676 None
1677 }
1678 }
1679
1680 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
1681 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1682 None
1683 }
1684 }
1685
1686 impl HasIcons for StaticTestResource {}
1687
1688 #[async_trait::async_trait]
1689 impl McpResource for StaticTestResource {
1690 async fn read(
1691 &self,
1692 _params: Option<serde_json::Value>,
1693 _session: Option<&turul_mcp_server::SessionContext>,
1694 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1695 {
1696 use turul_mcp_protocol::resources::ResourceContent;
1697 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
1698 }
1699 }
1700
1701 #[derive(Clone)]
1703 struct TemplateTestResource;
1704
1705 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
1706 fn name(&self) -> &str {
1707 "template_test"
1708 }
1709 }
1710
1711 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
1712 fn description(&self) -> Option<&str> {
1713 Some("Template test resource")
1714 }
1715 }
1716
1717 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
1718 fn uri(&self) -> &str {
1719 "agent://agents/{agent_id}"
1720 }
1721 }
1722
1723 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
1724 fn mime_type(&self) -> Option<&str> {
1725 Some("application/json")
1726 }
1727 }
1728
1729 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
1730 fn size(&self) -> Option<u64> {
1731 None
1732 }
1733 }
1734
1735 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
1736 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
1737 None
1738 }
1739 }
1740
1741 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
1742 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1743 None
1744 }
1745 }
1746
1747 impl HasIcons for TemplateTestResource {}
1748
1749 #[async_trait::async_trait]
1750 impl McpResource for TemplateTestResource {
1751 async fn read(
1752 &self,
1753 _params: Option<serde_json::Value>,
1754 _session: Option<&turul_mcp_server::SessionContext>,
1755 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
1756 {
1757 use turul_mcp_protocol::resources::ResourceContent;
1758 Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
1759 }
1760 }
1761
1762 #[test]
1763 fn test_resource_auto_detection_static() {
1764 let builder = LambdaMcpServerBuilder::new()
1765 .name("test")
1766 .resource(StaticTestResource);
1767
1768 assert_eq!(builder.resources.len(), 1);
1769 assert!(builder.resources.contains_key("file:///test.txt"));
1770 assert_eq!(builder.template_resources.len(), 0);
1771 }
1772
1773 #[test]
1774 fn test_resource_auto_detection_template() {
1775 let builder = LambdaMcpServerBuilder::new()
1776 .name("test")
1777 .resource(TemplateTestResource);
1778
1779 assert_eq!(builder.resources.len(), 0);
1780 assert_eq!(builder.template_resources.len(), 1);
1781
1782 let (template, _) = &builder.template_resources[0];
1783 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1784 }
1785
1786 #[test]
1787 fn test_resource_auto_detection_mixed() {
1788 let builder = LambdaMcpServerBuilder::new()
1789 .name("test")
1790 .resource(StaticTestResource)
1791 .resource(TemplateTestResource);
1792
1793 assert_eq!(builder.resources.len(), 1);
1794 assert!(builder.resources.contains_key("file:///test.txt"));
1795 assert_eq!(builder.template_resources.len(), 1);
1796
1797 let (template, _) = &builder.template_resources[0];
1798 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
1799 }
1800
1801 #[tokio::test]
1802 async fn test_build_advertises_resources_capability_for_templates_only() {
1803 let server = LambdaMcpServerBuilder::new()
1804 .name("template-only")
1805 .resource(TemplateTestResource)
1806 .storage(Arc::new(InMemorySessionStorage::new()))
1807 .sse(false)
1808 .build()
1809 .await
1810 .unwrap();
1811
1812 assert!(
1813 server.capabilities().resources.is_some(),
1814 "Resources capability should be advertised when template resources are registered"
1815 );
1816 }
1817
1818 #[tokio::test]
1819 async fn test_build_advertises_resources_capability_for_static_only() {
1820 let server = LambdaMcpServerBuilder::new()
1821 .name("static-only")
1822 .resource(StaticTestResource)
1823 .storage(Arc::new(InMemorySessionStorage::new()))
1824 .sse(false)
1825 .build()
1826 .await
1827 .unwrap();
1828
1829 assert!(
1830 server.capabilities().resources.is_some(),
1831 "Resources capability should be advertised when static resources are registered"
1832 );
1833 }
1834
1835 #[tokio::test]
1836 async fn test_build_no_resources_no_capability() {
1837 let server = LambdaMcpServerBuilder::new()
1838 .name("no-resources")
1839 .tool(TestTool)
1840 .storage(Arc::new(InMemorySessionStorage::new()))
1841 .sse(false)
1842 .build()
1843 .await
1844 .unwrap();
1845
1846 assert!(
1847 server.capabilities().resources.is_none(),
1848 "Resources capability should NOT be advertised when no resources are registered"
1849 );
1850 }
1851
1852 #[tokio::test]
1853 async fn test_lambda_builder_templates_list_returns_template() {
1854 use turul_mcp_server::handlers::McpHandler;
1855
1856 let builder = LambdaMcpServerBuilder::new()
1858 .name("template-test")
1859 .resource(TemplateTestResource);
1860
1861 assert_eq!(builder.template_resources.len(), 1);
1863
1864 let handler =
1866 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1867
1868 let result = handler.handle(None).await.expect("should succeed");
1870
1871 let templates = result["resourceTemplates"]
1872 .as_array()
1873 .expect("resourceTemplates should be an array");
1874 assert_eq!(
1875 templates.len(),
1876 1,
1877 "Should have exactly 1 template resource"
1878 );
1879 assert_eq!(
1880 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
1881 "Template URI should match"
1882 );
1883 assert_eq!(templates[0]["name"], "template_test");
1884 }
1885
1886 #[tokio::test]
1887 async fn test_lambda_builder_resources_list_returns_static() {
1888 use turul_mcp_server::handlers::McpHandler;
1889
1890 let builder = LambdaMcpServerBuilder::new()
1892 .name("static-test")
1893 .resource(StaticTestResource);
1894
1895 assert_eq!(builder.resources.len(), 1);
1896
1897 let mut handler = ResourcesHandler::new();
1898 for resource in builder.resources.values() {
1899 handler = handler.add_resource_arc(resource.clone());
1900 }
1901
1902 let result = handler.handle(None).await.expect("should succeed");
1903
1904 let resources = result["resources"]
1905 .as_array()
1906 .expect("resources should be an array");
1907 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
1908 assert_eq!(resources[0]["uri"], "file:///test.txt");
1909 assert_eq!(resources[0]["name"], "static_test");
1910 }
1911
1912 #[tokio::test]
1913 async fn test_lambda_builder_mixed_resources_separation() {
1914 use turul_mcp_server::handlers::McpHandler;
1915
1916 let builder = LambdaMcpServerBuilder::new()
1918 .name("mixed-test")
1919 .resource(StaticTestResource)
1920 .resource(TemplateTestResource);
1921
1922 assert_eq!(builder.resources.len(), 1);
1923 assert_eq!(builder.template_resources.len(), 1);
1924
1925 let mut list_handler = ResourcesHandler::new();
1927 for resource in builder.resources.values() {
1928 list_handler = list_handler.add_resource_arc(resource.clone());
1929 }
1930
1931 let templates_handler =
1932 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
1933
1934 let list_result = list_handler.handle(None).await.expect("should succeed");
1936 let resources = list_result["resources"]
1937 .as_array()
1938 .expect("resources should be an array");
1939 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
1940 assert_eq!(resources[0]["uri"], "file:///test.txt");
1941
1942 let templates_result = templates_handler
1944 .handle(None)
1945 .await
1946 .expect("should succeed");
1947 let templates = templates_result["resourceTemplates"]
1948 .as_array()
1949 .expect("resourceTemplates should be an array");
1950 assert_eq!(
1951 templates.len(),
1952 1,
1953 "Only template resource in resources/templates/list"
1954 );
1955 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
1956 }
1957
1958 #[tokio::test]
1959 async fn test_tasks_get_route_registered() {
1960 use turul_mcp_server::TasksGetHandler;
1961 use turul_mcp_server::handlers::McpHandler;
1962 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1963
1964 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
1965 Arc::new(InMemoryTaskStorage::new()),
1966 ));
1967 let handler = TasksGetHandler::new(runtime);
1968
1969 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
1972
1973 let result = handler.handle(Some(params)).await;
1974
1975 assert!(
1977 result.is_err(),
1978 "tasks/get with unknown task should return error"
1979 );
1980 let err = result.unwrap_err();
1981 let err_str = err.to_string();
1982 assert!(
1983 !err_str.contains("method not found"),
1984 "Error should not be 'method not found' — handler should respond to tasks/get"
1985 );
1986 }
1987
1988 #[tokio::test]
1995 async fn test_resources_read_registered_by_default() {
1996 use lambda_http::Body as LambdaBody;
1997
1998 let server = LambdaMcpServerBuilder::new()
1999 .name("parity-test")
2000 .version("1.0.0")
2001 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2003 .strict_lifecycle(false) .sse(false)
2005 .build()
2006 .await
2007 .unwrap();
2008
2009 let handler = server.handler().await.unwrap();
2010
2011 let init_req = http::Request::builder()
2013 .method("POST")
2014 .uri("/mcp")
2015 .header("Content-Type", "application/json")
2016 .header("MCP-Protocol-Version", "2025-11-25")
2017 .body(LambdaBody::Text(serde_json::json!({
2018 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2019 "params": {
2020 "protocolVersion": "2025-11-25",
2021 "capabilities": {},
2022 "clientInfo": { "name": "test", "version": "1.0.0" }
2023 }
2024 }).to_string()))
2025 .unwrap();
2026 let init_resp = handler.handle(init_req).await.unwrap();
2027 let session_id = init_resp
2028 .headers()
2029 .get("Mcp-Session-Id")
2030 .unwrap()
2031 .to_str()
2032 .unwrap()
2033 .to_string();
2034
2035 let read_req = http::Request::builder()
2038 .method("POST")
2039 .uri("/mcp")
2040 .header("Content-Type", "application/json")
2041 .header("MCP-Protocol-Version", "2025-11-25")
2042 .header("Mcp-Session-Id", &session_id)
2043 .body(LambdaBody::Text(serde_json::json!({
2044 "jsonrpc": "2.0", "method": "resources/read", "id": 2,
2045 "params": { "uri": "file:///nonexistent" }
2046 }).to_string()))
2047 .unwrap();
2048 let read_resp = handler.handle(read_req).await.unwrap();
2049 let body = String::from_utf8_lossy(read_resp.body().as_ref()).to_string();
2050 let json: serde_json::Value = serde_json::from_str(&body)
2051 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2052
2053 assert!(
2055 json["error"].is_object(),
2056 "resources/read must return JSON-RPC error, got: {json}"
2057 );
2058 let error_code = json["error"]["code"].as_i64().unwrap();
2062 assert_ne!(
2063 error_code, -32601,
2064 "resources/read must be registered (got method-not-found -32601): {json}"
2065 );
2066 }
2067
2068 #[tokio::test]
2072 async fn test_resources_templates_list_absent_without_templates() {
2073 use lambda_http::Body as LambdaBody;
2074
2075 let server = LambdaMcpServerBuilder::new()
2076 .name("parity-test")
2077 .version("1.0.0")
2078 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2080 .strict_lifecycle(false) .sse(false)
2082 .build()
2083 .await
2084 .unwrap();
2085
2086 let handler = server.handler().await.unwrap();
2087
2088 let init_req = http::Request::builder()
2090 .method("POST")
2091 .uri("/mcp")
2092 .header("Content-Type", "application/json")
2093 .header("MCP-Protocol-Version", "2025-11-25")
2094 .body(LambdaBody::Text(serde_json::json!({
2095 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2096 "params": {
2097 "protocolVersion": "2025-11-25",
2098 "capabilities": {},
2099 "clientInfo": { "name": "test", "version": "1.0.0" }
2100 }
2101 }).to_string()))
2102 .unwrap();
2103 let init_resp = handler.handle(init_req).await.unwrap();
2104 let session_id = init_resp
2105 .headers()
2106 .get("Mcp-Session-Id")
2107 .unwrap()
2108 .to_str()
2109 .unwrap()
2110 .to_string();
2111
2112 let tmpl_req = http::Request::builder()
2115 .method("POST")
2116 .uri("/mcp")
2117 .header("Content-Type", "application/json")
2118 .header("MCP-Protocol-Version", "2025-11-25")
2119 .header("Mcp-Session-Id", &session_id)
2120 .body(LambdaBody::Text(serde_json::json!({
2121 "jsonrpc": "2.0", "method": "resources/templates/list", "id": 2
2122 }).to_string()))
2123 .unwrap();
2124 let tmpl_resp = handler.handle(tmpl_req).await.unwrap();
2125 let body = String::from_utf8_lossy(tmpl_resp.body().as_ref()).to_string();
2126 let json: serde_json::Value = serde_json::from_str(&body)
2127 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2128
2129 assert!(
2131 json["error"].is_object(),
2132 "resources/templates/list should return error without templates: {json}"
2133 );
2134 assert_eq!(
2135 json["error"]["code"].as_i64().unwrap(),
2136 -32601,
2137 "resources/templates/list must be method-not-found (-32601) without templates: {json}"
2138 );
2139 }
2140}