MIKAEELS

Explore the blog

Deploy a Python MCP Server on EC2 with AWS CDK and SSM

Run a Python FastMCP HTTP server in a Docker container on EC2, then use AWS Systems Manager to forward its port to your laptop. The server uses Streamable HTTP at /mcp, while the EC2 security group accepts no inbound application or SSH traffic.

This is a single-instance development deployment. It demonstrates remote execution and local verification without publishing an unauthenticated tool endpoint. It does not provide a production identity system, HTTPS gateway, or high availability.

Prerequisites and layout

Use Node.js, AWS CDK v2, an AWS sandbox identity, the AWS CLI, and the Session Manager plugin on your laptop. Your identity needs permission to deploy the stack and start a port-forwarding session; those are separate permissions.

Create the CDK app first:

mkdir mcp-deploy cd mcp-deploy cdk init app --language typescript npm install --save-exact aws-cdk-lib@2.272.0 constructs@10.5.0 mkdir server

All following CDK commands run from mcp-deploy. The local verification client uses a Python virtual environment. No cloud deployment or Docker build was performed for this revision; local checks cannot establish AWS account permissions or startup success.

Step 1: Build the FastMCP server

Save server/server.py:

from fastmcp import FastMCP mcp = FastMCP("MathService") @mcp.tool() def add(a: int, b: int) -> int: """Add two integers.""" return a + b @mcp.tool() def multiply(a: int, b: int) -> int: """Multiply two integers.""" return a * b if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8000)

Save server/Dockerfile:

FROM python:3.12-slim WORKDIR /app RUN pip install --no-cache-dir fastmcp==2.13.0 COPY server.py . EXPOSE 8000 CMD ["python", "server.py"]

FastMCP 2.13.0 is the example API baseline, not a claim about the newest version. For a production image, lock transitive dependencies, select an approved base-image digest, and scan the result. FastMCP documents HTTP transport and server startup.

The server source, Dockerfile, and verification client are also available as text downloads; save them with the filenames used in this guide.

Step 2: Define the CDK stack

Replace lib/mcp-deploy-stack.ts:

import { CfnOutput, Stack, StackProps } from "aws-cdk-lib"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as iam from "aws-cdk-lib/aws-iam"; import { Construct } from "constructs"; import { readFileSync } from "node:fs"; export class McpDeployStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, "Network", { maxAzs: 1, natGateways: 0, subnetConfiguration: [{ name: "public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }], }); const securityGroup = new ec2.SecurityGroup(this, "ServerSecurityGroup", { vpc }); const role = new iam.Role(this, "InstanceRole", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")], }); const server = readFileSync("server/server.py").toString("base64"); const dockerfile = readFileSync("server/Dockerfile").toString("base64"); const userData = ec2.UserData.forLinux(); userData.addCommands( "set -euo pipefail", "dnf install -y docker", "systemctl enable --now docker", "mkdir -p /app", `echo '${server}' | base64 -d > /app/server.py`, `echo '${dockerfile}' | base64 -d > /app/Dockerfile`, "docker build -t mcp-server:demo /app", "docker run -d -p 127.0.0.1:8000:8000 --restart unless-stopped --name mcp-running mcp-server:demo", ); const instance = new ec2.Instance(this, "Server", { vpc, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL), machineImage: ec2.MachineImage.latestAmazonLinux2023(), associatePublicIpAddress: true, requireImdsv2: true, securityGroup, role, userData, }); new CfnOutput(this, "InstanceId", { value: instance.instanceId }); } }

Base64 transfers the source files into user data without indentation-sensitive heredoc delimiters. Do not place credentials in these files: user data is not secret storage.

The instance uses a public subnet and address for outbound package downloads and Systems Manager communication. No inbound rules are added. Public IPv4, EC2, disk, and data-transfer costs remain even without a NAT gateway.

Step 3: Deploy and inspect startup

npx tsc --noEmit npx aws-cdk synth npx aws-cdk diff npx aws-cdk deploy --outputs-file stack-outputs.json

Bootstrap the environment if necessary. Use the InstanceId output to open a Session Manager shell once the instance registers, then inspect:

sudo tail -n 100 /var/log/cloud-init-output.log sudo docker ps sudo docker logs mcp-running --tail 100

A successful CloudFormation deployment does not wait for a successful docker build. If the instance does not register with Systems Manager, check the agent, instance role, region, and outbound connectivity before debugging MCP itself.

Step 4: Forward the port and verify MCP

From your laptop, keep this session open:

aws ssm start-session --target YOUR_INSTANCE_ID \ --document-name AWS-StartPortForwardingSession \ --parameters '{"portNumber":["8000"],"localPortNumber":["8000"]}'

Use the same AWS region as the instance. If local port 8000 is occupied, change localPortNumber and the client URL together. AWS documents the Session Manager port-forwarding prerequisites.

In a second local terminal:

python3 -m venv .venv .venv/bin/python -m pip install fastmcp==2.13.0

Save test_client.py:

import asyncio from fastmcp import Client async def main(): async with Client("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() assert {"add", "multiply"}.issubset({tool.name for tool in tools}) result = await client.call_tool("add", {"a": 2, "b": 3}) assert not result.is_error print(result.data) if __name__ == "__main__": asyncio.run(main())

Run .venv/bin/python test_client.py; expect 5. Listing tools and making a successful call checks MCP behavior more meaningfully than a browser GET to /mcp, which may reject an unsupported request method or missing session headers.

Local clients versus cloud connectors

The tunnel is available to clients on your laptop. Claude supports remote custom connectors, including in Claude Desktop, but those connections originate from Anthropic's cloud infrastructure; they cannot use your laptop's loopback tunnel. See the official connector network requirements.

To publish a connector, design a separate authenticated HTTPS deployment and verify the client's transport and authorization requirements. Do not open this demo's raw HTTP port to the internet. For endpoint discovery, use finding a remote MCP server URL.

Cleanup and next steps

Close the local tunnel, review npx aws-cdk destroy for this disposable stack, and verify deletion in the deployment region. If deletion fails, inspect CloudFormation events and resource ownership before taking action; blindly deleting a blocking shared resource can damage another workload.

Continue with building a local MCP server, MCP clients, and MCP versus retrieval and function calling to separate server execution from model orchestration.

Quick Recap

Is the MCP endpoint public in this example?

No. The container port is published on the EC2 host's loopback address and reached through an authenticated Systems Manager port-forwarding session. There is no inbound security-group rule for port 8000.

Can Claude connect to remote MCP servers directly?

Claude supports remote custom connectors, but those connections originate from Anthropic's infrastructure. This tunnel-only demo is reachable by a local client, not a cloud connector.

Does CDK deployment success mean the container is healthy?

No. CloudFormation can finish creating the instance before its user data finishes. Inspect cloud-init and Docker logs, then perform an MCP handshake and tool call.

Add Mikaeels Blog as a preferred source on Google

Comments

Share your thoughts and questions below