title: "Deploying MCPs on AWS Lambda" description: "Run MCP servers on AWS Lambda with API Gateway integration and cost-effective scaling." slug: "aws-lambda" category: "deploy" updatedAt: "2025-09-21T00:00:00.000Z" faqs:
- q: "What are the cost benefits of running MCPs on AWS Lambda?" a: "Lambda's pay-per-request model can be very cost-effective for MCPs with variable usage, especially compared to always-on servers."
- q: "How do I handle cold starts with MCP servers on Lambda?" a: "Use provisioned concurrency for critical MCPs, optimize bundle size, and implement connection pooling to minimize cold start impact."
Deploying MCPs on AWS Lambda
Overview
AWS Lambda provides serverless compute for MCP servers with automatic scaling, pay-per-request pricing, and integration with AWS services. This guide covers deployment, optimization, and best practices.
Prerequisites
- AWS Account with appropriate permissions
- AWS CLI configured
- Node.js 18+ or Python 3.9+
- Serverless Framework or AWS SAM (recommended)
Project Setup
Serverless Framework Setup
# Install Serverless Framework
npm install -g serverless
# Create new project
serverless create --template aws-nodejs --path mcp-lambda-server
cd mcp-lambda-server
# Install dependencies
npm install @modelcontextprotocol/sdk aws-lambda
Project Structure
mcp-lambda-server/
├── src/
│ ├── handlers/
│ │ ├── mcp.js
│ │ └── health.js
│ ├── lib/
│ │ ├── mcp-server.js
│ │ └── utils.js
│ └── layers/
│ └── nodejs/
├── serverless.yml
├── package.json
└── README.md
Lambda Function Implementation
MCP Server Core (src/lib/mcp-server.js)
const { MCPServer } = require('@modelcontextprotocol/sdk');
const AWS = require('aws-sdk');
class LambdaMCPServer {
constructor() {
this.server = new MCPServer({
name: 'lambda-mcp-server',
version: '1.0.0'
});
this.s3 = new AWS.S3();
this.dynamodb = new AWS.DynamoDB.DocumentClient();
this.setupTools();
}
setupTools() {
// S3 operations
this.server.addTool('s3_list_objects', {
description: 'List objects in S3 bucket',
parameters: {
bucket: { type: 'string', required: true },
prefix: { type: 'string', default: '' }
},
handler: async ({ bucket, prefix }) => {
const params = { Bucket: bucket, Prefix: prefix };
const result = await this.s3.listObjectsV2(params).promise();
return { objects: result.Contents };
}
});
// DynamoDB operations
this.server.addTool('dynamodb_query', {
description: 'Query DynamoDB table',
parameters: {
tableName: { type: 'string', required: true },
key: { type: 'object', required: true }
},
handler: async ({ tableName, key }) => {
const params = { TableName: tableName, Key: key };
const result = await this.dynamodb.get(params).promise();
return { item: result.Item };
}
});
// HTTP requests
this.server.addTool('http_request', {
description: 'Make HTTP requests',
parameters: {
url: { type: 'string', required: true },
method: { type: 'string', default: 'GET' }
},
handler: async ({ url, method }) => {
const https = require('https');
return new Promise((resolve, reject) => {
const req = https.request(url, { method }, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({
status: res.statusCode,
data: JSON.parse(data)
}));
});
req.on('error', reject);
req.end();
});
}
});
}
async handleRequest(event) {
try {
const request = JSON.parse(event.body);
const response = await this.server.handleRequest(request);
return response;
} catch (error) {
throw new Error(`MCP request failed: ${error.message}`);
}
}
}
module.exports = { LambdaMCPServer };
Lambda Handler (src/handlers/mcp.js)
const { LambdaMCPServer } = require('../lib/mcp-server');
let mcpServer;
const initializeServer = () => {
if (!mcpServer) {
mcpServer = new LambdaMCPServer();
}
return mcpServer;
};
exports.handler = async (event, context) => {
// Enable connection reuse
context.callbackWaitsForEmptyEventLoop = false;
const server = initializeServer();
try {
// Handle different HTTP methods
switch (event.httpMethod) {
case 'GET':
if (event.path === '/tools') {
const tools = await server.server.listTools();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ tools })
};
}
break;
case 'POST':
if (event.path === '/call') {
const result = await server.handleRequest(event);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(result)
};
}
break;
case 'OPTIONS':
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
},
body: ''
};
}
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' })
};
} catch (error) {
console.error('Lambda error:', error);
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
error: 'Internal server error',
message: error.message,
requestId: context.awsRequestId
})
};
}
};
Health Check Handler (src/handlers/health.js)
exports.handler = async (event, context) => {
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.VERSION || '1.0.0',
requestId: context.awsRequestId,
memoryLimit: context.memoryLimitInMB,
remainingTime: context.getRemainingTimeInMillis()
})
};
};
Serverless Configuration
serverless.yml
service: mcp-lambda-server
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
stage: ${opt:stage, 'dev'}
memorySize: 512
timeout: 30
environment:
STAGE: ${self:provider.stage}
VERSION: ${env:VERSION, '1.0.0'}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- "arn:aws:s3:::your-bucket/*"
- "arn:aws:s3:::your-bucket"
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
- dynamodb:Scan
Resource:
- "arn:aws:dynamodb:${self:provider.region}:*:table/mcp-*"
functions:
mcp:
handler: src/handlers/mcp.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
environment:
MCP_SERVER_NAME: lambda-mcp
reservedConcurrency: 10
health:
handler: src/handlers/health.handler
events:
- http:
path: /health
method: get
cors: true
layers:
mcpDependencies:
path: src/layers
name: mcp-dependencies-${self:provider.stage}
description: MCP server dependencies
compatibleRuntimes:
- nodejs18.x
resources:
Resources:
# DynamoDB table for MCP data
MCPDataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: mcp-data-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
# S3 bucket for MCP files
MCPFilesBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: mcp-files-${self:provider.stage}-${aws:accountId}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
plugins:
- serverless-offline
- serverless-webpack
- serverless-prune-plugin
custom:
webpack:
webpackConfig: webpack.config.js
includeModules: true
prune:
automatic: true
number: 3
Optimization Strategies
Bundle Optimization (webpack.config.js)
const path = require('path');
module.exports = {
entry: {
mcp: './src/handlers/mcp.js',
health: './src/handlers/health.js'
},
target: 'node',
mode: 'production',
optimization: {
minimize: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
externals: {
'aws-sdk': 'aws-sdk'
},
resolve: {
extensions: ['.js', '.json']
}
};
Connection Pooling
const AWS = require('aws-sdk');
// Reuse connections outside handler
const s3 = new AWS.S3({
maxRetries: 3,
httpOptions: {
timeout: 5000,
agent: new (require('https').Agent)({
keepAlive: true,
maxSockets: 50
})
}
});
const dynamodb = new AWS.DynamoDB.DocumentClient({
maxRetries: 3,
httpOptions: {
timeout: 5000
}
});
// Use in MCP server
class OptimizedLambdaMCPServer {
constructor() {
this.s3 = s3;
this.dynamodb = dynamodb;
// ... rest of implementation
}
}
Provisioned Concurrency
# In serverless.yml
functions:
mcp:
handler: src/handlers/mcp.handler
provisionedConcurrency: 5 # Keep 5 instances warm
events:
- http:
path: /{proxy+}
method: ANY
Deployment
Using Serverless Framework
# Deploy to development
serverless deploy --stage dev
# Deploy to production
serverless deploy --stage prod
# Deploy single function
serverless deploy function --function mcp --stage prod
# View logs
serverless logs --function mcp --tail
Using AWS SAM
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs18.x
Timeout: 30
MemorySize: 512
Environment:
Variables:
NODE_ENV: production
Resources:
MCPFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: handlers/mcp.handler
Events:
MCPApi:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
RestApiId: !Ref MCPApi
Policies:
- S3ReadPolicy:
BucketName: !Ref MCPBucket
- DynamoDBCrudPolicy:
TableName: !Ref MCPTable
MCPApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'GET,POST,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'*'"
MCPBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'mcp-files-${AWS::StackName}'
MCPTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub 'mcp-data-${AWS::StackName}'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
# Deploy with SAM
sam build
sam deploy --guided
Monitoring and Logging
CloudWatch Integration
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();
// Custom metrics
const putMetric = async (metricName, value, unit = 'Count') => {
const params = {
Namespace: 'MCP/Lambda',
MetricData: [{
MetricName: metricName,
Value: value,
Unit: unit,
Timestamp: new Date()
}]
};
await cloudwatch.putMetricData(params).promise();
};
// In your handler
exports.handler = async (event, context) => {
const startTime = Date.now();
try {
// Your MCP logic
const result = await processMCPRequest(event);
// Log success metric
await putMetric('MCPRequestSuccess', 1);
return result;
} catch (error) {
// Log error metric
await putMetric('MCPRequestError', 1);
throw error;
} finally {
// Log duration
const duration = Date.now() - startTime;
await putMetric('MCPRequestDuration', duration, 'Milliseconds');
}
};
Structured Logging
const log = (level, message, data = {}) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
requestId: context.awsRequestId,
...data
}));
};
// Usage
log('INFO', 'MCP tool called', { tool: 'get_weather', args: { city: 'NYC' } });
log('ERROR', 'Database connection failed', { error: error.message });
Security Best Practices
IAM Roles and Policies
# Minimal IAM policy
Resources:
MCPExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: MCPResourceAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- !Sub '${MCPBucket}/*'
- !Ref MCPBucket
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
Resource: !GetAtt MCPTable.Arn
Environment Variables Encryption
functions:
mcp:
handler: src/handlers/mcp.handler
environment:
API_KEY: ${ssm:/mcp/api-key~true} # Encrypted parameter
DB_PASSWORD: ${ssm:/mcp/db-password~true}
kmsKeyArn: arn:aws:kms:region:account:key/key-id
Cost Optimization
Request-Based Pricing
// Optimize for Lambda pricing model
const optimizeForLambda = {
// Batch operations when possible
batchS3Operations: async (operations) => {
const promises = operations.map(op => s3[op.method](op.params).promise());
return await Promise.all(promises);
},
// Cache frequently accessed data
cache: new Map(),
getCachedData: async (key, fetcher, ttl = 300000) => {
const cached = optimizeForLambda.cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await fetcher();
optimizeForLambda.cache.set(key, { data, timestamp: Date.now() });
return data;
}
};
Resource Right-Sizing
# Different memory configs for different functions
functions:
lightMCP:
handler: src/handlers/light.handler
memorySize: 128 # Minimal memory for simple operations
heavyMCP:
handler: src/handlers/heavy.handler
memorySize: 1024 # More memory for complex operations
batchMCP:
handler: src/handlers/batch.handler
memorySize: 3008 # Maximum memory for batch processing
timeout: 900 # 15 minutes for long operations
FAQ
What are the cost benefits of running MCPs on AWS Lambda?
Lambda's pay-per-request model can be very cost-effective for MCPs with variable or low usage patterns. You only pay for actual execution time, not idle server time, making it ideal for development environments and sporadic workloads.
How do I handle cold starts with MCP servers on Lambda?
Use provisioned concurrency for critical MCPs, optimize your bundle size with webpack, implement connection pooling, and consider using Lambda layers for shared dependencies to reduce cold start times.
Can I run long-running MCP operations on Lambda?
Lambda has a 15-minute maximum execution time. For longer operations, use Step Functions to orchestrate multiple Lambda invocations, or consider using SQS/SNS for asynchronous processing patterns.
Was this guide helpful?
Last updated: September 21, 2025
Edit this page: aws-lambda/page.mdx