Skip to content
Agentic Control Plane

The AWS API MCP server control model, explained

The AWS API MCP Server is one server inside awslabs/mcp, AWS’s monorepo suite of first-party MCP servers — there are dozens, per-service and specialized; this page covers the general-purpose one, which wraps the entire AWS CLI (source and README, docs). Its adoption in coding agents is thinner than GitHub’s or Supabase’s default wiring, but it earns the deepest page in this series for one reason: it is the only server we’ve surveyed that ships a native per-call human approval. Two more facts frame everything below: the docs say plainly that “IAM permissions remain the primary security control,” and the repo now carries a notice that this server is superseded by a managed remote AWS MCP Server — a succession that, as we’ll see, retires the approval mechanism.

This page covers the server’s own controls. For the cross-server picture, see the MCP server controls comparison; for what a control layer adds on top, the coverage matrix.

What’s at stake on this surface

Every other server in this series fronts one product. This one fronts an AWS account. The primary tool, call_aws, validates and executes arbitrary AWS CLI commands — the surface is whatever the credentials can reach: EC2 termination, S3 deletion, IAM policy changes (the write that widens every other write), KMS, databases, billing. A suggest_aws_commands tool helps the model find commands it doesn’t know, including APIs newer than its training data. Unscoped, this is the maximum blast radius a tool call can have. The server’s own docs are unusually frank about the residual risks: no sandboxing, local file writes without confirmation, file-access restrictions that apply only to paths passed directly as CLI arguments, and read-only calls that “can still return AWS credentials or sensitive information.”

IAM is the floor — and here, the vendor-side boundary

Stripe is this series’ benchmark for vendor-side scoping: a restricted key checked by Stripe’s servers on every call. AWS has had the richer version of that for two decades — IAM. The server authenticates through the standard credential chain (AWS_API_MCP_PROFILE_NAME, default default, or the usual env vars), and every call is authorized by AWS against the principal’s policy. That boundary holds regardless of anything in the MCP config: condition keys, resource ARNs, permission boundaries, session policies — a vocabulary far finer than Stripe’s binary read/write per resource. AWS’s guidance matches: least privilege, custom policies with condition statements, scoped-down credentials when the agent touches untrusted data.

The floor is real; the practice is the risk. The same credential chain happily picks up an administrator profile, and nothing in the server stops you from handing an agent AdministratorAccess — the docs simply note that with it, “mutating actions” are allowed. Which credentials you point the profile at is the control model, exactly as the key was at Stripe. Everything below is defense in depth above that line.

Read-only mode: a classification filter, fail-closed

READ_OPERATIONS_ONLY=true (default false) restricts execution to operations classified read-only against an index the server bundles. The engineering is careful in a way worth crediting: if the index fails to load at startup, the server refuses to start, and if it’s missing at request time, the request is denied — fail-closed at both ends, which is rarer than it should be. Three honest caveats from AWS’s own docs: the classification is API-level, so a “read-only” command can still write to the local filesystem; it’s client-side enforcement, subordinate to IAM; and it composes with, rather than replaces, a read-only credential. Set both.

This is the page’s centerpiece, so precision matters. Verified against the shipped source, not just the README:

  • The switch. REQUIRE_MUTATION_CONSENT=true (default false). When on, any operation not classified read-only returns an ELICIT decision from the server’s policy check.
  • The mechanism. The server uses MCP elicitation: before executing, it sends the client an elicitation request showing the exact CLI command — “requires explicit consent. Do you approve the execution of this command?” — and waits. The human answers in the client’s UI. Approve executes; decline raises an error and the command never runs.
  • Per-command consent without the global switch. A JSON policy at ~/.aws/aws-api-mcp/mcp-security-policy.json takes a denyList (never executed) and an elicitList (consent required), as exact-match aws <service> <operation> strings — no wildcards, a documented limitation. Composite commands like s3 cp are expanded into their underlying API calls and graded DENY > ELICIT > ALLOW: one deny-listed sub-call blocks the whole command; one elicit-listed sub-call forces consent for it.
  • The fail-mode — the load-bearing fact. Elicitation requires client support, and not every client has it. The server fails closed, twice over. At the policy layer, elicit-list entries are treated as deny when elicitation is unavailable. At execution time, if the elicitation request returns METHOD_NOT_FOUND, the server raises an error — “Client does not support elicitation. Use a different client or update the server configuration.” — and the command is rejected, not executed. Even operations the server can’t identify well enough to classify get ELICIT where possible and DENY otherwise. A consent gate that silently allowed when nobody could be asked would be theater; this one holds.

