├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── lambda-ops-mcp-server ├── .gitignore ├── README.md ├── index.js ├── mcp-server.js ├── package-lock.json ├── package.json ├── tools │ ├── get-function.js │ ├── get-runtimes-info.js │ ├── invoke-function.js │ ├── list-functions.js │ └── update-function-runtime.js ├── utils │ ├── lambdaClient.js │ ├── logging.js │ └── runtimes-page-parser.js └── video.png ├── logo.png ├── stateful-mcp-on-ecs-nodejs ├── README.md ├── architecture.png ├── publish-to-ecr.sh ├── src │ ├── mcpclient │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── mcpserver │ │ ├── Dockerfile │ │ ├── index.js │ │ ├── logging.js │ │ ├── mcp-errors.js │ │ ├── mcp-server.js │ │ ├── metadata.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── transport.js └── terraform │ ├── alb.tf │ ├── ecs.tf │ ├── locals.tf │ ├── outputs.tf │ ├── provider.tf │ ├── route53.tf │ └── vpc.tf ├── stateless-mcp-on-ecs-nodejs ├── README.md ├── architecture.png ├── publish-to-ecr.sh ├── src │ ├── mcpclient │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── mcpserver │ │ ├── Dockerfile │ │ ├── index.js │ │ ├── logging.js │ │ ├── mcp-errors.js │ │ ├── mcp-server.js │ │ ├── metadata.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── transport.js └── terraform │ ├── alb.tf │ ├── ecs.tf │ ├── locals.tf │ ├── outputs.tf │ ├── provider.tf │ ├── route53.tf │ └── vpc.tf ├── stateless-mcp-on-lambda-nodejs ├── README.md ├── architecture.png ├── src │ ├── authorizer │ │ ├── index.js │ │ └── package.json │ ├── mcpclient │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── mcpserver │ │ ├── index.js │ │ ├── logging.js │ │ ├── mcp-errors.js │ │ ├── mcp-server.js │ │ ├── metadata.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── run.sh │ │ └── transport.js └── terraform │ ├── apigateway.tf │ ├── lambda_authorizer.tf │ ├── lambda_mcpserver.tf │ ├── locals.tf │ ├── outputs.tf │ └── provider.tf └── stateless-mcp-on-lambda-python ├── .gitignore ├── README.md ├── etc └── environment.sh ├── makefile ├── sam ├── layer.yaml ├── openapi.yaml └── template.yaml └── src ├── dependencies └── requirements.txt └── mcpserver ├── echo.py ├── run.sh └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .terraform 4 | .terraform.lock.hcl 5 | terraform.tfstate 6 | terraform.tfstate.backup 7 | tmp -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Serverless MCP Servers 2 | 3 | ![](logo.png) 4 | 5 | This repo contains a collection of sample implementations of MCP Servers. 6 | 7 | | Directory | Runtime | IaC | Description | 8 | |---|---|---|---| 9 | | [stateless-mcp-on-lambda-nodejs](./stateless-mcp-on-lambda-nodejs) | Node.js | Terraform | A sample implementation of a remote stateless MCP Server running natively on AWS Lambda and Amazon API Gateway | 10 | | [stateless-mcp-on-lambda-python](./stateless-mcp-on-lambda-python) | Python | SAM | A sample implementation of a remote stateless MCP Server running natively on AWS Lambda and Amazon API Gateway | 11 | | [stateless-mcp-on-ecs-nodejs](./stateless-mcp-on-ecs-nodejs) | Node.js | Terraform | A sample implementation of a remote stateless MCP Server running natively on Amazon ECS with Application Load Balancer | 12 | | [stateful-mcp-on-ecs-nodejs](./stateful-mcp-on-ecs-nodejs) | Node.js | Terraform | A sample implementation of a remote stateful MCP Server running natively on Amazon ECS with Application Load Balancer | 13 | | [lambda-ops-mcp-server](./lambda-ops-mcp-server) | Node.js | Terraform | A demo PoC of a local MCP Server that can be used for discovering and upgrading functions on deprecated runtimes | 14 | 15 | ## Stateful VS Stateless MCP Servers 16 | 17 | When using [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) with MCP Servers/Clients, it’s important to understand the difference and trade-offs between stateful and stateless MCP server implementations. Each model has implications for scalability, connection handling, and session management. 18 | 19 | #### Key Aspects 20 | 21 | The [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) specification outlines two important capabilities. First, a client may [initiate a long-lived HTTP GET request](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server) to establish a persistent SSE (Server-Sent Events) connection, allowing the server to push data even when the client hasn’t sent a POST request. Second, if the connection is interrupted, the client should be able to [resume communication by reconnecting through another GET request](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#resumability-and-redelivery). Both features imply that the server must be able to maintain a persistent session context and support long-lived connections. Read more about Streamable HTTP Transport session management [here](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management). 22 | 23 | #### Stateful model challenges 24 | 25 | In a stateful MCP server, this means maintaining session state in memory and keeping SSE connections alive over time. However, as of early May 2025, none of the official MCP SDKs support external session persistence (e.g. in Redis or DynamoDB). This limitation makes it difficult to scale stateful servers horizontally. For example, a client might establish a session with one server instance, but if subsequent requests are routed to a different instance, the session context will be lost and the connection will fail. 26 | 27 | That said, it is still possible to scale stateful MCP servers if you're willing to configure session affinity, also known as sticky sessions, at the load balancer level. For example, cookie-based sticky sessions can ensure that a client is routed to the same server instance for the duration of a session. However, the TypeScript MCP Client SDK currently relies on the `fetch` API, which doesn’t natively support cookies. To work around this limitation, you'll need to implement manual cookie handling, as shown in the `stateful-mcp-on-ecs` example. 28 | 29 | ![](./stateful-mcp-on-ecs/architecture.png) 30 | 31 | #### Stateless to the rescue 32 | 33 | The MCP specification also allows for a stateless server mode. In this mode, the server doesn’t maintain session context between requests, and clients are not expected to resume dropped connections. Stateless mode enables seamless horizontal scaling and works well in environments where elasticity and load distribution are critical. This model is demonstrated in the `stateless-mcp-on-lambda`, `stateless-mcp-on-ecs`, and `stateless-mcp-on-lambda-python` samples included in this repository. 34 | 35 | ![](./stateless-mcp-on-lambda/architecture.png) 36 | 37 | 38 | ![](./stateless-mcp-on-ecs/architecture.png) 39 | 40 | 41 | Refer to each folder separately for further instructions and deployment steps. 42 | 43 | ## License 44 | 45 | This library is licensed under the MIT-0 License. See the LICENSE file. 46 | 47 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | server.log 2 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # Sample Lambda Ops MCP Server 2 | 3 | This sample demonstrates how to build an MCP Server to support AWS Lambda function operations. Specifically, it showcases the discovery of Lambda functions running on deprecated or soon-to-be-deprecated runtimes and provides a workflow for upgrading them to a supported runtime version. The implementation can be extended to support additional operational scenarios based on your environment's requirements. 4 | 5 | IMPORTANT: Use this MCP Server for sample and education purposes only. THIS TOOL MAKES CHANGES TO FUNCTIONS IN YOUR ACCOUNT. DO NOT USE IT FOR YOUR PRODUCTION ENVIRONMENTS. 6 | 7 | ## Demo 8 | 9 | [![](video.png)](https://www.youtube.com/watch?v=Lf5zdo80T-I) 10 | 11 | ## Installation 12 | 13 | Make sure you have [AWS CLI](https://aws.amazon.com/cli/) installed and configured. This MCP Server uses credentials configured in the AWS SDK profile. 14 | 15 | 1. Clone this repo 16 | 2. Update your AI Assistant's MCP configuration file: 17 | 18 | ```json 19 | { 20 | "mcpServers": { 21 | "lambda-ops": { 22 | "command":"node", 23 | "args": [ 24 | "/{path-to-where-you-cloned-the-repo}/sample-serverless-mcp-servers/lambda-ops-mcp-server/index.js" 25 | ] 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 3. Restart your Agentic AI tool to pick up the new configuration. 32 | 33 | 4. Start asking questions about Lambda functions in your account. 34 | 35 | ## License 36 | 37 | This library is licensed under the MIT-0 License. See the LICENSE file. 38 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/index.js: -------------------------------------------------------------------------------- 1 | import './utils/logging.js'; 2 | import log4js from 'log4js'; 3 | const l = log4js.getLogger(); 4 | l.info('starting...'); 5 | 6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 7 | import mcpServer from './mcp-server.js'; 8 | const transport = new StdioServerTransport(); 9 | await mcpServer.connect(transport); 10 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/mcp-server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import getRuntimesInfo from "./tools/get-runtimes-info.js"; 3 | import listFunctions from "./tools/list-functions.js"; 4 | import getFunction from "./tools/get-function.js"; 5 | import invokeFunction from "./tools/invoke-function.js"; 6 | import updateFunctionRuntime from "./tools/update-function-runtime.js"; 7 | 8 | const server = new McpServer({ 9 | name: "AWS Lambda Operations MCP Server", 10 | version: "0.0.1" 11 | }, { 12 | capabilities: { 13 | tools: {}, 14 | resources: {} 15 | }, 16 | instructions: 17 | 'Use this MCP server to retrieve various information about AWS Lambda functions \ 18 | and perform runtime updates. \ 19 | This MCP server allows to retrieve list of lambda function, get information about \ 20 | specific functions, retrieve information about Lambda runtimes deprecation dates, \ 21 | and update Lambda function runtime versions. \ 22 | Always start using this server by running the get-runtimes-info tool to get the most\ 23 | up-to-date information about supported and deprecated runtimes.' 24 | }); 25 | 26 | server.tool( 27 | getRuntimesInfo.toolName, 28 | getRuntimesInfo.toolDescription, 29 | getRuntimesInfo.toolParamsSchema, 30 | getRuntimesInfo.toolCallback); 31 | 32 | server.tool( 33 | listFunctions.toolName, 34 | listFunctions.toolDescription, 35 | listFunctions.toolParamsSchema, 36 | listFunctions.toolCallback); 37 | 38 | server.tool( 39 | getFunction.toolName, 40 | getFunction.toolDescription, 41 | getFunction.toolParamsSchema, 42 | getFunction.toolCallback); 43 | 44 | server.tool( 45 | invokeFunction.toolName, 46 | invokeFunction.toolDescription, 47 | invokeFunction.toolParamsSchema, 48 | invokeFunction.toolCallback); 49 | 50 | server.tool( 51 | updateFunctionRuntime.toolName, 52 | updateFunctionRuntime.toolDescription, 53 | updateFunctionRuntime.toolParamsSchema, 54 | updateFunctionRuntime.toolCallback); 55 | 56 | export default server; 57 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-mcp-server", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "keywords": [], 6 | "author": "Anton Aleksandrov", 7 | "license": "Apache-2.0", 8 | "description": "A sample MCP Server", 9 | "dependencies": { 10 | "@aws-sdk/client-lambda": "^3.787.0", 11 | "@modelcontextprotocol/sdk": "^1.11.0", 12 | "express": "^5.1.0", 13 | "jquery": "^3.7.1", 14 | "jsdom": "^26.1.0", 15 | "log4js": "^6.9.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/tools/get-function.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import lambdaClient from '../utils/lambdaClient.js'; 5 | import { z } from "zod"; 6 | 7 | const toolName = "get-function"; 8 | 9 | const toolDescription = 10 | "1. This tool can be used for retrieving information about one specific Lambda function. \ 11 | 2. A Function Name parameter MUST be supplied by the caller. \ 12 | 3. Whenever you need to make any changes to function configuration, such as updating \ 13 | a runtime version, ALWAYS use this tool first to get the configuration state before updating \ 14 | in order to be able to roll back changes in case the update will break function functionality. "; 15 | 16 | const toolParamsSchema = { 17 | functionName: z.string().describe("Name of the Lambda function to retrieve information about.") 18 | } 19 | 20 | const toolCallback = async ({ functionName }) => { 21 | l.debug(`> functionName=${functionName}`); 22 | 23 | const fn = await lambdaClient.getFunction(functionName); 24 | 25 | l.debug(`< returning response`); 26 | return { 27 | content: [{ 28 | type: 'text', 29 | text: JSON.stringify(fn) 30 | }] 31 | } 32 | } 33 | 34 | export default { 35 | toolName, 36 | toolDescription, 37 | toolParamsSchema, 38 | toolCallback 39 | } -------------------------------------------------------------------------------- /lambda-ops-mcp-server/tools/get-runtimes-info.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import runtimePageParser from '../utils/runtimes-page-parser.js'; 5 | runtimePageParser.bootstrap(); 6 | 7 | const toolName = "get-runtimes-info"; 8 | 9 | const toolDescription = 10 | "Use this tool to retrieve most up-to-date information about Lambda runtimes support and deprecation dates. \ 11 | This tool will return a JSON array which contains information about runtimes with deprecation dates." 12 | 13 | const toolParamsSchema = {}; 14 | 15 | const toolCallback = async ({}) => { 16 | l.debug(`>`); 17 | 18 | const runtimesInfo = await runtimePageParser.get(); 19 | 20 | return { 21 | content: [{ 22 | type: 'text', 23 | text: JSON.stringify(runtimesInfo) 24 | }] 25 | } 26 | } 27 | 28 | export default { 29 | toolName, 30 | toolDescription, 31 | toolParamsSchema, 32 | toolCallback 33 | } -------------------------------------------------------------------------------- /lambda-ops-mcp-server/tools/invoke-function.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import lambdaClient from '../utils/lambdaClient.js'; 5 | import { z } from "zod"; 6 | 7 | const toolName = "invoke-function"; 8 | 9 | const toolDescription = 10 | "1. This tool can be used for invoking Lambda functions. \ 11 | 2. A functionName parameter MUST be supplied by the caller. \ 12 | 3. A payload parameter MUST be supplied by the caller. Ask explicitly if needed."; 13 | 14 | const toolParamsSchema = { 15 | functionName: z.string().describe("Name of the Lambda function to retrieve information about."), 16 | payload: z.string().describe("The JSON payload to be sent to the Lambda function.") 17 | } 18 | 19 | const toolCallback = async ({ functionName, payload }) => { 20 | l.debug(`> functionName=${functionName}, payload=${payload}`); 21 | 22 | const resp = await lambdaClient.invokeFunction(functionName, payload); 23 | 24 | l.debug(`< returning response`); 25 | return { 26 | content: [{ 27 | type: 'text', 28 | text: JSON.stringify(resp) 29 | }] 30 | } 31 | } 32 | 33 | export default { 34 | toolName, 35 | toolDescription, 36 | toolParamsSchema, 37 | toolCallback 38 | } -------------------------------------------------------------------------------- /lambda-ops-mcp-server/tools/list-functions.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import lambdaClient from '../utils/lambdaClient.js'; 5 | import { z } from "zod"; 6 | 7 | const toolName = "list-functions"; 8 | 9 | const toolDescription = 10 | "1. This tool can be used for retrieving a list of AWS Lambda functions. \ 11 | 2. This tool should be used to answer questions about what functions are available \ 12 | in user's account, as well as questions about specific properties of these functions. \ 13 | 3. This tool should not be used when you need information about one specific function. \ 14 | If you need information about one specific function, use the get-function tool instead. \ 15 | 4. This tool supports optional pagination via the marker parameter. If you see a 'marker' \ 16 | property in the tool response, you SHOULD use it to make another request to the tool to \ 17 | continue building the list. When 'marker' property is not present in the tool response, \ 18 | it means you've reached the last page of results and you should stop retrieving more functions. \ 19 | 5. When you're using this tool for the first time, you obviously do not have \ 20 | marker yet, so supply an empty string as a value instead \ 21 | 5. This tool returns a JSON object with two elements. The first element is an array of \ 22 | functions. The second element is a marker that can be used for further pagination. \ 23 | "; 24 | 25 | const toolParamsSchema = { 26 | marker: z.string().optional().describe( 27 | "Pagination marker. Send empty string for the first \ 28 | request, when previous marker is not yet available." 29 | ) 30 | }; 31 | 32 | 33 | const toolCallback = async ({ marker }) => { 34 | l.debug(`> marker=${marker?.substring(0,20)}`); 35 | 36 | const {functions, nextMarker} = await lambdaClient.listFunctions(marker) 37 | 38 | const response = JSON.stringify({ 39 | functions, 40 | marker: nextMarker 41 | }); 42 | 43 | l.debug(`< returning response functions.length=${functions.length}`); 44 | 45 | return { 46 | content: [{ 47 | type: 'text', 48 | text: response 49 | }] 50 | } 51 | } 52 | 53 | export default { 54 | toolName, 55 | toolDescription, 56 | toolParamsSchema, 57 | toolCallback 58 | } -------------------------------------------------------------------------------- /lambda-ops-mcp-server/tools/update-function-runtime.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import lambdaClient from '../utils/lambdaClient.js'; 5 | import { z } from "zod"; 6 | 7 | const toolName = "update-function-runtime"; 8 | 9 | const toolDescription = 10 | "1. This tool can be used for updating the runtime version used by a Lambda function. \ 11 | You MUST supply two parameters as explained below. \ 12 | 2. A 'functionName' parameter value MUST be supplied by the caller. \ 13 | 3. A 'runtime' parameter value MUST be spllied by the caller. \ 14 | 4. After updating a function runtime, you SHOULD ALWAYS invoke the function \ 15 | in order to validate it still works. \ 16 | 5. Use invoke-function tool to test if the function can still be invoked. If invocation fails - \ 17 | ALWAYS rollback to the previous runtime version automatically. Do not attempt to fix code until you \ 18 | get to a state when function is working again.\ 19 | 6. When rolling back, check if there's an in-between runtime version that you can try upgrading to. \ 20 | For example, if you upgraded from runtime version 1 to version 3, and version 3 is failing, try \ 21 | downgrading to version 2 first, and so on. If it still fails, go back to the original runtime version. \ 22 | 7. When user is asking to downgrade runtime version to a version you know is either deprecated or \ 23 | soon-to-be-deprecated, ALWAYS confirm whether user actually wants to do it. \ 24 | "; 25 | 26 | const toolParamsSchema = { 27 | functionName: z.string().describe("Name of the Lambda function to retrieve information about."), 28 | runtime: z.string().describe("The new runtime version to be used by the Lambda function.") 29 | } 30 | 31 | const toolCallback = async ({ functionName, runtime }) => { 32 | l.debug(`> functionName=${functionName} runtime=${runtime}`); 33 | 34 | const fn = await lambdaClient.updateFunctionRuntime(functionName, runtime); 35 | 36 | l.debug(`< returning response`); 37 | return { 38 | content: [{ 39 | type: 'text', 40 | text: JSON.stringify(fn) 41 | }] 42 | } 43 | } 44 | 45 | export default { 46 | toolName, 47 | toolDescription, 48 | toolParamsSchema, 49 | toolCallback 50 | } -------------------------------------------------------------------------------- /lambda-ops-mcp-server/utils/lambdaClient.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | import { 3 | LambdaClient, 4 | GetFunctionCommand, 5 | ListFunctionsCommand, 6 | UpdateFunctionConfigurationCommand, 7 | InvokeCommand 8 | } from "@aws-sdk/client-lambda"; 9 | 10 | const l = log4js.getLogger(); 11 | const REGION = process.env.AWS_REGION || "us-east-1"; 12 | const lambdaClient = new LambdaClient({ region: REGION }); 13 | 14 | async function listFunctions(marker){ 15 | l.debug(`> marker=${marker?.substring(0,20)}`); 16 | 17 | const command = new ListFunctionsCommand({ 18 | Marker: marker || null, 19 | MaxItems: 50 20 | }); 21 | 22 | const lambdaResponse = await lambdaClient.send(command); 23 | // console.log(lambdaResponse.NextMarker); 24 | 25 | const functions = []; 26 | for (const func of lambdaResponse.Functions) { 27 | // if (functions.length==3) break; 28 | 29 | functions.push({ 30 | name: func.FunctionName, 31 | runtime: func.Runtime, 32 | lastModified: func.LastModified, 33 | arn: func.FunctionArn, 34 | // memorySize: func.MemorySize, 35 | // timeout: func.Timeout, 36 | // arch: func.Architectures[0] 37 | }); 38 | } 39 | return { 40 | functions, 41 | nextMarker: lambdaResponse.NextMarker 42 | }; 43 | } 44 | 45 | async function getFunction(name) { 46 | l.debug(`> name=${name}`); 47 | const command = new GetFunctionCommand({ 48 | FunctionName: name 49 | }); 50 | 51 | const response = await lambdaClient.send(command); 52 | return { 53 | config: response.Configuration, 54 | tags: response.Tags, 55 | concurrency: response.Concurrency, 56 | }; 57 | } 58 | 59 | async function updateFunctionRuntime(name, runtime) { 60 | l.debug(`> name=${name} runtime=${runtime}`); 61 | const command = new UpdateFunctionConfigurationCommand({ 62 | FunctionName: name, 63 | Runtime: runtime 64 | }); 65 | 66 | const response = await lambdaClient.send(command); 67 | return response; 68 | } 69 | 70 | async function invokeFunction(name, payload) { 71 | l.debug(`> name=${name} payload=${payload}`); 72 | const command = new InvokeCommand({ 73 | FunctionName: name, 74 | Payload: payload 75 | }); 76 | 77 | const response = await lambdaClient.send(command); 78 | 79 | return { 80 | statusCode: response.StatusCode, 81 | error: response.FunctionError, 82 | payload: Buffer.from(response.Payload).toString('utf-8') 83 | }; 84 | } 85 | 86 | export default { 87 | listFunctions, 88 | getFunction, 89 | updateFunctionRuntime, 90 | invokeFunction 91 | }; 92 | 93 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/utils/logging.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | const LOG_FILE = `${import.meta.dirname}/../server.log`; 5 | 6 | const layout = { 7 | type: 'pattern', 8 | pattern: '%[%p [%f{1}:%l:%M] %m%]' 9 | } 10 | 11 | log4js.configure({ 12 | appenders: { 13 | stdout: { 14 | type: 'stdout', 15 | enableCallStack: true, 16 | layout 17 | }, 18 | file: { 19 | type: 'file', 20 | filename: LOG_FILE, 21 | enableCallStack: true, 22 | layout 23 | } 24 | }, 25 | categories: { 26 | default: { 27 | // appenders: ['stdout'], 28 | appenders: ['file'], 29 | level: 'debug', 30 | enableCallStack: true 31 | } 32 | } 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /lambda-ops-mcp-server/utils/runtimes-page-parser.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const l = log4js.getLogger(); 3 | 4 | import {JSDOM} from 'jsdom'; 5 | import $ from 'jquery'; 6 | 7 | const deprecatedRuntimes = []; 8 | 9 | const LAMBDA_RUNTIMES_DOC_URL = 'https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html'; 10 | 11 | const bootstrap = async ()=>{ 12 | l.debug('>'); 13 | const html = await getLambdaRuntimesHtml(); 14 | await parseLambdaRuntimesHtml(html); 15 | return; 16 | } 17 | 18 | const get = async()=>{ 19 | l.debug('>'); 20 | if (!deprecatedRuntimes || deprecatedRuntimes.length===0) { 21 | await bootstrap(); 22 | } 23 | return deprecatedRuntimes; 24 | } 25 | 26 | const getLambdaRuntimesHtml = async () => { 27 | l.debug(`retrieving from ${LAMBDA_RUNTIMES_DOC_URL}`); 28 | 29 | const resp = await fetch(LAMBDA_RUNTIMES_DOC_URL); 30 | if (resp.status !== 200) { 31 | l.error(`unable to retrieve data from ${LAMBDA_RUNTIMES_DOC_URL}`); 32 | l.error(`resp.status=${resp.status} resp.statusText=${resp.statusText}`); 33 | return ''; 34 | } 35 | const html = await resp.text(); 36 | l.debug(`retrieved successfully html.length=${html.length}`); 37 | return html; 38 | } 39 | 40 | const parseLambdaRuntimesHtml = async (html)=> { 41 | l.debug(`parsing html.length=${html.length}]`); 42 | const dom = new JSDOM(html); 43 | const $window = $(dom.window); 44 | 45 | const $tableContainers = $window('.table-container'); 46 | const $supportedRuntimesTable = $tableContainers.eq(0); 47 | const $deprecatedRuntimesTable = $tableContainers.eq(2); 48 | l.debug(`parsed successfully`); 49 | 50 | l.debug(`processing tables`); 51 | processTable($supportedRuntimesTable); 52 | processTable($deprecatedRuntimesTable); 53 | l.debug(`success, found ${deprecatedRuntimes.length} runtimes with deprecation dates`); 54 | } 55 | 56 | const processTable = ($sourceTable) => { 57 | const $tableRows = $sourceTable.find('table').find('tbody').find('tr'); 58 | for (let i = 0; i < $tableRows.length; i++) { 59 | const $tableRow = $tableRows.eq(i); 60 | const $tableCells = $tableRow.find('td'); 61 | const name = $tableCells.eq(0).text().trim(); 62 | const id = $tableCells.eq(1).text().trim(); 63 | const os = $tableCells.eq(2).text().trim(); 64 | const deprecationDate = $tableCells.eq(3).text().trim(); 65 | const blockFunctionCreateDate = $tableCells.eq(4).text().trim(); 66 | const blockFunctionUpdateDate = $tableCells.eq(5).text().trim(); 67 | 68 | if (deprecationDate || blockFunctionUpdateDate || blockFunctionCreateDate) { 69 | deprecatedRuntimes.push({ 70 | name, 71 | id, 72 | os, 73 | deprecationDate, 74 | blockFunctionCreateDate, 75 | blockFunctionUpdateDate, 76 | }); 77 | } 78 | } 79 | 80 | } 81 | 82 | 83 | export default {bootstrap, get}; -------------------------------------------------------------------------------- /lambda-ops-mcp-server/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-mcp-servers/c276bc74d5d60b2875c44a2423de094e72aad4ee/lambda-ops-mcp-server/video.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-mcp-servers/c276bc74d5d60b2875c44a2423de094e72aad4ee/logo.png -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Stateful MCP Server on ECS Fargate 2 | 3 | This is a sample MCP Server running natively on ECS Fargate and ALB without any extra bridging components or custom transports. This is now possible thanks to the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport introduced in v2025-03-26. 4 | 5 | ![](architecture.png) 6 | 7 | ## Prereqs 8 | 9 | * AWS CLI 10 | * Terraform 11 | 12 | ## Instructions 13 | 14 | ### Clone the project 15 | 16 | ```bash 17 | git clone https://github.com/aws-samples/sample-serverless-mcp-servers.git 18 | cd sample-serverless-mcp-servers/stateful-mcp-on-ecs-nodejs 19 | ``` 20 | 21 | ### Install dependencies 22 | 23 | ```bash 24 | (cd src/mcpclient && npm install) 25 | (cd src/mcpserver && npm install) 26 | ``` 27 | 28 | ### Тest the server locally 29 | 30 | ```bash 31 | node src/mcpserver/index.js 32 | ``` 33 | 34 | Once the server is running, run client in a separate terminal window 35 | 36 | ```bash 37 | node src/mcpclient/index.js 38 | ``` 39 | 40 | ### Build and upload image to ECR 41 | 42 | Update `publish-to-ecr.sh` with your ECR alias, and run the script to build and deploy MCP Server image to ECR. 43 | 44 | ```bash 45 | ./publish-to-ecr.sh 46 | ``` 47 | 48 | ### Deploy to AWS with Terraform 49 | 50 | Update `terraform/locals.tf` with your ECR alias. Optionally, update region, VPC configuraion, and Route53 Zone Name if you have a DNS name registered to use for SSL certificate (see HTTPS considerations below for details). 51 | 52 | Run below commands to deploy the sample to AWS 53 | 54 | ```bash 55 | cd terraform 56 | terraform init 57 | terraform plan 58 | terraform apply 59 | export MCP_SERVER_ENDPOINT=$(terraform output --raw mcp_endpoint) 60 | cd .. 61 | ``` 62 | 63 | Deployment takes 3-4 minutes. Once Terraform deployment has completed, it will take 2-3 more minutes for ECS tasks to spin up and get recognized by the ALB target group. 64 | 65 | ### Test your remote MCP Server with MCP client: 66 | ```bash 67 | node src/mcpclient/index.js 68 | ``` 69 | 70 | Observe the response: 71 | ```bash 72 | Connecting ENDPOINT_URL=http://stateful-mcp-on-ecs-1870111106.us-east-1.elb.amazonaws.com/mcp 73 | connected 74 | listTools response: { tools: [ { name: 'ping', inputSchema: [Object] } ] } 75 | callTool:ping response: { 76 | content: [ 77 | { 78 | type: 'text', 79 | text: 'pong! taskId=task/stateful-mcp-on-ecs/6907042c100c4adf80c2d0957c38706f v=0.0.10 d=101' 80 | } 81 | ] 82 | } 83 | callTool:ping response: { 84 | content: [ 85 | { 86 | type: 'text', 87 | text: 'pong! taskId=task/stateful-mcp-on-ecs/6907042c100c4adf80c2d0957c38706f v=0.0.10 d=50' 88 | } 89 | ] 90 | } 91 | ``` 92 | 93 | ## Statefull vs Stateless considerations 94 | MCP Server can run in two modes - stateless and stateful. This repo demonstrates the stateful mode. 95 | 96 | Stateful mode implies a persistent SSE connection established between MCP Client and MCP Server. This connection is used for MCP Server to be able to support resumability and proactively send notifications to MCP Clients. This works fine when you have a single instance of MCP Server running (e.g a single ECS Task). This does not work out-of-the-box if you want to have more than one ECS Task since a session will be established with one task, but subsequent requests may hit a different task. 97 | 98 | As of building this sample (early May 2025), the TypeScript implementation of MCP Server SDK does not support externalizing session info, meaning session cannot be synchronized across different server instances. 99 | 100 | It is possible to address this concern by using ALB with cookie-based sticky sessions, which will insure that requests for a session established with a particular task will always be forwarded to the same task. However, MCP Client SDK does not support cookies by default. To address this concern, this sample injects cookie support into `fetch`, the framework MCP Client uses under-the-hood for HTTP communications (see `src/mcpclient/index.js`) 101 | 102 | See more info about this in `terraform/ecs.tf` 103 | 104 | ## HTTPS considerations 105 | 106 | By default, this sample uses the default ALB endpoint, which is HTTP only. See a comment in terraform/alb.tf for instructions how to enable HTTPS. 107 | 108 | Only use HTTP for testing purposes ONLY!!! NEVER expose ANYTHING via plain HTTP, always use HTTPS!!! 109 | 110 | ## Cost considerations 111 | 112 | This sample provisions paid resources in your account, such as ECS Tasks and ALB. Remember to delete these resources when you're done evaluating. 113 | 114 | ```bash 115 | terraform destroy 116 | ``` 117 | 118 | ## Learn about mcp 119 | [Intro](https://modelcontextprotocol.io/introduction) 120 | 121 | [Protocol specification](https://modelcontextprotocol.io/specification/2025-03-26) 122 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-mcp-servers/c276bc74d5d60b2875c44a2423de094e72aad4ee/stateful-mcp-on-ecs-nodejs/architecture.png -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/publish-to-ecr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ECR_REPO=stateful-mcp-on-ecs 3 | ECR_IMAGE_TAG=latest 4 | ECR_ALIAS=your-ecr-alias-here 5 | ECR_REPO_URI=public.ecr.aws/${ECR_ALIAS}/$ECR_REPO:$ECR_IMAGE_TAG 6 | 7 | echo Logging in... 8 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws 9 | 10 | echo Incrementing app version... 11 | cd src/mcpserver 12 | npm version patch 13 | 14 | echo Building image and publishing to Public ECR... 15 | aws ecr-public create-repository --repository-name $ECR_REPO --no-cli-pager 16 | docker buildx build --platform linux/amd64 --provenance=false -t $ECR_REPO_URI . --push 17 | 18 | echo All done! 19 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpclient/index.js: -------------------------------------------------------------------------------- 1 | // As of building this sample (early May 2025), MCP Client SDK does not support 2 | // cookie-based sticky sessions. In case you want to run more than one 3 | // instance of MCP Servers (multiple ECS tasks), you need to add cookie 4 | // support to `fetch`, the underlying framework that MCP Client uses for HTTP. 5 | // Note that other clients, e.g. MCP Inspector, do not have this patch 6 | // implemented so they will not work when you have more than one instance 7 | // of MCP Server (==ECS Task) running. 8 | // A potential alternative is to run your MCP Servers in stateless mode, 9 | // see stateless-mcp-on-ecs and stateless-mcp-on-lambda samples in this repo. 10 | // See big comment in ecs.tf for more details. 11 | import fetchCookie from 'fetch-cookie'; 12 | fetch = fetchCookie(fetch); 13 | 14 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 15 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 16 | 17 | const ENDPOINT_URL = process.env.MCP_SERVER_ENDPOINT || 'http://localhost:3000/mcp'; 18 | 19 | console.log(`Connecting ENDPOINT_URL=${ENDPOINT_URL}`); 20 | 21 | const transport = new StreamableHTTPClientTransport(new URL(ENDPOINT_URL)); 22 | 23 | const client = new Client({ 24 | name: "node-client", 25 | version: "0.0.1" 26 | }) 27 | 28 | await client.connect(transport); 29 | console.log('connected'); 30 | 31 | const tools = await client.listTools(); 32 | console.log(`listTools response: `, tools); 33 | 34 | for (let i = 0; i < 2; i++) { 35 | let result = await client.callTool({ 36 | name: "ping" 37 | }); 38 | console.log(`callTool:ping response: `, result); 39 | } 40 | 41 | await client.close(); 42 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpclient/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcpclient", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "license": "Apache-2.0", 8 | "dependencies": { 9 | "@modelcontextprotocol/sdk": "^1.11.0", 10 | "fetch-cookie": "^3.1.0" 11 | } 12 | }, 13 | "node_modules/@modelcontextprotocol/sdk": { 14 | "version": "1.11.0", 15 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", 16 | "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", 17 | "license": "MIT", 18 | "dependencies": { 19 | "content-type": "^1.0.5", 20 | "cors": "^2.8.5", 21 | "cross-spawn": "^7.0.3", 22 | "eventsource": "^3.0.2", 23 | "express": "^5.0.1", 24 | "express-rate-limit": "^7.5.0", 25 | "pkce-challenge": "^5.0.0", 26 | "raw-body": "^3.0.0", 27 | "zod": "^3.23.8", 28 | "zod-to-json-schema": "^3.24.1" 29 | }, 30 | "engines": { 31 | "node": ">=18" 32 | } 33 | }, 34 | "node_modules/accepts": { 35 | "version": "2.0.0", 36 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 37 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 38 | "license": "MIT", 39 | "dependencies": { 40 | "mime-types": "^3.0.0", 41 | "negotiator": "^1.0.0" 42 | }, 43 | "engines": { 44 | "node": ">= 0.6" 45 | } 46 | }, 47 | "node_modules/body-parser": { 48 | "version": "2.2.0", 49 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 50 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 51 | "license": "MIT", 52 | "dependencies": { 53 | "bytes": "^3.1.2", 54 | "content-type": "^1.0.5", 55 | "debug": "^4.4.0", 56 | "http-errors": "^2.0.0", 57 | "iconv-lite": "^0.6.3", 58 | "on-finished": "^2.4.1", 59 | "qs": "^6.14.0", 60 | "raw-body": "^3.0.0", 61 | "type-is": "^2.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=18" 65 | } 66 | }, 67 | "node_modules/bytes": { 68 | "version": "3.1.2", 69 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 70 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 71 | "license": "MIT", 72 | "engines": { 73 | "node": ">= 0.8" 74 | } 75 | }, 76 | "node_modules/call-bind-apply-helpers": { 77 | "version": "1.0.2", 78 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 79 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 80 | "license": "MIT", 81 | "dependencies": { 82 | "es-errors": "^1.3.0", 83 | "function-bind": "^1.1.2" 84 | }, 85 | "engines": { 86 | "node": ">= 0.4" 87 | } 88 | }, 89 | "node_modules/call-bound": { 90 | "version": "1.0.4", 91 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 92 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 93 | "license": "MIT", 94 | "dependencies": { 95 | "call-bind-apply-helpers": "^1.0.2", 96 | "get-intrinsic": "^1.3.0" 97 | }, 98 | "engines": { 99 | "node": ">= 0.4" 100 | }, 101 | "funding": { 102 | "url": "https://github.com/sponsors/ljharb" 103 | } 104 | }, 105 | "node_modules/content-disposition": { 106 | "version": "1.0.0", 107 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 108 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 109 | "license": "MIT", 110 | "dependencies": { 111 | "safe-buffer": "5.2.1" 112 | }, 113 | "engines": { 114 | "node": ">= 0.6" 115 | } 116 | }, 117 | "node_modules/content-type": { 118 | "version": "1.0.5", 119 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 120 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 121 | "license": "MIT", 122 | "engines": { 123 | "node": ">= 0.6" 124 | } 125 | }, 126 | "node_modules/cookie": { 127 | "version": "0.7.2", 128 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 129 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 130 | "license": "MIT", 131 | "engines": { 132 | "node": ">= 0.6" 133 | } 134 | }, 135 | "node_modules/cookie-signature": { 136 | "version": "1.2.2", 137 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 138 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 139 | "license": "MIT", 140 | "engines": { 141 | "node": ">=6.6.0" 142 | } 143 | }, 144 | "node_modules/cors": { 145 | "version": "2.8.5", 146 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 147 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 148 | "license": "MIT", 149 | "dependencies": { 150 | "object-assign": "^4", 151 | "vary": "^1" 152 | }, 153 | "engines": { 154 | "node": ">= 0.10" 155 | } 156 | }, 157 | "node_modules/cross-spawn": { 158 | "version": "7.0.6", 159 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 160 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 161 | "license": "MIT", 162 | "dependencies": { 163 | "path-key": "^3.1.0", 164 | "shebang-command": "^2.0.0", 165 | "which": "^2.0.1" 166 | }, 167 | "engines": { 168 | "node": ">= 8" 169 | } 170 | }, 171 | "node_modules/debug": { 172 | "version": "4.4.0", 173 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 174 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 175 | "license": "MIT", 176 | "dependencies": { 177 | "ms": "^2.1.3" 178 | }, 179 | "engines": { 180 | "node": ">=6.0" 181 | }, 182 | "peerDependenciesMeta": { 183 | "supports-color": { 184 | "optional": true 185 | } 186 | } 187 | }, 188 | "node_modules/depd": { 189 | "version": "2.0.0", 190 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 191 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 192 | "license": "MIT", 193 | "engines": { 194 | "node": ">= 0.8" 195 | } 196 | }, 197 | "node_modules/dunder-proto": { 198 | "version": "1.0.1", 199 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 200 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 201 | "license": "MIT", 202 | "dependencies": { 203 | "call-bind-apply-helpers": "^1.0.1", 204 | "es-errors": "^1.3.0", 205 | "gopd": "^1.2.0" 206 | }, 207 | "engines": { 208 | "node": ">= 0.4" 209 | } 210 | }, 211 | "node_modules/ee-first": { 212 | "version": "1.1.1", 213 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 214 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 215 | "license": "MIT" 216 | }, 217 | "node_modules/encodeurl": { 218 | "version": "2.0.0", 219 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 220 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 221 | "license": "MIT", 222 | "engines": { 223 | "node": ">= 0.8" 224 | } 225 | }, 226 | "node_modules/es-define-property": { 227 | "version": "1.0.1", 228 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 229 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 230 | "license": "MIT", 231 | "engines": { 232 | "node": ">= 0.4" 233 | } 234 | }, 235 | "node_modules/es-errors": { 236 | "version": "1.3.0", 237 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 238 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 239 | "license": "MIT", 240 | "engines": { 241 | "node": ">= 0.4" 242 | } 243 | }, 244 | "node_modules/es-object-atoms": { 245 | "version": "1.1.1", 246 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 247 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 248 | "license": "MIT", 249 | "dependencies": { 250 | "es-errors": "^1.3.0" 251 | }, 252 | "engines": { 253 | "node": ">= 0.4" 254 | } 255 | }, 256 | "node_modules/escape-html": { 257 | "version": "1.0.3", 258 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 259 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 260 | "license": "MIT" 261 | }, 262 | "node_modules/etag": { 263 | "version": "1.8.1", 264 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 265 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 266 | "license": "MIT", 267 | "engines": { 268 | "node": ">= 0.6" 269 | } 270 | }, 271 | "node_modules/eventsource": { 272 | "version": "3.0.6", 273 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", 274 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 275 | "license": "MIT", 276 | "dependencies": { 277 | "eventsource-parser": "^3.0.1" 278 | }, 279 | "engines": { 280 | "node": ">=18.0.0" 281 | } 282 | }, 283 | "node_modules/eventsource-parser": { 284 | "version": "3.0.1", 285 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 286 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 287 | "license": "MIT", 288 | "engines": { 289 | "node": ">=18.0.0" 290 | } 291 | }, 292 | "node_modules/express": { 293 | "version": "5.1.0", 294 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 295 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 296 | "license": "MIT", 297 | "dependencies": { 298 | "accepts": "^2.0.0", 299 | "body-parser": "^2.2.0", 300 | "content-disposition": "^1.0.0", 301 | "content-type": "^1.0.5", 302 | "cookie": "^0.7.1", 303 | "cookie-signature": "^1.2.1", 304 | "debug": "^4.4.0", 305 | "encodeurl": "^2.0.0", 306 | "escape-html": "^1.0.3", 307 | "etag": "^1.8.1", 308 | "finalhandler": "^2.1.0", 309 | "fresh": "^2.0.0", 310 | "http-errors": "^2.0.0", 311 | "merge-descriptors": "^2.0.0", 312 | "mime-types": "^3.0.0", 313 | "on-finished": "^2.4.1", 314 | "once": "^1.4.0", 315 | "parseurl": "^1.3.3", 316 | "proxy-addr": "^2.0.7", 317 | "qs": "^6.14.0", 318 | "range-parser": "^1.2.1", 319 | "router": "^2.2.0", 320 | "send": "^1.1.0", 321 | "serve-static": "^2.2.0", 322 | "statuses": "^2.0.1", 323 | "type-is": "^2.0.1", 324 | "vary": "^1.1.2" 325 | }, 326 | "engines": { 327 | "node": ">= 18" 328 | }, 329 | "funding": { 330 | "type": "opencollective", 331 | "url": "https://opencollective.com/express" 332 | } 333 | }, 334 | "node_modules/express-rate-limit": { 335 | "version": "7.5.0", 336 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 337 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 338 | "license": "MIT", 339 | "engines": { 340 | "node": ">= 16" 341 | }, 342 | "funding": { 343 | "url": "https://github.com/sponsors/express-rate-limit" 344 | }, 345 | "peerDependencies": { 346 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 347 | } 348 | }, 349 | "node_modules/fetch-cookie": { 350 | "version": "3.1.0", 351 | "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz", 352 | "integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==", 353 | "license": "Unlicense", 354 | "dependencies": { 355 | "set-cookie-parser": "^2.4.8", 356 | "tough-cookie": "^5.0.0" 357 | } 358 | }, 359 | "node_modules/finalhandler": { 360 | "version": "2.1.0", 361 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 362 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 363 | "license": "MIT", 364 | "dependencies": { 365 | "debug": "^4.4.0", 366 | "encodeurl": "^2.0.0", 367 | "escape-html": "^1.0.3", 368 | "on-finished": "^2.4.1", 369 | "parseurl": "^1.3.3", 370 | "statuses": "^2.0.1" 371 | }, 372 | "engines": { 373 | "node": ">= 0.8" 374 | } 375 | }, 376 | "node_modules/forwarded": { 377 | "version": "0.2.0", 378 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 379 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 380 | "license": "MIT", 381 | "engines": { 382 | "node": ">= 0.6" 383 | } 384 | }, 385 | "node_modules/fresh": { 386 | "version": "2.0.0", 387 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 388 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 389 | "license": "MIT", 390 | "engines": { 391 | "node": ">= 0.8" 392 | } 393 | }, 394 | "node_modules/function-bind": { 395 | "version": "1.1.2", 396 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 397 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 398 | "license": "MIT", 399 | "funding": { 400 | "url": "https://github.com/sponsors/ljharb" 401 | } 402 | }, 403 | "node_modules/get-intrinsic": { 404 | "version": "1.3.0", 405 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 406 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 407 | "license": "MIT", 408 | "dependencies": { 409 | "call-bind-apply-helpers": "^1.0.2", 410 | "es-define-property": "^1.0.1", 411 | "es-errors": "^1.3.0", 412 | "es-object-atoms": "^1.1.1", 413 | "function-bind": "^1.1.2", 414 | "get-proto": "^1.0.1", 415 | "gopd": "^1.2.0", 416 | "has-symbols": "^1.1.0", 417 | "hasown": "^2.0.2", 418 | "math-intrinsics": "^1.1.0" 419 | }, 420 | "engines": { 421 | "node": ">= 0.4" 422 | }, 423 | "funding": { 424 | "url": "https://github.com/sponsors/ljharb" 425 | } 426 | }, 427 | "node_modules/get-proto": { 428 | "version": "1.0.1", 429 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 430 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 431 | "license": "MIT", 432 | "dependencies": { 433 | "dunder-proto": "^1.0.1", 434 | "es-object-atoms": "^1.0.0" 435 | }, 436 | "engines": { 437 | "node": ">= 0.4" 438 | } 439 | }, 440 | "node_modules/gopd": { 441 | "version": "1.2.0", 442 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 443 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 444 | "license": "MIT", 445 | "engines": { 446 | "node": ">= 0.4" 447 | }, 448 | "funding": { 449 | "url": "https://github.com/sponsors/ljharb" 450 | } 451 | }, 452 | "node_modules/has-symbols": { 453 | "version": "1.1.0", 454 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 455 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 456 | "license": "MIT", 457 | "engines": { 458 | "node": ">= 0.4" 459 | }, 460 | "funding": { 461 | "url": "https://github.com/sponsors/ljharb" 462 | } 463 | }, 464 | "node_modules/hasown": { 465 | "version": "2.0.2", 466 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 467 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 468 | "license": "MIT", 469 | "dependencies": { 470 | "function-bind": "^1.1.2" 471 | }, 472 | "engines": { 473 | "node": ">= 0.4" 474 | } 475 | }, 476 | "node_modules/http-errors": { 477 | "version": "2.0.0", 478 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 479 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 480 | "license": "MIT", 481 | "dependencies": { 482 | "depd": "2.0.0", 483 | "inherits": "2.0.4", 484 | "setprototypeof": "1.2.0", 485 | "statuses": "2.0.1", 486 | "toidentifier": "1.0.1" 487 | }, 488 | "engines": { 489 | "node": ">= 0.8" 490 | } 491 | }, 492 | "node_modules/iconv-lite": { 493 | "version": "0.6.3", 494 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 495 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 496 | "license": "MIT", 497 | "dependencies": { 498 | "safer-buffer": ">= 2.1.2 < 3.0.0" 499 | }, 500 | "engines": { 501 | "node": ">=0.10.0" 502 | } 503 | }, 504 | "node_modules/inherits": { 505 | "version": "2.0.4", 506 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 507 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 508 | "license": "ISC" 509 | }, 510 | "node_modules/ipaddr.js": { 511 | "version": "1.9.1", 512 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 513 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 514 | "license": "MIT", 515 | "engines": { 516 | "node": ">= 0.10" 517 | } 518 | }, 519 | "node_modules/is-promise": { 520 | "version": "4.0.0", 521 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 522 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 523 | "license": "MIT" 524 | }, 525 | "node_modules/isexe": { 526 | "version": "2.0.0", 527 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 528 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 529 | "license": "ISC" 530 | }, 531 | "node_modules/math-intrinsics": { 532 | "version": "1.1.0", 533 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 534 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 535 | "license": "MIT", 536 | "engines": { 537 | "node": ">= 0.4" 538 | } 539 | }, 540 | "node_modules/media-typer": { 541 | "version": "1.1.0", 542 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 543 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 544 | "license": "MIT", 545 | "engines": { 546 | "node": ">= 0.8" 547 | } 548 | }, 549 | "node_modules/merge-descriptors": { 550 | "version": "2.0.0", 551 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 552 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 553 | "license": "MIT", 554 | "engines": { 555 | "node": ">=18" 556 | }, 557 | "funding": { 558 | "url": "https://github.com/sponsors/sindresorhus" 559 | } 560 | }, 561 | "node_modules/mime-db": { 562 | "version": "1.54.0", 563 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 564 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 565 | "license": "MIT", 566 | "engines": { 567 | "node": ">= 0.6" 568 | } 569 | }, 570 | "node_modules/mime-types": { 571 | "version": "3.0.1", 572 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 573 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 574 | "license": "MIT", 575 | "dependencies": { 576 | "mime-db": "^1.54.0" 577 | }, 578 | "engines": { 579 | "node": ">= 0.6" 580 | } 581 | }, 582 | "node_modules/ms": { 583 | "version": "2.1.3", 584 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 585 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 586 | "license": "MIT" 587 | }, 588 | "node_modules/negotiator": { 589 | "version": "1.0.0", 590 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 591 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 592 | "license": "MIT", 593 | "engines": { 594 | "node": ">= 0.6" 595 | } 596 | }, 597 | "node_modules/object-assign": { 598 | "version": "4.1.1", 599 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 600 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 601 | "license": "MIT", 602 | "engines": { 603 | "node": ">=0.10.0" 604 | } 605 | }, 606 | "node_modules/object-inspect": { 607 | "version": "1.13.4", 608 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 609 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 610 | "license": "MIT", 611 | "engines": { 612 | "node": ">= 0.4" 613 | }, 614 | "funding": { 615 | "url": "https://github.com/sponsors/ljharb" 616 | } 617 | }, 618 | "node_modules/on-finished": { 619 | "version": "2.4.1", 620 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 621 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 622 | "license": "MIT", 623 | "dependencies": { 624 | "ee-first": "1.1.1" 625 | }, 626 | "engines": { 627 | "node": ">= 0.8" 628 | } 629 | }, 630 | "node_modules/once": { 631 | "version": "1.4.0", 632 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 633 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 634 | "license": "ISC", 635 | "dependencies": { 636 | "wrappy": "1" 637 | } 638 | }, 639 | "node_modules/parseurl": { 640 | "version": "1.3.3", 641 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 642 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 643 | "license": "MIT", 644 | "engines": { 645 | "node": ">= 0.8" 646 | } 647 | }, 648 | "node_modules/path-key": { 649 | "version": "3.1.1", 650 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 651 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 652 | "license": "MIT", 653 | "engines": { 654 | "node": ">=8" 655 | } 656 | }, 657 | "node_modules/path-to-regexp": { 658 | "version": "8.2.0", 659 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 660 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 661 | "license": "MIT", 662 | "engines": { 663 | "node": ">=16" 664 | } 665 | }, 666 | "node_modules/pkce-challenge": { 667 | "version": "5.0.0", 668 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 669 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 670 | "license": "MIT", 671 | "engines": { 672 | "node": ">=16.20.0" 673 | } 674 | }, 675 | "node_modules/proxy-addr": { 676 | "version": "2.0.7", 677 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 678 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 679 | "license": "MIT", 680 | "dependencies": { 681 | "forwarded": "0.2.0", 682 | "ipaddr.js": "1.9.1" 683 | }, 684 | "engines": { 685 | "node": ">= 0.10" 686 | } 687 | }, 688 | "node_modules/qs": { 689 | "version": "6.14.0", 690 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 691 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 692 | "license": "BSD-3-Clause", 693 | "dependencies": { 694 | "side-channel": "^1.1.0" 695 | }, 696 | "engines": { 697 | "node": ">=0.6" 698 | }, 699 | "funding": { 700 | "url": "https://github.com/sponsors/ljharb" 701 | } 702 | }, 703 | "node_modules/range-parser": { 704 | "version": "1.2.1", 705 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 706 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 707 | "license": "MIT", 708 | "engines": { 709 | "node": ">= 0.6" 710 | } 711 | }, 712 | "node_modules/raw-body": { 713 | "version": "3.0.0", 714 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 715 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 716 | "license": "MIT", 717 | "dependencies": { 718 | "bytes": "3.1.2", 719 | "http-errors": "2.0.0", 720 | "iconv-lite": "0.6.3", 721 | "unpipe": "1.0.0" 722 | }, 723 | "engines": { 724 | "node": ">= 0.8" 725 | } 726 | }, 727 | "node_modules/router": { 728 | "version": "2.2.0", 729 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 730 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 731 | "license": "MIT", 732 | "dependencies": { 733 | "debug": "^4.4.0", 734 | "depd": "^2.0.0", 735 | "is-promise": "^4.0.0", 736 | "parseurl": "^1.3.3", 737 | "path-to-regexp": "^8.0.0" 738 | }, 739 | "engines": { 740 | "node": ">= 18" 741 | } 742 | }, 743 | "node_modules/safe-buffer": { 744 | "version": "5.2.1", 745 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 746 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 747 | "funding": [ 748 | { 749 | "type": "github", 750 | "url": "https://github.com/sponsors/feross" 751 | }, 752 | { 753 | "type": "patreon", 754 | "url": "https://www.patreon.com/feross" 755 | }, 756 | { 757 | "type": "consulting", 758 | "url": "https://feross.org/support" 759 | } 760 | ], 761 | "license": "MIT" 762 | }, 763 | "node_modules/safer-buffer": { 764 | "version": "2.1.2", 765 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 766 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 767 | "license": "MIT" 768 | }, 769 | "node_modules/send": { 770 | "version": "1.2.0", 771 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 772 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 773 | "license": "MIT", 774 | "dependencies": { 775 | "debug": "^4.3.5", 776 | "encodeurl": "^2.0.0", 777 | "escape-html": "^1.0.3", 778 | "etag": "^1.8.1", 779 | "fresh": "^2.0.0", 780 | "http-errors": "^2.0.0", 781 | "mime-types": "^3.0.1", 782 | "ms": "^2.1.3", 783 | "on-finished": "^2.4.1", 784 | "range-parser": "^1.2.1", 785 | "statuses": "^2.0.1" 786 | }, 787 | "engines": { 788 | "node": ">= 18" 789 | } 790 | }, 791 | "node_modules/serve-static": { 792 | "version": "2.2.0", 793 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 794 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 795 | "license": "MIT", 796 | "dependencies": { 797 | "encodeurl": "^2.0.0", 798 | "escape-html": "^1.0.3", 799 | "parseurl": "^1.3.3", 800 | "send": "^1.2.0" 801 | }, 802 | "engines": { 803 | "node": ">= 18" 804 | } 805 | }, 806 | "node_modules/set-cookie-parser": { 807 | "version": "2.7.1", 808 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 809 | "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", 810 | "license": "MIT" 811 | }, 812 | "node_modules/setprototypeof": { 813 | "version": "1.2.0", 814 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 815 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 816 | "license": "ISC" 817 | }, 818 | "node_modules/shebang-command": { 819 | "version": "2.0.0", 820 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 821 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 822 | "license": "MIT", 823 | "dependencies": { 824 | "shebang-regex": "^3.0.0" 825 | }, 826 | "engines": { 827 | "node": ">=8" 828 | } 829 | }, 830 | "node_modules/shebang-regex": { 831 | "version": "3.0.0", 832 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 833 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">=8" 837 | } 838 | }, 839 | "node_modules/side-channel": { 840 | "version": "1.1.0", 841 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 842 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 843 | "license": "MIT", 844 | "dependencies": { 845 | "es-errors": "^1.3.0", 846 | "object-inspect": "^1.13.3", 847 | "side-channel-list": "^1.0.0", 848 | "side-channel-map": "^1.0.1", 849 | "side-channel-weakmap": "^1.0.2" 850 | }, 851 | "engines": { 852 | "node": ">= 0.4" 853 | }, 854 | "funding": { 855 | "url": "https://github.com/sponsors/ljharb" 856 | } 857 | }, 858 | "node_modules/side-channel-list": { 859 | "version": "1.0.0", 860 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 861 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 862 | "license": "MIT", 863 | "dependencies": { 864 | "es-errors": "^1.3.0", 865 | "object-inspect": "^1.13.3" 866 | }, 867 | "engines": { 868 | "node": ">= 0.4" 869 | }, 870 | "funding": { 871 | "url": "https://github.com/sponsors/ljharb" 872 | } 873 | }, 874 | "node_modules/side-channel-map": { 875 | "version": "1.0.1", 876 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 877 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 878 | "license": "MIT", 879 | "dependencies": { 880 | "call-bound": "^1.0.2", 881 | "es-errors": "^1.3.0", 882 | "get-intrinsic": "^1.2.5", 883 | "object-inspect": "^1.13.3" 884 | }, 885 | "engines": { 886 | "node": ">= 0.4" 887 | }, 888 | "funding": { 889 | "url": "https://github.com/sponsors/ljharb" 890 | } 891 | }, 892 | "node_modules/side-channel-weakmap": { 893 | "version": "1.0.2", 894 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 895 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 896 | "license": "MIT", 897 | "dependencies": { 898 | "call-bound": "^1.0.2", 899 | "es-errors": "^1.3.0", 900 | "get-intrinsic": "^1.2.5", 901 | "object-inspect": "^1.13.3", 902 | "side-channel-map": "^1.0.1" 903 | }, 904 | "engines": { 905 | "node": ">= 0.4" 906 | }, 907 | "funding": { 908 | "url": "https://github.com/sponsors/ljharb" 909 | } 910 | }, 911 | "node_modules/statuses": { 912 | "version": "2.0.1", 913 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 914 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 915 | "license": "MIT", 916 | "engines": { 917 | "node": ">= 0.8" 918 | } 919 | }, 920 | "node_modules/tldts": { 921 | "version": "6.1.86", 922 | "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", 923 | "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", 924 | "license": "MIT", 925 | "dependencies": { 926 | "tldts-core": "^6.1.86" 927 | }, 928 | "bin": { 929 | "tldts": "bin/cli.js" 930 | } 931 | }, 932 | "node_modules/tldts-core": { 933 | "version": "6.1.86", 934 | "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", 935 | "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", 936 | "license": "MIT" 937 | }, 938 | "node_modules/toidentifier": { 939 | "version": "1.0.1", 940 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 941 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 942 | "license": "MIT", 943 | "engines": { 944 | "node": ">=0.6" 945 | } 946 | }, 947 | "node_modules/tough-cookie": { 948 | "version": "5.1.2", 949 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", 950 | "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", 951 | "license": "BSD-3-Clause", 952 | "dependencies": { 953 | "tldts": "^6.1.32" 954 | }, 955 | "engines": { 956 | "node": ">=16" 957 | } 958 | }, 959 | "node_modules/type-is": { 960 | "version": "2.0.1", 961 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 962 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 963 | "license": "MIT", 964 | "dependencies": { 965 | "content-type": "^1.0.5", 966 | "media-typer": "^1.1.0", 967 | "mime-types": "^3.0.0" 968 | }, 969 | "engines": { 970 | "node": ">= 0.6" 971 | } 972 | }, 973 | "node_modules/unpipe": { 974 | "version": "1.0.0", 975 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 976 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 977 | "license": "MIT", 978 | "engines": { 979 | "node": ">= 0.8" 980 | } 981 | }, 982 | "node_modules/vary": { 983 | "version": "1.1.2", 984 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 985 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 986 | "license": "MIT", 987 | "engines": { 988 | "node": ">= 0.8" 989 | } 990 | }, 991 | "node_modules/which": { 992 | "version": "2.0.2", 993 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 994 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 995 | "license": "ISC", 996 | "dependencies": { 997 | "isexe": "^2.0.0" 998 | }, 999 | "bin": { 1000 | "node-which": "bin/node-which" 1001 | }, 1002 | "engines": { 1003 | "node": ">= 8" 1004 | } 1005 | }, 1006 | "node_modules/wrappy": { 1007 | "version": "1.0.2", 1008 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1009 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1010 | "license": "ISC" 1011 | }, 1012 | "node_modules/zod": { 1013 | "version": "3.24.3", 1014 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 1015 | "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 1016 | "license": "MIT", 1017 | "funding": { 1018 | "url": "https://github.com/sponsors/colinhacks" 1019 | } 1020 | }, 1021 | "node_modules/zod-to-json-schema": { 1022 | "version": "3.24.5", 1023 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1024 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1025 | "license": "ISC", 1026 | "peerDependencies": { 1027 | "zod": "^3.24.1" 1028 | } 1029 | } 1030 | } 1031 | } 1032 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": "Anton Aleksandrov", 4 | "license": "Apache-2.0", 5 | "description": "A simple MCP Client", 6 | "dependencies": { 7 | "@modelcontextprotocol/sdk": "^1.11.0", 8 | "fetch-cookie": "^3.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | 7 | RUN chown -R node:node /app 8 | USER node 9 | 10 | EXPOSE 3000 11 | CMD ["node", "index.js"] 12 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/index.js: -------------------------------------------------------------------------------- 1 | import './logging.js'; 2 | import log4js from 'log4js'; 3 | import express from 'express'; 4 | import metadata from './metadata.js'; 5 | import transport from './transport.js'; 6 | 7 | await metadata.init(); 8 | 9 | const l = log4js.getLogger(); 10 | const PORT = 3000; 11 | 12 | const app = express(); 13 | app.use(express.json()); 14 | 15 | app.get('/health', (req, res) => { 16 | res.json(metadata.all); 17 | }); 18 | 19 | app.use(async (req, res, next) => { 20 | l.debug(`> ${req.method} ${req.originalUrl}`); 21 | l.debug(req.body); 22 | // l.debug(req.headers); 23 | return next(); 24 | }); 25 | 26 | await transport.bootstrap(app); 27 | 28 | await app.listen(PORT, () => { 29 | l.debug(metadata.all); 30 | l.debug(`listening on http://localhost:${PORT}`); 31 | }); 32 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/logging.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | 3 | const layout = { 4 | type: 'pattern', 5 | pattern: '%p [%f{1}:%l:%M] %m%' 6 | } 7 | 8 | log4js.configure({ 9 | appenders: { 10 | stdout: { 11 | type: 'stdout', 12 | enableCallStack: true, 13 | layout 14 | } 15 | }, 16 | categories: { 17 | default: { 18 | appenders: ['stdout'], 19 | level: 'debug', 20 | enableCallStack: true 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/mcp-errors.js: -------------------------------------------------------------------------------- 1 | const TEMPLATE = { 2 | jsonrpc: '2.0', 3 | error: { 4 | code: 0, 5 | message: 'n/a', 6 | }, 7 | id: null, 8 | }; 9 | 10 | function build(code, message){ 11 | const result = {...TEMPLATE}; 12 | result.error.code = code; 13 | result.error.message = message; 14 | return result; 15 | } 16 | 17 | 18 | export default { 19 | get internalServerError(){ 20 | return build(-32603, 'Internal Server Error'); 21 | }, 22 | 23 | get noValidSessionId(){ 24 | return build(-32000, 'No valid session ID'); 25 | }, 26 | 27 | get invalidOrMissingSessionId(){ 28 | return build(-32000, 'Invalid or missing session ID'); 29 | }, 30 | 31 | get methodNotAllowed(){ 32 | return build(-32000, 'Method not allowed'); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/mcp-server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import metadata from "./metadata.js"; 3 | 4 | let SHORT_DELAY = true; 5 | const LONG_DELAY_MS = 100; 6 | const SHORT_DELAY_MS = 50; 7 | 8 | const create = () => { 9 | const mcpServer = new McpServer({ 10 | name: "demo-mcp-server", 11 | version: metadata.version 12 | }, { 13 | capabilities: { 14 | tools: {} 15 | } 16 | }); 17 | 18 | mcpServer.tool("ping", async () => { 19 | const startTime = Date.now(); 20 | SHORT_DELAY=!SHORT_DELAY; 21 | 22 | if (SHORT_DELAY){ 23 | await new Promise((resolve) => setTimeout(resolve, SHORT_DELAY_MS)); 24 | } else { 25 | await new Promise((resolve) => setTimeout(resolve, LONG_DELAY_MS)); 26 | } 27 | const duration = Date.now() - startTime; 28 | 29 | return { 30 | content: [ 31 | { 32 | type: "text", 33 | text: `pong! taskId=${metadata.taskId} v=${metadata.version} d=${duration}` 34 | } 35 | ] 36 | } 37 | }); 38 | 39 | return mcpServer 40 | }; 41 | 42 | export default { create }; 43 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/metadata.js: -------------------------------------------------------------------------------- 1 | const ECS_METADATA_URI = process.env.ECS_CONTAINER_METADATA_URI_V4; 2 | import packageInfo from './package.json' with { type: 'json'}; 3 | 4 | const metadata = { 5 | } 6 | 7 | async function init() { 8 | metadata.version = packageInfo.version; 9 | 10 | if (!ECS_METADATA_URI) return; 11 | 12 | const resp = await fetch(`${ECS_METADATA_URI}/task`); 13 | const respJson = await resp.json(); 14 | metadata.taskId = respJson.TaskARN.split(':')[5]; 15 | } 16 | 17 | export default { 18 | init, 19 | 20 | get all() { 21 | return metadata; 22 | }, 23 | 24 | get version() { 25 | return metadata.version; 26 | }, 27 | 28 | get taskId() { 29 | return metadata.taskId; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.0.11", 4 | "author": "Anton Aleksandrov", 5 | "license": "Apache-2.0", 6 | "description": "A sample stateful MCP Server running on ECS Fargate", 7 | "dependencies": { 8 | "@modelcontextprotocol/sdk": "^1.11.0", 9 | "express": "^5.1.0", 10 | "log4js": "^6.9.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/src/mcpserver/transport.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | import { randomUUID } from "node:crypto"; 3 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 4 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" 5 | import mcpServer from './mcp-server.js'; 6 | import mcpErrors from './mcp-errors.js'; 7 | 8 | const MCP_PATH = '/mcp'; 9 | const MCP_SESSION_ID_HEADER = 'mcp-session-id'; 10 | 11 | const l = log4js.getLogger(); 12 | 13 | const transports = {}; 14 | 15 | const bootstrap = async (app) => { 16 | app.post(MCP_PATH, postRequestHandler); 17 | app.get(MCP_PATH, sessionRequestHandler); 18 | app.delete(MCP_PATH, sessionRequestHandler); 19 | } 20 | 21 | const postRequestHandler = async (req, res) => { 22 | const sessionId = req.headers[MCP_SESSION_ID_HEADER]; 23 | l.debug(`> sessionId=${sessionId}`); 24 | let transport; 25 | 26 | if (sessionId && transports[sessionId]){ 27 | l.debug(`using existing transport for sessionid=${sessionId}`); 28 | // Reuse existing transport 29 | transport = transports[sessionId]; 30 | } else if (!sessionId && isInitializeRequest(req.body)){ 31 | // New initialization request 32 | // Create new instances of MCP Server and Transport 33 | l.debug(`creating new MCP Server and Transport`); 34 | const newMcpServer = mcpServer.create(); 35 | transport = new StreamableHTTPServerTransport({ 36 | sessionIdGenerator: () => randomUUID(), 37 | onsessioninitialized: (sessionId) => { 38 | l.debug(`session initialized for sessionid=${sessionId}`); 39 | transports[sessionId] = transport; 40 | } 41 | // Uncomment if you want to disable SSE in responses 42 | // enableJsonResponse: true, 43 | }); 44 | 45 | transport.onclose = () => { 46 | if (transport.sessionId){ 47 | l.debug(`deleting transport for sessionid=${sessionId}`); 48 | delete transports[transport.sessionId]; 49 | } 50 | } 51 | 52 | await newMcpServer.connect(transport); 53 | } else { 54 | // Invalid request 55 | l.debug(`Prodived invalid sessionId=${sessionId}`); 56 | res.status(400).json(mcpErrors.noValidSessionId); 57 | return; 58 | } 59 | 60 | await transport.handleRequest(req, res, req.body); 61 | } 62 | 63 | const sessionRequestHandler = async (req, res) => { 64 | const sessionId = req.headers[MCP_SESSION_ID_HEADER]; 65 | l.debug(`> sessionId=${sessionId}`); 66 | if (!sessionId || !transports[sessionId]){ 67 | res.status(400).json(mcpErrors.invalidOrMissingSessionId); 68 | return; 69 | } 70 | 71 | const transport = transports[sessionId]; 72 | await transport.handleRequest(req, res); 73 | } 74 | 75 | export default { 76 | bootstrap 77 | } 78 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "mcp_server" { 2 | name = local.project_name 3 | internal = false 4 | load_balancer_type = "application" 5 | subnets = [aws_subnet.public1.id, aws_subnet.public2.id] 6 | drop_invalid_header_fields = true 7 | } 8 | 9 | resource "aws_lb_target_group" "mcp_server" { 10 | name = local.project_name 11 | port = local.ecs_task_container_port 12 | protocol = "HTTP" 13 | vpc_id = aws_vpc.main.id 14 | target_type = "ip" 15 | deregistration_delay = 60 16 | 17 | stickiness { 18 | enabled = true 19 | type = "lb_cookie" 20 | cookie_duration = 86400 # 1 day 21 | } 22 | 23 | health_check { 24 | enabled = true 25 | path = "/health" 26 | healthy_threshold = 2 27 | unhealthy_threshold = 2 28 | timeout = 5 29 | interval = 10 30 | matcher = "200" 31 | } 32 | } 33 | 34 | # Use HTTP listered ONLY if you do not have a custom domain name registered 35 | # with Route53. Otherwise: 36 | # 1. Uncomment the route53.tf file 37 | # 2. Remove the HTTP listener and uncomment HTTPS listener in this file 38 | # 3. Update outputs.tf to print HTTPS endpoint instead of HTTP. 39 | # Only use HTTP for testing purposes!!! Never expose ANYTHING via plain 40 | # HTTP, use HTTPS only. 41 | resource "aws_lb_listener" "http" { 42 | load_balancer_arn = aws_lb.mcp_server.arn 43 | port = 80 44 | protocol = "HTTP" 45 | 46 | default_action { 47 | type = "forward" 48 | target_group_arn = aws_lb_target_group.mcp_server.arn 49 | } 50 | } 51 | 52 | # resource "aws_lb_listener" "https" { 53 | # load_balancer_arn = aws_lb.mcp_server.arn 54 | # port = 443 55 | # protocol = "HTTPS" 56 | # certificate_arn = aws_acm_certificate.mcp_server.arn 57 | 58 | # default_action { 59 | # type = "forward" 60 | # target_group_arn = aws_lb_target_group.mcp_server.arn 61 | # } 62 | 63 | # depends_on = [ aws_acm_certificate_validation.cert_validation ] 64 | # } 65 | 66 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "mcp_server" { 2 | name = local.project_name 3 | } 4 | 5 | resource "aws_ecs_cluster_capacity_providers" "mcp_server" { 6 | cluster_name = aws_ecs_cluster.mcp_server.name 7 | capacity_providers = ["FARGATE"] 8 | default_capacity_provider_strategy { 9 | base = 1 10 | weight = 100 11 | capacity_provider = "FARGATE" 12 | } 13 | } 14 | 15 | resource "aws_cloudwatch_log_group" "mcp_server" { 16 | name = "/ecs/${local.project_name}" 17 | retention_in_days = 7 18 | } 19 | 20 | 21 | resource "aws_iam_role" "ecs_task_role" { 22 | name = local.project_name 23 | 24 | assume_role_policy = jsonencode({ 25 | Version = "2012-10-17" 26 | Statement = [ 27 | { 28 | Action = "sts:AssumeRole" 29 | Effect = "Allow" 30 | Principal = { 31 | Service = "ecs-tasks.amazonaws.com" 32 | } 33 | }, 34 | ] 35 | }) 36 | } 37 | 38 | resource "aws_iam_role_policy_attachment" "ecs_task_role" { 39 | role = aws_iam_role.ecs_task_role.name 40 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 41 | } 42 | 43 | resource "aws_ecs_task_definition" "mcp_server" { 44 | family = local.project_name 45 | requires_compatibilities = ["FARGATE"] 46 | network_mode = "awsvpc" 47 | execution_role_arn = aws_iam_role.ecs_task_role.arn 48 | cpu = 512 49 | memory = 1024 50 | 51 | container_definitions = jsonencode([ 52 | { 53 | name = local.ecs_task_container_name 54 | image = local.ecs_task_container_image 55 | essential = true 56 | 57 | portMappings = [{ 58 | containerPort = local.ecs_task_container_port 59 | hostPort = local.ecs_task_container_port 60 | protocol = "tcp" 61 | }] 62 | 63 | logConfiguration = { 64 | logDriver = "awslogs" 65 | options = { 66 | awslogs-group = aws_cloudwatch_log_group.mcp_server.name 67 | awslogs-region = local.region 68 | awslogs-stream-prefix = "ecs" 69 | } 70 | } 71 | } 72 | ]) 73 | } 74 | 75 | resource "aws_ecs_service" "mcp_server" { 76 | name = local.project_name 77 | cluster = aws_ecs_cluster.mcp_server.id 78 | task_definition = aws_ecs_task_definition.mcp_server.arn 79 | 80 | # As of building this sample (early May 2025): 81 | # 1. Official SDK implementations/MCP spec do not support serializing and 82 | # externalizing sessions, e.g. by storing them in external Redis or DynamoDB. 83 | # 2. When used in stateful mode, the spec requires MCP server to be able to 84 | # maintain a persistent SSE connection between server and clients. This means 85 | # if you want to run more than one instance of your MCP Server, you need to 86 | # implement sticky sessions. 87 | # 3. Official TypeScript MCP Client SDK does not support cookie-based sticky 88 | # sessions since the underlying `fetch` library does not support cookies. 89 | # 4. Other official MCP Client SDKs do not implement StatelessHttpTransport yet. 90 | # 91 | # This implies that BY DEFAULT you cannot have more than one instance of MCP Server 92 | # running behind a load balancer with cookie-based sticky sessions. 93 | # There are discussions in MCP Github to address this concern, but at the moment if you need 94 | # to run multiple instances of your MCP Server for scaling or HA purposes, you got two options: 95 | # 1. Use stateless mode, see stateless-mcp-on-ecs and stateless-mcp-on-lambda samples 96 | # in this Github repo. 97 | # 2. Add cookie support to your MCP Client. This is exactly what this specific sample does, 98 | # see /src/js/mcpclient/index.js 99 | desired_count = 3 100 | force_new_deployment = true 101 | launch_type = "FARGATE" 102 | platform_version = "LATEST" 103 | 104 | network_configuration { 105 | subnets = [aws_subnet.public1.id, aws_subnet.public2.id] 106 | 107 | # Public IP is used for demo purposes in order to pull image from the Public ECR. 108 | assign_public_ip = true 109 | } 110 | 111 | load_balancer { 112 | target_group_arn = aws_lb_target_group.mcp_server.arn 113 | container_name = local.ecs_task_container_name 114 | container_port = local.ecs_task_container_port 115 | } 116 | 117 | triggers = { 118 | redeploy = plantimestamp() 119 | } 120 | 121 | depends_on = [ aws_iam_role.ecs_task_role ] # aws_acm_certificate_validation.cert_validation ] 122 | } 123 | 124 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | project_name = "stateful-mcp-on-ecs" 3 | region = "us-east-1" 4 | az1 = "${local.region}a" 5 | az2 = "${local.region}b" 6 | vpc_cidr = "10.0.0.0/16" 7 | subnet1_cidr = "10.0.1.0/24" 8 | subnet2_cidr = "10.0.2.0/24" 9 | ecs_task_container_name = "mcp_server" 10 | ecs_task_container_image = "public.ecr.aws/antonaws/stateful-mcp-on-ecs:latest" 11 | ecs_task_container_port = 3000 12 | # r53_zone_name = "your-zone-name" 13 | } 14 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "mcp_endpoint" { 2 | value = "http://${aws_lb.mcp_server.dns_name}/mcp" 3 | } -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = local.region 12 | } 13 | -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/route53.tf: -------------------------------------------------------------------------------- 1 | # resource "aws_acm_certificate" "mcp_server"{ 2 | # domain_name = "${local.project_name}.${local.r53_zone_name}" 3 | # validation_method = "DNS" 4 | # lifecycle { 5 | # create_before_destroy = true 6 | # } 7 | # } 8 | 9 | # data "aws_route53_zone" "zone" { 10 | # name = local.r53_zone_name 11 | # private_zone = false 12 | # } 13 | 14 | # resource "aws_route53_record" "cert_validation" { 15 | # for_each = { 16 | # for dvo in aws_acm_certificate.mcp_server.domain_validation_options : dvo.domain_name => { 17 | # name = dvo.resource_record_name 18 | # record = dvo.resource_record_value 19 | # type = dvo.resource_record_type 20 | # } 21 | # } 22 | 23 | # allow_overwrite = true 24 | # name = each.value.name 25 | # records = [each.value.record] 26 | # ttl = 60 27 | # type = each.value.type 28 | # zone_id = data.aws_route53_zone.zone.zone_id 29 | # } 30 | 31 | # resource "aws_acm_certificate_validation" "cert_validation" { 32 | # certificate_arn = aws_acm_certificate.mcp_server.arn 33 | # validation_record_fqdns = [for record in aws_route53_record.cert_validation: record.fqdn] 34 | # } 35 | 36 | # resource "aws_route53_record" "mcp_server" { 37 | # zone_id = data.aws_route53_zone.zone.zone_id 38 | # name = aws_acm_certificate.mcp_server.domain_name 39 | # type = "A" 40 | 41 | # alias { 42 | # name = aws_lb.mcp_server.dns_name 43 | # zone_id = aws_lb.mcp_server.zone_id 44 | # evaluate_target_health = true 45 | # } 46 | 47 | # depends_on = [ aws_acm_certificate_validation.cert_validation ] 48 | # } -------------------------------------------------------------------------------- /stateful-mcp-on-ecs-nodejs/terraform/vpc.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "main" { 2 | cidr_block = local.vpc_cidr 3 | 4 | enable_dns_support = true 5 | enable_dns_hostnames = true 6 | 7 | tags = { 8 | Name = local.project_name 9 | } 10 | } 11 | 12 | resource "aws_internet_gateway" "main" { 13 | vpc_id = aws_vpc.main.id 14 | 15 | tags = { 16 | Name = local.project_name 17 | } 18 | } 19 | 20 | # The following networking configuration is for demo purposes ONLY. 21 | # You should ALWAYS implement least privilege access using private subnets, ACLs, and 22 | # security groups with specific ingress/egress rules, ports, and cidr blocks. 23 | resource "aws_default_route_table" "main" { 24 | default_route_table_id = aws_vpc.main.default_route_table_id 25 | route { 26 | cidr_block = "0.0.0.0/0" 27 | gateway_id = aws_internet_gateway.main.id 28 | } 29 | 30 | tags = { 31 | Name = local.project_name 32 | } 33 | } 34 | 35 | resource "aws_subnet" "public1" { 36 | vpc_id = aws_vpc.main.id 37 | cidr_block = local.subnet1_cidr 38 | availability_zone = local.az1 39 | 40 | tags = { 41 | Name = "${local.project_name}-public1" 42 | } 43 | } 44 | 45 | resource "aws_subnet" "public2" { 46 | vpc_id = aws_vpc.main.id 47 | cidr_block = local.subnet2_cidr 48 | availability_zone = local.az2 49 | 50 | tags = { 51 | Name = "${local.project_name}-public2" 52 | } 53 | } 54 | 55 | resource "aws_default_security_group" "main" { 56 | vpc_id = aws_vpc.main.id 57 | 58 | ingress { 59 | protocol = "-1" 60 | from_port = 0 61 | to_port = 0 62 | cidr_blocks = ["0.0.0.0/0"] 63 | } 64 | 65 | egress { 66 | protocol = "-1" 67 | from_port = 0 68 | to_port = 0 69 | cidr_blocks = ["0.0.0.0/0"] 70 | } 71 | 72 | tags = { 73 | Name = local.project_name 74 | } 75 | } -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Stateless MCP Server on ECS Fargate 2 | 3 | This is a sample MCP Server running natively on ECS Fargate and ALB without any extra bridging components or custom transports. This is now possible thanks to the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport introduced in v2025-03-26. 4 | 5 | ![](architecture.png) 6 | 7 | ## Prereqs 8 | 9 | * AWS CLI 10 | * Terraform 11 | 12 | ## Instructions 13 | 14 | ### Clone the project 15 | 16 | ```bash 17 | git clone https://github.com/aws-samples/sample-serverless-mcp-servers.git 18 | cd sample-serverless-mcp-servers/stateless-mcp-on-ecs-nodejs 19 | ``` 20 | 21 | ### Install dependencies 22 | 23 | ```bash 24 | (cd src/mcpclient && npm install) 25 | (cd src/mcpserver && npm install) 26 | ``` 27 | 28 | ### Тest the server locally 29 | 30 | ```bash 31 | node src/mcpserver/index.js 32 | ``` 33 | 34 | Once the server is running, run client in a separate terminal window 35 | 36 | ```bash 37 | node src/mcpclient/index.js 38 | ``` 39 | 40 | ### Build and upload image to ECR 41 | 42 | Update `publish-to-ecr.sh` with your ECR alias, and run the script to build and deploy MCP Server image to ECR. 43 | 44 | ```bash 45 | ./publish-to-ecr.sh 46 | ``` 47 | 48 | ### Deploy to AWS with Terraform 49 | 50 | Update `terraform/locals.tf` with your ECR alias. Optionally, update region, VPC configuraion, and Route53 Zone Name if you have a DNS name registered to use for SSL certificate (see HTTPS considerations below for details). 51 | 52 | Run below commands to deploy the sample to AWS 53 | 54 | ```bash 55 | cd terraform 56 | terraform init 57 | terraform plan 58 | terraform apply 59 | export MCP_SERVER_ENDPOINT=$(terraform output --raw mcp_endpoint) 60 | cd .. 61 | ``` 62 | 63 | Deployment takes 3-4 minutes. Once Terraform deployment has completed, it will take 2-3 more minutes for ECS tasks to spin up and get recognized by the ALB target group. 64 | 65 | ### Test your remote MCP Server with MCP client: 66 | ```bash 67 | node src/mcpclient/index.js 68 | ``` 69 | 70 | Observe the response: 71 | ```bash 72 | Connecting ENDPOINT_URL=http://stateless-mcp-on-ecs-1870111106.us-east-1.elb.amazonaws.com/mcp 73 | connected 74 | listTools response: { tools: [ { name: 'ping', inputSchema: [Object] } ] } 75 | callTool:ping response: { 76 | content: [ 77 | { 78 | type: 'text', 79 | text: 'pong! taskId=task/stateful-mcp-on-ecs/6907042c100c4adf80c2d0957c38706f v=0.0.10 d=101' 80 | } 81 | ] 82 | } 83 | callTool:ping response: { 84 | content: [ 85 | { 86 | type: 'text', 87 | text: 'pong! taskId=task/stateful-mcp-on-ecs/6907042c100c4adf80c2d0957c38706f v=0.0.10 d=50' 88 | } 89 | ] 90 | } 91 | ``` 92 | 93 | ## Statefull vs Stateless considerations 94 | MCP Server can run in two modes - stateless and stateful. This repo demonstrates the stateless mode. 95 | 96 | In stateless mode, clients do not establish persistent SSE connections to MCP Server. This means clients will not receive proactive notifications from the server. On the other hand, stateless mode allows you to scale your server horizontally. 97 | 98 | If you want to see how to built a stateful MCP Server, that supports persistent SSE connections, see the `stateful-mcp-on-ecs-nodejs` sample in this repo. 99 | 100 | ## HTTPS considerations 101 | 102 | By default, this sample uses the default ALB endpoint, which is HTTP only. See a comment in terraform/alb.tf for instructions how to enable HTTPS. 103 | 104 | Only use HTTP for testing purposes ONLY!!! NEVER expose ANYTHING via plain HTTP, always use HTTPS!!! 105 | 106 | ## Cost considerations 107 | 108 | This sample provisions paid resources in your account, such as ECS Tasks and ALB. Remember to delete these resources when you're done evaluating. 109 | 110 | ```bash 111 | terraform destroy 112 | ``` 113 | 114 | ## Learn about mcp 115 | [Intro](https://modelcontextprotocol.io/introduction) 116 | 117 | [Protocol specification](https://modelcontextprotocol.io/specification/2025-03-26) 118 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-mcp-servers/c276bc74d5d60b2875c44a2423de094e72aad4ee/stateless-mcp-on-ecs-nodejs/architecture.png -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/publish-to-ecr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ECR_REPO=stateless-mcp-on-ecs 3 | ECR_IMAGE_TAG=latest 4 | ECR_ALIAS=your-ecr-alias-here 5 | ECR_REPO_URI=public.ecr.aws/${ECR_ALIAS}/$ECR_REPO:$ECR_IMAGE_TAG 6 | 7 | echo Logging in... 8 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws 9 | 10 | echo Incrementing app version... 11 | cd src/mcpserver 12 | npm version patch 13 | 14 | echo Building image and publishing to Public ECR... 15 | aws ecr-public create-repository --repository-name $ECR_REPO --no-cli-pager 16 | docker buildx build --platform linux/amd64 --provenance=false -t $ECR_REPO_URI . --push 17 | 18 | echo All done! 19 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpclient/index.js: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 3 | 4 | const ENDPOINT_URL = process.env.MCP_SERVER_ENDPOINT || 'http://localhost:3000/mcp'; 5 | 6 | console.log(`Connecting ENDPOINT_URL=${ENDPOINT_URL}`); 7 | 8 | const transport = new StreamableHTTPClientTransport(new URL(ENDPOINT_URL)); 9 | 10 | const client = new Client({ 11 | name: "node-client", 12 | version: "0.0.1" 13 | }) 14 | 15 | await client.connect(transport); 16 | console.log('connected'); 17 | 18 | const tools = await client.listTools(); 19 | console.log(`listTools response: `, tools); 20 | 21 | for (let i = 0; i < 2; i++) { 22 | let result = await client.callTool({ 23 | name: "ping" 24 | }); 25 | console.log(`callTool:ping response: `, result); 26 | } 27 | 28 | await client.close(); 29 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpclient/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcpclient", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "license": "Apache-2.0", 8 | "dependencies": { 9 | "@modelcontextprotocol/sdk": "^1.11.0", 10 | "fetch-cookie": "^3.1.0" 11 | } 12 | }, 13 | "node_modules/@modelcontextprotocol/sdk": { 14 | "version": "1.11.0", 15 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", 16 | "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", 17 | "license": "MIT", 18 | "dependencies": { 19 | "content-type": "^1.0.5", 20 | "cors": "^2.8.5", 21 | "cross-spawn": "^7.0.3", 22 | "eventsource": "^3.0.2", 23 | "express": "^5.0.1", 24 | "express-rate-limit": "^7.5.0", 25 | "pkce-challenge": "^5.0.0", 26 | "raw-body": "^3.0.0", 27 | "zod": "^3.23.8", 28 | "zod-to-json-schema": "^3.24.1" 29 | }, 30 | "engines": { 31 | "node": ">=18" 32 | } 33 | }, 34 | "node_modules/accepts": { 35 | "version": "2.0.0", 36 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 37 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 38 | "license": "MIT", 39 | "dependencies": { 40 | "mime-types": "^3.0.0", 41 | "negotiator": "^1.0.0" 42 | }, 43 | "engines": { 44 | "node": ">= 0.6" 45 | } 46 | }, 47 | "node_modules/body-parser": { 48 | "version": "2.2.0", 49 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 50 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 51 | "license": "MIT", 52 | "dependencies": { 53 | "bytes": "^3.1.2", 54 | "content-type": "^1.0.5", 55 | "debug": "^4.4.0", 56 | "http-errors": "^2.0.0", 57 | "iconv-lite": "^0.6.3", 58 | "on-finished": "^2.4.1", 59 | "qs": "^6.14.0", 60 | "raw-body": "^3.0.0", 61 | "type-is": "^2.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=18" 65 | } 66 | }, 67 | "node_modules/bytes": { 68 | "version": "3.1.2", 69 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 70 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 71 | "license": "MIT", 72 | "engines": { 73 | "node": ">= 0.8" 74 | } 75 | }, 76 | "node_modules/call-bind-apply-helpers": { 77 | "version": "1.0.2", 78 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 79 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 80 | "license": "MIT", 81 | "dependencies": { 82 | "es-errors": "^1.3.0", 83 | "function-bind": "^1.1.2" 84 | }, 85 | "engines": { 86 | "node": ">= 0.4" 87 | } 88 | }, 89 | "node_modules/call-bound": { 90 | "version": "1.0.4", 91 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 92 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 93 | "license": "MIT", 94 | "dependencies": { 95 | "call-bind-apply-helpers": "^1.0.2", 96 | "get-intrinsic": "^1.3.0" 97 | }, 98 | "engines": { 99 | "node": ">= 0.4" 100 | }, 101 | "funding": { 102 | "url": "https://github.com/sponsors/ljharb" 103 | } 104 | }, 105 | "node_modules/content-disposition": { 106 | "version": "1.0.0", 107 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 108 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 109 | "license": "MIT", 110 | "dependencies": { 111 | "safe-buffer": "5.2.1" 112 | }, 113 | "engines": { 114 | "node": ">= 0.6" 115 | } 116 | }, 117 | "node_modules/content-type": { 118 | "version": "1.0.5", 119 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 120 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 121 | "license": "MIT", 122 | "engines": { 123 | "node": ">= 0.6" 124 | } 125 | }, 126 | "node_modules/cookie": { 127 | "version": "0.7.2", 128 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 129 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 130 | "license": "MIT", 131 | "engines": { 132 | "node": ">= 0.6" 133 | } 134 | }, 135 | "node_modules/cookie-signature": { 136 | "version": "1.2.2", 137 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 138 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 139 | "license": "MIT", 140 | "engines": { 141 | "node": ">=6.6.0" 142 | } 143 | }, 144 | "node_modules/cors": { 145 | "version": "2.8.5", 146 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 147 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 148 | "license": "MIT", 149 | "dependencies": { 150 | "object-assign": "^4", 151 | "vary": "^1" 152 | }, 153 | "engines": { 154 | "node": ">= 0.10" 155 | } 156 | }, 157 | "node_modules/cross-spawn": { 158 | "version": "7.0.6", 159 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 160 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 161 | "license": "MIT", 162 | "dependencies": { 163 | "path-key": "^3.1.0", 164 | "shebang-command": "^2.0.0", 165 | "which": "^2.0.1" 166 | }, 167 | "engines": { 168 | "node": ">= 8" 169 | } 170 | }, 171 | "node_modules/debug": { 172 | "version": "4.4.0", 173 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 174 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 175 | "license": "MIT", 176 | "dependencies": { 177 | "ms": "^2.1.3" 178 | }, 179 | "engines": { 180 | "node": ">=6.0" 181 | }, 182 | "peerDependenciesMeta": { 183 | "supports-color": { 184 | "optional": true 185 | } 186 | } 187 | }, 188 | "node_modules/depd": { 189 | "version": "2.0.0", 190 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 191 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 192 | "license": "MIT", 193 | "engines": { 194 | "node": ">= 0.8" 195 | } 196 | }, 197 | "node_modules/dunder-proto": { 198 | "version": "1.0.1", 199 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 200 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 201 | "license": "MIT", 202 | "dependencies": { 203 | "call-bind-apply-helpers": "^1.0.1", 204 | "es-errors": "^1.3.0", 205 | "gopd": "^1.2.0" 206 | }, 207 | "engines": { 208 | "node": ">= 0.4" 209 | } 210 | }, 211 | "node_modules/ee-first": { 212 | "version": "1.1.1", 213 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 214 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 215 | "license": "MIT" 216 | }, 217 | "node_modules/encodeurl": { 218 | "version": "2.0.0", 219 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 220 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 221 | "license": "MIT", 222 | "engines": { 223 | "node": ">= 0.8" 224 | } 225 | }, 226 | "node_modules/es-define-property": { 227 | "version": "1.0.1", 228 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 229 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 230 | "license": "MIT", 231 | "engines": { 232 | "node": ">= 0.4" 233 | } 234 | }, 235 | "node_modules/es-errors": { 236 | "version": "1.3.0", 237 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 238 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 239 | "license": "MIT", 240 | "engines": { 241 | "node": ">= 0.4" 242 | } 243 | }, 244 | "node_modules/es-object-atoms": { 245 | "version": "1.1.1", 246 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 247 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 248 | "license": "MIT", 249 | "dependencies": { 250 | "es-errors": "^1.3.0" 251 | }, 252 | "engines": { 253 | "node": ">= 0.4" 254 | } 255 | }, 256 | "node_modules/escape-html": { 257 | "version": "1.0.3", 258 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 259 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 260 | "license": "MIT" 261 | }, 262 | "node_modules/etag": { 263 | "version": "1.8.1", 264 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 265 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 266 | "license": "MIT", 267 | "engines": { 268 | "node": ">= 0.6" 269 | } 270 | }, 271 | "node_modules/eventsource": { 272 | "version": "3.0.6", 273 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", 274 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 275 | "license": "MIT", 276 | "dependencies": { 277 | "eventsource-parser": "^3.0.1" 278 | }, 279 | "engines": { 280 | "node": ">=18.0.0" 281 | } 282 | }, 283 | "node_modules/eventsource-parser": { 284 | "version": "3.0.1", 285 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 286 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 287 | "license": "MIT", 288 | "engines": { 289 | "node": ">=18.0.0" 290 | } 291 | }, 292 | "node_modules/express": { 293 | "version": "5.1.0", 294 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 295 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 296 | "license": "MIT", 297 | "dependencies": { 298 | "accepts": "^2.0.0", 299 | "body-parser": "^2.2.0", 300 | "content-disposition": "^1.0.0", 301 | "content-type": "^1.0.5", 302 | "cookie": "^0.7.1", 303 | "cookie-signature": "^1.2.1", 304 | "debug": "^4.4.0", 305 | "encodeurl": "^2.0.0", 306 | "escape-html": "^1.0.3", 307 | "etag": "^1.8.1", 308 | "finalhandler": "^2.1.0", 309 | "fresh": "^2.0.0", 310 | "http-errors": "^2.0.0", 311 | "merge-descriptors": "^2.0.0", 312 | "mime-types": "^3.0.0", 313 | "on-finished": "^2.4.1", 314 | "once": "^1.4.0", 315 | "parseurl": "^1.3.3", 316 | "proxy-addr": "^2.0.7", 317 | "qs": "^6.14.0", 318 | "range-parser": "^1.2.1", 319 | "router": "^2.2.0", 320 | "send": "^1.1.0", 321 | "serve-static": "^2.2.0", 322 | "statuses": "^2.0.1", 323 | "type-is": "^2.0.1", 324 | "vary": "^1.1.2" 325 | }, 326 | "engines": { 327 | "node": ">= 18" 328 | }, 329 | "funding": { 330 | "type": "opencollective", 331 | "url": "https://opencollective.com/express" 332 | } 333 | }, 334 | "node_modules/express-rate-limit": { 335 | "version": "7.5.0", 336 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 337 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 338 | "license": "MIT", 339 | "engines": { 340 | "node": ">= 16" 341 | }, 342 | "funding": { 343 | "url": "https://github.com/sponsors/express-rate-limit" 344 | }, 345 | "peerDependencies": { 346 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 347 | } 348 | }, 349 | "node_modules/fetch-cookie": { 350 | "version": "3.1.0", 351 | "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz", 352 | "integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==", 353 | "license": "Unlicense", 354 | "dependencies": { 355 | "set-cookie-parser": "^2.4.8", 356 | "tough-cookie": "^5.0.0" 357 | } 358 | }, 359 | "node_modules/finalhandler": { 360 | "version": "2.1.0", 361 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 362 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 363 | "license": "MIT", 364 | "dependencies": { 365 | "debug": "^4.4.0", 366 | "encodeurl": "^2.0.0", 367 | "escape-html": "^1.0.3", 368 | "on-finished": "^2.4.1", 369 | "parseurl": "^1.3.3", 370 | "statuses": "^2.0.1" 371 | }, 372 | "engines": { 373 | "node": ">= 0.8" 374 | } 375 | }, 376 | "node_modules/forwarded": { 377 | "version": "0.2.0", 378 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 379 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 380 | "license": "MIT", 381 | "engines": { 382 | "node": ">= 0.6" 383 | } 384 | }, 385 | "node_modules/fresh": { 386 | "version": "2.0.0", 387 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 388 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 389 | "license": "MIT", 390 | "engines": { 391 | "node": ">= 0.8" 392 | } 393 | }, 394 | "node_modules/function-bind": { 395 | "version": "1.1.2", 396 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 397 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 398 | "license": "MIT", 399 | "funding": { 400 | "url": "https://github.com/sponsors/ljharb" 401 | } 402 | }, 403 | "node_modules/get-intrinsic": { 404 | "version": "1.3.0", 405 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 406 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 407 | "license": "MIT", 408 | "dependencies": { 409 | "call-bind-apply-helpers": "^1.0.2", 410 | "es-define-property": "^1.0.1", 411 | "es-errors": "^1.3.0", 412 | "es-object-atoms": "^1.1.1", 413 | "function-bind": "^1.1.2", 414 | "get-proto": "^1.0.1", 415 | "gopd": "^1.2.0", 416 | "has-symbols": "^1.1.0", 417 | "hasown": "^2.0.2", 418 | "math-intrinsics": "^1.1.0" 419 | }, 420 | "engines": { 421 | "node": ">= 0.4" 422 | }, 423 | "funding": { 424 | "url": "https://github.com/sponsors/ljharb" 425 | } 426 | }, 427 | "node_modules/get-proto": { 428 | "version": "1.0.1", 429 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 430 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 431 | "license": "MIT", 432 | "dependencies": { 433 | "dunder-proto": "^1.0.1", 434 | "es-object-atoms": "^1.0.0" 435 | }, 436 | "engines": { 437 | "node": ">= 0.4" 438 | } 439 | }, 440 | "node_modules/gopd": { 441 | "version": "1.2.0", 442 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 443 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 444 | "license": "MIT", 445 | "engines": { 446 | "node": ">= 0.4" 447 | }, 448 | "funding": { 449 | "url": "https://github.com/sponsors/ljharb" 450 | } 451 | }, 452 | "node_modules/has-symbols": { 453 | "version": "1.1.0", 454 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 455 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 456 | "license": "MIT", 457 | "engines": { 458 | "node": ">= 0.4" 459 | }, 460 | "funding": { 461 | "url": "https://github.com/sponsors/ljharb" 462 | } 463 | }, 464 | "node_modules/hasown": { 465 | "version": "2.0.2", 466 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 467 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 468 | "license": "MIT", 469 | "dependencies": { 470 | "function-bind": "^1.1.2" 471 | }, 472 | "engines": { 473 | "node": ">= 0.4" 474 | } 475 | }, 476 | "node_modules/http-errors": { 477 | "version": "2.0.0", 478 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 479 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 480 | "license": "MIT", 481 | "dependencies": { 482 | "depd": "2.0.0", 483 | "inherits": "2.0.4", 484 | "setprototypeof": "1.2.0", 485 | "statuses": "2.0.1", 486 | "toidentifier": "1.0.1" 487 | }, 488 | "engines": { 489 | "node": ">= 0.8" 490 | } 491 | }, 492 | "node_modules/iconv-lite": { 493 | "version": "0.6.3", 494 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 495 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 496 | "license": "MIT", 497 | "dependencies": { 498 | "safer-buffer": ">= 2.1.2 < 3.0.0" 499 | }, 500 | "engines": { 501 | "node": ">=0.10.0" 502 | } 503 | }, 504 | "node_modules/inherits": { 505 | "version": "2.0.4", 506 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 507 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 508 | "license": "ISC" 509 | }, 510 | "node_modules/ipaddr.js": { 511 | "version": "1.9.1", 512 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 513 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 514 | "license": "MIT", 515 | "engines": { 516 | "node": ">= 0.10" 517 | } 518 | }, 519 | "node_modules/is-promise": { 520 | "version": "4.0.0", 521 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 522 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 523 | "license": "MIT" 524 | }, 525 | "node_modules/isexe": { 526 | "version": "2.0.0", 527 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 528 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 529 | "license": "ISC" 530 | }, 531 | "node_modules/math-intrinsics": { 532 | "version": "1.1.0", 533 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 534 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 535 | "license": "MIT", 536 | "engines": { 537 | "node": ">= 0.4" 538 | } 539 | }, 540 | "node_modules/media-typer": { 541 | "version": "1.1.0", 542 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 543 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 544 | "license": "MIT", 545 | "engines": { 546 | "node": ">= 0.8" 547 | } 548 | }, 549 | "node_modules/merge-descriptors": { 550 | "version": "2.0.0", 551 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 552 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 553 | "license": "MIT", 554 | "engines": { 555 | "node": ">=18" 556 | }, 557 | "funding": { 558 | "url": "https://github.com/sponsors/sindresorhus" 559 | } 560 | }, 561 | "node_modules/mime-db": { 562 | "version": "1.54.0", 563 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 564 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 565 | "license": "MIT", 566 | "engines": { 567 | "node": ">= 0.6" 568 | } 569 | }, 570 | "node_modules/mime-types": { 571 | "version": "3.0.1", 572 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 573 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 574 | "license": "MIT", 575 | "dependencies": { 576 | "mime-db": "^1.54.0" 577 | }, 578 | "engines": { 579 | "node": ">= 0.6" 580 | } 581 | }, 582 | "node_modules/ms": { 583 | "version": "2.1.3", 584 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 585 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 586 | "license": "MIT" 587 | }, 588 | "node_modules/negotiator": { 589 | "version": "1.0.0", 590 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 591 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 592 | "license": "MIT", 593 | "engines": { 594 | "node": ">= 0.6" 595 | } 596 | }, 597 | "node_modules/object-assign": { 598 | "version": "4.1.1", 599 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 600 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 601 | "license": "MIT", 602 | "engines": { 603 | "node": ">=0.10.0" 604 | } 605 | }, 606 | "node_modules/object-inspect": { 607 | "version": "1.13.4", 608 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 609 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 610 | "license": "MIT", 611 | "engines": { 612 | "node": ">= 0.4" 613 | }, 614 | "funding": { 615 | "url": "https://github.com/sponsors/ljharb" 616 | } 617 | }, 618 | "node_modules/on-finished": { 619 | "version": "2.4.1", 620 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 621 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 622 | "license": "MIT", 623 | "dependencies": { 624 | "ee-first": "1.1.1" 625 | }, 626 | "engines": { 627 | "node": ">= 0.8" 628 | } 629 | }, 630 | "node_modules/once": { 631 | "version": "1.4.0", 632 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 633 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 634 | "license": "ISC", 635 | "dependencies": { 636 | "wrappy": "1" 637 | } 638 | }, 639 | "node_modules/parseurl": { 640 | "version": "1.3.3", 641 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 642 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 643 | "license": "MIT", 644 | "engines": { 645 | "node": ">= 0.8" 646 | } 647 | }, 648 | "node_modules/path-key": { 649 | "version": "3.1.1", 650 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 651 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 652 | "license": "MIT", 653 | "engines": { 654 | "node": ">=8" 655 | } 656 | }, 657 | "node_modules/path-to-regexp": { 658 | "version": "8.2.0", 659 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 660 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 661 | "license": "MIT", 662 | "engines": { 663 | "node": ">=16" 664 | } 665 | }, 666 | "node_modules/pkce-challenge": { 667 | "version": "5.0.0", 668 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 669 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 670 | "license": "MIT", 671 | "engines": { 672 | "node": ">=16.20.0" 673 | } 674 | }, 675 | "node_modules/proxy-addr": { 676 | "version": "2.0.7", 677 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 678 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 679 | "license": "MIT", 680 | "dependencies": { 681 | "forwarded": "0.2.0", 682 | "ipaddr.js": "1.9.1" 683 | }, 684 | "engines": { 685 | "node": ">= 0.10" 686 | } 687 | }, 688 | "node_modules/qs": { 689 | "version": "6.14.0", 690 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 691 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 692 | "license": "BSD-3-Clause", 693 | "dependencies": { 694 | "side-channel": "^1.1.0" 695 | }, 696 | "engines": { 697 | "node": ">=0.6" 698 | }, 699 | "funding": { 700 | "url": "https://github.com/sponsors/ljharb" 701 | } 702 | }, 703 | "node_modules/range-parser": { 704 | "version": "1.2.1", 705 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 706 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 707 | "license": "MIT", 708 | "engines": { 709 | "node": ">= 0.6" 710 | } 711 | }, 712 | "node_modules/raw-body": { 713 | "version": "3.0.0", 714 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 715 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 716 | "license": "MIT", 717 | "dependencies": { 718 | "bytes": "3.1.2", 719 | "http-errors": "2.0.0", 720 | "iconv-lite": "0.6.3", 721 | "unpipe": "1.0.0" 722 | }, 723 | "engines": { 724 | "node": ">= 0.8" 725 | } 726 | }, 727 | "node_modules/router": { 728 | "version": "2.2.0", 729 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 730 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 731 | "license": "MIT", 732 | "dependencies": { 733 | "debug": "^4.4.0", 734 | "depd": "^2.0.0", 735 | "is-promise": "^4.0.0", 736 | "parseurl": "^1.3.3", 737 | "path-to-regexp": "^8.0.0" 738 | }, 739 | "engines": { 740 | "node": ">= 18" 741 | } 742 | }, 743 | "node_modules/safe-buffer": { 744 | "version": "5.2.1", 745 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 746 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 747 | "funding": [ 748 | { 749 | "type": "github", 750 | "url": "https://github.com/sponsors/feross" 751 | }, 752 | { 753 | "type": "patreon", 754 | "url": "https://www.patreon.com/feross" 755 | }, 756 | { 757 | "type": "consulting", 758 | "url": "https://feross.org/support" 759 | } 760 | ], 761 | "license": "MIT" 762 | }, 763 | "node_modules/safer-buffer": { 764 | "version": "2.1.2", 765 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 766 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 767 | "license": "MIT" 768 | }, 769 | "node_modules/send": { 770 | "version": "1.2.0", 771 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 772 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 773 | "license": "MIT", 774 | "dependencies": { 775 | "debug": "^4.3.5", 776 | "encodeurl": "^2.0.0", 777 | "escape-html": "^1.0.3", 778 | "etag": "^1.8.1", 779 | "fresh": "^2.0.0", 780 | "http-errors": "^2.0.0", 781 | "mime-types": "^3.0.1", 782 | "ms": "^2.1.3", 783 | "on-finished": "^2.4.1", 784 | "range-parser": "^1.2.1", 785 | "statuses": "^2.0.1" 786 | }, 787 | "engines": { 788 | "node": ">= 18" 789 | } 790 | }, 791 | "node_modules/serve-static": { 792 | "version": "2.2.0", 793 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 794 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 795 | "license": "MIT", 796 | "dependencies": { 797 | "encodeurl": "^2.0.0", 798 | "escape-html": "^1.0.3", 799 | "parseurl": "^1.3.3", 800 | "send": "^1.2.0" 801 | }, 802 | "engines": { 803 | "node": ">= 18" 804 | } 805 | }, 806 | "node_modules/set-cookie-parser": { 807 | "version": "2.7.1", 808 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 809 | "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", 810 | "license": "MIT" 811 | }, 812 | "node_modules/setprototypeof": { 813 | "version": "1.2.0", 814 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 815 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 816 | "license": "ISC" 817 | }, 818 | "node_modules/shebang-command": { 819 | "version": "2.0.0", 820 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 821 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 822 | "license": "MIT", 823 | "dependencies": { 824 | "shebang-regex": "^3.0.0" 825 | }, 826 | "engines": { 827 | "node": ">=8" 828 | } 829 | }, 830 | "node_modules/shebang-regex": { 831 | "version": "3.0.0", 832 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 833 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">=8" 837 | } 838 | }, 839 | "node_modules/side-channel": { 840 | "version": "1.1.0", 841 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 842 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 843 | "license": "MIT", 844 | "dependencies": { 845 | "es-errors": "^1.3.0", 846 | "object-inspect": "^1.13.3", 847 | "side-channel-list": "^1.0.0", 848 | "side-channel-map": "^1.0.1", 849 | "side-channel-weakmap": "^1.0.2" 850 | }, 851 | "engines": { 852 | "node": ">= 0.4" 853 | }, 854 | "funding": { 855 | "url": "https://github.com/sponsors/ljharb" 856 | } 857 | }, 858 | "node_modules/side-channel-list": { 859 | "version": "1.0.0", 860 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 861 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 862 | "license": "MIT", 863 | "dependencies": { 864 | "es-errors": "^1.3.0", 865 | "object-inspect": "^1.13.3" 866 | }, 867 | "engines": { 868 | "node": ">= 0.4" 869 | }, 870 | "funding": { 871 | "url": "https://github.com/sponsors/ljharb" 872 | } 873 | }, 874 | "node_modules/side-channel-map": { 875 | "version": "1.0.1", 876 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 877 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 878 | "license": "MIT", 879 | "dependencies": { 880 | "call-bound": "^1.0.2", 881 | "es-errors": "^1.3.0", 882 | "get-intrinsic": "^1.2.5", 883 | "object-inspect": "^1.13.3" 884 | }, 885 | "engines": { 886 | "node": ">= 0.4" 887 | }, 888 | "funding": { 889 | "url": "https://github.com/sponsors/ljharb" 890 | } 891 | }, 892 | "node_modules/side-channel-weakmap": { 893 | "version": "1.0.2", 894 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 895 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 896 | "license": "MIT", 897 | "dependencies": { 898 | "call-bound": "^1.0.2", 899 | "es-errors": "^1.3.0", 900 | "get-intrinsic": "^1.2.5", 901 | "object-inspect": "^1.13.3", 902 | "side-channel-map": "^1.0.1" 903 | }, 904 | "engines": { 905 | "node": ">= 0.4" 906 | }, 907 | "funding": { 908 | "url": "https://github.com/sponsors/ljharb" 909 | } 910 | }, 911 | "node_modules/statuses": { 912 | "version": "2.0.1", 913 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 914 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 915 | "license": "MIT", 916 | "engines": { 917 | "node": ">= 0.8" 918 | } 919 | }, 920 | "node_modules/tldts": { 921 | "version": "6.1.86", 922 | "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", 923 | "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", 924 | "license": "MIT", 925 | "dependencies": { 926 | "tldts-core": "^6.1.86" 927 | }, 928 | "bin": { 929 | "tldts": "bin/cli.js" 930 | } 931 | }, 932 | "node_modules/tldts-core": { 933 | "version": "6.1.86", 934 | "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", 935 | "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", 936 | "license": "MIT" 937 | }, 938 | "node_modules/toidentifier": { 939 | "version": "1.0.1", 940 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 941 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 942 | "license": "MIT", 943 | "engines": { 944 | "node": ">=0.6" 945 | } 946 | }, 947 | "node_modules/tough-cookie": { 948 | "version": "5.1.2", 949 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", 950 | "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", 951 | "license": "BSD-3-Clause", 952 | "dependencies": { 953 | "tldts": "^6.1.32" 954 | }, 955 | "engines": { 956 | "node": ">=16" 957 | } 958 | }, 959 | "node_modules/type-is": { 960 | "version": "2.0.1", 961 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 962 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 963 | "license": "MIT", 964 | "dependencies": { 965 | "content-type": "^1.0.5", 966 | "media-typer": "^1.1.0", 967 | "mime-types": "^3.0.0" 968 | }, 969 | "engines": { 970 | "node": ">= 0.6" 971 | } 972 | }, 973 | "node_modules/unpipe": { 974 | "version": "1.0.0", 975 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 976 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 977 | "license": "MIT", 978 | "engines": { 979 | "node": ">= 0.8" 980 | } 981 | }, 982 | "node_modules/vary": { 983 | "version": "1.1.2", 984 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 985 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 986 | "license": "MIT", 987 | "engines": { 988 | "node": ">= 0.8" 989 | } 990 | }, 991 | "node_modules/which": { 992 | "version": "2.0.2", 993 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 994 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 995 | "license": "ISC", 996 | "dependencies": { 997 | "isexe": "^2.0.0" 998 | }, 999 | "bin": { 1000 | "node-which": "bin/node-which" 1001 | }, 1002 | "engines": { 1003 | "node": ">= 8" 1004 | } 1005 | }, 1006 | "node_modules/wrappy": { 1007 | "version": "1.0.2", 1008 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1009 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1010 | "license": "ISC" 1011 | }, 1012 | "node_modules/zod": { 1013 | "version": "3.24.3", 1014 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 1015 | "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 1016 | "license": "MIT", 1017 | "funding": { 1018 | "url": "https://github.com/sponsors/colinhacks" 1019 | } 1020 | }, 1021 | "node_modules/zod-to-json-schema": { 1022 | "version": "3.24.5", 1023 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1024 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1025 | "license": "ISC", 1026 | "peerDependencies": { 1027 | "zod": "^3.24.1" 1028 | } 1029 | } 1030 | } 1031 | } 1032 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": "Anton Aleksandrov", 4 | "license": "Apache-2.0", 5 | "description": "A simple MCP Client", 6 | "dependencies": { 7 | "@modelcontextprotocol/sdk": "^1.11.0", 8 | "fetch-cookie": "^3.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | 7 | RUN chown -R node:node /app 8 | USER node 9 | 10 | EXPOSE 3000 11 | CMD ["node", "index.js"] 12 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/index.js: -------------------------------------------------------------------------------- 1 | import './logging.js'; 2 | import log4js from 'log4js'; 3 | import express from 'express'; 4 | import metadata from './metadata.js'; 5 | import transport from './transport.js'; 6 | 7 | await metadata.init(); 8 | 9 | const l = log4js.getLogger(); 10 | const PORT = 3000; 11 | 12 | const app = express(); 13 | app.use(express.json()); 14 | 15 | app.get('/health', (req, res) => { 16 | res.json(metadata.all); 17 | }); 18 | 19 | app.use(async (req, res, next) => { 20 | l.debug(`> ${req.method} ${req.originalUrl}`); 21 | l.debug(req.body); 22 | // l.debug(req.headers); 23 | return next(); 24 | }); 25 | 26 | await transport.bootstrap(app); 27 | 28 | await app.listen(PORT, () => { 29 | l.debug(metadata.all); 30 | l.debug(`listening on http://localhost:${PORT}`); 31 | }); 32 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/logging.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | 3 | const layout = { 4 | type: 'pattern', 5 | pattern: '%p [%f{1}:%l:%M] %m%' 6 | } 7 | 8 | log4js.configure({ 9 | appenders: { 10 | stdout: { 11 | type: 'stdout', 12 | enableCallStack: true, 13 | layout 14 | } 15 | }, 16 | categories: { 17 | default: { 18 | appenders: ['stdout'], 19 | level: 'debug', 20 | enableCallStack: true 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/mcp-errors.js: -------------------------------------------------------------------------------- 1 | const TEMPLATE = { 2 | jsonrpc: '2.0', 3 | error: { 4 | code: 0, 5 | message: 'n/a', 6 | }, 7 | id: null, 8 | }; 9 | 10 | function build(code, message){ 11 | const result = {...TEMPLATE}; 12 | result.error.code = code; 13 | result.error.message = message; 14 | return result; 15 | } 16 | 17 | 18 | export default { 19 | get internalServerError(){ 20 | return build(-32603, 'Internal Server Error'); 21 | }, 22 | 23 | get noValidSessionId(){ 24 | return build(-32000, 'No valid session ID'); 25 | }, 26 | 27 | get invalidOrMissingSessionId(){ 28 | return build(-32000, 'Invalid or missing session ID'); 29 | }, 30 | 31 | get methodNotAllowed(){ 32 | return build(-32000, 'Method not allowed'); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/mcp-server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import metadata from "./metadata.js"; 3 | 4 | let SHORT_DELAY = true; 5 | const LONG_DELAY_MS = 100; 6 | const SHORT_DELAY_MS = 50; 7 | 8 | const create = () => { 9 | const mcpServer = new McpServer({ 10 | name: "demo-mcp-server", 11 | version: metadata.version 12 | }, { 13 | capabilities: { 14 | tools: {} 15 | } 16 | }); 17 | 18 | mcpServer.tool("ping", async () => { 19 | const startTime = Date.now(); 20 | SHORT_DELAY=!SHORT_DELAY; 21 | 22 | if (SHORT_DELAY){ 23 | await new Promise((resolve) => setTimeout(resolve, SHORT_DELAY_MS)); 24 | } else { 25 | await new Promise((resolve) => setTimeout(resolve, LONG_DELAY_MS)); 26 | } 27 | const duration = Date.now() - startTime; 28 | 29 | return { 30 | content: [ 31 | { 32 | type: "text", 33 | text: `pong! taskId=${metadata.taskId} v=${metadata.version} d=${duration}` 34 | } 35 | ] 36 | } 37 | }); 38 | 39 | return mcpServer 40 | }; 41 | 42 | export default { create }; 43 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/metadata.js: -------------------------------------------------------------------------------- 1 | const ECS_METADATA_URI = process.env.ECS_CONTAINER_METADATA_URI_V4; 2 | import packageInfo from './package.json' with { type: 'json'}; 3 | 4 | const metadata = { 5 | } 6 | 7 | async function init() { 8 | metadata.version = packageInfo.version; 9 | 10 | if (!ECS_METADATA_URI) return; 11 | 12 | const resp = await fetch(`${ECS_METADATA_URI}/task`); 13 | const respJson = await resp.json(); 14 | metadata.taskId = respJson.TaskARN.split(':')[5]; 15 | } 16 | 17 | export default { 18 | init, 19 | 20 | get all() { 21 | return metadata; 22 | }, 23 | 24 | get version() { 25 | return metadata.version; 26 | }, 27 | 28 | get taskId() { 29 | return metadata.taskId; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.0.14", 4 | "author": "Anton Aleksandrov", 5 | "license": "Apache-2.0", 6 | "description": "A sample stateless MCP Server running on ECS Fargate", 7 | "dependencies": { 8 | "@modelcontextprotocol/sdk": "^1.11.0", 9 | "express": "^5.1.0", 10 | "log4js": "^6.9.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/src/mcpserver/transport.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 3 | import mcpServer from './mcp-server.js'; 4 | import mcpErrors from './mcp-errors.js'; 5 | 6 | const MCP_PATH = '/mcp'; 7 | 8 | const l = log4js.getLogger(); 9 | 10 | const bootstrap = async (app) => { 11 | app.post(MCP_PATH, postRequestHandler); 12 | app.get(MCP_PATH, sessionRequestHandler); 13 | app.delete(MCP_PATH, sessionRequestHandler); 14 | } 15 | 16 | const postRequestHandler = async (req, res) => { 17 | try { 18 | // Create new instances of MCP Server and Transport for each incoming request 19 | const newMcpServer = mcpServer.create(); 20 | const transport = new StreamableHTTPServerTransport({ 21 | // This is a stateless MCP server, so we don't need to keep track of sessions 22 | sessionIdGenerator: undefined, 23 | 24 | // Uncomment if you want to disable SSE in responses 25 | // enableJsonResponse: true, 26 | }); 27 | 28 | res.on('close', () => { 29 | l.debug(`request processing complete`); 30 | transport.close(); 31 | newMcpServer.close(); 32 | }); 33 | await newMcpServer.connect(transport); 34 | await transport.handleRequest(req, res, req.body); 35 | } catch (err) { 36 | l.error(`Error handling MCP request ${err}`); 37 | if (!res.headersSent) { 38 | res.status(500).json(mcpErrors.internalServerError) 39 | } 40 | } 41 | } 42 | 43 | const sessionRequestHandler = async (req, res) => { 44 | res.status(405).set('Allow', 'POST').json(mcpErrors.methodNotAllowed); 45 | } 46 | 47 | export default { 48 | bootstrap 49 | } 50 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "mcp_server" { 2 | name = local.project_name 3 | internal = false 4 | load_balancer_type = "application" 5 | subnets = [aws_subnet.public1.id, aws_subnet.public2.id] 6 | drop_invalid_header_fields = true 7 | 8 | } 9 | 10 | resource "aws_lb_target_group" "mcp_server" { 11 | name = local.project_name 12 | port = local.ecs_task_container_port 13 | protocol = "HTTP" 14 | vpc_id = aws_vpc.main.id 15 | target_type = "ip" 16 | deregistration_delay = 60 17 | 18 | health_check { 19 | enabled = true 20 | path = "/health" 21 | healthy_threshold = 2 22 | unhealthy_threshold = 2 23 | timeout = 5 24 | interval = 10 25 | matcher = "200" 26 | } 27 | } 28 | 29 | # Use HTTP listered ONLY if you do not have a custom domain name registered 30 | # with Route53. Otherwise: 31 | # 1. Uncomment the route53.tf file 32 | # 2. Remove the HTTP listener and uncomment HTTPS listener in this file 33 | # 3. Update outputs.tf to print HTTPS endpoint instead of HTTP. 34 | # Only use HTTP for testing purposes!!! Never expose ANYTHING via plain 35 | # HTTP, use HTTPS only. 36 | resource "aws_lb_listener" "http" { 37 | load_balancer_arn = aws_lb.mcp_server.arn 38 | port = 80 39 | protocol = "HTTP" 40 | 41 | default_action { 42 | type = "forward" 43 | target_group_arn = aws_lb_target_group.mcp_server.arn 44 | } 45 | } 46 | 47 | # resource "aws_lb_listener" "https" { 48 | # load_balancer_arn = aws_lb.mcp_server.arn 49 | # port = 443 50 | # protocol = "HTTPS" 51 | # certificate_arn = aws_acm_certificate.mcp_server.arn 52 | 53 | # default_action { 54 | # type = "forward" 55 | # target_group_arn = aws_lb_target_group.mcp_server.arn 56 | # } 57 | 58 | # depends_on = [ aws_acm_certificate_validation.cert_validation ] 59 | # } 60 | 61 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "mcp_server" { 2 | name = local.project_name 3 | } 4 | 5 | resource "aws_ecs_cluster_capacity_providers" "mcp_server" { 6 | cluster_name = aws_ecs_cluster.mcp_server.name 7 | capacity_providers = ["FARGATE"] 8 | default_capacity_provider_strategy { 9 | base = 1 10 | weight = 100 11 | capacity_provider = "FARGATE" 12 | } 13 | } 14 | 15 | resource "aws_cloudwatch_log_group" "mcp_server" { 16 | name = "/ecs/${local.project_name}" 17 | retention_in_days = 7 18 | } 19 | 20 | 21 | resource "aws_iam_role" "ecs_task_role" { 22 | name = local.project_name 23 | 24 | assume_role_policy = jsonencode({ 25 | Version = "2012-10-17" 26 | Statement = [ 27 | { 28 | Action = "sts:AssumeRole" 29 | Effect = "Allow" 30 | Principal = { 31 | Service = "ecs-tasks.amazonaws.com" 32 | } 33 | }, 34 | ] 35 | }) 36 | } 37 | 38 | resource "aws_iam_role_policy_attachment" "ecs_task_role" { 39 | role = aws_iam_role.ecs_task_role.name 40 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 41 | } 42 | 43 | resource "aws_ecs_task_definition" "mcp_server" { 44 | family = local.project_name 45 | requires_compatibilities = ["FARGATE"] 46 | network_mode = "awsvpc" 47 | execution_role_arn = aws_iam_role.ecs_task_role.arn 48 | cpu = 512 49 | memory = 1024 50 | 51 | container_definitions = jsonencode([ 52 | { 53 | name = local.ecs_task_container_name 54 | image = local.ecs_task_container_image 55 | essential = true 56 | 57 | portMappings = [{ 58 | containerPort = local.ecs_task_container_port 59 | hostPort = local.ecs_task_container_port 60 | protocol = "tcp" 61 | }] 62 | 63 | logConfiguration = { 64 | logDriver = "awslogs" 65 | options = { 66 | awslogs-group = aws_cloudwatch_log_group.mcp_server.name 67 | awslogs-region = local.region 68 | awslogs-stream-prefix = "ecs" 69 | } 70 | } 71 | } 72 | ]) 73 | } 74 | 75 | resource "aws_ecs_service" "mcp_server" { 76 | name = local.project_name 77 | cluster = aws_ecs_cluster.mcp_server.id 78 | task_definition = aws_ecs_task_definition.mcp_server.arn 79 | 80 | desired_count = 3 81 | force_new_deployment = true 82 | launch_type = "FARGATE" 83 | platform_version = "LATEST" 84 | 85 | network_configuration { 86 | subnets = [aws_subnet.public1.id, aws_subnet.public2.id] 87 | 88 | # Public IP is used for demo purposes in order to pull image from the Public ECR. 89 | assign_public_ip = true 90 | } 91 | 92 | load_balancer { 93 | target_group_arn = aws_lb_target_group.mcp_server.arn 94 | container_name = local.ecs_task_container_name 95 | container_port = local.ecs_task_container_port 96 | } 97 | 98 | triggers = { 99 | redeploy = plantimestamp() 100 | } 101 | 102 | depends_on = [ aws_iam_role.ecs_task_role ] # aws_acm_certificate_validation.cert_validation ] 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | r53_zone_name = "your-zone-name" # optional, not used by default 3 | ecr_alias = "your-ecr-alias" 4 | 5 | project_name = "stateless-mcp-on-ecs" 6 | region = "us-east-1" 7 | az1 = "${local.region}a" 8 | az2 = "${local.region}b" 9 | vpc_cidr = "11.0.0.0/16" 10 | subnet1_cidr = "11.0.1.0/24" 11 | subnet2_cidr = "11.0.2.0/24" 12 | ecs_task_container_name = "mcp_server" 13 | ecs_task_container_image = "public.ecr.aws/${local.ecr_alias}/stateless-mcp-on-ecs:latest" 14 | ecs_task_container_port = 3000 15 | } 16 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "mcp_endpoint" { 2 | value = "http://${aws_lb.mcp_server.dns_name}/mcp" 3 | } -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = local.region 12 | } 13 | -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/route53.tf: -------------------------------------------------------------------------------- 1 | # resource "aws_acm_certificate" "mcp_server"{ 2 | # domain_name = "${local.project_name}.${local.r53_zone_name}" 3 | # validation_method = "DNS" 4 | # lifecycle { 5 | # create_before_destroy = true 6 | # } 7 | # } 8 | 9 | # data "aws_route53_zone" "zone" { 10 | # name = local.r53_zone_name 11 | # private_zone = false 12 | # } 13 | 14 | # resource "aws_route53_record" "cert_validation" { 15 | # for_each = { 16 | # for dvo in aws_acm_certificate.mcp_server.domain_validation_options : dvo.domain_name => { 17 | # name = dvo.resource_record_name 18 | # record = dvo.resource_record_value 19 | # type = dvo.resource_record_type 20 | # } 21 | # } 22 | 23 | # allow_overwrite = true 24 | # name = each.value.name 25 | # records = [each.value.record] 26 | # ttl = 60 27 | # type = each.value.type 28 | # zone_id = data.aws_route53_zone.zone.zone_id 29 | # } 30 | 31 | # resource "aws_acm_certificate_validation" "cert_validation" { 32 | # certificate_arn = aws_acm_certificate.mcp_server.arn 33 | # validation_record_fqdns = [for record in aws_route53_record.cert_validation: record.fqdn] 34 | # } 35 | 36 | # resource "aws_route53_record" "mcp_server" { 37 | # zone_id = data.aws_route53_zone.zone.zone_id 38 | # name = aws_acm_certificate.mcp_server.domain_name 39 | # type = "A" 40 | 41 | # alias { 42 | # name = aws_lb.mcp_server.dns_name 43 | # zone_id = aws_lb.mcp_server.zone_id 44 | # evaluate_target_health = true 45 | # } 46 | 47 | # depends_on = [ aws_acm_certificate_validation.cert_validation ] 48 | # } -------------------------------------------------------------------------------- /stateless-mcp-on-ecs-nodejs/terraform/vpc.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "main" { 2 | cidr_block = local.vpc_cidr 3 | 4 | enable_dns_support = true 5 | enable_dns_hostnames = true 6 | 7 | tags = { 8 | Name = local.project_name 9 | } 10 | } 11 | 12 | resource "aws_internet_gateway" "main" { 13 | vpc_id = aws_vpc.main.id 14 | 15 | tags = { 16 | Name = local.project_name 17 | } 18 | } 19 | 20 | # The following networking configuration is for demo purposes ONLY. 21 | # You should ALWAYS implement least privilege access using private subnets, ACLs, and 22 | # security groups with specific ingress/egress rules, ports, and cidr blocks. 23 | resource "aws_default_route_table" "main" { 24 | default_route_table_id = aws_vpc.main.default_route_table_id 25 | route { 26 | cidr_block = "0.0.0.0/0" 27 | gateway_id = aws_internet_gateway.main.id 28 | } 29 | 30 | tags = { 31 | Name = local.project_name 32 | } 33 | } 34 | 35 | resource "aws_subnet" "public1" { 36 | vpc_id = aws_vpc.main.id 37 | cidr_block = local.subnet1_cidr 38 | availability_zone = local.az1 39 | 40 | tags = { 41 | Name = "${local.project_name}-public1" 42 | } 43 | } 44 | 45 | resource "aws_subnet" "public2" { 46 | vpc_id = aws_vpc.main.id 47 | cidr_block = local.subnet2_cidr 48 | availability_zone = local.az2 49 | 50 | tags = { 51 | Name = "${local.project_name}-public2" 52 | } 53 | } 54 | 55 | resource "aws_default_security_group" "main" { 56 | vpc_id = aws_vpc.main.id 57 | 58 | ingress { 59 | protocol = "-1" 60 | from_port = 0 61 | to_port = 0 62 | cidr_blocks = ["0.0.0.0/0"] 63 | } 64 | 65 | egress { 66 | protocol = "-1" 67 | from_port = 0 68 | to_port = 0 69 | cidr_blocks = ["0.0.0.0/0"] 70 | } 71 | 72 | tags = { 73 | Name = local.project_name 74 | } 75 | } -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Stateless MCP Server on AWS Lambda 2 | 3 | This is a sample MCP Server running natively on AWS Lambda and API Gateway without any extra bridging components or custom transports. This is now possible thanks to the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport introduced in v2025-03-26. 4 | 5 | ![](architecture.png) 6 | 7 | ## Prereqs 8 | 9 | * AWS CLI 10 | * Terraform 11 | 12 | ## Instructions 13 | 14 | ### Clone the project 15 | 16 | ```bash 17 | git clone https://github.com/aws-samples/sample-serverless-mcp-servers.git 18 | cd sample-serverless-mcp-servers/stateless-mcp-on-lambda-nodejs 19 | ``` 20 | 21 | ### Install dependencies 22 | 23 | ```bash 24 | (cd src/mcpclient && npm install) 25 | (cd src/mcpserver && npm install) 26 | ``` 27 | 28 | ### Тest the server locally 29 | 30 | ```bash 31 | node src/mcpserver/index.js 32 | ``` 33 | 34 | Once the server is running, run client in a separate terminal window 35 | 36 | ```bash 37 | node src/mcpclient/index.js 38 | ``` 39 | 40 | ### Deploy to AWS with Terraform 41 | 42 | Run below commands to deploy the sample to AWS 43 | 44 | ```bash 45 | cd terraform 46 | terraform init 47 | terraform plan 48 | terraform apply 49 | export MCP_SERVER_ENDPOINT=$(terraform output --raw mcp_endpoint) 50 | cd .. 51 | ``` 52 | 53 | Once deployment has completed, it might take about a minute for API Gateway endpoint to become available. 54 | 55 | ### Test your remote MCP Server with MCP client: 56 | ```bash 57 | node src/mcpclient/index.js 58 | ``` 59 | 60 | Observe the response: 61 | ```bash 62 | Connecting ENDPOINT_URL=https://nh0u4q5dcf.execute-api.us-east-1.amazonaws.com/dev/mcp 63 | connected 64 | listTools response: { tools: [ { name: 'ping', inputSchema: [Object] } ] } 65 | callTool:ping response: { 66 | content: [ 67 | { 68 | type: 'text', 69 | text: 'pong! logStream=2025/05/06/[$LATEST]7037eebd7f314fa18d6320801a54a50f v=0.0.12 d=49' 70 | } 71 | ] 72 | } 73 | callTool:ping response: { 74 | content: [ 75 | { 76 | type: 'text', 77 | text: 'pong! logStream=2025/05/06/[$LATEST]7037eebd7f314fa18d6320801a54a50f v=0.0.12 d=101' 78 | } 79 | ] 80 | } 81 | ``` 82 | 83 | ## Statefull vs Stateless considerations 84 | 85 | MCP Server can run in two modes - stateless and stateful. This repo demonstrates the stateless mode. 86 | 87 | In stateless mode, clients do not establish persistent SSE connections to MCP Server. This means clients will not receive proactive notifications from the server. On the other hand, stateless mode allows you to scale your server horizontally. 88 | 89 | If you want to see how to built a stateful MCP Server, that supports persistent SSE connections, see the `stateful-mcp-on-ecs` sample in this repo. 90 | 91 | ## Authorization demo 92 | 93 | This sample implements simple authorization demo with API Gateway Custom Authorizer. To enable authorization, update the `aws_api_gateway_method` resource in `terraform/apigateway.tf`, and change authorization to CUSTOM. 94 | 95 | See transport initalization in `src/client.js` for how to add a custom authorization header. 96 | 97 | ## Cost considerations 98 | 99 | This sample provisions paid resources in your account, such as ECS Tasks and ALB. Remember to delete these resources when you're done evaluating. 100 | 101 | ```bash 102 | terraform destroy 103 | ``` 104 | 105 | ## Learn about mcp 106 | [Intro](https://modelcontextprotocol.io/introduction) 107 | 108 | [Protocol specification](https://modelcontextprotocol.io/specification/2025-03-26) 109 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-mcp-servers/c276bc74d5d60b2875c44a2423de094e72aad4ee/stateless-mcp-on-lambda-nodejs/architecture.png -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/authorizer/index.js: -------------------------------------------------------------------------------- 1 | 2 | export const handler = async (event) => { 3 | const authToken = event.authorizationToken; 4 | if (!authToken) { 5 | return generatePolicy('Deny', event.methodArn); 6 | } 7 | 8 | if (authToken === 'Bearer good_access_token') { 9 | return generatePolicy('Allow', event.methodArn); 10 | } 11 | 12 | return generatePolicy('Deny', event.methodArn); 13 | 14 | }; 15 | 16 | const generatePolicy = (effect, resource) => { 17 | return { 18 | principalId: 'user', 19 | policyDocument: { 20 | Version: '2012-10-17', 21 | Statement: [{ 22 | Action: 'execute-api:Invoke', 23 | Effect: effect, 24 | Resource: resource 25 | }] 26 | } 27 | }; 28 | }; -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/authorizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": "Anton Aleksandrov", 4 | "license": "Apache-2.0", 5 | "description": "A simple API Gateway authorizer" 6 | } 7 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpclient/index.js: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 3 | 4 | const ENDPOINT_URL = process.env.MCP_SERVER_ENDPOINT || 'http://localhost:3000/mcp'; 5 | // Change to good_access_token for successful authorization when authorization is enabled 6 | const fake_token = 'bad_access_token'; 7 | 8 | console.log(`Connecting ENDPOINT_URL=${ENDPOINT_URL}`); 9 | 10 | const transport = new StreamableHTTPClientTransport(new URL(ENDPOINT_URL), { 11 | requestInit: { 12 | headers: { 13 | 'Authorization': `Bearer ${fake_token}` 14 | } 15 | } 16 | }); 17 | 18 | const client = new Client({ 19 | name: "node-client", 20 | version: "0.0.1" 21 | }) 22 | 23 | await client.connect(transport); 24 | console.log('connected'); 25 | 26 | const tools = await client.listTools(); 27 | console.log(`listTools response: `, tools); 28 | 29 | for (let i = 0; i < 2; i++) { 30 | let result = await client.callTool({ 31 | name: "ping" 32 | }); 33 | console.log(`callTool:ping response: `, result); 34 | } 35 | 36 | await client.close(); 37 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": "Anton Aleksandrov", 4 | "license": "Apache-2.0", 5 | "description": "A simple MCP Client", 6 | "dependencies": { 7 | "@modelcontextprotocol/sdk": "^1.11.0", 8 | "fetch-cookie": "^3.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/index.js: -------------------------------------------------------------------------------- 1 | import './logging.js'; 2 | import log4js from 'log4js'; 3 | import express from 'express'; 4 | import metadata from './metadata.js'; 5 | import transport from './transport.js'; 6 | 7 | await metadata.init(); 8 | 9 | const l = log4js.getLogger(); 10 | const PORT = 3000; 11 | 12 | // This function is using Lambda Web Adapter to run express.js on Lambda 13 | // https://github.com/awslabs/aws-lambda-web-adapter 14 | const app = express(); 15 | app.use(express.json()); 16 | 17 | app.get('/health', (req, res) => { 18 | res.json(metadata.all); 19 | }); 20 | 21 | app.use(async (req, res, next) => { 22 | l.debug(`> ${req.method} ${req.originalUrl}`); 23 | l.debug(req.body); 24 | // l.debug(req.headers); 25 | return next(); 26 | }); 27 | 28 | await transport.bootstrap(app); 29 | 30 | app.listen(PORT, () => { 31 | l.debug(metadata.all); 32 | l.debug(`listening on http://localhost:${PORT}`); 33 | }); 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/logging.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | 3 | const layout = { 4 | type: 'pattern', 5 | pattern: '%p [%f{1}:%l:%M] %m%' 6 | } 7 | 8 | log4js.configure({ 9 | appenders: { 10 | stdout: { 11 | type: 'stdout', 12 | enableCallStack: true, 13 | layout 14 | } 15 | }, 16 | categories: { 17 | default: { 18 | appenders: ['stdout'], 19 | level: 'debug', 20 | enableCallStack: true 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/mcp-errors.js: -------------------------------------------------------------------------------- 1 | const TEMPLATE = { 2 | jsonrpc: '2.0', 3 | error: { 4 | code: 0, 5 | message: 'n/a', 6 | }, 7 | id: null, 8 | }; 9 | 10 | function build(code, message){ 11 | const result = {...TEMPLATE}; 12 | result.error.code = code; 13 | result.error.message = message; 14 | return result; 15 | } 16 | 17 | 18 | export default { 19 | get internalServerError(){ 20 | return build(-32603, 'Internal Server Error'); 21 | }, 22 | 23 | get noValidSessionId(){ 24 | return build(-32000, 'No valid session ID'); 25 | }, 26 | 27 | get invalidOrMissingSessionId(){ 28 | return build(-32000, 'Invalid or missing session ID'); 29 | }, 30 | 31 | get methodNotAllowed(){ 32 | return build(-32000, 'Method not allowed'); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/mcp-server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import metadata from "./metadata.js"; 3 | 4 | let SHORT_DELAY = true; 5 | const LONG_DELAY_MS = 100; 6 | const SHORT_DELAY_MS = 50; 7 | 8 | const create = () => { 9 | const mcpServer = new McpServer({ 10 | name: "demo-mcp-server", 11 | version: metadata.version 12 | }, { 13 | capabilities: { 14 | tools: {} 15 | } 16 | }); 17 | 18 | mcpServer.tool("ping", async () => { 19 | const startTime = Date.now(); 20 | SHORT_DELAY=!SHORT_DELAY; 21 | 22 | if (SHORT_DELAY){ 23 | await new Promise((resolve) => setTimeout(resolve, SHORT_DELAY_MS)); 24 | } else { 25 | await new Promise((resolve) => setTimeout(resolve, LONG_DELAY_MS)); 26 | } 27 | const duration = Date.now() - startTime; 28 | 29 | return { 30 | content: [ 31 | { 32 | type: "text", 33 | text: `pong! logStream=${metadata.logStreamName} v=${metadata.version} d=${duration}` 34 | } 35 | ] 36 | } 37 | }); 38 | 39 | return mcpServer 40 | }; 41 | 42 | export default { create }; 43 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/metadata.js: -------------------------------------------------------------------------------- 1 | import packageInfo from './package.json' with { type: 'json'}; 2 | 3 | const metadata = { 4 | } 5 | 6 | async function init() { 7 | metadata.version = packageInfo.version; 8 | metadata.logStreamName = process.env.AWS_LAMBDA_LOG_STREAM_NAME || 'unknown'; 9 | } 10 | 11 | export default { 12 | init, 13 | 14 | get all() { 15 | return metadata; 16 | }, 17 | 18 | get version() { 19 | return metadata.version; 20 | }, 21 | 22 | get logStreamName() { 23 | return metadata.logStreamName; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.0.12", 4 | "author": "Anton Aleksandrov", 5 | "license": "Apache-2.0", 6 | "description": "A sample stateless MCP Server running on AWS Lambda", 7 | "dependencies": { 8 | "@modelcontextprotocol/sdk": "^1.11.0", 9 | "express": "^5.1.0", 10 | "log4js": "^6.9.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node index.js -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/src/mcpserver/transport.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 3 | import mcpServer from './mcp-server.js'; 4 | import mcpErrors from './mcp-errors.js'; 5 | 6 | const MCP_PATH = '/mcp'; 7 | 8 | const l = log4js.getLogger(); 9 | 10 | const bootstrap = async (app) => { 11 | app.post(MCP_PATH, postRequestHandler); 12 | app.get(MCP_PATH, sessionRequestHandler); 13 | app.delete(MCP_PATH, sessionRequestHandler); 14 | } 15 | 16 | const postRequestHandler = async (req, res) => { 17 | try { 18 | // Create new instances of MCP Server and Transport for each incoming request 19 | const newMcpServer = mcpServer.create(); 20 | const transport = new StreamableHTTPServerTransport({ 21 | // This is a stateless MCP server, so we don't need to keep track of sessions 22 | sessionIdGenerator: undefined, 23 | 24 | // Change to `false` if you want to enable SSE in responses. 25 | enableJsonResponse: true, 26 | }); 27 | 28 | res.on('close', () => { 29 | l.debug(`request processing complete`); 30 | transport.close(); 31 | newMcpServer.close(); 32 | }); 33 | await newMcpServer.connect(transport); 34 | await transport.handleRequest(req, res, req.body); 35 | } catch (err) { 36 | l.error(`Error handling MCP request ${err}`); 37 | if (!res.headersSent) { 38 | res.status(500).json(mcpErrors.internalServerError) 39 | } 40 | } 41 | } 42 | 43 | const sessionRequestHandler = async (req, res) => { 44 | res.status(405).set('Allow', 'POST').json(mcpErrors.methodNotAllowed); 45 | } 46 | 47 | export default { 48 | bootstrap 49 | } 50 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/apigateway.tf: -------------------------------------------------------------------------------- 1 | resource "aws_api_gateway_rest_api" "api" { 2 | name = local.project_name 3 | lifecycle { 4 | create_before_destroy = true 5 | } 6 | } 7 | 8 | resource "aws_api_gateway_resource" "mcp" { 9 | rest_api_id = aws_api_gateway_rest_api.api.id 10 | parent_id = aws_api_gateway_rest_api.api.root_resource_id 11 | path_part = "mcp" 12 | } 13 | 14 | resource "aws_api_gateway_method" "any" { 15 | rest_api_id = aws_api_gateway_rest_api.api.id 16 | resource_id = aws_api_gateway_resource.mcp.id 17 | # Change authorization from NONE to CUSTOM to enable custom Lambda authorizer 18 | # Note: it might take up to 60 seconds for API Gateway configuration 19 | # change to take effect 20 | authorization = "NONE" 21 | authorizer_id = aws_api_gateway_authorizer.authorizer.id 22 | http_method = "ANY" 23 | } 24 | 25 | resource "aws_api_gateway_integration" "lambda" { 26 | rest_api_id = aws_api_gateway_rest_api.api.id 27 | resource_id = aws_api_gateway_resource.mcp.id 28 | http_method = aws_api_gateway_method.any.http_method 29 | integration_http_method = "POST" 30 | type = "AWS_PROXY" 31 | uri = aws_lambda_function.mcp_server.invoke_arn 32 | } 33 | 34 | resource "aws_lambda_permission" "apigw_to_mcp_server" { 35 | statement_id = "AllowExecutionFromAPIGateway" 36 | action = "lambda:InvokeFunction" 37 | function_name = aws_lambda_function.mcp_server.function_name 38 | principal = "apigateway.amazonaws.com" 39 | source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*/*" 40 | } 41 | 42 | resource "aws_api_gateway_deployment" "api" { 43 | rest_api_id = aws_api_gateway_rest_api.api.id 44 | depends_on = [aws_api_gateway_method.any, aws_api_gateway_integration.lambda] 45 | lifecycle { 46 | create_before_destroy = true 47 | } 48 | triggers = { 49 | redeployment = timestamp() //always 50 | } 51 | } 52 | 53 | resource "aws_api_gateway_stage" "dev" { 54 | rest_api_id = aws_api_gateway_rest_api.api.id 55 | deployment_id = aws_api_gateway_deployment.api.id 56 | stage_name = "dev" 57 | } 58 | 59 | resource "aws_api_gateway_authorizer" "authorizer" { 60 | name = "mcp-authorizer" 61 | rest_api_id = aws_api_gateway_rest_api.api.id 62 | type = "TOKEN" 63 | authorizer_uri = aws_lambda_function.authorizer.invoke_arn 64 | authorizer_result_ttl_in_seconds = 0 65 | identity_source = "method.request.header.Authorization" 66 | # authorizer_uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.authorizer.arn}/invocations" 67 | } 68 | 69 | resource "aws_lambda_permission" "apigw_to_authorizer" { 70 | statement_id = "AllowExecutionFromAPIGateway" 71 | action = "lambda:InvokeFunction" 72 | function_name = aws_lambda_function.authorizer.function_name 73 | principal = "apigateway.amazonaws.com" 74 | source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/authorizers/*" 75 | } 76 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/lambda_authorizer.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "authorizer" { 2 | name = "${local.project_name}-authorizer" 3 | assume_role_policy = jsonencode({ 4 | Version = "2012-10-17" 5 | Statement = [ 6 | { 7 | Action = "sts:AssumeRole" 8 | Effect = "Allow" 9 | Principal = { 10 | Service = "lambda.amazonaws.com" 11 | } 12 | } 13 | ] 14 | }) 15 | } 16 | 17 | resource "aws_iam_role_policy_attachment" "authorizer" { 18 | role = aws_iam_role.authorizer.name 19 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 20 | } 21 | 22 | data "archive_file" "authorizer" { 23 | type = "zip" 24 | source_dir = "${path.root}/../src/authorizer" 25 | output_path = "${path.root}/tmp/authorizer.zip" 26 | } 27 | 28 | resource "aws_lambda_function" "authorizer" { 29 | function_name = "${local.project_name}-authorizer" 30 | filename = data.archive_file.authorizer.output_path 31 | source_code_hash = data.archive_file.authorizer.output_base64sha256 32 | role = aws_iam_role.authorizer.arn 33 | handler = "index.handler" 34 | runtime = "nodejs22.x" 35 | memory_size = 256 36 | timeout = 5 37 | } 38 | 39 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/lambda_mcpserver.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "mcp_server" { 2 | name = local.project_name 3 | assume_role_policy = jsonencode({ 4 | Version = "2012-10-17" 5 | Statement = [ 6 | { 7 | Action = "sts:AssumeRole" 8 | Effect = "Allow" 9 | Principal = { 10 | Service = "lambda.amazonaws.com" 11 | } 12 | } 13 | ] 14 | }) 15 | } 16 | 17 | resource "aws_iam_role_policy_attachment" "mcp_server" { 18 | role = aws_iam_role.mcp_server.name 19 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 20 | } 21 | 22 | data "archive_file" "mcp_server" { 23 | type = "zip" 24 | source_dir = "${path.root}/../src/mcpserver" 25 | output_path = "${path.root}/tmp/mcpserver.zip" 26 | } 27 | 28 | resource "aws_lambda_function" "mcp_server" { 29 | function_name = local.project_name 30 | filename = data.archive_file.mcp_server.output_path 31 | source_code_hash = data.archive_file.mcp_server.output_base64sha256 32 | role = aws_iam_role.mcp_server.arn 33 | handler = "run.sh" 34 | runtime = "nodejs22.x" 35 | memory_size = 512 36 | timeout = 10 37 | layers = [ 38 | "arn:aws:lambda:${local.region}:753240598075:layer:LambdaAdapterLayerX86:25" 39 | ] 40 | environment { 41 | variables = { 42 | AWS_LWA_PORT = "3000" 43 | AWS_LAMBDA_EXEC_WRAPPER = "/opt/bootstrap" 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | project_name = "stateless-mcp-on-lambda" 3 | region = "us-east-1" 4 | } 5 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "mcp_endpoint" { 2 | value = "${aws_api_gateway_stage.dev.invoke_url}/${aws_api_gateway_resource.mcp.path_part}" 3 | } -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-nodejs/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = local.region 12 | } 13 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | build 3 | 4 | # files 5 | *_output.yaml 6 | build.toml -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/README.md: -------------------------------------------------------------------------------- 1 | # Stateless MCP on AWS Lambda (Python) 2 | 3 | This project demonstrates how to deploy a stateless MCP (Model Context Protocol) server on AWS Lambda using Python. The implementation uses Amazon API Gateway for HTTP endpoints and AWS Lambda as the serverless compute backend. 4 | 5 | ## Prerequisites 6 | 7 | - AWS CLI configured with appropriate credentials 8 | - Python 3.x 9 | - Make 10 | - AWS SAM CLI 11 | - Docker Desktop or Podman for local builds 12 | - MCP Inspector tool for testing 13 | 14 | ## Project Structure 15 | 16 | ``` 17 | stateless-mcp-on-lambda-python/ 18 | ├── build/ # Build artifacts 19 | ├── etc/ # Configuration files 20 | ├── sam/ # SAM template files 21 | ├── src/ # Source code 22 | ├── tmp/ # Temporary files 23 | └── makefile # Build and deployment commands 24 | ``` 25 | 26 | ## Configuration 27 | 28 | Before deploying, you need to configure the environment variables in `etc/environment.sh`: 29 | 30 | 1. AWS Configuration: 31 | - `PROFILE`: Your AWS CLI profile name 32 | - `BUCKET`: S3 bucket name for deployment artifacts 33 | - `REGION`: AWS region (default: us-east-1) 34 | 35 | 2. MCP Dependencies: 36 | - `P_DESCRIPTION`: MCP package version (default: "mcp==1.8.0") 37 | - `O_LAYER_ARN`: This will be updated after creating the Lambda layer 38 | 39 | 3. API Gateway and Lambda Configuration: 40 | - `P_API_STAGE`: API Gateway stage name (default: dev) 41 | - `P_FN_MEMORY`: Lambda function memory in MB (default: 128) 42 | - `P_FN_TIMEOUT`: Lambda function timeout in seconds (default: 15) 43 | 44 | ## Deployment Steps 45 | 46 | 1. Create the Lambda Layer: 47 | ```bash 48 | make layer 49 | ``` 50 | After execution, copy the `outLayer` value and update the `O_LAYER_ARN` in `etc/environment.sh`. 51 | 52 | 2. Deploy the API Gateway and Lambda function: 53 | ```bash 54 | make apigw 55 | ``` 56 | This will create the API Gateway and Lambda function, which will have the MCP dependencies layer attached. 57 | 58 | The provided `template.yaml` file assumes a deployment in us-east-1. If deploying to an alternate region, update the ARN for the [Lambda Insights extension](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html) and the [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter?tab=readme-ov-file#lambda-functions-packaged-as-zip-package-for-aws-managed-runtimes). 59 | 60 | ## Testing 61 | 62 | 1. After deployment, you'll receive an `outApiEndpoint` value. 63 | 2. Use [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector#py-pi-package) to test the endpoint: 64 | - After setting up MCP Inspector, you can start the tool with the following command: `mcp dev src/mcpserver/server.py` 65 | - Enter the following URL in MCP Inspector: `${outApiEndpoint}/echo/mcp/` 66 | 67 | ## Make Commands 68 | 69 | - `make layer`: Creates the Lambda layer with MCP dependencies 70 | - `make apigw`: Deploys the API Gateway and Lambda function 71 | 72 | ## Troubleshooting 73 | 74 | If you encounter any issues: 75 | 1. Ensure all environment variables are properly set in `etc/environment.sh` 76 | 2. Verify AWS credentials are correctly configured 77 | 3. Check AWS CloudWatch logs for Lambda function errors 78 | 4. Ensure the S3 bucket specified in `BUCKET` exists, is accessible, and has versioning enabled 79 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/etc/environment.sh: -------------------------------------------------------------------------------- 1 | # aws configuration 2 | PROFILE=your-cli-profile 3 | BUCKET=your-cli-bucket 4 | REGION=us-east-1 5 | 6 | # mcp dependencies 7 | P_DESCRIPTION="mcp==1.8.0" 8 | LAYER_STACK=mcp-lambda-layer 9 | LAYER_TEMPLATE=sam/layer.yaml 10 | LAYER_OUTPUT=sam/layer_output.yaml 11 | LAYER_PARAMS="ParameterKey=description,ParameterValue=${P_DESCRIPTION}" 12 | O_LAYER_ARN=your-output-layer-arn 13 | 14 | # api gateway and lambdastack 15 | P_API_STAGE=dev 16 | P_FN_MEMORY=128 17 | P_FN_TIMEOUT=15 18 | APIGW_STACK=mcp-apigw 19 | APIGW_TEMPLATE=sam/template.yaml 20 | APIGW_OUTPUT=sam/template_output.yaml 21 | APIGW_PARAMS="ParameterKey=apiStage,ParameterValue=${P_API_STAGE} ParameterKey=fnMemory,ParameterValue=${P_FN_MEMORY} ParameterKey=fnTimeout,ParameterValue=${P_FN_TIMEOUT} ParameterKey=dependencies,ParameterValue=${O_LAYER_ARN}" 22 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/makefile: -------------------------------------------------------------------------------- 1 | include etc/environment.sh 2 | 3 | # dependencies 4 | layer: layer.build layer.package layer.deploy 5 | layer.build: 6 | sam build -t ${LAYER_TEMPLATE} --parameter-overrides ${LAYER_PARAMS} --build-dir build --manifest src/dependencies/requirements.txt --use-container 7 | layer.package: 8 | sam package -t build/template.yaml --region ${REGION} --output-template-file ${LAYER_OUTPUT} --s3-bucket ${BUCKET} --s3-prefix ${LAYER_STACK} 9 | layer.deploy: 10 | sam deploy -t ${LAYER_OUTPUT} --region ${REGION} --stack-name ${LAYER_STACK} --parameter-overrides ${LAYER_PARAMS} --capabilities CAPABILITY_NAMED_IAM 11 | 12 | # api gateway 13 | apigw: apigw.package apigw.deploy 14 | apigw.package: 15 | sam package -t ${APIGW_TEMPLATE} --output-template-file ${APIGW_OUTPUT} --s3-bucket ${BUCKET} --s3-prefix ${APIGW_STACK} 16 | apigw.deploy: 17 | sam deploy -t ${APIGW_OUTPUT} --region ${REGION} --stack-name ${APIGW_STACK} --parameter-overrides ${APIGW_PARAMS} --capabilities CAPABILITY_NAMED_IAM 18 | apigw.delete: 19 | sam delete --stack-name ${APIGW_STACK} 20 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/sam/layer.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: MCP dependencies for Lambda 3 | Transform: AWS::Serverless-2016-10-31 4 | Parameters: 5 | description: 6 | Type: String 7 | Resources: 8 | LayerMcp: 9 | Type: AWS::Serverless::LayerVersion 10 | Properties: 11 | CompatibleRuntimes: 12 | - python3.11 13 | - python3.12 14 | ContentUri: src/dependencies 15 | Description: !Ref description 16 | LayerName: mcp-dependencies-python3 17 | Metadata: 18 | BuildMethod: python3.12 19 | BuildProperties: 20 | UseContainer: true 21 | Outputs: 22 | outLayer: 23 | Value: !Ref LayerMcp -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/sam/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: apigw-mcp 4 | description: API Gateway endpoint for MCP server 5 | version: 0.1.0 6 | license: 7 | name: Apache 2.0 8 | url: https://www.apache.org/licenses/LICENSE-2.0.html 9 | 10 | components: 11 | schemas: 12 | error: 13 | type: object 14 | properties: 15 | code: 16 | type: integer 17 | format: int32 18 | message: 19 | type: string 20 | responses: 21 | error: 22 | description: internal server error 23 | content: 24 | application/json: 25 | schema: 26 | $ref: "#/components/schemas/error" 27 | example: 28 | code: 500 29 | message: "unable to retrieve message" 30 | 31 | paths: 32 | /echo/mcp: 33 | post: 34 | summary: echo 35 | description: endpoint for mcp server 36 | responses: 37 | 200: 38 | description: ok 39 | 500: 40 | $ref: "#/components/responses/error" 41 | default: 42 | $ref: "#/components/responses/error" 43 | x-amazon-apigateway-integration: 44 | httpMethod: POST 45 | payloadFormatVersion: "1.0" 46 | responses: 47 | default: 48 | statusCode: 200 49 | type: AWS_PROXY 50 | uri: 51 | Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Fn}/invocations" 52 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/sam/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: MCP deployment on API Gateway and Lambda 3 | Transform: AWS::Serverless-2016-10-31 4 | Globals: 5 | Api: 6 | OpenApiVersion: 3.0.1 7 | Function: 8 | Layers: 9 | - arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:55 10 | - arn:aws:lambda:us-east-1:753240598075:layer:LambdaAdapterLayerX86:25 11 | - !Ref dependencies 12 | MemorySize: !Ref fnMemory 13 | Runtime: python3.12 14 | Timeout: !Ref fnTimeout 15 | Tracing: Active 16 | Parameters: 17 | apiStage: 18 | Type: String 19 | fnMemory: 20 | Type: Number 21 | fnTimeout: 22 | Type: Number 23 | dependencies: 24 | Type: String 25 | Resources: 26 | # APIGW 27 | Api: 28 | Type: AWS::Serverless::Api 29 | Properties: 30 | AccessLogSetting: 31 | DestinationArn: !GetAtt ApiLogGroup.Arn 32 | Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength", "auth.status":"$context.authorizer.status", "auth.error":"$context.authorizer.error", "auth.token":"$context.authorizer.token", "auth.reason":"$context.authorizer.reason", "auth.simple":"$context.authorizer.simple", "auth.pversion":"$context.authorizer.pversion" }' 33 | StageName: !Ref apiStage 34 | DefinitionBody: 35 | Fn::Transform: 36 | Name: AWS::Include 37 | Parameters: 38 | Location: openapi.yaml 39 | ApiLogGroup: 40 | Type: AWS::Logs::LogGroup 41 | Properties: 42 | LogGroupName: !Sub "/aws/apigateway/apigw-mcp-${Api}" 43 | RetentionInDays: 7 44 | # Lambda 45 | FnRole: 46 | Type: AWS::IAM::Role 47 | Properties: 48 | AssumeRolePolicyDocument: 49 | Version: '2012-10-17' 50 | Statement: 51 | - Effect: Allow 52 | Principal: 53 | Service: 54 | - lambda.amazonaws.com 55 | Action: 56 | - sts:AssumeRole 57 | Path: / 58 | Policies: 59 | - PolicyName: cloudwatch-insights 60 | PolicyDocument: 61 | Version: '2012-10-17' 62 | Statement: 63 | - Effect: Allow 64 | Action: 65 | - logs:CreateLogGroup 66 | Resource: '*' 67 | - PolicyName: cloudwatch-logs 68 | PolicyDocument: 69 | Version: '2012-10-17' 70 | Statement: 71 | - Effect: Allow 72 | Action: 73 | - logs:CreateLogStream 74 | - logs:PutLogEvents 75 | - logs:DescribeLogStreams 76 | Resource: 'arn:aws:logs:*:*:log-group:*:*' 77 | - PolicyName: xray 78 | PolicyDocument: 79 | Version: '2012-10-17' 80 | Statement: 81 | - Effect: Allow 82 | Action: 83 | - xray:PutTraceSegments 84 | - xray:PutTelemetryRecords 85 | - xray:GetSamplingRules 86 | - xray:GetSamplingTargets 87 | - xray:GetSamplingStatisticSummaries 88 | Resource: '*' 89 | Fn: 90 | Type: AWS::Serverless::Function 91 | Properties: 92 | CodeUri: ../src/mcpserver 93 | Handler: run.sh 94 | Role: !GetAtt FnRole.Arn 95 | Environment: 96 | Variables: 97 | AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap 98 | PORT: 8000 99 | Tags: 100 | application:group: example_group 101 | application:owner: example_owner 102 | FnLogGroup: 103 | Type: AWS::Logs::LogGroup 104 | Properties: 105 | LogGroupName: !Sub '/aws/lambda/${Fn}' 106 | RetentionInDays: 7 107 | FnPerm: 108 | Type: AWS::Lambda::Permission 109 | Properties: 110 | FunctionName: !GetAtt Fn.Arn 111 | Principal: apigateway.amazonaws.com 112 | Action: lambda:InvokeFunction 113 | SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${Api}/*/*/*' 114 | Outputs: 115 | outApi: 116 | Value: !Ref Api 117 | outApiEndpoint: 118 | Value: !Sub 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${apiStage}' 119 | outFn: 120 | Value: !Ref Fn 121 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/src/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.12 2 | fastmcp==2.3.0 3 | mcp==1.8.0 4 | pydantic==2.11.4 5 | uvicorn==0.34.2 -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/src/mcpserver/echo.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | 3 | mcp = FastMCP(name="EchoServer", stateless_http=True) 4 | 5 | @mcp.tool(description="A simple echo tool") 6 | def echo(message: str) -> str: 7 | return f"Echo: {message}" 8 | -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/src/mcpserver/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=$PATH:$LAMBDA_TASK_ROOT/bin 4 | export PYTHONPATH=$PYTHONPATH:/opt/python:$LAMBDA_RUNTIME_DIR 5 | exec python -m uvicorn --port=$PORT server:app -------------------------------------------------------------------------------- /stateless-mcp-on-lambda-python/src/mcpserver/server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from fastapi import FastAPI 3 | from echo import mcp 4 | 5 | app = FastAPI(title="Echo",lifespan=lambda app: mcp.session_manager.run()) 6 | app.mount("/echo", mcp.streamable_http_app()) 7 | 8 | if __name__ == "__main__": 9 | parser = argparse.ArgumentParser(description='Run the MCP server') 10 | parser.add_argument('--mode', 11 | choices=['stdio', 'streamable-http', 'fastapi'], 12 | default='fastapi', 13 | help='Server mode: stdio, streamable-http, or fastapi') 14 | parser.add_argument('--host', 15 | default='0.0.0.0', 16 | help='Host for FastAPI server (default: 0.0.0.0)') 17 | parser.add_argument('--port', 18 | type=int, 19 | default=8000, 20 | help='Port for FastAPI server (default: 8000)') 21 | 22 | args = parser.parse_args() 23 | match args.mode: 24 | case 'stdio': 25 | mcp.run(transport='stdio') 26 | case 'streamable-http': 27 | mcp.run(transport='streamable-http') 28 | case _: 29 | import uvicorn 30 | uvicorn.run(app, host=args.host, port=args.port, log_level="info") --------------------------------------------------------------------------------