Claude Code is the cleanest environment to run AWS MCP in day to day, because the same session that reads your Lambda handler code can also query the live function config, the log group it writes to, and the IAM role it assumes — no tab switching, no copy-pasting ARNs between windows. This topic covers wiring it up and three workflows you'll use constantly: log-driven Lambda debugging, general resource inspection, and IAM denial diagnosis.
Installing and Connecting AWS MCP to Claude Code
Claude Code reads MCP server definitions from .mcp.json (project-scoped, checked into the repo) or from your user-level config (claude mcp add with --scope user). For AWS work, project-scoped is usually wrong — you don't want an AWS profile name baked into a file your whole team pulls — so add it at user scope and reference the profile via environment variable instead.
claude mcp add aws-api --scope user \
--env AWS_PROFILE=dev-readonly \
--env AWS_REGION=us-east-1 \
-- uvx awslabs.aws-api-mcp-server@latest
claude mcp add aws-cloudwatch --scope user \
--env AWS_PROFILE=dev-readonly \
--env AWS_REGION=us-east-1 \
-- uvx awslabs.cloudwatch-mcp-server@latest
Verify both are live before starting real work:
claude mcp list
If either shows ✗ Failed, it's almost always one of three things: uvx not on PATH (Claude Code inherits your shell's PATH, but a login shell vs. non-login shell mismatch trips people up on macOS), the SSO token expired (aws sso login --profile dev-readonly fixes it), or a typo in the profile name. Run claude mcp get aws-api to see the exact resolved command and env before digging further.
For the VS Code extension, the setup is identical because it shells out to the same claude CLI under the hood — MCP servers registered at user scope show up automatically in the extension's session. You don't need a separate VS Code-specific MCP config. Open the Claude Code panel, and confirm connection status via the MCP indicator in the status bar rather than re-adding the server.
/mcp
Tips
- Add AWS MCP servers at--scope user, not--scope project— an.mcp.jsonwith someone's dev profile name committed to a shared repo is a minor but real leak of internal infra naming, and it breaks for teammates with differently named profiles.
- Runclaude mcp addonce per machine, not per project — user-scope servers are available across every Claude Code session on that machine, so there's no reason to redo this per repo.
Querying CloudWatch Logs and Metrics to Diagnose a Lambda Failure
This is the highest-value AWS MCP workflow for most backend teams: a Lambda is failing, and instead of opening the CloudWatch console, finding the right log group, guessing a time range, and squinting at raw log lines, you ask Claude Code directly.
Our `order-fulfillment-prod` Lambda has an elevated error rate in the last hour.
Pull the CloudWatch metrics for Errors and Throttles, then run a Logs Insights
query against /aws/lambda/order-fulfillment-prod filtering for ERROR level
entries in the same window. Summarize the top 3 distinct error messages by
frequency and show one full stack trace for the most common one.
Behind that prompt, the agent runs something close to this CloudWatch Logs Insights query via the execute_log_insights_query tool:
fields @timestamp, @message
| filter @message like /ERROR/
| stats count(*) as errorCount by @message
| sort errorCount desc
| limit 20
And a metric pull for the error/throttle counts:
aws cloudwatch get-metric-data \
--metric-data-queries '[
{
"Id": "errors",
"MetricStat": {
"Metric": {
"Namespace": "AWS/Lambda",
"MetricName": "Errors",
"Dimensions": [{"Name": "FunctionName", "Value": "order-fulfillment-prod"}]
},
"Period": 300,
"Stat": "Sum"
}
}
]' \
--start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--profile dev-readonly
The value-add over doing this manually isn't the query itself — any senior engineer can write Logs Insights syntax. It's that the agent correlates the log output with your actual handler code in the same breath, without you re-pasting stack traces into a chat window. If the top error is a KeyError: 'customer_tier', it can immediately grep your repo for where that key is read and flag the code path that assumes a field the upstream event stopped sending.
Narrow the time window aggressively for cost and speed — Logs Insights bills per GB scanned, and a limit clause doesn't reduce the scan cost, only the returned rows:
fields @timestamp, @message, @requestId
| filter @message like /ERROR/ and @timestamp > 1719400000000
| sort @timestamp desc
| limit 50
Tips
- Always give the agent an explicit time window ("last hour", "since 14:00 UTC") — an unscoped Logs Insights query against a high-volume log group can scan gigabytes and take over a minute, and you'll pay for the scan either way.
- Ask for@requestIdalongside@messagewhen debugging Lambda — it lets you (or a follow-up query) pull the complete set of log lines for one specific invocation instead of a mixed stream from concurrent executions.
Inspecting S3 Buckets, EC2 Instances, and Security Groups from the Terminal
The aws-api-mcp-server's call_aws tool accepts CLI-syntax commands directly, so prompts read almost like giving a very literal junior engineer CLI instructions — except this one also reads and reasons over the output.
List all S3 buckets tagged Environment=production, then for each one check
whether public access block is fully enabled and whether default encryption
is configured. Flag any bucket where either check fails.
This resolves into a sequence like:
aws s3api list-buckets --query "Buckets[].Name" --profile dev-readonly
aws s3api get-bucket-tagging --bucket my-prod-assets --profile dev-readonly
aws s3api get-public-access-block --bucket my-prod-assets --profile dev-readonly
aws s3api get-bucket-encryption --bucket my-prod-assets --profile dev-readonly
For EC2 and security groups, a common real workflow is tracing why a service can't reach a database:
Our `checkout-api` EC2 instances (tag Name=checkout-api-*) can't reach the
`orders-db` RDS instance on port 5432. Describe the security groups attached
to both, and tell me specifically which inbound/outbound rule is missing.
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=checkout-api-*" \
--query "Reservations[].Instances[].[InstanceId,SecurityGroups]" \
--profile dev-readonly
aws ec2 describe-security-groups \
--group-ids sg-0abc123def456789 \
--query "SecurityGroups[].IpPermissionsEgress" \
--profile dev-readonly
aws ec2 describe-db-instances \
--db-instance-identifier orders-db \
--query "DBInstances[].VpcSecurityGroups" \
--profile dev-readonly
The agent diffs the checkout API's egress rules against the RDS instance's ingress rules and tells you, in plain language, "the RDS security group only allows inbound 5432 from sg-0aaa111, but your EC2 instances are in sg-0bbb222 — add an ingress rule referencing that group ID." That's a five-minute manual task turned into a fifteen-second one, and it's exactly the kind of cross-referencing LLMs are good at when given real data instead of being asked to guess.
Tips
- Ask for--queryJMESPath filtering in your prompts when you know roughly what shape of data you need — it keeps tool output small, which keeps your context window from filling with raw JSON you don't need the model to re-read on every turn.
- For security group tracing, always ask the agent to show its intermediate command output, not just the conclusion — SG rule diffing is exactly the kind of task where a wrong assumption (wrong instance, wrong SG) produces a confident, wrong-sounding answer.
Prompting AI to Explain and Fix an IAM Permission Denial
AccessDenied errors are one of the best use cases for AWS MCP because the fix requires correlating three things a human has to manually gather: the exact API call that failed, the IAM policy attached to the caller, and (often) a resource-based policy on the target resource. The agent can pull all three in one session.
This Lambda function's execution role is getting AccessDenied when it tries
to write to the `analytics-events` S3 bucket. The role is
`lambda-analytics-ingest-role`. Get the role's attached and inline policies,
get the bucket policy on analytics-events, and tell me exactly which
statement is missing or which one is denying the action.
The agent typically runs:
aws iam list-attached-role-policies --role-name lambda-analytics-ingest-role --profile dev-readonly
aws iam list-role-policies --role-name lambda-analytics-ingest-role --profile dev-readonly
aws iam get-role-policy --role-name lambda-analytics-ingest-role --policy-name inline-s3-write --profile dev-readonly
aws s3api get-bucket-policy --bucket analytics-events --profile dev-readonly
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/lambda-analytics-ingest-role \
--action-names s3:PutObject \
--resource-arns arn:aws:s3:::analytics-events/* \
--profile dev-readonly
simulate-principal-policy is the tool that actually answers the question definitively — it returns explicitDeny, implicitDeny, or allowed per action/resource pair, which tells you immediately whether the problem is a missing Allow or an active Deny (commonly from an SCP at the Organizations level, which won't show up in the role's own policies at all). A good agent response flags that distinction: "the role's own policy allows s3:PutObject, but an Organizations SCP is denying it — check with your account admin for an SCP exception, this isn't fixable at the role level."
For a genuine missing-permission case, ask the agent to draft — not apply — the fix:
{
"Sid": "AllowAnalyticsWrite",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::analytics-events/*"
}
aws iam put-role-policy \
--role-name lambda-analytics-ingest-role \
--policy-name inline-s3-write \
--policy-document file://updated-policy.json
Tips
- Always ask foriam simulate-principal-policyoutput, not just a policy read — a policy that looks correct on paper can still be overridden by an SCP, a permissions boundary, or a resource-based policy the agent hasn't checked yet.
- Keep policy mutation as a human-executed step even when the agent's diagnosis is clearly correct — IAM changes are one of the few AWS operations where a subtly wrongResourceARN (a missing/*suffix, for instance) silently grants far more than intended.
Tips
Tips
- Register AWS MCP servers at Claude Code's user scope with the profile baked into--env, so every session on your machine gets a consistent, auditable identity — never a project-committed config with someone's personal profile name in it.
- Default to CloudWatch-first debugging for anything Lambda-related; it's cheaper and faster than the genericcall_awstool for log-heavy investigations, and the Logs Insights query language gives you aggregation the raw CLI doesn't.
- For IAM denials, insist onsimulate-principal-policyin the diagnosis — it's the one tool that actually resolves ambiguity between role policy, resource policy, and SCP-level denies instead of guessing from policy JSON alone.