DEV Community

5 2

πŸš€ From Serverless Framework to AWS CDK: rebuilding our minimal serverless MCP server

Hey devs πŸ‘‹, if you saw my last post about building a minimal Model Context Protocol server with AWS Lambda using the Serverless Framework, this is the natural follow-up for those who prefer using AWS CDK.

The post on how to deploy an MCP server in a serverless environment was particularly well received and even got featured in two outstanding newsletters: Serverless Developer Advocate #33 by Lee Gilmore and Ready, Set, Cloud #160 by Allen Helton. I highly recommend subscribing to both as they’re packed with insights and inspiration for serverless enthusiasts!

πŸ€” Why Use CDK

AWS CDK gives us fine-grained control over infrastructure, which makes it a great option if you’re scaling things up later.

Here’s the repo if you want to jump in: πŸ‘‰ cdk-serverless-mcp-server

The best thing about AWS CDK is that lets you define infrastructure in actual code (TypeScript, Python, etc.), which can feel more natural as dev than writing YAML or JSON.

If you're:

  • Planning to integrate with other AWS services
  • Wanting more programmatic control
  • Or just curious about how to do Infrastructure as Code with CDK without YAML/JSON files

…you should give it a try!

Here is a comparison table between Serverless Framework, AWS SAM and AWS CKD. Read it carefully before choosing your preferred way to do IaC in your project.

Feature / Tool Serverless Framework AWS SAM (Serverless Application Model) AWS CDK (Cloud Development Kit)
Ownership V3 independent (deprecated), V4 enterprise, OSS alternative to V4 AWS AWS
Abstraction Level High-level Medium-level Low to medium-level
Language YAML + plugins (JavaScript/TS) YAML + some scripting TypeScript, Python, Java, C#, Go
Cloud Provider Support Multi-cloud (AWS, Azure, GCP, etc) AWS only AWS only
Template Syntax Custom syntax (serverless.yml) CloudFormation-compatible YAML Imperative (code-based)
Local Development Good support via plugins Good (via sam local) Limited, depends on constructs
Deployment CLI-driven CLI-driven (sam deploy) CLI-driven (cdk deploy)
State Management Built-in via .serverless folder CloudFormation CloudFormation
Extensibility High (plugins, hooks) Moderate (some hooks/plugins) High (custom constructs, reusable code)
Maturity Very mature Mature Rapidly growing
Best For Multi-cloud serverless apps Simple AWS Lambda apps Complex infrastructure-as-code on AWS
Learning Curve Low to moderate Low Moderate to high
Testing/Debugging Plugin-based sam local invoke/start-api Manual / unit tests on code
CI/CD Integration Easy (via plugins or custom) Easy (via CodePipeline or custom) Easy (via CodePipeline or custom)
Cost V3 Free, V4 pricing Free Free

πŸ“¦ What’s Inside the Repo

This project spins up as the previous one:

  • An AWS Lambda function hosting a serverless MCP server
  • An Amazon API Gateway with a POST /mcp route

Our goal is always to have a skeleton to deploy our MCP server in a serverless environment, but using AWS CDK.

Project Structure

We add a bin and lib folder for our CDK app and stack.
Also note the cdk.json to define our project.

cdk-serverless-mcp-server/
β”œβ”€β”€ __tests__/                              # Jest tests
β”œβ”€β”€ bin/                                    # CDK entry point
β”œβ”€β”€ cdk-serverless-mcp-server.ts                # CDK app
β”œβ”€β”€ lib/                                    # CDK stack
β”‚   └── cdk-serverless-mcp-server-stack.ts      # CDK stack
β”œβ”€β”€ src/                                    # Source code
β”‚   └── index.mjs                               # MCP server handler
β”œβ”€β”€ .gitignore                              # Git ignore file
β”œβ”€β”€ cdk.json                                # CDK project config
β”œβ”€β”€ package.json                            # Project dependencies
β”œβ”€β”€ package-lock.json                       # Project lock file
β”œβ”€β”€ README.md                               # This documentation file
Enter fullscreen mode Exit fullscreen mode

πŸš€ Getting Started

To get it up and running follow those steps.

Install dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Install AWS CDK globally (if not already installed):

npm install -g aws-cdk
Enter fullscreen mode Exit fullscreen mode

Test locally with jest

npm run test
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ CDK code

You can easily read the code following comments in the stack file.

