@yawlabs/aws-mcp
[Add to Yaw MCP]
One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
A small AWS MCP for AI assistants: one server, one config entry, SSO re-auth baked in, generic CRUD over 1,300+ resource types, live docs lookup, server-side scripting for batched workflows.
It's an alternative to AWS's official MCP server, not a complement -- both reach any AWS API, so running both hands the model two overlapping ways to do the same thing. (AWS gives the same advice about its own older servers: its setup guide says to remove them "to avoid tool conflicts that can confuse AI agents".) Pick one. They overlap in coverage and differ in shape. The honest comparison:
- AWS MCP Server -- AWS's hosted server, GA since May 2026 and part of the Agent Toolkit for AWS. Strong on AWS-team-curated skills, a server-side Python sandbox (
run_script) with days-fresh API coverage, a read-only serverless troubleshooting capability (Lambda diagnose, recent changes, X-Ray trace summaries), IAM condition keys that tell its calls apart from direct API calls, and per-tool CloudWatch metrics. As of September 2026, run_script is its only general-purpose way to call an AWS API -- the single-call call_aws tool has been removed -- so every call, even a one-off describe, is a Python script the model writes. The endpoint runs in us-east-1 and eu-central-1. Two ways to connect:
- OAuth through AWS Sign-in (since July 2026): nothing to install -- your client opens a browser and connects straight to the endpoint. Each session is bound to one IAM role and refreshes for up to 12 hours, and the principal needs
signin:AuthorizeOAuth2Access and signin:CreateOAuth2Token.
- SigV4 through a local proxy run with
uv (uvx mcp-proxy-for-aws-cli@latest in AWS's guide), signing with your AWS CLI credentials (CLI 2.32.0+). AWS recommends this path for terminal and IDE coding agents, and it is the only one that switches profiles per call, from an allowlist fixed when the proxy starts. Since AWS CLI 2.35.0, aws configure agent-toolkit writes a SigV4 entry (uvx mcp-proxy-for-aws@latest) into your agent's MCP config for you, under the key aws-mcp.
@yawlabs/aws-mcp (this server) -- installs from npm and runs locally on your own aws CLI and profiles: one npx line, no uv, no proxy, no hosted hop. Wins on SSO re-login when aws sso login's browser handoff drops (Windows especially), one AWS operation per tool call (aws_call takes service, operation and params, so a host's approval prompt shows the operation itself, not a script), ergonomic CCAPI CRUD with dry-run diffs, multi-region and multi-account fan-out, pre-flight IAM permission checks, and a JS scripting tool for when you do want a batch (in-process, not a security sandbox -- see the tools table). Live AWS docs search + page read are built in too, so you don't need a second docs server either way -- they cover the same ground as the official server's search_documentation / read_documentation, without its topic routing or skills results.
The MCPs that genuinely pair with either choice are the per-service servers in awslabs/mcp that reach what a general AWS-API tool cannot -- Bedrock's agentic Knowledge Base retrieval is the clearest case (see the companion config). AWS now describes that repo as succeeded by the Agent Toolkit for AWS; it still works and takes contributions, but some of its servers are deprecated or superseded -- its general AWS API server among them -- so check a server's README before adding it.
Five things this server tries to handle well:
- SSO re-login. When your token expires mid-session,
aws sso login tries to open a browser from a subprocess -- on Windows (and sometimes elsewhere) that handoff drops silently. You end up context-switching to a terminal, running the command yourself, then coming back. The --no-browser device-code flow fixes this: the assistant surfaces a short URL + code, you click once, done. (--no-browser on its own is no longer enough -- AWS CLI 2.22.0 made the PKCE authorization-code flow the default, and it prints no short code -- so this server pairs it with --use-device-code, probing aws --version once to stay compatible with pre-2.22 CLIs.) There's also aws_refresh_if_expiring_soon for proactive top-ups before a long workflow. AWS's hosted server goes around the problem rather than through it. On its OAuth path your MCP client runs its own browser sign-in, and the tokens are bound to that client and that server, so nothing else on the machine benefits; on its SigV4 path, AWS's troubleshooting table tells SSO users to run aws sso login themselves and then restart the MCP client. Here the re-login refreshes the same ~/.aws/sso/cache token the CLI, the SDKs and every other tool on the machine read.
- Calling any AWS API.
aws_call proxies the aws CLI directly. One tool covers the full API surface -- including services AWS adds tomorrow -- with no SDK bundling and no service-by-service tool sprawl. That is not aspirational: September 2026's arrivals -- AWS Batch bulk cancel-jobs / terminate-jobs (CLI 2.36.44), the STS session-token size fields (2.36.45), Elastic Beanstalk cluster environments (2.36.47), "Tunnel" VPC endpoints (2.36.48) -- are reachable the moment your local aws CLI knows them, with no @yawlabs/aws-mcp upgrade. An older CLI rejects an operation it does not know before anything is sent, and the error says to upgrade. aws_paginate handles paginated list/describe ops, aws_multi_region fans the same op out across N regions in parallel, and a JMESPath query parameter trims responses server-side. Reach for them long before this server's 5 MB output cap: MCP hosts cut in much sooner -- Claude Code warns at 10,000 tokens and, by default, saves any result over 25,000 tokens to a file the model has to read back.
- Generic CRUD across services.
aws_resource_* (seven tools, including aws_resource_diff for dry-run previews) wraps AWS Cloud Control API, so the same lifecycle -- get / list / create / update / delete / status -- works for any control-plane resource with a CloudFormation schema: Lambda functions, S3 buckets, IAM roles, SSM parameters, RDS instances, and the rest of the 1,300 types on AWS's published list (not every type implements every verb). Pass awaitCompletion: true and the server polls the async create/update/delete through to terminal state for you. AWS Labs deprecated its own Cloud Control API MCP server in March 2026, and its migration guide lists no direct replacement for resource get / list / create / update / delete: the successor authors CloudFormation and CDK instead. CCAPI is control-plane only. On the data plane, DynamoDB get-item / query and Bedrock converse are ordinary operations aws_call handles (DynamoDB values stay in its typed JSON, {"S": "..."}), and Lambda invokes have their own tool, aws_lambda_invoke. Three kinds of operation are out of aws_call's reach: those that write their response body to a positional outfile (S3 get-object, Bedrock invoke-model), the CLI's hand-written commands, which register no --cli-input-json (s3 cp/ls/sync, logs tail, cloudformation deploy), and event-stream operations the CLI does not ship at all (Bedrock converse-stream, invoke-agent, agentic Knowledge Base retrieval).
- Live AWS docs.
aws_docs_search queries the same backend that powers the docs.aws.amazon.com search box; aws_docs_read fetches a doc page and returns it as paginated markdown. Lets the agent discover new services and look up exact parameter names without a second MCP server installed.
- Batched workflows in one round-trip.
aws_script runs a short JS snippet in a node:vm context with aws.call, aws.paginate, aws.paginateAll, aws.resource.*, aws.logsTail, aws.metricsQuery, aws.iamSimulate, aws.multiRegion, aws.assumeRole, and aws.docs.{search,read} available. Best for "list X, fetch Y for each, return Z" pipelines that would otherwise need N tool calls. Same idea as AWS's run_script (Python, sandboxed server-side), which is now that server's only general-purpose way to call an AWS API; here it is the batching option -- JS-native, running locally -- with aws_call for single operations.
Optional companion: AWS Labs per-service servers
For work a general AWS-API tool cannot do, add the relevant awslabs/mcp server alongside this one. Bedrock's agentic Knowledge Base retrieval is the clearest case: it calls AgenticRetrieveStream, an event-stream operation the AWS CLI leaves out of its command table, so no CLI-based tool -- aws_call included -- can reach it. (Plain retrieval, bedrock-agent-runtime retrieve, is an ordinary aws_call operation.) These are Python servers run with uvx, and they have no tool-name overlap with this one, so they pair cleanly:
Its agentic tool works on managed knowledge bases, and by default the server lists only knowledge bases tagged mcp-multirag-kb=true; its README covers the tag and the IAM permissions. Skip the older awslabs.lambda-mcp-server: every release is yanked on PyPI, and Lambda invokes are built in here as aws_lambda_invoke.
When to reach for this vs the other AWS MCPs
@yawlabs/aws-mcp and AWS's official server are an either/or -- pick the one whose tradeoffs fit. awslabs/mcp per-service servers pair cleanly with whichever you pick.
What this server borrows from AWS's official one
Credit where due -- two features here were shaped by the official AWS MCP Server:
aws_script mirrors the official server's run_script: a scripting tool that collapses "list X, fetch Y for each, return Z" pipelines into one round-trip. Theirs is Python, sandboxed server-side, and is now that server's only general-purpose API path; this one is JS-native, runs in this server's own process -- see the trust note in the tools table -- and sits beside aws_call rather than replacing it.
aws_docs_search / aws_docs_read were added so you don't need a separate docs MCP whichever server you pick. They cover the same ground as the official server's search_documentation / read_documentation -- live search and page reads -- without its topic routing or skills results.
The rest -- SSO device-code re-login, CCAPI CRUD with dry-run diffs, multi-region fan-out, IAM pre-flight checks -- is this server's own.
Tools
Install
Add to your MCP client config (e.g. .mcp.json):
Keep the key aws (anything but aws-mcp). AWS's aws configure agent-toolkit wizard registers its hosted server under aws-mcp, and reports an existing aws-mcp entry as already configured without looking at what it runs.
The -y flag is what gives you auto-update on each session load: every time your MCP client spawns the server, npx checks the registry for the latest @yawlabs/aws-mcp and downloads it if newer. The first launch in a fresh cache adds ~100-500 ms; subsequent launches use npm's cache (typical metadata-freshness window: 5 min) and add ~50 ms or less. Once the server is up, tool calls have zero auto-update overhead -- the check fires only on (re-)spawn. No separate install step is needed; -y covers both first-time install and ongoing updates.
If you'd rather pin a specific version (no auto-update, but zero startup overhead), install globally and point the config at the installed binary:
You'll need to npm install -g @yawlabs/aws-mcp@latest manually when you want a newer version.
Example session
You ask the assistant to check a staging bucket, but your SSO token just expired. What the assistant does (and what you see):
The SSO flow took one click. No "the browser didn't open, let me run it in a terminal" context switch.
For a larger list -- anything that would run past your MCP host's output limit, which is far smaller than this server's 5 MB cap -- the assistant reaches for aws_paginate:
query (JMESPath) trims the response server-side -- a typical describe-instances result shrinks from megabytes to kilobytes when you only need two fields.
For "create this resource and tell me when it's ready," aws_resource_create with awaitCompletion: true collapses the usual create-then-poll loop into one tool call:
Same shape for aws_resource_update and aws_resource_delete. Drop awaitCompletion (or set it false) for the default fire-and-poll behavior -- useful when you want to kick off a long-running update and check back later.
For "preview the patch before applying":
No mutation is sent to AWS; the agent can verify the patch before invoking aws_resource_update.
For batched workflows, aws_script collapses N tool calls into one:
For multi-region reads:
Requirements
- Node.js 22+ (or oam.js -- see Runtime)
- AWS CLI v2 on
PATH. Every tool that talks to AWS shells out to it (all but aws_docs_*, aws_session_* and aws_list_profiles), so the CLI you have installed decides which services, operations and parameters are reachable. No minimum version is enforced:
- 2.22.0+ recommended. That release added
--use-device-code, which this server needs to keep the SSO short-code flow working. Older 2.x still works -- the server detects the version and adapts.
- Developed and tested against 2.34.3. Anything newer than your CLI is rejected by the CLI itself before a request is sent: an unknown service or operation as an "invalid choice" (the error then says to upgrade), a new parameter as
Unknown parameter in input. Upgrade with aws update (CLI 2.36.0+, for installs made with AWS's installer or install script), otherwise with the installer or your package manager.
- Security, as of 2026-09: CLI 2.35.3 or newer clears every published AWS CLI v2 advisory (GHSA-747p-wmpv-9c78, CVE-2026-13769, CVE-2026-18654). None of the affected paths is reachable through this server: the four commands (
emr ssh/socks/put/get, codeartifact login, deploy register, iam create-virtual-mfa-device) all register no --cli-input-json, which is the only way aws_call passes parameters, and the third advisory is about the opt-in cli_history database, which this server never enables. You do share that CLI install with everything else on the machine, though. Worth checking the advisory list for newer ones: v2 ships as an installer, so a dependency scanner will never flag it.
- AWS CLI v1 is unsupported; it entered maintenance mode on 2026-07-15 and reaches end of support on 2027-07-15.
- An AWS profile the CLI can already use -- see Environment for how the profile is chosen. SSO / IAM Identity Center profiles also get the device-code re-login tools.
Runtime
This server runs on oam.js and on Node, unmodified, and
the launcher never serves on an oam older than 0.19.1, and picks the newest
oam binary it can find at or above that floor. Verified on oam 0.19.1
(aarch64-pc-windows-msvc): full MCP handshake listing all 28 tools, a local tool
call, and the aws_script sandbox behavior described below.
oam 0.19.1 is the minimum. The launcher picks the newest oam it can find at
or above it, never serves on an older one, and falls back to Node when there is
none (AWS_MCP_RUNTIME=oam turns that into a hard error). A floor matters here:
releases before 0.9.0 ran child_process.execFile arguments through a shell,
accepted exec's timeout and ignored it, and treated stdio: 'inherit' as
'pipe', and this server shells out to the aws CLI on essentially every tool.
To run it under oam, point your MCP client's command at it:
Measure startup on your own hardware. An MCP client cold-starts this server
once per session, so startup is the cost that actually gets paid. The numbers
below were taken with oam 0.8.2, long before the current 0.19.1 floor, and have
not been re-run since, so do not read them as a current ranking. To a completed
initialize + tools/list handshake, median of 10 warmed runs:
The published aws-mcp command prefers the newest oam it finds (see
AWS_MCP_RUNTIME under Environment). Without oam that costs
almost nothing: discovery is file-existence checks only, never a subprocess, and
the fallback runs the server inside the Node process npm already started. With
oam installed, though, the command boots Node, runs --version on every oam
binary it found to pick the newest, and only then boots oam, so it is always
slower than pointing your client at oam directly with the config above.
AWS_MCP_RUNTIME=node skips oam entirely.
Two more places oam wins for this repo, both opt-in and neither touching the
published npm package:
npm run check:oam -- type-checks via oam check (tsgo, TypeScript 7
native). Measured ~1.0s against ~3.8-4.7s for tsc --noEmit, resolving the
same tsconfig.json and covering the same files -- including tests, confirmed
by planting a type error in a test file and watching both reject it.
npx tsc --noEmit remains the portable default.
npm run build:binary:oam -- builds the standalone binary via
oam compile. Measured 58.60 MB on win32-arm64, including ~493 KB of
embedded V8 bytecode. Writes to bin/<platform>-<arch>/, which the release
staging script consumes. If you redistribute that binary it embeds oam's
runtime, so ship oam's LICENSE, NOTICE and THIRD_PARTY_LICENSES.md with
it.
The source stays runtime-agnostic on purpose: no oam: imports anywhere, and
tests stay on node:test. That is what keeps the Node fallback real rather than
nominal.
One behavioral difference worth knowing if you run aws_script under oam: Node
honors codeGeneration: { strings: false } on the node:vm context, so eval
and Function throw; oam does not, so they work. Re-measured against oam 0.19.1
and still divergent -- inside the sandbox, eval('1+1') returns 2 and
Function('return 7')() returns 7 under oam, while both raise EvalError under
Node -- so treat it as a standing difference. The containment that
matters is unaffected -- under oam, Function('return this')() yields a global
whose process and require are both undefined, and Function('return require') throws -- so a script gains nothing it couldn't already do by writing
the same code in its body. aws_script was never a security boundary (see its
description); the shadowed-globals list is the real defense, not that flag.
Note that any oam invocation writes a bytecode cache to oam/ in the working
directory -- already in .gitignore.
Environment
The launcher that the published aws-mcp command runs (bin/aws-mcp.mjs, which is what npx @yawlabs/aws-mcp starts) reads two more. They pick the runtime, not anything about AWS, and pointing your client straight at dist/index.js bypasses both. See Runtime for what running on oam changes.
What the server sets on every aws call. Each aws child process gets these, overriding your shell and ~/.aws/config, because each one changes output this server parses: AWS_CLI_ERROR_FORMAT=enhanced (CLI 2.34.0's json/yaml/text/table error formats remove the An error occurred (Code) text that errorKind classification reads), AWS_CLI_AUTO_PROMPT=off (auto-prompt wants a console and fails every call from an MCP host, aws sso login included), AWS_CLI_OUTPUT_ENCODING=utf-8 and PYTHONUTF8=1 (on Windows the CLI otherwise writes the ANSI code page and fails on any character outside it), and on Windows NoDefaultCurrentDirectoryInExePath=1 (so the CLI's own helpers, such as session-manager-plugin, are never run from the working directory). These are environment variables rather than flags, so an older 2.x CLI that does not know one simply ignores it. One side effect, and it is Windows-only: on CLIs older than 2.25.0, PYTHONUTF8=1 also makes the CLI read ~/.aws/config and ~/.aws/credentials as UTF-8, so a non-ASCII character saved there in a legacy Windows code page stops parsing -- re-save the file as UTF-8, or update the CLI. On macOS and Linux the pin costs nothing here, because there is no ANSI code page to switch away from: measured on linux/arm64 with aws-cli 2.36.49, a cp1252 byte in ~/.aws/config fails to parse identically with PYTHONUTF8 unset, =0 and =1, and under LC_ALL=C and LC_ALL=POSIX, while the same character encoded as UTF-8 parses in all of them. Calls that carry params also pass --cli-binary-format base64, so blob-typed params are always base64 whatever your config says. Left to your config on purpose: retry mode and max attempts, cli_timestamp_format (config-only in the CLI; wire returns epoch numbers instead of ISO strings), cli_history, and endpoint, proxy and CA settings.
If you authenticate via SAML (Okta / Azure AD / ADFS) or a custom credential_process, set AWS_PROFILE to that profile.
Every call resolves a profile name first -- explicit tool profile argument -> the session profile set by aws_session_set -> $AWS_PROFILE -> $AWS_DEFAULT_PROFILE -> the literal default -- and then passes it to the CLI as --profile <name>. There is no "no profile" mode, with one exception: aws_multi_account uses the resolved profile only for its sts:AssumeRole calls, and each per-account operation then runs on that account's assumed-role credentials with no --profile flag. Inside the chosen profile the CLI's own chain resolves as usual: credential_process, SSO sessions (both sso_session blocks and inline sso_start_url), role chaining via source_profile / role_arn, static keys stored in ~/.aws/credentials, container credentials, and IMDS.
Exception -- static keys in your environment are not used. Because a profile is always passed explicitly, botocore drops the environment credential provider from the chain, so AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN exported in your shell are never consulted. (Container credentials and IMDS are unaffected -- they sit later in the chain and are not profile-gated.) To use static keys, put them in a profile section of ~/.aws/credentials and point AWS_PROFILE at it, rather than exporting them.
Behind a proxy or a private CA
aws_docs_search and aws_docs_read fetch over HTTPS from this process, so they are the two tools a corporate gateway breaks. They now name the cause rather than blaming AWS's backend, and the fix is an env entry in your MCP config -- not an export in your shell, because both variables below are read when the process starts and your MCP client launches the server itself.
The AWS CLI is a separate process with its own rules, so a credential_process, an SSO login or any aws_call behind the same gateway follows the AWS CLI's own proxy configuration (HTTP_PROXY / HTTPS_PROXY and AWS_CA_BUNDLE) rather than the Node variables above. Those failures now carry their own remedy too.
If a call omits profile, aws_session_set has not been called, neither AWS_PROFILE nor AWS_DEFAULT_PROFILE is set to a non-empty value, and neither ~/.aws/config nor ~/.aws/credentials defines a default profile, the CLI rejects --profile default with ProfileNotFound, which the tool reports as a no_creds error. Set AWS_PROFILE in your MCP config to your usual working profile.
How the SSO login flow works
The token is cached in ~/.aws/sso/cache/<hash>.json the same way a normal aws sso login would, so the AWS CLI, the SDK, and every other tool on your machine pick it up transparently.
Why this server must run locally (not on mcp.hosting)
SSO tokens live in ~/.aws/sso/cache/ on your device. A remote MCP server can't read them. So this is a stdio server, not a hosted one. That's a constraint of AWS SSO, not a limitation of mcp.hosting.
Stability
From 1.0 onward this package follows Semantic Versioning. The 0.x line is the pre-stability tightening phase -- breaking changes are documented in CHANGELOG.md but are not necessarily gated on a major bump.
Stable in 1.x (anything below is a breaking change requiring a major bump):
-
Tool names -- the 28 tool names listed in the Tools table above will not be renamed or removed.
-
Tool annotations -- readOnlyHint, destructiveHint, idempotentHint, openWorldHint. These signal to MCP hosts how to gate calls; flipping them silently would break host UIs. Note the direction of the guarantee: an annotation may be tightened (made more cautious) in a patch release when it was previously understating what a tool can do -- v2.0.1 set destructiveHint: true on aws_call, aws_multi_region and aws_resource_update for exactly that reason. It will not be loosened outside a major. If your host suppresses confirmation prompts based on these, treat aws_call and aws_multi_region as able to invoke any AWS API the caller's IAM identity permits, including deletes.
-
Required input fields -- the required fields per tool will not change shape or be removed. New optional fields may be added.
-
Success envelope shape per tool -- the data object on {ok: true, data} responses, specifically:
aws_call -> {command, commandArgv, result}
aws_paginate -> {command, result, nextToken, hasMore}
aws_multi_region -> {service, operation, regionCount, okCount, errorCount, results: [{region, ok, data?, command?, error?, errorKind?, truncated?}]} (the aggregate response is capped; entries past the budget keep their region/ok but drop data and are flagged truncated: true. Error entries are never dropped, and okCount/errorCount are computed before capping, so they always describe what the calls did rather than what survived the cap.)
aws_multi_account -> {service, operation, roleName, accountCount, okCount, errorCount, results: [{accountId, ok, data?, command?, error?, errorKind?, truncated?}]} plus truncated, truncatedAccounts and maxTotalResultBytes when the 5 MB aggregate cap fired. Mirrors aws_multi_region field for field with accountId in place of region, including that okCount/errorCount are computed BEFORE capping so they describe what the calls did rather than what survived. Duplicate account IDs collapse, so results.length may be under accounts.length; use accountCount. Credentials are never written to ~/.aws/credentials and never appear in command, error or rawBody.
aws_whoami -> {account, userId, arn, profile, region, ssoToken: {expiresAt, minutesLeft, startUrl?} | null} (startUrl is omitted when the cached token didn't record one)
aws_login_start -> {sessionId, profile, verificationUrl, userCode, instructions, reused?} (reused: true when re-surfacing an in-flight login for the same profile)
aws_login_complete -> {loggedIn, account, userId, arn, profile, region, ssoToken} (same ssoToken shape as aws_whoami, including the optional startUrl)
aws_refresh_if_expiring_soon -> one of two shapes by branch: {status: "ok", minutesLeft, expiresAt, profile} when the cached token has more than thresholdMinutes left, or {status: "refreshing", reason, sessionId, profile, verificationUrl, userCode, reused?, instructions} when a refresh is in flight. Discriminate on status.
aws_assume_role -> {profile, credentialsPath, expiration, assumedRoleArn, assumedRoleId, sourceProfile, hint, warning?} (warning is present only when the target profile already existed and its three credential keys were overwritten in place; credentialsPath follows AWS_SHARED_CREDENTIALS_FILE when that is set)
aws_list_profiles -> {configPath, profiles: [{name, region?, ssoStartUrl?, ssoRegion?, ssoSession?, isSso}]}
aws_session_get / aws_session_set / aws_session_clear -> {profile, region, profileSource, regionSource} where *Source is "session" | "env" | "default". All three return the same shape (set/clear return the post-mutation state).
aws_resource_get -> {command, typeName, identifier, properties, propertiesRaw?}
aws_resource_list -> {command, typeName, resources: [{identifier, properties, propertiesRaw?}], nextToken, hasMore} (propertiesRaw rides along on an entry whose Properties string didn't parse, matching aws_resource_get)
aws_resource_create / _update / _delete / _status -> flat-promoted {command, requestToken, operationStatus, identifier, errorCode, statusMessage, retryAfter, progressEvent} plus an awaited: {attempts, elapsedMs} block when awaitCompletion: true was passed, or an awaitSkipped string when awaitCompletion: true was passed but no request token came back to poll on
aws_resource_diff -> {command, typeName, identifier, before, after, changes, changeCount}
aws_logs_tail -> {command, logGroupName, logGroupIdentifier, since, eventCount, totalEvents, truncated, events}. Each event is {timestamp, logStreamName, message}: timestamp is ISO 8601 UTC with milliseconds and message is the event text verbatim; any of the three is null on an event that arrived without that member, which is kept rather than dropped so eventCount matches what the service returned (real CloudWatch sends all three; an endpoint that diverges may not). events is oldest-first and capped at maxEvents (default 500), keeping the NEWEST; eventCount is how many are in events, and truncated is true exactly when the window held more. totalEvents is how many events the window held when the tool read all of it, and null when it stopped early: on AWS CLI 2.35.8+ the read goes newest-first and stops once it has more than maxEvents, so a truncated result there has totalEvents: null, while an older CLI -- or an endpoint that ignores FilterLogEvents' startFromHead -- reads the whole window and reports the exact count. logGroupName is always the bare group name; logGroupIdentifier is the ARN sent to FilterLogEvents (trailing :* removed) when the input was an ARN, otherwise null.
aws_logs_query -> {command, startCommand, profile, region, queryId, status, queryLanguage, logGroupNames, startTime, endTime, fields, rows, rowCount, statistics, truncated, polled: {attempts, elapsedMs}}. status is always "Complete" on the ok: true arm -- every other terminal status (Failed, Cancelled, Timeout, an unrecognized one, or a missing one) returns ok: false, as do a maxWaitMs timeout and a client cancellation, both of which carry the queryId in the error string because the error envelope has no data. command is the last get-query-results call, startCommand the start-query call (its --cli-input-json payload is redacted, so the query text does not echo back). rows are the API's [{field, value}] pairs flattened to plain objects with null for a non-string value; fields is the union of field names in first-seen order; logGroupNames are the RESOLVED bare names actually queried (an ARN input echoes its extracted name). queryLanguage and statistics are null when the response omits them. truncated is true when rowCount reached the effective limit. The query is never stopped AWS-side by this tool on any path.
aws_metrics_query -> {command, profile, region, startTime, endTime, periodSeconds, series: [{id, label?, timestamps, values, period?, statusCode?}], nextToken, hasMore, messages?: [{code?, value?}]} (messages is omitted when empty; per-series label / period / statusCode are present when CloudWatch returns them or the query specifies/inherits a period; nextToken is null and hasMore false unless CloudWatch truncated the response)
aws_iam_simulate -> {command, principalArn, summary: {allowed, denied, unknown, total}, results, marker, hasMore} (results has one entry per (action, resource) pair, read from IAM's per-resource ResourceSpecificResults. A call without resources gets one entry per action with resource: "*"; an action AWS does not break down per resource gets a single entry carrying AWS's own EvalResourceName (* or the action's ARN template). summary counts entries. organizationsDecision and permissionsBoundaryDecision fall back to AWS's action-level value when it gives no per-resource one, except that an allowed entry always reads "allowed". unknown counts entries whose decision was missing or unrecognized, so a malformed response can't be silently folded into denied. The CLI follows IAM's pagination itself, so a first call is complete -- hasMore: false, marker: null; the two carry a cursor only on a call that resumed from marker, and summary then describes only that page.)
aws_lambda_invoke -> {command, statusCode, functionError, executedVersion, payload, logTail}, plus payloadTruncated: true only when the response body was clipped (absent otherwise, so the field reads as an exception flag rather than a size report). logTail is the function's LogResult already base64-DECODED. A non-empty functionError is still ok: true: the invocation succeeded and the function's handler threw, with the thrown error in payload -- an invocation failure (bad function name, no permission, throttling, timeout) is the ok: false case. The invoke is never sent more than once; errorKind: "timeout" means no answer arrived in time, and its message says whether the invoke was sent (if it was, the function may have run and may still be running).
aws_script -> {result, logs, truncatedLogs, durationMs} where result is whatever the script returned (any JSON-serializable value, including undefined)
aws_docs_search -> {query, count, results: [{title, url, summary?, excerpt?, lexicalMatch}], queryTerms, termsMatchedNowhere?, bestLexicalOverlap, lowRelevance, relevanceNote?} (summary / excerpt are present only when the upstream search backend returns them. The relevance fields, shipped since 2.1.0, are computed locally by this server: literal word overlap between the query's terms and a result's title/summary/excerpt, NOT a backend score and NOT semantic ranking -- results are annotated, never re-ordered. Per result, lexicalMatch is {overlap, matchedTerms, unmatchedTerms}, or null for a query with no scorable terms -- the same case where bestLexicalOverlap is null and termsMatchedNowhere is omitted. lowRelevance is true when the best result matched at or under half the query terms, when any term appears in no result at all, or when the backend returned nothing. relevanceNote is the prose explanation -- of a lowRelevance verdict, or of the no-scorable-terms case -- and is absent when there is nothing to explain.)
aws_docs_read -> {url, cached, content, startIndex, endIndex, totalLength, hasMore, nextStartIndex}
-
Error envelope -- {ok: false, error: string, rawBody?: string, errorKind?: string, suggestion?: string}. The error string is human-readable; its wording is best-effort (see below), and errorKind is the stable machine-readable part -- see the enum below. suggestion carries the one-line remedy for a recognized AWS error code; it is also embedded at the end of error, so it is a convenience for programmatic callers rather than extra information. On the wire an error result is a single text block, and errorKind rides on its own first line: errorKind: <kind> followed by a newline, then Error: <message>, then a blank line and rawBody when one is present and the message does not already quote it. A failure with no classification omits that line entirely and starts at Error: exactly as before.
-
errorKind enum -- "sso_expired" | "expired_creds" | "no_creds" | "invalid_creds" | "bad_input" | "spawn_failure" | "timeout" | "output_too_large" | "malformed_json" | "nonzero_exit" | "unexpected" | "cancelled". It appears on two distinct surfaces, with different rules.
On the top-level error envelope, for every tool that wraps an aws CLI call -- aws_call, aws_paginate, aws_logs_tail, aws_logs_query, aws_metrics_query, aws_iam_simulate, aws_lambda_invoke, aws_assume_role, aws_whoami, aws_login_complete, the aws_resource_* family. There it is ABSENT, never defaulted, when the failure did not reach the CLI: a tool's own input validation, an aws_docs_* HTTP failure, a client-cancelled poll. Treat a missing errorKind as "unclassified", not as nonzero_exit.
On each entry of a fan-out tool's results array -- aws_multi_region and aws_multi_account. A per-entry errorKind is always present on a failed entry, including for failures that never reached the CLI: both tools classify an entry they rejected themselves (a malformed region name, an account ID that is not 12 digits) as bad_input, and an entry whose worker threw as unexpected. unexpected is fan-out-only -- it cannot appear on a top-level envelope, and so is cancelled, which marks an entry that was NEVER ATTEMPTED because the client cancelled the request before a worker claimed it. Nothing was sent to AWS for a cancelled entry; entries that had already run keep their real results, and the array still covers the full requested set so okCount/errorCount cannot mistake a cancelled sweep for a smaller successful one.
New variants may be added (additive); existing ones won't be renamed or repurposed. The three credential kinds are deliberately distinct, because the remedy differs: no_creds means none were found, invalid_creds means credentials resolved and AWS rejected them (typical after a key rotation), and expired_creds means a temporary session expired. expired_creds is origin-agnostic -- AWS emits the same ExpiredToken wrapper for an SSO-derived session, an aws_assume_role session, and a web-identity one -- so its message names both remedies rather than assuming SSO; sso_expired is reserved for errors that name botocore's SSO token provider specifically. malformed_json means stdout opened with { or [ and failed to parse, i.e. a truncated response rather than the scalar output a --query can legitimately produce.
Best-effort (may change in a minor or patch):
-
Error message wording. Strings like "SSO session expired for profile 'X'. Call aws_login_start..." may be retuned for clarity. Anchor on errorKind or the structured envelope, not on regex-matching error text.
-
suggestion wording -- the one-line remedy derived from a recognized AWS error code. Whether a suggestion is present tracks the error code, but the sentence itself may be retuned; branch on errorKind, not on this text. It is duplicated at the end of error, so a caller reading both must not print it twice.
-
rawBody content -- raw stderr/stdout from the underlying aws CLI for diagnostic purposes. Format follows whatever the CLI emits in your installed version.
-
command strings -- the human-readable command shown alongside results. Argv ordering and the exact redaction-stub format (<redacted len=N>) may shift. It is quoted for the shell of the host the server runs on: a POSIX shell, or PowerShell on Windows. It is not correct in cmd.exe, where &, | and a newline are live whatever the quoting, nor in Git Bash on Windows, where a value containing a single quote loses its quotes and arrives wrong (Buckets[?Name=='prod'].Name is the realistic case; it becomes invalid JMESPath rather than anything that runs).
Use commandArgv instead of parsing command. Every envelope that carries command also carries commandArgv: the same call as an array of exact, unquoted tokens -- entry 0 the binary as displayed, then one entry per argument -- redacted identically, because the string is rendered from the array. It is what the server actually spawns (no shell is involved at any point), so there is nothing to unpick and nothing that can be re-interpreted by a shell you did not expect. Re-quote it for your own shell if you need to run it; the string form is a convenience for reading, and the array is the contract.
-
Tool descriptions -- the prose surfaced to the model. Tightening these is non-breaking.
Deprecation policy: breaking a stable shape requires a major bump. A deprecation lands first in a minor (the old shape continues to work and the new shape becomes available alongside it), with a removal scheduled for the next major. Both the deprecation and the removal show up in CHANGELOG.md.
Development
npm test runs both unit tests and integration tests. The integration suites
spawn a local fake-aws subprocess that stubs the AWS CLI -- no AWS credentials
or network access required. Suites named *.realcli.test.ts check the fake
against the real thing: they drive the AWS CLI v2 on your PATH against an
in-process endpoint on 127.0.0.1, with throwaway keys and every other address
routed to a dead proxy, so nothing leaves the machine. The ones that need only a
few CLI starts run on every npm test and skip themselves when no CLI v2 is
installed; the ones that wait out real timeouts and retries also need
AWS_MCP_REAL_CLI_TESTS=1, which release.sh sets. The only tests that need
real AWS credentials are the live tests gated behind the AWS_MCP_LIVE_TESTS
environment variable, which are skipped in a standard npm test run.
License
MIT
[Follow @YawLabs on X]