├── .gitignore ├── README.md ├── __tests__ ├── add-tool │ ├── add-tool-edge-cases.test.mjs │ └── add-tool.test.mjs └── tool-list │ └── tool-list.test.mjs ├── jest.config.mjs ├── package-lock.json ├── package.json ├── serverless.yml └── src └── index.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧠 serverless-mcp-server 2 | A super simple Model Context Protocol (MCP) server deployed on AWS Lambda and exposed via Amazon API Gateway, deployed with Serverless Framework. 3 | This skeleton is based on the awesome work of [Frédéric Barthelet](https://github.com/fredericbarthelet): which has developed a middy middleware for Model Context Protocol (MCP) server integration with AWS Lambda functions in [this repo](https://github.com/fredericbarthelet/middy-mcp) 4 | 5 | ## Long story 6 | 📖[Read the full article here on dev.to](https://dev.to/aws-builders/deploy-a-minimal-mcp-server-on-aws-lambda-with-serverless-framework-3e42) 7 | 8 | ## 🛠 Features 9 | - 🪄 Minimal MCP server setup using @modelcontextprotocol/sdk 10 | - 🚀 Deployed as a single AWS Lambda function 11 | - 🌐 HTTP POST endpoint exposed via API Gateway at /mcp 12 | - 🔄 Supports local development via serverless-offline 13 | - 🧪 Includes a simple example tool (add) with JSON-RPC interaction 14 | 15 | ## 📦 Project Structure 16 | ``` 17 | serverless-mcp-server/ 18 | ├── src/ # Source code 19 | │ └── index.js # MCP server handler 20 | ├── .gitignore # Git ignore file 21 | ├── package.json # Project dependencies 22 | ├── package-lock.json # Project lock file 23 | ├── README.md # This documentation file 24 | └── serverless.yml # Serverless Framework config 25 | ``` 26 | 27 | ## 🛠 Prerequisites 28 | - Node.js v22+ 29 | - [Open Source Serverless](https://github.com/oss-serverless/serverless) or Serverless Framework v3+ 30 | - Serverless Offline 31 | 32 | ## 🚀 Getting Started 33 | 34 | 1. Install dependencies: 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | 2. Install open source severless globally (if not already installed): 40 | ```bash 41 | npm install -g osls 42 | ``` 43 | 44 | 3. Run Locally with serverless-offline 45 | ```bash 46 | npm sls offline 47 | ``` 48 | 49 | Local endpoint will be available at: 50 | POST `http://localhost:3000/dev/mcp` 51 | 52 | Note that the `/dev/` stage is added by default when using serverless-offline, reflecting Api Gateway V1 (REST API) behavior. 53 | 54 | ## Switch to Api Gateway V2 (HTTP API) 55 | If you want to use API Gateway V2, you can change the `serverless.yml` file to use `httpApi` instead of `http` in the `events` section. This will allow you to use HTTP APIs instead of REST APIs. 56 | This will allow you to use HTTP APIs instead of REST APIs. 57 | 58 | ```yaml 59 | functions: 60 | mcpServer: 61 | handler: src/index.handler 62 | events: 63 | - httpApi: 64 | path: mcp 65 | method: post 66 | ``` 67 | 68 | Local endpoint will be available at: 69 | POST `http://localhost:3000/mcp` 70 | 71 | Note that the `/dev/` stage is not needed when using API Gateway V2. 72 | Note you should change test curl and postman requests accordingly. 73 | 74 | ## 🧪 Test with curl requests 75 | 76 | ### List tools 77 | ```bash 78 | curl --location 'http://localhost:3000/dev/mcp' \ 79 | --header 'content-type: application/json' \ 80 | --header 'accept: application/json' \ 81 | --header 'jsonrpc: 2.0' \ 82 | --data '{ 83 | "jsonrpc": "2.0", 84 | "method": "tools/list", 85 | "id": 1 86 | }' 87 | ``` 88 | 89 | ### ➕ Use the add Tool 90 | ```bash 91 | curl --location 'http://localhost:3000/dev/mcp' \ 92 | --header 'content-type: application/json' \ 93 | --header 'accept: application/json' \ 94 | --header 'jsonrpc: 2.0' \ 95 | --data '{ 96 | "jsonrpc": "2.0", 97 | "id": 2, 98 | "method": "tools/call", 99 | "params": { 100 | "name": "add", 101 | "arguments": { 102 | "a": 5, 103 | "b": 3 104 | } 105 | } 106 | }' 107 | ``` 108 | 109 | ## 🧪 Test with jest 110 | 111 | There are some basic tests included in the `__tests__` folder. You can run them with: 112 | 113 | ```bash 114 | npm run test 115 | ``` 116 | 117 | ## 🧬 Code Breakdown 118 | This code is based on the awesome work of [Frédéric Barthelet](https://github.com/fredericbarthelet): which has developed a middy middleware for Model Context Protocol (MCP) server integration with AWS Lambda functions in [this repo](https://github.com/fredericbarthelet/middy-mcp) 119 | 120 | ### src/index.js 121 | ```javascript 122 | import middy from "@middy/core"; 123 | import httpErrorHandler from "@middy/http-error-handler"; 124 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 125 | import { z } from "zod"; 126 | import mcpMiddleware from "middy-mcp"; 127 | 128 | const server = new McpServer({ 129 | name: "Lambda hosted MCP Server", 130 | version: "1.0.0", 131 | }); 132 | 133 | server.tool("add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ 134 | content: [{ type: "text", text: String(a + b) }], 135 | })); 136 | 137 | export const handler = middy() 138 | .use(mcpMiddleware({ server })) 139 | .use(httpErrorHandler()); 140 | ``` 141 | 142 | ## 📡 Deploy to AWS 143 | 144 | Just run: 145 | 146 | ```bash 147 | sls deploy 148 | ``` 149 | After deployment, the MCP server will be live at the URL output by the command. 150 | 151 | ## 🔖 Quotes 152 | This repository has been quoted in the following newsletters: 153 | - [Serverless Advocate Newsletter #33](https://serverlessadvocate.substack.com/p/33-resilient-solutions?r=rad0z&utm_campaign=post&utm_medium=web&triedRedirect=true) by [Lee Gilmore](https://www.linkedin.com/in/lee-james-gilmore/) (AWS Hero) 154 | 155 | ## 📘 License 156 | MIT — feel free to fork, tweak, and deploy your own version! 157 | 158 | -------------------------------------------------------------------------------- /__tests__/add-tool/add-tool-edge-cases.test.mjs: -------------------------------------------------------------------------------- 1 | // __tests__/mcpAddToolEdgeCases.test.mjs 2 | import { handler } from '../../src/index.mjs'; 3 | 4 | describe('MCP Server - tools/call "add" method (edge cases)', () => { 5 | const baseEvent = { 6 | httpMethod: 'POST', 7 | headers: { 8 | 'content-type': 'application/json', 9 | accept: 'application/json', 10 | jsonrpc: '2.0', 11 | }, 12 | }; 13 | 14 | const callAdd = (params, id = 999) => ({ 15 | ...baseEvent, 16 | body: JSON.stringify({ 17 | jsonrpc: '2.0', 18 | id, 19 | method: 'tools/call', 20 | params: { 21 | name: 'add', 22 | arguments: params, 23 | }, 24 | }), 25 | }); 26 | 27 | it('should return a validation error if "a" is missing', async () => { 28 | const response = await handler(callAdd({ b: 2 }, 101)); 29 | const body = JSON.parse(response.body); 30 | expect(body).toHaveProperty('error'); 31 | expect(body.error.message).toMatch(/a/i); 32 | }); 33 | 34 | it('should return a validation error if "b" is missing', async () => { 35 | const response = await handler(callAdd({ a: 2 }, 102)); 36 | const body = JSON.parse(response.body); 37 | expect(body.error.message).toMatch(/b/i); 38 | }); 39 | 40 | it('should return a validation error if "a" is a string', async () => { 41 | const response = await handler(callAdd({ a: '5', b: 2 }, 103)); 42 | const body = JSON.parse(response.body); 43 | expect(body.error.message).toMatch(/a/i); 44 | }); 45 | 46 | it('should return a validation error if both are strings', async () => { 47 | const response = await handler(callAdd({ a: 'foo', b: 'bar' }, 104)); 48 | const body = JSON.parse(response.body); 49 | expect(body.error.message).toMatch(/number/i); 50 | }); 51 | 52 | it('should return 0 when both a and b are 0', async () => { 53 | const response = await handler(callAdd({ a: 0, b: 0 }, 105)); 54 | const body = JSON.parse(response.body); 55 | expect(body.result.content[0].text).toBe('0'); 56 | }); 57 | 58 | it('should handle negative numbers correctly', async () => { 59 | const response = await handler(callAdd({ a: -3, b: -7 }, 106)); 60 | const body = JSON.parse(response.body); 61 | expect(body.result.content[0].text).toBe('-10'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/add-tool/add-tool.test.mjs: -------------------------------------------------------------------------------- 1 | // __tests__/mcpAddTool.test.mjs 2 | import { handler } from '../../src/index.mjs'; 3 | 4 | describe('MCP Server - tools/call "add" method', () => { 5 | it('Should return the sum of a and b', async () => { 6 | const event = { 7 | httpMethod: 'POST', 8 | headers: { 9 | 'content-type': 'application/json', 10 | accept: 'application/json', 11 | jsonrpc: '2.0', 12 | }, 13 | body: JSON.stringify({ 14 | jsonrpc: '2.0', 15 | id: 2, 16 | method: 'tools/call', 17 | params: { 18 | name: 'add', 19 | arguments: { 20 | a: 5, 21 | b: 3, 22 | }, 23 | }, 24 | }), 25 | }; 26 | 27 | const context = {}; 28 | 29 | const response = await handler(event, context); 30 | 31 | expect(response.statusCode).toBe(200); 32 | 33 | const body = JSON.parse(response.body); 34 | expect(body).toHaveProperty('jsonrpc', '2.0'); 35 | expect(body).toHaveProperty('id', 2); 36 | expect(body).toHaveProperty('result'); 37 | expect(body.result).toHaveProperty('content'); 38 | 39 | const content = body.result.content; 40 | expect(Array.isArray(content)).toBe(true); 41 | expect(content[0]).toEqual({ 42 | type: 'text', 43 | text: '8', 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/tool-list/tool-list.test.mjs: -------------------------------------------------------------------------------- 1 | import { handler } from '../../src/index.mjs'; 2 | 3 | describe('MCP Server - tools/list method', () => { 4 | it('Should return a list of tools', async () => { 5 | const event = { 6 | httpMethod: 'POST', 7 | headers: { 8 | 'content-type': 'application/json', 9 | accept: 'application/json', 10 | jsonrpc: '2.0', 11 | }, 12 | body: JSON.stringify({ 13 | jsonrpc: '2.0', 14 | method: 'tools/list', 15 | id: 1, 16 | }), 17 | }; 18 | 19 | const context = {}; // Lambda context (empty for unit tests) 20 | 21 | const response = await handler(event, context); 22 | 23 | expect(response.statusCode).toBe(200); 24 | 25 | const body = JSON.parse(response.body); 26 | expect(body).toHaveProperty('jsonrpc', '2.0'); 27 | expect(body).toHaveProperty('id', 1); 28 | expect(body).toHaveProperty('result'); 29 | expect(body.result).toHaveProperty('tools'); 30 | expect(Array.isArray(body.result.tools)).toBe(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | moduleFileExtensions: [ 3 | "mjs", 4 | // must include "js" to pass validation https://github.com/facebook/jest/issues/12116 5 | "js", 6 | ], 7 | testRegex: `test\.mjs$`, 8 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-mcp-server", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "node --experimental-vm-modules ./node_modules/.bin/jest" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "description": "", 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.148", 13 | "@types/http-errors": "^2.0.4", 14 | "@types/node": "^22.14.0", 15 | "jest": "^29.7.0", 16 | "serverless-offline": "^14.4.0" 17 | }, 18 | "dependencies": { 19 | "@middy/core": "^6.1.6", 20 | "@middy/http-error-handler": "^6.1.6", 21 | "@modelcontextprotocol/sdk": "^1.9.0", 22 | "http-errors": "^2.0.0", 23 | "middy-mcp": "^0.0.1", 24 | "zod": "^3.24.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-mcp-server 2 | frameworkVersion: "3" 3 | 4 | provider: 5 | name: aws 6 | runtime: nodejs22.x 7 | timeout: 30 8 | 9 | plugins: 10 | - serverless-offline 11 | 12 | functions: 13 | mcpServer: 14 | handler: src/index.handler 15 | events: 16 | - http: 17 | path: mcp 18 | method: post -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | import middy from "@middy/core"; 2 | import httpErrorHandler from "@middy/http-error-handler"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { z } from "zod"; 5 | 6 | import mcpMiddleware from "middy-mcp"; 7 | 8 | // Create an MCP server 9 | const server = new McpServer({ 10 | name: "Lambda hosted MCP Server", 11 | version: "1.0.0", 12 | }); 13 | 14 | // Add an addition tool 15 | server.tool("add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ 16 | content: [{ type: "text", text: String(a + b) }], 17 | })); 18 | export const handler = middy() 19 | .use(mcpMiddleware({ server })) 20 | .use(httpErrorHandler()); --------------------------------------------------------------------------------