{"slug": "building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python", "title": "Building Lightweight and Streamable MCP Servers on AWS Lambda with Python", "summary": "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.", "body_md": "MCP servers don't always need containers, persistent processes, or a full web framework.\n\nFor many tools, a regular Python AWS Lambda is enough:\n\n```\nMCP request\n    ↓\nAPI Gateway\n    ↓\nLambda\n    ↓\nPython tool\n    ↓\nJSON-RPC response\n```\n\nBut some operations are different.\n\nA tool may spend 30 seconds searching, analyzing, or coordinating work and need to report progress while it runs.\n\nFor those workloads we need real Streamable HTTP:\n\n```\ntools/call\n    ↓\nprogress\n    ↓\nprogress\n    ↓\nprogress\n    ↓\nfinal result\n```\n\nWe wanted both models in Python without requiring applications to adopt different MCP programming models.\n\nThat led to two open-source projects:\n\nTogether 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.\n\nThe simplest deployment doesn't require Lambda Web Adapter or response streaming.\n\nInstall `modmex-lambda`\n\n:\n\n```\npip install modmex-lambda\n```\n\nCreate an MCP server:\n\n``` python\nfrom modmex_lambda import APIGatewayHttpResolver\nfrom modmex_lambda.mcp import MCPServer\n\nmcp = MCPServer(\n    name=\"orders\",\n    version=\"1.0.0\",\n)\n```\n\nThen expose capabilities as regular Python functions.\n\n``` php\n@mcp.tool()\ndef get_order(order_id: str) -> dict:\n    return {\n        \"id\": order_id,\n        \"status\": \"confirmed\",\n    }\nphp\n@mcp.resource(\"orders://{order_id}\")\ndef order_resource(order_id: str) -> dict:\n    return load_order(order_id)\npython\n@mcp.prompt()\ndef order_assistant(customer_id: str):\n    return build_order_prompt(customer_id)\n```\n\nMount the MCP server on the normal API Gateway resolver:\n\n```\napp = APIGatewayHttpResolver()\n\napp.include_mcp(\n    mcp,\n    path=\"/mcp\",\n)\n\nhandler = app.handler\n```\n\nThe resulting architecture is deliberately boring:\n\n```\nMCP Client\n    ↓\nAPI Gateway HTTP API v2\n    ↓\nAWS Lambda\n    ↓\nmodmex-lambda\n    ↓\nMCPServer\n```\n\nFor short-lived tools, resources, and prompts, that's usually exactly what we want.\n\nNo FastAPI.\n\nNo Flask.\n\nNo ASGI server.\n\nNo response-streaming infrastructure.\n\nJust Python and Lambda.\n\nOne of the design goals was not to create a separate application architecture for MCP.\n\nTools can use the same dependency injection mechanisms as regular Lambda endpoints:\n\n``` python\n@mcp.tool()\ndef get_order(\n    order_id: str,\n    service: Annotation[OrderService, Depends()],\n):\n    return service.get(order_id)\n```\n\nThat means REST and MCP can remain thin interfaces over the same application services:\n\n```\n                 OrderService\n                     ▲\n                     │\n          ┌──────────┴──────────┐\n          │                     │\n       REST API                MCP\n```\n\nThe same idea applies to middleware.\n\nAuthorization, tenant resolution, logging, auditing, tracing, and policies don't need to be implemented inside every tool:\n\n```\n@mcp.tool(\n    middlewares=[\n        RequirePermission(\"orders:read\"),\n    ]\n)\ndef get_order(...):\n    ...\n```\n\nMCP becomes another transport into the application rather than another application architecture.\n\nIt's easy to associate MCP with streaming, but many MCP operations don't benefit from it.\n\nConsider:\n\n```\nget_customer\nget_order\ncalculate_route\nlookup_inventory\nread_resource\nget_prompt\n```\n\nIf a tool finishes in 300 milliseconds or two seconds, a normal Lambda response is simpler.\n\nFor this reason, streaming isn't a requirement in `modmex-lambda`\n\n.\n\nYou can run MCP through a regular managed Python Lambda and API Gateway HTTP API v2.\n\nThat gives us the first deployment model:\n\n```\nPython\n+\nmodmex-lambda\n+\nLambda\n+\nHTTP API v2\n```\n\nThen, when a workload actually needs incremental communication, we can move to the second model.\n\nConsider a tool that performs several expensive steps:\n\n``` python\n@mcp.tool()\ndef analyze_market(ctx: MCPContext):\n    opportunities = search_opportunities()\n    ranked = rank_opportunities(opportunities)\n    analysis = analyze_market_conditions(ranked)\n\n    return build_recommendation(analysis)\n```\n\nMaybe the complete operation takes 30 or 40 seconds.\n\nWith a buffered response, the MCP client sees nothing until the function finishes.\n\nInstead, we want the tool to report progress:\n\n``` python\n@mcp.tool()\ndef analyze_market(ctx: MCPContext):\n\n    ctx.progress.report(\n        1,\n        total=4,\n        message=\"Searching opportunities\",\n    )\n\n    opportunities = search_opportunities()\n\n    ctx.progress.report(\n        2,\n        total=4,\n        message=\"Ranking candidates\",\n    )\n\n    ranked = rank_opportunities(opportunities)\n\n    ctx.progress.report(\n        3,\n        total=4,\n        message=\"Analyzing market conditions\",\n    )\n\n    analysis = analyze_market_conditions(ranked)\n\n    ctx.progress.report(\n        4,\n        total=4,\n        message=\"Building recommendation\",\n    )\n\n    return build_recommendation(analysis)\n```\n\nThose progress reports are translated into MCP `notifications/progress`\n\nmessages and sent over the same Streamable HTTP response before the final JSON-RPC result.\n\nNow we need real response streaming.\n\nFor streaming, `modmex-lambda`\n\nprovides `LambdaWebAdapterResolver`\n\n.\n\nThe application remains Python:\n\n``` python\nfrom modmex_lambda import LambdaWebAdapterResolver\nfrom modmex_lambda.mcp import MCPServer\n\nmcp = MCPServer(\n    name=\"orders\",\n    version=\"1.0.0\",\n)\n\napp = LambdaWebAdapterResolver()\n\napp.include_mcp(\n    mcp,\n    path=\"/mcp\",\n)\n\nhandler = app.handler\n```\n\nThe infrastructure changes underneath it:\n\n```\nMCP Client\n    ↓\nAPI Gateway REST API\n    ↓\nresponseTransferMode = STREAM\n    ↓\nAWS Lambda\n    ↓\nLambda Web Adapter\n    ↓\nPython application\n    ↓\nmodmex-lambda\n```\n\nLambda Web Adapter connects the HTTP response produced by the Python application with Lambda response streaming.\n\nNow an MCP tool can emit progress while it is still running:\n\n```\n0s    tools/call\n      ↓\n4s    notifications/progress\n      \"Searching opportunities\"\n      ↓\n12s   notifications/progress\n      \"Ranking candidates\"\n      ↓\n21s   notifications/progress\n      \"Analyzing market conditions\"\n      ↓\n30s   final JSON-RPC result\n```\n\nThe Lambda invocation hasn't completed when those progress messages reach the MCP client.\n\nThat's real incremental MCP streaming.\n\nGetting streaming to work inside Python is only part of the job.\n\nA streaming Lambda deployment also needs the right infrastructure:\n\n```\nLambda Web Adapter\nresponse streaming mode\nAPI Gateway REST API\nSTREAM transfer mode\nlauncher configuration\narchitecture-specific adapter layer\npackaging\n```\n\nWe didn't want every Python MCP service to reproduce that configuration manually.\n\nThat's why we built `serverless-python-mcp`\n\n.\n\nInstall it:\n\n```\nnpm install --save-dev serverless-python-mcp\n```\n\nand register it like any other Serverless Framework plugin:\n\n```\nplugins:\n  - serverless-python-mcp\n```\n\nMCP servers are declared under `custom.pythonMcp.servers`\n\n.\n\nFor a normal buffered MCP server:\n\n```\ncustom:\n  pythonMcp:\n    servers:\n      orders:\n        handler: app.handler\n        transport: httpApi\n        streaming: false\n```\n\nThe application uses:\n\n```\napp = APIGatewayHttpResolver()\napp.include_mcp(mcp, path=\"/mcp\")\n\nhandler = app.handler\n```\n\nThe plugin creates a normal Lambda behind API Gateway HTTP API v2.\n\nNo Lambda Web Adapter is added.\n\nNo streaming launcher is added.\n\nThis remains the lightweight deployment path.\n\nWhen the same class of application needs real streaming:\n\n```\ncustom:\n  pythonMcp:\n    servers:\n      orders:\n        handler: app.handler\n        transport: http\n        streaming: true\n```\n\nThe Python application switches to:\n\n```\napp = LambdaWebAdapterResolver()\napp.include_mcp(mcp, path=\"/mcp\")\n\nhandler = app.handler\n```\n\nThe plugin takes care of the AWS-specific pieces required for streaming.\n\nIt 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.\n\nThe developer still works with a Python MCP server.\n\n`serverless-python-mcp`\n\ncurrently supports three AWS front doors:\n\n| Transport | AWS front door | Buffered | Streaming |\n|---|---|---|---|\n`httpApi` |\nAPI Gateway HTTP API v2 | Yes | No |\n`http` |\nAPI Gateway REST API v1 | Yes | Yes |\n`url` |\nLambda Function URL | Yes | Yes |\n\nThis lets the infrastructure match the workload.\n\nFor a simple internal MCP service:\n\n```\ntransport: httpApi\nstreaming: false\n```\n\nFor an MCP server that needs API Gateway capabilities and real streaming:\n\n```\ntransport: http\nstreaming: true\n```\n\nAnd for cases where a Function URL is sufficient:\n\n```\ntransport: url\nstreaming: true\n```\n\nThe application doesn't need a new MCP abstraction for each one.\n\nStreaming and transport are intentionally separate choices.\n\nFor example:\n\n```\ncustom:\n  pythonMcp:\n    servers:\n      orders:\n        handler: app.handler\n        transport: http\n        streaming: false\n```\n\nuses API Gateway REST API but invokes the Python Lambda normally.\n\nThe application uses:\n\n```\napp = APIGatewayRestResolver()\n```\n\nThis can be useful when REST API features are desired but incremental MCP streaming isn't.\n\nThe deployment model therefore isn't simply:\n\n```\nHTTP API = simple\nREST API = streaming\n```\n\nIt's more accurately:\n\n```\n                       Buffered       Streaming\n\nHTTP API v2               ✓               -\n\nREST API v1               ✓               ✓\n\nFunction URL              ✓               ✓\n```\n\nEach server can expose its own path:\n\n```\ncustom:\n  pythonMcp:\n    servers:\n      orders:\n        handler: orders.handler\n        transport: http\n        streaming: true\n        path: /orders/mcp\n\n      inventory:\n        handler: inventory.handler\n        transport: http\n        streaming: true\n        path: /inventory/mcp\n```\n\nThe Python application registers the same path:\n\n```\napp.include_mcp(\n    mcp,\n    path=\"/orders/mcp\",\n)\n```\n\nServers using API Gateway can share the underlying API while keeping separate Lambda functions and MCP endpoints.\n\nThat makes it possible to expose multiple domain capabilities without building one giant MCP server.\n\nThe plugin also deliberately doesn't invent an MCP-specific authentication model.\n\nFor HTTP API, existing Serverless authorizer configuration can be used.\n\nFor REST API, the plugin passes authorizer configuration through to the normal Serverless REST API event compiler.\n\nFunction URLs can use their supported public or AWS IAM modes.\n\nSo the architecture remains:\n\n```\nMCP Client\n    ↓\nAWS authentication / authorizer\n    ↓\nMCP transport\n    ↓\nmiddleware / application authorization\n    ↓\ntool\n```\n\nThis keeps authentication independent from the MCP programming model.\n\nAn important lesson from building this was that streaming shouldn't become the default architecture just because MCP supports it.\n\nFor many servers:\n\n```\nHTTP API v2\n+\nLambda\n+\nmodmex-lambda\n```\n\nis enough.\n\nIt's lightweight and fits the serverless execution model extremely well.\n\nStreaming becomes useful when the operation actually has intermediate information worth delivering.\n\nThen we can move to:\n\n```\nREST API\n+\nLambda response streaming\n+\nLambda Web Adapter\n+\nmodmex-lambda\n```\n\nwithout redesigning tools, resources, prompts, middleware, or application services.\n\nThere is one serverless behavior worth understanding.\n\nA client disconnect doesn't guarantee that the Lambda invocation immediately stops.\n\nThere are multiple network boundaries between the MCP client and the Python process:\n\n```\nMCP Client\n    ↓\nAPI Gateway\n    ↓\nLambda\n    ↓\nLambda Web Adapter\n    ↓\nPython\n```\n\nWhen the transport can observe a disconnect, `modmex-lambda`\n\ncan propagate cooperative cancellation through `MCPContext`\n\n.\n\nA long-running tool can therefore check:\n\n```\nif ctx.cancelled:\n    return {\"status\": \"cancelled\"}\n```\n\nBut applications shouldn't assume that every downstream network failure will immediately terminate a running Lambda invocation.\n\nResponse streaming and distributed execution cancellation are separate concerns.\n\nThe final architecture looks like this:\n\n```\n                         MCPServer\n                            │\n             ┌──────────────┴──────────────┐\n             │                             │\n             ▼                             ▼\n      Buffered execution             Streamable execution\n             │                             │\n      regular Lambda                  HTTP process\n             │                             │\n             ▼                             ▼\n   HTTP API / REST / URL            Lambda Web Adapter\n                                           │\n                                           ▼\n                                    Lambda streaming\n```\n\nThe important part is what doesn't change:\n\n```\ntools\nresources\nprompts\ndependency injection\nmiddleware\napplication services\n```\n\nStreaming is an infrastructure capability, not a new application architecture.\n\nThe complete implementation is available in two projects:\n\n**modmex-lambda** contains the Python MCP runtime and application integration.\n\nIt provides the MCP server, tools, resources, prompts, middleware, dependency injection, protocol validation, buffered HTTP transports, and Streamable HTTP support.\n\n**serverless-python-mcp** provides the Serverless Framework deployment integration.\n\nIt creates MCP Lambda functions from `custom.pythonMcp.servers`\n\nand configures the appropriate AWS front door and runtime behavior for buffered or streaming execution.\n\nThe design goal behind both projects is straightforward:\n\nYou don't need streaming to run MCP on Lambda. But when you need it, you shouldn't have to rewrite your MCP server.\n\nStart with the smallest architecture that works.\n\nAdd streaming when the workload earns the complexity.\n\nKeep the Python application the same.", "url": "https://wpnews.pro/news/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python", "canonical_source": "https://dev.to/clandro89/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python-1hab", "published_at": "2026-08-12 23:53:24+00:00", "updated_at": "2026-08-13 00:15:45.751717+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["AWS Lambda", "API Gateway", "modmex-lambda", "MCP"], "alternates": {"html": "https://wpnews.pro/news/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python", "markdown": "https://wpnews.pro/news/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python.md", "text": "https://wpnews.pro/news/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python.txt", "jsonld": "https://wpnews.pro/news/building-lightweight-and-streamable-mcp-servers-on-aws-lambda-with-python.jsonld"}}