serverless-mcp-aws
Provides a Lambda handler adapter and AWS CDK construct for deploying an MCP server to AWS Lambda behind API Gateway HTTP API, with stateless Streamable HTTP, JWT bearer authentication, per-scope tool allowlists, OAuth protected resource metadata, and least-privilege IAM.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@serverless-mcp-awsdeploy my notes MCP server to Lambda with JWT auth and per-scope tool allowlists"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
serverless-mcp-aws
Run an MCP server on AWS Lambda behind API Gateway (HTTP API): stateless Streamable HTTP through the official
mcpSDK, JWT bearer auth (RS256/ES256 + JWKS), per-scope tool allowlists, OAuth protected resource metadata and an AWS CDK construct.

serverless-mcp-aws turns an MCPServer from the official Python SDK into a
Lambda handler: handler = lambda_handler_for(server, auth=...). Each API
Gateway HTTP API event becomes one stateless MCP exchange (the 2026-07-28
per-request envelope, or the 2025 initialize handshake for older clients),
the bearer token is verified and stopped at the handler, the caller's scopes
decide which tools it can list and call, and the response goes back as a Lambda
proxy result. A CDK construct builds the Lambda, the HTTP API, an optional
authorizer and least-privilege IAM, and a dev server runs the very same handler
on localhost. Runs 100% locally, no paid APIs required: tests use locally
generated keys, moto and the official MCP client, and the CDK code is only ever
synthesised.
Why
MCP's Streamable HTTP transport was designed for long-lived processes. Lambda is the opposite: no memory between requests, a buffered 30-second response, and an API Gateway in front that already does TLS, throttling and authorizers. The 2026-07-28 revision made MCP stateless, but getting it right on Lambda is still a pile of details:
One request, one invocation. No
Mcp-Session-Id, no GET stream, no in-memory state. The SDK runs in stateless JSON-response mode per invocation;subscriptions/listen(a long-lived stream) is refused with 404,GET/DELETEwith 405, as the spec says for stateless servers.Auth the way the MCP authorization spec wants it. RFC 9728 metadata at both well-known paths,
401withWWW-Authenticate: Bearer resource_metadata=...,403 insufficient_scopenaming the scopes that would grant the tool, audience bound to the server's canonical URI (RFC 8707), tokens never accepted in the URL.JWT verification that does not fall for the classics.
alg=none, HS256-signed-with-the-public-key,kidconfusion, embeddedjwk/jku, oversized tokens, unknown-kidrefresh storms: all refused and all tested.No token passthrough. Tools see a
Principal(subject, scopes), never the bearer token;Authorizationand cookies are stripped before the SDK.Infrastructure you can read. One construct, 1-day logs, a throttled stage, an IAM role that can only write its own logs until you grant more.
Related MCP server: serverless-mcp-servers-cdk
Quickstart
pip install "serverless-mcp-aws[cdk] @ git+https://github.com/MartinCastroAlvarez/serverless-mcp-aws"
git clone https://github.com/MartinCastroAlvarez/serverless-mcp-aws && cd serverless-mcp-aws
python -m examples.local_demo # fake IdP + moto S3 + dev server + MCP client
serverless-mcp-aws dev examples.notes.app:server # :8000/mcp, no auth; set NOTES_BUCKET (examples/README.md)
claude mcp add --transport http notes http://127.0.0.1:8000/mcpIt will be pip install "serverless-mcp-aws[cdk]" once the first release is on PyPI.
# app.py, deployed as the Lambda handler "app.handler"
from mcp.server.mcpserver import MCPServer
from serverless_mcp_aws import (
BearerAuth, JWTVerifier, ProtectedResourceMetadata, ToolPolicy,
current_principal, lambda_handler_for,
)
server = MCPServer("notes")
@server.tool()
def whoami() -> str:
"""Who is calling."""
return current_principal().subject
handler = lambda_handler_for(
server,
auth=BearerAuth(JWTVerifier.from_env()), # MCP_JWT_ISSUER, MCP_JWT_JWKS_URL, ...
resource_metadata=ProtectedResourceMetadata.from_env(), # MCP_RESOURCE, MCP_AUTHORIZATION_SERVERS
tool_policy=ToolPolicy({"notes:read": ["whoami"]}),
allowed_origins=["https://app.example.com"], # browsers; non-browser clients send no Origin
)# stack.py (synth only in this repo; deploying is your call)
from serverless_mcp_aws.cdk import McpServerFunction
mcp = McpServerFunction(self, "Notes", code=lambda_.Code.from_asset("build"), handler="app.handler",
environment={...})
bucket.grant_read_write(mcp.function, "notes/*")How it works
flowchart LR
C["MCP client<br/>(Claude Code, agents, SDK)"] -->|"POST /mcp<br/>Authorization: Bearer"| GW["API Gateway HTTP API<br/>$default stage, throttled"]
C -->|"GET /.well-known/<br/>oauth-protected-resource/mcp"| GW
GW -.->|optional| AZ["Lambda authorizer<br/>(same JWTVerifier)"]
GW --> H
subgraph H["Lambda: McpLambdaHandler"]
direction TB
R["route · CORS preflight"] --> O["Origin allowlist<br/>403"]
O --> M["POST only (405)<br/>no token in URL (400)<br/>body limit (413)"]
M --> A["authenticate<br/>401 + WWW-Authenticate"]
A --> B["JSON-RPC: single,<br/>or batch (2025-03-26)"]
B --> P["ToolPolicy<br/>403 insufficient_scope"]
P --> S["mcp SDK<br/>stateless, JSON responses"]
S --> F["filter tools/list<br/>strip session ids"]
end
S --> T["your tools<br/>current_principal()"]
T --> AWS[("S3 / APIs<br/>function's own IAM role")]
JW[("JWKS<br/>cached, rate-limited refresh")] -.-> A
JW -.-> AZRouting. The MCP path (default
/mcp) and the two RFC 9728 paths (/.well-known/oauth-protected-resource/mcpand the root variant) are served; everything else is 404. Metadata is public and CORS-open (*, no credentials).Origin. A browser always sends
Originon a cross-site POST; it must be inallowed_origins(exactscheme://host[:port], orhttp://localhost:*) or the request gets 403 before auth runs. Requests withoutOrigin(SDKs, agents, curl) pass. This is the spec's DNS-rebinding defence.Authentication.
BearerAuth(JWTVerifier(...))checks the token in the function;AuthorizerContextAuth()trusts what an API Gateway authorizer put inrequestContext.authorizer(a client cannot write that), re-checkingexp. Failures are 401 withresource_metadata,scopeand a fixederror_description.Messages. A single JSON-RPC message goes to the SDK. A JSON array is a batch: allowed only for protocol
2025-03-26(or no version header, which the spec says means 2025-03-26), at most 20 items, noinitialize; each item is dispatched and the replies come back as an array (202 if all were notifications).Authorization. For
tools/call, the tool must be granted by one of the token's scopes (ToolPolicy, default deny,"*"for an admin scope); otherwise 403insufficient_scopewith the scopes that would grant it.tools/listresults are filtered to the same set and markedcacheScope: private.The SDK. A fresh stateless
streamable_http_app(JSON responses) handles the message: protocol-version negotiation,Mcp-Method/Mcp-Nameheader validation (HeaderMismatch-32020),server/discover, tool dispatch. The principal is visible to tools viacurrent_principal()and the SDK'sget_access_token()(with an emptytokenfield).Response.
Mcp-Session-Idis never minted or echoed;Cache-Control: no-store,X-Content-Type-Options: nosniff, CORS headers for allowed origins.
The JWT verifier
Check | Rule |
Algorithms | allowlist subset of |
Key selection | only from the configured JWKS URL (https; http only for loopback); |
Keys skipped |
|
Header |
|
Claims |
|
Limits | token ≤ 8 KiB before parsing; JWKS ≤ 64 KiB, 3 s timeout, no redirects |
JWKS cache | 10 min TTL; an unknown |
Two ways to authenticate
| Lambda authorizer + | |
401 challenge with | yes | no: API Gateway answers a bare 401/403 (clients fall back to the well-known URL) |
Cost per refused request | a Lambda invocation | an authorizer invocation (cacheable) |
Tool allowlist | yes | yes (scopes travel in the authorizer context) |
Also |
|
CDK construct
McpServerFunction(scope, id, code=..., handler=..., environment=..., authorizer=None):
Resource | Default |
Lambda | Python 3.12, arm64, 512 MB, 29 s timeout, optional reserved concurrency |
IAM | its own role: |
Logs | log groups created up front, 1-day retention, destroyed with the stack |
HTTP API |
|
Routes |
|
Authorizer | none, |
Tests check the synthesised template with aws_cdk.assertions only. Nothing in
this repository runs cdk deploy.
Configuration
Python | Environment ( | Meaning |
|
| canonical URI of the MCP endpoint, e.g. |
|
| comma-separated issuer URLs |
|
| space-separated |
|
| exact |
|
| https JWKS endpoint |
|
| defaults to |
|
| subset of |
| browser origins allowed to call (none by default) | |
|
| |
| required to run without auth (refused otherwise) | |
|
|
Copy .env.example for the notes example.
Local development
serverless-mcp-aws dev module:attr [--port 8000] [--authorizer module:attr]
serves a handler (or a bare MCPServer, anonymously, localhost origins only) on
127.0.0.1 by turning each HTTP request into an API Gateway v2 event. With
--authorizer it emulates API Gateway's authorizer step (401 without a token,
403 on refusal, the context in requestContext.authorizer.lambda). It refuses
non-loopback addresses unless --allow-remote.
Testing
python3 -m venv .venv && . .venv/bin/activate && pip install poetry
poetry install --all-extras
poetry run pytest # ~270 tests, coverage ≥ 85% (currently ~98%)tests/test_handler.py: the adapter with API Gateway events (a captured sample intests/events/): both protocol eras, batching, 405/413/400 paths, origins and CORS, 401/403 challenges,tools/listfiltering, metadata.tests/test_jwt.py: locally generated RSA/EC keys; expired,nbf, wrongaud/iss,alg=none, HS256 with the public key,kidconfusion, key injection headers, oversized and malformed tokens, JWKS caching and rotation.tests/test_e2e.py: the official MCP client (mode="auto"and"legacy") against the dev server, with JWTs and with the emulated authorizer.tests/test_notes_example.py: the notes tools on a moto bucket, per-caller isolation.tests/test_cdk.py: CDK assertions (needs Node.js; skipped without thecdkextra).tests/test_security.py: noeval/pickle/subprocess, tools never see the token, tokens never logged.
Part of the series
Series | Repositories |
Agent tooling | agent-lint · repo-graph-mcp · s3-agent-memory · pytest-llm-evals · serverless-mcp-aws · agent-model-router |
Django admin family | django-admin-react · django-admin-rest-api · django-admin-mcp-api · django-admin-graph · django-admin-skills |
Apps | |
Patterns catalog | agentic-architectures — every agent pattern, linking the LangGraph, ADK, CrewAI and LangChain repos |
Contributing
See CONTRIBUTING.md, ARCHITECTURE.md,
SECURITY.md and the agent contract in CLAUDE.md.
License
MIT © 2026 Martín Castro Álvarez. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
One OAuth connector for your team's remote MCP servers, with Tool Packs, guardrails and logs.
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
Streamable HTTP MCP server for Google Calendar and Sheets with OAuth login.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA minimal, production-ready MCP server running on AWS Lambda with Streamable HTTP transport, enabling deployment of custom tools behind API Gateway.1MIT
- FlicenseNot gradedqualityFmaintenanceDeploys a stateless MCP server on AWS Lambda and API Gateway using CDK, supporting Express.js or Hono implementations with bearer token authentication and MCP tools, resources, and prompts.15-
- FlicenseNot gradedqualityCmaintenanceProvides a production-oriented MCP server exposing tools, resources, and prompts over Streamable HTTP with JWT authentication and per-tool RBAC, enabling Amazon Bedrock Claude clients to securely invoke MCP capabilities.-
- FlicenseNot gradedqualityCmaintenanceEnables deployment and connection to a minimal Streamable HTTP MCP server with bearer-token authentication, a health endpoint, and an example tool, while allowing developers to add custom tools.-