Deploy a Python MCP Server on AWS EC2 Using AWS CDK
The Model Context Protocol, or MCP, is a standard for connecting AI clients to external tools. FastMCP provides a straightforward way to implement MCP servers in Python and run them locally over STDIO or remotely over HTTP. This guide deploys a minimal FastMCP server to AWS EC2 using AWS CDK, then connects Claude Desktop using a local proxy process, because Claude Desktop expects to launch a local command and communicate over STDIO.
What We Will Build
- A FastMCP tool server with two example tools:
addandmultiply - An EC2 instance running the server in a Docker container
- A low cost VPC (public subnets only, no NAT Gateway)
- A security group that exposes the MCP port
- A local proxy script that Claude Desktop runs to bridge STDIO to your remote HTTP MCP endpoint
Prerequisites
Local machine:
- AWS CLI configured with credentials for your target AWS account
- Node.js and npm
- AWS CDK v2
- Python 3.10 or newer
AWS account:
- Permissions to create VPC, EC2, IAM roles, and related resources
If this is your first time using CDK in the account and region, bootstrap CDK once:
cdk bootstrap
Step 1: Build the FastMCP Server
FastMCP supports multiple transports. For a remote deployment on EC2, use HTTP transport and bind to 0.0.0.0 so the service accepts external traffic.
1. Create server.py
Create a folder server/ and add server/server.py:
from fastmcp import FastMCP mcp = FastMCP("MathService") @mcp.tool() def add(a: int, b: int) -> int: """Adds two numbers.""" return a + b @mcp.tool() def multiply(a: int, b: int) -> int: """Multiplies two numbers.""" return a * b if __name__ == "__main__": # HTTP transport is recommended for network deployments. # The default MCP endpoint path is /mcp. mcp.run(transport="http", host="0.0.0.0", port=8000)
2. Add a Dockerfile
Create server/Dockerfile:
FROM python:3.11-slim WORKDIR /app # FastMCP uses Uvicorn for HTTP transports. RUN pip install "fastmcp[server]" uvicorn COPY server.py . EXPOSE 8000 CMD ["python", "server.py"]
Step 2: Deploy Infrastructure on AWS with CDK
This stack creates a new VPC to avoid Vpc.fromLookup context issues, uses public subnets only to avoid NAT Gateway costs, and configures AWS Systems Manager Session Manager so you do not need to open SSH.
1. Initialize the CDK project
mkdir mcp-deploy cd mcp-deploy cdk init app --language typescript npm install aws-cdk-lib constructs
2. Replace the stack
Replace lib/mcp-deploy-stack.ts with the following:
import * as cdk 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"; export class McpDeployStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // 1. Low cost VPC: public subnets only, no NAT Gateway const vpc = new ec2.Vpc(this, "McpVpc", { maxAzs: 1, natGateways: 0, subnetConfiguration: [ { cidrMask: 24, name: "Public", subnetType: ec2.SubnetType.PUBLIC, }, ], }); // 2. Security group: allow inbound TCP 8000 // For production, restrict to your IP address or a private access path. const mcpSg = new ec2.SecurityGroup(this, "McpSg", { vpc, description: "MCP server security group", allowAllOutbound: true, }); mcpSg.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(8000), "Allow MCP over HTTP", ); // 3. IAM role so you can connect using Systems Manager Session Manager (no SSH required) const instanceRole = new iam.Role(this, "McpInstanceRole", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), }); instanceRole.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName( "AmazonSSMManagedInstanceCore", ), ); // 4. User data for Amazon Linux 2023 const userData = ec2.UserData.forLinux(); userData.addCommands( "set -euxo pipefail", // Install Docker "dnf update -y", "dnf install -y docker", "systemctl enable --now docker", // Prepare app directory "mkdir -p /app", "cd /app", // Write server.py `cat > server.py << 'EOF' from fastmcp import FastMCP mcp = FastMCP("MathService") @mcp.tool() def add(a: int, b: int) -> int: return a + b @mcp.tool() def multiply(a: int, b: int) -> int: return a * b if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8000) EOF`, // Write Dockerfile `cat > Dockerfile << 'EOF' FROM python:3.11-slim WORKDIR /app RUN pip install "fastmcp[server]" uvicorn COPY server.py . EXPOSE 8000 CMD ["python", "server.py"] EOF`, // Build and run container "docker build -t mcp-server:latest .", "docker rm -f mcp-running || true", "docker run -d -p 8000:8000 --restart always --name mcp-running mcp-server:latest", ); // 5. EC2 instance const instance = new ec2.Instance(this, "McpInstance", { vpc, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, instanceType: ec2.InstanceType.of( ec2.InstanceClass.T3, ec2.InstanceSize.MICRO, ), machineImage: ec2.MachineImage.latestAmazonLinux2023(), securityGroup: mcpSg, role: instanceRole, userData, associatePublicIpAddress: true, }); // Outputs new cdk.CfnOutput(this, "McpHttpEndpoint", { value: `http://${instance.instancePublicIp}:8000/mcp`, description: "Remote MCP HTTP endpoint", }); new cdk.CfnOutput(this, "InstanceId", { value: instance.instanceId, description: "EC2 Instance ID (useful for Session Manager)", }); } }
3. Deploy
cdk deploy
After deployment, CDK will print an output similar to:
http://54.x.x.x:8000/mcp
That is your remote MCP endpoint.
Step 3: Verify the Remote MCP Server
Option A: Verify using a FastMCP client
Install FastMCP locally:
pip install fastmcp
Create test_client.py:
import asyncio from fastmcp import Client REMOTE_MCP_URL = "http://54.x.x.x:8000/mcp" async def main(): client = Client(REMOTE_MCP_URL) async with client: tools = await client.list_tools() print("Tools:", [t.name for t in tools]) result = await client.call_tool("add", {"a": 2, "b": 3}) print("add(2,3) =", result) asyncio.run(main())
Run:
python test_client.py
If you see the tools and a valid result, your EC2 deployment is working.
Option B: Inspect logs using Session Manager
Because the instance role includes Session Manager permissions, you can connect without SSH keys:
- AWS Console
- Systems Manager
- Session Manager
- Start session
- Select your instance
Then run:
sudo docker logs mcp-running --tail 200 sudo tail -n 200 /var/log/cloud-init-output.log
Step 4: Connect Claude Desktop (Local Proxy Bridge)
Claude Desktop expects to launch a local command and communicate over STDIO. It cannot directly use a remote URL in its standard MCP server configuration. The solution is to run a local proxy that Claude launches, which forwards STDIO MCP requests to your remote HTTP MCP endpoint.
1. Create bridge.py locally
Save this on your laptop somewhere stable, for example Documents/mcp-bridge/bridge.py:
from fastmcp import FastMCP, Client REMOTE_MCP_URL = "http://54.x.x.x:8000/mcp" backend_client = Client(REMOTE_MCP_URL) proxy_server = FastMCP.from_client( backend_client, name="AWS-Math-Proxy", ) if __name__ == "__main__": # Default transport is STDIO, which is what Claude Desktop expects. proxy_server.run()
Install the dependency locally:
pip install fastmcp
2. Configure Claude Desktop
Edit your claude_desktop_config.json and add:
{ "mcpServers": { "aws-math": { "command": "python", "args": ["<ABSOLUTE_PATH_TO>/bridge.py"] } } }
Restart Claude Desktop after updating the file.
Security Notes
- Opening port 8000 to the entire internet is suitable only for testing.
- For a safer setup, restrict inbound access to your IP address, put the service behind a load balancer with TLS, or host the server privately and connect via VPN.
Cleanup: Delete Everything and Stop Costs
When you are done testing, destroy the CDK stack:
cdk destroy
Confirm resources are deleted
In the AWS console, confirm these are gone in the region you used:
- CloudFormation stack deleted
- EC2 instance terminated
- Security group deleted
- VPC deleted
If cdk destroy fails
If deletion fails, the CloudFormation events will show which resource is blocking teardown. Typical causes include manual changes to resources created by the stack. Delete the blocking resource manually, then run cdk destroy again.
Summary
You deployed a FastMCP server to AWS EC2 using CDK, verified it using a FastMCP client, and connected Claude Desktop through a local proxy bridge so Claude can speak STDIO locally while your tools live remotely over HTTP. You also have a clean teardown path via cdk destroy to avoid ongoing AWS charges.
More Reading on MCP
If you are new to MCP or want a deeper understanding of the concepts used in this guide, check out these articles:
- Introduction to MCP – Why Context Is the New Infrastructure: Understand why MCP exists and how it structures context for LLMs.
- MCP Core Concepts Explained – Resources, Tools, and Prompts: Learn about the three building blocks of MCP servers.
- Building Your First MCP Server – A Practical Python Walkthrough: A step by step guide to building MCP servers in Python.
- MCP Clients Explained: Understand how clients discover and interact with MCP servers.
Comments
Share your thoughts and questions below