Serverless MCP Server
by goodandco
README.md
# Serverless MCP Server
A minimal, production-ready example of an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server running on AWS Lambda with **Streamable HTTP** transport, deployed behind **API Gateway HTTP API v2**.
Built with the official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — no stdio adapters, no proxies.
## Architecture
```
AI Client (Cursor / Windsurf / Claude Code / VS Code)
│
│ POST /mcp x-api-key: <key>
▼
API Gateway HTTP API v2
│
├─ Lambda Authorizer ← validates x-api-key against Secrets Manager
│
▼
Lambda (Docker image, Node.js 22)
│
│ Lambda Web Adapter translates Lambda events → HTTP
▼
Node.js HTTP server (MCP SDK StreamableHTTPServerTransport)
```
**Stateless by design** — each request creates a fresh MCP server + transport instance. No session state, no DynamoDB, no cold-start coordination needed.
### Stack
| Layer | Technology |
|---|---|
| Infrastructure | AWS CDK v2 (TypeScript) |
| API | API Gateway HTTP API v2 |
| Runtime | Lambda — Docker image (Node.js 22) |
| Authorizer | Lambda — `NodejsFunction` esbuild bundle (Node.js 22) |
| Auth | API key stored in Secrets Manager, validated per-request |
| MCP transport | `StreamableHTTPServerTransport` (stateless, JSON response mode) |
| Lambda ↔ HTTP bridge | [AWS Lambda Web Adapter](https://github.com/aws/aws-lambda-web-adapter) |
### Included sample tools
| Tool | Description |
|---|---|
| `get_weather` | Returns mock weather for a city |
| `calculate` | Basic arithmetic (add / subtract / multiply / divide) |
| `get_server_status` | Server status, timestamp, AWS region |
| `lookup_user` | Mock user lookup by ID |
Replace these with your real tool implementations in [`app/src/server.ts`](app/src/server.ts).
## Prerequisites
- Node.js 22+
- Docker (running)
- AWS CLI configured — `aws sts get-caller-identity` should succeed
- CDK bootstrapped in your target region:
```bash
npx cdk bootstrap aws://YOUR_ACCOUNT_ID/YOUR_REGION
```
## Setup
**1. Install dependencies**
```bash
npm install
cd app && npm install && cd ..
cd lib/authorizer && npm install && cd ../..
```
**2. Configure your API key**
```bash
cp .env.example .env
```
Generate a strong key and paste it into `.env`:
```bash
openssl rand -hex 32
# → paste output as API_KEY=... in .env
```
**3. Deploy**
```bash
npx cdk deploy
```
CDK builds the Docker image, pushes to ECR, creates the Lambda functions, wires up API Gateway, and stores your key in Secrets Manager. The endpoint URL is printed as output:
```
Outputs:
McpServerStack.McpEndpoint = https://<id>.execute-api.<region>.amazonaws.com/mcp
```
## Local testing
```bash
cd app && npm run build && node dist/server.js
```
```bash
# Initialize
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"clientInfo":{"name":"test","version":"1.0"},"protocolVersion":"2025-03-26","capabilities":{}},"id":1}' | jq .
# List tools
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}' | jq .
# Call a tool
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_weather","arguments":{"city":"Kyiv"}},"id":3}' | jq .
```
## Testing the deployed endpoint
```bash
MCP_URL="https://<id>.execute-api.<region>.amazonaws.com/mcp"
API_KEY="<your key from .env>"
curl -s -X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: $API_KEY" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq .
# Should return 403
curl -s -X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "x-api-key: wrong-key" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
```
For interactive debugging, use [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector
```
Select **Streamable HTTP**, enter the endpoint URL, add `x-api-key` header.
## Connecting clients
### Cursor
`.cursor/mcp.json`:
```json
{
"mcpServers": {
"my-server": {
"url": "https://<id>.execute-api.<region>.amazonaws.com/mcp",
"headers": { "x-api-key": "<API_KEY>" }
}
}
}
```
### Windsurf
`~/.windsurf/mcp.json` (or workspace `.windsurf/mcp.json`):
```json
{
"mcpServers": {
"my-server": {
"url": "https://<id>.execute-api.<region>.amazonaws.com/mcp",
"headers": { "x-api-key": "<API_KEY>" }
}
}
}
```
### VS Code
`.vscode/mcp.json`:
```json
{
"servers": {
"my-server": {
"type": "sse",
"url": "https://<id>.execute-api.<region>.amazonaws.com/mcp",
"headers": { "x-api-key": "<API_KEY>" }
}
}
}
```
> VS Code uses `"type": "sse"` — it does not yet support `"type": "http"` (Streamable HTTP).
### Claude Code
```bash
claude mcp add my-mcp-server --transport sse "$MCP_URL" \
--header "x-api-key: $API_KEY"
```
## Adding your own tools
Edit [`app/src/server.ts`](app/src/server.ts) and add tools inside `createMcpServer()`:
```typescript
server.tool(
"my_tool",
"What this tool does",
{ param: z.string().describe("A parameter") },
async ({ param }) => ({
content: [{ type: "text", text: `Result for ${param}` }],
})
);
```
Then redeploy with `npx cdk deploy`.
## Cleanup
```bash
npx cdk destroy
```
## Why Lambda Web Adapter?
`StreamableHTTPServerTransport` expects standard Node.js `IncomingMessage`/`ServerResponse` objects, not Lambda's JSON event format. [Lambda Web Adapter](https://github.com/aws/aws-lambda-web-adapter) runs as a Lambda extension and transparently translates Lambda invocations into HTTP requests to the local server — no application code changes needed.
## Why stateless?
Lambda instances are ephemeral and may not be reused between requests. Stateful MCP sessions (with session IDs) would require persisting transport state externally (e.g. DynamoDB), adding complexity. Stateless mode (`sessionIdGenerator: undefined`) makes each request fully self-contained — a natural fit for Lambda.
> **Note:** API Gateway HTTP API v2 has a hard 30-second integration timeout. All tool calls must complete within this window.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues