CloudWatch MCP Server
README.md
# CloudWatch MCP Server
A custom [MCP](https://modelcontextprotocol.io) server, built with
[FastMCP](https://gofastmcp.com), that exposes Amazon CloudWatch
Metrics, Logs Insights, and Alarms as tools an AI assistant can call.
Designed to run locally for development and to deploy onto AWS Lambda
as a container image, fronted by a Lambda Function URL.
Tested against `fastmcp==3.4.7` and `boto3` with `moto` mocks (all
tools listed below were exercised against mocked CloudWatch/Logs
calls). Pin your own `fastmcp` version in `requirements.txt` before
deploying, and re-check this README against
[gofastmcp.com](https://gofastmcp.com) if it's been a while — FastMCP's
API has changed shape more than once (see the "A note on FastMCP's
moving API" section below).
## Tools exposed
| Tool | What it does |
|---|---|
| `list_metrics` | Discover available metrics by namespace/name/dimension |
| `get_metric_data` | Fetch time-series data points for one metric |
| `list_log_groups` | List CloudWatch Log Groups, optionally by prefix |
| `query_logs` | Run a Logs Insights query and wait for results |
| `list_alarms` | List alarms, optionally filtered by state |
| `get_alarm_history` | Get state-change history for one alarm |
All tools are **read-only** — none of them can modify, delete, or
create anything in CloudWatch. Keep it that way unless there's a
specific reason to add write access; least privilege matters a lot
more once an AI model is the one deciding when to call these.
## 1. Run it locally first
This is the fastest way to prove the tools work before touching AWS
deployment at all.
```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Uses your normal AWS credentials (aws configure / AWS_PROFILE / SSO)
export AWS_PROFILE=your-profile
export AWS_REGION=us-east-1
python3 server.py # defaults to stdio transport
```
Point an MCP client (Claude Desktop, Claude Code, etc.) at this
command directly — for Claude Desktop, add to its MCP config:
```json
{
"mcpServers": {
"cloudwatch": {
"command": "/full/path/to/.venv/bin/python3",
"args": ["/full/path/to/server.py"],
"env": { "AWS_PROFILE": "your-profile", "AWS_REGION": "us-east-1" }
}
}
}
```
To test the HTTP transport locally (the mode used on Lambda):
```bash
MCP_TRANSPORT=http PORT=8080 python3 server.py
# then, from another terminal:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
```
## 2. IAM: what the server is allowed to touch
`iam-policy.json` in this folder is the minimum permission set the
server needs — `GetMetricData`/`ListMetrics`/`DescribeAlarms*` for
CloudWatch, `DescribeLogGroups`/`StartQuery`/`GetQueryResults`/`StopQuery`
for Logs Insights. Nothing else. Attach it to whatever identity runs
the server:
- **Locally:** attach it to an IAM user/role and use that via
`AWS_PROFILE`, or grant it to your SSO role.
- **On Lambda:** attach it to the Lambda function's **execution
role** (plus the standard `AWSLambdaBasicExecutionRole` for the
function's own logging) — never bake access keys into the image.
## 3. Deploying to AWS Lambda
CloudWatch's own recommended path for "expose an existing function as
an MCP tool with no protocol code" is **Amazon Bedrock AgentCore
Gateway** — worth a look if a fully managed option becomes acceptable
later. What's below is the literal "we own the MCP server" path, using
the [AWS Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter)
to let this exact FastMCP app run inside Lambda unmodified.
### Build and push the container image
```bash
aws ecr create-repository --repository-name cloudwatch-mcp-server
aws ecr get-login-password --region <region> | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
docker build -t cloudwatch-mcp-server .
docker tag cloudwatch-mcp-server:latest \
<account-id>.dkr.ecr.<region>.amazonaws.com/cloudwatch-mcp-server:latest
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/cloudwatch-mcp-server:latest
```
### Create the Lambda function
1. Create the function from the container image you just pushed.
2. Memory: start at 512 MB; timeout: 30s is enough for most metric/alarm
calls, bump to 60–120s if your Logs Insights queries are large
(`query_logs`'s `max_wait_seconds` should stay comfortably under the
function timeout).
3. Attach the execution role with the IAM policy from step 2.
4. Set **Invoke mode** to `RESPONSE_STREAM` if you enable a Function
URL with streaming (needed for the adapter to proxy long responses).
5. Create a **Function URL**:
- Auth type: **AWS_IAM** (do *not* use `NONE` outside of a quick
personal test — that leaves your CloudWatch data reachable by
anyone with the URL).
- Your MCP client will need to sign requests with SigV4 to call it;
most MCP clients don't do this natively yet, so in practice this
usually sits behind something that *can* sign requests — e.g. an
internal gateway/proxy your team controls, or API Gateway with
IAM auth in front of the Function URL instead of using the
Function URL's own IAM auth directly.
6. The MCP endpoint will be `https://<function-url>/mcp`.
### Sanity-check the deployed function
```bash
aws lambda invoke --function-name cloudwatch-mcp-server \
--payload '{}' /tmp/out.json && cat /tmp/out.json
```
and then a real MCP `initialize` call against the Function URL (with
SigV4 signing, e.g. via `awscurl` or a small signed-request script) —
the same JSON body as the local `curl` test above.
## 4. Known rough edges (be honest with your team about these)
- **Cold starts**: a full HTTP server (uvicorn + FastMCP) booting
inside a Lambda cold start is slower than a typical lightweight
Lambda handler — expect multi-second latency on the first call after
idle. Provisioned concurrency mitigates this if it matters for your
use case.
- **No server push / no long-lived session**: `stateless_http=True`
means every tool call is a fresh, self-contained request — nothing
is remembered between calls, and the server can't proactively push
messages to the client. Design tools so each call carries everything
it needs (this server already does — e.g. `query_logs` takes the
full time range and query string in one call).
- **Auth is on you**: the Function URL's IAM auth (or whatever you put
in front of it) is the only thing standing between "AI assistant" and
"anyone with the URL." Don't skip it, even in early testing, if the
function ever touches anything beyond a disposable sandbox account.
- **Logs Insights queries are polled, not pushed**: `query_logs` blocks
and polls `get_query_results` for up to `max_wait_seconds`. For very
large queries this can eat into the Lambda timeout — keep queries
scoped (`limit`, tight time ranges) rather than open-ended.
## 5. A note on FastMCP's moving API
FastMCP has changed its HTTP/stateless API more than once across
versions (the `stateless_http` flag has moved between the `FastMCP()`
constructor and `mcp.run()`/`mcp.http_app()` in different releases).
The code in `server.py` was verified against `fastmcp==3.4.7`
(`mcp.run(transport="http", stateless_http=True, ...)`). If you bump
the version and something breaks, check
[gofastmcp.com/deployment/http](https://gofastmcp.com/deployment/http)
first — this is the most likely thing to have shifted.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing