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.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues