@yawlabs/aws-mcp 0.9.8 → 0.9.10

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.
Files changed (2) hide show
  1. package/dist/index.js +181 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -53570,6 +53570,14 @@ function killProc(proc, escalationMs = KILL_ESCALATION_MS) {
53570
53570
  // src/session.ts
53571
53571
  var sessionProfile;
53572
53572
  var sessionRegion;
53573
+ var PROFILE_NAME_RE = /^[A-Za-z0-9_+,.@:][A-Za-z0-9_+=,.@:-]{0,127}$/;
53574
+ var REGION_NAME_RE = /^[a-z][a-z0-9-]{2,30}$/;
53575
+ function isValidProfileName(name) {
53576
+ return PROFILE_NAME_RE.test(name);
53577
+ }
53578
+ function isValidRegionName(name) {
53579
+ return REGION_NAME_RE.test(name);
53580
+ }
53573
53581
  function getProfile() {
53574
53582
  return sessionProfile ?? process.env.AWS_PROFILE ?? "default";
53575
53583
  }
@@ -53580,13 +53588,25 @@ function setProfile(name) {
53580
53588
  if (!name?.trim()) {
53581
53589
  throw new Error("Profile name cannot be empty");
53582
53590
  }
53583
- sessionProfile = name.trim();
53591
+ const trimmed = name.trim();
53592
+ if (!isValidProfileName(trimmed)) {
53593
+ throw new Error(
53594
+ `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '=', no whitespace or shell metacharacters.`
53595
+ );
53596
+ }
53597
+ sessionProfile = trimmed;
53584
53598
  }
53585
53599
  function setRegion(name) {
53586
53600
  if (!name?.trim()) {
53587
53601
  throw new Error("Region cannot be empty");
53588
53602
  }
53589
- sessionRegion = name.trim();
53603
+ const trimmed = name.trim();
53604
+ if (!isValidRegionName(trimmed)) {
53605
+ throw new Error(
53606
+ `Invalid region '${trimmed}'. Must match /^[a-z][a-z0-9-]{2,30}$/ (e.g. 'us-east-1', 'eu-west-3').`
53607
+ );
53608
+ }
53609
+ sessionRegion = trimmed;
53590
53610
  }
53591
53611
  function clearProfile() {
53592
53612
  sessionProfile = void 0;
@@ -53672,6 +53692,20 @@ function runAwsCall(opts) {
53672
53692
  }
53673
53693
  const profile = opts.profile ?? getProfile();
53674
53694
  const region = opts.region ?? getRegion();
53695
+ if (!isValidProfileName(profile)) {
53696
+ return Promise.resolve({
53697
+ ok: false,
53698
+ kind: "bad_input",
53699
+ 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.`
53700
+ });
53701
+ }
53702
+ if (!isValidRegionName(region)) {
53703
+ return Promise.resolve({
53704
+ ok: false,
53705
+ kind: "bad_input",
53706
+ error: `Invalid region '${region}'. Must match /^[a-z][a-z0-9-]{2,30}$/ (e.g. 'us-east-1'). Check the 'region' arg or AWS_REGION / AWS_DEFAULT_REGION env var.`
53707
+ });
53708
+ }
53675
53709
  const outputFormat = opts.outputFormat ?? "json";
53676
53710
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
53677
53711
  const envCommand = process.env.AWS_MCP_TEST_AWS_COMMAND;
@@ -53844,9 +53878,11 @@ import {
53844
53878
  readFileSync,
53845
53879
  renameSync,
53846
53880
  statSync,
53881
+ unlinkSync,
53847
53882
  writeSync
53848
53883
  } from "node:fs";
53849
53884
  import { platform } from "node:os";
53885
+ import { setTimeout as delay } from "node:timers/promises";
53850
53886
  function splitSections(text) {
53851
53887
  const sections = [];
53852
53888
  let current = { header: null, body: "" };
@@ -53927,24 +53963,95 @@ function upsertProfileIntoText(text, profile, creds) {
53927
53963
  return sections.map((s) => s.header === null ? s.body : `${s.header}
53928
53964
  ${s.body}`).join("").replace(/\n*$/, "\n");
53929
53965
  }
53930
- function upsertProfile(path, profile, creds) {
53931
- const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
53932
- const nextText = upsertProfileIntoText(existing, profile, creds);
53933
- const tmpPath = `${path}.tmp-${process.pid}-${randomUUID()}`;
53934
- const fd = openSync(tmpPath, "w", 384);
53966
+ var LOCK_MAX_WAIT_MS = 1e4;
53967
+ var LOCK_STALE_AFTER_MS = 3e4;
53968
+ var LOCK_BASE_RETRY_MS = 25;
53969
+ var LOCK_MAX_RETRY_MS = 250;
53970
+ async function acquireLock(lockPath) {
53971
+ const start = Date.now();
53972
+ let attempt = 0;
53973
+ while (true) {
53974
+ if (Date.now() - start > LOCK_MAX_WAIT_MS) {
53975
+ throw new Error(
53976
+ `upsertProfile: failed to acquire lock at ${lockPath} after ${LOCK_MAX_WAIT_MS}ms. If a previous writer crashed, remove the lock file manually.`
53977
+ );
53978
+ }
53979
+ let fd = null;
53980
+ try {
53981
+ fd = openSync(lockPath, "wx");
53982
+ } catch (err) {
53983
+ const code = err.code;
53984
+ if (code !== "EEXIST") {
53985
+ throw err;
53986
+ }
53987
+ }
53988
+ if (fd !== null) {
53989
+ try {
53990
+ try {
53991
+ writeSync(fd, `pid=${process.pid} time=${Date.now()}
53992
+ `);
53993
+ } finally {
53994
+ closeSync(fd);
53995
+ }
53996
+ return;
53997
+ } catch (err) {
53998
+ try {
53999
+ unlinkSync(lockPath);
54000
+ } catch {
54001
+ }
54002
+ throw err;
54003
+ }
54004
+ }
54005
+ let shouldRetryImmediately = false;
54006
+ try {
54007
+ const st = statSync(lockPath);
54008
+ if (Date.now() - st.mtimeMs > LOCK_STALE_AFTER_MS) {
54009
+ try {
54010
+ unlinkSync(lockPath);
54011
+ } catch {
54012
+ }
54013
+ shouldRetryImmediately = true;
54014
+ }
54015
+ } catch {
54016
+ shouldRetryImmediately = true;
54017
+ }
54018
+ if (shouldRetryImmediately) continue;
54019
+ const cappedAttempt = Math.min(attempt, 8);
54020
+ const baseDelay = Math.min(LOCK_BASE_RETRY_MS * (1 + cappedAttempt), LOCK_MAX_RETRY_MS);
54021
+ await delay(baseDelay + Math.random() * baseDelay);
54022
+ attempt++;
54023
+ }
54024
+ }
54025
+ function releaseLock(lockPath) {
53935
54026
  try {
53936
- writeSync(fd, nextText);
53937
- } finally {
53938
- closeSync(fd);
54027
+ unlinkSync(lockPath);
54028
+ } catch {
53939
54029
  }
53940
- renameSync(tmpPath, path);
53941
- if (platform() !== "win32") {
53942
- let existingMode = 384;
53943
- if (existsSync(path)) {
53944
- const st = statSync(path);
53945
- existingMode = st.mode & 511;
54030
+ }
54031
+ async function upsertProfile(path, profile, creds) {
54032
+ const lockPath = `${path}.lock`;
54033
+ await acquireLock(lockPath);
54034
+ try {
54035
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
54036
+ const nextText = upsertProfileIntoText(existing, profile, creds);
54037
+ const tmpPath = `${path}.tmp-${process.pid}-${randomUUID()}`;
54038
+ const fd = openSync(tmpPath, "w", 384);
54039
+ try {
54040
+ writeSync(fd, nextText);
54041
+ } finally {
54042
+ closeSync(fd);
54043
+ }
54044
+ renameSync(tmpPath, path);
54045
+ if (platform() !== "win32") {
54046
+ let existingMode = 384;
54047
+ if (existsSync(path)) {
54048
+ const st = statSync(path);
54049
+ existingMode = st.mode & 511;
54050
+ }
54051
+ if ((existingMode & 63) !== 0) chmodSync(path, 384);
53946
54052
  }
53947
- if ((existingMode & 63) !== 0) chmodSync(path, 384);
54053
+ } finally {
54054
+ releaseLock(lockPath);
53948
54055
  }
53949
54056
  }
53950
54057
 
@@ -53985,6 +54092,12 @@ var assumeTools = [
53985
54092
  const sourceProfile = i.sourceProfile || getProfile();
53986
54093
  const useRegion = i.region || getRegion();
53987
54094
  const targetProfile = resolveTargetProfile({ targetProfile: i.targetProfile, sessionName: i.sessionName });
54095
+ if (!isValidProfileName(targetProfile)) {
54096
+ return {
54097
+ ok: false,
54098
+ 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.`
54099
+ };
54100
+ }
53988
54101
  const params = {
53989
54102
  RoleArn: i.roleArn,
53990
54103
  RoleSessionName: i.sessionName,
@@ -54020,7 +54133,7 @@ var assumeTools = [
54020
54133
  return { ok: false, error: "STS AssumeRole succeeded but returned incomplete credentials." };
54021
54134
  }
54022
54135
  const credentialsPath = join(homedir(), ".aws", "credentials");
54023
- upsertProfile(credentialsPath, targetProfile, {
54136
+ await upsertProfile(credentialsPath, targetProfile, {
54024
54137
  aws_access_key_id: creds.AccessKeyId,
54025
54138
  aws_secret_access_key: creds.SecretAccessKey,
54026
54139
  aws_session_token: creds.SessionToken
@@ -54049,11 +54162,22 @@ import { join as join3 } from "node:path";
54049
54162
 
54050
54163
  // src/sso.ts
54051
54164
  import { spawn as spawn2 } from "node:child_process";
54052
- import { randomUUID as randomUUID2 } from "node:crypto";
54165
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
54053
54166
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
54054
54167
  var MAX_STDERR_BYTES = 5 * 1024 * 1024;
54055
54168
  var sessions = /* @__PURE__ */ new Map();
54056
54169
  var pendingStarts = /* @__PURE__ */ new Map();
54170
+ function dedupeKey(profile, opts) {
54171
+ const payload = JSON.stringify({
54172
+ profile,
54173
+ command: opts.command ?? null,
54174
+ prefixArgs: opts.prefixArgs ?? null,
54175
+ urlWaitMs: opts.urlWaitMs ?? null,
54176
+ sessionTtlMs: opts.sessionTtlMs ?? null,
54177
+ env: opts.env ? Object.entries(opts.env).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0) : null
54178
+ });
54179
+ return createHash("sha256").update(payload).digest("hex");
54180
+ }
54057
54181
  var URL_RE = /https:\/\/device\.sso[.\w-]*\.amazonaws\.com\/[^\s]*/;
54058
54182
  var CODE_RE = /\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/;
54059
54183
  var URL_WAIT_MS = 15e3;
@@ -54065,13 +54189,20 @@ function _ttlKillswitchTick(s, proc, killFn = killProc) {
54065
54189
  killFn(proc);
54066
54190
  }
54067
54191
  function startSsoLogin(profile, opts = {}) {
54068
- const pending = pendingStarts.get(profile);
54192
+ if (!isValidProfileName(profile)) {
54193
+ return Promise.resolve({
54194
+ ok: false,
54195
+ error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-], must not start with '-' or '='.`
54196
+ });
54197
+ }
54198
+ const key = dedupeKey(profile, opts);
54199
+ const pending = pendingStarts.get(key);
54069
54200
  if (pending) return pending;
54070
54201
  const promise2 = doStartSsoLogin(profile, opts);
54071
- pendingStarts.set(profile, promise2);
54202
+ pendingStarts.set(key, promise2);
54072
54203
  void promise2.finally(() => {
54073
- if (pendingStarts.get(profile) === promise2) {
54074
- pendingStarts.delete(profile);
54204
+ if (pendingStarts.get(key) === promise2) {
54205
+ pendingStarts.delete(key);
54075
54206
  }
54076
54207
  });
54077
54208
  return promise2;
@@ -54722,7 +54853,6 @@ var import_node_html_parser = __toESM(require_dist2(), 1);
54722
54853
  var import_turndown = __toESM(require_turndown_cjs(), 1);
54723
54854
  import { randomUUID as randomUUID3 } from "node:crypto";
54724
54855
  var SEARCH_API_URL = "https://proxy.search.docs.aws.com/search";
54725
- var SESSION_UUID = randomUUID3();
54726
54856
  var USER_AGENT = "@yawlabs/aws-mcp (https://github.com/YawLabs/aws-mcp)";
54727
54857
  var DOCS_URL_RE = /^https:\/\/docs\.aws\.amazon\.com\/[^\s]*\.html(?:[?#][^\s]*)?$/i;
54728
54858
  var DEFAULT_MAX_LENGTH = 5e3;
@@ -54730,12 +54860,21 @@ var MAX_MAX_LENGTH = 1e6;
54730
54860
  var DEFAULT_SEARCH_LIMIT = 10;
54731
54861
  var MAX_SEARCH_LIMIT = 50;
54732
54862
  var FETCH_TIMEOUT_MS = 3e4;
54733
- var DOC_CACHE_MAX_ENTRIES = 16;
54863
+ var DOC_CACHE_MAX_ENTRIES = 64;
54734
54864
  var DOC_CACHE_TTL_MS = 5 * 6e4;
54865
+ var schemaWarned = false;
54735
54866
  function parseSearchResults(json2, limit) {
54736
54867
  if (!json2 || typeof json2 !== "object") return [];
54737
54868
  const suggestions = json2.suggestions;
54738
- if (!Array.isArray(suggestions)) return [];
54869
+ if (!Array.isArray(suggestions)) {
54870
+ if (!schemaWarned && Object.keys(json2).length > 0) {
54871
+ schemaWarned = true;
54872
+ console.warn(
54873
+ "[aws-mcp] aws_docs_search: response from proxy.search.docs.aws.com is missing the expected 'suggestions' array. The undocumented backend shape may have changed; aws_docs_search will return empty results until parseSearchResults is updated."
54874
+ );
54875
+ }
54876
+ return [];
54877
+ }
54739
54878
  const out = [];
54740
54879
  for (const s of suggestions) {
54741
54880
  if (out.length >= limit) break;
@@ -54872,6 +55011,7 @@ function makeDocCache() {
54872
55011
  }
54873
55012
  function buildDocsTools(fetchImpl = fetch) {
54874
55013
  const docCache = makeDocCache();
55014
+ const sessionUuid = randomUUID3();
54875
55015
  return [
54876
55016
  {
54877
55017
  name: "aws_docs_search",
@@ -54894,13 +55034,13 @@ function buildDocsTools(fetchImpl = fetch) {
54894
55034
  try {
54895
55035
  response = await fetchWithTimeout(
54896
55036
  fetchImpl,
54897
- `${SEARCH_API_URL}?session=${SESSION_UUID}`,
55037
+ `${SEARCH_API_URL}?session=${sessionUuid}`,
54898
55038
  {
54899
55039
  method: "POST",
54900
55040
  headers: {
54901
55041
  "Content-Type": "application/json",
54902
55042
  "User-Agent": USER_AGENT,
54903
- "X-MCP-Session-Id": SESSION_UUID
55043
+ "X-MCP-Session-Id": sessionUuid
54904
55044
  },
54905
55045
  body: JSON.stringify({
54906
55046
  textQuery: { input: i.query },
@@ -55030,7 +55170,7 @@ function buildDocsTools(fetchImpl = fetch) {
55030
55170
  var docsTools = buildDocsTools();
55031
55171
 
55032
55172
  // src/tools/iam-simulate.ts
55033
- var ARN_RE = /^arn:[a-z0-9-]{1,32}:[a-z0-9-]{0,32}:[a-z0-9-]{0,32}:[0-9]{0,32}:[^:\s][^\s]{0,1024}$/;
55173
+ var ARN_RE = /^arn:[a-z0-9-]{1,32}:[a-z0-9-]{1,32}:[a-z0-9-]{0,32}:(?:[0-9]{12})?:[^:\s][^\s]{0,1024}$/;
55034
55174
  var ACTION_RE = /^[a-z][a-z0-9-]{0,32}:[A-Za-z0-9*]{1,128}$/;
55035
55175
  var CONTEXT_KEY_TYPES = [
55036
55176
  "string",
@@ -55191,7 +55331,7 @@ var iamSimulateTools = [
55191
55331
  ];
55192
55332
 
55193
55333
  // src/tools/logs.ts
55194
- var SINCE_RE = /^\d+[smhdw]$/i;
55334
+ var SINCE_RE = /^\d+[smhdw]$/;
55195
55335
  var LOG_GROUP_RE = /^[.A-Za-z0-9_/#][.\-_/#A-Za-z0-9]{0,511}$/;
55196
55336
  var LOG_STREAM_NAME_RE = /^[^-:*\s][^:*]{0,511}$/;
55197
55337
  function isValidLogStreamName(name) {
@@ -55231,7 +55371,7 @@ var logsTools = [
55231
55371
  },
55232
55372
  inputSchema: external_exports3.object({
55233
55373
  logGroupName: external_exports3.string().min(1).describe("Log group name, e.g. '/aws/lambda/my-fn' or '/aws/ecs/my-service'. No leading 'logs/'."),
55234
- since: external_exports3.string().regex(SINCE_RE, "since must match /^\\d+[smhdw]$/i, e.g. '5m', '2h', '1d'").optional().describe("Window to tail: '<number><s|m|h|d|w>'. Default '10m'. Example: '30m', '1h', '3d'."),
55374
+ since: external_exports3.string().regex(SINCE_RE, "since must match /^\\d+[smhdw]$/ (lowercase units only), e.g. '5m', '2h', '1d'").optional().describe("Window to tail: '<number><s|m|h|d|w>'. Default '10m'. Example: '30m', '1h', '3d'."),
55235
55375
  filterPattern: external_exports3.string().optional().describe(
55236
55376
  `CloudWatch Logs filter pattern. E.g. 'ERROR', '"stack trace"', '[timestamp, request_id, level = ERROR, ...]'.`
55237
55377
  ),
@@ -56386,13 +56526,16 @@ async function runScript(opts, handlers = defaultScriptHandlers()) {
56386
56526
  ${opts.code}
56387
56527
  })()`;
56388
56528
  const started = Date.now();
56389
- let timer;
56529
+ let timeoutReject;
56390
56530
  const timeoutPromise = new Promise((_, reject) => {
56391
- timer = setTimeout(() => {
56392
- reject(new Error(`Script timed out after ${Math.round(timeoutMs / 1e3)}s. Raise timeoutMs or trim the script.`));
56393
- }, timeoutMs);
56531
+ timeoutReject = reject;
56394
56532
  });
56395
- if (timer && typeof timer.unref === "function") timer.unref();
56533
+ const timer = setTimeout(() => {
56534
+ timeoutReject(
56535
+ new Error(`Script timed out after ${Math.round(timeoutMs / 1e3)}s. Raise timeoutMs or trim the script.`)
56536
+ );
56537
+ }, timeoutMs);
56538
+ timer.unref();
56396
56539
  try {
56397
56540
  const evalResult = runInContext(wrappedSource, ctx, {
56398
56541
  timeout: timeoutMs,
@@ -56401,7 +56544,7 @@ ${opts.code}
56401
56544
  const data = await Promise.race([evalResult, timeoutPromise]);
56402
56545
  return { data, logs, truncatedLogs, durationMs: Date.now() - started };
56403
56546
  } finally {
56404
- if (timer) clearTimeout(timer);
56547
+ clearTimeout(timer);
56405
56548
  }
56406
56549
  }
56407
56550
  var scriptTools = [
@@ -56520,7 +56663,7 @@ var sessionTools = [
56520
56663
  ];
56521
56664
 
56522
56665
  // src/index.ts
56523
- var version2 = true ? "0.9.8" : (await null).createRequire(import.meta.url)("../package.json").version;
56666
+ var version2 = true ? "0.9.10" : (await null).createRequire(import.meta.url)("../package.json").version;
56524
56667
  var subcommand = process.argv[2];
56525
56668
  if (subcommand === "version" || subcommand === "--version") {
56526
56669
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/aws-mcp",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "mcpName": "io.github.YawLabs/aws-mcp",
5
5
  "description": "AWS MCP server — call any AWS API from AI assistants, with first-class SSO re-login (no more 'browser won't open' dead ends)",
6
6
  "license": "MIT",