AWS MCP is not one server — it is a family of Model Context Protocol servers, published and maintained by AWS Labs (awslabs/mcp on GitHub), that expose AWS account state and AWS APIs to an AI coding agent through a standard tool-calling interface. Instead of your agent guessing what a Lambda function does from source code alone, or hallucinating an IAM policy that "should" work, it can call a tool that hits the real AWS API and gets back the real answer: the actual environment variables on that function, the actual CloudWatch error rate for the last hour, the actual attached IAM role and its resolved permissions.
For Mid/Senior engineers this matters because most AWS debugging time is spent context-switching: tailing logs in one browser tab, checking IAM policies in another, cross-referencing a security group in a third. MCP collapses that loop into the same terminal or editor session where you're already reading code — which is the whole value proposition of connecting cloud state directly into an LLM-driven workflow.
Core AWS MCP Tools: S3, Lambda, EC2, CloudWatch, and IAM Reads
The practically useful AWS MCP surface breaks into a handful of servers you'll actually reach for day to day. AWS Labs ships them as separate packages so you only load what you need — a smaller tool manifest means fewer tokens spent on tool definitions and less chance the model picks the wrong tool.
awslabs.aws-api-mcp-server— the general-purpose workhorse. It exposes acall_awstool that accepts an AWS CLI-style command string (e.g.s3api list-buckets,lambda get-function --function-name my-fn,ec2 describe-instances) and executes it against your configured credentials. This is how S3, Lambda, EC2, and IAM reads mostly happen in practice — one tool, arbitrary read (and optionally write) commands.awslabs.cloudwatch-mcp-server— purpose-built for logs and metrics. Tools likeget_metric_data,describe_log_groups,analyze_log_group, andexecute_log_insights_queryrun CloudWatch Logs Insights queries directly, which is materially faster than round-tripping through the generic CLI wrapper for anything involving log filtering.awslabs.cost-analysis-mcp-server— wraps Cost Explorer and cost-and-usage data so the agent can answer "what's driving my bill" without you exporting a CSV first.awslabs.core-mcp-server— a thin router/prompt-understanding layer AWS ships to help the agent pick the right specialized server when you've got several installed at once. Optional, but useful once you're running four or five AWS MCP servers together.
In this module, "AWS MCP" refers to this combination — primarily aws-api-mcp-server for S3/EC2/Lambda/IAM inspection, and cloudwatch-mcp-server for logs/metrics/alarms. Both are open source, Python-based (built on the same mcp SDK as other AWS Labs servers), and installable via uvx without a persistent install.
uv --version
uvx --version
uvx awslabs.aws-api-mcp-server@latest
uvx awslabs.cloudwatch-mcp-server@latest
Tips
- Don't install every AWS Labs MCP server "just in case." Each one adds tool definitions to every request's context window — for a Lambda-and-S3-heavy team,aws-api-mcp-server+cloudwatch-mcp-serveralone is usually enough.
- Pin versions in production configs (awslabs.aws-api-mcp-server==1.2.3style, or lock theuvxcache) rather than@latest— AWS Labs ships these servers fast and a breaking tool-schema change can silently break your agent's prompts.
AWS MCP Authentication: Profiles, SSO, Assumed Roles, and Region Config
AWS MCP servers do not manage credentials themselves — they inherit whatever the AWS SDK for Python (boto3) resolves from your environment, in the standard boto3 credential chain order: environment variables, shared credentials file, SSO cache, then instance/container role. This is the single most important thing to get right before you trust any output: the agent sees exactly what aws sts get-caller-identity would show you, no more, no less.
For day-to-day dev work, IAM Identity Center (AWS SSO) profiles are the right pattern — no long-lived access keys sitting in ~/.aws/credentials:
[profile dev-readonly]
sso_start_url = https://my-org.awsapps.com/start
sso_region = us-east-1
sso_account_id = 111122223333
sso_role_name = ReadOnlyAgentAccess
region = us-east-1
output = json
[profile prod-debug]
sso_start_url = https://my-org.awsapps.com/start
sso_region = us-east-1
sso_account_id = 444455556666
sso_role_name = ReadOnlyAgentAccess
region = us-west-2
output = json
Log in once per day (SSO tokens typically last 8–12 hours depending on your Identity Center session policy):
aws sso login --profile dev-readonly
Point the MCP server at a specific profile using the AWS_PROFILE env var in the MCP client config, rather than relying on whatever the default profile happens to be — this avoids the classic mistake of an agent accidentally running against prod because that was your last aws sso login:
{
"mcpServers": {
"aws-api": {
"command": "uvx",
"args": ["awslabs.aws-api-mcp-server@latest"],
"env": {
"AWS_PROFILE": "dev-readonly",
"AWS_REGION": "us-east-1"
}
}
}
}
Cross-account access via assumed roles works the same way it does for the CLI — chain a source_profile and role_arn:
[profile shared-services]
sso_start_url = https://my-org.awsapps.com/start
sso_region = us-east-1
sso_account_id = 777788889999
sso_role_name = AgentBaseAccess
region = us-east-1
[profile app-prod-assume]
role_arn = arn:aws:iam::444455556666:role/AgentCrossAccountReadOnly
source_profile = shared-services
region = us-west-2
Region matters more than people expect: an MCP tool call with no explicit region param silently uses whatever AWS_REGION or the profile's region resolves to. If your Lambda is in eu-west-1 and your profile defaults to us-east-1, you'll get a clean "resource not found" instead of the answer you wanted — and a model will often just report "the function doesn't exist," which is technically true only for the region it looked in.
Tips
- Never hand an AWS MCP server a static IAM user's long-lived access key pair for anything beyond a quick local experiment — SSO-based profiles rotate and are auditable per human in CloudTrail; static keys are not.
- When debugging "the agent can't see my resource," runaws sts get-caller-identity --profile <name>andaws configure list --profile <name>yourself first — 80% of the time it's a stale SSO token or wrong region, not an MCP bug.
What AI Can Automate on AWS — and Which Operations to Keep Human-Gated
Draw this line explicitly before you connect anything to a real account, because the default posture of "give the agent a broad role and see what happens" is how you end up explaining a deleted RDS snapshot to your VP.
Safe to fully automate (read-only, side-effect-free):
- Listing and describing resources — s3api list-buckets, ec2 describe-instances, lambda list-functions, iam get-role.
- Log and metric queries — CloudWatch Logs Insights, get-metric-statistics, X-Ray trace lookups.
- Cost and usage reporting — Cost Explorer queries, tag-based cost breakdowns.
- Configuration drift detection — comparing live resource config against IaC (Terraform state, CDK synth output).
Fine to automate with a review step (agent proposes, human approves the diff):
- IAM policy edits — the agent drafts a least-privilege policy JSON, you review the exact statements before aws iam put-role-policy runs.
- Lambda environment variable or memory/timeout changes.
- Security group rule additions — these are cheap to get wrong and easy to over-scope (0.0.0.0/0 creeping in from a copy-pasted example).
Keep entirely human-gated, no matter how convenient MCP makes it:
- Anything with delete, terminate, or remove in the action name against production — s3 rm --recursive, rds delete-db-instance, ec2 terminate-instances.
- IAM role/policy changes that touch trust relationships (who can assume the role) rather than just permissions — a bad trust policy can grant account-wide access.
- Billing and account-level settings — closing accounts, changing payment methods, modifying Organizations SCPs.
- Anything affecting a resource without a recent, verified backup or Terraform/CloudFormation source of truth to roll back to.
The practical mechanism for enforcing this is the IAM policy attached to the credentials the MCP server runs under — not agent-side promises in a system prompt. A model instructed "don't delete anything" will still call a delete action if it decides that's the right fix and the underlying IAM policy allows it. Treat the prompt as a UX hint and the IAM policy as the actual control.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/ReadOnlyAgentAccess \
--action-names s3:DeleteBucket ec2:TerminateInstances \
--profile dev-readonly
Tips
- Runiam simulate-principal-policyagainst the agent's actual role before your first real session — don't assume a role named "ReadOnly" is actually read-only; someone may have appended a broader managed policy to it later.
- For any mutating action you do allow, prefer a two-step flow: agent generates the exact CLI command or Terraform diff, human runsterraform applyor executes the command themselves. Skip the "AI executes mutations autonomously" pattern until you have solid guardrails and rollback tested.
IAM Least-Privilege Policy Design for an AI Agent
The role your MCP server assumes should be scoped to exactly what your workflows need — not ReadOnlyAccess (the AWS managed policy), which is broader than most teams realize: it includes reading Secrets Manager secret values in some contexts, KMS key policies, and other resources you probably don't want an LLM ingesting into a context window that might get logged or sent to a third-party API.
Start from a deny-by-default custom policy and add specific read actions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Inspection",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:GetBucketPolicy",
"s3:GetBucketTagging"
],
"Resource": "*"
},
{
"Sid": "LambdaInspection",
"Effect": "Allow",
"Action": [
"lambda:ListFunctions",
"lambda:GetFunction",
"lambda:GetFunctionConfiguration",
"lambda:GetPolicy",
"lambda:ListTags"
],
"Resource": "arn:aws:lambda:*:111122223333:function:*"
},
{
"Sid": "Ec2Inspection",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcs",
"ec2:DescribeSubnets"
],
"Resource": "*"
},
{
"Sid": "CloudWatchInspection",
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:GetLogEvents",
"logs:StartQuery",
"logs:GetQueryResults",
"cloudwatch:GetMetricData",
"cloudwatch:DescribeAlarms"
],
"Resource": "*"
},
{
"Sid": "IamReadOnly",
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:SimulatePrincipalPolicy"
],
"Resource": "*"
},
{
"Sid": "ExplicitDenySecrets",
"Effect": "Deny",
"Action": [
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"ssm:GetParameters",
"kms:Decrypt"
],
"Resource": "*"
}
]
}
Three design decisions worth calling out explicitly:
- Explicit deny on secret values, even under a read-only role. IAM evaluates explicit denies before allows, so this survives someone later attaching a broader managed policy to the same role by mistake. Your agent can still list that a secret exists (useful for "is this configured") without ever pulling the plaintext value into the model's context.
- Resource-scoped ARNs where the API supports it.
lambda:GetFunctionscoped tofunction:*in your account is meaningfully tighter thanResource: "*", even though several EC2/CloudWatch describe-style actions don't support resource-level scoping at all (check the IAM service authorization reference per action — it varies). iam:SimulatePrincipalPolicyincluded on purpose. It lets the agent (and you) sanity-check what a role can actually do without granting the role itself broader access — genuinely useful for the "explain this permission denial" workflows covered later in this module.
Attach this to a dedicated role, never to a shared human IAM user, and tag it clearly:
aws iam create-role \
--role-name ReadOnlyAgentAccess \
--assume-role-policy-document file://trust-policy.json \
--tags Key=Purpose,Value=ai-agent-mcp Key=Owner,Value=platform-team
aws iam put-role-policy \
--role-name ReadOnlyAgentAccess \
--policy-name agent-readonly-inspection \
--policy-document file://agent-readonly-policy.json
Tips
- Audit which policy is actually attached every quarter —aws iam list-attached-role-policiesandlist-role-policies— least-privilege roles drift toward over-permissioned over time as people add "just one more action" under deadline pressure.
- If you need the agent to propose (not execute) mutations, grant it read access plusiam:SimulatePrincipalPolicyand*:GenerateServiceLastAccessedDetails-style analysis actions, but keep actualPut/Delete/Createactions out of its role entirely — force those through a human-runawscommand or a PR-reviewed Terraform change.
Tips
Tips
- Treat AWS MCP as read-first tooling. The fastest way to build trust in the setup — with your team and with yourself — is a few weeks of pure inspection and debugging before you ever let it touch aPut/Create/Deleteaction.
- Keep a dedicateddev-readonlySSO profile for agent sessions, separate from your personal admin profile, and make the MCP config reference it explicitly viaAWS_PROFILE— never rely on "whatever profile happens to be active."
- Re-runiam simulate-principal-policyafter any change to the agent's role. Policy JSON is easy to get subtly wrong (a missingResourcescope, an overly broadAction: "s3:*"), and simulation catches it before an agent session does.