Skip to main content

turul_mcp_aws_lambda/
server.rs

1//! Lambda MCP Server Implementation
2//!
3//! This module provides the main Lambda MCP server implementation that mirrors
4//! the architecture of turul-mcp-server but adapted for AWS Lambda deployment.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use tracing::{debug, info};
10
11use turul_http_mcp_server::{ServerConfig, StreamConfig, StreamManager};
12use turul_mcp_protocol::{Implementation, ServerCapabilities};
13use turul_mcp_server::{
14    McpCompletion, McpNotification, McpPrompt, McpResource, McpTool, handlers::McpHandler,
15    session::SessionManager,
16};
17#[cfg(feature = "protocol-2025-11-25")]
18use turul_mcp_server::{McpElicitation, McpLogger, McpSampling};
19use turul_mcp_session_storage::BoxedSessionStorage;
20
21use crate::error::Result;
22use crate::handler::LambdaMcpHandler;
23
24#[cfg(feature = "cors")]
25use crate::cors::CorsConfig;
26
27/// Main Lambda MCP server
28///
29/// This server stores all configuration and can create Lambda handlers when needed.
30/// It mirrors the architecture of McpServer but is designed for Lambda deployment.
31#[allow(dead_code)]
32pub struct LambdaMcpServer {
33    /// Server implementation information
34    pub implementation: Implementation,
35    /// Server capabilities
36    pub capabilities: ServerCapabilities,
37    /// Registered tools
38    tools: HashMap<String, Arc<dyn McpTool>>,
39    /// Registered resources
40    resources: HashMap<String, Arc<dyn McpResource>>,
41    /// Registered prompts
42    prompts: HashMap<String, Arc<dyn McpPrompt>>,
43    /// Registered elicitations
44    #[cfg(feature = "protocol-2025-11-25")]
45    elicitations: HashMap<String, Arc<dyn McpElicitation>>,
46    /// Registered sampling providers
47    #[cfg(feature = "protocol-2025-11-25")]
48    sampling: HashMap<String, Arc<dyn McpSampling>>,
49    /// Registered completion providers
50    completions: Vec<Arc<dyn McpCompletion>>,
51    /// Registered loggers
52    #[cfg(feature = "protocol-2025-11-25")]
53    loggers: HashMap<String, Arc<dyn McpLogger>>,
54    /// Registered root providers
55    /// Registered notification providers
56    notifications: HashMap<String, Arc<dyn McpNotification>>,
57    /// Registered handlers
58    handlers: HashMap<String, Arc<dyn McpHandler>>,
59    /// Configured roots
60    // `Root` is deprecated-but-present in 2026-07-28 (SEP-2577); roots remain a valid feature.
61    #[allow(deprecated)]
62    roots: Vec<turul_mcp_protocol::roots::Root>,
63    /// Optional client instructions
64    instructions: Option<String>,
65    /// Session manager for state persistence
66    session_manager: Arc<SessionManager>,
67    /// Session storage backend (shared between SessionManager and handler)
68    session_storage: Arc<BoxedSessionStorage>,
69    /// Strict MCP lifecycle enforcement
70    strict_lifecycle: bool,
71    /// Server configuration
72    server_config: ServerConfig,
73    /// Enable SSE streaming
74    enable_sse: bool,
75    /// Stream configuration
76    stream_config: StreamConfig,
77    /// CORS configuration (if enabled)
78    #[cfg(feature = "cors")]
79    cors_config: Option<CorsConfig>,
80    /// Middleware stack for request/response interception
81    middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
82    /// Custom route registry (e.g., .well-known endpoints)
83    route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
84    /// Optional task runtime for MCP task support
85    #[cfg(feature = "protocol-2025-11-25")]
86    task_runtime: Option<Arc<turul_mcp_server::TaskRuntime>>,
87    /// Stable fingerprint of the registered tool set for session versioning
88    tool_fingerprint: String,
89    /// Dynamic tool registry (only in Dynamic mode)
90    #[cfg(feature = "dynamic-tools")]
91    tool_registry: Option<Arc<turul_mcp_server::ToolRegistry>>,
92    /// Whether cross-instance coordination is enabled (explicit storage was provided)
93    #[cfg(feature = "dynamic-tools")]
94    coordination_enabled: bool,
95}
96
97impl LambdaMcpServer {
98    /// Create a new Lambda MCP server (use builder instead)
99    #[allow(clippy::too_many_arguments)]
100    // `Root` is deprecated-but-present in 2026-07-28 (SEP-2577); roots remain a valid feature.
101    #[allow(deprecated)]
102    pub(crate) fn new(
103        implementation: Implementation,
104        capabilities: ServerCapabilities,
105        tools: HashMap<String, Arc<dyn McpTool>>,
106        resources: HashMap<String, Arc<dyn McpResource>>,
107        prompts: HashMap<String, Arc<dyn McpPrompt>>,
108        #[cfg(feature = "protocol-2025-11-25")] elicitations: HashMap<
109            String,
110            Arc<dyn McpElicitation>,
111        >,
112        #[cfg(feature = "protocol-2025-11-25")] sampling: HashMap<String, Arc<dyn McpSampling>>,
113        completions: Vec<Arc<dyn McpCompletion>>,
114        #[cfg(feature = "protocol-2025-11-25")] loggers: HashMap<String, Arc<dyn McpLogger>>,
115        notifications: HashMap<String, Arc<dyn McpNotification>>,
116        handlers: HashMap<String, Arc<dyn McpHandler>>,
117        roots: Vec<turul_mcp_protocol::roots::Root>,
118        instructions: Option<String>,
119        session_storage: Arc<BoxedSessionStorage>,
120        strict_lifecycle: bool,
121        server_config: ServerConfig,
122        enable_sse: bool,
123        stream_config: StreamConfig,
124        #[cfg(feature = "cors")] cors_config: Option<CorsConfig>,
125        middleware_stack: turul_http_mcp_server::middleware::MiddlewareStack,
126        route_registry: Arc<turul_http_mcp_server::RouteRegistry>,
127        #[cfg(feature = "protocol-2025-11-25")] task_runtime: Option<
128            Arc<turul_mcp_server::TaskRuntime>,
129        >,
130        tool_fingerprint: String,
131        #[cfg(feature = "dynamic-tools")] dynamic_tools: bool,
132        #[cfg(feature = "dynamic-tools")] server_state_storage: Option<
133            Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>,
134        >,
135    ) -> Self {
136        // Create session manager with server capabilities
137        let session_manager = Arc::new(SessionManager::with_storage_and_timeouts(
138            Arc::clone(&session_storage),
139            capabilities.clone(),
140            std::time::Duration::from_secs(30 * 60), // 30 minutes default
141            std::time::Duration::from_secs(60),      // 1 minute cleanup interval
142        ));
143
144        // Coordination enabled only for shared backends that can be accessed by multiple instances.
145        // InMemory and SQLite are local-only. Only PostgreSQL and DynamoDB are shared.
146        #[cfg(feature = "dynamic-tools")]
147        let coordination_enabled = server_state_storage
148            .as_ref()
149            .map(|s| matches!(s.backend_name(), "PostgreSQL" | "DynamoDB"))
150            .unwrap_or(false);
151
152        // Create ToolRegistry when dynamic mode is enabled
153        #[cfg(feature = "dynamic-tools")]
154        let tool_registry = if dynamic_tools {
155            let storage = server_state_storage.unwrap_or_else(|| {
156                Arc::new(turul_mcp_server_state_storage::InMemoryServerStateStorage::new())
157            });
158            Some(Arc::new(turul_mcp_server::ToolRegistry::new(
159                tools.clone(),
160                session_manager.clone(),
161                storage,
162            )))
163        } else {
164            None
165        };
166
167        Self {
168            implementation,
169            capabilities,
170            tools,
171            resources,
172            prompts,
173            #[cfg(feature = "protocol-2025-11-25")]
174            elicitations,
175            #[cfg(feature = "protocol-2025-11-25")]
176            sampling,
177            completions,
178            #[cfg(feature = "protocol-2025-11-25")]
179            loggers,
180            notifications,
181            handlers,
182            roots,
183            instructions,
184            session_manager,
185            session_storage,
186            strict_lifecycle,
187            server_config,
188            enable_sse,
189            stream_config,
190            #[cfg(feature = "cors")]
191            cors_config,
192            middleware_stack,
193            route_registry,
194            #[cfg(feature = "protocol-2025-11-25")]
195            task_runtime,
196            tool_fingerprint,
197            #[cfg(feature = "dynamic-tools")]
198            tool_registry,
199            #[cfg(feature = "dynamic-tools")]
200            coordination_enabled,
201        }
202    }
203
204    /// Get a reference to the server capabilities.
205    pub fn capabilities(&self) -> &ServerCapabilities {
206        &self.capabilities
207    }
208
209    /// Whether a task runtime is configured (always false when tasks are not part of the spec).
210    fn has_task_runtime(&self) -> bool {
211        #[cfg(feature = "protocol-2025-11-25")]
212        {
213            self.task_runtime.is_some()
214        }
215        #[cfg(not(feature = "protocol-2025-11-25"))]
216        {
217            false
218        }
219    }
220
221    /// Create a Lambda handler ready for use with Lambda runtime
222    ///
223    /// This is equivalent to McpServer::run_http() but creates a handler instead of running a server.
224    pub async fn handler(&self) -> Result<LambdaMcpHandler> {
225        info!(
226            "Creating Lambda MCP handler: {} v{}",
227            self.implementation.name, self.implementation.version
228        );
229        info!("Session management: enabled with automatic cleanup");
230
231        if self.enable_sse {
232            info!("SSE notifications: enabled for Lambda responses");
233
234            // ⚠️ GUARDRAIL: SSE enabled without streaming feature
235            #[cfg(not(feature = "streaming"))]
236            {
237                use tracing::warn;
238                warn!("⚠️  SSE is enabled but 'streaming' feature is not available!");
239                warn!(
240                    "   For real SSE streaming, use handle_streaming() with run_with_streaming_response"
241                );
242                warn!(
243                    "   Current handle() method will return SSE snapshots, not real-time streams"
244                );
245                warn!("   To enable streaming: add 'streaming' feature to turul-mcp-aws-lambda");
246            }
247        }
248
249        // Start session cleanup task (same as MCP server)
250        let _cleanup_task = self.session_manager.clone().start_cleanup_task();
251
252        // Sync tool registry with shared storage on startup (coordination mode only)
253        #[cfg(feature = "dynamic-tools")]
254        if self.coordination_enabled
255            && let Some(ref registry) = self.tool_registry
256        {
257            use tracing::warn;
258            match registry.sync_from_storage().await {
259                Ok(_) => {
260                    info!("Dynamic: synced tool registry with shared storage");
261                }
262                Err(e) => {
263                    warn!(error = %e, "Dynamic: failed to sync with shared storage on cold start");
264                }
265            }
266        }
267
268        // Cold-start recovery: handler() is called once per Lambda cold start from main().
269        // The returned LambdaMcpHandler is Clone'd for each request — recovery runs exactly once.
270        #[cfg(feature = "protocol-2025-11-25")]
271        if let Some(ref runtime) = self.task_runtime {
272            match runtime.recover_stuck_tasks().await {
273                Ok(recovered) if !recovered.is_empty() => {
274                    info!(
275                        count = recovered.len(),
276                        "Recovered stuck tasks from previous invocations"
277                    );
278                }
279                Err(e) => {
280                    use tracing::warn;
281                    warn!(error = %e, "Failed to recover stuck tasks on startup");
282                }
283                _ => {}
284            }
285        }
286
287        // Create stream manager for SSE
288        let stream_manager = Arc::new(StreamManager::with_config(
289            self.session_storage.clone(),
290            self.stream_config.clone(),
291        ));
292
293        // Install awaited event dispatcher for guaranteed persistence.
294        // Custom events are persisted via StreamManager on the request path,
295        // not via the detached bridge task.
296        {
297            use turul_mcp_server::SessionEventDispatcher;
298
299            struct LambdaEventDispatcher {
300                stream_manager: Arc<StreamManager>,
301            }
302
303            #[async_trait::async_trait]
304            impl SessionEventDispatcher for LambdaEventDispatcher {
305                async fn dispatch_to_session(
306                    &self,
307                    session_id: &str,
308                    event_type: String,
309                    data: serde_json::Value,
310                ) -> std::result::Result<(), String> {
311                    self.stream_manager
312                        .broadcast_to_session(session_id, event_type, data)
313                        .await
314                        .map(|_| ())
315                        .map_err(|e| e.to_string())
316                }
317            }
318
319            let dispatcher = Arc::new(LambdaEventDispatcher {
320                stream_manager: Arc::clone(&stream_manager),
321            });
322            self.session_manager.set_event_dispatcher(dispatcher).await;
323            debug!("Lambda event dispatcher installed (guaranteed persistence for Custom events)");
324        }
325
326        // SSE event bridge — observer-only for Custom events.
327        // The dispatcher above handles persistence on the request path.
328        {
329            let mut global_events = self.session_manager.subscribe_all_session_events();
330
331            tokio::spawn(async move {
332                debug!("Lambda SSE Event Bridge: started (observer-only for Custom events)");
333
334                while let Ok((session_id, event)) = global_events.recv().await {
335                    match event {
336                        turul_mcp_server::session::SessionEvent::Custom {
337                            ref event_type, ..
338                        } => {
339                            debug!(
340                                "Lambda SSE Bridge: observed custom event '{}' for session {} (dispatcher handles persistence)",
341                                event_type, session_id
342                            );
343                        }
344                        _ => {
345                            debug!(
346                                "Lambda SSE Bridge: non-custom event for session {}",
347                                session_id
348                            );
349                        }
350                    }
351                }
352
353                debug!("Lambda SSE Event Bridge: stopped");
354            });
355        }
356
357        // Create JSON-RPC dispatcher
358        use turul_rpc::JsonRpcDispatcher;
359        let mut dispatcher = JsonRpcDispatcher::new();
360
361        // Create session-aware initialize handler (2025-11-25 stateful handshake;
362        // the 2026-07-28 stateless core has no `initialize` method).
363        #[cfg(feature = "protocol-2025-11-25")]
364        {
365            use turul_mcp_server::SessionAwareInitializeHandler;
366            #[cfg_attr(not(feature = "dynamic-tools"), allow(unused_mut))]
367            let mut init_handler = SessionAwareInitializeHandler::new(
368                self.implementation.clone(),
369                self.capabilities.clone(),
370                self.instructions.clone(),
371                self.session_manager.clone(),
372                self.strict_lifecycle,
373                self.tool_fingerprint.clone(),
374            );
375            #[cfg(feature = "dynamic-tools")]
376            if let Some(ref registry) = self.tool_registry {
377                init_handler = init_handler.with_tool_registry(Arc::clone(registry));
378            }
379            dispatcher.register_method("initialize".to_string(), init_handler);
380        }
381
382        // Create session-aware tools/list handler (reuse MCP server handler)
383        use turul_mcp_server::ListToolsHandler;
384        #[allow(unused_mut)]
385        let mut list_handler = ListToolsHandler::new_with_session_manager(
386            self.tools.clone(),
387            self.session_manager.clone(),
388            self.strict_lifecycle,
389            self.has_task_runtime(),
390        );
391        #[cfg(feature = "dynamic-tools")]
392        if let Some(ref registry) = self.tool_registry {
393            list_handler = list_handler.with_tool_registry(Arc::clone(registry));
394        }
395        dispatcher.register_method("tools/list".to_string(), list_handler);
396
397        // Create session-aware tool handler for tools/call (reuse MCP server handler)
398        use turul_mcp_server::SessionAwareToolHandler;
399        #[cfg_attr(
400            not(any(feature = "protocol-2025-11-25", feature = "dynamic-tools")),
401            allow(unused_mut)
402        )]
403        let mut tool_handler = SessionAwareToolHandler::new(
404            self.tools.clone(),
405            self.session_manager.clone(),
406            self.strict_lifecycle,
407        );
408        #[cfg(feature = "protocol-2025-11-25")]
409        if let Some(ref runtime) = self.task_runtime {
410            tool_handler = tool_handler.with_task_runtime(Arc::clone(runtime));
411        }
412        #[cfg(feature = "dynamic-tools")]
413        if let Some(ref registry) = self.tool_registry {
414            tool_handler = tool_handler.with_tool_registry(Arc::clone(registry));
415        }
416        dispatcher.register_method("tools/call".to_string(), tool_handler);
417
418        #[cfg(feature = "protocol-2026-07-28")]
419        {
420            use turul_mcp_protocol::SERVER_DISCOVER_METHOD;
421            use turul_mcp_server::DiscoverHandler;
422            dispatcher.register_method(
423                SERVER_DISCOVER_METHOD.to_string(),
424                DiscoverHandler::new(self.capabilities.clone(), self.instructions.clone()),
425            );
426        }
427
428        // Register all MCP handlers with session awareness (reuse MCP server bridge)
429        use turul_mcp_server::SessionAwareMcpHandlerBridge;
430        for (method, handler) in &self.handlers {
431            let bridge_handler = SessionAwareMcpHandlerBridge::new(
432                handler.clone(),
433                self.session_manager.clone(),
434                self.strict_lifecycle,
435            );
436            dispatcher.register_method(method.clone(), bridge_handler);
437        }
438
439        // notifications/initialized exists only in the 2025-11-25 lifecycle; the
440        // 2026-07-28 stateless core has no initialize/initialized handshake.
441        #[cfg(feature = "protocol-2025-11-25")]
442        {
443            use turul_mcp_server::handlers::InitializedNotificationHandler;
444            let initialized_handler =
445                InitializedNotificationHandler::new(self.session_manager.clone());
446            let initialized_bridge = SessionAwareMcpHandlerBridge::new(
447                Arc::new(initialized_handler),
448                self.session_manager.clone(),
449                self.strict_lifecycle,
450            );
451            dispatcher.register_method("notifications/initialized".to_string(), initialized_bridge);
452        }
453
454        // Create the Lambda handler with all components and middleware
455        let middleware_stack = Arc::new(self.middleware_stack.clone());
456
457        let handler = LambdaMcpHandler::with_middleware_and_fingerprint(
458            self.server_config.clone(),
459            Arc::new(dispatcher),
460            self.session_storage.clone(),
461            stream_manager,
462            self.stream_config.clone(),
463            self.capabilities.clone(),
464            middleware_stack,
465            self.enable_sse,
466            Arc::clone(&self.route_registry),
467            Some(self.tool_fingerprint.clone()),
468        )
469        .with_server_info(self.implementation.clone());
470
471        // Wire tool change notifier for restart/redeploy fingerprint mismatch.
472        // Uses SessionManager → dispatch_custom_event() → dispatcher → guaranteed persistence.
473        // dispatch_custom_event is storage-backed and does NOT depend on the in-memory session cache.
474        let handler = {
475            struct LambdaToolNotifier {
476                session_manager: Arc<turul_mcp_server::SessionManager>,
477            }
478            #[async_trait::async_trait]
479            impl turul_http_mcp_server::ToolChangeNotifier for LambdaToolNotifier {
480                async fn notify_tools_changed(
481                    &self,
482                    session_id: &str,
483                ) -> std::result::Result<(), String> {
484                    let notification = turul_rpc::JsonRpcNotification::new_no_params(
485                        "notifications/tools/list_changed".to_string(),
486                    );
487                    let data = serde_json::to_value(&notification).map_err(|e| e.to_string())?;
488                    self.session_manager
489                        .dispatch_custom_event(
490                            session_id,
491                            "notifications/tools/list_changed".to_string(),
492                            data,
493                        )
494                        .await
495                }
496            }
497            handler.with_tool_notifier(Arc::new(LambdaToolNotifier {
498                session_manager: Arc::clone(&self.session_manager),
499            }))
500        };
501
502        #[cfg(feature = "dynamic-tools")]
503        let handler = if let Some(ref registry) = self.tool_registry {
504            handler.with_tool_registry(Arc::clone(registry))
505        } else {
506            handler
507        };
508
509        // Propagate builder-configured CORS to the constructed handler.
510        // Must be applied after notifier/registry wiring so the final handler
511        // returned to the caller carries the CORS config — every
512        // `if let Some(cors_config)` branch inside `LambdaMcpHandler` reads
513        // from `self.cors_config`, which `with_middleware_and_fingerprint`
514        // initializes to `None`.
515        #[cfg(feature = "cors")]
516        let handler = match self.cors_config.clone() {
517            Some(cors) => handler.with_cors(cors),
518            None => handler,
519        };
520
521        Ok(handler)
522    }
523
524    /// Get information about the session storage backend
525    pub fn session_storage_info(&self) -> &str {
526        "Session storage configured"
527    }
528}