@yawlabs/aws-mcp 1.4.1 → 1.5.0

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
@@ -53507,7 +53507,7 @@ function setProfile(name) {
53507
53507
  const trimmed = name.trim();
53508
53508
  if (!isValidProfileName(trimmed)) {
53509
53509
  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.`
53510
+ `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
53511
  );
53512
53512
  }
53513
53513
  sessionProfile = trimmed;
@@ -53546,9 +53546,10 @@ var MAX_ERROR_MSG_BYTES = 8 * 1024;
53546
53546
  var SAFE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
53547
53547
  function redactDisplayArgs(args) {
53548
53548
  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}>`;
53549
+ for (let i = 0; i < out.length - 1; i++) {
53550
+ if (out[i] === "--cli-input-json") {
53551
+ out[i + 1] = `<redacted len=${out[i + 1].length}>`;
53552
+ }
53552
53553
  }
53553
53554
  return out;
53554
53555
  }
@@ -53612,7 +53613,7 @@ function runAwsCall(opts) {
53612
53613
  return Promise.resolve({
53613
53614
  ok: false,
53614
53615
  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.`
53616
+ 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
53617
  });
53617
53618
  }
53618
53619
  if (!isValidRegionName(region)) {
@@ -53642,6 +53643,13 @@ function runAwsCall(opts) {
53642
53643
  region
53643
53644
  ];
53644
53645
  if (opts.query !== void 0 && opts.query.trim().length > 0) {
53646
+ if (opts.query.length > 2048) {
53647
+ return Promise.resolve({
53648
+ ok: false,
53649
+ kind: "bad_input",
53650
+ error: `query expression too long (${opts.query.length} chars; max 2048). Simplify the JMESPath expression.`
53651
+ });
53652
+ }
53645
53653
  args.push("--query", opts.query);
53646
53654
  }
53647
53655
  if (opts.params !== void 0 && Object.keys(opts.params).length > 0) {
@@ -53973,7 +53981,7 @@ function resolveTargetProfile(input) {
53973
53981
  if (input.targetProfile) {
53974
53982
  return input.targetProfile.startsWith("mcp-") ? input.targetProfile : `mcp-${input.targetProfile}`;
53975
53983
  }
53976
- return `mcp-${input.sessionName}`;
53984
+ return input.sessionName.startsWith("mcp-") ? input.sessionName : `mcp-${input.sessionName}`;
53977
53985
  }
53978
53986
  var assumeTools = [
53979
53987
  {
@@ -53987,7 +53995,10 @@ var assumeTools = [
53987
53995
  openWorldHint: true
53988
53996
  },
53989
53997
  inputSchema: external_exports3.object({
53990
- roleArn: external_exports3.string().describe("Target role ARN, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'."),
53998
+ roleArn: external_exports3.string().regex(
53999
+ /^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/,
54000
+ "roleArn must match arn:aws[partition]:iam::<12-digit-account>:role/<name>"
54001
+ ).describe("Target role ARN, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'."),
53991
54002
  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
54003
  durationSeconds: external_exports3.number().int().min(900).max(43200).optional().describe("Session duration in seconds (900-43200). Default 3600."),
53993
54004
  externalId: external_exports3.string().optional().describe("External ID (only required if the role's trust policy demands it)."),
@@ -54008,13 +54019,19 @@ var assumeTools = [
54008
54019
  if (!isValidProfileName(sourceProfile)) {
54009
54020
  return {
54010
54021
  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.`
54022
+ 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
54023
  };
54013
54024
  }
54014
54025
  if (!isValidProfileName(targetProfile)) {
54015
54026
  return {
54016
54027
  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.`
54028
+ 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.`
54029
+ };
54030
+ }
54031
+ if (!/^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/.test(i.roleArn)) {
54032
+ return {
54033
+ ok: false,
54034
+ 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
54035
  };
54019
54036
  }
54020
54037
  const params = {
@@ -54111,7 +54128,7 @@ function startSsoLogin(profile, opts = {}) {
54111
54128
  if (!isValidProfileName(profile)) {
54112
54129
  return Promise.resolve({
54113
54130
  ok: false,
54114
- error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='.`
54131
+ 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
54132
  });
54116
54133
  }
54117
54134
  const key = dedupeKey(profile, opts);
@@ -54157,6 +54174,7 @@ function doStartSsoLogin(profile, opts) {
54157
54174
  let codeSeen = null;
54158
54175
  let settled = false;
54159
54176
  let registeredSession = null;
54177
+ let exitResult = null;
54160
54178
  let completionResolve;
54161
54179
  const completion = new Promise((res) => {
54162
54180
  completionResolve = res;
@@ -54254,14 +54272,17 @@ ${stderrBuf}` : "");
54254
54272
  rawOutput
54255
54273
  };
54256
54274
  }
54275
+ exitResult = result;
54257
54276
  completionResolve(result);
54277
+ });
54278
+ proc.on("close", () => {
54258
54279
  if (!settled) {
54259
54280
  settled = true;
54260
54281
  clearTimeout(urlTimeout);
54261
54282
  resolve({
54262
54283
  ok: false,
54263
- error: result.error ?? "aws sso login exited before printing a verification URL",
54264
- rawOutput: result.rawOutput
54284
+ error: exitResult?.error ?? "aws sso login exited before printing a verification URL",
54285
+ rawOutput: exitResult?.rawOutput
54265
54286
  });
54266
54287
  }
54267
54288
  });
@@ -54965,7 +54986,7 @@ function buildDocsTools(fetchImpl = fetch) {
54965
54986
  }),
54966
54987
  handler: async (input) => {
54967
54988
  const i = input;
54968
- const limit = i.limit ?? DEFAULT_SEARCH_LIMIT;
54989
+ const limit = Math.min(Math.max(1, i.limit ?? DEFAULT_SEARCH_LIMIT), MAX_SEARCH_LIMIT);
54969
54990
  let response;
54970
54991
  try {
54971
54992
  response = await fetchWithTimeout(
@@ -55035,7 +55056,7 @@ function buildDocsTools(fetchImpl = fetch) {
55035
55056
  openWorldHint: true
55036
55057
  },
55037
55058
  inputSchema: external_exports3.object({
55038
- url: external_exports3.string().min(1).describe(
55059
+ url: external_exports3.string().min(1).max(2048).describe(
55039
55060
  "AWS docs page URL: https://docs.aws.amazon.com/<...>.html. Usually from an aws_docs_search result."
55040
55061
  ),
55041
55062
  startIndex: external_exports3.number().int().min(0).optional().describe("Character offset to start from (for paginated reads). Default 0."),
@@ -55049,8 +55070,8 @@ function buildDocsTools(fetchImpl = fetch) {
55049
55070
  error: `Invalid url '${i.url}'. Must be an 'https://docs.aws.amazon.com/...html' page. Use aws_docs_search to find one.`
55050
55071
  };
55051
55072
  }
55052
- const startIndex = i.startIndex ?? 0;
55053
- const maxLength = i.maxLength ?? DEFAULT_MAX_LENGTH;
55073
+ const startIndex = Math.max(0, i.startIndex ?? 0);
55074
+ const maxLength = Math.min(Math.max(1, i.maxLength ?? DEFAULT_MAX_LENGTH), MAX_MAX_LENGTH);
55054
55075
  let markdown = docCache.get(i.url);
55055
55076
  let cached2 = true;
55056
55077
  if (markdown === void 0) {
@@ -55165,7 +55186,7 @@ function parseSimulationResults(raw) {
55165
55186
  var iamSimulateTools = [
55166
55187
  {
55167
55188
  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.",
55189
+ 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
55190
  annotations: {
55170
55191
  title: "Simulate IAM permissions for a principal",
55171
55192
  readOnlyHint: true,
@@ -55175,13 +55196,13 @@ var iamSimulateTools = [
55175
55196
  },
55176
55197
  inputSchema: external_exports3.object({
55177
55198
  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'."
55199
+ "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
55200
  ),
55180
55201
  actions: external_exports3.array(external_exports3.string().min(1)).min(1).max(50).describe(
55181
55202
  "IAM action names to test, e.g. ['lambda:CreateFunction', 's3:GetObject']. 1-50 entries. Wildcards (e.g. 's3:*') are accepted."
55182
55203
  ),
55183
55204
  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?')."
55205
+ "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
55206
  ),
55186
55207
  contextEntries: external_exports3.array(
55187
55208
  external_exports3.object({
@@ -55251,13 +55272,14 @@ var iamSimulateTools = [
55251
55272
  const raw = result.data;
55252
55273
  const results = parseSimulationResults(raw?.EvaluationResults);
55253
55274
  const allowed = results.filter((r) => r.decision === "allowed").length;
55254
- const denied = results.length - allowed;
55275
+ const unknown2 = results.filter((r) => r.decision === "unknown").length;
55276
+ const denied = results.length - allowed - unknown2;
55255
55277
  return {
55256
55278
  ok: true,
55257
55279
  data: {
55258
55280
  command: result.command,
55259
55281
  principalArn: i.principalArn,
55260
- summary: { allowed, denied, total: results.length },
55282
+ summary: { allowed, denied, unknown: unknown2, total: results.length },
55261
55283
  results,
55262
55284
  evaluationResults: raw?.EvaluationResults ?? []
55263
55285
  }
@@ -55384,736 +55406,288 @@ var logsTools = [
55384
55406
  }
55385
55407
  ];
55386
55408
 
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;
55409
+ // src/tools/resource.ts
55410
+ var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
55411
+ function isValidIdentifier(id) {
55412
+ if (id.length === 0 || id.length > 2048) return false;
55413
+ if (id.startsWith("-")) return false;
55414
+ for (let i = 0; i < id.length; i++) {
55415
+ if (id.charCodeAt(i) < 32) return false;
55416
+ }
55417
+ return true;
55418
+ }
55419
+ function isValidOpaqueToken(token) {
55420
+ if (token.length === 0 || token.length > 128) return false;
55421
+ if (token.startsWith("-")) return false;
55422
+ for (let i = 0; i < token.length; i++) {
55423
+ if (token.charCodeAt(i) < 32) return false;
55424
+ }
55425
+ return true;
55426
+ }
55427
+ function parseResourceProperties(raw) {
55428
+ if (!raw || typeof raw !== "object") return { Properties: raw };
55429
+ const rec = raw;
55430
+ const identifier = typeof rec.Identifier === "string" ? rec.Identifier : void 0;
55431
+ const rawProps = rec.Properties;
55432
+ if (typeof rawProps !== "string") {
55433
+ return { Identifier: identifier, Properties: rawProps };
55434
+ }
55435
+ try {
55436
+ return { Identifier: identifier, Properties: JSON.parse(rawProps) };
55437
+ } catch {
55438
+ return { Identifier: identifier, Properties: rawProps, propertiesRaw: rawProps };
55439
+ }
55440
+ }
55441
+ function validateTypeName(typeName) {
55442
+ if (!TYPE_NAME_RE.test(typeName)) {
55443
+ return `Invalid typeName '${typeName}'. Must be '<Namespace>::<Service>::<Resource>' in PascalCase, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket'.`;
55392
55444
  }
55393
55445
  return null;
55394
55446
  }
55395
- function wrapQueryForPagination(userQuery) {
55396
- return `{NextToken: NextToken, items: ${userQuery}}`;
55447
+ function validateIdentifier(id) {
55448
+ if (!isValidIdentifier(id)) {
55449
+ const preview = id.length > 40 ? `${id.slice(0, 40)}...` : id;
55450
+ return `Invalid identifier '${preview}'. Must be 1-2048 chars, not start with '-', and contain no control characters.`;
55451
+ }
55452
+ return null;
55397
55453
  }
55398
- var paginateTools = [
55454
+ function validateOpaqueToken(token, fieldName) {
55455
+ if (!isValidOpaqueToken(token)) {
55456
+ return `Invalid ${fieldName}. Must be 1-128 chars, not start with '-', and contain no control characters.`;
55457
+ }
55458
+ return null;
55459
+ }
55460
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "FAILED", "CANCEL_COMPLETE"]);
55461
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
55462
+ var DEFAULT_MAX_WAIT_MS = 5 * 6e4;
55463
+ var MIN_POLL_INTERVAL_MS = 500;
55464
+ var MAX_POLL_INTERVAL_MS = 3e4;
55465
+ var MIN_MAX_WAIT_MS = 1e3;
55466
+ var MAX_MAX_WAIT_MS = 30 * 6e4;
55467
+ function extractProgressFields(progressEvent) {
55468
+ const pe = progressEvent && typeof progressEvent === "object" ? progressEvent : {};
55469
+ const str = (k) => {
55470
+ const v = pe[k];
55471
+ return typeof v === "string" && v.length > 0 ? v : null;
55472
+ };
55473
+ return {
55474
+ requestToken: str("RequestToken"),
55475
+ operationStatus: str("OperationStatus"),
55476
+ identifier: str("Identifier"),
55477
+ errorCode: str("ErrorCode"),
55478
+ statusMessage: str("StatusMessage"),
55479
+ retryAfter: str("RetryAfter")
55480
+ };
55481
+ }
55482
+ async function pollUntilTerminal(opts, awsCall = runAwsCall, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
55483
+ const start = Date.now();
55484
+ let attempts = 0;
55485
+ let lastEvent = null;
55486
+ let lastCommand = "";
55487
+ while (true) {
55488
+ attempts++;
55489
+ const result = await awsCall({
55490
+ service: "cloudcontrol",
55491
+ operation: "get-resource-request-status",
55492
+ profile: opts.profile,
55493
+ region: opts.region,
55494
+ timeoutMs: opts.timeoutMs,
55495
+ outputFormat: "json",
55496
+ extraFlags: ["--request-token", opts.requestToken]
55497
+ });
55498
+ if (!result.ok) {
55499
+ return {
55500
+ ok: false,
55501
+ progressEvent: lastEvent,
55502
+ command: result.command ?? lastCommand,
55503
+ attempts,
55504
+ elapsedMs: Date.now() - start,
55505
+ error: result.error,
55506
+ kind: result.kind,
55507
+ rawBody: result.rawStderr ?? result.rawStdout
55508
+ };
55509
+ }
55510
+ lastCommand = result.command;
55511
+ const raw = result.data;
55512
+ lastEvent = raw?.ProgressEvent ?? null;
55513
+ const status = lastEvent && typeof lastEvent.OperationStatus === "string" ? lastEvent.OperationStatus : null;
55514
+ if (status && TERMINAL_STATUSES.has(status)) {
55515
+ return { ok: true, progressEvent: lastEvent, command: lastCommand, attempts, elapsedMs: Date.now() - start };
55516
+ }
55517
+ const elapsed = Date.now() - start;
55518
+ if (elapsed >= opts.maxWaitMs) {
55519
+ return {
55520
+ ok: false,
55521
+ progressEvent: lastEvent,
55522
+ command: lastCommand,
55523
+ attempts,
55524
+ elapsedMs: elapsed,
55525
+ error: `Polled for ${Math.round(elapsed / 1e3)}s without reaching a terminal state (last status: ${status ?? "unknown"}). Increase maxWaitMs, or call aws_resource_status with requestToken='${opts.requestToken}' to keep checking.`
55526
+ };
55527
+ }
55528
+ let waitMs = opts.pollIntervalMs;
55529
+ const retryAfterRaw = lastEvent && typeof lastEvent.RetryAfter === "string" ? lastEvent.RetryAfter : null;
55530
+ if (retryAfterRaw) {
55531
+ const target = Date.parse(retryAfterRaw);
55532
+ if (!Number.isNaN(target)) {
55533
+ const ra = target - Date.now();
55534
+ if (ra > 0) waitMs = ra;
55535
+ }
55536
+ }
55537
+ waitMs = Math.min(waitMs, opts.maxWaitMs - elapsed);
55538
+ if (waitMs > 0) await sleep(waitMs);
55539
+ }
55540
+ }
55541
+ async function buildMutationResponse(initial, i) {
55542
+ const raw = initial.data;
55543
+ const progressEvent = raw?.ProgressEvent ?? null;
55544
+ const fields = extractProgressFields(progressEvent);
55545
+ const initialStatus = fields.operationStatus ?? "";
55546
+ const alreadyTerminal = TERMINAL_STATUSES.has(initialStatus);
55547
+ if (i.awaitCompletion && fields.requestToken && !alreadyTerminal) {
55548
+ const polled = await pollUntilTerminal({
55549
+ requestToken: fields.requestToken,
55550
+ profile: i.profile,
55551
+ region: i.region,
55552
+ timeoutMs: i.timeoutMs,
55553
+ pollIntervalMs: i.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
55554
+ maxWaitMs: i.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
55555
+ });
55556
+ if (!polled.ok) {
55557
+ if (polled.kind === "sso_expired" || polled.kind === "no_creds") {
55558
+ const useProfile = i.profile ?? getProfile();
55559
+ const reLoginHint = polled.kind === "sso_expired" ? `SSO session expired while awaiting completion. Call aws_login_start with profile='${useProfile}' to re-authenticate, then call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side.` : `No credentials available while awaiting completion. After fixing credentials for profile '${useProfile}', call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side. Underlying error: ${polled.error}`;
55560
+ return { ok: false, error: reLoginHint, rawBody: polled.rawBody };
55561
+ }
55562
+ return { ok: false, error: polled.error ?? "Poll failed", rawBody: polled.rawBody };
55563
+ }
55564
+ const finalFields = extractProgressFields(polled.progressEvent);
55565
+ return {
55566
+ ok: true,
55567
+ data: {
55568
+ command: polled.command,
55569
+ ...finalFields,
55570
+ progressEvent: polled.progressEvent,
55571
+ awaited: { attempts: polled.attempts, elapsedMs: polled.elapsedMs }
55572
+ }
55573
+ };
55574
+ }
55575
+ return {
55576
+ ok: true,
55577
+ data: { command: initial.command, ...fields, progressEvent }
55578
+ };
55579
+ }
55580
+ var baseFields = {
55581
+ profile: external_exports3.string().optional().describe("Override session profile for this call."),
55582
+ region: external_exports3.string().optional().describe("Override session region for this call."),
55583
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
55584
+ };
55585
+ var awaitFields = {
55586
+ awaitCompletion: external_exports3.boolean().optional().describe(
55587
+ "If true, poll get-resource-request-status until the operation reaches SUCCESS / FAILED / CANCEL_COMPLETE and return the final ProgressEvent. Default false (returns immediately with IN_PROGRESS, caller polls via aws_resource_status)."
55588
+ ),
55589
+ pollIntervalMs: external_exports3.number().int().min(MIN_POLL_INTERVAL_MS).max(MAX_POLL_INTERVAL_MS).optional().describe(
55590
+ `Poll interval in ms when awaitCompletion is true (range ${MIN_POLL_INTERVAL_MS}-${MAX_POLL_INTERVAL_MS}). Default ${DEFAULT_POLL_INTERVAL_MS}. ProgressEvent.RetryAfter overrides when CCAPI returns one.`
55591
+ ),
55592
+ maxWaitMs: external_exports3.number().int().min(MIN_MAX_WAIT_MS).max(MAX_MAX_WAIT_MS).optional().describe(
55593
+ `Maximum total wait in ms when awaitCompletion is true (range ${MIN_MAX_WAIT_MS}-${MAX_MAX_WAIT_MS}). Default ${DEFAULT_MAX_WAIT_MS}. On timeout, returns the last seen status with a hint to keep polling.`
55594
+ )
55595
+ };
55596
+ var resourceTools = [
55399
55597
  {
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.",
55598
+ name: "aws_resource_get",
55599
+ description: "Read a single AWS resource via Cloud Control API. Covers hundreds of resource types with a CloudFormation schema. `typeName` is '<Namespace>::<Service>::<Resource>' (e.g. 'AWS::Lambda::Function'); `identifier` is the primary key for that type (function name, bucket name, IAM role name, ARN, or composite id). Returns parsed Properties. For resources not covered by CCAPI or for data-plane operations, use aws_call.",
55402
55600
  annotations: {
55403
- title: "Fetch one page of a paginated AWS operation",
55601
+ title: "Get an AWS resource by type + identifier",
55404
55602
  readOnlyHint: true,
55405
55603
  destructiveHint: false,
55406
55604
  idempotentHint: true,
55407
55605
  openWorldHint: true
55408
55606
  },
55409
55607
  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.")
55608
+ typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket', 'AWS::IAM::Role'."),
55609
+ identifier: external_exports3.string().min(1).describe("Primary identifier for the resource (function name, bucket name, ARN, or composite id)."),
55610
+ ...baseFields
55421
55611
  }),
55422
55612
  handler: async (input) => {
55423
55613
  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;
55614
+ const tnErr = validateTypeName(i.typeName);
55615
+ if (tnErr) return { ok: false, error: tnErr };
55616
+ const idErr = validateIdentifier(i.identifier);
55617
+ if (idErr) return { ok: false, error: idErr };
55431
55618
  const result = await runAwsCall({
55432
- service: i.service,
55433
- operation: i.operation,
55434
- params: i.params,
55435
- query: queryWrapped,
55619
+ service: "cloudcontrol",
55620
+ operation: "get-resource",
55436
55621
  profile: i.profile,
55437
55622
  region: i.region,
55438
- outputFormat: "json",
55439
55623
  timeoutMs: i.timeoutMs,
55440
- extraFlags
55624
+ outputFormat: "json",
55625
+ extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
55441
55626
  });
55442
55627
  if (!result.ok) {
55443
55628
  return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
55444
55629
  }
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
- }
55630
+ const raw = result.data;
55631
+ const parsed = parseResourceProperties(raw?.ResourceDescription);
55455
55632
  return {
55456
55633
  ok: true,
55457
55634
  data: {
55458
55635
  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;
55636
+ typeName: raw?.TypeName ?? i.typeName,
55637
+ identifier: parsed.Identifier,
55638
+ properties: parsed.Properties,
55639
+ ...parsed.propertiesRaw ? { propertiesRaw: parsed.propertiesRaw } : {}
55640
+ }
55641
+ };
55528
55642
  }
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 = [
55643
+ },
55545
55644
  {
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.",
55645
+ name: "aws_resource_list",
55646
+ description: "List resources of a given type via Cloud Control API, paginated. Returns an array of {identifier, properties}, a `nextToken` (null when exhausted), and `hasMore`. Some types need parent identifiers (e.g. nested resources under a cluster); pass those as `resourceModel`.",
55548
55647
  annotations: {
55549
- title: "Query CloudWatch metrics (GetMetricData)",
55648
+ title: "List AWS resources of a type (paginated)",
55550
55649
  readOnlyHint: true,
55551
55650
  destructiveHint: false,
55552
55651
  idempotentHint: true,
55553
55652
  openWorldHint: true
55554
55653
  },
55555
55654
  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).")
55655
+ typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
55656
+ resourceModel: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Parent identifier properties for nested types, e.g. {ClusterArn: '...'}."),
55657
+ maxResults: external_exports3.number().int().positive().max(100).optional().describe("Page size (1-100). Default 100."),
55658
+ nextToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
55659
+ ...baseFields
55590
55660
  }),
55591
55661
  handler: async (input) => {
55592
55662
  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
- };
55663
+ const tnErr = validateTypeName(i.typeName);
55664
+ if (tnErr) return { ok: false, error: tnErr };
55665
+ if (i.nextToken !== void 0) {
55666
+ const ntErr = validateOpaqueToken(i.nextToken, "nextToken");
55667
+ if (ntErr) return { ok: false, error: ntErr };
55653
55668
  }
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
- }
55669
+ const extraFlags = ["--type-name", i.typeName, "--max-results", String(i.maxResults ?? 100)];
55670
+ if (i.nextToken) extraFlags.push("--next-token", i.nextToken);
55671
+ if (i.resourceModel && Object.keys(i.resourceModel).length > 0) {
55672
+ extraFlags.push("--resource-model", JSON.stringify(i.resourceModel));
55670
55673
  }
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
55674
  const result = await runAwsCall({
55684
- service: "cloudwatch",
55685
- operation: "get-metric-data",
55675
+ service: "cloudcontrol",
55676
+ operation: "list-resources",
55686
55677
  profile: i.profile,
55687
55678
  region: i.region,
55688
55679
  timeoutMs: i.timeoutMs,
55689
55680
  outputFormat: "json",
55690
- params
55681
+ extraFlags
55691
55682
  });
55692
55683
  if (!result.ok) {
55693
55684
  return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
55694
55685
  }
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
- // src/tools/resource.ts
55836
- var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
55837
- function isValidIdentifier(id) {
55838
- if (id.length === 0 || id.length > 2048) return false;
55839
- if (id.startsWith("-")) return false;
55840
- for (let i = 0; i < id.length; i++) {
55841
- if (id.charCodeAt(i) < 32) return false;
55842
- }
55843
- return true;
55844
- }
55845
- function isValidOpaqueToken(token) {
55846
- if (token.length === 0 || token.length > 128) return false;
55847
- if (token.startsWith("-")) return false;
55848
- for (let i = 0; i < token.length; i++) {
55849
- if (token.charCodeAt(i) < 32) return false;
55850
- }
55851
- return true;
55852
- }
55853
- function parseResourceProperties(raw) {
55854
- if (!raw || typeof raw !== "object") return { Properties: raw };
55855
- const rec = raw;
55856
- const identifier = typeof rec.Identifier === "string" ? rec.Identifier : void 0;
55857
- const rawProps = rec.Properties;
55858
- if (typeof rawProps !== "string") {
55859
- return { Identifier: identifier, Properties: rawProps };
55860
- }
55861
- try {
55862
- return { Identifier: identifier, Properties: JSON.parse(rawProps) };
55863
- } catch {
55864
- return { Identifier: identifier, Properties: rawProps, propertiesRaw: rawProps };
55865
- }
55866
- }
55867
- function validateTypeName(typeName) {
55868
- if (!TYPE_NAME_RE.test(typeName)) {
55869
- return `Invalid typeName '${typeName}'. Must be '<Namespace>::<Service>::<Resource>' in PascalCase, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket'.`;
55870
- }
55871
- return null;
55872
- }
55873
- function validateIdentifier(id) {
55874
- if (!isValidIdentifier(id)) {
55875
- const preview = id.length > 40 ? `${id.slice(0, 40)}...` : id;
55876
- return `Invalid identifier '${preview}'. Must be 1-2048 chars, not start with '-', and contain no control characters.`;
55877
- }
55878
- return null;
55879
- }
55880
- function validateOpaqueToken(token, fieldName) {
55881
- if (!isValidOpaqueToken(token)) {
55882
- return `Invalid ${fieldName}. Must be 1-128 chars, not start with '-', and contain no control characters.`;
55883
- }
55884
- return null;
55885
- }
55886
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "FAILED", "CANCEL_COMPLETE"]);
55887
- var DEFAULT_POLL_INTERVAL_MS = 2e3;
55888
- var DEFAULT_MAX_WAIT_MS = 5 * 6e4;
55889
- var MIN_POLL_INTERVAL_MS = 500;
55890
- var MAX_POLL_INTERVAL_MS = 3e4;
55891
- var MIN_MAX_WAIT_MS = 1e3;
55892
- var MAX_MAX_WAIT_MS = 30 * 6e4;
55893
- function extractProgressFields(progressEvent) {
55894
- const pe = progressEvent && typeof progressEvent === "object" ? progressEvent : {};
55895
- const str = (k) => {
55896
- const v = pe[k];
55897
- return typeof v === "string" && v.length > 0 ? v : null;
55898
- };
55899
- return {
55900
- requestToken: str("RequestToken"),
55901
- operationStatus: str("OperationStatus"),
55902
- identifier: str("Identifier"),
55903
- errorCode: str("ErrorCode"),
55904
- statusMessage: str("StatusMessage"),
55905
- retryAfter: str("RetryAfter")
55906
- };
55907
- }
55908
- async function pollUntilTerminal(opts, awsCall = runAwsCall, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
55909
- const start = Date.now();
55910
- let attempts = 0;
55911
- let lastEvent = null;
55912
- let lastCommand = "";
55913
- while (true) {
55914
- attempts++;
55915
- const result = await awsCall({
55916
- service: "cloudcontrol",
55917
- operation: "get-resource-request-status",
55918
- profile: opts.profile,
55919
- region: opts.region,
55920
- timeoutMs: opts.timeoutMs,
55921
- outputFormat: "json",
55922
- extraFlags: ["--request-token", opts.requestToken]
55923
- });
55924
- if (!result.ok) {
55925
- return {
55926
- ok: false,
55927
- progressEvent: lastEvent,
55928
- command: result.command ?? lastCommand,
55929
- attempts,
55930
- elapsedMs: Date.now() - start,
55931
- error: result.error,
55932
- kind: result.kind,
55933
- rawBody: result.rawStderr ?? result.rawStdout
55934
- };
55935
- }
55936
- lastCommand = result.command;
55937
- const raw = result.data;
55938
- lastEvent = raw?.ProgressEvent ?? null;
55939
- const status = lastEvent && typeof lastEvent.OperationStatus === "string" ? lastEvent.OperationStatus : null;
55940
- if (status && TERMINAL_STATUSES.has(status)) {
55941
- return { ok: true, progressEvent: lastEvent, command: lastCommand, attempts, elapsedMs: Date.now() - start };
55942
- }
55943
- const elapsed = Date.now() - start;
55944
- if (elapsed >= opts.maxWaitMs) {
55945
- return {
55946
- ok: false,
55947
- progressEvent: lastEvent,
55948
- command: lastCommand,
55949
- attempts,
55950
- elapsedMs: elapsed,
55951
- error: `Polled for ${Math.round(elapsed / 1e3)}s without reaching a terminal state (last status: ${status ?? "unknown"}). Increase maxWaitMs, or call aws_resource_status with requestToken='${opts.requestToken}' to keep checking.`
55952
- };
55953
- }
55954
- let waitMs = opts.pollIntervalMs;
55955
- const retryAfterRaw = lastEvent && typeof lastEvent.RetryAfter === "string" ? lastEvent.RetryAfter : null;
55956
- if (retryAfterRaw) {
55957
- const target = Date.parse(retryAfterRaw);
55958
- if (!Number.isNaN(target)) {
55959
- const ra = target - Date.now();
55960
- if (ra > 0) waitMs = ra;
55961
- }
55962
- }
55963
- waitMs = Math.min(waitMs, opts.maxWaitMs - elapsed);
55964
- if (waitMs > 0) await sleep(waitMs);
55965
- }
55966
- }
55967
- async function buildMutationResponse(initial, i) {
55968
- const raw = initial.data;
55969
- const progressEvent = raw?.ProgressEvent ?? null;
55970
- const fields = extractProgressFields(progressEvent);
55971
- const initialStatus = fields.operationStatus ?? "";
55972
- const alreadyTerminal = TERMINAL_STATUSES.has(initialStatus);
55973
- if (i.awaitCompletion && fields.requestToken && !alreadyTerminal) {
55974
- const polled = await pollUntilTerminal({
55975
- requestToken: fields.requestToken,
55976
- profile: i.profile,
55977
- region: i.region,
55978
- timeoutMs: i.timeoutMs,
55979
- pollIntervalMs: i.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
55980
- maxWaitMs: i.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
55981
- });
55982
- if (!polled.ok) {
55983
- if (polled.kind === "sso_expired" || polled.kind === "no_creds") {
55984
- const useProfile = i.profile ?? getProfile();
55985
- const reLoginHint = polled.kind === "sso_expired" ? `SSO session expired while awaiting completion. Call aws_login_start with profile='${useProfile}' to re-authenticate, then call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side.` : `No credentials available while awaiting completion. After fixing credentials for profile '${useProfile}', call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side. Underlying error: ${polled.error}`;
55986
- return { ok: false, error: reLoginHint, rawBody: polled.rawBody };
55987
- }
55988
- return { ok: false, error: polled.error ?? "Poll failed", rawBody: polled.rawBody };
55989
- }
55990
- const finalFields = extractProgressFields(polled.progressEvent);
55991
- return {
55992
- ok: true,
55993
- data: {
55994
- command: polled.command,
55995
- ...finalFields,
55996
- progressEvent: polled.progressEvent,
55997
- awaited: { attempts: polled.attempts, elapsedMs: polled.elapsedMs }
55998
- }
55999
- };
56000
- }
56001
- return {
56002
- ok: true,
56003
- data: { command: initial.command, ...fields, progressEvent }
56004
- };
56005
- }
56006
- var baseFields = {
56007
- profile: external_exports3.string().optional().describe("Override session profile for this call."),
56008
- region: external_exports3.string().optional().describe("Override session region for this call."),
56009
- timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
56010
- };
56011
- var awaitFields = {
56012
- awaitCompletion: external_exports3.boolean().optional().describe(
56013
- "If true, poll get-resource-request-status until the operation reaches SUCCESS / FAILED / CANCEL_COMPLETE and return the final ProgressEvent. Default false (returns immediately with IN_PROGRESS, caller polls via aws_resource_status)."
56014
- ),
56015
- pollIntervalMs: external_exports3.number().int().min(MIN_POLL_INTERVAL_MS).max(MAX_POLL_INTERVAL_MS).optional().describe(
56016
- `Poll interval in ms when awaitCompletion is true (range ${MIN_POLL_INTERVAL_MS}-${MAX_POLL_INTERVAL_MS}). Default ${DEFAULT_POLL_INTERVAL_MS}. ProgressEvent.RetryAfter overrides when CCAPI returns one.`
56017
- ),
56018
- maxWaitMs: external_exports3.number().int().min(MIN_MAX_WAIT_MS).max(MAX_MAX_WAIT_MS).optional().describe(
56019
- `Maximum total wait in ms when awaitCompletion is true (range ${MIN_MAX_WAIT_MS}-${MAX_MAX_WAIT_MS}). Default ${DEFAULT_MAX_WAIT_MS}. On timeout, returns the last seen status with a hint to keep polling.`
56020
- )
56021
- };
56022
- var resourceTools = [
56023
- {
56024
- name: "aws_resource_get",
56025
- description: "Read a single AWS resource via Cloud Control API. Covers hundreds of resource types with a CloudFormation schema. `typeName` is '<Namespace>::<Service>::<Resource>' (e.g. 'AWS::Lambda::Function'); `identifier` is the primary key for that type (function name, bucket name, IAM role name, ARN, or composite id). Returns parsed Properties. For resources not covered by CCAPI or for data-plane operations, use aws_call.",
56026
- annotations: {
56027
- title: "Get an AWS resource by type + identifier",
56028
- readOnlyHint: true,
56029
- destructiveHint: false,
56030
- idempotentHint: true,
56031
- openWorldHint: true
56032
- },
56033
- inputSchema: external_exports3.object({
56034
- typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket', 'AWS::IAM::Role'."),
56035
- identifier: external_exports3.string().min(1).describe("Primary identifier for the resource (function name, bucket name, ARN, or composite id)."),
56036
- ...baseFields
56037
- }),
56038
- handler: async (input) => {
56039
- const i = input;
56040
- const tnErr = validateTypeName(i.typeName);
56041
- if (tnErr) return { ok: false, error: tnErr };
56042
- const idErr = validateIdentifier(i.identifier);
56043
- if (idErr) return { ok: false, error: idErr };
56044
- const result = await runAwsCall({
56045
- service: "cloudcontrol",
56046
- operation: "get-resource",
56047
- profile: i.profile,
56048
- region: i.region,
56049
- timeoutMs: i.timeoutMs,
56050
- outputFormat: "json",
56051
- extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
56052
- });
56053
- if (!result.ok) {
56054
- return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56055
- }
56056
- const raw = result.data;
56057
- const parsed = parseResourceProperties(raw?.ResourceDescription);
56058
- return {
56059
- ok: true,
56060
- data: {
56061
- command: result.command,
56062
- typeName: raw?.TypeName ?? i.typeName,
56063
- identifier: parsed.Identifier,
56064
- properties: parsed.Properties,
56065
- ...parsed.propertiesRaw ? { propertiesRaw: parsed.propertiesRaw } : {}
56066
- }
56067
- };
56068
- }
56069
- },
56070
- {
56071
- name: "aws_resource_list",
56072
- description: "List resources of a given type via Cloud Control API, paginated. Returns an array of {identifier, properties}, a `nextToken` (null when exhausted), and `hasMore`. Some types need parent identifiers (e.g. nested resources under a cluster); pass those as `resourceModel`.",
56073
- annotations: {
56074
- title: "List AWS resources of a type (paginated)",
56075
- readOnlyHint: true,
56076
- destructiveHint: false,
56077
- idempotentHint: true,
56078
- openWorldHint: true
56079
- },
56080
- inputSchema: external_exports3.object({
56081
- typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
56082
- resourceModel: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Parent identifier properties for nested types, e.g. {ClusterArn: '...'}."),
56083
- maxResults: external_exports3.number().int().positive().max(100).optional().describe("Page size (1-100). Default 100."),
56084
- nextToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
56085
- ...baseFields
56086
- }),
56087
- handler: async (input) => {
56088
- const i = input;
56089
- const tnErr = validateTypeName(i.typeName);
56090
- if (tnErr) return { ok: false, error: tnErr };
56091
- if (i.nextToken !== void 0) {
56092
- const ntErr = validateOpaqueToken(i.nextToken, "nextToken");
56093
- if (ntErr) return { ok: false, error: ntErr };
56094
- }
56095
- const extraFlags = ["--type-name", i.typeName, "--max-results", String(i.maxResults ?? 100)];
56096
- if (i.nextToken) extraFlags.push("--next-token", i.nextToken);
56097
- if (i.resourceModel && Object.keys(i.resourceModel).length > 0) {
56098
- extraFlags.push("--resource-model", JSON.stringify(i.resourceModel));
56099
- }
56100
- const result = await runAwsCall({
56101
- service: "cloudcontrol",
56102
- operation: "list-resources",
56103
- profile: i.profile,
56104
- region: i.region,
56105
- timeoutMs: i.timeoutMs,
56106
- outputFormat: "json",
56107
- extraFlags
56108
- });
56109
- if (!result.ok) {
56110
- return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56111
- }
56112
- const raw = result.data;
56113
- const descriptions = Array.isArray(raw?.ResourceDescriptions) ? raw.ResourceDescriptions : [];
56114
- const resources = descriptions.map((d) => {
56115
- const p = parseResourceProperties(d);
56116
- return { identifier: p.Identifier, properties: p.Properties };
55686
+ const raw = result.data;
55687
+ const descriptions = Array.isArray(raw?.ResourceDescriptions) ? raw.ResourceDescriptions : [];
55688
+ const resources = descriptions.map((d) => {
55689
+ const p = parseResourceProperties(d);
55690
+ return { identifier: p.Identifier, properties: p.Properties };
56117
55691
  });
56118
55692
  const nextToken = extractNextToken(raw);
56119
55693
  return {
@@ -56228,343 +55802,791 @@ var resourceTools = [
56228
55802
  if (!result.ok) {
56229
55803
  return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56230
55804
  }
56231
- return buildMutationResponse({ command: result.command, data: result.data }, i);
55805
+ return buildMutationResponse({ command: result.command, data: result.data }, i);
55806
+ }
55807
+ },
55808
+ {
55809
+ name: "aws_resource_delete",
55810
+ description: "Delete an AWS resource via Cloud Control API. Async by default: returns a ProgressEvent with OperationStatus=IN_PROGRESS and a top-level `requestToken`. Pass `awaitCompletion: true` to have the server poll until terminal. Destructive -- double-check `identifier` before calling.",
55811
+ annotations: {
55812
+ title: "Delete an AWS resource (async via CCAPI)",
55813
+ readOnlyHint: false,
55814
+ destructiveHint: true,
55815
+ idempotentHint: false,
55816
+ openWorldHint: true
55817
+ },
55818
+ inputSchema: external_exports3.object({
55819
+ typeName: external_exports3.string().describe("CloudFormation type name."),
55820
+ identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
55821
+ clientToken: external_exports3.string().optional().describe("Idempotency token (max 128 chars)."),
55822
+ ...baseFields,
55823
+ ...awaitFields
55824
+ }),
55825
+ handler: async (input) => {
55826
+ const i = input;
55827
+ const tnErr = validateTypeName(i.typeName);
55828
+ if (tnErr) return { ok: false, error: tnErr };
55829
+ const idErr = validateIdentifier(i.identifier);
55830
+ if (idErr) return { ok: false, error: idErr };
55831
+ if (i.clientToken !== void 0) {
55832
+ const ctErr = validateOpaqueToken(i.clientToken, "clientToken");
55833
+ if (ctErr) return { ok: false, error: ctErr };
55834
+ }
55835
+ const extraFlags = ["--type-name", i.typeName, "--identifier", i.identifier];
55836
+ if (i.clientToken) extraFlags.push("--client-token", i.clientToken);
55837
+ const result = await runAwsCall({
55838
+ service: "cloudcontrol",
55839
+ operation: "delete-resource",
55840
+ profile: i.profile,
55841
+ region: i.region,
55842
+ timeoutMs: i.timeoutMs,
55843
+ outputFormat: "json",
55844
+ extraFlags
55845
+ });
55846
+ if (!result.ok) {
55847
+ return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
55848
+ }
55849
+ return buildMutationResponse({ command: result.command, data: result.data }, i);
55850
+ }
55851
+ },
55852
+ {
55853
+ name: "aws_resource_status",
55854
+ description: "Poll the status of an async Cloud Control API request (create/update/delete). Pass the `requestToken` returned by those tools. Returns the current ProgressEvent with OperationStatus: PENDING | IN_PROGRESS | SUCCESS | FAILED | CANCEL_IN_PROGRESS | CANCEL_COMPLETE.",
55855
+ annotations: {
55856
+ title: "Get the status of an async CCAPI request",
55857
+ readOnlyHint: true,
55858
+ destructiveHint: false,
55859
+ idempotentHint: true,
55860
+ openWorldHint: true
55861
+ },
55862
+ inputSchema: external_exports3.object({
55863
+ requestToken: external_exports3.string().min(1).describe("RequestToken from a previous create/update/delete call."),
55864
+ ...baseFields
55865
+ }),
55866
+ handler: async (input) => {
55867
+ const i = input;
55868
+ const rtErr = validateOpaqueToken(i.requestToken, "requestToken");
55869
+ if (rtErr) return { ok: false, error: rtErr };
55870
+ const result = await runAwsCall({
55871
+ service: "cloudcontrol",
55872
+ operation: "get-resource-request-status",
55873
+ profile: i.profile,
55874
+ region: i.region,
55875
+ timeoutMs: i.timeoutMs,
55876
+ outputFormat: "json",
55877
+ extraFlags: ["--request-token", i.requestToken]
55878
+ });
55879
+ if (!result.ok) {
55880
+ return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
55881
+ }
55882
+ const raw = result.data;
55883
+ const progressEvent = raw?.ProgressEvent ?? null;
55884
+ const fields = extractProgressFields(progressEvent);
55885
+ return {
55886
+ ok: true,
55887
+ data: { command: result.command, ...fields, progressEvent }
55888
+ };
55889
+ }
55890
+ },
55891
+ {
55892
+ name: "aws_resource_diff",
55893
+ description: "Dry-run a CCAPI update: fetch the current resource state, simulate applying a JSON Patch in memory, and return before/after plus a flat list of changed paths. No mutation is sent to AWS. Use this before aws_resource_update to verify the patch does what you expect. Supports the add/remove/replace subset of RFC 6902 (covers the vast majority of CCAPI updates); 'move'/'copy'/'test' are rejected at schema validation -- use aws_resource_update directly if you need those (CCAPI accepts them, this preview tool just doesn't simulate them locally).",
55894
+ annotations: {
55895
+ title: "Preview a CCAPI update without applying it",
55896
+ readOnlyHint: true,
55897
+ destructiveHint: false,
55898
+ idempotentHint: true,
55899
+ openWorldHint: true
55900
+ },
55901
+ inputSchema: external_exports3.object({
55902
+ typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
55903
+ identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
55904
+ patchDocument: external_exports3.array(
55905
+ external_exports3.object({
55906
+ // Diff simulates patches locally via applyJsonPatch; only the
55907
+ // add/remove/replace subset is implemented. Reject the other
55908
+ // three RFC 6902 ops here so the model gets schema-validation
55909
+ // feedback instead of a runtime "not implemented" error
55910
+ // surfaced as a generic "Patch application failed". The
55911
+ // sibling aws_resource_update tool accepts the full op set
55912
+ // because CCAPI does -- only this preview tool is restricted.
55913
+ op: external_exports3.enum(["add", "remove", "replace"]),
55914
+ path: external_exports3.string(),
55915
+ value: external_exports3.unknown().optional(),
55916
+ from: external_exports3.string().optional()
55917
+ })
55918
+ ).min(1).describe(
55919
+ "RFC 6902 JSON Patch (add/remove/replace subset). For move/copy/test, use aws_resource_update directly."
55920
+ ),
55921
+ ...baseFields
55922
+ }),
55923
+ handler: async (input) => {
55924
+ const i = input;
55925
+ const tnErr = validateTypeName(i.typeName);
55926
+ if (tnErr) return { ok: false, error: tnErr };
55927
+ const idErr = validateIdentifier(i.identifier);
55928
+ if (idErr) return { ok: false, error: idErr };
55929
+ const getResult = await runAwsCall({
55930
+ service: "cloudcontrol",
55931
+ operation: "get-resource",
55932
+ profile: i.profile,
55933
+ region: i.region,
55934
+ timeoutMs: i.timeoutMs,
55935
+ outputFormat: "json",
55936
+ extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
55937
+ });
55938
+ if (!getResult.ok) {
55939
+ return { ok: false, error: getResult.error, rawBody: getResult.rawStderr ?? getResult.rawStdout };
55940
+ }
55941
+ const raw = getResult.data;
55942
+ const parsed = parseResourceProperties(raw?.ResourceDescription);
55943
+ const before = parsed.Properties;
55944
+ let after;
55945
+ try {
55946
+ after = applyJsonPatch(before, i.patchDocument);
55947
+ } catch (err) {
55948
+ const msg = err instanceof Error ? err.message : String(err);
55949
+ return { ok: false, error: `Patch application failed: ${msg}` };
55950
+ }
55951
+ const changes = summarizePatch(i.patchDocument, before, after);
55952
+ return {
55953
+ ok: true,
55954
+ data: {
55955
+ command: getResult.command,
55956
+ typeName: i.typeName,
55957
+ identifier: parsed.Identifier ?? i.identifier,
55958
+ before,
55959
+ after,
55960
+ changes,
55961
+ changeCount: changes.length
55962
+ }
55963
+ };
55964
+ }
55965
+ }
55966
+ ];
55967
+ function parseJsonPointer(pointer) {
55968
+ if (pointer === "") return [];
55969
+ if (!pointer.startsWith("/")) {
55970
+ throw new Error(`Invalid JSON Pointer '${pointer}': must start with '/' or be empty.`);
55971
+ }
55972
+ return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
55973
+ }
55974
+ function clone2(v) {
55975
+ return v === void 0 ? v : JSON.parse(JSON.stringify(v));
55976
+ }
55977
+ function isObj(v) {
55978
+ return v !== null && typeof v === "object" && !Array.isArray(v);
55979
+ }
55980
+ function applyJsonPatch(original, ops) {
55981
+ const doc = clone2(original);
55982
+ return _applyJsonPatchInPlace(doc, ops);
55983
+ }
55984
+ function _applyJsonPatchInPlace(doc, ops) {
55985
+ let root = doc;
55986
+ for (let i = 0; i < ops.length; i++) {
55987
+ const op = ops[i];
55988
+ if (op.op === "move" || op.op === "copy" || op.op === "test") {
55989
+ throw new Error(`op '${op.op}' at index ${i} is not implemented in aws_resource_diff (use add/remove/replace).`);
55990
+ }
55991
+ const tokens = parseJsonPointer(op.path);
55992
+ if (tokens.length === 0) {
55993
+ if (op.op === "remove") {
55994
+ throw new Error(`Cannot remove the document root at index ${i}.`);
55995
+ }
55996
+ root = clone2(op.value);
55997
+ continue;
55998
+ }
55999
+ const parentTokens = tokens.slice(0, -1);
56000
+ const lastToken = tokens[tokens.length - 1];
56001
+ let parent = root;
56002
+ for (let t = 0; t < parentTokens.length; t++) {
56003
+ const segment = parentTokens[t];
56004
+ if (Array.isArray(parent)) {
56005
+ const idx = Number.parseInt(segment, 10);
56006
+ if (!Number.isInteger(idx) || idx < 0) {
56007
+ throw new Error(`Path '${op.path}' segment '${segment}' is not a valid array index at op index ${i}.`);
56008
+ }
56009
+ if (idx >= parent.length) {
56010
+ throw new Error(
56011
+ `Path '${op.path}' cannot traverse into intermediate array element at segment '${segment}': array has length ${parent.length}, so index ${idx} does not exist yet (at op index ${i}).`
56012
+ );
56013
+ }
56014
+ parent = parent[idx];
56015
+ } else if (isObj(parent)) {
56016
+ if (!(segment in parent)) {
56017
+ if (op.op === "add") {
56018
+ parent[segment] = {};
56019
+ parent = parent[segment];
56020
+ continue;
56021
+ }
56022
+ throw new Error(`Path '${op.path}' segment '${segment}' does not exist at index ${i}.`);
56023
+ }
56024
+ parent = parent[segment];
56025
+ } else {
56026
+ throw new Error(`Path '${op.path}' traverses a non-container value at index ${i}.`);
56027
+ }
56232
56028
  }
56233
- },
56234
- {
56235
- name: "aws_resource_delete",
56236
- description: "Delete an AWS resource via Cloud Control API. Async by default: returns a ProgressEvent with OperationStatus=IN_PROGRESS and a top-level `requestToken`. Pass `awaitCompletion: true` to have the server poll until terminal. Destructive -- double-check `identifier` before calling.",
56237
- annotations: {
56238
- title: "Delete an AWS resource (async via CCAPI)",
56239
- readOnlyHint: false,
56240
- destructiveHint: true,
56241
- idempotentHint: false,
56242
- openWorldHint: true
56243
- },
56244
- inputSchema: external_exports3.object({
56245
- typeName: external_exports3.string().describe("CloudFormation type name."),
56246
- identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
56247
- clientToken: external_exports3.string().optional().describe("Idempotency token (max 128 chars)."),
56248
- ...baseFields,
56249
- ...awaitFields
56250
- }),
56251
- handler: async (input) => {
56252
- const i = input;
56253
- const tnErr = validateTypeName(i.typeName);
56254
- if (tnErr) return { ok: false, error: tnErr };
56255
- const idErr = validateIdentifier(i.identifier);
56256
- if (idErr) return { ok: false, error: idErr };
56257
- if (i.clientToken !== void 0) {
56258
- const ctErr = validateOpaqueToken(i.clientToken, "clientToken");
56259
- if (ctErr) return { ok: false, error: ctErr };
56029
+ if (Array.isArray(parent)) {
56030
+ if (lastToken === "-") {
56031
+ if (op.op === "remove") {
56032
+ throw new Error(`Cannot remove '-' (end-of-array) at index ${i}.`);
56033
+ }
56034
+ parent.push(clone2(op.value));
56035
+ continue;
56260
56036
  }
56261
- const extraFlags = ["--type-name", i.typeName, "--identifier", i.identifier];
56262
- if (i.clientToken) extraFlags.push("--client-token", i.clientToken);
56263
- const result = await runAwsCall({
56264
- service: "cloudcontrol",
56265
- operation: "delete-resource",
56266
- profile: i.profile,
56267
- region: i.region,
56268
- timeoutMs: i.timeoutMs,
56269
- outputFormat: "json",
56270
- extraFlags
56271
- });
56272
- if (!result.ok) {
56273
- return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56037
+ const idx = Number.parseInt(lastToken, 10);
56038
+ if (!Number.isInteger(idx) || idx < 0) {
56039
+ throw new Error(`Path '${op.path}' has non-integer array index '${lastToken}' at index ${i}.`);
56274
56040
  }
56275
- return buildMutationResponse({ command: result.command, data: result.data }, i);
56041
+ if (op.op === "add") {
56042
+ if (idx > parent.length) {
56043
+ throw new Error(`Add index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56044
+ }
56045
+ parent.splice(idx, 0, clone2(op.value));
56046
+ } else if (op.op === "remove") {
56047
+ if (idx >= parent.length) {
56048
+ throw new Error(`Remove index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56049
+ }
56050
+ parent.splice(idx, 1);
56051
+ } else {
56052
+ if (idx >= parent.length) {
56053
+ throw new Error(`Replace index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56054
+ }
56055
+ parent[idx] = clone2(op.value);
56056
+ }
56057
+ } else if (isObj(parent)) {
56058
+ if (op.op === "remove") {
56059
+ if (!(lastToken in parent)) {
56060
+ throw new Error(`Cannot remove missing key '${lastToken}' at index ${i}.`);
56061
+ }
56062
+ delete parent[lastToken];
56063
+ } else if (op.op === "replace") {
56064
+ if (!(lastToken in parent)) {
56065
+ throw new Error(`Cannot replace missing key '${lastToken}' at index ${i} (use 'add' to create it).`);
56066
+ }
56067
+ parent[lastToken] = clone2(op.value);
56068
+ } else {
56069
+ parent[lastToken] = clone2(op.value);
56070
+ }
56071
+ } else {
56072
+ throw new Error(`Path '${op.path}' parent is not a container at index ${i}.`);
56276
56073
  }
56277
- },
56074
+ }
56075
+ return root;
56076
+ }
56077
+ function summarizePatch(ops, before, after) {
56078
+ let working;
56079
+ let replayOk = true;
56080
+ try {
56081
+ working = clone2(before);
56082
+ } catch {
56083
+ replayOk = false;
56084
+ working = void 0;
56085
+ }
56086
+ const out = [];
56087
+ for (const op of ops) {
56088
+ const beforeAt = resolvePointer(before, op.path);
56089
+ let afterAt;
56090
+ if (replayOk) {
56091
+ try {
56092
+ working = _applyJsonPatchInPlace(working, [op]);
56093
+ afterAt = resolvePointer(working, op.path);
56094
+ } catch {
56095
+ replayOk = false;
56096
+ afterAt = resolvePointer(after, op.path);
56097
+ }
56098
+ } else {
56099
+ afterAt = resolvePointer(after, op.path);
56100
+ }
56101
+ if (op.op === "add" && afterAt === void 0 && op.path.endsWith("/-")) {
56102
+ afterAt = op.value;
56103
+ }
56104
+ out.push({ op: op.op, path: op.path, before: beforeAt, after: afterAt });
56105
+ }
56106
+ return out;
56107
+ }
56108
+ function resolvePointer(doc, pointer) {
56109
+ let tokens;
56110
+ try {
56111
+ tokens = parseJsonPointer(pointer);
56112
+ } catch {
56113
+ return void 0;
56114
+ }
56115
+ let cur = doc;
56116
+ for (const t of tokens) {
56117
+ if (Array.isArray(cur)) {
56118
+ if (t === "-") return void 0;
56119
+ const idx = Number.parseInt(t, 10);
56120
+ if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return void 0;
56121
+ cur = cur[idx];
56122
+ } else if (isObj(cur)) {
56123
+ if (!(t in cur)) return void 0;
56124
+ cur = cur[t];
56125
+ } else {
56126
+ return void 0;
56127
+ }
56128
+ }
56129
+ return cur;
56130
+ }
56131
+
56132
+ // src/tools/paginate.ts
56133
+ function extractNextToken(data) {
56134
+ if (data && typeof data === "object" && "NextToken" in data) {
56135
+ const token = data.NextToken;
56136
+ if (typeof token === "string" && token.length > 0) return token;
56137
+ }
56138
+ return null;
56139
+ }
56140
+ function wrapQueryForPagination(userQuery) {
56141
+ return `{NextToken: NextToken, items: ${userQuery}}`;
56142
+ }
56143
+ var paginateTools = [
56278
56144
  {
56279
- name: "aws_resource_status",
56280
- description: "Poll the status of an async Cloud Control API request (create/update/delete). Pass the `requestToken` returned by those tools. Returns the current ProgressEvent with OperationStatus: PENDING | IN_PROGRESS | SUCCESS | FAILED | CANCEL_IN_PROGRESS | CANCEL_COMPLETE.",
56145
+ name: "aws_paginate",
56146
+ 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.",
56281
56147
  annotations: {
56282
- title: "Get the status of an async CCAPI request",
56148
+ title: "Fetch one page of a paginated AWS operation",
56283
56149
  readOnlyHint: true,
56284
56150
  destructiveHint: false,
56285
56151
  idempotentHint: true,
56286
56152
  openWorldHint: true
56287
56153
  },
56288
56154
  inputSchema: external_exports3.object({
56289
- requestToken: external_exports3.string().min(1).describe("RequestToken from a previous create/update/delete call."),
56290
- ...baseFields
56155
+ service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', 'logs', etc."),
56156
+ operation: external_exports3.string().describe("Paginated operation: 'list-objects-v2', 'describe-instances', 'list-roles', etc."),
56157
+ params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) passed via --cli-input-json."),
56158
+ query: external_exports3.string().optional().describe(
56159
+ "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."
56160
+ ),
56161
+ 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."),
56162
+ startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
56163
+ profile: external_exports3.string().optional().describe("Override session profile for this call."),
56164
+ region: external_exports3.string().optional().describe("Override session region for this call."),
56165
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
56291
56166
  }),
56292
56167
  handler: async (input) => {
56293
56168
  const i = input;
56294
- const rtErr = validateOpaqueToken(i.requestToken, "requestToken");
56295
- if (rtErr) return { ok: false, error: rtErr };
56169
+ if (i.startingToken !== void 0) {
56170
+ const stErr = validateOpaqueToken(i.startingToken, "startingToken");
56171
+ if (stErr) return { ok: false, error: stErr };
56172
+ }
56173
+ const maxItems = Math.min(Math.max(1, i.maxItems ?? 100), 1e4);
56174
+ const extraFlags = ["--max-items", String(maxItems)];
56175
+ if (i.startingToken) {
56176
+ extraFlags.push("--starting-token", i.startingToken);
56177
+ }
56178
+ const userQuery = i.query?.trim();
56179
+ const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
56296
56180
  const result = await runAwsCall({
56297
- service: "cloudcontrol",
56298
- operation: "get-resource-request-status",
56181
+ service: i.service,
56182
+ operation: i.operation,
56183
+ params: i.params,
56184
+ query: queryWrapped,
56299
56185
  profile: i.profile,
56300
56186
  region: i.region,
56301
- timeoutMs: i.timeoutMs,
56302
56187
  outputFormat: "json",
56303
- extraFlags: ["--request-token", i.requestToken]
56188
+ timeoutMs: i.timeoutMs,
56189
+ extraFlags
56304
56190
  });
56305
56191
  if (!result.ok) {
56306
56192
  return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56307
56193
  }
56308
- const raw = result.data;
56309
- const progressEvent = raw?.ProgressEvent ?? null;
56310
- const fields = extractProgressFields(progressEvent);
56194
+ let resultBody;
56195
+ let nextToken;
56196
+ if (queryWrapped) {
56197
+ const wrapped = result.data ?? {};
56198
+ nextToken = extractNextToken(wrapped);
56199
+ resultBody = wrapped.items ?? null;
56200
+ } else {
56201
+ nextToken = extractNextToken(result.data);
56202
+ resultBody = result.data;
56203
+ }
56311
56204
  return {
56312
56205
  ok: true,
56313
- data: { command: result.command, ...fields, progressEvent }
56206
+ data: {
56207
+ command: result.command,
56208
+ result: resultBody,
56209
+ nextToken,
56210
+ hasMore: nextToken !== null
56211
+ }
56314
56212
  };
56315
56213
  }
56316
- },
56214
+ }
56215
+ ];
56216
+
56217
+ // src/tools/metrics.ts
56218
+ var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
56219
+ var EXTENDED_STAT_RE = /^((p|tm|tc|wm|pr|ts)(\d{1,3}(\.\d{1,3})?)?|iqm)$/i;
56220
+ function isValidStatistic(s) {
56221
+ const lower = s.toLowerCase();
56222
+ if (SIMPLE_STATS.some((stat) => stat.toLowerCase() === lower)) return true;
56223
+ return EXTENDED_STAT_RE.test(s);
56224
+ }
56225
+ function canonicalizeStatistic(s) {
56226
+ const lower = s.toLowerCase();
56227
+ for (const stat of SIMPLE_STATS) {
56228
+ if (stat.toLowerCase() === lower) return stat;
56229
+ }
56230
+ if (EXTENDED_STAT_RE.test(s)) return lower;
56231
+ return s;
56232
+ }
56233
+ var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
56234
+ var MAX_QUERIES = 100;
56235
+ var CLOUDWATCH_MAX_DATAPOINTS = 100800;
56236
+ var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
56237
+ var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
56238
+ var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
56239
+ function pickAutoPeriodSeconds(startMs, endMs) {
56240
+ const rangeMs = Math.max(0, endMs - startMs);
56241
+ if (rangeMs <= PERIOD_3H_MS) return 60;
56242
+ if (rangeMs <= PERIOD_24H_MS) return 300;
56243
+ if (rangeMs <= PERIOD_15D_MS) return 900;
56244
+ return 3600;
56245
+ }
56246
+ var RELATIVE_TIME_RE = /^\d+[smhdw]$/;
56247
+ var UNIT_MS = {
56248
+ s: 1e3,
56249
+ m: 60 * 1e3,
56250
+ h: 60 * 60 * 1e3,
56251
+ d: 24 * 60 * 60 * 1e3,
56252
+ w: 7 * 24 * 60 * 60 * 1e3
56253
+ };
56254
+ function resolveTime(input, now) {
56255
+ if (input === "now") return new Date(now);
56256
+ const rel = input.match(RELATIVE_TIME_RE);
56257
+ if (rel) {
56258
+ const num = Number(input.slice(0, -1));
56259
+ const unit = input.slice(-1);
56260
+ const ms = UNIT_MS[unit];
56261
+ if (!ms || !Number.isFinite(num)) return null;
56262
+ return new Date(now - num * ms);
56263
+ }
56264
+ const t = new Date(input);
56265
+ if (Number.isNaN(t.getTime())) return null;
56266
+ return t;
56267
+ }
56268
+ function buildMetricDataQueries(inputs, autoPeriod) {
56269
+ return inputs.map((q) => {
56270
+ const base = { Id: q.id };
56271
+ if (q.label !== void 0) base.Label = q.label;
56272
+ if (q.returnData !== void 0) base.ReturnData = q.returnData;
56273
+ if (q.expression !== void 0) {
56274
+ base.Expression = q.expression;
56275
+ if (q.period !== void 0) base.Period = q.period;
56276
+ return base;
56277
+ }
56278
+ const dimensions = q.dimensions ? Object.entries(q.dimensions).map(([Name, Value]) => ({ Name, Value })) : void 0;
56279
+ const stat = {
56280
+ Metric: {
56281
+ Namespace: q.namespace,
56282
+ MetricName: q.metricName,
56283
+ ...dimensions ? { Dimensions: dimensions } : {}
56284
+ },
56285
+ Period: q.period ?? autoPeriod,
56286
+ Stat: q.statistic !== void 0 ? canonicalizeStatistic(q.statistic) : "Average"
56287
+ };
56288
+ if (q.unit !== void 0) stat.Unit = q.unit;
56289
+ base.MetricStat = stat;
56290
+ return base;
56291
+ });
56292
+ }
56293
+ var metricsTools = [
56317
56294
  {
56318
- name: "aws_resource_diff",
56319
- description: "Dry-run a CCAPI update: fetch the current resource state, simulate applying a JSON Patch in memory, and return before/after plus a flat list of changed paths. No mutation is sent to AWS. Use this before aws_resource_update to verify the patch does what you expect. Supports the add/remove/replace subset of RFC 6902 (covers the vast majority of CCAPI updates); 'move'/'copy'/'test' are rejected at schema validation -- use aws_resource_update directly if you need those (CCAPI accepts them, this preview tool just doesn't simulate them locally).",
56295
+ name: "aws_metrics_query",
56296
+ 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.",
56320
56297
  annotations: {
56321
- title: "Preview a CCAPI update without applying it",
56298
+ title: "Query CloudWatch metrics (GetMetricData)",
56322
56299
  readOnlyHint: true,
56323
56300
  destructiveHint: false,
56324
56301
  idempotentHint: true,
56325
56302
  openWorldHint: true
56326
56303
  },
56327
56304
  inputSchema: external_exports3.object({
56328
- typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
56329
- identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
56330
- patchDocument: external_exports3.array(
56305
+ queries: external_exports3.array(
56331
56306
  external_exports3.object({
56332
- // Diff simulates patches locally via applyJsonPatch; only the
56333
- // add/remove/replace subset is implemented. Reject the other
56334
- // three RFC 6902 ops here so the model gets schema-validation
56335
- // feedback instead of a runtime "not implemented" error
56336
- // surfaced as a generic "Patch application failed". The
56337
- // sibling aws_resource_update tool accepts the full op set
56338
- // because CCAPI does -- only this preview tool is restricted.
56339
- op: external_exports3.enum(["add", "remove", "replace"]),
56340
- path: external_exports3.string(),
56341
- value: external_exports3.unknown().optional(),
56342
- from: external_exports3.string().optional()
56307
+ id: external_exports3.string().regex(QUERY_ID_RE, "id must match /^[a-z][A-Za-z0-9_]*$/ (CloudWatch's MetricDataQuery.Id contract)"),
56308
+ namespace: external_exports3.string().min(1).optional().describe("AWS metric namespace, e.g. 'AWS/Lambda', 'AWS/EC2'. Required unless `expression` is set."),
56309
+ metricName: external_exports3.string().min(1).optional().describe("Metric name, e.g. 'Invocations', 'CPUUtilization'. Required unless `expression` is set."),
56310
+ dimensions: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("Dimension Name -> Value map, e.g. {FunctionName: 'my-fn'}."),
56311
+ statistic: external_exports3.string().optional().describe(
56312
+ "Statistic: Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like 'p99', 'p99.9', 'tm95'. Default 'Average'."
56313
+ ),
56314
+ period: external_exports3.number().int().positive().optional().describe("Period in seconds. Defaults to an auto-pick from the time range (60s/300s/900s/3600s)."),
56315
+ expression: external_exports3.string().min(1).optional().describe(
56316
+ `CloudWatch metric math expression, e.g. 'SUM([m1, m2])' or 'AVG(METRICS("AWS/Lambda"))'. Mutually exclusive with namespace/metricName/dimensions.`
56317
+ ),
56318
+ label: external_exports3.string().optional().describe("Human-readable label for the series in the response."),
56319
+ returnData: external_exports3.boolean().optional().describe(
56320
+ "Set false to compute this query but not return its data (useful for intermediate values in expressions). Default true."
56321
+ ),
56322
+ unit: external_exports3.string().optional().describe(
56323
+ "Restrict to a specific Unit (e.g. 'Seconds', 'Bytes'). Default: no filter. Only meaningful on metric-stat queries."
56324
+ )
56343
56325
  })
56344
- ).min(1).describe(
56345
- "RFC 6902 JSON Patch (add/remove/replace subset). For move/copy/test, use aws_resource_update directly."
56326
+ ).min(1).max(MAX_QUERIES).describe(`1-${MAX_QUERIES} queries. Each is either a metric-stat (namespace + metricName) or an expression.`),
56327
+ startTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand ('15m', '1h', '1d', '1w'). Default '1h' (one hour ago)."),
56328
+ endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
56329
+ scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
56330
+ maxDataPoints: external_exports3.number().int().positive().optional().describe(
56331
+ "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."
56346
56332
  ),
56347
- ...baseFields
56333
+ nextToken: external_exports3.string().optional().describe(
56334
+ "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`."
56335
+ ),
56336
+ profile: external_exports3.string().optional().describe("Override session profile for this call."),
56337
+ region: external_exports3.string().optional().describe("Override session region for this call."),
56338
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000 (60s).")
56348
56339
  }),
56349
56340
  handler: async (input) => {
56350
56341
  const i = input;
56351
- const tnErr = validateTypeName(i.typeName);
56352
- if (tnErr) return { ok: false, error: tnErr };
56353
- const idErr = validateIdentifier(i.identifier);
56354
- if (idErr) return { ok: false, error: idErr };
56355
- const getResult = await runAwsCall({
56356
- service: "cloudcontrol",
56357
- operation: "get-resource",
56342
+ const seenIds = /* @__PURE__ */ new Map();
56343
+ for (let qi = 0; qi < i.queries.length; qi++) {
56344
+ const q = i.queries[qi];
56345
+ const firstIdx = seenIds.get(q.id);
56346
+ if (firstIdx !== void 0) {
56347
+ return {
56348
+ ok: false,
56349
+ error: `Duplicate query id '${q.id}' at queries[${qi}]; first seen at queries[${firstIdx}]. Each MetricDataQuery.Id must be unique in a batch.`
56350
+ };
56351
+ }
56352
+ seenIds.set(q.id, qi);
56353
+ const hasMetricStat = q.namespace !== void 0 || q.metricName !== void 0 || q.dimensions !== void 0;
56354
+ const hasExpression = q.expression !== void 0;
56355
+ if (hasMetricStat && hasExpression) {
56356
+ return {
56357
+ ok: false,
56358
+ error: `Query '${q.id}' mixes metric-stat fields (namespace/metricName/dimensions) with 'expression'. Pick one shape per query.`
56359
+ };
56360
+ }
56361
+ if (!hasMetricStat && !hasExpression) {
56362
+ return {
56363
+ ok: false,
56364
+ error: `Query '${q.id}' has neither metric-stat (namespace+metricName) nor 'expression'. One is required.`
56365
+ };
56366
+ }
56367
+ if (hasMetricStat && (q.namespace === void 0 || q.metricName === void 0)) {
56368
+ return {
56369
+ ok: false,
56370
+ error: `Query '${q.id}' must include BOTH 'namespace' and 'metricName' (or use 'expression' instead).`
56371
+ };
56372
+ }
56373
+ if (q.statistic !== void 0 && !isValidStatistic(q.statistic)) {
56374
+ return {
56375
+ ok: false,
56376
+ error: `Query '${q.id}' has invalid statistic '${q.statistic}'. Use Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like p99 / p99.9 / tm95.`
56377
+ };
56378
+ }
56379
+ }
56380
+ const now = Date.now();
56381
+ const startStr = i.startTime ?? "1h";
56382
+ const endStr = i.endTime ?? "now";
56383
+ const startDate = resolveTime(startStr, now);
56384
+ const endDate = resolveTime(endStr, now);
56385
+ if (!startDate) {
56386
+ return {
56387
+ ok: false,
56388
+ error: `Invalid startTime '${startStr}'. Use ISO 8601 (e.g. '2026-05-16T10:00:00Z') or relative shorthand (e.g. '1h', '15m', '1d').`
56389
+ };
56390
+ }
56391
+ if (!endDate) {
56392
+ return {
56393
+ ok: false,
56394
+ error: `Invalid endTime '${endStr}'. Use ISO 8601 or relative shorthand, or 'now' for the current moment.`
56395
+ };
56396
+ }
56397
+ if (endDate.getTime() <= startDate.getTime()) {
56398
+ return {
56399
+ ok: false,
56400
+ error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
56401
+ };
56402
+ }
56403
+ const rangeSeconds = (endDate.getTime() - startDate.getTime()) / 1e3;
56404
+ for (const q of i.queries) {
56405
+ if (q.period === void 0) continue;
56406
+ if (q.period <= 0 || q.period % 60 !== 0) {
56407
+ return {
56408
+ ok: false,
56409
+ error: `Query '${q.id}' has invalid period ${q.period}. CloudWatch requires period to be a positive multiple of 60 (seconds).`
56410
+ };
56411
+ }
56412
+ const datapoints = Math.ceil(rangeSeconds / q.period);
56413
+ if (datapoints > CLOUDWATCH_MAX_DATAPOINTS) {
56414
+ return {
56415
+ ok: false,
56416
+ 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.`
56417
+ };
56418
+ }
56419
+ }
56420
+ const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
56421
+ const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
56422
+ const params = {
56423
+ MetricDataQueries: metricDataQueries,
56424
+ StartTime: startDate.toISOString(),
56425
+ EndTime: endDate.toISOString(),
56426
+ ScanBy: i.scanBy ?? "TimestampDescending"
56427
+ };
56428
+ if (i.maxDataPoints !== void 0) params.MaxDatapoints = i.maxDataPoints;
56429
+ if (i.nextToken !== void 0) params.NextToken = i.nextToken;
56430
+ const effectiveProfile = i.profile ?? getProfile();
56431
+ const effectiveRegion = i.region ?? getRegion();
56432
+ const result = await runAwsCall({
56433
+ service: "cloudwatch",
56434
+ operation: "get-metric-data",
56358
56435
  profile: i.profile,
56359
56436
  region: i.region,
56360
56437
  timeoutMs: i.timeoutMs,
56361
56438
  outputFormat: "json",
56362
- extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
56439
+ params
56363
56440
  });
56364
- if (!getResult.ok) {
56365
- return { ok: false, error: getResult.error, rawBody: getResult.rawStderr ?? getResult.rawStdout };
56366
- }
56367
- const raw = getResult.data;
56368
- const parsed = parseResourceProperties(raw?.ResourceDescription);
56369
- const before = parsed.Properties;
56370
- let after;
56371
- try {
56372
- after = applyJsonPatch(before, i.patchDocument);
56373
- } catch (err) {
56374
- const msg = err instanceof Error ? err.message : String(err);
56375
- return { ok: false, error: `Patch application failed: ${msg}` };
56441
+ if (!result.ok) {
56442
+ return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
56376
56443
  }
56377
- const changes = summarizePatch(i.patchDocument, before, after);
56444
+ const raw = result.data ?? {};
56445
+ const queryById = new Map(i.queries.map((q) => [q.id, q]));
56446
+ const series = (raw.MetricDataResults ?? []).map((r) => {
56447
+ const q = queryById.get(r.Id ?? "");
56448
+ const effectivePeriod = q?.period ?? (q && q.expression === void 0 ? periodSeconds : void 0);
56449
+ return {
56450
+ id: r.Id ?? "",
56451
+ ...r.Label !== void 0 ? { label: r.Label } : {},
56452
+ timestamps: r.Timestamps ?? [],
56453
+ values: r.Values ?? [],
56454
+ ...effectivePeriod !== void 0 ? { period: effectivePeriod } : {},
56455
+ ...r.StatusCode !== void 0 ? { statusCode: r.StatusCode } : {}
56456
+ };
56457
+ });
56458
+ const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
56459
+ code: m.Code,
56460
+ value: m.Value
56461
+ }));
56462
+ const nextToken = extractNextToken(raw);
56378
56463
  return {
56379
56464
  ok: true,
56380
56465
  data: {
56381
- command: getResult.command,
56382
- typeName: i.typeName,
56383
- identifier: parsed.Identifier ?? i.identifier,
56384
- before,
56385
- after,
56386
- changes,
56387
- changeCount: changes.length
56466
+ command: result.command,
56467
+ profile: effectiveProfile,
56468
+ region: effectiveRegion,
56469
+ startTime: startDate.toISOString(),
56470
+ endTime: endDate.toISOString(),
56471
+ periodSeconds,
56472
+ series,
56473
+ nextToken,
56474
+ hasMore: nextToken !== null,
56475
+ ...messages && messages.length > 0 ? { messages } : {}
56388
56476
  }
56389
56477
  };
56390
56478
  }
56391
56479
  }
56392
56480
  ];
56393
- function parseJsonPointer(pointer) {
56394
- if (pointer === "") return [];
56395
- if (!pointer.startsWith("/")) {
56396
- throw new Error(`Invalid JSON Pointer '${pointer}': must start with '/' or be empty.`);
56397
- }
56398
- return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
56399
- }
56400
- function clone2(v) {
56401
- return v === void 0 ? v : JSON.parse(JSON.stringify(v));
56402
- }
56403
- function isObj(v) {
56404
- return v !== null && typeof v === "object" && !Array.isArray(v);
56405
- }
56406
- function applyJsonPatch(original, ops) {
56407
- const doc = clone2(original);
56408
- return _applyJsonPatchInPlace(doc, ops);
56409
- }
56410
- function _applyJsonPatchInPlace(doc, ops) {
56411
- let root = doc;
56412
- for (let i = 0; i < ops.length; i++) {
56413
- const op = ops[i];
56414
- if (op.op === "move" || op.op === "copy" || op.op === "test") {
56415
- throw new Error(`op '${op.op}' at index ${i} is not implemented in aws_resource_diff (use add/remove/replace).`);
56416
- }
56417
- const tokens = parseJsonPointer(op.path);
56418
- if (tokens.length === 0) {
56419
- if (op.op === "remove") {
56420
- throw new Error(`Cannot remove the document root at index ${i}.`);
56421
- }
56422
- root = clone2(op.value);
56423
- continue;
56424
- }
56425
- const parentTokens = tokens.slice(0, -1);
56426
- const lastToken = tokens[tokens.length - 1];
56427
- let parent = root;
56428
- for (let t = 0; t < parentTokens.length; t++) {
56429
- const segment = parentTokens[t];
56430
- if (Array.isArray(parent)) {
56431
- const idx = Number.parseInt(segment, 10);
56432
- if (!Number.isInteger(idx) || idx < 0) {
56433
- throw new Error(`Path '${op.path}' segment '${segment}' is not a valid array index at op index ${i}.`);
56434
- }
56435
- if (idx >= parent.length) {
56436
- throw new Error(
56437
- `Path '${op.path}' cannot traverse into intermediate array element at segment '${segment}': array has length ${parent.length}, so index ${idx} does not exist yet (at op index ${i}).`
56438
- );
56439
- }
56440
- parent = parent[idx];
56441
- } else if (isObj(parent)) {
56442
- if (!(segment in parent)) {
56443
- if (op.op === "add") {
56444
- parent[segment] = {};
56445
- parent = parent[segment];
56446
- continue;
56447
- }
56448
- throw new Error(`Path '${op.path}' segment '${segment}' does not exist at index ${i}.`);
56449
- }
56450
- parent = parent[segment];
56451
- } else {
56452
- throw new Error(`Path '${op.path}' traverses a non-container value at index ${i}.`);
56453
- }
56481
+
56482
+ // src/tools/multi-region.ts
56483
+ var DEFAULT_CONCURRENCY = 8;
56484
+ var MAX_CONCURRENCY = 32;
56485
+ var MAX_REGIONS = 32;
56486
+ async function runWithConcurrency(inputs, concurrency, fn) {
56487
+ const results = new Array(inputs.length);
56488
+ let next = 0;
56489
+ const worker = async () => {
56490
+ while (true) {
56491
+ const i = next++;
56492
+ if (i >= inputs.length) return;
56493
+ results[i] = await fn(inputs[i], i);
56454
56494
  }
56455
- if (Array.isArray(parent)) {
56456
- if (lastToken === "-") {
56457
- if (op.op === "remove") {
56458
- throw new Error(`Cannot remove '-' (end-of-array) at index ${i}.`);
56495
+ };
56496
+ const workerCount = Math.min(concurrency, inputs.length);
56497
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
56498
+ return results;
56499
+ }
56500
+ var multiRegionTools = [
56501
+ {
56502
+ name: "aws_multi_region",
56503
+ 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'.",
56504
+ annotations: {
56505
+ title: "Run an AWS operation across multiple regions in parallel",
56506
+ // The operation can be anything -- we conservatively annotate as not
56507
+ // read-only / not destructive. The caller chooses what to invoke.
56508
+ readOnlyHint: false,
56509
+ destructiveHint: false,
56510
+ idempotentHint: false,
56511
+ openWorldHint: true
56512
+ },
56513
+ inputSchema: external_exports3.object({
56514
+ service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
56515
+ operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
56516
+ regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
56517
+ `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).`
56518
+ ),
56519
+ params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
56520
+ query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
56521
+ outputFormat: external_exports3.enum(["json", "text", "table", "yaml"]).optional().describe("Output format. Default 'json'."),
56522
+ profile: external_exports3.string().optional().describe("Override session profile for the batch."),
56523
+ timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in ms applied PER region. Default 60000."),
56524
+ concurrency: external_exports3.number().int().positive().max(MAX_CONCURRENCY).optional().describe(`Max regions in flight at once (1-${MAX_CONCURRENCY}). Default ${DEFAULT_CONCURRENCY}.`)
56525
+ }),
56526
+ handler: async (input) => {
56527
+ const i = input;
56528
+ const seen = /* @__PURE__ */ new Set();
56529
+ const regions = [];
56530
+ for (const r of i.regions) {
56531
+ if (!seen.has(r)) {
56532
+ seen.add(r);
56533
+ regions.push(r);
56459
56534
  }
56460
- parent.push(clone2(op.value));
56461
- continue;
56462
56535
  }
56463
- const idx = Number.parseInt(lastToken, 10);
56464
- if (!Number.isInteger(idx) || idx < 0) {
56465
- throw new Error(`Path '${op.path}' has non-integer array index '${lastToken}' at index ${i}.`);
56466
- }
56467
- if (op.op === "add") {
56468
- if (idx > parent.length) {
56469
- throw new Error(`Add index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56470
- }
56471
- parent.splice(idx, 0, clone2(op.value));
56472
- } else if (op.op === "remove") {
56473
- if (idx >= parent.length) {
56474
- throw new Error(`Remove index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56475
- }
56476
- parent.splice(idx, 1);
56477
- } else {
56478
- if (idx >= parent.length) {
56479
- throw new Error(`Replace index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
56536
+ const concurrency = i.concurrency ?? DEFAULT_CONCURRENCY;
56537
+ const results = await runWithConcurrency(regions, concurrency, async (region) => {
56538
+ if (!isValidRegionName(region)) {
56539
+ return {
56540
+ region,
56541
+ ok: false,
56542
+ error: `Invalid region '${region}'. Must match ${REGION_NAME_RE} (e.g. 'us-east-1').`,
56543
+ errorKind: "bad_input"
56544
+ };
56480
56545
  }
56481
- parent[idx] = clone2(op.value);
56482
- }
56483
- } else if (isObj(parent)) {
56484
- if (op.op === "remove") {
56485
- if (!(lastToken in parent)) {
56486
- throw new Error(`Cannot remove missing key '${lastToken}' at index ${i}.`);
56546
+ const r = await runAwsCall({
56547
+ service: i.service,
56548
+ operation: i.operation,
56549
+ params: i.params,
56550
+ query: i.query,
56551
+ profile: i.profile,
56552
+ region,
56553
+ outputFormat: i.outputFormat,
56554
+ timeoutMs: i.timeoutMs
56555
+ });
56556
+ if (!r.ok) {
56557
+ return {
56558
+ region,
56559
+ ok: false,
56560
+ command: r.command,
56561
+ error: r.error,
56562
+ errorKind: r.kind
56563
+ };
56487
56564
  }
56488
- delete parent[lastToken];
56489
- } else if (op.op === "replace") {
56490
- if (!(lastToken in parent)) {
56491
- throw new Error(`Cannot replace missing key '${lastToken}' at index ${i} (use 'add' to create it).`);
56565
+ return { region, ok: true, command: r.command, data: r.data };
56566
+ });
56567
+ const okCount = results.filter((r) => r.ok).length;
56568
+ const errCount = results.length - okCount;
56569
+ return {
56570
+ ok: true,
56571
+ data: {
56572
+ service: i.service,
56573
+ operation: i.operation,
56574
+ regionCount: regions.length,
56575
+ okCount,
56576
+ errorCount: errCount,
56577
+ results
56492
56578
  }
56493
- parent[lastToken] = clone2(op.value);
56494
- } else {
56495
- parent[lastToken] = clone2(op.value);
56496
- }
56497
- } else {
56498
- throw new Error(`Path '${op.path}' parent is not a container at index ${i}.`);
56499
- }
56500
- }
56501
- return root;
56502
- }
56503
- function summarizePatch(ops, before, after) {
56504
- let working;
56505
- let replayOk = true;
56506
- try {
56507
- working = clone2(before);
56508
- } catch {
56509
- replayOk = false;
56510
- working = void 0;
56511
- }
56512
- const out = [];
56513
- 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
- const beforeAt = resolvePointer(before, op.path);
56519
- let afterAt;
56520
- if (replayOk) {
56521
- try {
56522
- working = _applyJsonPatchInPlace(working, [op]);
56523
- afterAt = resolvePointer(working, op.path);
56524
- } catch {
56525
- replayOk = false;
56526
- afterAt = resolvePointer(after, op.path);
56527
- }
56528
- } else {
56529
- afterAt = resolvePointer(after, op.path);
56530
- }
56531
- if (op.op === "add" && afterAt === void 0 && op.path.endsWith("/-")) {
56532
- afterAt = op.value;
56533
- }
56534
- out.push({ op: op.op, path: op.path, before: beforeAt, after: afterAt });
56535
- }
56536
- return out;
56537
- }
56538
- function resolvePointer(doc, pointer) {
56539
- let tokens;
56540
- try {
56541
- tokens = parseJsonPointer(pointer);
56542
- } catch {
56543
- return void 0;
56544
- }
56545
- let cur = doc;
56546
- for (const t of tokens) {
56547
- if (Array.isArray(cur)) {
56548
- if (t === "-") return void 0;
56549
- const idx = Number.parseInt(t, 10);
56550
- if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return void 0;
56551
- cur = cur[idx];
56552
- } else if (isObj(cur)) {
56553
- if (!(t in cur)) return void 0;
56554
- cur = cur[t];
56555
- } else {
56556
- return void 0;
56579
+ };
56557
56580
  }
56558
56581
  }
56559
- return cur;
56560
- }
56582
+ ];
56561
56583
 
56562
56584
  // src/tools/script.ts
56563
56585
  import { createContext, runInContext } from "node:vm";
56564
56586
  var DEFAULT_TIMEOUT_MS2 = 6e4;
56565
56587
  var MAX_TIMEOUT_MS = 5 * 6e4;
56566
56588
  var MAX_LOG_LINES = 500;
56567
- var MAX_LOG_LINE_BYTES = 4 * 1024;
56589
+ var MAX_LOG_LINE_CHARS = 4 * 1024;
56568
56590
  var DEFAULT_MAX_PAGES = 50;
56569
56591
  var MAX_PAGES_HARD_CAP = 1e3;
56570
56592
  function findTool(name, source) {
@@ -56664,7 +56686,7 @@ async function runScript(opts, handlers = defaultScriptHandlers()) {
56664
56686
  }
56665
56687
  }
56666
56688
  }).join(" ");
56667
- const capped = text.length > MAX_LOG_LINE_BYTES ? `${text.slice(0, MAX_LOG_LINE_BYTES)}... [line truncated]` : text;
56689
+ const capped = text.length > MAX_LOG_LINE_CHARS ? `${text.slice(0, MAX_LOG_LINE_CHARS)}... [line truncated]` : text;
56668
56690
  logs.push(`[${level}] ${capped}`);
56669
56691
  };
56670
56692
  const ctx = createContext(
@@ -56797,9 +56819,11 @@ var scriptTools = [
56797
56819
  annotations: {
56798
56820
  title: "Run a JS snippet that orchestrates AWS tool calls",
56799
56821
  // The script may invoke destructive tools (resource.create/update/delete)
56800
- // so we conservatively annotate as non-read-only.
56822
+ // so annotate the worst case honestly: non-read-only AND destructive.
56823
+ // Cautious clients may confirm read-only scripts too -- acceptable cost;
56824
+ // an unflagged delete is not.
56801
56825
  readOnlyHint: false,
56802
- destructiveHint: false,
56826
+ destructiveHint: true,
56803
56827
  idempotentHint: false,
56804
56828
  openWorldHint: true
56805
56829
  },
@@ -56865,7 +56889,7 @@ var sessionTools = [
56865
56889
  if (!isValidProfileName(trimmed)) {
56866
56890
  return {
56867
56891
  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.`
56892
+ 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
56893
  };
56870
56894
  }
56871
56895
  }
@@ -56952,7 +56976,7 @@ function errorToMcpResult(err, toolName) {
56952
56976
  isError: true
56953
56977
  };
56954
56978
  }
56955
- var version2 = true ? "1.4.1" : (await null).createRequire(import.meta.url)("../package.json").version;
56979
+ var version2 = true ? "1.5.0" : (await null).createRequire(import.meta.url)("../package.json").version;
56956
56980
  var isEntryPoint = (() => {
56957
56981
  const entry = process.argv[1];
56958
56982
  if (!entry) return false;