import { Stack, StackProps, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import {LayerVersion} from "aws-cdk-lib/aws-lambda";

export class CdkServerlessMcpServerStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props); // Initialize the stack

        // Define runtime
        const runtime = lambda.Runtime.NODEJS_22_X

        // Create dependencies lambda layer
        const dependenciesLayerName = 'dependencies-layer'; // Name of the layer
        const dependenciesLayerFolder = 'layer/dependencies'; // Folder containing the layer code
        const dependenciesLayerDesc = 'Layer containing project dependencies'; // Description of the layer
        const dependenciesLayerProps = {
            code: lambda.Code.fromAsset(dependenciesLayerFolder), // Path to the layer code
            compatibleRuntimes: [ runtime], // Specify the compatible runtimes
            description: dependenciesLayerDesc, // Description of the layer
        };
        const dependenciesLayer = new LayerVersion(this, dependenciesLayerName, dependenciesLayerProps);

        // Create lambda function
        const mcpLambda = new lambda.Function(this, 'McpHandler', {
            runtime: runtime, // The runtime environment for the Lambda function
            handler: 'index.handler', // The name of the exported function in our code
            code: lambda.Code.fromAsset('src'), // Path to our lambda function code
            timeout: Duration.seconds(29), // Set timeout to 29 seconds
            layers: [dependenciesLayer] // Add the layer to the lambda function
        });

        // Create API Gateway
        const api = new apigateway.RestApi(this, 'McpApi', {
            restApiName: 'MCP Service', // The name of the API
        });

        // Add a resource and method to the API Gateway
        const mcpResource = api.root.addResource('mcp'); // Create a resource named 'mcp'
        mcpResource.addMethod('POST', new apigateway.LambdaIntegration(mcpLambda)); // Add a POST method to the resource
    }
}
Enter fullscreen mode Exit fullscreen mode

What’s happening here?

  • Node.js 22 support: just set the runtime to NODEJS_22_X and you're good to go.
  • Custom Lambda Layer: to avoid bloating your function zip and to improve reusability, we package our dependencies into a Lambda Layer (layer/dependencies). This also helps with cold starts!
  • A Lambda function with MCP server code
  • API Gateway integration: we expose the Lambda via API Gateway, setting up a POST method on the /mcp endpoint in just a few lines.

πŸ“‘ Deploy to AWS

Follow those steps.

Before all, install dependencies for the layer

npm run layer-dependencies-install
Enter fullscreen mode Exit fullscreen mode

You should bootstrap the CDK (just one time on the account):

cdk bootstrap
Enter fullscreen mode Exit fullscreen mode

Then, deploy the stack:

cdk deploy
Enter fullscreen mode Exit fullscreen mode

After deployment, the MCP server will be live at the URL output by the command.

Here is how it looks like when deploying:

Image description

Take a moment to copy the endpoint shown after running the command.

πŸ§ͺ Once deployed, test with curl requests

List tools

Change your-endpoint with the one noted.

curl --location 'https://your-endpoint/dev/mcp' \
--header 'content-type: application/json' \
--header 'accept: application/json' \
--header 'jsonrpc: 2.0' \
--data '{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "id": 1
}'
Enter fullscreen mode Exit fullscreen mode

Here is an example (response linted with jq):

Image description

βž• Use the add Tool

Change your-endpoint with the one noted.

curl --location 'https://your-endpoint/dev/mcp' \
--header 'content-type: application/json' \
--header 'accept: application/json' \
--header 'jsonrpc: 2.0' \
--data '{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "add",
    "arguments": {
      "a": 5,
      "b": 3
    }
  }
}'
Enter fullscreen mode Exit fullscreen mode

Here is an example (response linted with jq):

Image description

⏭️ Next Step

I'm planning to continue this series giving an example with SAM! Let me know if this is needed by you in the comments!

πŸ™‹ Who am I

I'm D. De Sio and I work as a Head of Software Engineering in Eleva.
I'm currently (Apr 2025) an AWS Certified Solution Architect Professional and AWS Certified DevOps Engineer Professional, but also a User Group Leader (in Pavia), an AWS Community Builder and, last but not least, a #serverless enthusiast.

My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly β€” using the tools and languages you already love!

Learn More

Top comments (0)

Best Practices for Running  Container WordPress on AWS (ECS, EFS, RDS, ELB) using CDK cover image

Best Practices for Running Container WordPress on AWS (ECS, EFS, RDS, ELB) using CDK

This post discusses the process of migrating a growing WordPress eShop business to AWS using AWS CDK for an easily scalable, high availability architecture. The detailed structure encompasses several pillars: Compute, Storage, Database, Cache, CDN, DNS, Security, and Backup.

Read full post