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