@yawlabs/aws-mcp 1.3.3 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -305
- package/dist/index.js +236 -155
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,305 +1,305 @@
|
|
|
1
|
-
# @yawlabs/aws-mcp
|
|
2
|
-
|
|
3
|
-
A small AWS MCP for AI assistants: **one server, one config entry, SSO re-auth baked in, generic CRUD over hundreds of resource types, live docs lookup, server-side scripting for batched workflows.**
|
|
4
|
-
|
|
5
|
-
It's an **alternative to AWS's official MCP server**, not a complement -- both call any AWS API, so running both just gives the model two redundant tools. Pick one. The honest comparison:
|
|
6
|
-
|
|
7
|
-
- **[AWS MCP Server](https://aws.amazon.com/blogs/aws/the-aws-mcp-server-is-now-generally-available/)** -- AWS's hosted server (`uvx mcp-proxy-for-aws`). Strong on AWS-team-curated skills, a server-side Python sandbox (`run_script`), and days-fresh API coverage. Requires Python + `uv`, routes through a proxy that bridges IAM SigV4 to OAuth, and assumes your local credentials already work.
|
|
8
|
-
- **`@yawlabs/aws-mcp`** (this server) -- Node/npm-only, runs locally. Wins on SSO re-login when `aws sso login`'s browser handoff drops (Windows especially), ergonomic CCAPI CRUD with dry-run diffs, multi-region fan-out, pre-flight IAM permission checks, and a JS scripting sandbox. Live AWS docs search + read is built in too -- parity with the official server's `search_documentation` / `read_documentation`, no second server needed either way.
|
|
9
|
-
|
|
10
|
-
The one MCP that genuinely pairs with *either* choice is **[`awslabs/mcp`](https://github.com/awslabs/mcp)** -- AWS Labs' fleet of typed per-service servers (Lambda invoke, Bedrock retrieval, DynamoDB with type-marshalling). Those are per-service helpers, no overlap with a general AWS-API server.
|
|
11
|
-
|
|
12
|
-
Five things this server tries to handle well:
|
|
13
|
-
|
|
14
|
-
1. **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. There's also `aws_refresh_if_expiring_soon` for proactive top-ups before a long workflow. AWS's hosted server bridges IAM-to-OAuth via a local proxy; it doesn't help with the `aws sso login` browser-handoff failure.
|
|
15
|
-
2. **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. `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 (useful when a `describe-instances` result would otherwise blow past the 5 MB output cap).
|
|
16
|
-
3. **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 a few hundred more. Pass `awaitCompletion: true` and the server polls the async create/update/delete through to terminal state for you. CCAPI is control-plane only -- for data-plane ops (S3 reads, Lambda invokes, Bedrock inference, DynamoDB GetItem) drop down to `aws_call` or use a typed AWS Labs server.
|
|
17
|
-
4. **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.
|
|
18
|
-
5. **Batched workflows in one round-trip.** `aws_script` runs a short JS snippet inside a constrained `node:vm` sandbox with `aws.call`, `aws.paginate`, `aws.paginateAll`, `aws.resource.*`, and `aws.
|
|
19
|
-
|
|
20
|
-
[](https://yaw.sh/mcp/install?name=AWS&command=npx&args=-y%2C%40yawlabs%2Faws-mcp&env=AWS_PROFILE%2CAWS_REGION&description=Call%20any%20AWS%20API%20from%20one%20server%20-%20CCAPI%20CRUD%2C%20multi-region%2C%20SSO%20re-login&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Faws-mcp)
|
|
21
|
-
|
|
22
|
-
One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
|
|
23
|
-
|
|
24
|
-
## Optional companion: AWS Labs per-service servers
|
|
25
|
-
|
|
26
|
-
For deep work in a single service -- typed `lambda_invoke`, Bedrock KB retrieval, DynamoDB with type-marshalling -- add the relevant [`awslabs/mcp`](https://github.com/awslabs/mcp) server alongside this one. Those are per-service helpers with no tool-name overlap, so they pair cleanly:
|
|
27
|
-
|
|
28
|
-
```json
|
|
29
|
-
{
|
|
30
|
-
"mcpServers": {
|
|
31
|
-
"aws": {
|
|
32
|
-
"command": "npx",
|
|
33
|
-
"args": ["-y", "@yawlabs/aws-mcp@latest"]
|
|
34
|
-
},
|
|
35
|
-
"aws-lambda": {
|
|
36
|
-
"command": "uvx",
|
|
37
|
-
"args": ["awslabs.lambda-mcp-server@latest"]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## When to reach for this vs the other AWS MCPs
|
|
44
|
-
|
|
45
|
-
| Need | Best fit |
|
|
46
|
-
|------|----------|
|
|
47
|
-
| One config entry covering most of AWS | **`@yawlabs/aws-mcp`** |
|
|
48
|
-
| SSO re-login on Windows / broken browser handoff | **`@yawlabs/aws-mcp`** (`aws_login_start` device-code flow) |
|
|
49
|
-
| Generic CRUD across hundreds of resource types | **`@yawlabs/aws-mcp`** (`aws_resource_*`) |
|
|
50
|
-
| Dry-run an update before applying it | **`@yawlabs/aws-mcp`** (`aws_resource_diff`) |
|
|
51
|
-
| Multi-region fan-out in one call | **`@yawlabs/aws-mcp`** (`aws_multi_region`) |
|
|
52
|
-
| Batch N tool calls into one round-trip (JS) | **`@yawlabs/aws-mcp`** (`aws_script`) |
|
|
53
|
-
| Check IAM permissions before attempting an op | **`@yawlabs/aws-mcp`** (`aws_iam_simulate`) |
|
|
54
|
-
| Node/npm-only install (no Python) | **`@yawlabs/aws-mcp`** |
|
|
55
|
-
| Sandboxed Python script execution server-side | **AWS MCP Server** (`run_script`) |
|
|
56
|
-
| AWS-team-curated best-practice skills | **AWS MCP Server** (skills) |
|
|
57
|
-
| Days-fresh API coverage via hosted endpoint | **AWS MCP Server** (`call_aws`) |
|
|
58
|
-
| Typed per-service helpers (Lambda invoke, Bedrock KB, DynamoDB type-marshalling, ...) | **`awslabs/mcp`** (per-service servers) |
|
|
59
|
-
|
|
60
|
-
`@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.
|
|
61
|
-
|
|
62
|
-
## What this server borrows from AWS's official one
|
|
63
|
-
|
|
64
|
-
Credit where due -- two features here were shaped by the official AWS MCP Server:
|
|
65
|
-
|
|
66
|
-
- **`aws_script`** mirrors the official server's `run_script`: a sandboxed scripting tool that collapses "list X, fetch Y for each, return Z" pipelines into one round-trip. Theirs is Python, sandboxed server-side; this one is JS-native and runs locally.
|
|
67
|
-
- **`aws_docs_search` / `aws_docs_read`** were added to match the official server's `search_documentation` / `read_documentation`, so you don't need a separate docs MCP regardless of which server you pick.
|
|
68
|
-
|
|
69
|
-
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.
|
|
70
|
-
|
|
71
|
-
## Tools
|
|
72
|
-
|
|
73
|
-
| Tool | What it does |
|
|
74
|
-
|------|--------------|
|
|
75
|
-
| `aws_whoami` | Current identity (account, ARN) + SSO token expiry countdown. Call this first. |
|
|
76
|
-
| `aws_login_start` | Start `aws sso login --no-browser`, returns a verification URL + short code and a `sessionId`. |
|
|
77
|
-
| `aws_login_complete` | Block until the SSO subprocess finishes (you auth in your browser), returns the new identity. |
|
|
78
|
-
| `aws_refresh_if_expiring_soon` | Check the cached SSO token and auto-start a refresh when < `thresholdMinutes` remain (default 10). One round-trip for "am I about to expire? if so, re-login." |
|
|
79
|
-
| `aws_session_set` | Set the default profile and/or region for the rest of this MCP session. "Switch to prod," "use us-west-2." |
|
|
80
|
-
| `aws_session_get` | Show the current session defaults and where each value came from (`session`/`env`/`default`). |
|
|
81
|
-
| `aws_session_clear` | Remove session profile/region overrides so env vars / defaults take over again. No args clears both. |
|
|
82
|
-
| `aws_list_profiles` | List profiles configured in `~/.aws/config` -- names, regions, and SSO metadata. Use before switching profiles or when an SSO error names one you haven't seen. |
|
|
83
|
-
| `aws_assume_role` | Call STS AssumeRole with your current identity and stash the temp creds as a new profile (`mcp-<sessionName>`) in `~/.aws/credentials`. Use for cross-account access. The secret/session token stay on disk -- not returned to the model. Optional `timeoutMs` (default 120s) for slow SAML / `credential_process` cold starts. |
|
|
84
|
-
| `aws_call` | Run any AWS API operation. `service: 's3api', operation: 'list-buckets'`, optional `params` (PascalCase JSON), optional `query` (JMESPath). Returns parsed JSON. |
|
|
85
|
-
| `aws_paginate` | Fetch one page of a paginated list/describe operation. Supports `query` too. Returns `nextToken`/`hasMore`; call again with the token to continue. |
|
|
86
|
-
| `aws_logs_tail` | Fetch recent CloudWatch Logs events for a log group. Wraps `aws logs tail --format json` with `since`, `filterPattern`, and stream-name filters; returns events as a parsed array. |
|
|
87
|
-
| `aws_metrics_query` | Query CloudWatch metrics via GetMetricData (the modern multi-metric / expression-capable API). Pass `queries: [{id, namespace, metricName, dimensions?, statistic?, period?}]` or expression-based queries; `startTime`/`endTime` accept ISO 8601 or relative shorthand (`'15m'`, `'1h'`, `'1d'`). Period auto-picks from the time range. Returns `{series, periodSeconds, messages?}
|
|
88
|
-
| `aws_resource_get` | Read an AWS resource via Cloud Control API by `typeName` + `identifier` (e.g. `AWS::Lambda::Function` + function name). Returns parsed Properties. |
|
|
89
|
-
| `aws_resource_list` | List resources of a type via CCAPI, paginated. Returns `{identifier, properties}` per entry plus a `nextToken`/`hasMore`. |
|
|
90
|
-
| `aws_resource_create` | Create an AWS resource via CCAPI. Async — returns top-level `requestToken` + `operationStatus`. Pass `awaitCompletion: true` to have the server poll to terminal state in one call. |
|
|
91
|
-
| `aws_resource_update` | Update an AWS resource via CCAPI using RFC 6902 JSON Patch. Same async + `awaitCompletion` shape as create. |
|
|
92
|
-
| `aws_resource_delete` | Delete an AWS resource via CCAPI. Same async + `awaitCompletion` shape as create. Destructive — verify `identifier` first. |
|
|
93
|
-
| `aws_resource_status` | Poll an async CCAPI request by `requestToken`. Returns the current state with `operationStatus`, `identifier`, `errorCode`, `statusMessage` flat-promoted (PENDING / IN_PROGRESS / SUCCESS / FAILED / CANCEL_*). |
|
|
94
|
-
| `aws_resource_diff` | Dry-run a CCAPI update: fetches current state, simulates the JSON Patch in memory, returns `{before, after, changes[]}`. No mutation sent to AWS. Supports the add/remove/replace subset of RFC 6902; `add` auto-creates missing object parents to match CCAPI's actual update semantics (so patches like `/Environment/Variables/NEW_KEY` work even when `/Environment/Variables` doesn't exist yet). `changes[i].after` reflects what op `i` produced (not the final post-patch state), so sequential ops on the same path read correctly. Call before `aws_resource_update` when you want to verify the patch does what you expect. |
|
|
95
|
-
| `aws_multi_region` | Run the same AWS operation across N regions in parallel. Same shape as `aws_call` but takes `regions: string[]`. Returns `{region, ok, data?, error?}[]` with `okCount`/`errorCount`. Partial failure is expected (services aren't everywhere, perms may be region-scoped). |
|
|
96
|
-
| `aws_script` | Run a short JS snippet that orchestrates the other tools and returns a combined result. Sandbox exposes `aws.call`, `aws.paginate`, `aws.paginateAll`, `aws.resource.{get,list,create,update,delete,status}`, `aws.logsTail`, plus standard JS builtins (`JSON`, `Math`, `Date`, `Promise`, etc.) and `console`. No `require`/`import`/`process`/`fs`/`fetch`/timers. Best for "list X, fetch Y for each, return Z" pipelines that would otherwise be N round-trips. Use `return <value>` to surface a result. Not a security sandbox -- treat the same as any other tool the model can call. |
|
|
97
|
-
| `aws_iam_simulate` | Simulate IAM permissions for a principal: can principal X do actions Y on resources Z? Wraps `iam simulate-principal-policy`. Returns one entry per (action, resource) pair with `decision` (allowed / explicitDeny / implicitDeny), `matchedStatementIds` (which IAM statements decided), and `missingContextValues` (context keys the policy needed but you didn't provide). Use BEFORE a risky operation to avoid a 403 -- pairs with the post-failure Suggestion from aws_call. Requires `iam:SimulatePrincipalPolicy` on the caller. |
|
|
98
|
-
| `aws_docs_search` | Search live AWS documentation (the backend behind the docs.aws.amazon.com search box). Returns ranked `{title, url, summary, excerpt}`. Use to discover the right doc page for a service/API/concept the model may not know -- new services, recently changed APIs, exact parameter names. |
|
|
99
|
-
| `aws_docs_read` | Fetch an `https://docs.aws.amazon.com/...html` page and return it as markdown. Strips nav/cookie-banner/feedback chrome. Long pages paginate via `startIndex` + `maxLength`; the response carries `hasMore` and `nextStartIndex`. Usually fed a url from `aws_docs_search`. |
|
|
100
|
-
|
|
101
|
-
## Install
|
|
102
|
-
|
|
103
|
-
Add to your MCP client config (e.g. `.mcp.json`):
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"mcpServers": {
|
|
108
|
-
"aws": {
|
|
109
|
-
"command": "npx",
|
|
110
|
-
"args": ["-y", "@yawlabs/aws-mcp@latest"]
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
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.
|
|
117
|
-
|
|
118
|
-
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:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
npm install -g @yawlabs/aws-mcp
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"mcpServers": {
|
|
127
|
-
"aws": {
|
|
128
|
-
"command": "aws-mcp"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
You'll need to `npm install -g @yawlabs/aws-mcp@latest` manually when you want a newer version.
|
|
135
|
-
|
|
136
|
-
## Example session
|
|
137
|
-
|
|
138
|
-
You ask the assistant to check a staging bucket, but your SSO token just expired. What the assistant does (and what you see):
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
You: "How many objects are in the staging-artifacts bucket right now?"
|
|
142
|
-
|
|
143
|
-
Claude: (calls aws_whoami) -> SSO session expired for profile 'staging'.
|
|
144
|
-
(calls aws_login_start with profile='staging')
|
|
145
|
-
"Your SSO token expired. Open
|
|
146
|
-
https://device.sso.us-east-1.amazonaws.com/
|
|
147
|
-
and enter code: ABCD-EFGH
|
|
148
|
-
I'll wait."
|
|
149
|
-
|
|
150
|
-
You: *click, authenticate in your browser*
|
|
151
|
-
|
|
152
|
-
Claude: (calls aws_login_complete with the sessionId)
|
|
153
|
-
(calls aws_call with service='s3api', operation='list-objects-v2',
|
|
154
|
-
params={ Bucket: 'staging-artifacts' },
|
|
155
|
-
query='KeyCount')
|
|
156
|
-
"There are 4,182 objects in staging-artifacts."
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
The SSO flow took one click. No "the browser didn't open, let me run it in a terminal" context switch.
|
|
160
|
-
|
|
161
|
-
For a larger list where the response might exceed the 5 MB output cap, the assistant reaches for `aws_paginate`:
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
(calls aws_paginate with service='ec2', operation='describe-instances',
|
|
165
|
-
maxItems=50,
|
|
166
|
-
query='Reservations[].Instances[].{Id:InstanceId,State:State.Name}')
|
|
167
|
-
-> returns one page + a nextToken; Claude calls again until hasMore=false
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
`query` (JMESPath) trims the response server-side -- a typical `describe-instances` result shrinks from megabytes to kilobytes when you only need two fields.
|
|
171
|
-
|
|
172
|
-
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:
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
(calls aws_resource_create with
|
|
176
|
-
typeName='AWS::SSM::Parameter',
|
|
177
|
-
desiredState={Name: '/my/param', Type: 'String', Value: 'hello'},
|
|
178
|
-
awaitCompletion: true)
|
|
179
|
-
-> server polls get-resource-request-status until SUCCESS / FAILED / CANCEL_COMPLETE
|
|
180
|
-
and returns the terminal ProgressEvent in one call
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
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.
|
|
184
|
-
|
|
185
|
-
For "preview the patch before applying":
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
(calls aws_resource_diff with
|
|
189
|
-
typeName='AWS::Lambda::Function',
|
|
190
|
-
identifier='my-fn',
|
|
191
|
-
patchDocument=[{op: 'replace', path: '/MemorySize', value: 1024}])
|
|
192
|
-
-> returns { before: {MemorySize: 256, ...}, after: {MemorySize: 1024, ...},
|
|
193
|
-
changes: [{op: 'replace', path: '/MemorySize', before: 256, after: 1024}] }
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
No mutation is sent to AWS; the agent can verify the patch before invoking `aws_resource_update`.
|
|
197
|
-
|
|
198
|
-
For batched workflows, `aws_script` collapses N tool calls into one:
|
|
199
|
-
|
|
200
|
-
```
|
|
201
|
-
(calls aws_script with code=`
|
|
202
|
-
const listed = await aws.resource.list({ typeName: "AWS::Lambda::Function" });
|
|
203
|
-
const big = [];
|
|
204
|
-
for (const r of listed.resources) {
|
|
205
|
-
const cfg = await aws.resource.get({
|
|
206
|
-
typeName: "AWS::Lambda::Function", identifier: r.identifier });
|
|
207
|
-
if (cfg.properties.MemorySize > 1024) {
|
|
208
|
-
big.push({ name: cfg.properties.FunctionName, mem: cfg.properties.MemorySize });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return big;
|
|
212
|
-
`)
|
|
213
|
-
-> one round-trip; the agent gets the filtered list without N intermediate tool calls
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
For multi-region reads:
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
(calls aws_multi_region with
|
|
220
|
-
service='ec2', operation='describe-instances',
|
|
221
|
-
regions=['us-east-1','us-west-2','eu-west-1'],
|
|
222
|
-
query='Reservations[].Instances[].InstanceId')
|
|
223
|
-
-> {okCount: 3, errorCount: 0, results: [{region, ok, data}, ...]}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Requirements
|
|
227
|
-
|
|
228
|
-
- Node.js 22+
|
|
229
|
-
- AWS CLI v2 installed and on `PATH` (for `aws sso login --no-browser`)
|
|
230
|
-
- An AWS profile configured for SSO / IAM Identity Center in `~/.aws/config`
|
|
231
|
-
|
|
232
|
-
## Environment
|
|
233
|
-
|
|
234
|
-
| Variable | Default | Purpose |
|
|
235
|
-
|----------|---------|---------|
|
|
236
|
-
| `AWS_PROFILE` | `default` | Profile used when a tool call omits `profile`. |
|
|
237
|
-
| `AWS_REGION` / `AWS_DEFAULT_REGION` | `us-east-1` | Region used when a tool call omits `region`. `AWS_REGION` wins if both are set. |
|
|
238
|
-
|
|
239
|
-
If you authenticate via SAML (Okta / Azure AD / ADFS) or a custom `credential_process`, set `AWS_PROFILE` to that profile. The server passes `--profile` through to the AWS CLI, so the CLI's standard credential chain -- `credential_process`, SSO sessions, role chaining, static keys, IMDS -- resolves as usual.
|
|
240
|
-
|
|
241
|
-
If neither `AWS_PROFILE` is set nor `aws_session_set` has been called and there's no `[default]` section in `~/.aws/config`, tools will fail with `ProfileNotFound`. Set `AWS_PROFILE` in your MCP config to your usual working profile.
|
|
242
|
-
|
|
243
|
-
## How the SSO login flow works
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
1. Claude calls aws_login_start({ profile: "prod" })
|
|
247
|
-
2. Server spawns: aws sso login --no-browser --profile prod
|
|
248
|
-
3. Server parses the URL + code from stdout, returns them to Claude
|
|
249
|
-
4. Claude surfaces: "Open https://device.sso.us-east-1.amazonaws.com/ and enter ABCD-EFGH"
|
|
250
|
-
5. You click — browser opens in your own user session — auth in ~10 seconds
|
|
251
|
-
6. Claude calls aws_login_complete({ sessionId })
|
|
252
|
-
7. Tool returns your new identity. Back to work.
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
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.
|
|
256
|
-
|
|
257
|
-
## Why this server must run locally (not on mcp.hosting)
|
|
258
|
-
|
|
259
|
-
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.
|
|
260
|
-
|
|
261
|
-
## Stability
|
|
262
|
-
|
|
263
|
-
From 1.0 onward this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The 0.x line is the pre-stability tightening phase -- breaking changes are documented in [`CHANGELOG.md`](./CHANGELOG.md) but are not necessarily gated on a major bump.
|
|
264
|
-
|
|
265
|
-
**Stable in 1.x (anything below is a breaking change requiring a major bump):**
|
|
266
|
-
|
|
267
|
-
- **Tool names** -- the 25 tool names listed in the Tools table above will not be renamed or removed.
|
|
268
|
-
- **Tool annotations** -- `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. These signal to MCP hosts how to gate calls; flipping them silently would break host UIs.
|
|
269
|
-
- **Required input fields** -- the required fields per tool will not change shape or be removed. New *optional* fields may be added.
|
|
270
|
-
- **Success envelope shape per tool** -- the `data` object on `{ok: true, data}` responses, specifically:
|
|
271
|
-
- `aws_call` -> `{command, result}`
|
|
272
|
-
- `aws_paginate` -> `{command, result, nextToken, hasMore}`
|
|
273
|
-
- `aws_multi_region` -> `{service, operation, regionCount, okCount, errorCount, results: [{region, ok, data?, command?, error?, errorKind?}]}`
|
|
274
|
-
- `aws_whoami` -> `{account, userId, arn, profile, region, ssoToken: {expiresAt, minutesLeft, startUrl?} | null}` (`startUrl` is omitted when the cached token didn't record one)
|
|
275
|
-
- `aws_login_start` -> `{sessionId, profile, verificationUrl, userCode, instructions, reused?}` (`reused: true` when re-surfacing an in-flight login for the same profile)
|
|
276
|
-
- `aws_login_complete` -> `{loggedIn, account, userId, arn, profile, region, ssoToken}` (same `ssoToken` shape as `aws_whoami`, including the optional `startUrl`)
|
|
277
|
-
- `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`.
|
|
278
|
-
- `aws_assume_role` -> `{profile, credentialsPath, expiration, assumedRoleArn, assumedRoleId, sourceProfile, hint}`
|
|
279
|
-
- `aws_list_profiles` -> `{configPath, profiles: [{name, region?, ssoStartUrl?, ssoRegion?, ssoSession?, isSso}]}`
|
|
280
|
-
- `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).
|
|
281
|
-
- `aws_resource_get` -> `{command, typeName, identifier, properties, propertiesRaw?}`
|
|
282
|
-
- `aws_resource_list` -> `{command, typeName, resources: [{identifier, properties}], nextToken, hasMore}`
|
|
283
|
-
- `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
|
|
284
|
-
- `aws_resource_diff` -> `{command, typeName, identifier, before, after, changes, changeCount}`
|
|
285
|
-
- `aws_logs_tail` -> `{command, logGroupName, since, eventCount, events}`
|
|
286
|
-
- `aws_metrics_query` -> `{command, startTime, endTime, periodSeconds, series: [{id, label?, timestamps, values, statusCode?}], messages?: [{code?, value?}]}` (`messages` is omitted when empty; per-series `label` / `statusCode` are present when CloudWatch returns them)
|
|
287
|
-
- `aws_iam_simulate` -> `{command, principalArn, summary: {allowed, denied, total}, results, evaluationResults}`
|
|
288
|
-
- `aws_script` -> `{result, logs, truncatedLogs, durationMs}` where `result` is whatever the script `return`ed (any JSON-serializable value, including `undefined`)
|
|
289
|
-
- `aws_docs_search` -> `{query, count, results: [{title, url, summary?, excerpt?}]}` (`summary` / `excerpt` are present only when the upstream search backend returns them)
|
|
290
|
-
- `aws_docs_read` -> `{url, cached, content, startIndex, endIndex, totalLength, hasMore, nextStartIndex}`
|
|
291
|
-
- **Error envelope** -- `{ok: false, error: string, rawBody?: string}`. The `error` string is human-readable; its *wording* is best-effort (see below).
|
|
292
|
-
- **`errorKind` enum on `aws_multi_region`** -- `"sso_expired" | "no_creds" | "bad_input" | "spawn_failure" | "timeout" | "output_too_large" | "nonzero_exit"`. New variants may be added (additive); existing ones won't be renamed or repurposed.
|
|
293
|
-
|
|
294
|
-
**Best-effort (may change in a minor or patch):**
|
|
295
|
-
|
|
296
|
-
- **Error message wording.** Strings like "SSO session expired for profile 'X'. Call aws_login_start..." may be retuned for clarity. Anchor on `errorKind` (for `aws_multi_region`) or the structured envelope, not on regex-matching `error` text.
|
|
297
|
-
- **`rawBody`** content -- raw stderr/stdout from the underlying `aws` CLI for diagnostic purposes. Format follows whatever the CLI emits in your installed version.
|
|
298
|
-
- **`command`** strings -- the human-readable command shown alongside results. Argv ordering and the exact redaction-stub format (`<redacted len=N>`) may shift.
|
|
299
|
-
- **Tool *descriptions*** -- the prose surfaced to the model. Tightening these is non-breaking.
|
|
300
|
-
|
|
301
|
-
**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`.
|
|
302
|
-
|
|
303
|
-
## License
|
|
304
|
-
|
|
305
|
-
MIT
|
|
1
|
+
# @yawlabs/aws-mcp
|
|
2
|
+
|
|
3
|
+
A small AWS MCP for AI assistants: **one server, one config entry, SSO re-auth baked in, generic CRUD over hundreds of resource types, live docs lookup, server-side scripting for batched workflows.**
|
|
4
|
+
|
|
5
|
+
It's an **alternative to AWS's official MCP server**, not a complement -- both call any AWS API, so running both just gives the model two redundant tools. Pick one. The honest comparison:
|
|
6
|
+
|
|
7
|
+
- **[AWS MCP Server](https://aws.amazon.com/blogs/aws/the-aws-mcp-server-is-now-generally-available/)** -- AWS's hosted server (`uvx mcp-proxy-for-aws`). Strong on AWS-team-curated skills, a server-side Python sandbox (`run_script`), and days-fresh API coverage. Requires Python + `uv`, routes through a proxy that bridges IAM SigV4 to OAuth, and assumes your local credentials already work.
|
|
8
|
+
- **`@yawlabs/aws-mcp`** (this server) -- Node/npm-only, runs locally. Wins on SSO re-login when `aws sso login`'s browser handoff drops (Windows especially), ergonomic CCAPI CRUD with dry-run diffs, multi-region fan-out, pre-flight IAM permission checks, and a JS scripting sandbox. Live AWS docs search + read is built in too -- parity with the official server's `search_documentation` / `read_documentation`, no second server needed either way.
|
|
9
|
+
|
|
10
|
+
The one MCP that genuinely pairs with *either* choice is **[`awslabs/mcp`](https://github.com/awslabs/mcp)** -- AWS Labs' fleet of typed per-service servers (Lambda invoke, Bedrock retrieval, DynamoDB with type-marshalling). Those are per-service helpers, no overlap with a general AWS-API server.
|
|
11
|
+
|
|
12
|
+
Five things this server tries to handle well:
|
|
13
|
+
|
|
14
|
+
1. **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. There's also `aws_refresh_if_expiring_soon` for proactive top-ups before a long workflow. AWS's hosted server bridges IAM-to-OAuth via a local proxy; it doesn't help with the `aws sso login` browser-handoff failure.
|
|
15
|
+
2. **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. `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 (useful when a `describe-instances` result would otherwise blow past the 5 MB output cap).
|
|
16
|
+
3. **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 a few hundred more. Pass `awaitCompletion: true` and the server polls the async create/update/delete through to terminal state for you. CCAPI is control-plane only -- for data-plane ops (S3 reads, Lambda invokes, Bedrock inference, DynamoDB GetItem) drop down to `aws_call` or use a typed AWS Labs server.
|
|
17
|
+
4. **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.
|
|
18
|
+
5. **Batched workflows in one round-trip.** `aws_script` runs a short JS snippet inside a constrained `node:vm` sandbox 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 shape as AWS's `run_script` (Python, sandboxed server-side) -- yours is JS-native and runs locally.
|
|
19
|
+
|
|
20
|
+
[](https://yaw.sh/mcp/install?name=AWS&command=npx&args=-y%2C%40yawlabs%2Faws-mcp&env=AWS_PROFILE%2CAWS_REGION&description=Call%20any%20AWS%20API%20from%20one%20server%20-%20CCAPI%20CRUD%2C%20multi-region%2C%20SSO%20re-login&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Faws-mcp)
|
|
21
|
+
|
|
22
|
+
One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
|
|
23
|
+
|
|
24
|
+
## Optional companion: AWS Labs per-service servers
|
|
25
|
+
|
|
26
|
+
For deep work in a single service -- typed `lambda_invoke`, Bedrock KB retrieval, DynamoDB with type-marshalling -- add the relevant [`awslabs/mcp`](https://github.com/awslabs/mcp) server alongside this one. Those are per-service helpers with no tool-name overlap, so they pair cleanly:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"aws": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "@yawlabs/aws-mcp@latest"]
|
|
34
|
+
},
|
|
35
|
+
"aws-lambda": {
|
|
36
|
+
"command": "uvx",
|
|
37
|
+
"args": ["awslabs.lambda-mcp-server@latest"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## When to reach for this vs the other AWS MCPs
|
|
44
|
+
|
|
45
|
+
| Need | Best fit |
|
|
46
|
+
|------|----------|
|
|
47
|
+
| One config entry covering most of AWS | **`@yawlabs/aws-mcp`** |
|
|
48
|
+
| SSO re-login on Windows / broken browser handoff | **`@yawlabs/aws-mcp`** (`aws_login_start` device-code flow) |
|
|
49
|
+
| Generic CRUD across hundreds of resource types | **`@yawlabs/aws-mcp`** (`aws_resource_*`) |
|
|
50
|
+
| Dry-run an update before applying it | **`@yawlabs/aws-mcp`** (`aws_resource_diff`) |
|
|
51
|
+
| Multi-region fan-out in one call | **`@yawlabs/aws-mcp`** (`aws_multi_region`) |
|
|
52
|
+
| Batch N tool calls into one round-trip (JS) | **`@yawlabs/aws-mcp`** (`aws_script`) |
|
|
53
|
+
| Check IAM permissions before attempting an op | **`@yawlabs/aws-mcp`** (`aws_iam_simulate`) |
|
|
54
|
+
| Node/npm-only install (no Python) | **`@yawlabs/aws-mcp`** |
|
|
55
|
+
| Sandboxed Python script execution server-side | **AWS MCP Server** (`run_script`) |
|
|
56
|
+
| AWS-team-curated best-practice skills | **AWS MCP Server** (skills) |
|
|
57
|
+
| Days-fresh API coverage via hosted endpoint | **AWS MCP Server** (`call_aws`) |
|
|
58
|
+
| Typed per-service helpers (Lambda invoke, Bedrock KB, DynamoDB type-marshalling, ...) | **`awslabs/mcp`** (per-service servers) |
|
|
59
|
+
|
|
60
|
+
`@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.
|
|
61
|
+
|
|
62
|
+
## What this server borrows from AWS's official one
|
|
63
|
+
|
|
64
|
+
Credit where due -- two features here were shaped by the official AWS MCP Server:
|
|
65
|
+
|
|
66
|
+
- **`aws_script`** mirrors the official server's `run_script`: a sandboxed scripting tool that collapses "list X, fetch Y for each, return Z" pipelines into one round-trip. Theirs is Python, sandboxed server-side; this one is JS-native and runs locally.
|
|
67
|
+
- **`aws_docs_search` / `aws_docs_read`** were added to match the official server's `search_documentation` / `read_documentation`, so you don't need a separate docs MCP regardless of which server you pick.
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
## Tools
|
|
72
|
+
|
|
73
|
+
| Tool | What it does |
|
|
74
|
+
|------|--------------|
|
|
75
|
+
| `aws_whoami` | Current identity (account, ARN) + SSO token expiry countdown. Call this first. |
|
|
76
|
+
| `aws_login_start` | Start `aws sso login --no-browser`, returns a verification URL + short code and a `sessionId`. |
|
|
77
|
+
| `aws_login_complete` | Block until the SSO subprocess finishes (you auth in your browser), returns the new identity. |
|
|
78
|
+
| `aws_refresh_if_expiring_soon` | Check the cached SSO token and auto-start a refresh when < `thresholdMinutes` remain (default 10). One round-trip for "am I about to expire? if so, re-login." |
|
|
79
|
+
| `aws_session_set` | Set the default profile and/or region for the rest of this MCP session. "Switch to prod," "use us-west-2." |
|
|
80
|
+
| `aws_session_get` | Show the current session defaults and where each value came from (`session`/`env`/`default`). |
|
|
81
|
+
| `aws_session_clear` | Remove session profile/region overrides so env vars / defaults take over again. No args clears both. |
|
|
82
|
+
| `aws_list_profiles` | List profiles configured in `~/.aws/config` -- names, regions, and SSO metadata. Use before switching profiles or when an SSO error names one you haven't seen. |
|
|
83
|
+
| `aws_assume_role` | Call STS AssumeRole with your current identity and stash the temp creds as a new profile (`mcp-<sessionName>`) in `~/.aws/credentials`. Use for cross-account access. The secret/session token stay on disk -- not returned to the model. Optional `timeoutMs` (default 120s) for slow SAML / `credential_process` cold starts. |
|
|
84
|
+
| `aws_call` | Run any AWS API operation. `service: 's3api', operation: 'list-buckets'`, optional `params` (PascalCase JSON), optional `query` (JMESPath). Returns parsed JSON. |
|
|
85
|
+
| `aws_paginate` | Fetch one page of a paginated list/describe operation. Supports `query` too. Returns `nextToken`/`hasMore`; call again with the token to continue. |
|
|
86
|
+
| `aws_logs_tail` | Fetch recent CloudWatch Logs events for a log group. Wraps `aws logs tail --format json` with `since`, `filterPattern`, and stream-name filters; returns events as a parsed array. |
|
|
87
|
+
| `aws_metrics_query` | Query CloudWatch metrics via GetMetricData (the modern multi-metric / expression-capable API). Pass `queries: [{id, namespace, metricName, dimensions?, statistic?, period?}]` or expression-based queries; `startTime`/`endTime` accept ISO 8601 or relative shorthand (`'15m'`, `'1h'`, `'1d'`). Period auto-picks from the time range. Returns `{series: [{id, label?, timestamps, values, period?, statusCode?}], periodSeconds, profile, region, nextToken, hasMore, messages?}` (full envelope under Stability). |
|
|
88
|
+
| `aws_resource_get` | Read an AWS resource via Cloud Control API by `typeName` + `identifier` (e.g. `AWS::Lambda::Function` + function name). Returns parsed Properties. |
|
|
89
|
+
| `aws_resource_list` | List resources of a type via CCAPI, paginated. Returns `{identifier, properties}` per entry plus a `nextToken`/`hasMore`. |
|
|
90
|
+
| `aws_resource_create` | Create an AWS resource via CCAPI. Async — returns top-level `requestToken` + `operationStatus`. Pass `awaitCompletion: true` to have the server poll to terminal state in one call. |
|
|
91
|
+
| `aws_resource_update` | Update an AWS resource via CCAPI using RFC 6902 JSON Patch. Same async + `awaitCompletion` shape as create. |
|
|
92
|
+
| `aws_resource_delete` | Delete an AWS resource via CCAPI. Same async + `awaitCompletion` shape as create. Destructive — verify `identifier` first. |
|
|
93
|
+
| `aws_resource_status` | Poll an async CCAPI request by `requestToken`. Returns the current state with `operationStatus`, `identifier`, `errorCode`, `statusMessage` flat-promoted (PENDING / IN_PROGRESS / SUCCESS / FAILED / CANCEL_*). |
|
|
94
|
+
| `aws_resource_diff` | Dry-run a CCAPI update: fetches current state, simulates the JSON Patch in memory, returns `{before, after, changes[]}`. No mutation sent to AWS. Supports the add/remove/replace subset of RFC 6902; `add` auto-creates missing object parents to match CCAPI's actual update semantics (so patches like `/Environment/Variables/NEW_KEY` work even when `/Environment/Variables` doesn't exist yet). `changes[i].after` reflects what op `i` produced (not the final post-patch state), so sequential ops on the same path read correctly. Call before `aws_resource_update` when you want to verify the patch does what you expect. |
|
|
95
|
+
| `aws_multi_region` | Run the same AWS operation across N regions in parallel. Same shape as `aws_call` but takes `regions: string[]`. Returns `{region, ok, data?, error?}[]` with `okCount`/`errorCount`. Partial failure is expected (services aren't everywhere, perms may be region-scoped). |
|
|
96
|
+
| `aws_script` | Run a short JS snippet that orchestrates the other tools and returns a combined result. Sandbox exposes `aws.call`, `aws.paginate`, `aws.paginateAll`, `aws.resource.{get,list,create,update,delete,status}`, `aws.logsTail`, `aws.metricsQuery`, `aws.iamSimulate`, `aws.multiRegion`, `aws.assumeRole`, `aws.docs.{search,read}`, plus standard JS builtins (`JSON`, `Math`, `Date`, `Promise`, etc.) and `console`. No `require`/`import`/`process`/`fs`/`fetch`/timers. Best for "list X, fetch Y for each, return Z" pipelines that would otherwise be N round-trips. Use `return <value>` to surface a result. Not a security sandbox -- treat the same as any other tool the model can call. |
|
|
97
|
+
| `aws_iam_simulate` | Simulate IAM permissions for a principal: can principal X do actions Y on resources Z? Wraps `iam simulate-principal-policy`. Returns one entry per (action, resource) pair with `decision` (allowed / explicitDeny / implicitDeny), `matchedStatementIds` (which IAM statements decided), and `missingContextValues` (context keys the policy needed but you didn't provide). Use BEFORE a risky operation to avoid a 403 -- pairs with the post-failure Suggestion from aws_call. Requires `iam:SimulatePrincipalPolicy` on the caller. |
|
|
98
|
+
| `aws_docs_search` | Search live AWS documentation (the backend behind the docs.aws.amazon.com search box). Returns ranked `{title, url, summary, excerpt}`. Use to discover the right doc page for a service/API/concept the model may not know -- new services, recently changed APIs, exact parameter names. |
|
|
99
|
+
| `aws_docs_read` | Fetch an `https://docs.aws.amazon.com/...html` page and return it as markdown. Strips nav/cookie-banner/feedback chrome. Long pages paginate via `startIndex` + `maxLength`; the response carries `hasMore` and `nextStartIndex`. Usually fed a url from `aws_docs_search`. |
|
|
100
|
+
|
|
101
|
+
## Install
|
|
102
|
+
|
|
103
|
+
Add to your MCP client config (e.g. `.mcp.json`):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"aws": {
|
|
109
|
+
"command": "npx",
|
|
110
|
+
"args": ["-y", "@yawlabs/aws-mcp@latest"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
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:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install -g @yawlabs/aws-mcp
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"aws": {
|
|
128
|
+
"command": "aws-mcp"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You'll need to `npm install -g @yawlabs/aws-mcp@latest` manually when you want a newer version.
|
|
135
|
+
|
|
136
|
+
## Example session
|
|
137
|
+
|
|
138
|
+
You ask the assistant to check a staging bucket, but your SSO token just expired. What the assistant does (and what you see):
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
You: "How many objects are in the staging-artifacts bucket right now?"
|
|
142
|
+
|
|
143
|
+
Claude: (calls aws_whoami) -> SSO session expired for profile 'staging'.
|
|
144
|
+
(calls aws_login_start with profile='staging')
|
|
145
|
+
"Your SSO token expired. Open
|
|
146
|
+
https://device.sso.us-east-1.amazonaws.com/
|
|
147
|
+
and enter code: ABCD-EFGH
|
|
148
|
+
I'll wait."
|
|
149
|
+
|
|
150
|
+
You: *click, authenticate in your browser*
|
|
151
|
+
|
|
152
|
+
Claude: (calls aws_login_complete with the sessionId)
|
|
153
|
+
(calls aws_call with service='s3api', operation='list-objects-v2',
|
|
154
|
+
params={ Bucket: 'staging-artifacts' },
|
|
155
|
+
query='KeyCount')
|
|
156
|
+
"There are 4,182 objects in staging-artifacts."
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The SSO flow took one click. No "the browser didn't open, let me run it in a terminal" context switch.
|
|
160
|
+
|
|
161
|
+
For a larger list where the response might exceed the 5 MB output cap, the assistant reaches for `aws_paginate`:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
(calls aws_paginate with service='ec2', operation='describe-instances',
|
|
165
|
+
maxItems=50,
|
|
166
|
+
query='Reservations[].Instances[].{Id:InstanceId,State:State.Name}')
|
|
167
|
+
-> returns one page + a nextToken; Claude calls again until hasMore=false
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`query` (JMESPath) trims the response server-side -- a typical `describe-instances` result shrinks from megabytes to kilobytes when you only need two fields.
|
|
171
|
+
|
|
172
|
+
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:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
(calls aws_resource_create with
|
|
176
|
+
typeName='AWS::SSM::Parameter',
|
|
177
|
+
desiredState={Name: '/my/param', Type: 'String', Value: 'hello'},
|
|
178
|
+
awaitCompletion: true)
|
|
179
|
+
-> server polls get-resource-request-status until SUCCESS / FAILED / CANCEL_COMPLETE
|
|
180
|
+
and returns the terminal ProgressEvent in one call
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
For "preview the patch before applying":
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
(calls aws_resource_diff with
|
|
189
|
+
typeName='AWS::Lambda::Function',
|
|
190
|
+
identifier='my-fn',
|
|
191
|
+
patchDocument=[{op: 'replace', path: '/MemorySize', value: 1024}])
|
|
192
|
+
-> returns { before: {MemorySize: 256, ...}, after: {MemorySize: 1024, ...},
|
|
193
|
+
changes: [{op: 'replace', path: '/MemorySize', before: 256, after: 1024}] }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
No mutation is sent to AWS; the agent can verify the patch before invoking `aws_resource_update`.
|
|
197
|
+
|
|
198
|
+
For batched workflows, `aws_script` collapses N tool calls into one:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
(calls aws_script with code=`
|
|
202
|
+
const listed = await aws.resource.list({ typeName: "AWS::Lambda::Function" });
|
|
203
|
+
const big = [];
|
|
204
|
+
for (const r of listed.resources) {
|
|
205
|
+
const cfg = await aws.resource.get({
|
|
206
|
+
typeName: "AWS::Lambda::Function", identifier: r.identifier });
|
|
207
|
+
if (cfg.properties.MemorySize > 1024) {
|
|
208
|
+
big.push({ name: cfg.properties.FunctionName, mem: cfg.properties.MemorySize });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return big;
|
|
212
|
+
`)
|
|
213
|
+
-> one round-trip; the agent gets the filtered list without N intermediate tool calls
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
For multi-region reads:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
(calls aws_multi_region with
|
|
220
|
+
service='ec2', operation='describe-instances',
|
|
221
|
+
regions=['us-east-1','us-west-2','eu-west-1'],
|
|
222
|
+
query='Reservations[].Instances[].InstanceId')
|
|
223
|
+
-> {okCount: 3, errorCount: 0, results: [{region, ok, data}, ...]}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Requirements
|
|
227
|
+
|
|
228
|
+
- Node.js 22+
|
|
229
|
+
- AWS CLI v2 installed and on `PATH` (for `aws sso login --no-browser`)
|
|
230
|
+
- An AWS profile configured for SSO / IAM Identity Center in `~/.aws/config`
|
|
231
|
+
|
|
232
|
+
## Environment
|
|
233
|
+
|
|
234
|
+
| Variable | Default | Purpose |
|
|
235
|
+
|----------|---------|---------|
|
|
236
|
+
| `AWS_PROFILE` | `default` | Profile used when a tool call omits `profile`. |
|
|
237
|
+
| `AWS_REGION` / `AWS_DEFAULT_REGION` | `us-east-1` | Region used when a tool call omits `region`. `AWS_REGION` wins if both are set. |
|
|
238
|
+
|
|
239
|
+
If you authenticate via SAML (Okta / Azure AD / ADFS) or a custom `credential_process`, set `AWS_PROFILE` to that profile. The server passes `--profile` through to the AWS CLI, so the CLI's standard credential chain -- `credential_process`, SSO sessions, role chaining, static keys, IMDS -- resolves as usual.
|
|
240
|
+
|
|
241
|
+
If neither `AWS_PROFILE` is set nor `aws_session_set` has been called and there's no `[default]` section in `~/.aws/config`, tools will fail with `ProfileNotFound`. Set `AWS_PROFILE` in your MCP config to your usual working profile.
|
|
242
|
+
|
|
243
|
+
## How the SSO login flow works
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
1. Claude calls aws_login_start({ profile: "prod" })
|
|
247
|
+
2. Server spawns: aws sso login --no-browser --profile prod
|
|
248
|
+
3. Server parses the URL + code from stdout, returns them to Claude
|
|
249
|
+
4. Claude surfaces: "Open https://device.sso.us-east-1.amazonaws.com/ and enter ABCD-EFGH"
|
|
250
|
+
5. You click — browser opens in your own user session — auth in ~10 seconds
|
|
251
|
+
6. Claude calls aws_login_complete({ sessionId })
|
|
252
|
+
7. Tool returns your new identity. Back to work.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
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.
|
|
256
|
+
|
|
257
|
+
## Why this server must run locally (not on mcp.hosting)
|
|
258
|
+
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
## Stability
|
|
262
|
+
|
|
263
|
+
From 1.0 onward this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The 0.x line is the pre-stability tightening phase -- breaking changes are documented in [`CHANGELOG.md`](./CHANGELOG.md) but are not necessarily gated on a major bump.
|
|
264
|
+
|
|
265
|
+
**Stable in 1.x (anything below is a breaking change requiring a major bump):**
|
|
266
|
+
|
|
267
|
+
- **Tool names** -- the 25 tool names listed in the Tools table above will not be renamed or removed.
|
|
268
|
+
- **Tool annotations** -- `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. These signal to MCP hosts how to gate calls; flipping them silently would break host UIs.
|
|
269
|
+
- **Required input fields** -- the required fields per tool will not change shape or be removed. New *optional* fields may be added.
|
|
270
|
+
- **Success envelope shape per tool** -- the `data` object on `{ok: true, data}` responses, specifically:
|
|
271
|
+
- `aws_call` -> `{command, result}`
|
|
272
|
+
- `aws_paginate` -> `{command, result, nextToken, hasMore}`
|
|
273
|
+
- `aws_multi_region` -> `{service, operation, regionCount, okCount, errorCount, results: [{region, ok, data?, command?, error?, errorKind?}]}`
|
|
274
|
+
- `aws_whoami` -> `{account, userId, arn, profile, region, ssoToken: {expiresAt, minutesLeft, startUrl?} | null}` (`startUrl` is omitted when the cached token didn't record one)
|
|
275
|
+
- `aws_login_start` -> `{sessionId, profile, verificationUrl, userCode, instructions, reused?}` (`reused: true` when re-surfacing an in-flight login for the same profile)
|
|
276
|
+
- `aws_login_complete` -> `{loggedIn, account, userId, arn, profile, region, ssoToken}` (same `ssoToken` shape as `aws_whoami`, including the optional `startUrl`)
|
|
277
|
+
- `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`.
|
|
278
|
+
- `aws_assume_role` -> `{profile, credentialsPath, expiration, assumedRoleArn, assumedRoleId, sourceProfile, hint}`
|
|
279
|
+
- `aws_list_profiles` -> `{configPath, profiles: [{name, region?, ssoStartUrl?, ssoRegion?, ssoSession?, isSso}]}`
|
|
280
|
+
- `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).
|
|
281
|
+
- `aws_resource_get` -> `{command, typeName, identifier, properties, propertiesRaw?}`
|
|
282
|
+
- `aws_resource_list` -> `{command, typeName, resources: [{identifier, properties}], nextToken, hasMore}`
|
|
283
|
+
- `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
|
|
284
|
+
- `aws_resource_diff` -> `{command, typeName, identifier, before, after, changes, changeCount}`
|
|
285
|
+
- `aws_logs_tail` -> `{command, logGroupName, since, eventCount, events}`
|
|
286
|
+
- `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)
|
|
287
|
+
- `aws_iam_simulate` -> `{command, principalArn, summary: {allowed, denied, total}, results, evaluationResults}`
|
|
288
|
+
- `aws_script` -> `{result, logs, truncatedLogs, durationMs}` where `result` is whatever the script `return`ed (any JSON-serializable value, including `undefined`)
|
|
289
|
+
- `aws_docs_search` -> `{query, count, results: [{title, url, summary?, excerpt?}]}` (`summary` / `excerpt` are present only when the upstream search backend returns them)
|
|
290
|
+
- `aws_docs_read` -> `{url, cached, content, startIndex, endIndex, totalLength, hasMore, nextStartIndex}`
|
|
291
|
+
- **Error envelope** -- `{ok: false, error: string, rawBody?: string}`. The `error` string is human-readable; its *wording* is best-effort (see below).
|
|
292
|
+
- **`errorKind` enum on `aws_multi_region`** -- `"sso_expired" | "no_creds" | "bad_input" | "spawn_failure" | "timeout" | "output_too_large" | "nonzero_exit"`. New variants may be added (additive); existing ones won't be renamed or repurposed.
|
|
293
|
+
|
|
294
|
+
**Best-effort (may change in a minor or patch):**
|
|
295
|
+
|
|
296
|
+
- **Error message wording.** Strings like "SSO session expired for profile 'X'. Call aws_login_start..." may be retuned for clarity. Anchor on `errorKind` (for `aws_multi_region`) or the structured envelope, not on regex-matching `error` text.
|
|
297
|
+
- **`rawBody`** content -- raw stderr/stdout from the underlying `aws` CLI for diagnostic purposes. Format follows whatever the CLI emits in your installed version.
|
|
298
|
+
- **`command`** strings -- the human-readable command shown alongside results. Argv ordering and the exact redaction-stub format (`<redacted len=N>`) may shift.
|
|
299
|
+
- **Tool *descriptions*** -- the prose surfaced to the model. Tightening these is non-breaking.
|
|
300
|
+
|
|
301
|
+
**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`.
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -30054,6 +30054,9 @@ var require_turndown_cjs = __commonJS({
|
|
|
30054
30054
|
}
|
|
30055
30055
|
});
|
|
30056
30056
|
|
|
30057
|
+
// src/index.ts
|
|
30058
|
+
import { pathToFileURL } from "node:url";
|
|
30059
|
+
|
|
30057
30060
|
// node_modules/zod/v3/helpers/util.js
|
|
30058
30061
|
var util;
|
|
30059
30062
|
(function(util2) {
|
|
@@ -50924,11 +50927,11 @@ var Protocol = class {
|
|
|
50924
50927
|
*
|
|
50925
50928
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
50926
50929
|
*/
|
|
50927
|
-
async connect(
|
|
50930
|
+
async connect(transport) {
|
|
50928
50931
|
if (this._transport) {
|
|
50929
50932
|
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
50930
50933
|
}
|
|
50931
|
-
this._transport =
|
|
50934
|
+
this._transport = transport;
|
|
50932
50935
|
const _onclose = this.transport?.onclose;
|
|
50933
50936
|
this._transport.onclose = () => {
|
|
50934
50937
|
_onclose?.();
|
|
@@ -52518,8 +52521,8 @@ var McpServer = class {
|
|
|
52518
52521
|
*
|
|
52519
52522
|
* The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
52520
52523
|
*/
|
|
52521
|
-
async connect(
|
|
52522
|
-
return await this.server.connect(
|
|
52524
|
+
async connect(transport) {
|
|
52525
|
+
return await this.server.connect(transport);
|
|
52523
52526
|
}
|
|
52524
52527
|
/**
|
|
52525
52528
|
* Closes the connection.
|
|
@@ -54207,6 +54210,8 @@ function doStartSsoLogin(profile, opts) {
|
|
|
54207
54210
|
sessionId,
|
|
54208
54211
|
verificationUrl: urlSeen,
|
|
54209
54212
|
userCode: codeSeen,
|
|
54213
|
+
// `|| "default"` is vestigial -- see the empty-profile note above;
|
|
54214
|
+
// `profile` is always non-empty here (startSsoLogin rejects "").
|
|
54210
54215
|
profile: profile || "default"
|
|
54211
54216
|
});
|
|
54212
54217
|
}
|
|
@@ -54307,7 +54312,7 @@ async function waitForLogin(sessionId) {
|
|
|
54307
54312
|
return {
|
|
54308
54313
|
ok: false,
|
|
54309
54314
|
exitCode: null,
|
|
54310
|
-
error: `No active login session with id '${sessionId}'.
|
|
54315
|
+
error: `No active login session with id '${sessionId}'. It may have already completed (waitForLogin is fire-once -- the session is dropped after the first call resolves) or it may never have started. If a prior aws_login_complete already returned success for this id, the login is done; run aws_whoami to confirm rather than starting over. Otherwise call aws_login_start first.`
|
|
54311
54316
|
};
|
|
54312
54317
|
}
|
|
54313
54318
|
try {
|
|
@@ -54350,6 +54355,9 @@ function parseAwsConfig(text) {
|
|
|
54350
54355
|
currentSsoSession = { name: ssoName, data: {} };
|
|
54351
54356
|
continue;
|
|
54352
54357
|
}
|
|
54358
|
+
if (sectionName2 !== "default" && !/^profile\s+/.test(sectionName2)) {
|
|
54359
|
+
continue;
|
|
54360
|
+
}
|
|
54353
54361
|
const name = sectionName2 === "default" ? "default" : sectionName2.replace(/^profile\s+/, "");
|
|
54354
54362
|
current = { name, isSso: false };
|
|
54355
54363
|
continue;
|
|
@@ -54431,6 +54439,7 @@ var profilesTools = [
|
|
|
54431
54439
|
|
|
54432
54440
|
// src/tools/auth.ts
|
|
54433
54441
|
var MAX_SSO_CACHE_FILE_BYTES = 64 * 1024;
|
|
54442
|
+
var _startSsoLoginImpl = (profile) => startSsoLogin(profile);
|
|
54434
54443
|
function findCachedSsoToken(cacheDir = join3(homedir3(), ".aws", "sso", "cache"), opts = {}) {
|
|
54435
54444
|
try {
|
|
54436
54445
|
const files = readdirSync(cacheDir).filter((f) => f.endsWith(".json"));
|
|
@@ -54462,6 +54471,14 @@ function findCachedSsoToken(cacheDir = join3(homedir3(), ".aws", "sso", "cache")
|
|
|
54462
54471
|
}
|
|
54463
54472
|
return null;
|
|
54464
54473
|
}
|
|
54474
|
+
function projectSsoToken(cachedToken) {
|
|
54475
|
+
if (!cachedToken) return null;
|
|
54476
|
+
return {
|
|
54477
|
+
expiresAt: cachedToken.expiresAt,
|
|
54478
|
+
minutesLeft: cachedToken.minutesLeft,
|
|
54479
|
+
startUrl: cachedToken.startUrl
|
|
54480
|
+
};
|
|
54481
|
+
}
|
|
54465
54482
|
function startUrlForProfile(profile) {
|
|
54466
54483
|
try {
|
|
54467
54484
|
const text = readFileSync3(join3(homedir3(), ".aws", "config"), "utf-8");
|
|
@@ -54526,11 +54543,7 @@ var authTools = [
|
|
|
54526
54543
|
arn: identity.arn,
|
|
54527
54544
|
profile: useProfile,
|
|
54528
54545
|
region: useRegion,
|
|
54529
|
-
ssoToken: cachedToken
|
|
54530
|
-
expiresAt: cachedToken.expiresAt,
|
|
54531
|
-
minutesLeft: cachedToken.minutesLeft,
|
|
54532
|
-
startUrl: cachedToken.startUrl
|
|
54533
|
-
} : null
|
|
54546
|
+
ssoToken: projectSsoToken(cachedToken)
|
|
54534
54547
|
}
|
|
54535
54548
|
};
|
|
54536
54549
|
}
|
|
@@ -54565,7 +54578,7 @@ var authTools = [
|
|
|
54565
54578
|
}
|
|
54566
54579
|
};
|
|
54567
54580
|
}
|
|
54568
|
-
const result = await
|
|
54581
|
+
const result = await _startSsoLoginImpl(useProfile);
|
|
54569
54582
|
if (!result.ok) {
|
|
54570
54583
|
return {
|
|
54571
54584
|
ok: false,
|
|
@@ -54630,7 +54643,7 @@ var authTools = [
|
|
|
54630
54643
|
arn: identity.arn,
|
|
54631
54644
|
profile: useProfile,
|
|
54632
54645
|
region: useRegion,
|
|
54633
|
-
ssoToken: cachedToken
|
|
54646
|
+
ssoToken: projectSsoToken(cachedToken)
|
|
54634
54647
|
}
|
|
54635
54648
|
};
|
|
54636
54649
|
}
|
|
@@ -54682,7 +54695,7 @@ var authTools = [
|
|
|
54682
54695
|
}
|
|
54683
54696
|
};
|
|
54684
54697
|
}
|
|
54685
|
-
const loginResult = await
|
|
54698
|
+
const loginResult = await _startSsoLoginImpl(useProfile);
|
|
54686
54699
|
if (!loginResult.ok) {
|
|
54687
54700
|
return { ok: false, error: loginResult.error, rawBody: loginResult.rawOutput };
|
|
54688
54701
|
}
|
|
@@ -54750,7 +54763,14 @@ var callTools = [
|
|
|
54750
54763
|
return {
|
|
54751
54764
|
ok: false,
|
|
54752
54765
|
error: result.error,
|
|
54753
|
-
|
|
54766
|
+
// Treat an empty-string rawStderr as "no stderr" so a nonzero exit
|
|
54767
|
+
// that wrote its diagnostic to stdout (rare but observed: some
|
|
54768
|
+
// `aws` operations route through stdout when stderr is closed or
|
|
54769
|
+
// when a wrapper script swallows stderr) still surfaces the
|
|
54770
|
+
// stdout body. Pinned by call.test.ts -- the failure-shape
|
|
54771
|
+
// contract is "diagnostic text first, stdout second" rather
|
|
54772
|
+
// than "stderr always wins even when empty".
|
|
54773
|
+
rawBody: result.rawStderr ? result.rawStderr : result.rawStdout
|
|
54754
54774
|
};
|
|
54755
54775
|
}
|
|
54756
54776
|
return {
|
|
@@ -55321,6 +55341,12 @@ var logsTools = [
|
|
|
55321
55341
|
}
|
|
55322
55342
|
}
|
|
55323
55343
|
}
|
|
55344
|
+
if (i.logStreamNamePrefix && !isValidLogStreamName(i.logStreamNamePrefix)) {
|
|
55345
|
+
return {
|
|
55346
|
+
ok: false,
|
|
55347
|
+
error: `Invalid logStreamNamePrefix '${i.logStreamNamePrefix}'. Must be 1-512 chars, not start with '-', and contain no ':', '*', or control characters.`
|
|
55348
|
+
};
|
|
55349
|
+
}
|
|
55324
55350
|
const extraFlags = [i.logGroupName, "--format", "json", "--since", i.since ?? "10m"];
|
|
55325
55351
|
if (i.filterPattern) extraFlags.push("--filter-pattern", i.filterPattern);
|
|
55326
55352
|
if (i.logStreamNames && i.logStreamNames.length > 0) {
|
|
@@ -55358,6 +55384,87 @@ var logsTools = [
|
|
|
55358
55384
|
}
|
|
55359
55385
|
];
|
|
55360
55386
|
|
|
55387
|
+
// src/tools/paginate.ts
|
|
55388
|
+
function extractNextToken(data) {
|
|
55389
|
+
if (data && typeof data === "object" && "NextToken" in data) {
|
|
55390
|
+
const token = data.NextToken;
|
|
55391
|
+
if (typeof token === "string" && token.length > 0) return token;
|
|
55392
|
+
}
|
|
55393
|
+
return null;
|
|
55394
|
+
}
|
|
55395
|
+
function wrapQueryForPagination(userQuery) {
|
|
55396
|
+
return `{NextToken: NextToken, items: ${userQuery}}`;
|
|
55397
|
+
}
|
|
55398
|
+
var paginateTools = [
|
|
55399
|
+
{
|
|
55400
|
+
name: "aws_paginate",
|
|
55401
|
+
description: "Fetch one page of a paginated AWS list/describe operation. Identical to aws_call plus `maxItems` (page size) and `startingToken` (resume cursor). Returns the parsed response, a `nextToken` (null when the list is exhausted), and `hasMore`. Call again with the returned nextToken as startingToken until hasMore is false. Use this instead of aws_call for operations that might exceed the 5 MB stdout cap: list-objects-v2, describe-instances, describe-log-streams, list-roles, etc.",
|
|
55402
|
+
annotations: {
|
|
55403
|
+
title: "Fetch one page of a paginated AWS operation",
|
|
55404
|
+
readOnlyHint: true,
|
|
55405
|
+
destructiveHint: false,
|
|
55406
|
+
idempotentHint: true,
|
|
55407
|
+
openWorldHint: true
|
|
55408
|
+
},
|
|
55409
|
+
inputSchema: external_exports3.object({
|
|
55410
|
+
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', 'logs', etc."),
|
|
55411
|
+
operation: external_exports3.string().describe("Paginated operation: 'list-objects-v2', 'describe-instances', 'list-roles', etc."),
|
|
55412
|
+
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) passed via --cli-input-json."),
|
|
55413
|
+
query: external_exports3.string().optional().describe(
|
|
55414
|
+
"JMESPath expression to extract fields from each page (--query). The query is wrapped server-side as {NextToken, items: <query>} so pagination still works even when the projection drops NextToken; the handler unwraps `items` before returning."
|
|
55415
|
+
),
|
|
55416
|
+
maxItems: external_exports3.number().int().positive().optional().describe("Items per page. Default 100. Lower this if hitting the 5 MB output cap."),
|
|
55417
|
+
startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
55418
|
+
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
55419
|
+
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
55420
|
+
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
55421
|
+
}),
|
|
55422
|
+
handler: async (input) => {
|
|
55423
|
+
const i = input;
|
|
55424
|
+
const maxItems = i.maxItems ?? 100;
|
|
55425
|
+
const extraFlags = ["--max-items", String(maxItems)];
|
|
55426
|
+
if (i.startingToken) {
|
|
55427
|
+
extraFlags.push("--starting-token", i.startingToken);
|
|
55428
|
+
}
|
|
55429
|
+
const userQuery = i.query?.trim();
|
|
55430
|
+
const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
|
|
55431
|
+
const result = await runAwsCall({
|
|
55432
|
+
service: i.service,
|
|
55433
|
+
operation: i.operation,
|
|
55434
|
+
params: i.params,
|
|
55435
|
+
query: queryWrapped,
|
|
55436
|
+
profile: i.profile,
|
|
55437
|
+
region: i.region,
|
|
55438
|
+
outputFormat: "json",
|
|
55439
|
+
timeoutMs: i.timeoutMs,
|
|
55440
|
+
extraFlags
|
|
55441
|
+
});
|
|
55442
|
+
if (!result.ok) {
|
|
55443
|
+
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55444
|
+
}
|
|
55445
|
+
let resultBody;
|
|
55446
|
+
let nextToken;
|
|
55447
|
+
if (queryWrapped) {
|
|
55448
|
+
const wrapped = result.data ?? {};
|
|
55449
|
+
nextToken = extractNextToken(wrapped);
|
|
55450
|
+
resultBody = wrapped.items ?? null;
|
|
55451
|
+
} else {
|
|
55452
|
+
nextToken = extractNextToken(result.data);
|
|
55453
|
+
resultBody = result.data;
|
|
55454
|
+
}
|
|
55455
|
+
return {
|
|
55456
|
+
ok: true,
|
|
55457
|
+
data: {
|
|
55458
|
+
command: result.command,
|
|
55459
|
+
result: resultBody,
|
|
55460
|
+
nextToken,
|
|
55461
|
+
hasMore: nextToken !== null
|
|
55462
|
+
}
|
|
55463
|
+
};
|
|
55464
|
+
}
|
|
55465
|
+
}
|
|
55466
|
+
];
|
|
55467
|
+
|
|
55361
55468
|
// src/tools/metrics.ts
|
|
55362
55469
|
var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
|
|
55363
55470
|
var EXTENDED_STAT_RE = /^(p|tm|tc|wm|pr|ts|iqm)(\d{1,3}(\.\d{1,3})?)?$/i;
|
|
@@ -55376,6 +55483,7 @@ function canonicalizeStatistic(s) {
|
|
|
55376
55483
|
}
|
|
55377
55484
|
var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
|
|
55378
55485
|
var MAX_QUERIES = 100;
|
|
55486
|
+
var CLOUDWATCH_MAX_DATAPOINTS = 100800;
|
|
55379
55487
|
var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
|
|
55380
55488
|
var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
|
|
55381
55489
|
var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
|
|
@@ -55436,7 +55544,7 @@ function buildMetricDataQueries(inputs, autoPeriod) {
|
|
|
55436
55544
|
var metricsTools = [
|
|
55437
55545
|
{
|
|
55438
55546
|
name: "aws_metrics_query",
|
|
55439
|
-
description: "Query CloudWatch metrics via GetMetricData (the modern multi-metric / expression-capable API, not the legacy get-metric-statistics). Pass `queries` as a flat array of {id, namespace, metricName, dimensions?, statistic?, period?, expression?, label?}; the tool shapes them into MetricDataQueries for you. `startTime`/`endTime` accept ISO 8601 or relative shorthand ('15m', '1h', '1d', '1w'); endTime defaults to 'now'. Period is auto-picked from the time range when omitted (60s for <=3h, 300s for <=24h, 900s for <=15d, 3600s otherwise) to stay under CloudWatch's ~100,800-datapoint response cap. Returns {series: [{id, label?, timestamps, values, statusCode?}], messages?, periodSeconds, profile, region, nextToken, hasMore}. When CloudWatch truncates a large response, `hasMore` is true and `nextToken` carries the resume cursor -- call again with `nextToken` set to fetch the next page (rare for typical agent queries that stay within the per-request cap). Use for 'show me the CPU on this instance for the last hour', 'sum lambda invocations across these 3 functions', or expression-based 'p99 latency divided by average latency' lookups.",
|
|
55547
|
+
description: "Query CloudWatch metrics via GetMetricData (the modern multi-metric / expression-capable API, not the legacy get-metric-statistics). Pass `queries` as a flat array of {id, namespace, metricName, dimensions?, statistic?, period?, expression?, label?}; the tool shapes them into MetricDataQueries for you. `startTime`/`endTime` accept ISO 8601 or relative shorthand ('15m', '1h', '1d', '1w'); endTime defaults to 'now'. Period is auto-picked from the time range when omitted (60s for <=3h, 300s for <=24h, 900s for <=15d, 3600s otherwise) to stay under CloudWatch's ~100,800-datapoint response cap. Returns {series: [{id, label?, timestamps, values, period?, statusCode?}], messages?, periodSeconds, profile, region, nextToken, hasMore}. Each series' `period` is the effective granularity for that query (its explicit period, or the auto-pick it inherited); it is omitted for an expression query that didn't set one. The top-level `periodSeconds` is always the auto-pick. When CloudWatch truncates a large response, `hasMore` is true and `nextToken` carries the resume cursor -- call again with `nextToken` set to fetch the next page (rare for typical agent queries that stay within the per-request cap). Use for 'show me the CPU on this instance for the last hour', 'sum lambda invocations across these 3 functions', or expression-based 'p99 latency divided by average latency' lookups.",
|
|
55440
55548
|
annotations: {
|
|
55441
55549
|
title: "Query CloudWatch metrics (GetMetricData)",
|
|
55442
55550
|
readOnlyHint: true,
|
|
@@ -55471,7 +55579,7 @@ var metricsTools = [
|
|
|
55471
55579
|
endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
|
|
55472
55580
|
scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
|
|
55473
55581
|
maxDataPoints: external_exports3.number().int().positive().optional().describe(
|
|
55474
|
-
"
|
|
55582
|
+
"Target datapoint count. CloudWatch does not truncate to the first N points -- it widens (coarsens) the period server-side so the series aggregates down to fit this many points. CloudWatch's own ceiling is ~100,800; lower this to make CloudWatch return a coarser, smaller series. Forwarded as CloudWatch's MaxDatapoints (single 'p') field; the camelCase schema name follows this server's convention."
|
|
55475
55583
|
),
|
|
55476
55584
|
nextToken: external_exports3.string().optional().describe(
|
|
55477
55585
|
"Resume cursor from a previous call's `nextToken`. Omit for the first page. Forwarded as CloudWatch's NextToken; only meaningful when a prior call returned `hasMore: true`."
|
|
@@ -55543,6 +55651,23 @@ var metricsTools = [
|
|
|
55543
55651
|
error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
|
|
55544
55652
|
};
|
|
55545
55653
|
}
|
|
55654
|
+
const rangeSeconds = (endDate.getTime() - startDate.getTime()) / 1e3;
|
|
55655
|
+
for (const q of i.queries) {
|
|
55656
|
+
if (q.period === void 0) continue;
|
|
55657
|
+
if (q.period <= 0 || q.period % 60 !== 0) {
|
|
55658
|
+
return {
|
|
55659
|
+
ok: false,
|
|
55660
|
+
error: `Query '${q.id}' has invalid period ${q.period}. CloudWatch requires period to be a positive multiple of 60 (seconds).`
|
|
55661
|
+
};
|
|
55662
|
+
}
|
|
55663
|
+
const datapoints = Math.ceil(rangeSeconds / q.period);
|
|
55664
|
+
if (datapoints > CLOUDWATCH_MAX_DATAPOINTS) {
|
|
55665
|
+
return {
|
|
55666
|
+
ok: false,
|
|
55667
|
+
error: `Query '${q.id}' with period ${q.period}s over the requested range (${startDate.toISOString()} to ${endDate.toISOString()}) would request ${datapoints} datapoints, exceeding CloudWatch's per-request cap of ${CLOUDWATCH_MAX_DATAPOINTS}. Widen the period or narrow the time range.`
|
|
55668
|
+
};
|
|
55669
|
+
}
|
|
55670
|
+
}
|
|
55546
55671
|
const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
|
|
55547
55672
|
const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
|
|
55548
55673
|
const params = {
|
|
@@ -55568,18 +55693,24 @@ var metricsTools = [
|
|
|
55568
55693
|
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55569
55694
|
}
|
|
55570
55695
|
const raw = result.data ?? {};
|
|
55571
|
-
const
|
|
55572
|
-
|
|
55573
|
-
|
|
55574
|
-
|
|
55575
|
-
|
|
55576
|
-
|
|
55577
|
-
|
|
55696
|
+
const queryById = new Map(i.queries.map((q) => [q.id, q]));
|
|
55697
|
+
const series = (raw.MetricDataResults ?? []).map((r) => {
|
|
55698
|
+
const q = queryById.get(r.Id ?? "");
|
|
55699
|
+
const effectivePeriod = q?.period ?? (q && q.expression === void 0 ? periodSeconds : void 0);
|
|
55700
|
+
return {
|
|
55701
|
+
id: r.Id ?? "",
|
|
55702
|
+
...r.Label !== void 0 ? { label: r.Label } : {},
|
|
55703
|
+
timestamps: r.Timestamps ?? [],
|
|
55704
|
+
values: r.Values ?? [],
|
|
55705
|
+
...effectivePeriod !== void 0 ? { period: effectivePeriod } : {},
|
|
55706
|
+
...r.StatusCode !== void 0 ? { statusCode: r.StatusCode } : {}
|
|
55707
|
+
};
|
|
55708
|
+
});
|
|
55578
55709
|
const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
|
|
55579
55710
|
code: m.Code,
|
|
55580
55711
|
value: m.Value
|
|
55581
55712
|
}));
|
|
55582
|
-
const nextToken =
|
|
55713
|
+
const nextToken = extractNextToken(raw);
|
|
55583
55714
|
return {
|
|
55584
55715
|
ok: true,
|
|
55585
55716
|
data: {
|
|
@@ -55634,7 +55765,7 @@ var multiRegionTools = [
|
|
|
55634
55765
|
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
|
|
55635
55766
|
operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
|
|
55636
55767
|
regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
|
|
55637
|
-
`Region IDs (e.g. ['us-east-1','us-west-2','eu-west-1']). 1-${MAX_REGIONS}. Validated for argv-safety; bad region
|
|
55768
|
+
`Region IDs (e.g. ['us-east-1','us-west-2','eu-west-1']). 1-${MAX_REGIONS}. Validated for argv-safety; a bad region name yields a clear per-region error and skips its CLI spawn (per-region isolation comes from each region being a separate call, not from this pre-check).`
|
|
55638
55769
|
),
|
|
55639
55770
|
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
|
|
55640
55771
|
query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
|
|
@@ -55701,87 +55832,6 @@ var multiRegionTools = [
|
|
|
55701
55832
|
}
|
|
55702
55833
|
];
|
|
55703
55834
|
|
|
55704
|
-
// src/tools/paginate.ts
|
|
55705
|
-
function extractNextToken(data) {
|
|
55706
|
-
if (data && typeof data === "object" && "NextToken" in data) {
|
|
55707
|
-
const token = data.NextToken;
|
|
55708
|
-
if (typeof token === "string" && token.length > 0) return token;
|
|
55709
|
-
}
|
|
55710
|
-
return null;
|
|
55711
|
-
}
|
|
55712
|
-
function wrapQueryForPagination(userQuery) {
|
|
55713
|
-
return `{NextToken: NextToken, items: ${userQuery}}`;
|
|
55714
|
-
}
|
|
55715
|
-
var paginateTools = [
|
|
55716
|
-
{
|
|
55717
|
-
name: "aws_paginate",
|
|
55718
|
-
description: "Fetch one page of a paginated AWS list/describe operation. Identical to aws_call plus `maxItems` (page size) and `startingToken` (resume cursor). Returns the parsed response, a `nextToken` (null when the list is exhausted), and `hasMore`. Call again with the returned nextToken as startingToken until hasMore is false. Use this instead of aws_call for operations that might exceed the 5 MB stdout cap: list-objects-v2, describe-instances, describe-log-streams, list-roles, etc.",
|
|
55719
|
-
annotations: {
|
|
55720
|
-
title: "Fetch one page of a paginated AWS operation",
|
|
55721
|
-
readOnlyHint: true,
|
|
55722
|
-
destructiveHint: false,
|
|
55723
|
-
idempotentHint: true,
|
|
55724
|
-
openWorldHint: true
|
|
55725
|
-
},
|
|
55726
|
-
inputSchema: external_exports3.object({
|
|
55727
|
-
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', 'logs', etc."),
|
|
55728
|
-
operation: external_exports3.string().describe("Paginated operation: 'list-objects-v2', 'describe-instances', 'list-roles', etc."),
|
|
55729
|
-
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) passed via --cli-input-json."),
|
|
55730
|
-
query: external_exports3.string().optional().describe(
|
|
55731
|
-
"JMESPath expression to extract fields from each page (--query). The query is wrapped server-side as {NextToken, items: <query>} so pagination still works even when the projection drops NextToken; the handler unwraps `items` before returning."
|
|
55732
|
-
),
|
|
55733
|
-
maxItems: external_exports3.number().int().positive().optional().describe("Items per page. Default 100. Lower this if hitting the 5 MB output cap."),
|
|
55734
|
-
startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
55735
|
-
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
55736
|
-
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
55737
|
-
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
55738
|
-
}),
|
|
55739
|
-
handler: async (input) => {
|
|
55740
|
-
const i = input;
|
|
55741
|
-
const maxItems = i.maxItems ?? 100;
|
|
55742
|
-
const extraFlags = ["--max-items", String(maxItems)];
|
|
55743
|
-
if (i.startingToken) {
|
|
55744
|
-
extraFlags.push("--starting-token", i.startingToken);
|
|
55745
|
-
}
|
|
55746
|
-
const userQuery = i.query?.trim();
|
|
55747
|
-
const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
|
|
55748
|
-
const result = await runAwsCall({
|
|
55749
|
-
service: i.service,
|
|
55750
|
-
operation: i.operation,
|
|
55751
|
-
params: i.params,
|
|
55752
|
-
query: queryWrapped,
|
|
55753
|
-
profile: i.profile,
|
|
55754
|
-
region: i.region,
|
|
55755
|
-
outputFormat: "json",
|
|
55756
|
-
timeoutMs: i.timeoutMs,
|
|
55757
|
-
extraFlags
|
|
55758
|
-
});
|
|
55759
|
-
if (!result.ok) {
|
|
55760
|
-
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55761
|
-
}
|
|
55762
|
-
let resultBody;
|
|
55763
|
-
let nextToken;
|
|
55764
|
-
if (queryWrapped) {
|
|
55765
|
-
const wrapped = result.data ?? {};
|
|
55766
|
-
nextToken = typeof wrapped.NextToken === "string" && wrapped.NextToken.length > 0 ? wrapped.NextToken : null;
|
|
55767
|
-
resultBody = wrapped.items ?? null;
|
|
55768
|
-
} else {
|
|
55769
|
-
nextToken = extractNextToken(result.data);
|
|
55770
|
-
resultBody = result.data;
|
|
55771
|
-
}
|
|
55772
|
-
return {
|
|
55773
|
-
ok: true,
|
|
55774
|
-
data: {
|
|
55775
|
-
command: result.command,
|
|
55776
|
-
result: resultBody,
|
|
55777
|
-
nextToken,
|
|
55778
|
-
hasMore: nextToken !== null
|
|
55779
|
-
}
|
|
55780
|
-
};
|
|
55781
|
-
}
|
|
55782
|
-
}
|
|
55783
|
-
];
|
|
55784
|
-
|
|
55785
55835
|
// src/tools/resource.ts
|
|
55786
55836
|
var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
|
|
55787
55837
|
function isValidIdentifier(id) {
|
|
@@ -56065,7 +56115,7 @@ var resourceTools = [
|
|
|
56065
56115
|
const p = parseResourceProperties(d);
|
|
56066
56116
|
return { identifier: p.Identifier, properties: p.Properties };
|
|
56067
56117
|
});
|
|
56068
|
-
const nextToken =
|
|
56118
|
+
const nextToken = extractNextToken(raw);
|
|
56069
56119
|
return {
|
|
56070
56120
|
ok: true,
|
|
56071
56121
|
data: {
|
|
@@ -56755,10 +56805,10 @@ var scriptTools = [
|
|
|
56755
56805
|
},
|
|
56756
56806
|
inputSchema: external_exports3.object({
|
|
56757
56807
|
code: external_exports3.string().min(1).describe(
|
|
56758
|
-
"JavaScript snippet evaluated inside `(async () => { ... })()`. Use `return <value>` to surface a result. Bound globals: aws.call, aws.paginate, aws.paginateAll, aws.resource.{get,list,create,update,delete,status}, aws.logsTail, aws.metricsQuery, aws.iamSimulate, aws.multiRegion, aws.assumeRole, aws.docs.{search,read}, console (capture), JSON, Math, Date, Promise, Array, Object, String, Number, Boolean, Error, Intl, Atomics, SharedArrayBuffer, WebAssembly (compile blocked). Intentionally NOT bound (call as sibling MCP tools instead): aws_list_profiles, the auth/session tools, and aws_script itself. Shadowed (undefined): require,
|
|
56808
|
+
"JavaScript snippet evaluated inside `(async () => { ... })()`. Use `return <value>` to surface a result. Bound globals: aws.call, aws.paginate, aws.paginateAll, aws.resource.{get,list,create,update,delete,status}, aws.logsTail, aws.metricsQuery, aws.iamSimulate, aws.multiRegion, aws.assumeRole, aws.docs.{search,read}, console (capture), JSON, Math, Date, Promise, Array, Object, String, Number, Boolean, Error, Intl, Atomics, SharedArrayBuffer, WebAssembly (compile blocked). Intentionally NOT bound (call as sibling MCP tools instead): aws_list_profiles, the auth/session tools, and aws_script itself. Shadowed (undefined): require, process, fetch + family, BroadcastChannel, setTimeout/Interval, queueMicrotask, Buffer, global, globalThis. NOT available (ReferenceError if used): URL, URLSearchParams, TextEncoder, TextDecoder, crypto, structuredClone, EventTarget, MessageChannel, performance, fs, import. eval/Function are disabled (codeGeneration off). Tool helpers throw on failure -- wrap in try/catch when you want to handle errors per-call."
|
|
56759
56809
|
),
|
|
56760
56810
|
timeoutMs: external_exports3.number().int().positive().max(MAX_TIMEOUT_MS).optional().describe(
|
|
56761
|
-
`Wall-clock timeout in milliseconds. Default ${DEFAULT_TIMEOUT_MS2}; max ${MAX_TIMEOUT_MS}.
|
|
56811
|
+
`Wall-clock timeout in milliseconds. Default ${DEFAULT_TIMEOUT_MS2}; max ${MAX_TIMEOUT_MS}. Best-effort across evaluation plus awaited aws.* calls -- it fires on synchronous spin before the first await and on async wall-clock once the script has yielded, but a synchronous infinite loop BETWEEN awaits can outrun the timer and is not guaranteed to be interrupted. On timeout the script stops being awaited and the tool returns an error, but any aws.* call already in flight is NOT cancelled -- it continues until its own per-call timeout (default 60s). Plan retries accordingly: a script that timed out mid 'resource.delete' may have completed the delete; re-issuing the same script can double-mutate.`
|
|
56762
56812
|
)
|
|
56763
56813
|
}),
|
|
56764
56814
|
handler: async (input) => {
|
|
@@ -56807,12 +56857,32 @@ var sessionTools = [
|
|
|
56807
56857
|
error: "Nothing to set \u2014 pass at least one of 'profile' or 'region'. Use aws_session_get to read current values."
|
|
56808
56858
|
};
|
|
56809
56859
|
}
|
|
56810
|
-
|
|
56811
|
-
|
|
56812
|
-
if (
|
|
56813
|
-
|
|
56814
|
-
|
|
56860
|
+
if (profile !== void 0) {
|
|
56861
|
+
const trimmed = profile.trim();
|
|
56862
|
+
if (!trimmed) {
|
|
56863
|
+
return { ok: false, error: "Profile name cannot be empty" };
|
|
56864
|
+
}
|
|
56865
|
+
if (!isValidProfileName(trimmed)) {
|
|
56866
|
+
return {
|
|
56867
|
+
ok: false,
|
|
56868
|
+
error: `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '=', no whitespace or shell metacharacters.`
|
|
56869
|
+
};
|
|
56870
|
+
}
|
|
56871
|
+
}
|
|
56872
|
+
if (region !== void 0) {
|
|
56873
|
+
const trimmed = region.trim();
|
|
56874
|
+
if (!trimmed) {
|
|
56875
|
+
return { ok: false, error: "Region cannot be empty" };
|
|
56876
|
+
}
|
|
56877
|
+
if (!isValidRegionName(trimmed)) {
|
|
56878
|
+
return {
|
|
56879
|
+
ok: false,
|
|
56880
|
+
error: `Invalid region '${trimmed}'. Must match /^[a-z][a-z0-9-]{2,30}$/ (e.g. 'us-east-1', 'eu-west-3').`
|
|
56881
|
+
};
|
|
56882
|
+
}
|
|
56815
56883
|
}
|
|
56884
|
+
if (profile !== void 0) setProfile(profile);
|
|
56885
|
+
if (region !== void 0) setRegion(region);
|
|
56816
56886
|
return { ok: true, data: getSessionState() };
|
|
56817
56887
|
}
|
|
56818
56888
|
},
|
|
@@ -56856,9 +56926,40 @@ var sessionTools = [
|
|
|
56856
56926
|
];
|
|
56857
56927
|
|
|
56858
56928
|
// src/index.ts
|
|
56859
|
-
|
|
56929
|
+
function toMcpResult(response) {
|
|
56930
|
+
if (!response.ok) {
|
|
56931
|
+
const baseError = `Error: ${response.error || "Unknown error"}`;
|
|
56932
|
+
const errorText = response.rawBody ? `${baseError}
|
|
56933
|
+
|
|
56934
|
+
${response.rawBody}` : baseError;
|
|
56935
|
+
return {
|
|
56936
|
+
content: [{ type: "text", text: errorText }],
|
|
56937
|
+
isError: true
|
|
56938
|
+
};
|
|
56939
|
+
}
|
|
56940
|
+
const text = response.rawBody ?? JSON.stringify(response.data ?? { success: true }, null, 2);
|
|
56941
|
+
return {
|
|
56942
|
+
content: [{ type: "text", text }]
|
|
56943
|
+
};
|
|
56944
|
+
}
|
|
56945
|
+
function errorToMcpResult(err, toolName) {
|
|
56946
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56947
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
56948
|
+
console.error(`[aws-mcp] handler '${toolName}' threw: ${message}`);
|
|
56949
|
+
if (stack) console.error(stack);
|
|
56950
|
+
return {
|
|
56951
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
56952
|
+
isError: true
|
|
56953
|
+
};
|
|
56954
|
+
}
|
|
56955
|
+
var version2 = true ? "1.4.1" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
56956
|
+
var isEntryPoint = (() => {
|
|
56957
|
+
const entry = process.argv[1];
|
|
56958
|
+
if (!entry) return false;
|
|
56959
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
56960
|
+
})();
|
|
56860
56961
|
var subcommand = process.argv[2];
|
|
56861
|
-
if (subcommand === "version" || subcommand === "--version") {
|
|
56962
|
+
if (isEntryPoint && (subcommand === "version" || subcommand === "--version")) {
|
|
56862
56963
|
console.log(version2);
|
|
56863
56964
|
process.exit(0);
|
|
56864
56965
|
}
|
|
@@ -56877,49 +56978,29 @@ var allTools = [
|
|
|
56877
56978
|
...docsTools,
|
|
56878
56979
|
...scriptTools
|
|
56879
56980
|
];
|
|
56880
|
-
|
|
56881
|
-
|
|
56882
|
-
|
|
56883
|
-
|
|
56884
|
-
|
|
56885
|
-
|
|
56886
|
-
tool.name,
|
|
56887
|
-
tool.description,
|
|
56888
|
-
tool.inputSchema.shape,
|
|
56889
|
-
tool.annotations,
|
|
56890
|
-
async (input) => {
|
|
56981
|
+
if (isEntryPoint) {
|
|
56982
|
+
const server = new McpServer({
|
|
56983
|
+
name: "@yawlabs/aws-mcp",
|
|
56984
|
+
version: version2
|
|
56985
|
+
});
|
|
56986
|
+
for (const tool of allTools) {
|
|
56987
|
+
server.tool(tool.name, tool.description, tool.inputSchema.shape, tool.annotations, async (input) => {
|
|
56891
56988
|
try {
|
|
56892
|
-
|
|
56893
|
-
if (!response.ok) {
|
|
56894
|
-
const baseError = `Error: ${response.error || "Unknown error"}`;
|
|
56895
|
-
const errorText = response.rawBody ? `${baseError}
|
|
56896
|
-
|
|
56897
|
-
${response.rawBody}` : baseError;
|
|
56898
|
-
return {
|
|
56899
|
-
content: [{ type: "text", text: errorText }],
|
|
56900
|
-
isError: true
|
|
56901
|
-
};
|
|
56902
|
-
}
|
|
56903
|
-
const text = response.rawBody ?? JSON.stringify(response.data ?? { success: true }, null, 2);
|
|
56904
|
-
return {
|
|
56905
|
-
content: [{ type: "text", text }]
|
|
56906
|
-
};
|
|
56989
|
+
return toMcpResult(await tool.handler(input));
|
|
56907
56990
|
} catch (err) {
|
|
56908
|
-
|
|
56909
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
56910
|
-
console.error(`[aws-mcp] handler '${tool.name}' threw: ${message}`);
|
|
56911
|
-
if (stack) console.error(stack);
|
|
56912
|
-
return {
|
|
56913
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
56914
|
-
isError: true
|
|
56915
|
-
};
|
|
56991
|
+
return errorToMcpResult(err, tool.name);
|
|
56916
56992
|
}
|
|
56917
|
-
}
|
|
56918
|
-
|
|
56993
|
+
});
|
|
56994
|
+
}
|
|
56995
|
+
const transport = new StdioServerTransport();
|
|
56996
|
+
await server.connect(transport);
|
|
56997
|
+
console.error(`@yawlabs/aws-mcp v${version2} ready (${allTools.length} tools)`);
|
|
56919
56998
|
}
|
|
56920
|
-
|
|
56921
|
-
|
|
56922
|
-
|
|
56999
|
+
export {
|
|
57000
|
+
allTools,
|
|
57001
|
+
errorToMcpResult,
|
|
57002
|
+
toMcpResult
|
|
57003
|
+
};
|
|
56923
57004
|
/*! Bundled license information:
|
|
56924
57005
|
|
|
56925
57006
|
he/he.js:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/aws-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"mcpName": "io.github.YawLabs/aws-mcp",
|
|
5
5
|
"description": "AWS MCP server — call any AWS API from AI assistants, with first-class SSO re-login (no more 'browser won't open' dead ends)",
|
|
6
6
|
"license": "MIT",
|