cd /news/developer-tools/building-lightweight-and-streamable-… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-94532] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Building Lightweight and Streamable MCP Servers on AWS Lambda with Python

A developer has released two open-source projects that enable building lightweight and streamable MCP servers on AWS Lambda with Python. The projects, modmex-lambda and an unnamed companion, allow applications to start with a buffered MCP server and opt into real Lambda response streaming when needed, without requiring containers or a full web framework.

read9 min views1 publishedAug 12, 2026

MCP servers don't always need containers, persistent processes, or a full web framework.

For many tools, a regular Python AWS Lambda is enough:

MCP request
    ↓
API Gateway
    ↓
Lambda
    ↓
Python tool
    ↓
JSON-RPC response

But some operations are different.

A tool may spend 30 seconds searching, analyzing, or coordinating work and need to report progress while it runs.

For those workloads we need real Streamable HTTP:

tools/call
    ↓
progress
    ↓
progress
    ↓
progress
    ↓
final result

We wanted both models in Python without requiring applications to adopt different MCP programming models.

That led to two open-source projects:

Together they let a Python application start with a lightweight buffered MCP server and opt into real Lambda response streaming when the workload actually needs it.

The simplest deployment doesn't require Lambda Web Adapter or response streaming.

Install modmex-lambda

:

pip install modmex-lambda

Create an MCP server:

from modmex_lambda import APIGatewayHttpResolver
from modmex_lambda.mcp import MCPServer

mcp = MCPServer(
    name="orders",
    version="1.0.0",
)

Then expose capabilities as regular Python functions.

@mcp.tool()
def get_order(order_id: str) -> dict:
    return {
        "id": order_id,
        "status": "confirmed",
    }
php
@mcp.resource("orders://{order_id}")
def order_resource(order_id: str) -> dict:
    return load_order(order_id)
python
@mcp.prompt()
def order_assistant(customer_id: str):
    return build_order_prompt(customer_id)

Mount the MCP server on the normal API Gateway resolver:

app = APIGatewayHttpResolver()

app.include_mcp(
    mcp,
    path="/mcp",
)

handler = app.handler

The resulting architecture is deliberately boring:

MCP Client
    ↓
API Gateway HTTP API v2
    ↓
AWS Lambda
    ↓
modmex-lambda
    ↓
MCPServer

For short-lived tools, resources, and prompts, that's usually exactly what we want.

No FastAPI.

No Flask.

No ASGI server.

No response-streaming infrastructure.

Just Python and Lambda.

One of the design goals was not to create a separate application architecture for MCP.

Tools can use the same dependency injection mechanisms as regular Lambda endpoints:

@mcp.tool()
def get_order(
    order_id: str,
    service: Annotation[OrderService, Depends()],
):
    return service.get(order_id)

That means REST and MCP can remain thin interfaces over the same application services:

                 OrderService
                     β–²
                     β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚                     β”‚
       REST API                MCP

The same idea applies to middleware.

Authorization, tenant resolution, logging, auditing, tracing, and policies don't need to be implemented inside every tool:

@mcp.tool(
    middlewares=[
        RequirePermission("orders:read"),
    ]
)
def get_order(...):
    ...

MCP becomes another transport into the application rather than another application architecture.

It's easy to associate MCP with streaming, but many MCP operations don't benefit from it.

Consider:

get_customer
get_order
calculate_route
lookup_inventory
read_resource
get_prompt

If a tool finishes in 300 milliseconds or two seconds, a normal Lambda response is simpler.

For this reason, streaming isn't a requirement in modmex-lambda

.

You can run MCP through a regular managed Python Lambda and API Gateway HTTP API v2.

That gives us the first deployment model:

Python
+
modmex-lambda
+
Lambda
+
HTTP API v2

Then, when a workload actually needs incremental communication, we can move to the second model.

Consider a tool that performs several expensive steps:

@mcp.tool()
def analyze_market(ctx: MCPContext):
    opportunities = search_opportunities()
    ranked = rank_opportunities(opportunities)
    analysis = analyze_market_conditions(ranked)

    return build_recommendation(analysis)

Maybe the complete operation takes 30 or 40 seconds.