Grade it plainly: this is the only native, per-call, human-in-the-loop approval shipped by any first-party MCP server in this series, and it errs in the safe direction at every seam we checked. Now the boundaries of that credit. It’s still launch-time client configuration — an env var and a JSON file on the same machine as the agent, off by default. The trigger vocabulary is binary (read-only or not; or an exact-match list): no “ask only above this instance size,” no conditions on what was just read. The prompt renders in whatever UI the client gives elicitation, at whatever autonomy posture the client is in. And the record of who approved what exists nowhere the operator can query.

Then the succession. The repo marks this server superseded by the managed remote AWS MCP Server, and the migration guide is blunt about the consent mechanism’s fate: REQUIRE_MUTATION_CONSENT is “Not needed — use IAM policies to control write access.” The one native ask in the ecosystem is being retired by its own vendor, with per-call human approval folded back into launch-time credential scope — which answers a different question. IAM decides what the agent may ever do; consent decided whether a human wanted this call to happen. The series’ claim used to be that no vendor had shipped a native approval. The corrected claim is stranger: one did, built it fail-closed, and then deprecated it.

What CloudTrail does and doesn’t give you

The server writes a rotating local log (~/.aws/aws-api-mcp/aws-api-mcp-server.log) of command executions, shippable to CloudWatch — more than most servers in this series leave behind, and worth turning on. Downstream, the docs’ best practice is to monitor CloudTrail, and its character should be stated precisely: CloudTrail is the account-plane API record. It logs every call the server’s credentials made — attributed to the IAM principal, not the agent, indistinguishable from any other use of those credentials, with no notion of MCP tools, consent decisions, or which posture (READ_OPERATIONS_ONLY on or off) was in effect. It’s strong forensics for what happened in the account and silent on what the agent decided and who approved itthe general MCP audit gap, on the surface where forensics matter most.

Where the native model ends

The strongest single-server lineup in this series — vendor-enforced IAM floor, fail-closed read-only mode, a real consent gate, a deny/elicit policy file, an actual local log — and the seams are still visible:

  • Every server-side control is client-side configuration. Env vars and a JSON file, off by default, living where the agent lives. Only IAM is enforced beyond the agent’s reach.
  • The policy vocabulary is thin. Exact-match strings, no wildcards, binary read-only classification. “Consent for deletes but not tags,” “deny outside us-east-1” — expressible in IAM condition keys, not in the server’s own controls.
  • The approval leaves no record. Consent decisions render in a client dialog and vanish; the local log records executions, CloudTrail records the principal. Nothing joins agent, command, decision, and approver.
  • The mechanism is on the way out. The managed successor drops both READ_OPERATIONS_ONLY and REQUIRE_MUTATION_CONSENT, returning everything to IAM.

The practical posture: scope a dedicated IAM role tightly with condition keys (the floor, and here it’s the best floor in the series), run READ_OPERATIONS_ONLY=true unless the task needs writes, turn REQUIRE_MUTATION_CONSENT=true on when it does and use an elicitation-capable client, deny-list the operations you’d never want an agent near, ship the log to CloudWatch — and put the conditional policy, the approval that outlives this server’s deprecation, and the agent-attributed record in a layer that stands between the agent and the credentials. AWS proved a native ask can be built fail-closed. Its own roadmap is why the ask can’t live in the server.