Skip to main content
Glama
tahanadeem125

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.