Skip to main content
Glama
DataExplorerX

AWS Ops MCP Server

README.md
# AWS Ops MCP Server

An MCP (Model Context Protocol) server that lets an LLM client — Claude
Desktop, Claude Code, or any other MCP-compatible client — inspect the
health of your AWS Lambda functions and S3 buckets using plain English:

- "Which of my Lambda functions have errored in the last hour?"
- "Show me the config for `my-api-handler`."
- "How big is the `uploads` bucket and what storage classes is it using?"
- "Give me invocation count, error rate, and average duration for `my-api-handler` over the last day."

Every tool is **read-only** — nothing here creates, modifies, or deletes an
AWS resource. That's a deliberate design choice: it keeps the IAM policy
tiny and makes the project safe to point at a real account.

## Authentication

Claude's "custom connector" flow for remote MCP servers always performs an
OAuth handshake, even against servers that don't otherwise need auth --
connecting without one fails with *"Couldn't register with [name]'s sign-in
service."* This project implements a minimal, spec-compliant OAuth 2.1
authorization server (`src/auth_provider.py`) to satisfy that requirement:

- **Dynamic client registration, authorization-code + PKCE, and refresh
  token flows** are all implemented per the MCP SDK's
  `OAuthAuthorizationServerProvider` protocol. PKCE verification itself is
  handled by the SDK; this provider just stores/retrieves the state around
  it.
- **State is stored in DynamoDB**, not in memory, because Lambda containers
  are ephemeral -- a login flow spans several separate HTTP requests
  (register → authorize → token) that can each land on a different
  container or a cold start, so whatever holds that state has to survive
  across invocations. A single pay-per-request table
  (`OAuthStateTable` in `template.yaml`) with a TTL attribute covers
  clients, authorization codes, access tokens, and refresh tokens.
- **Authorization is auto-approved** (no login screen) rather than gated
  behind real user credentials, since this server has exactly one owner.
  That's a deliberate, documented tradeoff for a personal/portfolio project
  -- anyone holding the deployed URL and a valid token could call the
  tools. If you ever point this at something more sensitive, swap
  `authorize()` in `auth_provider.py` for a real login step before issuing
  the authorization code.

Locally (`local_run.py`, stdio transport), none of this runs -- auth is
only enabled when `MCP_BASE_URL` is set, which the SAM template sets
automatically for the deployed Lambda.

## Architecture

```
MCP Client (Claude Desktop / Claude Code)
        │  OAuth handshake (register/authorize/token) + streamable-HTTP
        ▼
  API Gateway ($default route) ──▶  Lambda (Mangum ▶ FastMCP ASGI app)
                                        │              │
                                        │              ▼
                                        │      DynamoDB (OAuth state:
                                        │       clients/codes/tokens)
                                        ▼
                        boto3 ▶ Lambda API / CloudWatch Logs & Metrics / S3
```

- **`src/server.py`** — the MCP server itself: tool definitions built with
  `FastMCP`, run in `stateless_http` mode (no in-memory session state,
  which matters because Lambda cold starts wipe memory between invocations
  anyway). Wires in the OAuth provider only when deployed (see
  "Authentication" below).
- **`src/auth_provider.py`** / **`src/oauth_store.py`** — the minimal OAuth
  authorization server and its DynamoDB-backed state store (see
  "Authentication" below).
- **`src/aws_clients.py`** — the actual boto3 calls, kept separate from the
  MCP layer so the AWS logic can be unit-tested on its own (see `tests/`).
- **`src/handler.py`** — the Lambda entry point. [Mangum](https://mangum.io/)
  adapts API Gateway's event format to the ASGI interface FastMCP exposes.
- **`template.yaml`** — AWS SAM template: one Lambda function, one HTTP API
  route (`ANY /mcp`), and an inline least-privilege IAM policy scoped to
  exactly the six read-only actions the tools need.
- **`local_run.py`** — runs the same server over stdio for local testing
  with Claude Desktop, no AWS deployment required.

## Tools exposed

| Tool | Purpose |
|---|---|
| `list_lambda_functions` | List all functions with runtime, memory, last-modified |
| `get_lambda_function_config` | Full config for one function |
| `get_lambda_recent_errors` | Tail recent ERROR/exception log lines via CloudWatch Logs Insights |
| `get_lambda_metrics` | Invocations, errors, error rate, throttles, avg duration |
| `list_s3_buckets` | List all buckets with creation date |
| `get_s3_bucket_summary` | Object count, total size, storage-class breakdown |

## Local setup

```bash
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

Make sure valid AWS credentials are available in your environment (e.g.
`aws configure`, or exported `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`).

### Test locally with Claude Desktop

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

```json
{
  "mcpServers": {
    "aws-ops": {
      "command": "/absolute/path/to/venv/bin/python",
      "args": ["/absolute/path/to/aws-ops-mcp-server/local_run.py"]
    }
  }
}
```

Restart Claude Desktop, then try: *"List my Lambda functions."*

### Run the unit tests

```bash
pip install pytest
pytest tests/ -v
```

These mock boto3 directly, so they run without AWS credentials or network
access.

## Deploying to AWS (Lambda + API Gateway)

Requires the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html).

```bash
sam build
sam deploy --guided
```

`sam deploy --guided` will prompt for a stack name and region, then print
the deployed API endpoint (also available afterwards via `sam list
stack-outputs`). Point an MCP client that supports streamable-HTTP servers
at `<endpoint>/mcp`.

### IAM permissions

The SAM template grants exactly these read-only actions, nothing more:

- `lambda:ListFunctions`, `lambda:GetFunctionConfiguration`
- `logs:StartQuery`, `logs:GetQueryResults`, `logs:DescribeLogGroups`
- `cloudwatch:GetMetricData`
- `s3:ListAllMyBuckets`, `s3:ListBucket`

## Extending it

Natural next steps if you want to keep building on this:

- Add a `get_cost_by_service` tool using the Cost Explorer API.
- Add write-scoped tools behind an explicit confirmation step (e.g.
  "restart this Lambda's concurrency" ) — a good way to demonstrate you
  understand the difference between read-only and mutating tool design.
- Swap the CloudWatch Logs Insights query in `get_lambda_recent_errors` for
  a structured-logging-aware filter if your functions emit JSON logs.
- Add auth (the `FastMCP` constructor accepts a `token_verifier` for this)
  before exposing the endpoint outside a private network.

## Why this project

This was built to demonstrate backend + AWS skills relevant to production
engineering roles: serverless architecture (Lambda, API Gateway, SAM),
least-privilege IAM design, structured use of CloudWatch Logs Insights and
metrics APIs, and wiring up the emerging MCP protocol as a real,
deployable service rather than a toy demo.