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::{McpCompletion, McpNotification, McpPrompt, McpResource, McpTool};
13#[cfg(feature = "protocol-2025-11-25")]
14use turul_mcp_server::{McpElicitation, McpLogger, McpSampling};
15use turul_mcp_session_storage::BoxedSessionStorage;
16
17use crate::error::Result;
18
19#[cfg(feature = "dynamodb")]
20use crate::error::LambdaError;
21use crate::server::LambdaMcpServer;
22
23#[cfg(feature = "cors")]
24use crate::cors::CorsConfig;
25
26pub struct LambdaMcpServerBuilder {
68 name: String,
70 version: String,
71 title: Option<String>,
72 icons: Option<Vec<turul_mcp_protocol::Icon>>,
73
74 capabilities: ServerCapabilities,
76
77 tools: HashMap<String, Arc<dyn McpTool>>,
79
80 resources: HashMap<String, Arc<dyn McpResource>>,
82
83 template_resources: Vec<(
85 turul_mcp_server::uri_template::UriTemplate,
86 Arc<dyn McpResource>,
87 )>,
88
89 prompts: HashMap<String, Arc<dyn McpPrompt>>,
91
92 #[cfg(feature = "protocol-2025-11-25")]
94 elicitations: HashMap<String, Arc<dyn McpElicitation>>,
95
96 #[cfg(feature = "protocol-2025-11-25")]
98 sampling: HashMap<String, Arc<dyn McpSampling>>,
99
100 completions: Vec<Arc<dyn McpCompletion>>,
102
103 #[cfg(feature = "protocol-2025-11-25")]
105 loggers: HashMap<String, Arc<dyn McpLogger>>,
106
107 notifications: HashMap<String, Arc<dyn McpNotification>>,
111
112 handlers: HashMap<String, Arc<dyn McpHandler>>,
114
115 #[allow(deprecated)]
118 roots: Vec<turul_mcp_protocol::roots::Root>,
119
120 instructions: Option<String>,
122
123 session_timeout_minutes: Option<u64>,
125 session_cleanup_interval_seconds: Option<u64>,
126
127 session_storage: Option<Arc<BoxedSessionStorage>>,
129
130 strict_lifecycle: bool,
132
133 enable_sse: bool,
135 server_config: ServerConfig,
137 stream_config: StreamConfig,
138
139 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
141
142 route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
144
145 #[cfg(feature = "protocol-2025-11-25")]
147 task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
148 #[cfg(feature = "protocol-2025-11-25")]
150 task_recovery_timeout_ms: u64,
151
152 tool_change_mode: turul_mcp_server::ToolChangeMode,
154
155 #[cfg(feature = "dynamic-tools")]
157 server_state_storage: Option<Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>>,
158
159 #[cfg(feature = "cors")]
161 cors_config: Option<CorsConfig>,
162 origin_policy: Option<turul_http_mcp_server::OriginPolicy>,
163}
164
165impl LambdaMcpServerBuilder {
166 pub fn new() -> Self {
168 let capabilities = ServerCapabilities::default();
171
172 let mut handlers: HashMap<String, Arc<dyn McpHandler>> = HashMap::new();
174 #[cfg(feature = "protocol-2025-11-25")]
177 handlers.insert("ping".to_string(), Arc::new(PingHandler));
178 handlers.insert(
183 "resources/list".to_string(),
184 Arc::new(ResourcesHandler::new()),
185 );
186 handlers.insert(
187 "resources/read".to_string(),
188 Arc::new(ResourcesReadHandler::new().without_security()),
189 );
190 handlers.insert(
195 "resources/templates/list".to_string(),
196 Arc::new(ResourceTemplatesHandler::new()),
197 );
198 handlers.insert(
199 "prompts/list".to_string(),
200 Arc::new(PromptsListHandler::new()),
201 );
202 handlers.insert(
203 "prompts/get".to_string(),
204 Arc::new(PromptsGetHandler::new()),
205 );
206 #[cfg(feature = "protocol-2025-11-25")]
207 handlers.insert("logging/setLevel".to_string(), Arc::new(LoggingHandler));
208 #[cfg(feature = "protocol-2025-11-25")]
211 handlers.insert("roots/list".to_string(), Arc::new(RootsHandler::new()));
212 #[cfg(feature = "protocol-2025-11-25")]
213 handlers.insert(
214 "sampling/createMessage".to_string(),
215 Arc::new(SamplingHandler),
216 );
217 #[cfg(feature = "protocol-2025-11-25")]
218 handlers.insert(
219 "elicitation/create".to_string(),
220 Arc::new(ElicitationHandler::with_mock_provider()),
221 );
222
223 let notifications_handler = Arc::new(NotificationsHandler);
225 #[cfg(feature = "protocol-2025-11-25")]
230 handlers.insert(
231 "notifications/message".to_string(),
232 notifications_handler.clone(),
233 );
234 #[cfg(feature = "protocol-2025-11-25")]
235 handlers.insert(
236 "notifications/progress".to_string(),
237 notifications_handler.clone(),
238 );
239 handlers.insert(
247 "notifications/cancelled".to_string(),
248 Arc::new(CancelledNotificationHandler),
249 );
250 handlers.insert(
252 "notifications/resources/list_changed".to_string(),
253 notifications_handler.clone(),
254 );
255 handlers.insert(
256 "notifications/resources/updated".to_string(),
257 notifications_handler.clone(),
258 );
259 handlers.insert(
260 "notifications/tools/list_changed".to_string(),
261 notifications_handler.clone(),
262 );
263 handlers.insert(
264 "notifications/prompts/list_changed".to_string(),
265 notifications_handler.clone(),
266 );
267 #[cfg(feature = "protocol-2025-11-25")]
268 handlers.insert(
269 "notifications/roots/list_changed".to_string(),
270 notifications_handler.clone(),
271 );
272 handlers.insert(
274 "notifications/resources/listChanged".to_string(),
275 notifications_handler.clone(),
276 );
277 handlers.insert(
278 "notifications/tools/listChanged".to_string(),
279 notifications_handler.clone(),
280 );
281 handlers.insert(
282 "notifications/prompts/listChanged".to_string(),
283 notifications_handler.clone(),
284 );
285 #[cfg(feature = "protocol-2025-11-25")]
286 handlers.insert(
287 "notifications/roots/listChanged".to_string(),
288 notifications_handler.clone(),
289 );
290 let _ = notifications_handler;
291
292 Self {
293 name: "turul-mcp-aws-lambda".to_string(),
294 version: env!("CARGO_PKG_VERSION").to_string(),
295 title: None,
296 icons: None,
297 capabilities,
298 tools: HashMap::new(),
299 resources: HashMap::new(),
300 template_resources: Vec::new(),
301 prompts: HashMap::new(),
302 #[cfg(feature = "protocol-2025-11-25")]
303 elicitations: HashMap::new(),
304 #[cfg(feature = "protocol-2025-11-25")]
305 sampling: HashMap::new(),
306 completions: Vec::new(),
307 #[cfg(feature = "protocol-2025-11-25")]
308 loggers: HashMap::new(),
309 notifications: HashMap::new(),
310 handlers,
311 roots: Vec::new(),
312 instructions: None,
313 session_timeout_minutes: None,
314 session_cleanup_interval_seconds: None,
315 session_storage: None,
316 strict_lifecycle: true, enable_sse: cfg!(feature = "sse"),
318 server_config: ServerConfig::default(),
319 origin_policy: None,
320 stream_config: StreamConfig::default(),
321 middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack::new(),
322 route_registry: Arc::new(turul_http_mcp_server::RouteRegistry::new()),
323 #[cfg(feature = "protocol-2025-11-25")]
324 task_runtime: None,
325 #[cfg(feature = "protocol-2025-11-25")]
326 task_recovery_timeout_ms: 300_000, tool_change_mode: turul_mcp_server::ToolChangeMode::Static,
328 #[cfg(feature = "dynamic-tools")]
329 server_state_storage: None,
330 #[cfg(feature = "cors")]
331 cors_config: None,
332 }
333 }
334
335 pub fn name(mut self, name: impl Into<String>) -> Self {
337 self.name = name.into();
338 self
339 }
340
341 pub fn version(mut self, version: impl Into<String>) -> Self {
343 self.version = version.into();
344 self
345 }
346
347 pub fn title(mut self, title: impl Into<String>) -> Self {
349 self.title = Some(title.into());
350 self
351 }
352
353 pub fn icons(mut self, icons: Vec<turul_mcp_protocol::Icon>) -> Self {
355 self.icons = Some(icons);
356 self
357 }
358
359 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
361 self.instructions = Some(instructions.into());
362 self
363 }
364
365 pub fn tool<T: McpTool + 'static>(mut self, tool: T) -> Self {
377 let name = tool.name().to_string();
378 self.tools.insert(name, Arc::new(tool));
379 self
380 }
381
382 pub fn tool_fn<F, T>(self, func: F) -> Self
384 where
385 F: Fn() -> T,
386 T: McpTool + 'static,
387 {
388 self.tool(func())
389 }
390
391 pub fn tools<T: McpTool + 'static, I: IntoIterator<Item = T>>(mut self, tools: I) -> Self {
393 for tool in tools {
394 self = self.tool(tool);
395 }
396 self
397 }
398
399 pub fn resource<R: McpResource + 'static>(mut self, resource: R) -> Self {
405 let uri = resource.uri().to_string();
406
407 if uri.contains('{') && uri.contains('}') {
408 match turul_mcp_server::uri_template::UriTemplate::new(&uri) {
410 Ok(template) => {
411 self.template_resources.push((template, Arc::new(resource)));
412 }
413 Err(e) => {
414 tracing::warn!(
415 "Failed to parse template resource URI '{}': {}. Registering as static.",
416 uri,
417 e
418 );
419 self.resources.insert(uri, Arc::new(resource));
420 }
421 }
422 } else {
423 self.resources.insert(uri, Arc::new(resource));
425 }
426 self
427 }
428
429 pub fn resources<R: McpResource + 'static, I: IntoIterator<Item = R>>(
431 mut self,
432 resources: I,
433 ) -> Self {
434 for resource in resources {
435 self = self.resource(resource);
436 }
437 self
438 }
439
440 pub fn prompt<P: McpPrompt + 'static>(mut self, prompt: P) -> Self {
442 let name = prompt.name().to_string();
443 self.prompts.insert(name, Arc::new(prompt));
444 self
445 }
446
447 pub fn prompts<P: McpPrompt + 'static, I: IntoIterator<Item = P>>(
449 mut self,
450 prompts: I,
451 ) -> Self {
452 for prompt in prompts {
453 self = self.prompt(prompt);
454 }
455 self
456 }
457
458 #[cfg(feature = "protocol-2025-11-25")]
460 pub fn elicitation<E: McpElicitation + 'static>(mut self, elicitation: E) -> Self {
461 let key = format!("elicitation_{}", self.elicitations.len());
462 self.elicitations.insert(key, Arc::new(elicitation));
463 self
464 }
465
466 #[cfg(feature = "protocol-2025-11-25")]
468 pub fn elicitations<E: McpElicitation + 'static, I: IntoIterator<Item = E>>(
469 mut self,
470 elicitations: I,
471 ) -> Self {
472 for elicitation in elicitations {
473 self = self.elicitation(elicitation);
474 }
475 self
476 }
477
478 #[cfg(feature = "protocol-2025-11-25")]
480 pub fn sampling_provider<S: McpSampling + 'static>(mut self, sampling: S) -> Self {
481 let key = format!("sampling_{}", self.sampling.len());
482 self.sampling.insert(key, Arc::new(sampling));
483 self
484 }
485
486 #[cfg(feature = "protocol-2025-11-25")]
488 pub fn sampling_providers<S: McpSampling + 'static, I: IntoIterator<Item = S>>(
489 mut self,
490 sampling: I,
491 ) -> Self {
492 for s in sampling {
493 self = self.sampling_provider(s);
494 }
495 self
496 }
497
498 pub fn completion_provider<C: McpCompletion + 'static>(mut self, completion: C) -> Self {
500 self.completions.push(Arc::new(completion));
501 self
502 }
503
504 pub fn completion_providers<C: McpCompletion + 'static, I: IntoIterator<Item = C>>(
506 mut self,
507 completions: I,
508 ) -> Self {
509 for completion in completions {
510 self = self.completion_provider(completion);
511 }
512 self
513 }
514
515 #[cfg(feature = "protocol-2025-11-25")]
517 pub fn logger<L: McpLogger + 'static>(mut self, logger: L) -> Self {
518 let key = format!("logger_{}", self.loggers.len());
519 self.loggers.insert(key, Arc::new(logger));
520 self
521 }
522
523 #[cfg(feature = "protocol-2025-11-25")]
525 pub fn loggers<L: McpLogger + 'static, I: IntoIterator<Item = L>>(
526 mut self,
527 loggers: I,
528 ) -> Self {
529 for logger in loggers {
530 self = self.logger(logger);
531 }
532 self
533 }
534
535 pub fn notification_provider<N: McpNotification + 'static>(mut self, notification: N) -> Self {
537 let key = format!("notification_{}", self.notifications.len());
538 self.notifications.insert(key, Arc::new(notification));
539 self
540 }
541
542 pub fn notification_providers<N: McpNotification + 'static, I: IntoIterator<Item = N>>(
544 mut self,
545 notifications: I,
546 ) -> Self {
547 for notification in notifications {
548 self = self.notification_provider(notification);
549 }
550 self
551 }
552
553 #[cfg(feature = "protocol-2025-11-25")]
559 pub fn sampler<S: McpSampling + 'static>(self, sampling: S) -> Self {
560 self.sampling_provider(sampling)
561 }
562
563 pub fn completer<C: McpCompletion + 'static>(self, completion: C) -> Self {
565 self.completion_provider(completion)
566 }
567
568 pub fn notification_type<N: McpNotification + 'static + Default>(self) -> Self {
570 let notification = N::default();
571 self.notification_provider(notification)
572 }
573
574 pub fn handler<H: McpHandler + 'static>(mut self, handler: H) -> Self {
576 let handler_arc = Arc::new(handler);
577 for method in handler_arc.supported_methods() {
578 self.handlers.insert(method, handler_arc.clone());
579 }
580 self
581 }
582
583 pub fn handlers<H: McpHandler + 'static, I: IntoIterator<Item = H>>(
585 mut self,
586 handlers: I,
587 ) -> Self {
588 for handler in handlers {
589 self = self.handler(handler);
590 }
591 self
592 }
593
594 #[allow(deprecated)]
596 pub fn root(mut self, root: turul_mcp_protocol::roots::Root) -> Self {
597 self.roots.push(root);
598 self
599 }
600
601 pub fn with_completion(mut self) -> Self {
607 use turul_mcp_protocol::initialize::CompletionsCapabilities;
608 self.capabilities.completions = Some(CompletionsCapabilities::default());
609 self.handler(CompletionHandler::new())
610 }
611
612 pub fn with_prompts(mut self) -> Self {
614 use turul_mcp_protocol::initialize::PromptsCapabilities;
615 self.capabilities.prompts = Some(PromptsCapabilities {
616 list_changed: Some(false),
617 });
618
619 self
622 }
623
624 pub fn with_resources(mut self) -> Self {
626 use turul_mcp_protocol::initialize::ResourcesCapabilities;
627 self.capabilities.resources = Some(ResourcesCapabilities {
628 subscribe: Some(false),
629 list_changed: Some(false),
630 });
631
632 let mut list_handler = ResourcesHandler::new();
634 for resource in self.resources.values() {
635 list_handler = list_handler.add_resource_arc(resource.clone());
636 }
637 self = self.handler(list_handler);
638
639 if !self.template_resources.is_empty() {
641 let templates_handler =
642 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
643 self = self.handler(templates_handler);
644 }
645
646 let mut read_handler = ResourcesReadHandler::new().without_security();
648 for resource in self.resources.values() {
649 read_handler = read_handler.add_resource_arc(resource.clone());
650 }
651 for (template, resource) in &self.template_resources {
652 read_handler =
653 read_handler.add_template_resource_arc(template.clone(), resource.clone());
654 }
655 self.handler(read_handler)
656 }
657
658 #[cfg(feature = "protocol-2025-11-25")]
660 pub fn with_logging(mut self) -> Self {
661 use turul_mcp_protocol::initialize::LoggingCapabilities;
662 #[allow(deprecated)] {
664 self.capabilities.logging = Some(LoggingCapabilities::default());
665 }
666 self.handler(LoggingHandler)
667 }
668
669 pub fn with_roots(self) -> Self {
671 self.handler(RootsHandler::new())
672 }
673
674 #[cfg(feature = "protocol-2025-11-25")]
676 pub fn with_sampling(self) -> Self {
677 self.handler(SamplingHandler)
678 }
679
680 #[cfg(feature = "protocol-2025-11-25")]
685 pub fn with_elicitation(self) -> Self {
686 self.handler(ElicitationHandler::with_mock_provider())
687 }
688
689 #[cfg(feature = "protocol-2025-11-25")]
694 pub fn with_elicitation_provider<P: ElicitationProvider + 'static>(self, provider: P) -> Self {
695 self.handler(ElicitationHandler::new(Arc::new(provider)))
696 }
697
698 pub fn with_notifications(self) -> Self {
700 self.handler(NotificationsHandler)
701 }
702
703 #[cfg(feature = "protocol-2025-11-25")]
718 pub fn with_task_storage(
719 mut self,
720 storage: Arc<dyn turul_mcp_server::task_storage::TaskStorage>,
721 ) -> Self {
722 let runtime = turul_mcp_server::TaskRuntime::with_default_executor(storage)
723 .with_recovery_timeout(self.task_recovery_timeout_ms);
724 self.task_runtime = Some(Arc::new(runtime));
725 self
726 }
727
728 #[cfg(feature = "protocol-2025-11-25")]
732 pub fn with_task_runtime(mut self, runtime: Arc<turul_mcp_server::TaskRuntime>) -> Self {
733 self.task_runtime = Some(runtime);
734 self
735 }
736
737 #[cfg(feature = "protocol-2025-11-25")]
742 pub fn task_recovery_timeout_ms(mut self, timeout_ms: u64) -> Self {
743 self.task_recovery_timeout_ms = timeout_ms;
744 self
745 }
746
747 pub fn tool_change_mode(mut self, mode: turul_mcp_server::ToolChangeMode) -> Self {
758 self.tool_change_mode = mode;
759 self
760 }
761
762 #[cfg(feature = "dynamic-tools")]
769 pub fn server_state_storage(
770 mut self,
771 storage: Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>,
772 ) -> Self {
773 self.server_state_storage = Some(storage);
774 self
775 }
776
777 pub fn session_timeout_minutes(mut self, minutes: u64) -> Self {
783 self.session_timeout_minutes = Some(minutes);
784 self
785 }
786
787 pub fn session_cleanup_interval_seconds(mut self, seconds: u64) -> Self {
789 self.session_cleanup_interval_seconds = Some(seconds);
790 self
791 }
792
793 pub fn strict_lifecycle(mut self, strict: bool) -> Self {
795 self.strict_lifecycle = strict;
796 self
797 }
798
799 pub fn with_strict_lifecycle(self) -> Self {
801 self.strict_lifecycle(true)
802 }
803
804 pub fn sse(mut self, enable: bool) -> Self {
806 self.enable_sse = enable;
807
808 #[allow(deprecated)]
812 if enable {
813 self.server_config.enable_get_sse = true;
814 self.server_config.enable_post_sse = true;
815 } else {
816 self.server_config.enable_get_sse = false;
819 self.server_config.enable_post_sse = false;
820 }
821
822 self
823 }
824
825 pub fn with_long_sessions(mut self) -> Self {
827 self.session_timeout_minutes = Some(120); self.session_cleanup_interval_seconds = Some(300); self
830 }
831
832 pub fn with_short_sessions(mut self) -> Self {
834 self.session_timeout_minutes = Some(5); self.session_cleanup_interval_seconds = Some(30); self
837 }
838
839 pub fn storage(mut self, storage: Arc<BoxedSessionStorage>) -> Self {
847 self.session_storage = Some(storage);
848 self
849 }
850
851 #[cfg(feature = "dynamodb")]
858 pub async fn dynamodb_storage(self) -> Result<Self> {
859 use turul_mcp_session_storage::DynamoDbSessionStorage;
860
861 let storage = DynamoDbSessionStorage::new().await.map_err(|e| {
862 LambdaError::Configuration(format!("Failed to create DynamoDB storage: {}", e))
863 })?;
864
865 Ok(self.storage(Arc::new(storage)))
866 }
867
868 pub fn middleware(
904 mut self,
905 middleware: Arc<dyn turul_http_mcp_server::middleware::McpMiddleware>,
906 ) -> Self {
907 self.middleware_stack.push(middleware);
908 self
909 }
910
911 pub fn route(
913 mut self,
914 path: &str,
915 handler: Arc<dyn turul_http_mcp_server::RouteHandler>,
916 ) -> Self {
917 Arc::get_mut(&mut self.route_registry)
918 .expect("route_registry must not be shared during build")
919 .add_route(path, handler);
920 self
921 }
922
923 pub fn origin_policy(mut self, policy: turul_http_mcp_server::OriginPolicy) -> Self {
930 self.origin_policy = Some(policy);
931 self
932 }
933
934 pub fn server_config(mut self, config: ServerConfig) -> Self {
935 self.server_config = config;
936 self
937 }
938
939 pub fn stream_config(mut self, config: StreamConfig) -> Self {
941 self.stream_config = config;
942 self
943 }
944
945 #[cfg(feature = "cors")]
949 pub fn cors(mut self, config: CorsConfig) -> Self {
950 self.cors_config = Some(config);
951 self
952 }
953
954 #[cfg(feature = "cors")]
956 pub fn cors_allow_all_origins(mut self) -> Self {
957 self.cors_config = Some(CorsConfig::allow_all());
958 self
959 }
960
961 #[cfg(feature = "cors")]
963 pub fn cors_allow_origins(mut self, origins: Vec<String>) -> Self {
964 self.cors_config = Some(CorsConfig::for_origins(origins));
965 self
966 }
967
968 #[cfg(feature = "cors")]
975 pub fn cors_from_env(mut self) -> Self {
976 self.cors_config = Some(CorsConfig::from_env());
977 self
978 }
979
980 #[cfg(feature = "cors")]
982 pub fn cors_disabled(self) -> Self {
983 self
985 }
986
987 #[cfg(all(feature = "dynamodb", feature = "cors"))]
993 pub async fn production_config(self) -> Result<Self> {
994 Ok(self.dynamodb_storage().await?.cors_from_env())
995 }
996
997 #[cfg(feature = "cors")]
1001 pub fn development_config(self) -> Self {
1002 use turul_mcp_session_storage::InMemorySessionStorage;
1003
1004 self.storage(Arc::new(InMemorySessionStorage::new()))
1005 .cors_allow_all_origins()
1006 }
1007
1008 pub async fn build(mut self) -> Result<LambdaMcpServer> {
1012 use turul_mcp_session_storage::InMemorySessionStorage;
1013
1014 if let Some(policy) = self.origin_policy.take() {
1019 self.server_config.origin_policy = policy;
1020 } else {
1021 #[cfg(feature = "cors")]
1025 if let Some(cors) = &self.cors_config {
1026 self.server_config.origin_policy = if cors.allowed_origins.iter().any(|o| o == "*")
1027 {
1028 turul_http_mcp_server::OriginPolicy::Disabled
1029 } else {
1030 turul_http_mcp_server::OriginPolicy::AllowList(cors.allowed_origins.clone())
1031 };
1032 }
1033 }
1034
1035 if self.name.is_empty() {
1037 return Err(crate::error::LambdaError::Configuration(
1038 "Server name cannot be empty".to_string(),
1039 ));
1040 }
1041 if self.version.is_empty() {
1042 return Err(crate::error::LambdaError::Configuration(
1043 "Server version cannot be empty".to_string(),
1044 ));
1045 }
1046
1047 let session_storage = self
1056 .session_storage
1057 .unwrap_or_else(|| Arc::new(InMemorySessionStorage::new()));
1058
1059 let mut implementation = Implementation::new(&self.name, &self.version);
1061 if let Some(title) = self.title {
1062 implementation = implementation.with_title(title);
1063 }
1064 if let Some(icons) = self.icons {
1065 implementation = implementation.with_icons(icons);
1066 }
1067
1068 let mut capabilities = self.capabilities.clone();
1070 let has_tools = !self.tools.is_empty();
1071 let has_resources = !self.resources.is_empty() || !self.template_resources.is_empty();
1072 let has_prompts = !self.prompts.is_empty();
1073 #[cfg(feature = "protocol-2025-11-25")]
1074 let has_elicitations = !self.elicitations.is_empty();
1075 let has_completions = !self.completions.is_empty();
1076 #[cfg(feature = "protocol-2025-11-25")]
1077 {
1078 let has_logging = !self.loggers.is_empty();
1079 tracing::debug!("🔧 Has logging configured: {}", has_logging);
1080 }
1081
1082 if has_tools {
1084 let list_changed = !matches!(
1085 self.tool_change_mode,
1086 turul_mcp_server::ToolChangeMode::Static
1087 );
1088 capabilities.tools = Some(turul_mcp_protocol::initialize::ToolsCapabilities {
1089 list_changed: Some(list_changed),
1090 });
1091 }
1092
1093 if has_resources {
1095 capabilities.resources = Some(turul_mcp_protocol::initialize::ResourcesCapabilities {
1096 subscribe: Some(false), list_changed: Some(false), });
1099 }
1100
1101 if has_prompts {
1103 capabilities.prompts = Some(turul_mcp_protocol::initialize::PromptsCapabilities {
1104 list_changed: Some(false), });
1106 }
1107
1108 #[cfg(feature = "protocol-2025-11-25")]
1111 let _ = has_elicitations; if has_completions {
1115 capabilities.completions =
1116 Some(turul_mcp_protocol::initialize::CompletionsCapabilities::default());
1117 }
1118
1119 #[allow(deprecated)] {
1123 capabilities.logging =
1124 Some(turul_mcp_protocol::initialize::LoggingCapabilities::default());
1125 }
1126
1127 #[cfg(feature = "protocol-2025-11-25")]
1129 if self.task_runtime.is_some() {
1130 use turul_mcp_protocol::initialize::*;
1131 capabilities.tasks = Some(TasksCapabilities {
1132 list: Some(TasksListCapabilities::default()),
1133 cancel: Some(TasksCancelCapabilities::default()),
1134 requests: Some(TasksRequestCapabilities {
1135 tools: Some(TasksToolCapabilities {
1136 call: Some(TasksToolCallCapabilities::default()),
1137 extra: Default::default(),
1138 }),
1139 extra: Default::default(),
1140 }),
1141 extra: Default::default(),
1142 });
1143 }
1144
1145 let mut handlers = self.handlers;
1146
1147 if !self.completions.is_empty() {
1149 handlers.insert(
1150 "completion/complete".to_string(),
1151 Arc::new(CompletionHandler::new().with_providers(self.completions.clone())),
1152 );
1153 }
1154 #[cfg(feature = "protocol-2025-11-25")]
1158 if !self.roots.is_empty() {
1159 let mut roots_handler = RootsHandler::new();
1160 for root in &self.roots {
1161 roots_handler = roots_handler.add_root(root.clone());
1162 }
1163 handlers.insert("roots/list".to_string(), Arc::new(roots_handler));
1164 }
1165
1166 #[cfg(feature = "protocol-2025-11-25")]
1168 if let Some(ref runtime) = self.task_runtime {
1169 use turul_mcp_server::{
1170 TasksCancelHandler, TasksGetHandler, TasksListHandler, TasksResultHandler,
1171 };
1172 handlers.insert(
1173 "tasks/get".to_string(),
1174 Arc::new(TasksGetHandler::new(Arc::clone(runtime))),
1175 );
1176 handlers.insert(
1177 "tasks/list".to_string(),
1178 Arc::new(TasksListHandler::new(Arc::clone(runtime))),
1179 );
1180 handlers.insert(
1181 "tasks/cancel".to_string(),
1182 Arc::new(TasksCancelHandler::new(Arc::clone(runtime))),
1183 );
1184 handlers.insert(
1185 "tasks/result".to_string(),
1186 Arc::new(TasksResultHandler::new(Arc::clone(runtime))),
1187 );
1188 }
1189
1190 if has_resources {
1192 let mut list_handler = ResourcesHandler::new();
1194 for resource in self.resources.values() {
1195 list_handler = list_handler.add_resource_arc(resource.clone());
1196 }
1197 handlers.insert("resources/list".to_string(), Arc::new(list_handler));
1198
1199 if !self.template_resources.is_empty() {
1201 let templates_handler =
1202 ResourceTemplatesHandler::new().with_templates(self.template_resources.clone());
1203 handlers.insert(
1204 "resources/templates/list".to_string(),
1205 Arc::new(templates_handler),
1206 );
1207 }
1208
1209 let mut read_handler = ResourcesReadHandler::new().without_security();
1211 for resource in self.resources.values() {
1212 read_handler = read_handler.add_resource_arc(resource.clone());
1213 }
1214 for (template, resource) in &self.template_resources {
1215 read_handler =
1216 read_handler.add_template_resource_arc(template.clone(), resource.clone());
1217 }
1218 handlers.insert("resources/read".to_string(), Arc::new(read_handler));
1219 }
1220
1221 let tool_fingerprint = turul_mcp_server::compute_tool_fingerprint(&self.tools);
1223
1224 Ok(LambdaMcpServer::new(
1226 implementation,
1227 capabilities,
1228 self.tools,
1229 self.resources,
1230 self.prompts,
1231 #[cfg(feature = "protocol-2025-11-25")]
1232 self.elicitations,
1233 #[cfg(feature = "protocol-2025-11-25")]
1234 self.sampling,
1235 self.completions,
1236 #[cfg(feature = "protocol-2025-11-25")]
1237 self.loggers,
1238 self.notifications,
1239 handlers,
1240 self.roots,
1241 self.instructions,
1242 session_storage,
1243 self.strict_lifecycle,
1244 self.server_config,
1245 self.enable_sse,
1246 self.stream_config,
1247 #[cfg(feature = "cors")]
1248 self.cors_config,
1249 self.middleware_stack,
1250 self.route_registry,
1251 #[cfg(feature = "protocol-2025-11-25")]
1252 self.task_runtime,
1253 tool_fingerprint,
1254 #[cfg(feature = "dynamic-tools")]
1255 !matches!(
1256 self.tool_change_mode,
1257 turul_mcp_server::ToolChangeMode::Static
1258 ),
1259 #[cfg(feature = "dynamic-tools")]
1260 self.server_state_storage,
1261 ))
1262 }
1263}
1264
1265impl Default for LambdaMcpServerBuilder {
1266 fn default() -> Self {
1267 Self::new()
1268 }
1269}
1270
1271pub trait LambdaMcpServerBuilderExt {
1273 fn tools<I, T>(self, tools: I) -> Self
1275 where
1276 I: IntoIterator<Item = T>,
1277 T: McpTool + 'static;
1278}
1279
1280impl LambdaMcpServerBuilderExt for LambdaMcpServerBuilder {
1281 fn tools<I, T>(mut self, tools: I) -> Self
1282 where
1283 I: IntoIterator<Item = T>,
1284 T: McpTool + 'static,
1285 {
1286 for tool in tools {
1287 self = self.tool(tool);
1288 }
1289 self
1290 }
1291}
1292
1293pub async fn simple_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1298where
1299 I: IntoIterator<Item = T>,
1300 T: McpTool + 'static,
1301{
1302 let mut builder = LambdaMcpServerBuilder::new();
1303
1304 for tool in tools {
1305 builder = builder.tool(tool);
1306 }
1307
1308 #[cfg(feature = "cors")]
1309 {
1310 builder = builder.cors_allow_all_origins();
1311 }
1312
1313 builder.sse(false).build().await
1314}
1315
1316#[cfg(all(feature = "dynamodb", feature = "cors"))]
1320pub async fn production_lambda_server<I, T>(tools: I) -> Result<LambdaMcpServer>
1321where
1322 I: IntoIterator<Item = T>,
1323 T: McpTool + 'static,
1324{
1325 let mut builder = LambdaMcpServerBuilder::new();
1326
1327 for tool in tools {
1328 builder = builder.tool(tool);
1329 }
1330
1331 builder.production_config().await?.build().await
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use turul_mcp_builders::prelude::*;
1338 use turul_mcp_session_storage::InMemorySessionStorage; #[derive(Clone, Default)]
1342 struct TestTool;
1343
1344 impl HasBaseMetadata for TestTool {
1345 fn name(&self) -> &str {
1346 "test_tool"
1347 }
1348 }
1349
1350 impl HasDescription for TestTool {
1351 fn description(&self) -> Option<&str> {
1352 Some("Test tool")
1353 }
1354 }
1355
1356 impl HasInputSchema for TestTool {
1357 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1358 use turul_mcp_protocol::ToolSchema;
1359 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1360 SCHEMA.get_or_init(ToolSchema::object)
1361 }
1362 }
1363
1364 impl HasOutputSchema for TestTool {
1365 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1366 None
1367 }
1368 }
1369
1370 impl HasAnnotations for TestTool {
1371 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
1372 None
1373 }
1374 }
1375
1376 impl HasToolMeta for TestTool {
1377 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
1378 None
1379 }
1380 }
1381
1382 impl HasIcons for TestTool {}
1383 impl HasExecution for TestTool {}
1384
1385 #[async_trait::async_trait]
1386 impl McpTool for TestTool {
1387 async fn call(
1388 &self,
1389 _args: serde_json::Value,
1390 _session: Option<turul_mcp_server::SessionContext>,
1391 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
1392 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
1393 Ok(CallToolResult::success(vec![ToolResult::text(
1394 "test result",
1395 )]))
1396 }
1397 }
1398
1399 #[tokio::test]
1400 async fn test_builder_basic() {
1401 let server = LambdaMcpServerBuilder::new()
1402 .name("test-server")
1403 .version("1.0.0")
1404 .tool(TestTool)
1405 .storage(Arc::new(InMemorySessionStorage::new()))
1406 .sse(false) .build()
1408 .await
1409 .unwrap();
1410
1411 let handler = server.handler().await.unwrap();
1413 assert!(
1415 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1416 "Stream manager must be initialized"
1417 );
1418 }
1419
1420 #[tokio::test]
1421 async fn test_simple_lambda_server() {
1422 let tools = vec![TestTool];
1423 let server = simple_lambda_server(tools).await.unwrap();
1424
1425 let handler = server.handler().await.unwrap();
1427 assert!(
1430 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1431 "Stream manager must be initialized"
1432 );
1433 }
1434
1435 #[tokio::test]
1436 async fn test_builder_extension_trait() {
1437 let tools = vec![TestTool, TestTool];
1438
1439 let server = LambdaMcpServerBuilder::new()
1440 .tools(tools)
1441 .storage(Arc::new(InMemorySessionStorage::new()))
1442 .sse(false) .build()
1444 .await
1445 .unwrap();
1446
1447 let handler = server.handler().await.unwrap();
1448 assert!(
1451 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1452 "Stream manager must be initialized"
1453 );
1454 }
1455
1456 #[cfg(feature = "protocol-2026-07-28")]
1464 #[tokio::test]
1465 async fn test_registered_methods_parity_2026_07_28() {
1466 use std::collections::BTreeSet;
1467
1468 let server = LambdaMcpServerBuilder::new()
1469 .name("parity-test")
1470 .version("1.0.0")
1471 .tool(TestTool)
1472 .storage(Arc::new(InMemorySessionStorage::new()))
1473 .sse(false)
1474 .build()
1475 .await
1476 .unwrap();
1477 let handler = server.handler().await.unwrap();
1478
1479 let expected: BTreeSet<String> = [
1480 "tools/list",
1481 "tools/call",
1482 "server/discover",
1483 "resources/list",
1484 "resources/read",
1485 "resources/templates/list",
1486 "prompts/list",
1487 "prompts/get",
1488 "notifications/cancelled",
1489 "notifications/resources/list_changed",
1490 "notifications/resources/updated",
1491 "notifications/tools/list_changed",
1492 "notifications/prompts/list_changed",
1493 "notifications/resources/listChanged",
1494 "notifications/tools/listChanged",
1495 "notifications/prompts/listChanged",
1496 ]
1497 .into_iter()
1498 .map(String::from)
1499 .collect();
1500
1501 let actual: BTreeSet<String> = handler.registered_methods().into_iter().collect();
1502 assert_eq!(actual, expected);
1503 }
1504
1505 #[cfg(feature = "protocol-2025-11-25")]
1506 #[tokio::test]
1507 async fn test_registered_methods_parity_2025_11_25() {
1508 use std::collections::BTreeSet;
1509
1510 let server = LambdaMcpServerBuilder::new()
1511 .name("parity-test")
1512 .version("1.0.0")
1513 .tool(TestTool)
1514 .storage(Arc::new(InMemorySessionStorage::new()))
1515 .sse(false)
1516 .build()
1517 .await
1518 .unwrap();
1519 let handler = server.handler().await.unwrap();
1520
1521 let expected: BTreeSet<String> = [
1522 "initialize",
1523 "tools/list",
1524 "tools/call",
1525 "ping",
1526 "resources/list",
1527 "resources/read",
1528 "resources/templates/list",
1529 "prompts/list",
1530 "prompts/get",
1531 "logging/setLevel",
1532 "roots/list",
1533 "sampling/createMessage",
1534 "elicitation/create",
1535 "notifications/message",
1536 "notifications/progress",
1537 "notifications/cancelled",
1538 "notifications/resources/list_changed",
1539 "notifications/resources/updated",
1540 "notifications/tools/list_changed",
1541 "notifications/prompts/list_changed",
1542 "notifications/roots/list_changed",
1543 "notifications/resources/listChanged",
1544 "notifications/tools/listChanged",
1545 "notifications/prompts/listChanged",
1546 "notifications/roots/listChanged",
1547 "notifications/initialized",
1548 ]
1549 .into_iter()
1550 .map(String::from)
1551 .collect();
1552
1553 let actual: BTreeSet<String> = handler.registered_methods().into_iter().collect();
1554 assert_eq!(actual, expected);
1555 }
1556
1557 #[cfg(feature = "cors")]
1558 #[tokio::test]
1559 async fn test_cors_configuration() {
1560 let server = LambdaMcpServerBuilder::new()
1561 .cors_allow_all_origins()
1562 .storage(Arc::new(InMemorySessionStorage::new()))
1563 .sse(false) .build()
1565 .await
1566 .unwrap();
1567
1568 let handler = server.handler().await.unwrap();
1569 assert!(
1571 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1572 "Stream manager must be initialized"
1573 );
1574 }
1575
1576 #[cfg(feature = "cors")]
1591 mod cors_propagation {
1592 use super::*;
1593 use async_trait::async_trait;
1594 use http::Request;
1595 use lambda_http::Body as LambdaBody;
1596 use turul_http_mcp_server::middleware::{
1597 DispatcherResult, McpMiddleware, MiddlewareError, RequestContext, SessionInjection,
1598 };
1599 use turul_mcp_session_storage::SessionView;
1600
1601 fn preflight_request() -> lambda_http::Request {
1602 Request::builder()
1603 .method("OPTIONS")
1604 .uri("/mcp")
1605 .header("Origin", "https://client.example.test")
1606 .header("Access-Control-Request-Method", "POST")
1607 .body(LambdaBody::Empty)
1608 .unwrap()
1609 }
1610
1611 fn post_request() -> lambda_http::Request {
1612 Request::builder()
1613 .method("POST")
1614 .uri("/mcp")
1615 .header("Content-Type", "application/json")
1616 .header("Accept", "application/json, text/event-stream")
1617 .header("MCP-Protocol-Version", "2025-11-25")
1618 .header("Origin", "https://client.example.test")
1619 .body(LambdaBody::Text(
1620 r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
1621 ))
1622 .unwrap()
1623 }
1624
1625 #[tokio::test]
1631 async fn builder_path_streaming_preflight_has_cors() {
1632 let server = LambdaMcpServerBuilder::new()
1633 .cors_allow_all_origins()
1634 .storage(Arc::new(InMemorySessionStorage::new()))
1635 .sse(false)
1636 .build()
1637 .await
1638 .unwrap();
1639 let handler = server.handler().await.unwrap();
1640
1641 let resp = handler.handle_streaming(preflight_request()).await.unwrap();
1642 assert_eq!(resp.status(), 200);
1643 assert!(
1644 resp.headers().contains_key("access-control-allow-origin"),
1645 "builder-configured CORS must reach the streaming preflight response; \
1646 got headers: {:?}",
1647 resp.headers(),
1648 );
1649 assert!(
1650 resp.headers().contains_key("access-control-allow-methods"),
1651 "preflight must advertise methods",
1652 );
1653 }
1654
1655 #[tokio::test]
1659 async fn cors_origin_list_derives_origin_allowlist() {
1660 let server = LambdaMcpServerBuilder::new()
1661 .cors_allow_origins(vec!["https://client.example.test".to_string()])
1662 .storage(Arc::new(InMemorySessionStorage::new()))
1663 .sse(false)
1664 .build()
1665 .await
1666 .unwrap();
1667 let handler = server.handler().await.unwrap();
1668
1669 let resp = handler.handle_streaming(post_request()).await.unwrap();
1671 assert_ne!(
1672 resp.status(),
1673 403,
1674 "allowlisted origin must pass the origin gate"
1675 );
1676
1677 let req = Request::builder()
1679 .method("POST")
1680 .uri("/mcp")
1681 .header("Content-Type", "application/json")
1682 .header("Accept", "application/json, text/event-stream")
1683 .header("MCP-Protocol-Version", "2025-11-25")
1684 .header("Origin", "https://other.example.test")
1685 .body(LambdaBody::Text(
1686 r#"{"jsonrpc":"2.0","method":"initialize","id":1}"#.to_string(),
1687 ))
1688 .unwrap();
1689 let resp = handler.handle_streaming(req).await.unwrap();
1690 assert_eq!(
1691 resp.status(),
1692 403,
1693 "unlisted cross-origin request must get 403 Forbidden"
1694 );
1695 }
1696
1697 #[tokio::test]
1701 async fn builder_path_streaming_401_has_cors_and_exposes_www_authenticate() {
1702 struct ForceChallenge;
1703
1704 #[async_trait]
1705 impl McpMiddleware for ForceChallenge {
1706 fn runs_before_session(&self) -> bool {
1707 true
1708 }
1709 async fn before_dispatch(
1710 &self,
1711 _ctx: &mut RequestContext<'_>,
1712 _session: Option<&dyn SessionView>,
1713 _injection: &mut SessionInjection,
1714 ) -> std::result::Result<(), MiddlewareError> {
1715 Err(MiddlewareError::http_challenge(
1716 401,
1717 "Bearer realm=\"mcp\", \
1718 resource_metadata=\"https://example.test/.well-known/oauth-protected-resource\"",
1719 ))
1720 }
1721 async fn after_dispatch(
1722 &self,
1723 _ctx: &RequestContext<'_>,
1724 _result: &mut DispatcherResult,
1725 ) -> std::result::Result<(), MiddlewareError> {
1726 Ok(())
1727 }
1728 }
1729
1730 let server = LambdaMcpServerBuilder::new()
1731 .cors_allow_all_origins()
1732 .middleware(Arc::new(ForceChallenge))
1733 .storage(Arc::new(InMemorySessionStorage::new()))
1734 .sse(false)
1735 .build()
1736 .await
1737 .unwrap();
1738 let handler = server.handler().await.unwrap();
1739
1740 let resp = handler.handle_streaming(post_request()).await.unwrap();
1741 let headers = resp.headers();
1742
1743 assert_eq!(resp.status(), 401);
1744 assert!(
1745 headers.contains_key("www-authenticate"),
1746 "WWW-Authenticate must survive the builder-path streaming transport",
1747 );
1748 assert!(
1749 headers.contains_key("access-control-allow-origin"),
1750 "401 must carry CORS through the builder path — was the source of the v0.3.40 production gap",
1751 );
1752 let expose = headers
1753 .get("access-control-expose-headers")
1754 .and_then(|v| v.to_str().ok())
1755 .unwrap_or("");
1756 assert!(
1757 expose
1758 .split(',')
1759 .map(str::trim)
1760 .any(|h| h.eq_ignore_ascii_case("WWW-Authenticate")),
1761 "expose-headers must include WWW-Authenticate; got {expose:?}",
1762 );
1763 }
1764
1765 #[tokio::test]
1769 async fn builder_path_without_cors_emits_no_cors_headers() {
1770 let server = LambdaMcpServerBuilder::new()
1771 .storage(Arc::new(InMemorySessionStorage::new()))
1772 .sse(false)
1773 .build()
1774 .await
1775 .unwrap();
1776 let handler = server.handler().await.unwrap();
1777
1778 let resp = handler.handle_streaming(preflight_request()).await.unwrap();
1779 assert!(
1780 !resp.headers().contains_key("access-control-allow-origin"),
1781 "no builder CORS → no CORS headers; got {:?}",
1782 resp.headers(),
1783 );
1784 }
1785 }
1786
1787 #[tokio::test]
1788 #[allow(deprecated)] async fn test_sse_toggle_functionality() {
1790 let mut builder =
1792 LambdaMcpServerBuilder::new().storage(Arc::new(InMemorySessionStorage::new()));
1793
1794 builder = builder.sse(true);
1796 assert!(builder.enable_sse, "SSE should be enabled");
1797 assert!(
1798 builder.server_config.enable_get_sse,
1799 "GET SSE endpoint should be enabled"
1800 );
1801 assert!(
1802 builder.server_config.enable_post_sse,
1803 "POST SSE endpoint should be enabled"
1804 );
1805
1806 builder = builder.sse(false);
1808 assert!(!builder.enable_sse, "SSE should be disabled");
1809 assert!(
1810 !builder.server_config.enable_get_sse,
1811 "GET SSE endpoint should be disabled"
1812 );
1813 assert!(
1814 !builder.server_config.enable_post_sse,
1815 "POST SSE endpoint should be disabled"
1816 );
1817
1818 builder = builder.sse(true);
1820 assert!(builder.enable_sse, "SSE should be re-enabled");
1821 assert!(
1822 builder.server_config.enable_get_sse,
1823 "GET SSE endpoint should be re-enabled"
1824 );
1825 assert!(
1826 builder.server_config.enable_post_sse,
1827 "POST SSE endpoint should be re-enabled"
1828 );
1829
1830 let server = builder.build().await.unwrap();
1832 let handler = server.handler().await.unwrap();
1833 assert!(
1834 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1835 "Stream manager must be initialized"
1836 );
1837 }
1838
1839 #[cfg(feature = "protocol-2025-11-25")]
1846 #[tokio::test]
1847 async fn test_builder_without_tasks_no_capability() {
1848 let server = LambdaMcpServerBuilder::new()
1849 .name("no-tasks")
1850 .tool(TestTool)
1851 .storage(Arc::new(InMemorySessionStorage::new()))
1852 .sse(false)
1853 .build()
1854 .await
1855 .unwrap();
1856
1857 assert!(
1858 server.capabilities().tasks.is_none(),
1859 "Tasks capability should not be advertised without task storage"
1860 );
1861 }
1862
1863 #[cfg(feature = "protocol-2025-11-25")]
1864 #[tokio::test]
1865 async fn test_builder_with_task_storage_advertises_capability() {
1866 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1867
1868 let server = LambdaMcpServerBuilder::new()
1869 .name("with-tasks")
1870 .tool(TestTool)
1871 .storage(Arc::new(InMemorySessionStorage::new()))
1872 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1873 .sse(false)
1874 .build()
1875 .await
1876 .unwrap();
1877
1878 let tasks_cap = server
1879 .capabilities()
1880 .tasks
1881 .as_ref()
1882 .expect("Tasks capability should be advertised");
1883 assert!(tasks_cap.list.is_some(), "list capability should be set");
1884 assert!(
1885 tasks_cap.cancel.is_some(),
1886 "cancel capability should be set"
1887 );
1888 let requests = tasks_cap
1889 .requests
1890 .as_ref()
1891 .expect("requests capability should be set");
1892 let tools = requests
1893 .tools
1894 .as_ref()
1895 .expect("tools capability should be set");
1896 assert!(tools.call.is_some(), "tools.call capability should be set");
1897 }
1898
1899 #[cfg(feature = "protocol-2025-11-25")]
1900 #[tokio::test]
1901 async fn test_builder_with_task_runtime_advertises_capability() {
1902 let runtime = Arc::new(turul_mcp_server::TaskRuntime::in_memory());
1903
1904 let server = LambdaMcpServerBuilder::new()
1905 .name("with-runtime")
1906 .tool(TestTool)
1907 .storage(Arc::new(InMemorySessionStorage::new()))
1908 .with_task_runtime(runtime)
1909 .sse(false)
1910 .build()
1911 .await
1912 .unwrap();
1913
1914 assert!(
1915 server.capabilities().tasks.is_some(),
1916 "Tasks capability should be advertised with task runtime"
1917 );
1918 }
1919
1920 #[cfg(feature = "protocol-2025-11-25")]
1921 #[tokio::test]
1922 async fn test_task_recovery_timeout_configuration() {
1923 use turul_mcp_server::task_storage::InMemoryTaskStorage;
1924
1925 let server = LambdaMcpServerBuilder::new()
1926 .name("custom-timeout")
1927 .tool(TestTool)
1928 .storage(Arc::new(InMemorySessionStorage::new()))
1929 .task_recovery_timeout_ms(60_000)
1930 .with_task_storage(Arc::new(InMemoryTaskStorage::new()))
1931 .sse(false)
1932 .build()
1933 .await
1934 .unwrap();
1935
1936 assert!(
1937 server.capabilities().tasks.is_some(),
1938 "Tasks should be enabled with custom timeout"
1939 );
1940 }
1941
1942 #[cfg(feature = "protocol-2025-11-25")]
1943 #[tokio::test]
1944 async fn test_backward_compatibility_no_tasks() {
1945 let server = LambdaMcpServerBuilder::new()
1947 .name("backward-compat")
1948 .version("1.0.0")
1949 .tool(TestTool)
1950 .storage(Arc::new(InMemorySessionStorage::new()))
1951 .sse(false)
1952 .build()
1953 .await
1954 .unwrap();
1955
1956 let handler = server.handler().await.unwrap();
1957 assert!(
1958 handler.get_stream_manager().as_ref() as *const _ as usize > 0,
1959 "Stream manager must be initialized"
1960 );
1961 assert!(server.capabilities().tasks.is_none());
1962 }
1963
1964 #[cfg(feature = "protocol-2025-11-25")]
1967 #[derive(Clone, Default)]
1968 struct SlowTool;
1969
1970 #[cfg(feature = "protocol-2025-11-25")]
1971 impl HasBaseMetadata for SlowTool {
1972 fn name(&self) -> &str {
1973 "slow_tool"
1974 }
1975 }
1976
1977 #[cfg(feature = "protocol-2025-11-25")]
1978 impl HasDescription for SlowTool {
1979 fn description(&self) -> Option<&str> {
1980 Some("A slow tool for testing")
1981 }
1982 }
1983
1984 #[cfg(feature = "protocol-2025-11-25")]
1985 impl HasInputSchema for SlowTool {
1986 fn input_schema(&self) -> &turul_mcp_protocol::ToolSchema {
1987 use turul_mcp_protocol::ToolSchema;
1988 static SCHEMA: std::sync::OnceLock<ToolSchema> = std::sync::OnceLock::new();
1989 SCHEMA.get_or_init(ToolSchema::object)
1990 }
1991 }
1992
1993 #[cfg(feature = "protocol-2025-11-25")]
1994 impl HasOutputSchema for SlowTool {
1995 fn output_schema(&self) -> Option<&turul_mcp_protocol::ToolSchema> {
1996 None
1997 }
1998 }
1999
2000 #[cfg(feature = "protocol-2025-11-25")]
2001 impl HasAnnotations for SlowTool {
2002 fn annotations(&self) -> Option<&turul_mcp_protocol::tools::ToolAnnotations> {
2003 None
2004 }
2005 }
2006
2007 #[cfg(feature = "protocol-2025-11-25")]
2008 impl HasToolMeta for SlowTool {
2009 fn tool_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
2010 None
2011 }
2012 }
2013
2014 #[cfg(feature = "protocol-2025-11-25")]
2015 impl HasIcons for SlowTool {}
2016 #[cfg(feature = "protocol-2025-11-25")]
2017 impl HasExecution for SlowTool {
2018 fn execution(&self) -> Option<turul_mcp_protocol::tools::ToolExecution> {
2019 Some(turul_mcp_protocol::tools::ToolExecution {
2020 task_support: Some(turul_mcp_protocol::tools::TaskSupport::Optional),
2021 })
2022 }
2023 }
2024
2025 #[cfg(feature = "protocol-2025-11-25")]
2026 #[async_trait::async_trait]
2027 impl McpTool for SlowTool {
2028 async fn call(
2029 &self,
2030 _args: serde_json::Value,
2031 _session: Option<turul_mcp_server::SessionContext>,
2032 ) -> turul_mcp_server::McpResult<turul_mcp_protocol::tools::CallToolResult> {
2033 use turul_mcp_protocol::tools::{CallToolResult, ToolResult};
2034 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
2036 Ok(CallToolResult::success(vec![ToolResult::text("slow done")]))
2037 }
2038 }
2039
2040 #[cfg(feature = "protocol-2025-11-25")]
2041 #[tokio::test]
2042 async fn test_nonblocking_tools_call_with_task() {
2043 use turul_mcp_server::SessionAwareToolHandler;
2044 use turul_mcp_server::task_storage::InMemoryTaskStorage;
2045 use turul_rpc::r#async::JsonRpcHandler;
2046
2047 let task_storage = Arc::new(InMemoryTaskStorage::new());
2048 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
2049 task_storage,
2050 ));
2051
2052 let mut tools: HashMap<String, Arc<dyn McpTool>> = HashMap::new();
2054 tools.insert("slow_tool".to_string(), Arc::new(SlowTool));
2055
2056 let session_storage: Arc<turul_mcp_session_storage::BoxedSessionStorage> =
2058 Arc::new(InMemorySessionStorage::new());
2059 let session_manager = Arc::new(turul_mcp_server::session::SessionManager::with_storage(
2060 session_storage,
2061 turul_mcp_protocol::ServerCapabilities::default(),
2062 ));
2063
2064 let tool_handler = SessionAwareToolHandler::new(tools, session_manager, false)
2066 .with_task_runtime(Arc::clone(&runtime));
2067
2068 let params = serde_json::json!({
2070 "name": "slow_tool",
2071 "arguments": {},
2072 "task": {}
2073 });
2074 let request_params = turul_rpc::RequestParams::Object(
2075 params
2076 .as_object()
2077 .unwrap()
2078 .iter()
2079 .map(|(k, v)| (k.clone(), v.clone()))
2080 .collect(),
2081 );
2082
2083 let start = std::time::Instant::now();
2085 let result = tool_handler
2086 .handle("tools/call", Some(request_params), None)
2087 .await;
2088 let elapsed = start.elapsed();
2089
2090 let value = result.expect("tools/call with task should succeed");
2092 assert!(
2093 value.get("task").is_some(),
2094 "Response should contain 'task' field (CreateTaskResult shape)"
2095 );
2096 let task = value.get("task").unwrap();
2097 assert!(
2098 task.get("taskId").is_some(),
2099 "Task should have taskId field"
2100 );
2101 assert_eq!(
2102 task.get("status")
2103 .and_then(|v| v.as_str())
2104 .unwrap_or_default(),
2105 "working",
2106 "Task status should be 'working'"
2107 );
2108
2109 assert!(
2113 elapsed < std::time::Duration::from_secs(1),
2114 "tools/call with task should return immediately (took {:?}, expected < 1s)",
2115 elapsed
2116 );
2117 }
2118
2119 #[derive(Clone)]
2125 struct StaticTestResource;
2126
2127 impl turul_mcp_builders::prelude::HasResourceMetadata for StaticTestResource {
2128 fn name(&self) -> &str {
2129 "static_test"
2130 }
2131 }
2132
2133 impl turul_mcp_builders::prelude::HasResourceDescription for StaticTestResource {
2134 fn description(&self) -> Option<&str> {
2135 Some("Static test resource")
2136 }
2137 }
2138
2139 impl turul_mcp_builders::prelude::HasResourceUri for StaticTestResource {
2140 fn uri(&self) -> &str {
2141 "file:///test.txt"
2142 }
2143 }
2144
2145 impl turul_mcp_builders::prelude::HasResourceMimeType for StaticTestResource {
2146 fn mime_type(&self) -> Option<&str> {
2147 Some("text/plain")
2148 }
2149 }
2150
2151 impl turul_mcp_builders::prelude::HasResourceSize for StaticTestResource {
2152 fn size(&self) -> Option<u64> {
2153 None
2154 }
2155 }
2156
2157 impl turul_mcp_builders::prelude::HasResourceAnnotations for StaticTestResource {
2158 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
2159 None
2160 }
2161 }
2162
2163 impl turul_mcp_builders::prelude::HasResourceMeta for StaticTestResource {
2164 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
2165 None
2166 }
2167 }
2168
2169 impl HasIcons for StaticTestResource {}
2170
2171 #[async_trait::async_trait]
2172 impl McpResource for StaticTestResource {
2173 async fn read(
2174 &self,
2175 _params: Option<serde_json::Value>,
2176 _session: Option<&turul_mcp_server::SessionContext>,
2177 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
2178 {
2179 use turul_mcp_protocol::resources::ResourceContent;
2180 Ok(vec![ResourceContent::text("file:///test.txt", "test")])
2181 }
2182 }
2183
2184 #[derive(Clone)]
2186 struct TemplateTestResource;
2187
2188 impl turul_mcp_builders::prelude::HasResourceMetadata for TemplateTestResource {
2189 fn name(&self) -> &str {
2190 "template_test"
2191 }
2192 }
2193
2194 impl turul_mcp_builders::prelude::HasResourceDescription for TemplateTestResource {
2195 fn description(&self) -> Option<&str> {
2196 Some("Template test resource")
2197 }
2198 }
2199
2200 impl turul_mcp_builders::prelude::HasResourceUri for TemplateTestResource {
2201 fn uri(&self) -> &str {
2202 "agent://agents/{agent_id}"
2203 }
2204 }
2205
2206 impl turul_mcp_builders::prelude::HasResourceMimeType for TemplateTestResource {
2207 fn mime_type(&self) -> Option<&str> {
2208 Some("application/json")
2209 }
2210 }
2211
2212 impl turul_mcp_builders::prelude::HasResourceSize for TemplateTestResource {
2213 fn size(&self) -> Option<u64> {
2214 None
2215 }
2216 }
2217
2218 impl turul_mcp_builders::prelude::HasResourceAnnotations for TemplateTestResource {
2219 fn annotations(&self) -> Option<&turul_mcp_protocol::meta::Annotations> {
2220 None
2221 }
2222 }
2223
2224 impl turul_mcp_builders::prelude::HasResourceMeta for TemplateTestResource {
2225 fn resource_meta(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
2226 None
2227 }
2228 }
2229
2230 impl HasIcons for TemplateTestResource {}
2231
2232 #[async_trait::async_trait]
2233 impl McpResource for TemplateTestResource {
2234 async fn read(
2235 &self,
2236 _params: Option<serde_json::Value>,
2237 _session: Option<&turul_mcp_server::SessionContext>,
2238 ) -> turul_mcp_server::McpResult<Vec<turul_mcp_protocol::resources::ResourceContent>>
2239 {
2240 use turul_mcp_protocol::resources::ResourceContent;
2241 Ok(vec![ResourceContent::text("agent://agents/test", "{}")])
2242 }
2243 }
2244
2245 #[test]
2246 fn test_resource_auto_detection_static() {
2247 let builder = LambdaMcpServerBuilder::new()
2248 .name("test")
2249 .resource(StaticTestResource);
2250
2251 assert_eq!(builder.resources.len(), 1);
2252 assert!(builder.resources.contains_key("file:///test.txt"));
2253 assert_eq!(builder.template_resources.len(), 0);
2254 }
2255
2256 #[test]
2257 fn test_resource_auto_detection_template() {
2258 let builder = LambdaMcpServerBuilder::new()
2259 .name("test")
2260 .resource(TemplateTestResource);
2261
2262 assert_eq!(builder.resources.len(), 0);
2263 assert_eq!(builder.template_resources.len(), 1);
2264
2265 let (template, _) = &builder.template_resources[0];
2266 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
2267 }
2268
2269 #[test]
2270 fn test_resource_auto_detection_mixed() {
2271 let builder = LambdaMcpServerBuilder::new()
2272 .name("test")
2273 .resource(StaticTestResource)
2274 .resource(TemplateTestResource);
2275
2276 assert_eq!(builder.resources.len(), 1);
2277 assert!(builder.resources.contains_key("file:///test.txt"));
2278 assert_eq!(builder.template_resources.len(), 1);
2279
2280 let (template, _) = &builder.template_resources[0];
2281 assert_eq!(template.pattern(), "agent://agents/{agent_id}");
2282 }
2283
2284 #[tokio::test]
2285 async fn test_build_advertises_resources_capability_for_templates_only() {
2286 let server = LambdaMcpServerBuilder::new()
2287 .name("template-only")
2288 .resource(TemplateTestResource)
2289 .storage(Arc::new(InMemorySessionStorage::new()))
2290 .sse(false)
2291 .build()
2292 .await
2293 .unwrap();
2294
2295 assert!(
2296 server.capabilities().resources.is_some(),
2297 "Resources capability should be advertised when template resources are registered"
2298 );
2299 }
2300
2301 #[tokio::test]
2302 async fn test_build_advertises_resources_capability_for_static_only() {
2303 let server = LambdaMcpServerBuilder::new()
2304 .name("static-only")
2305 .resource(StaticTestResource)
2306 .storage(Arc::new(InMemorySessionStorage::new()))
2307 .sse(false)
2308 .build()
2309 .await
2310 .unwrap();
2311
2312 assert!(
2313 server.capabilities().resources.is_some(),
2314 "Resources capability should be advertised when static resources are registered"
2315 );
2316 }
2317
2318 #[tokio::test]
2319 async fn test_build_no_resources_no_capability() {
2320 let server = LambdaMcpServerBuilder::new()
2321 .name("no-resources")
2322 .tool(TestTool)
2323 .storage(Arc::new(InMemorySessionStorage::new()))
2324 .sse(false)
2325 .build()
2326 .await
2327 .unwrap();
2328
2329 assert!(
2330 server.capabilities().resources.is_none(),
2331 "Resources capability should NOT be advertised when no resources are registered"
2332 );
2333 }
2334
2335 #[tokio::test]
2336 async fn test_lambda_builder_templates_list_returns_template() {
2337 use turul_mcp_server::handlers::McpHandler;
2338
2339 let builder = LambdaMcpServerBuilder::new()
2341 .name("template-test")
2342 .resource(TemplateTestResource);
2343
2344 assert_eq!(builder.template_resources.len(), 1);
2346
2347 let handler =
2349 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
2350
2351 let result = handler.handle(None).await.expect("should succeed");
2353
2354 let templates = result["resourceTemplates"]
2355 .as_array()
2356 .expect("resourceTemplates should be an array");
2357 assert_eq!(
2358 templates.len(),
2359 1,
2360 "Should have exactly 1 template resource"
2361 );
2362 assert_eq!(
2363 templates[0]["uriTemplate"], "agent://agents/{agent_id}",
2364 "Template URI should match"
2365 );
2366 assert_eq!(templates[0]["name"], "template_test");
2367 }
2368
2369 #[tokio::test]
2370 async fn test_lambda_builder_resources_list_returns_static() {
2371 use turul_mcp_server::handlers::McpHandler;
2372
2373 let builder = LambdaMcpServerBuilder::new()
2375 .name("static-test")
2376 .resource(StaticTestResource);
2377
2378 assert_eq!(builder.resources.len(), 1);
2379
2380 let mut handler = ResourcesHandler::new();
2381 for resource in builder.resources.values() {
2382 handler = handler.add_resource_arc(resource.clone());
2383 }
2384
2385 let result = handler.handle(None).await.expect("should succeed");
2386
2387 let resources = result["resources"]
2388 .as_array()
2389 .expect("resources should be an array");
2390 assert_eq!(resources.len(), 1, "Should have exactly 1 static resource");
2391 assert_eq!(resources[0]["uri"], "file:///test.txt");
2392 assert_eq!(resources[0]["name"], "static_test");
2393 }
2394
2395 #[tokio::test]
2396 async fn test_lambda_builder_mixed_resources_separation() {
2397 use turul_mcp_server::handlers::McpHandler;
2398
2399 let builder = LambdaMcpServerBuilder::new()
2401 .name("mixed-test")
2402 .resource(StaticTestResource)
2403 .resource(TemplateTestResource);
2404
2405 assert_eq!(builder.resources.len(), 1);
2406 assert_eq!(builder.template_resources.len(), 1);
2407
2408 let mut list_handler = ResourcesHandler::new();
2410 for resource in builder.resources.values() {
2411 list_handler = list_handler.add_resource_arc(resource.clone());
2412 }
2413
2414 let templates_handler =
2415 ResourceTemplatesHandler::new().with_templates(builder.template_resources.clone());
2416
2417 let list_result = list_handler.handle(None).await.expect("should succeed");
2419 let resources = list_result["resources"]
2420 .as_array()
2421 .expect("resources should be an array");
2422 assert_eq!(resources.len(), 1, "Only static resource in resources/list");
2423 assert_eq!(resources[0]["uri"], "file:///test.txt");
2424
2425 let templates_result = templates_handler
2427 .handle(None)
2428 .await
2429 .expect("should succeed");
2430 let templates = templates_result["resourceTemplates"]
2431 .as_array()
2432 .expect("resourceTemplates should be an array");
2433 assert_eq!(
2434 templates.len(),
2435 1,
2436 "Only template resource in resources/templates/list"
2437 );
2438 assert_eq!(templates[0]["uriTemplate"], "agent://agents/{agent_id}");
2439 }
2440
2441 #[cfg(feature = "protocol-2025-11-25")]
2442 #[tokio::test]
2443 async fn test_tasks_get_route_registered() {
2444 use turul_mcp_server::TasksGetHandler;
2445 use turul_mcp_server::handlers::McpHandler;
2446 use turul_mcp_server::task_storage::InMemoryTaskStorage;
2447
2448 let runtime = Arc::new(turul_mcp_server::TaskRuntime::with_default_executor(
2449 Arc::new(InMemoryTaskStorage::new()),
2450 ));
2451 let handler = TasksGetHandler::new(runtime);
2452
2453 let params = serde_json::json!({ "taskId": "nonexistent-task-id" });
2456
2457 let result = handler.handle(Some(params)).await;
2458
2459 assert!(
2461 result.is_err(),
2462 "tasks/get with unknown task should return error"
2463 );
2464 let err = result.unwrap_err();
2465 let err_str = err.to_string();
2466 assert!(
2467 !err_str.contains("method not found"),
2468 "Error should not be 'method not found' — handler should respond to tasks/get"
2469 );
2470 }
2471
2472 #[cfg(feature = "protocol-2025-11-25")]
2481 #[tokio::test]
2482 async fn test_resources_read_registered_by_default() {
2483 use lambda_http::Body as LambdaBody;
2484
2485 let server = LambdaMcpServerBuilder::new()
2486 .name("parity-test")
2487 .version("1.0.0")
2488 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2490 .strict_lifecycle(false) .sse(false)
2492 .build()
2493 .await
2494 .unwrap();
2495
2496 let handler = server.handler().await.unwrap();
2497
2498 let init_req = http::Request::builder()
2500 .method("POST")
2501 .uri("/mcp")
2502 .header("Content-Type", "application/json")
2503 .header("MCP-Protocol-Version", "2025-11-25")
2504 .body(LambdaBody::Text(
2505 serde_json::json!({
2506 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2507 "params": {
2508 "protocolVersion": "2025-11-25",
2509 "capabilities": {},
2510 "clientInfo": { "name": "test", "version": "1.0.0" }
2511 }
2512 })
2513 .to_string(),
2514 ))
2515 .unwrap();
2516 let init_resp = handler.handle(init_req).await.unwrap();
2517 let session_id = init_resp
2518 .headers()
2519 .get("Mcp-Session-Id")
2520 .unwrap()
2521 .to_str()
2522 .unwrap()
2523 .to_string();
2524
2525 let read_req = http::Request::builder()
2528 .method("POST")
2529 .uri("/mcp")
2530 .header("Content-Type", "application/json")
2531 .header("MCP-Protocol-Version", "2025-11-25")
2532 .header("Mcp-Session-Id", &session_id)
2533 .body(LambdaBody::Text(
2534 serde_json::json!({
2535 "jsonrpc": "2.0", "method": "resources/read", "id": 2,
2536 "params": { "uri": "file:///nonexistent" }
2537 })
2538 .to_string(),
2539 ))
2540 .unwrap();
2541 let read_resp = handler.handle(read_req).await.unwrap();
2542 let body = String::from_utf8_lossy(read_resp.body().as_ref()).to_string();
2543 let json: serde_json::Value = serde_json::from_str(&body)
2544 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2545
2546 assert!(
2548 json["error"].is_object(),
2549 "resources/read must return JSON-RPC error, got: {json}"
2550 );
2551 let error_code = json["error"]["code"].as_i64().unwrap();
2555 assert_ne!(
2556 error_code, -32601,
2557 "resources/read must be registered (got method-not-found -32601): {json}"
2558 );
2559 }
2560
2561 #[cfg(feature = "protocol-2025-11-25")]
2564 #[tokio::test]
2565 async fn test_resources_templates_list_answers_empty_without_templates() {
2566 use lambda_http::Body as LambdaBody;
2567
2568 let server = LambdaMcpServerBuilder::new()
2569 .name("parity-test")
2570 .version("1.0.0")
2571 .tool(TestTool) .storage(Arc::new(InMemorySessionStorage::new()))
2573 .strict_lifecycle(false) .sse(false)
2575 .build()
2576 .await
2577 .unwrap();
2578
2579 let handler = server.handler().await.unwrap();
2580
2581 let init_req = http::Request::builder()
2583 .method("POST")
2584 .uri("/mcp")
2585 .header("Content-Type", "application/json")
2586 .header("MCP-Protocol-Version", "2025-11-25")
2587 .body(LambdaBody::Text(
2588 serde_json::json!({
2589 "jsonrpc": "2.0", "method": "initialize", "id": 1,
2590 "params": {
2591 "protocolVersion": "2025-11-25",
2592 "capabilities": {},
2593 "clientInfo": { "name": "test", "version": "1.0.0" }
2594 }
2595 })
2596 .to_string(),
2597 ))
2598 .unwrap();
2599 let init_resp = handler.handle(init_req).await.unwrap();
2600 let session_id = init_resp
2601 .headers()
2602 .get("Mcp-Session-Id")
2603 .unwrap()
2604 .to_str()
2605 .unwrap()
2606 .to_string();
2607
2608 let tmpl_req = http::Request::builder()
2610 .method("POST")
2611 .uri("/mcp")
2612 .header("Content-Type", "application/json")
2613 .header("MCP-Protocol-Version", "2025-11-25")
2614 .header("Mcp-Session-Id", &session_id)
2615 .body(LambdaBody::Text(
2616 serde_json::json!({
2617 "jsonrpc": "2.0", "method": "resources/templates/list", "id": 2
2618 })
2619 .to_string(),
2620 ))
2621 .unwrap();
2622 let tmpl_resp = handler.handle(tmpl_req).await.unwrap();
2623 let body = String::from_utf8_lossy(tmpl_resp.body().as_ref()).to_string();
2624 let json: serde_json::Value = serde_json::from_str(&body)
2625 .unwrap_or_else(|e| panic!("Response must be valid JSON: {e}\nBody: {body}"));
2626
2627 assert!(
2632 json.get("error").is_none(),
2633 "resources/templates/list must not error without templates: {json}"
2634 );
2635 assert_eq!(
2636 json["result"]["resourceTemplates"],
2637 serde_json::json!([]),
2638 "a server with no templates reports an empty list: {json}"
2639 );
2640 }
2641}