turul-mcp-aws-lambda
AWS Lambda integration for the turul-mcp-framework, enabling serverless deployment of MCP servers with full protocol compliance.
Overview
turul-mcp-aws-lambda provides seamless integration between the turul-mcp-framework and AWS Lambda runtime, enabling serverless MCP servers with proper session management, CORS handling, and SSE streaming support.
Features
- ✅ Zero-Cold-Start Architecture - Optimized Lambda integration
- ✅ MCP 2025-11-25 Compliance - Full protocol support with SSE (snapshots or streaming)
- ✅ DynamoDB Session Storage - Persistent session management across invocations
- ✅ CORS Support - Automatic CORS header injection for browser clients
- ✅ Type Conversion Layer - Clean
lambda_http↔hyperconversion - ⚠️ SSE Support - Snapshots via
handle()or real streaming viahandle_streaming() - ✅ Builder Pattern - Familiar API matching
McpServer::builder() - ✅ Truthful Capabilities - Framework advertises accurate server capabilities
- ✅ MCP Tasks Support - Task-augmented
tools/callwith durable storage persistence
Quick Start
Add this to your Cargo.toml:
[]
= "0.3"
= "0.3"
= "0.17"
= { = "1.0", = ["macros"] }
Basic Lambda MCP Server (Snapshot-based SSE)
use ;
use LambdaMcpServerBuilder;
use McpTool;
use ;
async
Real-time Streaming Lambda MCP Server
For real-time SSE streaming, enable the streaming feature and use run_streaming():
[]
= { = "0.3", = ["streaming"] }
use LambdaMcpServerBuilder;
// ... same tool definition ...
async
Custom Dispatch with run_streaming_with()
When you need pre-dispatch logic (e.g., .well-known routing) that runs before
the MCP handler, use run_streaming_with():
run_streaming_with.await
Both entry points classify raw Lambda runtime payloads three ways:
- API Gateway events — dispatched to your handler normally
- Streaming completion invocations — acknowledged silently (debug log)
- Unrecognized payloads — acknowledged with a warn log
This fixes the ERROR logs and CloudWatch Lambda Error metrics caused by
completion invocations when using lambda_http::run_with_streaming_response()
directly. It does not claim to resolve all Lambda streaming timeout
behavior — that remains a separate concern.
Architecture
Framework Integration
The crate bridges AWS Lambda's HTTP execution model with the turul-mcp-framework:
┌─────────────────────────┐
│ AWS Lambda Runtime │
├─────────────────────────┤
│ turul-mcp-aws-lambda │ ← This crate
│ ├─ Type Conversion │ ← lambda_http ↔ hyper
│ ├─ CORS Integration │ ← Automatic header injection
│ ├─ SSE Adaptation │ ← Lambda streaming responses
│ ├─ Session Management │ ← DynamoDB persistence
│ └─ Task Runtime │ ← MCP Tasks with durable storage
├─────────────────────────┤
│ turul-mcp-server │ ← Core framework
└─────────────────────────┘
Three-Layer Discovery
Through lambda development, we discovered the framework's 3-layer architecture:
- Layer 1:
McpServer- High-level builder and handler management - Layer 2:
HttpMcpServer- TCP server (incompatible with Lambda) - Layer 3:
SessionMcpHandler- Request handler (what Lambda needs)
This crate skips Layer 2 and provides clean integration to Layer 3.
DynamoDB Session Storage
Automatic Table Creation
use LambdaMcpServerBuilder;
use DynamoDbSessionStorage;
use Arc;
let storage = new;
let server = new
.name
.storage // Persistent session management
.tool
.build
.await?;
Session Persistence
Sessions automatically persist across Lambda invocations:
;
MCP Tasks Support
Enable task-augmented tools/call for long-running operations. When a client sends tools/call with a task parameter, the server creates a task record, dispatches execution asynchronously, and returns a CreateTaskResult immediately.
Task Storage Configuration
use LambdaMcpServerBuilder;
use InMemoryTaskStorage;
use Arc;
// For production: use DynamoDB task storage
let task_storage = new;
let server = new
.name
.tool
.with_task_storage // Enables tasks/get, tasks/list, etc.
.task_recovery_timeout_ms // 5 min stuck-task recovery (default)
.build
.await?;
How It Works
- Client sends
tools/callwith{ "task": {} }parameter - Server creates a task record in durable storage (status:
working) - Server returns
CreateTaskResultimmediately (non-blocking) - Tool execution runs asynchronously via the task executor
- Client polls
tasks/getor waits ontasks/resultfor completion
Lambda-Specific Considerations
- Durable storage required: Use DynamoDB for task storage.
InMemoryTaskStorageloses state between Lambda invocations. - Post-response task completion is best-effort (operational limitation): The
tools/callrequest path is non-blocking and returnsCreateTaskResultimmediately. However, the background tool work (tokio::spawn) may not complete before Lambda freezes the execution environment. Short-lived tools that finish within the invocation work reliably. Long-running tools MUST use an external updater (Step Functions, callback Lambda) to drive task completion via durable storage. This is a Lambda platform constraint, not a framework bug. - Stuck-task recovery: On each Lambda cold start,
recover_stuck_tasks()marks staleWorkingtasks asFailed(configurable viatask_recovery_timeout_ms). - Cross-invocation cancellation is best-effort:
tasks/cancelupdates storage status, but cannot signal a frozen Lambda invocation. Work may complete after cancellation. tasks/resultpolling: When the executor doesn't track a task (different invocation), the handler falls back to 500ms storage polling with a 5-minute timeout.- Cost optimization: Lambda billing is request + duration based; reducing invocation duration usually reduces cost. Non-blocking task dispatch returns
CreateTaskResultfast and frees the invocation rather than holding it open waiting for tool completion.
CORS Configuration
Automatic CORS for Browser Clients
let server = new
.cors_allow_all_origins // Enable CORS for all origins
.build
.await?;
Custom CORS Configuration
use ;
let mut cors = for_origins;
cors.allow_credentials = true;
let server = new
.cors
.build
.await?;
SSE Streaming in Lambda
Real-time Notifications
Lambda streaming responses enable real-time SSE notifications:
;
Deployment
Local Testing with cargo-lambda
# Install cargo-lambda
# Run locally for testing
RUST_LOG=debug
# Test with MCP Inspector
# Connect to: http://localhost:9000/lambda-url/my-lambda-server
Deploy to AWS Lambda
# Build for Lambda
# Deploy to AWS
Environment Configuration
# Required environment variables
# DynamoDB table name
Examples
Complete AWS Integration Server
See examples/lambda-mcp-server for a production-ready example with:
- DynamoDB query tools
- SNS publishing
- SQS message sending
- CloudWatch metrics
- Full session persistence
- SSE streaming
Builder Pattern Examples
use LambdaMcpServerBuilder;
use ToolBuilder;
// Runtime tool creation
let dynamic_tool = new
.description
.number_param
.number_param
.execute
.build?;
let server = new
.tool
.build
.await?;
Testing
Unit Tests
The crate includes comprehensive test coverage:
# Run all tests
# Test specific modules
Integration Testing
# Test with local Lambda runtime
&
Performance Optimization
Cold Start Optimization
// Cache expensive operations at module level
static SHARED_STORAGE: OnceCell = const_new;
async
Memory Management
Lambda functions benefit from efficient memory usage:
let server = new
.tool // Use Default for zero-sized types
.build
.await?;
Feature Flags
[]
= { = "0.3", = ["cors", "sse", "dynamodb"] }
default- Includescorsandssecors- CORS header injection for Lambda responsessse- Server-Sent Events stream adaptationdynamodb- DynamoDB session storage backend
Limitations
Lambda-Specific Considerations
- Request Timeout: Lambda has 15-minute maximum execution time
- Payload Size: 6MB maximum payload size for synchronous invocations
- Concurrent Executions: Subject to AWS Lambda concurrency limits
- Cold Starts: First invocation may have higher latency
SSE Streaming Notes
- Lambda streaming responses have size and time limits
- Long-running SSE connections may be terminated by Lambda
- Consider API Gateway + SSE patterns for persistent-style client updates
Error Handling
Lambda Error Patterns
// Proper error handling for Lambda
handler.handle.await
.map_err
Server Capabilities
Truthful Capability Reporting
The framework automatically sets server capabilities based on registered components:
let server = new
.tool
.resource
.build
.await?;
// Framework automatically advertises:
// - tools.listChanged = false (static tool list)
// - resources.subscribe = false (no subscriptions)
// - resources.listChanged = false (static resource list)
// - No prompts capability (none registered)
This ensures clients receive accurate information about server capabilities.
License
Licensed under the MIT License. See LICENSE for details.