@yawlabs/aws-mcp 1.4.1 → 1.5.2

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/dist/index.js CHANGED
@@ -7,7 +7,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
11
15
  };
12
16
  var __export = (target, all) => {
13
17
  for (var name in all)
@@ -30055,6 +30059,7 @@ var require_turndown_cjs = __commonJS({
30055
30059
  });
30056
30060
 
30057
30061
  // src/index.ts
30062
+ import { createRequire } from "node:module";
30058
30063
  import { pathToFileURL } from "node:url";
30059
30064
 
30060
30065
  // node_modules/zod/v3/helpers/util.js
@@ -53507,7 +53512,7 @@ function setProfile(name) {
53507
53512
  const trimmed = name.trim();
53508
53513
  if (!isValidProfileName(trimmed)) {
53509
53514
  throw new Error(
53510
- `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '=', no whitespace or shell metacharacters.`
53515
+ `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='); no whitespace or shell metacharacters.`
53511
53516
  );
53512
53517
  }
53513
53518
  sessionProfile = trimmed;
@@ -53546,9 +53551,10 @@ var MAX_ERROR_MSG_BYTES = 8 * 1024;
53546
53551
  var SAFE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
53547
53552
  function redactDisplayArgs(args) {
53548
53553
  const out = [...args];
53549
- const idx = out.indexOf("--cli-input-json");
53550
- if (idx >= 0 && idx < out.length - 1) {
53551
- out[idx + 1] = `<redacted len=${out[idx + 1].length}>`;
53554
+ for (let i = 0; i < out.length - 1; i++) {
53555
+ if (out[i] === "--cli-input-json") {
53556
+ out[i + 1] = `<redacted len=${out[i + 1].length}>`;
53557
+ }
53552
53558
  }
53553
53559
  return out;
53554
53560
  }
@@ -53612,7 +53618,7 @@ function runAwsCall(opts) {
53612
53618
  return Promise.resolve({
53613
53619
  ok: false,
53614
53620
  kind: "bad_input",
53615
- error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='. Check the 'profile' arg or AWS_PROFILE env var.`
53621
+ error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Check the 'profile' arg or AWS_PROFILE env var.`
53616
53622
  });
53617
53623
  }
53618
53624
  if (!isValidRegionName(region)) {
@@ -53642,6 +53648,13 @@ function runAwsCall(opts) {
53642
53648
  region
53643
53649
  ];
53644
53650
  if (opts.query !== void 0 && opts.query.trim().length > 0) {
53651
+ if (opts.query.length > 2048) {
53652
+ return Promise.resolve({
53653
+ ok: false,
53654
+ kind: "bad_input",
53655
+ error: `query expression too long (${opts.query.length} chars; max 2048). Simplify the JMESPath expression.`
53656
+ });
53657
+ }
53645
53658
  args.push("--query", opts.query);
53646
53659
  }
53647
53660
  if (opts.params !== void 0 && Object.keys(opts.params).length > 0) {
@@ -53838,7 +53851,7 @@ function mergeProfileBody(existingBody, creds) {
53838
53851
  updated.push(line);
53839
53852
  continue;
53840
53853
  }
53841
- const key = line.slice(0, eqIdx).trim();
53854
+ const key = line.slice(0, eqIdx).trim().toLowerCase();
53842
53855
  if (managedKeys.has(key)) {
53843
53856
  seen.add(key);
53844
53857
  updated.push(`${key} = ${creds[key]}`);
@@ -53973,7 +53986,7 @@ function resolveTargetProfile(input) {
53973
53986
  if (input.targetProfile) {
53974
53987
  return input.targetProfile.startsWith("mcp-") ? input.targetProfile : `mcp-${input.targetProfile}`;
53975
53988
  }
53976
- return `mcp-${input.sessionName}`;
53989
+ return input.sessionName.startsWith("mcp-") ? input.sessionName : `mcp-${input.sessionName}`;
53977
53990
  }
53978
53991
  var assumeTools = [
53979
53992
  {
@@ -53987,7 +54000,10 @@ var assumeTools = [
53987
54000
  openWorldHint: true
53988
54001
  },
53989
54002
  inputSchema: external_exports3.object({
53990
- roleArn: external_exports3.string().describe("Target role ARN, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'."),
54003
+ roleArn: external_exports3.string().regex(
54004
+ /^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/,
54005
+ "roleArn must match arn:aws[partition]:iam::<12-digit-account>:role/<name>"
54006
+ ).describe("Target role ARN, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'."),
53991
54007
  sessionName: external_exports3.string().min(2).max(64).regex(/^[\w+=,.@-]+$/, "sessionName must match [\\w+=,.@-]").describe("Role session name (shows up in CloudTrail). Alphanumeric + +=,.@- only."),
53992
54008
  durationSeconds: external_exports3.number().int().min(900).max(43200).optional().describe("Session duration in seconds (900-43200). Default 3600."),
53993
54009
  externalId: external_exports3.string().optional().describe("External ID (only required if the role's trust policy demands it)."),
@@ -54008,13 +54024,19 @@ var assumeTools = [
54008
54024
  if (!isValidProfileName(sourceProfile)) {
54009
54025
  return {
54010
54026
  ok: false,
54011
- error: `Invalid sourceProfile name '${sourceProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='. Check the 'sourceProfile' arg or AWS_PROFILE env var.`
54027
+ error: `Invalid sourceProfile name '${sourceProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Check the 'sourceProfile' arg or AWS_PROFILE env var.`
54012
54028
  };
54013
54029
  }
54014
54030
  if (!isValidProfileName(targetProfile)) {
54015
54031
  return {
54016
54032
  ok: false,
54017
- error: `Invalid targetProfile name '${targetProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='. Pick a different targetProfile or sessionName.`
54033
+ error: `Invalid targetProfile name '${targetProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Pick a different targetProfile or sessionName.`
54034
+ };
54035
+ }
54036
+ if (!/^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/.test(i.roleArn)) {
54037
+ return {
54038
+ ok: false,
54039
+ error: `Invalid roleArn '${i.roleArn}'. Must match arn:aws[partition]:iam::<12-digit-account>:role/<name>, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'.`
54018
54040
  };
54019
54041
  }
54020
54042
  const params = {
@@ -54044,7 +54066,7 @@ var assumeTools = [
54044
54066
  error: `SSO session expired for source profile '${sourceProfile}'. Call aws_login_start with profile='${sourceProfile}' before assuming.`
54045
54067
  };
54046
54068
  }
54047
- return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
54069
+ return { ok: false, error: result.error, rawBody: result.rawStderr };
54048
54070
  }
54049
54071
  const data = result.data ?? {};
54050
54072
  const creds = data.Credentials;
@@ -54052,11 +54074,22 @@ var assumeTools = [
54052
54074
  return { ok: false, error: "STS AssumeRole succeeded but returned incomplete credentials." };
54053
54075
  }
54054
54076
  const credentialsPath = join(homedir(), ".aws", "credentials");
54055
- await upsertProfile(credentialsPath, targetProfile, {
54056
- aws_access_key_id: creds.AccessKeyId,
54057
- aws_secret_access_key: creds.SecretAccessKey,
54058
- aws_session_token: creds.SessionToken
54059
- });
54077
+ try {
54078
+ await upsertProfile(credentialsPath, targetProfile, {
54079
+ aws_access_key_id: creds.AccessKeyId,
54080
+ aws_secret_access_key: creds.SecretAccessKey,
54081
+ aws_session_token: creds.SessionToken
54082
+ });
54083
+ } catch (err) {
54084
+ const code = err.code;
54085
+ if (code === "EACCES" || code === "EROFS" || code === "EPERM") {
54086
+ return {
54087
+ ok: false,
54088
+ error: `Cannot write ${credentialsPath} (permission denied). Check directory permissions or set AWS_SHARED_CREDENTIALS_FILE to a writable path.`
54089
+ };
54090
+ }
54091
+ throw err;
54092
+ }
54060
54093
  const expiration = creds.Expiration;
54061
54094
  return {
54062
54095
  ok: true,
@@ -54111,7 +54144,7 @@ function startSsoLogin(profile, opts = {}) {
54111
54144
  if (!isValidProfileName(profile)) {
54112
54145
  return Promise.resolve({
54113
54146
  ok: false,
54114
- error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='.`
54147
+ error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '=').`
54115
54148
  });
54116
54149
  }
54117
54150
  const key = dedupeKey(profile, opts);
@@ -54157,6 +54190,7 @@ function doStartSsoLogin(profile, opts) {
54157
54190
  let codeSeen = null;
54158
54191
  let settled = false;
54159
54192
  let registeredSession = null;
54193
+ let exitResult = null;
54160
54194
  let completionResolve;
54161
54195
  const completion = new Promise((res) => {
54162
54196
  completionResolve = res;
@@ -54254,14 +54288,17 @@ ${stderrBuf}` : "");
54254
54288
  rawOutput
54255
54289
  };
54256
54290
  }
54291
+ exitResult = result;
54257
54292
  completionResolve(result);
54293
+ });
54294
+ proc.on("close", () => {
54258
54295
  if (!settled) {
54259
54296
  settled = true;
54260
54297
  clearTimeout(urlTimeout);
54261
54298
  resolve({
54262
54299
  ok: false,
54263
- error: result.error ?? "aws sso login exited before printing a verification URL",
54264
- rawOutput: result.rawOutput
54300
+ error: exitResult?.error ?? "aws sso login exited before printing a verification URL",
54301
+ rawOutput: exitResult?.rawOutput
54265
54302
  });
54266
54303
  }
54267
54304
  });
@@ -54820,6 +54857,7 @@ function parseSearchResults(json2, limit) {
54820
54857
  const t = tes;
54821
54858
  const url2 = typeof t.link === "string" ? t.link : void 0;
54822
54859
  if (!url2) continue;
54860
+ if (!isValidDocsUrl(url2)) continue;
54823
54861
  const title = typeof t.title === "string" ? t.title : url2;
54824
54862
  const result = { title, url: url2 };
54825
54863
  if (typeof t.summary === "string" && t.summary.length > 0) result.summary = t.summary;
@@ -54965,7 +55003,7 @@ function buildDocsTools(fetchImpl = fetch) {
54965
55003
  }),
54966
55004
  handler: async (input) => {
54967
55005
  const i = input;
54968
- const limit = i.limit ?? DEFAULT_SEARCH_LIMIT;
55006
+ const limit = Math.min(Math.max(1, i.limit ?? DEFAULT_SEARCH_LIMIT), MAX_SEARCH_LIMIT);
54969
55007
  let response;
54970
55008
  try {
54971
55009
  response = await fetchWithTimeout(
@@ -55035,7 +55073,7 @@ function buildDocsTools(fetchImpl = fetch) {
55035
55073
  openWorldHint: true
55036
55074
  },
55037
55075
  inputSchema: external_exports3.object({
55038
- url: external_exports3.string().min(1).describe(
55076
+ url: external_exports3.string().min(1).max(2048).describe(
55039
55077
  "AWS docs page URL: https://docs.aws.amazon.com/<...>.html. Usually from an aws_docs_search result."
55040
55078
  ),
55041
55079
  startIndex: external_exports3.number().int().min(0).optional().describe("Character offset to start from (for paginated reads). Default 0."),
@@ -55049,8 +55087,8 @@ function buildDocsTools(fetchImpl = fetch) {
55049
55087
  error: `Invalid url '${i.url}'. Must be an 'https://docs.aws.amazon.com/...html' page. Use aws_docs_search to find one.`
55050
55088
  };
55051
55089
  }
55052
- const startIndex = i.startIndex ?? 0;
55053
- const maxLength = i.maxLength ?? DEFAULT_MAX_LENGTH;
55090
+ const startIndex = Math.max(0, i.startIndex ?? 0);
55091
+ const maxLength = Math.min(Math.max(1, i.maxLength ?? DEFAULT_MAX_LENGTH), MAX_MAX_LENGTH);
55054
55092
  let markdown = docCache.get(i.url);
55055
55093
  let cached2 = true;
55056
55094
  if (markdown === void 0) {
@@ -55135,9 +55173,25 @@ function parseSimulationResults(raw) {
55135
55173
  const matched = Array.isArray(er.MatchedStatements) ? er.MatchedStatements : [];
55136
55174
  const ids = [];
55137
55175
  for (const m of matched) {
55138
- if (m && typeof m === "object" && typeof m.SourcePolicyId === "string") {
55139
- ids.push(m.SourcePolicyId);
55176
+ if (!m || typeof m !== "object") continue;
55177
+ const ms = m;
55178
+ const sourceId = ms.SourcePolicyId;
55179
+ if (typeof sourceId === "string" && sourceId.length > 0) {
55180
+ ids.push(sourceId);
55181
+ continue;
55140
55182
  }
55183
+ if (sourceId !== void 0 && sourceId !== "") continue;
55184
+ const sourceType = ms.SourcePolicyType;
55185
+ if (typeof sourceType !== "string") continue;
55186
+ const startPos = ms.StartPosition;
55187
+ if (startPos && typeof startPos === "object") {
55188
+ const line = startPos.Line;
55189
+ if (typeof line === "number") {
55190
+ ids.push(`inline#L${line}`);
55191
+ continue;
55192
+ }
55193
+ }
55194
+ ids.push("inline");
55141
55195
  }
55142
55196
  if (ids.length > 0) result.matchedStatementIds = ids;
55143
55197
  const missing = Array.isArray(er.MissingContextValues) ? er.MissingContextValues : [];
@@ -55165,7 +55219,7 @@ function parseSimulationResults(raw) {
55165
55219
  var iamSimulateTools = [
55166
55220
  {
55167
55221
  name: "aws_iam_simulate",
55168
- description: "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 -- common for tag-based policies). Use this BEFORE a risky operation to avoid a 403; pairs with the post-failure Suggestion you get from aws_call. Requires iam:SimulatePrincipalPolicy on the caller.",
55222
+ description: "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 / unknown -- unknown is the malformed-response fallback when EvalDecision is missing or unrecognised), `matchedStatementIds` (which IAM statements decided), and `missingContextValues` (context keys the policy needed but you didn't provide -- common for tag-based policies). Use this BEFORE a risky operation to avoid a 403; pairs with the post-failure Suggestion you get from aws_call. Requires iam:SimulatePrincipalPolicy on the caller.",
55169
55223
  annotations: {
55170
55224
  title: "Simulate IAM permissions for a principal",
55171
55225
  readOnlyHint: true,
@@ -55175,13 +55229,13 @@ var iamSimulateTools = [
55175
55229
  },
55176
55230
  inputSchema: external_exports3.object({
55177
55231
  principalArn: external_exports3.string().min(1).describe(
55178
- "ARN of the principal whose policies you want to evaluate, e.g. 'arn:aws:iam::123456789012:user/jeff' or 'arn:aws:iam::123:role/my-role'."
55232
+ "ARN of the principal whose policies you want to evaluate, e.g. 'arn:aws:iam::123456789012:user/jeff' or 'arn:aws:iam::123456789012:role/my-role'."
55179
55233
  ),
55180
55234
  actions: external_exports3.array(external_exports3.string().min(1)).min(1).max(50).describe(
55181
55235
  "IAM action names to test, e.g. ['lambda:CreateFunction', 's3:GetObject']. 1-50 entries. Wildcards (e.g. 's3:*') are accepted."
55182
55236
  ),
55183
55237
  resources: external_exports3.array(external_exports3.string().min(1)).optional().describe(
55184
- "Resource ARNs to test against, e.g. ['arn:aws:s3:::my-bucket/*']. Omit to default to ['*'] (best-case 'is this action ever allowed?')."
55238
+ "Resource ARNs to test against, e.g. ['arn:aws:s3:::my-bucket/*']. When omitted, AWS applies its own default of ['*'] server-side (best-case 'is this action ever allowed?') -- this tool does not inject a ['*'] itself."
55185
55239
  ),
55186
55240
  contextEntries: external_exports3.array(
55187
55241
  external_exports3.object({
@@ -55251,13 +55305,14 @@ var iamSimulateTools = [
55251
55305
  const raw = result.data;
55252
55306
  const results = parseSimulationResults(raw?.EvaluationResults);
55253
55307
  const allowed = results.filter((r) => r.decision === "allowed").length;
55254
- const denied = results.length - allowed;
55308
+ const unknown2 = results.filter((r) => r.decision === "unknown").length;
55309
+ const denied = results.length - allowed - unknown2;
55255
55310
  return {
55256
55311
  ok: true,
55257
55312
  data: {
55258
55313
  command: result.command,
55259
55314
  principalArn: i.principalArn,
55260
- summary: { allowed, denied, total: results.length },
55315
+ summary: { allowed, denied, unknown: unknown2, total: results.length },
55261
55316
  results,
55262
55317
  evaluationResults: raw?.EvaluationResults ?? []
55263
55318
  }
@@ -55347,6 +55402,12 @@ var logsTools = [
55347
55402
  error: `Invalid logStreamNamePrefix '${i.logStreamNamePrefix}'. Must be 1-512 chars, not start with '-', and contain no ':', '*', or control characters.`
55348
55403
  };
55349
55404
  }
55405
+ if (i.filterPattern?.startsWith("-")) {
55406
+ return {
55407
+ ok: false,
55408
+ error: "Invalid filterPattern: must not start with '-'."
55409
+ };
55410
+ }
55350
55411
  const extraFlags = [i.logGroupName, "--format", "json", "--since", i.since ?? "10m"];
55351
55412
  if (i.filterPattern) extraFlags.push("--filter-pattern", i.filterPattern);
55352
55413
  if (i.logStreamNames && i.logStreamNames.length > 0) {
@@ -55384,454 +55445,6 @@ var logsTools = [
55384
55445
  }
55385
55446
  ];
55386
55447
 
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
-
55468
- // src/tools/metrics.ts
55469
- var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
55470
- var EXTENDED_STAT_RE = /^(p|tm|tc|wm|pr|ts|iqm)(\d{1,3}(\.\d{1,3})?)?$/i;
55471
- function isValidStatistic(s) {
55472
- const lower = s.toLowerCase();
55473
- if (SIMPLE_STATS.some((stat) => stat.toLowerCase() === lower)) return true;
55474
- return EXTENDED_STAT_RE.test(s);
55475
- }
55476
- function canonicalizeStatistic(s) {
55477
- const lower = s.toLowerCase();
55478
- for (const stat of SIMPLE_STATS) {
55479
- if (stat.toLowerCase() === lower) return stat;
55480
- }
55481
- if (EXTENDED_STAT_RE.test(s)) return lower;
55482
- return s;
55483
- }
55484
- var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
55485
- var MAX_QUERIES = 100;
55486
- var CLOUDWATCH_MAX_DATAPOINTS = 100800;
55487
- var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
55488
- var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
55489
- var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
55490
- function pickAutoPeriodSeconds(startMs, endMs) {
55491
- const rangeMs = Math.max(0, endMs - startMs);
55492
- if (rangeMs <= PERIOD_3H_MS) return 60;
55493
- if (rangeMs <= PERIOD_24H_MS) return 300;
55494
- if (rangeMs <= PERIOD_15D_MS) return 900;
55495
- return 3600;
55496
- }
55497
- var RELATIVE_TIME_RE = /^\d+[smhdw]$/;
55498
- var UNIT_MS = {
55499
- s: 1e3,
55500
- m: 60 * 1e3,
55501
- h: 60 * 60 * 1e3,
55502
- d: 24 * 60 * 60 * 1e3,
55503
- w: 7 * 24 * 60 * 60 * 1e3
55504
- };
55505
- function resolveTime(input, now) {
55506
- if (input === "now") return new Date(now);
55507
- const rel = input.match(RELATIVE_TIME_RE);
55508
- if (rel) {
55509
- const num = Number(input.slice(0, -1));
55510
- const unit = input.slice(-1);
55511
- const ms = UNIT_MS[unit];
55512
- if (!ms || !Number.isFinite(num)) return null;
55513
- return new Date(now - num * ms);
55514
- }
55515
- const t = new Date(input);
55516
- if (Number.isNaN(t.getTime())) return null;
55517
- return t;
55518
- }
55519
- function buildMetricDataQueries(inputs, autoPeriod) {
55520
- return inputs.map((q) => {
55521
- const base = { Id: q.id };
55522
- if (q.label !== void 0) base.Label = q.label;
55523
- if (q.returnData !== void 0) base.ReturnData = q.returnData;
55524
- if (q.expression !== void 0) {
55525
- base.Expression = q.expression;
55526
- if (q.period !== void 0) base.Period = q.period;
55527
- return base;
55528
- }
55529
- const dimensions = q.dimensions ? Object.entries(q.dimensions).map(([Name, Value]) => ({ Name, Value })) : void 0;
55530
- const stat = {
55531
- Metric: {
55532
- Namespace: q.namespace,
55533
- MetricName: q.metricName,
55534
- ...dimensions ? { Dimensions: dimensions } : {}
55535
- },
55536
- Period: q.period ?? autoPeriod,
55537
- Stat: q.statistic !== void 0 ? canonicalizeStatistic(q.statistic) : "Average"
55538
- };
55539
- if (q.unit !== void 0) stat.Unit = q.unit;
55540
- base.MetricStat = stat;
55541
- return base;
55542
- });
55543
- }
55544
- var metricsTools = [
55545
- {
55546
- name: "aws_metrics_query",
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.",
55548
- annotations: {
55549
- title: "Query CloudWatch metrics (GetMetricData)",
55550
- readOnlyHint: true,
55551
- destructiveHint: false,
55552
- idempotentHint: true,
55553
- openWorldHint: true
55554
- },
55555
- inputSchema: external_exports3.object({
55556
- queries: external_exports3.array(
55557
- external_exports3.object({
55558
- id: external_exports3.string().regex(QUERY_ID_RE, "id must match /^[a-z][A-Za-z0-9_]*$/ (CloudWatch's MetricDataQuery.Id contract)"),
55559
- namespace: external_exports3.string().min(1).optional().describe("AWS metric namespace, e.g. 'AWS/Lambda', 'AWS/EC2'. Required unless `expression` is set."),
55560
- metricName: external_exports3.string().min(1).optional().describe("Metric name, e.g. 'Invocations', 'CPUUtilization'. Required unless `expression` is set."),
55561
- dimensions: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("Dimension Name -> Value map, e.g. {FunctionName: 'my-fn'}."),
55562
- statistic: external_exports3.string().optional().describe(
55563
- "Statistic: Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like 'p99', 'p99.9', 'tm95'. Default 'Average'."
55564
- ),
55565
- period: external_exports3.number().int().positive().optional().describe("Period in seconds. Defaults to an auto-pick from the time range (60s/300s/900s/3600s)."),
55566
- expression: external_exports3.string().min(1).optional().describe(
55567
- `CloudWatch metric math expression, e.g. 'SUM([m1, m2])' or 'AVG(METRICS("AWS/Lambda"))'. Mutually exclusive with namespace/metricName/dimensions.`
55568
- ),
55569
- label: external_exports3.string().optional().describe("Human-readable label for the series in the response."),
55570
- returnData: external_exports3.boolean().optional().describe(
55571
- "Set false to compute this query but not return its data (useful for intermediate values in expressions). Default true."
55572
- ),
55573
- unit: external_exports3.string().optional().describe(
55574
- "Restrict to a specific Unit (e.g. 'Seconds', 'Bytes'). Default: no filter. Only meaningful on metric-stat queries."
55575
- )
55576
- })
55577
- ).min(1).max(MAX_QUERIES).describe(`1-${MAX_QUERIES} queries. Each is either a metric-stat (namespace + metricName) or an expression.`),
55578
- startTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand ('15m', '1h', '1d', '1w'). Default '1h' (one hour ago)."),
55579
- endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
55580
- scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
55581
- maxDataPoints: external_exports3.number().int().positive().optional().describe(
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."
55583
- ),
55584
- nextToken: external_exports3.string().optional().describe(
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`."
55586
- ),
55587
- profile: external_exports3.string().optional().describe("Override session profile for this call."),
55588
- region: external_exports3.string().optional().describe("Override session region for this call."),
55589
- timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000 (60s).")
55590
- }),
55591
- handler: async (input) => {
55592
- const i = input;
55593
- const seenIds = /* @__PURE__ */ new Map();
55594
- for (let qi = 0; qi < i.queries.length; qi++) {
55595
- const q = i.queries[qi];
55596
- const firstIdx = seenIds.get(q.id);
55597
- if (firstIdx !== void 0) {
55598
- return {
55599
- ok: false,
55600
- error: `Duplicate query id '${q.id}' at queries[${qi}]; first seen at queries[${firstIdx}]. Each MetricDataQuery.Id must be unique in a batch.`
55601
- };
55602
- }
55603
- seenIds.set(q.id, qi);
55604
- const hasMetricStat = q.namespace !== void 0 || q.metricName !== void 0 || q.dimensions !== void 0;
55605
- const hasExpression = q.expression !== void 0;
55606
- if (hasMetricStat && hasExpression) {
55607
- return {
55608
- ok: false,
55609
- error: `Query '${q.id}' mixes metric-stat fields (namespace/metricName/dimensions) with 'expression'. Pick one shape per query.`
55610
- };
55611
- }
55612
- if (!hasMetricStat && !hasExpression) {
55613
- return {
55614
- ok: false,
55615
- error: `Query '${q.id}' has neither metric-stat (namespace+metricName) nor 'expression'. One is required.`
55616
- };
55617
- }
55618
- if (hasMetricStat && (q.namespace === void 0 || q.metricName === void 0)) {
55619
- return {
55620
- ok: false,
55621
- error: `Query '${q.id}' must include BOTH 'namespace' and 'metricName' (or use 'expression' instead).`
55622
- };
55623
- }
55624
- if (q.statistic !== void 0 && !isValidStatistic(q.statistic)) {
55625
- return {
55626
- ok: false,
55627
- error: `Query '${q.id}' has invalid statistic '${q.statistic}'. Use Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like p99 / p99.9 / tm95.`
55628
- };
55629
- }
55630
- }
55631
- const now = Date.now();
55632
- const startStr = i.startTime ?? "1h";
55633
- const endStr = i.endTime ?? "now";
55634
- const startDate = resolveTime(startStr, now);
55635
- const endDate = resolveTime(endStr, now);
55636
- if (!startDate) {
55637
- return {
55638
- ok: false,
55639
- error: `Invalid startTime '${startStr}'. Use ISO 8601 (e.g. '2026-05-16T10:00:00Z') or relative shorthand (e.g. '1h', '15m', '1d').`
55640
- };
55641
- }
55642
- if (!endDate) {
55643
- return {
55644
- ok: false,
55645
- error: `Invalid endTime '${endStr}'. Use ISO 8601 or relative shorthand, or 'now' for the current moment.`
55646
- };
55647
- }
55648
- if (endDate.getTime() <= startDate.getTime()) {
55649
- return {
55650
- ok: false,
55651
- error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
55652
- };
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
- }
55671
- const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
55672
- const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
55673
- const params = {
55674
- MetricDataQueries: metricDataQueries,
55675
- StartTime: startDate.toISOString(),
55676
- EndTime: endDate.toISOString(),
55677
- ScanBy: i.scanBy ?? "TimestampDescending"
55678
- };
55679
- if (i.maxDataPoints !== void 0) params.MaxDatapoints = i.maxDataPoints;
55680
- if (i.nextToken !== void 0) params.NextToken = i.nextToken;
55681
- const effectiveProfile = i.profile ?? getProfile();
55682
- const effectiveRegion = i.region ?? getRegion();
55683
- const result = await runAwsCall({
55684
- service: "cloudwatch",
55685
- operation: "get-metric-data",
55686
- profile: i.profile,
55687
- region: i.region,
55688
- timeoutMs: i.timeoutMs,
55689
- outputFormat: "json",
55690
- params
55691
- });
55692
- if (!result.ok) {
55693
- return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
55694
- }
55695
- const raw = result.data ?? {};
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
- });
55709
- const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
55710
- code: m.Code,
55711
- value: m.Value
55712
- }));
55713
- const nextToken = extractNextToken(raw);
55714
- return {
55715
- ok: true,
55716
- data: {
55717
- command: result.command,
55718
- profile: effectiveProfile,
55719
- region: effectiveRegion,
55720
- startTime: startDate.toISOString(),
55721
- endTime: endDate.toISOString(),
55722
- periodSeconds,
55723
- series,
55724
- nextToken,
55725
- hasMore: nextToken !== null,
55726
- ...messages && messages.length > 0 ? { messages } : {}
55727
- }
55728
- };
55729
- }
55730
- }
55731
- ];
55732
-
55733
- // src/tools/multi-region.ts
55734
- var DEFAULT_CONCURRENCY = 8;
55735
- var MAX_CONCURRENCY = 32;
55736
- var MAX_REGIONS = 32;
55737
- async function runWithConcurrency(inputs, concurrency, fn) {
55738
- const results = new Array(inputs.length);
55739
- let next = 0;
55740
- const worker = async () => {
55741
- while (true) {
55742
- const i = next++;
55743
- if (i >= inputs.length) return;
55744
- results[i] = await fn(inputs[i], i);
55745
- }
55746
- };
55747
- const workerCount = Math.min(concurrency, inputs.length);
55748
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
55749
- return results;
55750
- }
55751
- var multiRegionTools = [
55752
- {
55753
- name: "aws_multi_region",
55754
- description: "Run the same AWS API operation across multiple regions in parallel. Same shape as aws_call (service, operation, params?, query?, outputFormat?, timeoutMs?) but takes `regions: string[]` instead of `region`. Returns an array of `{region, ok, data?, command?, error?, errorKind?}` -- partial failure is expected (services aren't everywhere, perms may be region-scoped). Duplicate regions in the input are collapsed (first occurrence wins), so `results.length` may be less than `regions.length`; use the returned `regionCount` for the actual count run. Use for fleet-wide reads: 'describe-instances across all our regions', 'list buckets in every region', 'check IAM password policy everywhere'.",
55755
- annotations: {
55756
- title: "Run an AWS operation across multiple regions in parallel",
55757
- // The operation can be anything -- we conservatively annotate as not
55758
- // read-only / not destructive. The caller chooses what to invoke.
55759
- readOnlyHint: false,
55760
- destructiveHint: false,
55761
- idempotentHint: false,
55762
- openWorldHint: true
55763
- },
55764
- inputSchema: external_exports3.object({
55765
- service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
55766
- operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
55767
- regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
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).`
55769
- ),
55770
- params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
55771
- query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
55772
- outputFormat: external_exports3.enum(["json", "text", "table", "yaml"]).optional().describe("Output format. Default 'json'."),
55773
- profile: external_exports3.string().optional().describe("Override session profile for the batch."),
55774
- timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in ms applied PER region. Default 60000."),
55775
- concurrency: external_exports3.number().int().positive().max(MAX_CONCURRENCY).optional().describe(`Max regions in flight at once (1-${MAX_CONCURRENCY}). Default ${DEFAULT_CONCURRENCY}.`)
55776
- }),
55777
- handler: async (input) => {
55778
- const i = input;
55779
- const seen = /* @__PURE__ */ new Set();
55780
- const regions = [];
55781
- for (const r of i.regions) {
55782
- if (!seen.has(r)) {
55783
- seen.add(r);
55784
- regions.push(r);
55785
- }
55786
- }
55787
- const concurrency = i.concurrency ?? DEFAULT_CONCURRENCY;
55788
- const results = await runWithConcurrency(regions, concurrency, async (region) => {
55789
- if (!isValidRegionName(region)) {
55790
- return {
55791
- region,
55792
- ok: false,
55793
- error: `Invalid region '${region}'. Must match ${REGION_NAME_RE} (e.g. 'us-east-1').`,
55794
- errorKind: "bad_input"
55795
- };
55796
- }
55797
- const r = await runAwsCall({
55798
- service: i.service,
55799
- operation: i.operation,
55800
- params: i.params,
55801
- query: i.query,
55802
- profile: i.profile,
55803
- region,
55804
- outputFormat: i.outputFormat,
55805
- timeoutMs: i.timeoutMs
55806
- });
55807
- if (!r.ok) {
55808
- return {
55809
- region,
55810
- ok: false,
55811
- command: r.command,
55812
- error: r.error,
55813
- errorKind: r.kind
55814
- };
55815
- }
55816
- return { region, ok: true, command: r.command, data: r.data };
55817
- });
55818
- const okCount = results.filter((r) => r.ok).length;
55819
- const errCount = results.length - okCount;
55820
- return {
55821
- ok: true,
55822
- data: {
55823
- service: i.service,
55824
- operation: i.operation,
55825
- regionCount: regions.length,
55826
- okCount,
55827
- errorCount: errCount,
55828
- results
55829
- }
55830
- };
55831
- }
55832
- }
55833
- ];
55834
-
55835
55448
  // src/tools/resource.ts
55836
55449
  var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
55837
55450
  function isValidIdentifier(id) {
@@ -56415,6 +56028,11 @@ function _applyJsonPatchInPlace(doc, ops) {
56415
56028
  throw new Error(`op '${op.op}' at index ${i} is not implemented in aws_resource_diff (use add/remove/replace).`);
56416
56029
  }
56417
56030
  const tokens = parseJsonPointer(op.path);
56031
+ for (const seg of tokens) {
56032
+ if (seg === "__proto__" || seg === "constructor" || seg === "prototype") {
56033
+ throw new Error(`Path '${op.path}' contains reserved segment '${seg}' at op index ${i}.`);
56034
+ }
56035
+ }
56418
56036
  if (tokens.length === 0) {
56419
56037
  if (op.op === "remove") {
56420
56038
  throw new Error(`Cannot remove the document root at index ${i}.`);
@@ -56439,7 +56057,7 @@ function _applyJsonPatchInPlace(doc, ops) {
56439
56057
  }
56440
56058
  parent = parent[idx];
56441
56059
  } else if (isObj(parent)) {
56442
- if (!(segment in parent)) {
56060
+ if (!Object.hasOwn(parent, segment)) {
56443
56061
  if (op.op === "add") {
56444
56062
  parent[segment] = {};
56445
56063
  parent = parent[segment];
@@ -56482,12 +56100,12 @@ function _applyJsonPatchInPlace(doc, ops) {
56482
56100
  }
56483
56101
  } else if (isObj(parent)) {
56484
56102
  if (op.op === "remove") {
56485
- if (!(lastToken in parent)) {
56103
+ if (!Object.hasOwn(parent, lastToken)) {
56486
56104
  throw new Error(`Cannot remove missing key '${lastToken}' at index ${i}.`);
56487
56105
  }
56488
56106
  delete parent[lastToken];
56489
56107
  } else if (op.op === "replace") {
56490
- if (!(lastToken in parent)) {
56108
+ if (!Object.hasOwn(parent, lastToken)) {
56491
56109
  throw new Error(`Cannot replace missing key '${lastToken}' at index ${i} (use 'add' to create it).`);
56492
56110
  }
56493
56111
  parent[lastToken] = clone2(op.value);
@@ -56511,10 +56129,6 @@ function summarizePatch(ops, before, after) {
56511
56129
  }
56512
56130
  const out = [];
56513
56131
  for (const op of ops) {
56514
- if (op.op === "move" || op.op === "copy" || op.op === "test") {
56515
- out.push({ op: "replace", path: op.path });
56516
- continue;
56517
- }
56518
56132
  const beforeAt = resolvePointer(before, op.path);
56519
56133
  let afterAt;
56520
56134
  if (replayOk) {
@@ -56559,12 +56173,473 @@ function resolvePointer(doc, pointer) {
56559
56173
  return cur;
56560
56174
  }
56561
56175
 
56176
+ // src/tools/paginate.ts
56177
+ function extractNextToken(data) {
56178
+ if (data && typeof data === "object" && "NextToken" in data) {
56179
+ const token = data.NextToken;
56180
+ if (typeof token === "string" && token.length > 0) return token;
56181
+ }
56182
+ return null;
56183
+ }
56184
+ function wrapQueryForPagination(userQuery) {
56185
+ return `{NextToken: NextToken, items: ${userQuery}}`;
56186
+ }
56187
+ var paginateTools = [
56188
+ {
56189
+ name: "aws_paginate",
56190
+ description: "Fetch one page of a paginated AWS list/describe operation. Identical to aws_call plus `maxItems` (page size) and `startingToken` (resume cursor). When `query` is supplied it is wrapped server-side as {NextToken, items: <query>} so pagination survives a projection that would otherwise drop NextToken; the handler unwraps `items` before returning. 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.",
56191
+ annotations: {
56192
+ title: "Fetch one page of a paginated AWS operation",
56193
+ readOnlyHint: true,
56194
+ destructiveHint: false,
56195
+ idempotentHint: true,
56196
+ openWorldHint: true
56197
+ },
56198
+ inputSchema: external_exports3.object({
56199
+ service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', 'logs', etc."),
56200
+ operation: external_exports3.string().describe("Paginated operation: 'list-objects-v2', 'describe-instances', 'list-roles', etc."),
56201
+ params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) passed via --cli-input-json."),
56202
+ query: external_exports3.string().optional().describe(
56203
+ "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."
56204
+ ),
56205
+ maxItems: external_exports3.number().int().positive().max(1e4).optional().describe("Items per page (1-10000). Default 100. Lower this if hitting the 5 MB output cap."),
56206
+ startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
56207
+ profile: external_exports3.string().optional().describe("Override session profile for this call."),
56208
+ region: external_exports3.string().optional().describe("Override session region for this call."),
56209
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
56210
+ }),
56211
+ handler: async (input) => {
56212
+ const i = input;
56213
+ if (i.startingToken !== void 0) {
56214
+ const stErr = validateOpaqueToken(i.startingToken, "startingToken");
56215
+ if (stErr) return { ok: false, error: stErr };
56216
+ }
56217
+ const maxItems = Math.min(Math.max(1, i.maxItems ?? 100), 1e4);
56218
+ const extraFlags = ["--max-items", String(maxItems)];
56219
+ if (i.startingToken) {
56220
+ extraFlags.push("--starting-token", i.startingToken);
56221
+ }
56222
+ const userQuery = i.query?.trim();
56223
+ const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
56224
+ const result = await runAwsCall({
56225
+ service: i.service,
56226
+ operation: i.operation,
56227
+ params: i.params,
56228
+ query: queryWrapped,
56229
+ profile: i.profile,
56230
+ region: i.region,
56231
+ outputFormat: "json",
56232
+ timeoutMs: i.timeoutMs,
56233
+ extraFlags
56234
+ });
56235
+ if (!result.ok) {
56236
+ return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56237
+ }
56238
+ let resultBody;
56239
+ let nextToken;
56240
+ if (queryWrapped) {
56241
+ const wrapped = result.data ?? {};
56242
+ nextToken = extractNextToken(wrapped);
56243
+ resultBody = wrapped.items ?? null;
56244
+ } else {
56245
+ nextToken = extractNextToken(result.data);
56246
+ resultBody = result.data;
56247
+ }
56248
+ return {
56249
+ ok: true,
56250
+ data: {
56251
+ command: result.command,
56252
+ result: resultBody,
56253
+ nextToken,
56254
+ hasMore: nextToken !== null
56255
+ }
56256
+ };
56257
+ }
56258
+ }
56259
+ ];
56260
+
56261
+ // src/tools/metrics.ts
56262
+ var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
56263
+ var EXTENDED_STAT_RE = /^((p|tm|tc|wm|pr|ts)(\d{1,3}(\.\d{1,3})?)?|iqm)$/i;
56264
+ function isValidStatistic(s) {
56265
+ const lower = s.toLowerCase();
56266
+ if (SIMPLE_STATS.some((stat) => stat.toLowerCase() === lower)) return true;
56267
+ return EXTENDED_STAT_RE.test(s);
56268
+ }
56269
+ function canonicalizeStatistic(s) {
56270
+ const lower = s.toLowerCase();
56271
+ for (const stat of SIMPLE_STATS) {
56272
+ if (stat.toLowerCase() === lower) return stat;
56273
+ }
56274
+ if (EXTENDED_STAT_RE.test(s)) return lower;
56275
+ return s;
56276
+ }
56277
+ var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
56278
+ var MAX_QUERIES = 100;
56279
+ var CLOUDWATCH_MAX_DATAPOINTS = 100800;
56280
+ var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
56281
+ var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
56282
+ var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
56283
+ function pickAutoPeriodSeconds(startMs, endMs) {
56284
+ const rangeMs = Math.max(0, endMs - startMs);
56285
+ if (rangeMs <= PERIOD_3H_MS) return 60;
56286
+ if (rangeMs <= PERIOD_24H_MS) return 300;
56287
+ if (rangeMs <= PERIOD_15D_MS) return 900;
56288
+ return 3600;
56289
+ }
56290
+ var RELATIVE_TIME_RE = /^\d+[smhdw]$/;
56291
+ var UNIT_MS = {
56292
+ s: 1e3,
56293
+ m: 60 * 1e3,
56294
+ h: 60 * 60 * 1e3,
56295
+ d: 24 * 60 * 60 * 1e3,
56296
+ w: 7 * 24 * 60 * 60 * 1e3
56297
+ };
56298
+ function resolveTime(input, now) {
56299
+ if (input === "now") return new Date(now);
56300
+ const rel = input.match(RELATIVE_TIME_RE);
56301
+ if (rel) {
56302
+ const num = Number(input.slice(0, -1));
56303
+ const unit = input.slice(-1);
56304
+ const ms = UNIT_MS[unit];
56305
+ if (!ms || !Number.isFinite(num)) return null;
56306
+ return new Date(now - num * ms);
56307
+ }
56308
+ const t = new Date(input);
56309
+ if (Number.isNaN(t.getTime())) return null;
56310
+ return t;
56311
+ }
56312
+ function buildMetricDataQueries(inputs, autoPeriod) {
56313
+ return inputs.map((q) => {
56314
+ const base = { Id: q.id };
56315
+ if (q.label !== void 0) base.Label = q.label;
56316
+ if (q.returnData !== void 0) base.ReturnData = q.returnData;
56317
+ if (q.expression !== void 0) {
56318
+ base.Expression = q.expression;
56319
+ if (q.period !== void 0) base.Period = q.period;
56320
+ return base;
56321
+ }
56322
+ const dimEntries = q.dimensions ? Object.entries(q.dimensions) : [];
56323
+ const stat = {
56324
+ Metric: {
56325
+ Namespace: q.namespace,
56326
+ MetricName: q.metricName,
56327
+ ...dimEntries.length > 0 ? { Dimensions: dimEntries.map(([Name, Value]) => ({ Name, Value })) } : {}
56328
+ },
56329
+ Period: q.period ?? autoPeriod,
56330
+ Stat: q.statistic !== void 0 ? canonicalizeStatistic(q.statistic) : "Average"
56331
+ };
56332
+ if (q.unit !== void 0) stat.Unit = q.unit;
56333
+ base.MetricStat = stat;
56334
+ return base;
56335
+ });
56336
+ }
56337
+ var metricsTools = [
56338
+ {
56339
+ name: "aws_metrics_query",
56340
+ 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.",
56341
+ annotations: {
56342
+ title: "Query CloudWatch metrics (GetMetricData)",
56343
+ readOnlyHint: true,
56344
+ destructiveHint: false,
56345
+ idempotentHint: true,
56346
+ openWorldHint: true
56347
+ },
56348
+ inputSchema: external_exports3.object({
56349
+ queries: external_exports3.array(
56350
+ external_exports3.object({
56351
+ id: external_exports3.string().regex(QUERY_ID_RE, "id must match /^[a-z][A-Za-z0-9_]*$/ (CloudWatch's MetricDataQuery.Id contract)"),
56352
+ namespace: external_exports3.string().min(1).optional().describe("AWS metric namespace, e.g. 'AWS/Lambda', 'AWS/EC2'. Required unless `expression` is set."),
56353
+ metricName: external_exports3.string().min(1).optional().describe("Metric name, e.g. 'Invocations', 'CPUUtilization'. Required unless `expression` is set."),
56354
+ dimensions: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("Dimension Name -> Value map, e.g. {FunctionName: 'my-fn'}."),
56355
+ statistic: external_exports3.string().optional().describe(
56356
+ "Statistic: Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like 'p99', 'p99.9', 'tm95'. Default 'Average'."
56357
+ ),
56358
+ period: external_exports3.number().int().positive().optional().describe("Period in seconds. Defaults to an auto-pick from the time range (60s/300s/900s/3600s)."),
56359
+ expression: external_exports3.string().min(1).optional().describe(
56360
+ `CloudWatch metric math expression, e.g. 'SUM([m1, m2])' or 'AVG(METRICS("AWS/Lambda"))'. Mutually exclusive with namespace/metricName/dimensions. Validated server-side by CloudWatch; malformed values surface as a downstream ValidationError rather than a local rejection.`
56361
+ ),
56362
+ label: external_exports3.string().optional().describe("Human-readable label for the series in the response."),
56363
+ returnData: external_exports3.boolean().optional().describe(
56364
+ "Set false to compute this query but not return its data (useful for intermediate values in expressions). Default true."
56365
+ ),
56366
+ unit: external_exports3.string().optional().describe(
56367
+ "Restrict to a specific Unit (e.g. 'Seconds', 'Bytes'). Default: no filter. Only meaningful on metric-stat queries. Validated server-side by CloudWatch; malformed values surface as a downstream ValidationError rather than a local rejection."
56368
+ )
56369
+ })
56370
+ ).min(1).max(MAX_QUERIES).describe(`1-${MAX_QUERIES} queries. Each is either a metric-stat (namespace + metricName) or an expression.`),
56371
+ startTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand ('15m', '1h', '1d', '1w'). Default '1h' (one hour ago)."),
56372
+ endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
56373
+ scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
56374
+ maxDataPoints: external_exports3.number().int().positive().optional().describe(
56375
+ "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."
56376
+ ),
56377
+ nextToken: external_exports3.string().optional().describe(
56378
+ "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`."
56379
+ ),
56380
+ profile: external_exports3.string().optional().describe("Override session profile for this call."),
56381
+ region: external_exports3.string().optional().describe("Override session region for this call."),
56382
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000 (60s).")
56383
+ }),
56384
+ handler: async (input) => {
56385
+ const i = input;
56386
+ const seenIds = /* @__PURE__ */ new Map();
56387
+ for (let qi = 0; qi < i.queries.length; qi++) {
56388
+ const q = i.queries[qi];
56389
+ const firstIdx = seenIds.get(q.id);
56390
+ if (firstIdx !== void 0) {
56391
+ return {
56392
+ ok: false,
56393
+ error: `Duplicate query id '${q.id}' at queries[${qi}]; first seen at queries[${firstIdx}]. Each MetricDataQuery.Id must be unique in a batch.`
56394
+ };
56395
+ }
56396
+ seenIds.set(q.id, qi);
56397
+ const hasMetricStat = q.namespace !== void 0 || q.metricName !== void 0 || q.dimensions !== void 0;
56398
+ const hasExpression = q.expression !== void 0;
56399
+ if (hasMetricStat && hasExpression) {
56400
+ return {
56401
+ ok: false,
56402
+ error: `Query '${q.id}' mixes metric-stat fields (namespace/metricName/dimensions) with 'expression'. Pick one shape per query.`
56403
+ };
56404
+ }
56405
+ if (!hasMetricStat && !hasExpression) {
56406
+ return {
56407
+ ok: false,
56408
+ error: `Query '${q.id}' has neither metric-stat (namespace+metricName) nor 'expression'. One is required.`
56409
+ };
56410
+ }
56411
+ if (hasMetricStat && (q.namespace === void 0 || q.metricName === void 0)) {
56412
+ return {
56413
+ ok: false,
56414
+ error: `Query '${q.id}' must include BOTH 'namespace' and 'metricName' (or use 'expression' instead).`
56415
+ };
56416
+ }
56417
+ if (q.statistic !== void 0 && !isValidStatistic(q.statistic)) {
56418
+ return {
56419
+ ok: false,
56420
+ error: `Query '${q.id}' has invalid statistic '${q.statistic}'. Use Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like p99 / p99.9 / tm95.`
56421
+ };
56422
+ }
56423
+ }
56424
+ const now = Date.now();
56425
+ const startStr = i.startTime ?? "1h";
56426
+ const endStr = i.endTime ?? "now";
56427
+ const startDate = resolveTime(startStr, now);
56428
+ const endDate = resolveTime(endStr, now);
56429
+ if (!startDate) {
56430
+ return {
56431
+ ok: false,
56432
+ error: `Invalid startTime '${startStr}'. Use ISO 8601 (e.g. '2026-05-16T10:00:00Z') or relative shorthand (e.g. '1h', '15m', '1d').`
56433
+ };
56434
+ }
56435
+ if (!endDate) {
56436
+ return {
56437
+ ok: false,
56438
+ error: `Invalid endTime '${endStr}'. Use ISO 8601 or relative shorthand, or 'now' for the current moment.`
56439
+ };
56440
+ }
56441
+ if (endDate.getTime() <= startDate.getTime()) {
56442
+ return {
56443
+ ok: false,
56444
+ error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
56445
+ };
56446
+ }
56447
+ const rangeSeconds = (endDate.getTime() - startDate.getTime()) / 1e3;
56448
+ for (const q of i.queries) {
56449
+ if (q.period === void 0) continue;
56450
+ if (q.period <= 0 || q.period % 60 !== 0) {
56451
+ return {
56452
+ ok: false,
56453
+ error: `Query '${q.id}' has invalid period ${q.period}. CloudWatch requires period to be a positive multiple of 60 (seconds).`
56454
+ };
56455
+ }
56456
+ const datapoints = Math.ceil(rangeSeconds / q.period);
56457
+ if (datapoints > CLOUDWATCH_MAX_DATAPOINTS) {
56458
+ return {
56459
+ ok: false,
56460
+ 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.`
56461
+ };
56462
+ }
56463
+ }
56464
+ const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
56465
+ const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
56466
+ const params = {
56467
+ MetricDataQueries: metricDataQueries,
56468
+ StartTime: startDate.toISOString(),
56469
+ EndTime: endDate.toISOString(),
56470
+ ScanBy: i.scanBy ?? "TimestampDescending"
56471
+ };
56472
+ if (i.maxDataPoints !== void 0) params.MaxDatapoints = i.maxDataPoints;
56473
+ if (i.nextToken !== void 0) params.NextToken = i.nextToken;
56474
+ const effectiveProfile = i.profile ?? getProfile();
56475
+ const effectiveRegion = i.region ?? getRegion();
56476
+ const result = await runAwsCall({
56477
+ service: "cloudwatch",
56478
+ operation: "get-metric-data",
56479
+ profile: i.profile,
56480
+ region: i.region,
56481
+ timeoutMs: i.timeoutMs,
56482
+ outputFormat: "json",
56483
+ params
56484
+ });
56485
+ if (!result.ok) {
56486
+ return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56487
+ }
56488
+ const raw = result.data ?? {};
56489
+ const queryById = new Map(i.queries.map((q) => [q.id, q]));
56490
+ const series = (raw.MetricDataResults ?? []).map((r) => {
56491
+ const q = queryById.get(r.Id ?? "");
56492
+ const effectivePeriod = q?.period ?? (q && q.expression === void 0 ? periodSeconds : void 0);
56493
+ return {
56494
+ id: r.Id ?? "",
56495
+ ...r.Label !== void 0 ? { label: r.Label } : {},
56496
+ timestamps: r.Timestamps ?? [],
56497
+ values: r.Values ?? [],
56498
+ ...effectivePeriod !== void 0 ? { period: effectivePeriod } : {},
56499
+ ...r.StatusCode !== void 0 ? { statusCode: r.StatusCode } : {}
56500
+ };
56501
+ });
56502
+ const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
56503
+ code: m.Code,
56504
+ value: m.Value
56505
+ }));
56506
+ const nextToken = extractNextToken(raw);
56507
+ return {
56508
+ ok: true,
56509
+ data: {
56510
+ command: result.command,
56511
+ profile: effectiveProfile,
56512
+ region: effectiveRegion,
56513
+ startTime: startDate.toISOString(),
56514
+ endTime: endDate.toISOString(),
56515
+ periodSeconds,
56516
+ series,
56517
+ nextToken,
56518
+ hasMore: nextToken !== null,
56519
+ ...messages && messages.length > 0 ? { messages } : {}
56520
+ }
56521
+ };
56522
+ }
56523
+ }
56524
+ ];
56525
+
56526
+ // src/tools/multi-region.ts
56527
+ var DEFAULT_CONCURRENCY = 8;
56528
+ var MAX_CONCURRENCY = 32;
56529
+ var MAX_REGIONS = 32;
56530
+ async function runWithConcurrency(inputs, concurrency, fn) {
56531
+ const results = new Array(inputs.length);
56532
+ let next = 0;
56533
+ const worker = async () => {
56534
+ while (true) {
56535
+ const i = next++;
56536
+ if (i >= inputs.length) return;
56537
+ results[i] = await fn(inputs[i], i);
56538
+ }
56539
+ };
56540
+ const workerCount = Math.min(concurrency, inputs.length);
56541
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
56542
+ return results;
56543
+ }
56544
+ var multiRegionTools = [
56545
+ {
56546
+ name: "aws_multi_region",
56547
+ description: "Run the same AWS API operation across multiple regions in parallel. Same shape as aws_call (service, operation, params?, query?, outputFormat?, timeoutMs?) but takes `regions: string[]` instead of `region`. Returns an array of `{region, ok, data?, command?, error?, errorKind?}` -- partial failure is expected (services aren't everywhere, perms may be region-scoped). Duplicate regions in the input are collapsed (first occurrence wins), so `results.length` may be less than `regions.length`; use the returned `regionCount` for the actual count run. Use for fleet-wide reads: 'describe-instances across all our regions', 'list buckets in every region', 'check IAM password policy everywhere'.",
56548
+ annotations: {
56549
+ title: "Run an AWS operation across multiple regions in parallel",
56550
+ // The operation can be anything -- we conservatively annotate as not
56551
+ // read-only / not destructive. The caller chooses what to invoke.
56552
+ readOnlyHint: false,
56553
+ destructiveHint: false,
56554
+ idempotentHint: false,
56555
+ openWorldHint: true
56556
+ },
56557
+ inputSchema: external_exports3.object({
56558
+ service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
56559
+ operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
56560
+ regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
56561
+ `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).`
56562
+ ),
56563
+ params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
56564
+ query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
56565
+ outputFormat: external_exports3.enum(["json", "text", "table", "yaml"]).optional().describe("Output format. Default 'json'."),
56566
+ profile: external_exports3.string().optional().describe("Override session profile for the batch."),
56567
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in ms applied PER region. Default 60000."),
56568
+ concurrency: external_exports3.number().int().positive().max(MAX_CONCURRENCY).optional().describe(`Max regions in flight at once (1-${MAX_CONCURRENCY}). Default ${DEFAULT_CONCURRENCY}.`)
56569
+ }),
56570
+ handler: async (input) => {
56571
+ const i = input;
56572
+ const seen = /* @__PURE__ */ new Set();
56573
+ const regions = [];
56574
+ for (const r of i.regions) {
56575
+ if (!seen.has(r)) {
56576
+ seen.add(r);
56577
+ regions.push(r);
56578
+ }
56579
+ }
56580
+ const concurrency = i.concurrency ?? DEFAULT_CONCURRENCY;
56581
+ const results = await runWithConcurrency(regions, concurrency, async (region) => {
56582
+ try {
56583
+ if (!isValidRegionName(region)) {
56584
+ return {
56585
+ region,
56586
+ ok: false,
56587
+ error: `Invalid region '${region}'. Must match ${REGION_NAME_RE} (e.g. 'us-east-1').`,
56588
+ errorKind: "bad_input"
56589
+ };
56590
+ }
56591
+ const r = await runAwsCall({
56592
+ service: i.service,
56593
+ operation: i.operation,
56594
+ params: i.params,
56595
+ query: i.query,
56596
+ profile: i.profile,
56597
+ region,
56598
+ outputFormat: i.outputFormat,
56599
+ timeoutMs: i.timeoutMs
56600
+ });
56601
+ if (!r.ok) {
56602
+ return {
56603
+ region,
56604
+ ok: false,
56605
+ command: r.command,
56606
+ error: r.error,
56607
+ errorKind: r.kind
56608
+ };
56609
+ }
56610
+ return { region, ok: true, command: r.command, data: r.data };
56611
+ } catch (err) {
56612
+ return {
56613
+ region,
56614
+ ok: false,
56615
+ error: err instanceof Error ? err.message : String(err),
56616
+ errorKind: "unexpected"
56617
+ };
56618
+ }
56619
+ });
56620
+ const okCount = results.filter((r) => r.ok).length;
56621
+ const errCount = results.length - okCount;
56622
+ return {
56623
+ ok: true,
56624
+ data: {
56625
+ service: i.service,
56626
+ operation: i.operation,
56627
+ regionCount: regions.length,
56628
+ okCount,
56629
+ errorCount: errCount,
56630
+ results
56631
+ }
56632
+ };
56633
+ }
56634
+ }
56635
+ ];
56636
+
56562
56637
  // src/tools/script.ts
56563
56638
  import { createContext, runInContext } from "node:vm";
56564
56639
  var DEFAULT_TIMEOUT_MS2 = 6e4;
56565
56640
  var MAX_TIMEOUT_MS = 5 * 6e4;
56566
56641
  var MAX_LOG_LINES = 500;
56567
- var MAX_LOG_LINE_BYTES = 4 * 1024;
56642
+ var MAX_LOG_LINE_CHARS = 4 * 1024;
56568
56643
  var DEFAULT_MAX_PAGES = 50;
56569
56644
  var MAX_PAGES_HARD_CAP = 1e3;
56570
56645
  function findTool(name, source) {
@@ -56664,7 +56739,7 @@ async function runScript(opts, handlers = defaultScriptHandlers()) {
56664
56739
  }
56665
56740
  }
56666
56741
  }).join(" ");
56667
- const capped = text.length > MAX_LOG_LINE_BYTES ? `${text.slice(0, MAX_LOG_LINE_BYTES)}... [line truncated]` : text;
56742
+ const capped = text.length > MAX_LOG_LINE_CHARS ? `${text.slice(0, MAX_LOG_LINE_CHARS)}... [line truncated]` : text;
56668
56743
  logs.push(`[${level}] ${capped}`);
56669
56744
  };
56670
56745
  const ctx = createContext(
@@ -56797,9 +56872,11 @@ var scriptTools = [
56797
56872
  annotations: {
56798
56873
  title: "Run a JS snippet that orchestrates AWS tool calls",
56799
56874
  // The script may invoke destructive tools (resource.create/update/delete)
56800
- // so we conservatively annotate as non-read-only.
56875
+ // so annotate the worst case honestly: non-read-only AND destructive.
56876
+ // Cautious clients may confirm read-only scripts too -- acceptable cost;
56877
+ // an unflagged delete is not.
56801
56878
  readOnlyHint: false,
56802
- destructiveHint: false,
56879
+ destructiveHint: true,
56803
56880
  idempotentHint: false,
56804
56881
  openWorldHint: true
56805
56882
  },
@@ -56865,7 +56942,7 @@ var sessionTools = [
56865
56942
  if (!isValidProfileName(trimmed)) {
56866
56943
  return {
56867
56944
  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.`
56945
+ error: `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='); no whitespace or shell metacharacters.`
56869
56946
  };
56870
56947
  }
56871
56948
  }
@@ -56952,8 +57029,18 @@ function errorToMcpResult(err, toolName) {
56952
57029
  isError: true
56953
57030
  };
56954
57031
  }
56955
- var version2 = true ? "1.4.1" : (await null).createRequire(import.meta.url)("../package.json").version;
56956
- var isEntryPoint = (() => {
57032
+ var version2 = true ? "1.5.2" : (await null).createRequire(import.meta.url)("../package.json").version;
57033
+ var isSeaBinary = (() => {
57034
+ try {
57035
+ const base = import.meta.url || pathToFileURL(process.execPath).href;
57036
+ const require2 = createRequire(base);
57037
+ const sea = require2("node:sea");
57038
+ return typeof sea.isSea === "function" && sea.isSea() === true;
57039
+ } catch {
57040
+ return false;
57041
+ }
57042
+ })();
57043
+ var isEntryPoint = isSeaBinary || (() => {
56957
57044
  const entry = process.argv[1];
56958
57045
  if (!entry) return false;
56959
57046
  return import.meta.url === pathToFileURL(entry).href;
@@ -56993,8 +57080,13 @@ if (isEntryPoint) {
56993
57080
  });
56994
57081
  }
56995
57082
  const transport = new StdioServerTransport();
56996
- await server.connect(transport);
56997
- console.error(`@yawlabs/aws-mcp v${version2} ready (${allTools.length} tools)`);
57083
+ server.connect(transport).then(() => {
57084
+ console.error(`@yawlabs/aws-mcp v${version2} ready (${allTools.length} tools)`);
57085
+ }).catch((err) => {
57086
+ process.stderr.write(`aws-mcp: ${err instanceof Error ? err.message : String(err)}
57087
+ `);
57088
+ process.exit(1);
57089
+ });
56998
57090
  }
56999
57091
  export {
57000
57092
  allTools,