With a buffered response, the MCP client sees nothing until the function finishes.

Instead, we want the tool to report progress:

@mcp.tool()
def analyze_market(ctx: MCPContext):

    ctx.progress.report(
        1,
        total=4,
        message="Searching opportunities",
    )

    opportunities = search_opportunities()

    ctx.progress.report(
        2,
        total=4,
        message="Ranking candidates",
    )

    ranked = rank_opportunities(opportunities)

    ctx.progress.report(
        3,
        total=4,
        message="Analyzing market conditions",
    )

    analysis = analyze_market_conditions(ranked)

    ctx.progress.report(
        4,
        total=4,
        message="Building recommendation",
    )

    return build_recommendation(analysis)

Those progress reports are translated into MCP notifications/progress

messages and sent over the same Streamable HTTP response before the final JSON-RPC result.

Now we need real response streaming.

For streaming, modmex-lambda

provides LambdaWebAdapterResolver

.

The application remains Python:

from modmex_lambda import LambdaWebAdapterResolver
from modmex_lambda.mcp import MCPServer

mcp = MCPServer(
    name="orders",
    version="1.0.0",
)

app = LambdaWebAdapterResolver()

app.include_mcp(
    mcp,
    path="/mcp",
)

handler = app.handler

The infrastructure changes underneath it:

MCP Client
    ↓
API Gateway REST API
    ↓
responseTransferMode = STREAM
    ↓
AWS Lambda
    ↓
Lambda Web Adapter
    ↓
Python application
    ↓
modmex-lambda

Lambda Web Adapter connects the HTTP response produced by the Python application with Lambda response streaming.

Now an MCP tool can emit progress while it is still running:

0s    tools/call
      ↓
4s    notifications/progress
      "Searching opportunities"
      ↓
12s   notifications/progress
      "Ranking candidates"
      ↓
21s   notifications/progress
      "Analyzing market conditions"
      ↓
30s   final JSON-RPC result

The Lambda invocation hasn't completed when those progress messages reach the MCP client.

That's real incremental MCP streaming.

Getting streaming to work inside Python is only part of the job.

A streaming Lambda deployment also needs the right infrastructure:

Lambda Web Adapter
response streaming mode
API Gateway REST API
STREAM transfer mode
launcher configuration
architecture-specific adapter layer
packaging

We didn't want every Python MCP service to reproduce that configuration manually.

That's why we built serverless-python-mcp

.

Install it:

npm install --save-dev serverless-python-mcp

and register it like any other Serverless Framework plugin:

plugins:
  - serverless-python-mcp

MCP servers are declared under custom.pythonMcp.servers

.

For a normal buffered MCP server:

custom:
  pythonMcp:
    servers:
      orders:
        handler: app.handler
        transport: httpApi
        streaming: false

The application uses:

app = APIGatewayHttpResolver()
app.include_mcp(mcp, path="/mcp")

handler = app.handler

The plugin creates a normal Lambda behind API Gateway HTTP API v2.

No Lambda Web Adapter is added.

No streaming launcher is added.

This remains the lightweight deployment path.

When the same class of application needs real streaming:

custom:
  pythonMcp:
    servers:
      orders:
        handler: app.handler
        transport: http
        streaming: true

The Python application switches to:

app = LambdaWebAdapterResolver()
app.include_mcp(mcp, path="/mcp")

handler = app.handler

The plugin takes care of the AWS-specific pieces required for streaming.

It attaches the architecture-specific Lambda Web Adapter layer, configures the execution wrapper and streaming mode, creates the launcher used by the HTTP process, and configures the REST API integration for streaming.

The developer still works with a Python MCP server.

serverless-python-mcp

currently supports three AWS front doors:

Transport AWS front door Buffered Streaming
httpApi
API Gateway HTTP API v2 Yes No
http
API Gateway REST API v1 Yes Yes
url
Lambda Function URL Yes Yes

This lets the infrastructure match the workload.

For a simple internal MCP service:

transport: httpApi
streaming: false

For an MCP server that needs API Gateway capabilities and real streaming:

transport: http
streaming: true

And for cases where a Function URL is sufficient:

transport: url
streaming: true

The application doesn't need a new MCP abstraction for each one.

