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")]
120        server_state_storage: Option<Arc<dyn turul_mcp_server_state_storage::ServerStateStorage>>,
121    ) -> Self {
122        // Create session manager with server capabilities
123        let session_manager = Arc::new(SessionManager::with_storage_and_timeouts(
124            Arc::clone(&session_storage),
125            capabilities.clone(),
126            std::time::Duration::from_secs(30 * 60), // 30 minutes default
127            std::time::Duration::from_secs(60),      // 1 minute cleanup interval
128        ));
129
130        // Coordination enabled only for shared backends that can be accessed by multiple instances.
131        // InMemory and SQLite are local-only. Only PostgreSQL and DynamoDB are shared.
132        #[cfg(feature = "dynamic-tools")]
133        let coordination_enabled = server_state_storage
134            .as_ref()
135            .map(|s| matches!(s.backend_name(), "PostgreSQL" | "DynamoDB"))
136            .unwrap_or(false);
137
138        // Create ToolRegistry when dynamic mode is enabled
139        #[cfg(feature = "dynamic-tools")]
140        let tool_registry = if dynamic_tools {
141            let storage = server_state_storage.unwrap_or_else(|| {
142                Arc::new(turul_mcp_server_state_storage::InMemoryServerStateStorage::new())
143            });
144            Some(Arc::new(turul_mcp_server::ToolRegistry::new(
145                tools.clone(),
146                session_manager.clone(),
147                storage,
148            )))
149        } else {
150            None
151        };
152
153        Self {
154            implementation,
155            capabilities,
156            tools,
157            resources,
158            prompts,
159            elicitations,
160            sampling,
161            completions,
162            loggers,
163            root_providers,
164            notifications,
165            handlers,
166            roots,
167            instructions,
168            session_manager,
169            session_storage,
170            strict_lifecycle,
171            server_config,
172            enable_sse,
173            stream_config,
174            #[cfg(feature = "cors")]
175            cors_config,
176            middleware_stack,
177            route_registry,
178            task_runtime,
179            tool_fingerprint,
180            #[cfg(feature = "dynamic-tools")]
181            tool_registry,
182            #[cfg(feature = "dynamic-tools")]
183            coordination_enabled,
184        }
185    }
186
187    /// Get a reference to the server capabilities.
188    pub fn capabilities(&self) -> &ServerCapabilities {
189        &self.capabilities
190    }
191
192    /// Create a Lambda handler ready for use with Lambda runtime
193    ///
194    /// This is equivalent to McpServer::run_http() but creates a handler instead of running a server.
195    pub async fn handler(&self) -> Result<LambdaMcpHandler> {
196        info!(
197            "Creating Lambda MCP handler: {} v{}",
198            self.implementation.name, self.implementation.version
199        );
200        info!("Session management: enabled with automatic cleanup");
201
202        if self.enable_sse {
203            info!("SSE notifications: enabled for Lambda responses");
204
205            // ⚠️ GUARDRAIL: SSE enabled without streaming feature
206            #[cfg(not(feature = "streaming"))]
207            {
208                use tracing::warn;
209                warn!("⚠️  SSE is enabled but 'streaming' feature is not available!");
210                warn!(
211                    "   For real SSE streaming, use handle_streaming() with run_with_streaming_response"
212                );
213                warn!(
214                    "   Current handle() method will return SSE snapshots, not real-time streams"
215                );
216                warn!("   To enable streaming: add 'streaming' feature to turul-mcp-aws-lambda");
217            }
218        }
219
220        // Start session cleanup task (same as MCP server)
221        let _cleanup_task = self.session_manager.clone().start_cleanup_task();
222
223        // Sync tool registry with shared storage on startup (coordination mode only)
224        #[cfg(feature = "dynamic-tools")]
225        if self.coordination_enabled {
226            if let Some(ref registry) = self.tool_registry {
227                use tracing::warn;
228                match registry.sync_from_storage().await {
229                    Ok(_) => {
230                        info!("Dynamic: synced tool registry with shared storage");
231                    }
232                    Err(e) => {
233                        warn!(error = %e, "Dynamic: failed to sync with shared storage on cold start");
234                    }
235                }
236            }
237        }
238
239        // Cold-start recovery: handler() is called once per Lambda cold start from main().
240        // The returned LambdaMcpHandler is Clone'd for each request — recovery runs exactly once.
241        if let Some(ref runtime) = self.task_runtime {
242            match runtime.recover_stuck_tasks().await {
243                Ok(recovered) if !recovered.is_empty() => {
244                    info!(
245                        count = recovered.len(),
246                        "Recovered stuck tasks from previous invocations"
247                    );
248                }
249                Err(e) => {
250                    use tracing::warn;
251                    warn!(error = %e, "Failed to recover stuck tasks on startup");
252                }
253                _ => {}
254            }
255        }
256
257        // Create stream manager for SSE
258        let stream_manager = Arc::new(StreamManager::with_config(
259            self.session_storage.clone(),
260            self.stream_config.clone(),
261        ));
262
263        // Install awaited event dispatcher for guaranteed persistence.
264        // Custom events are persisted via StreamManager on the request path,
265        // not via the detached bridge task.
266        {
267            use turul_mcp_server::SessionEventDispatcher;
268
269            struct LambdaEventDispatcher {
270                stream_manager: Arc<StreamManager>,
271            }
272
273            #[async_trait::async_trait]
274            impl SessionEventDispatcher for LambdaEventDispatcher {
275                async fn dispatch_to_session(
276                    &self,
277                    session_id: &str,
278                    event_type: String,
279                    data: serde_json::Value,
280                ) -> std::result::Result<(), String> {
281                    self.stream_manager
282                        .broadcast_to_session(session_id, event_type, data)
283                        .await
284                        .map(|_| ())
285                        .map_err(|e| e.to_string())
286                }
287            }
288
289            let dispatcher = Arc::new(LambdaEventDispatcher {
290                stream_manager: Arc::clone(&stream_manager),
291            });
292            self.session_manager.set_event_dispatcher(dispatcher).await;
293            debug!("Lambda event dispatcher installed (guaranteed persistence for Custom events)");
294        }
295
296        // SSE event bridge — observer-only for Custom events.
297        // The dispatcher above handles persistence on the request path.
298        {
299            let mut global_events = self.session_manager.subscribe_all_session_events();
300
301            tokio::spawn(async move {
302                debug!("Lambda SSE Event Bridge: started (observer-only for Custom events)");
303
304                while let Ok((session_id, event)) = global_events.recv().await {
305                    match event {
306                        turul_mcp_server::session::SessionEvent::Custom {
307                            ref event_type, ..
308                        } => {
309                            debug!(
310                                "Lambda SSE Bridge: observed custom event '{}' for session {} (dispatcher handles persistence)",
311                                event_type, session_id
312                            );
313                        }
314                        _ => {
315                            debug!("Lambda SSE Bridge: non-custom event for session {}", session_id);
316                        }
317                    }
318                }
319
320                debug!("Lambda SSE Event Bridge: stopped");
321            });
322        }
323
324        // Create JSON-RPC dispatcher
325        use turul_mcp_json_rpc_server::JsonRpcDispatcher;
326        let mut dispatcher = JsonRpcDispatcher::new();
327
328        // Create session-aware initialize handler (reuse MCP server handler)
329        use turul_mcp_server::SessionAwareInitializeHandler;
330        #[allow(unused_mut)]
331        let mut init_handler = SessionAwareInitializeHandler::new(
332            self.implementation.clone(),
333            self.capabilities.clone(),
334            self.instructions.clone(),
335            self.session_manager.clone(),
336            self.strict_lifecycle,
337            self.tool_fingerprint.clone(),
338        );
339        #[cfg(feature = "dynamic-tools")]
340        if let Some(ref registry) = self.tool_registry {
341            init_handler = init_handler.with_tool_registry(Arc::clone(registry));
342        }
343        dispatcher.register_method("initialize".to_string(), init_handler);
344
345        // Create session-aware tools/list handler (reuse MCP server handler)
346        use turul_mcp_server::ListToolsHandler;
347        #[allow(unused_mut)]
348        let mut list_handler = ListToolsHandler::new_with_session_manager(
349            self.tools.clone(),
350            self.session_manager.clone(),
351            self.strict_lifecycle,
352            self.task_runtime.is_some(),
353        );
354        #[cfg(feature = "dynamic-tools")]
355        if let Some(ref registry) = self.tool_registry {
356            list_handler = list_handler.with_tool_registry(Arc::clone(registry));
357        }
358        dispatcher.register_method("tools/list".to_string(), list_handler);
359
360        // Create session-aware tool handler for tools/call (reuse MCP server handler)
361        use turul_mcp_server::SessionAwareToolHandler;
362        let mut tool_handler = SessionAwareToolHandler::new(
363            self.tools.clone(),
364            self.session_manager.clone(),
365            self.strict_lifecycle,
366        );
367        if let Some(ref runtime) = self.task_runtime {
368            tool_handler = tool_handler.with_task_runtime(Arc::clone(runtime));
369        }
370        #[cfg(feature = "dynamic-tools")]
371        if let Some(ref registry) = self.tool_registry {
372            tool_handler = tool_handler.with_tool_registry(Arc::clone(registry));
373        }
374        dispatcher.register_method("tools/call".to_string(), tool_handler);
375
376        // Register all MCP handlers with session awareness (reuse MCP server bridge)
377        use turul_mcp_server::SessionAwareMcpHandlerBridge;
378        for (method, handler) in &self.handlers {
379            let bridge_handler = SessionAwareMcpHandlerBridge::new(
380                handler.clone(),
381                self.session_manager.clone(),
382                self.strict_lifecycle,
383            );
384            dispatcher.register_method(method.clone(), bridge_handler);
385        }
386
387        // Register notifications/initialized handler — required for strict lifecycle.
388        // Without this, clients can never complete the MCP handshake.
389        use turul_mcp_server::handlers::InitializedNotificationHandler;
390        let initialized_handler = InitializedNotificationHandler::new(self.session_manager.clone());
391        let initialized_bridge = SessionAwareMcpHandlerBridge::new(
392            Arc::new(initialized_handler),
393            self.session_manager.clone(),
394            self.strict_lifecycle,
395        );
396        dispatcher.register_method("notifications/initialized".to_string(), initialized_bridge);
397
398        // Create the Lambda handler with all components and middleware
399        let middleware_stack = Arc::new(self.middleware_stack.clone());
400
401        let handler = LambdaMcpHandler::with_middleware_and_fingerprint(
402            self.server_config.clone(),
403            Arc::new(dispatcher),
404            self.session_storage.clone(),
405            stream_manager,
406            self.stream_config.clone(),
407            self.capabilities.clone(),
408            middleware_stack,
409            self.enable_sse,
410            Arc::clone(&self.route_registry),
411            Some(self.tool_fingerprint.clone()),
412        );
413
414        // Wire tool change notifier for restart/redeploy fingerprint mismatch.
415        // Uses SessionManager → send_event_to_session() → dispatcher → guaranteed persistence.
416        let handler = {
417            struct LambdaToolNotifier {
418                session_manager: Arc<turul_mcp_server::SessionManager>,
419            }
420            #[async_trait::async_trait]
421            impl turul_http_mcp_server::ToolChangeNotifier for LambdaToolNotifier {
422                async fn notify_tools_changed(&self, session_id: &str) -> std::result::Result<(), String> {
423                    let notification = turul_mcp_protocol::JsonRpcNotification::new(
424                        "notifications/tools/list_changed".to_string(),
425                    );
426                    let data = serde_json::to_value(&notification).map_err(|e| e.to_string())?;
427                    self.session_manager.send_event_to_session(
428                        session_id,
429                        turul_mcp_server::SessionEvent::Custom {
430                            event_type: "notifications/tools/list_changed".to_string(),
431                            data,
432                        },
433                    ).await
434                }
435            }
436            handler.with_tool_notifier(Arc::new(LambdaToolNotifier {
437                session_manager: Arc::clone(&self.session_manager),
438            }))
439        };
440
441        #[cfg(feature = "dynamic-tools")]
442        let handler = if let Some(ref registry) = self.tool_registry {
443            handler.with_tool_registry(Arc::clone(registry))
444        } else {
445            handler
446        };
447
448        Ok(handler)
449    }
450
451    /// Get information about the session storage backend
452    pub fn session_storage_info(&self) -> &str {
453        "Session storage configured"
454    }
455}