Skip to main content
Glama
ziyuyu23

aws-readonly-mcp

by ziyuyu23
README.md
# aws-readonly-mcp

A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets
an LLM *inspect* an AWS account — list EC2 instances, S3 buckets, IAM users, and cost — but
**never mutate it**.

## Why I built it

When I help teams understand their cloud, I want an LLM that can answer "what's running and what's
it costing me?" without any ability to change the account. So the read-only guarantee is
structural, not a matter of trusting the model.

Every AWS call routes through a single function that checks the operation name against an
allow-list of read-only prefixes (`describe*`, `list*`, `get*`). There is **no code path** that can
`Run`, `Create`, `Delete`, `Terminate`, or `Modify` anything. If the model asks for a mutation, the
server refuses. Pair it with a read-only IAM policy (e.g. AWS-managed `ReadOnlyAccess`) for
defense in depth — the server enforces it in code, IAM enforces it at the boundary.

## What it does

| Tool | Description |
|------|-------------|
| `list_ec2_instances` | EC2 instances in a region: ID, state, type, AZ |
| `list_s3_buckets` | All S3 bucket names in the account |
| `get_cost_last_30_days` | Total cost over the last 30 days, grouped by service |
| `describe_iam_user` | Details for a single IAM user |

## Quick start

> **Prerequisite:** AWS credentials available via the standard chain (env vars, shared
> config, or `AWS_PROFILE`). The `aws` CLI is optional — only used for the identity check below.

```bash
# 1. Install (using uv — https://docs.astral.sh/uv/)
uv sync

# 2. Configure AWS credentials. STRONGLY recommended: use a profile whose IAM
#    role has only ReadOnlyAccess, so even a bug cannot mutate anything.
export AWS_PROFILE=my-readonly-profile
aws sts get-caller-identity   # confirm you're who you expect

# 3. Run the server
uv run aws-readonly-mcp
```

### Connect it to Claude Desktop

Add this to your Claude Desktop MCP config
(`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
  "mcpServers": {
    "aws-readonly": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/aws-readonly-mcp", "run", "aws-readonly-mcp"],
      "env": { "AWS_PROFILE": "my-readonly-profile" }
    }
  }
}
```

Then ask Claude things like *"Which services drove my AWS bill last month?"* — it calls
`get_cost_last_30_days`, reasons over the breakdown, and explains — but it physically cannot change
your account.

## Design decisions

- **Allow-list by operation prefix.** Read-only AWS operations are `Describe*`/`List*`/`Get*`. I
  allow those prefixes and reject everything else, so the boundary fails closed.
- **One chokepoint.** All boto3 calls go through `_aws_call`; the security boundary is one
  auditable function.
- **Defense in depth.** The README pushes you toward a `ReadOnlyAccess` IAM role so there are *two*
  independent guarantees (code + IAM), not one.
- **Trimmed responses.** Botocore `ResponseMetadata` is stripped so the model sees only the useful
  payload, saving tokens.

## What I'd do next

- Add pagination for accounts with many resources.
- Add a small eval asserting every mutating operation is refused.
- Support assuming a cross-account read-only role per request.

## License

MIT — see [LICENSE](LICENSE).