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