Streaming and transport are intentionally separate choices.

For example:

custom:
  pythonMcp:
    servers:
      orders:
        handler: app.handler
        transport: http
        streaming: false

uses API Gateway REST API but invokes the Python Lambda normally.

The application uses:

app = APIGatewayRestResolver()

This can be useful when REST API features are desired but incremental MCP streaming isn't.

The deployment model therefore isn't simply:

HTTP API = simple
REST API = streaming

It's more accurately:

                       Buffered       Streaming

HTTP API v2               βœ“               -

REST API v1               βœ“               βœ“

Function URL              βœ“               βœ“

Each server can expose its own path:

custom:
  pythonMcp:
    servers:
      orders:
        handler: orders.handler
        transport: http
        streaming: true
        path: /orders/mcp

      inventory:
        handler: inventory.handler
        transport: http
        streaming: true
        path: /inventory/mcp

The Python application registers the same path:

app.include_mcp(
    mcp,
    path="/orders/mcp",
)

Servers using API Gateway can share the underlying API while keeping separate Lambda functions and MCP endpoints.

That makes it possible to expose multiple domain capabilities without building one giant MCP server.

The plugin also deliberately doesn't invent an MCP-specific authentication model.

For HTTP API, existing Serverless authorizer configuration can be used.

For REST API, the plugin passes authorizer configuration through to the normal Serverless REST API event compiler.

Function URLs can use their supported public or AWS IAM modes.

So the architecture remains:

MCP Client
    ↓
AWS authentication / authorizer
    ↓
MCP transport
    ↓
middleware / application authorization
    ↓
tool

This keeps authentication independent from the MCP programming model.

An important lesson from building this was that streaming shouldn't become the default architecture just because MCP supports it.

For many servers:

HTTP API v2
+
Lambda
+
modmex-lambda

is enough.

It's lightweight and fits the serverless execution model extremely well.

Streaming becomes useful when the operation actually has intermediate information worth delivering.

Then we can move to:

REST API
+
Lambda response streaming
+
Lambda Web Adapter
+
modmex-lambda

without redesigning tools, resources, prompts, middleware, or application services.

There is one serverless behavior worth understanding.

A client disconnect doesn't guarantee that the Lambda invocation immediately stops.

There are multiple network boundaries between the MCP client and the Python process:

MCP Client
    ↓
API Gateway
    ↓
Lambda
    ↓
Lambda Web Adapter
    ↓
Python

When the transport can observe a disconnect, modmex-lambda

can propagate cooperative cancellation through MCPContext

.

A long-running tool can therefore check:

if ctx.cancelled:
    return {"status": "cancelled"}

But applications shouldn't assume that every downstream network failure will immediately terminate a running Lambda invocation.

Response streaming and distributed execution cancellation are separate concerns.

The final architecture looks like this:

                         MCPServer
                            β”‚
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚                             β”‚
             β–Ό                             β–Ό
      Buffered execution             Streamable execution
             β”‚                             β”‚
      regular Lambda                  HTTP process
             β”‚                             β”‚
             β–Ό                             β–Ό
   HTTP API / REST / URL            Lambda Web Adapter
                                           β”‚
                                           β–Ό
                                    Lambda streaming

The important part is what doesn't change:

tools
resources
prompts
dependency injection
middleware
application services

Streaming is an infrastructure capability, not a new application architecture.

The complete implementation is available in two projects:

modmex-lambda contains the Python MCP runtime and application integration.

It provides the MCP server, tools, resources, prompts, middleware, dependency injection, protocol validation, buffered HTTP transports, and Streamable HTTP support.

serverless-python-mcp provides the Serverless Framework deployment integration.

It creates MCP Lambda functions from custom.pythonMcp.servers

and configures the appropriate AWS front door and runtime behavior for buffered or streaming execution.

The design goal behind both projects is straightforward:

You don't need streaming to run MCP on Lambda. But when you need it, you shouldn't have to rewrite your MCP server.

Start with the smallest architecture that works.

Add streaming when the workload earns the complexity.

Keep the Python application the same.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @aws lambda 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/building-lightweight…] indexed:0 read:9min 2026-08-12 Β· β€”