@yawlabs/npmjs-mcp 0.11.15 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +274 -117
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31347,12 +31347,23 @@ function translateError(res, context) {
31347
31347
  ...res,
31348
31348
  error: `Rate limited${opPart}. Retried automatically and still failed \u2014 wait longer and retry, or contact npm support if this persists. Raw: ${res.error}`
31349
31349
  };
31350
+ case 409:
31351
+ return {
31352
+ ...res,
31353
+ error: `Version conflict${pkgPart}${opPart}. The package metadata changed between read and write (a concurrent publish, deprecate, or registry _rev bump). Re-run the operation \u2014 it re-fetches the current _rev each call. Raw: ${res.error}`
31354
+ };
31350
31355
  case 0:
31351
31356
  return {
31352
31357
  ...res,
31353
31358
  error: `Network error${opPart}. Could not reach the registry. Raw: ${res.error}`
31354
31359
  };
31355
31360
  default:
31361
+ if (res.status >= 500) {
31362
+ return {
31363
+ ...res,
31364
+ error: `Registry server error${pkgPart}${opPart} (HTTP ${res.status}). Retried automatically and still failed \u2014 the registry is likely having a transient outage. Wait and retry; check https://status.npmjs.org if it persists. Raw: ${res.error}`
31365
+ };
31366
+ }
31356
31367
  return res;
31357
31368
  }
31358
31369
  }
@@ -31383,7 +31394,7 @@ var accessTools = [
31383
31394
  if (authErr) return authErr;
31384
31395
  const res = await registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`);
31385
31396
  if (!res.ok) return translateError(res, { pkg: input.name, op: "collaborators" });
31386
- const collaborators = Object.entries(res.data).map(([username, permissions]) => ({
31397
+ const collaborators = Object.entries(res.data ?? {}).map(([username, permissions]) => ({
31387
31398
  username,
31388
31399
  permissions
31389
31400
  }));
@@ -31418,7 +31429,13 @@ var accessTools = [
31418
31429
  registryGetAuth(`/-/package/${encPkg(input.name)}/access`),
31419
31430
  registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`)
31420
31431
  ]);
31421
- if (!accessRes.ok && !collabRes.ok) return translateError(collabRes, { pkg: input.name, op: "package_access" });
31432
+ if (!accessRes.ok && !collabRes.ok) {
31433
+ return {
31434
+ ok: false,
31435
+ status: accessRes.status,
31436
+ error: `Both package_access requests failed. access endpoint: HTTP ${accessRes.status}${accessRes.error ? ` (${accessRes.error})` : ""}; collaborators endpoint: HTTP ${collabRes.status}${collabRes.error ? ` (${collabRes.error})` : ""}.`
31437
+ };
31438
+ }
31422
31439
  const result = {
31423
31440
  package: input.name,
31424
31441
  isScoped: input.name.startsWith("@")
@@ -31431,7 +31448,7 @@ var accessTools = [
31431
31448
  result.access = accessRes.data;
31432
31449
  }
31433
31450
  if (collabRes.ok) {
31434
- result.collaborators = Object.entries(collabRes.data).map(([username, permissions]) => ({
31451
+ result.collaborators = Object.entries(collabRes.data ?? {}).map(([username, permissions]) => ({
31435
31452
  username,
31436
31453
  permissions
31437
31454
  }));
@@ -31457,6 +31474,10 @@ var analysisTools = [
31457
31474
  packages: external_exports.array(external_exports.string()).min(2).max(5).describe("Package names to compare")
31458
31475
  }),
31459
31476
  handler: async (input) => {
31477
+ for (const pkg of input.packages) {
31478
+ const nameErr = validatePackageName(pkg);
31479
+ if (nameErr) return { ok: false, status: 400, error: nameErr };
31480
+ }
31460
31481
  const partials = await Promise.all(
31461
31482
  input.packages.map(async (name) => {
31462
31483
  const [pkgRes, dlRes] = await Promise.all([
@@ -31499,10 +31520,15 @@ var analysisTools = [
31499
31520
  latest,
31500
31521
  license: pkg.license ?? latestVersion?.license,
31501
31522
  maintainers: pkg.maintainers?.map((m) => m.name),
31502
- weeklyDownloads: dlRes.ok ? dlRes.data.downloads : null,
31523
+ // Guard on data presence, not just .ok: a 2xx with an empty body yields
31524
+ // ok:true with no data (api.ts), and dlRes.data!.downloads would throw
31525
+ // and crash the whole compare. Degrade to null instead.
31526
+ weeklyDownloads: dlRes.ok && dlRes.data ? dlRes.data.downloads : null,
31527
+ // versionCount includes ALL published versions (stable + pre-releases).
31503
31528
  versionCount: versionKeys.length,
31504
31529
  created: pkg.time.created,
31505
31530
  lastPublish: latest ? pkg.time[latest] : void 0,
31531
+ // `deprecated` is false when not deprecated, or the message string when deprecated.
31506
31532
  deprecated: latestVersion?.deprecated ?? false,
31507
31533
  hasReadme: !!(pkg.readme && pkg.readme.length > 0),
31508
31534
  repository: pkg.repository,
@@ -31565,12 +31591,15 @@ var analysisTools = [
31565
31591
  }
31566
31592
  avgDaysBetweenReleases = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
31567
31593
  }
31594
+ const STALE_DAYS = 365;
31595
+ const ACTIVE_DAYS = 90;
31568
31596
  const hasLicense = !!(pkg.license ?? latestVersion?.license);
31569
31597
  const hasReadme = !!(pkg.readme && pkg.readme.length > 0);
31570
31598
  const hasRepo = !!pkg.repository;
31571
31599
  const hasHomepage = !!pkg.homepage;
31572
31600
  const isDeprecated = !!latestVersion?.deprecated;
31573
- const isStale = daysSinceLastPublish !== null && daysSinceLastPublish > 365;
31601
+ const deprecatedMessage = latestVersion?.deprecated ?? false;
31602
+ const isStale = daysSinceLastPublish !== null && daysSinceLastPublish > STALE_DAYS;
31574
31603
  return {
31575
31604
  ok: true,
31576
31605
  status: 200,
@@ -31578,9 +31607,12 @@ var analysisTools = [
31578
31607
  name: pkg.name,
31579
31608
  latest,
31580
31609
  signals: {
31581
- weeklyDownloads: dlWeekRes.ok ? dlWeekRes.data.downloads : null,
31582
- monthlyDownloads: dlMonthRes.ok ? dlMonthRes.data.downloads : null,
31610
+ // Guard on data presence, not just .ok: an empty-body 2xx yields
31611
+ // ok:true with no data (api.ts); degrade to null rather than throw.
31612
+ weeklyDownloads: dlWeekRes.ok && dlWeekRes.data ? dlWeekRes.data.downloads : null,
31613
+ monthlyDownloads: dlMonthRes.ok && dlMonthRes.data ? dlMonthRes.data.downloads : null,
31583
31614
  maintainerCount: pkg.maintainers?.length ?? 0,
31615
+ // versionCount includes ALL published versions (stable + pre-releases).
31584
31616
  versionCount: versionKeys.length,
31585
31617
  daysSinceLastPublish,
31586
31618
  avgDaysBetweenReleases,
@@ -31590,7 +31622,8 @@ var analysisTools = [
31590
31622
  hasReadme,
31591
31623
  hasRepo,
31592
31624
  hasHomepage,
31593
- isDeprecated,
31625
+ // `isDeprecated` is false when not deprecated, or the message string when deprecated.
31626
+ isDeprecated: deprecatedMessage,
31594
31627
  isStale
31595
31628
  },
31596
31629
  // Holistic single-string verdict layered priority-first: a deprecated
@@ -31600,7 +31633,7 @@ var analysisTools = [
31600
31633
  // on the audit endpoint or a packument with no `latest` to audit) --
31601
31634
  // better to flag the unknown than confidently report ACTIVE on
31602
31635
  // unverified data. Then staleness, recency, and the catch-all.
31603
- assessment: isDeprecated ? "DEPRECATED" : vulnerabilityCount !== null && vulnerabilityCount > 0 ? "VULNERABLE" : !auditReliable ? "AUDIT_UNKNOWN" : isStale ? "STALE" : daysSinceLastPublish !== null && daysSinceLastPublish < 90 ? "ACTIVE" : "MAINTENANCE"
31636
+ assessment: isDeprecated ? "DEPRECATED" : vulnerabilityCount !== null && vulnerabilityCount > 0 ? "VULNERABLE" : !auditReliable ? "AUDIT_UNKNOWN" : isStale ? "STALE" : daysSinceLastPublish !== null && daysSinceLastPublish < ACTIVE_DAYS ? "ACTIVE" : "MAINTENANCE"
31604
31637
  }
31605
31638
  };
31606
31639
  }
@@ -31674,6 +31707,7 @@ var analysisTools = [
31674
31707
  status: 200,
31675
31708
  data: {
31676
31709
  name: pkg.name,
31710
+ // totalVersions includes ALL published versions (stable + pre-releases).
31677
31711
  totalVersions: Object.keys(pkg.versions).length,
31678
31712
  analyzed: releases.length,
31679
31713
  created: pkg.time.created,
@@ -31781,6 +31815,10 @@ var authTools = [
31781
31815
  tokens: data.objects.map((t) => ({
31782
31816
  key: t.key,
31783
31817
  readonly: t.readonly,
31818
+ // Surface token class + automation flag so callers can tell automation
31819
+ // tokens (which bypass 2FA) from granular/legacy ones. Undefined drops out of JSON.
31820
+ type: t.type,
31821
+ automation: t.automation,
31784
31822
  cidrWhitelist: t.cidr_whitelist,
31785
31823
  created: t.created,
31786
31824
  updated: t.updated
@@ -31813,12 +31851,20 @@ var authTools = [
31813
31851
  error: `Token failed /-/whoami check. Token is invalid, expired, or revoked. Create a new one at https://www.npmjs.com/settings/~/tokens. Raw: ${whoami.error}`
31814
31852
  };
31815
31853
  }
31816
- const tfaData = profile.ok ? profile.data?.tfa : null;
31817
- const tfa = tfaData ? {
31818
- enabled: !tfaData.pending,
31819
- mode: tfaData.mode,
31820
- ...tfaData.pending ? { pending: true } : {}
31821
- } : { enabled: false };
31854
+ let tfa;
31855
+ if (!profile.ok) {
31856
+ tfa = {
31857
+ unknown: true,
31858
+ warning: `2FA status unknown: profile lookup failed (HTTP ${profile.status}). Token is valid (whoami passed) but write-readiness could not be fully assessed. Raw: ${profile.error}`
31859
+ };
31860
+ } else {
31861
+ const tfaData = profile.data?.tfa;
31862
+ tfa = tfaData ? {
31863
+ enabled: !tfaData.pending,
31864
+ mode: tfaData.mode,
31865
+ ...tfaData.pending ? { pending: true } : {}
31866
+ } : { enabled: false };
31867
+ }
31822
31868
  return {
31823
31869
  ok: true,
31824
31870
  status: 200,
@@ -31917,7 +31963,7 @@ var dependencyTools = [
31917
31963
  inputSchema: external_exports.object({
31918
31964
  name: external_exports.string().describe("Package name"),
31919
31965
  version: external_exports.string().optional().describe("Semver version or dist-tag (default: 'latest')"),
31920
- depth: external_exports.number().min(1).max(5).optional().describe("Max tree depth (default 3, max 5)")
31966
+ depth: external_exports.number().min(1).max(5).optional().describe("Max tree depth where the root counts as level 1 (default 3 = root + 2 transitive levels, max 5)")
31921
31967
  }),
31922
31968
  handler: async (input) => {
31923
31969
  const maxDepth = input.depth ?? 3;
@@ -31933,7 +31979,18 @@ var dependencyTools = [
31933
31979
  resolved.add(hintKey);
31934
31980
  let pending = packumentCache.get(name);
31935
31981
  if (!pending) {
31936
- pending = runLimited(() => registryGetAbbreviated(`/${encPkg(name)}`)).then((res) => {
31982
+ let encodedName;
31983
+ try {
31984
+ encodedName = encPkg(name);
31985
+ } catch {
31986
+ warnings.push(`Invalid package name "${name}": skipped`);
31987
+ if (!failedPackages.has(name)) {
31988
+ failedPackages.add(name);
31989
+ tree[hintKey] = { version: versionHint2, dependencies: {}, failed: true };
31990
+ }
31991
+ return;
31992
+ }
31993
+ pending = runLimited(() => registryGetAbbreviated(`/${encodedName}`)).then((res) => {
31937
31994
  if (!res.ok) {
31938
31995
  warnings.push(`Failed to fetch ${name}: ${res.error}`);
31939
31996
  return null;
@@ -32071,7 +32128,7 @@ var downloadTools = [
32071
32128
  },
32072
32129
  inputSchema: external_exports.object({
32073
32130
  name: external_exports.string().describe("Package name"),
32074
- period: external_exports.string().optional().describe("Period: 'last-day', 'last-week', 'last-month', 'last-year', or 'YYYY-MM-DD:YYYY-MM-DD'")
32131
+ period: external_exports.string().optional().describe("Period: 'last-day', 'last-week', 'last-month', 'last-year', or 'YYYY-MM-DD:YYYY-MM-DD' (default: 'last-week')")
32075
32132
  }),
32076
32133
  handler: async (input) => {
32077
32134
  const period = input.period ?? "last-week";
@@ -32091,7 +32148,7 @@ var downloadTools = [
32091
32148
  },
32092
32149
  inputSchema: external_exports.object({
32093
32150
  name: external_exports.string().describe("Package name"),
32094
- period: external_exports.string().optional().describe("Period: 'last-week', 'last-month', 'last-year', or 'YYYY-MM-DD:YYYY-MM-DD'")
32151
+ period: external_exports.string().optional().describe("Period: 'last-week', 'last-month', 'last-year', or 'YYYY-MM-DD:YYYY-MM-DD' (default: 'last-month')")
32095
32152
  }),
32096
32153
  handler: async (input) => {
32097
32154
  const period = input.period ?? "last-month";
@@ -32115,6 +32172,10 @@ var downloadTools = [
32115
32172
  }),
32116
32173
  handler: async (input) => {
32117
32174
  const period = input.period ?? "last-week";
32175
+ for (const pkg of input.packages) {
32176
+ const nameErr = validatePackageName(pkg);
32177
+ if (nameErr) return { ok: false, status: 400, error: nameErr };
32178
+ }
32118
32179
  const scoped = input.packages.filter((p) => p.startsWith("@"));
32119
32180
  if (scoped.length > 0) {
32120
32181
  return {
@@ -32243,7 +32304,17 @@ var hookTools = [
32243
32304
  const authErr = requireAuth();
32244
32305
  if (authErr) return authErr;
32245
32306
  const qs = new URLSearchParams();
32246
- if (input.package) qs.set("package", input.package);
32307
+ if (input.package) {
32308
+ const pkgErr = validatePackageName(input.package);
32309
+ if (pkgErr) {
32310
+ return {
32311
+ ok: false,
32312
+ status: 400,
32313
+ error: `Invalid package filter '${input.package}': ${pkgErr}`
32314
+ };
32315
+ }
32316
+ qs.set("package", input.package);
32317
+ }
32247
32318
  if (input.limit !== void 0) qs.set("limit", String(input.limit));
32248
32319
  if (input.offset !== void 0) qs.set("offset", String(input.offset));
32249
32320
  const q = qs.toString();
@@ -32736,7 +32807,7 @@ var packageTools = [
32736
32807
  var provenanceTools = [
32737
32808
  {
32738
32809
  name: "npm_provenance",
32739
- description: "Get Sigstore provenance attestations for a specific package version. Shows SLSA provenance (which CI built it, from which repo/commit) and publish attestations. Essential for supply chain security verification.",
32810
+ description: "Retrieve Sigstore attestations for a specific package version. Shows SLSA provenance (which CI built it, from which repo/commit) and publish attestations. NOTE: this tool RETRIEVES attestations from the registry -- it does NOT perform cryptographic signature, certificate-chain, or Rekor transparency-log verification. Use a dedicated Sigstore client to cryptographically verify the bundles.",
32740
32811
  annotations: {
32741
32812
  title: "Package provenance",
32742
32813
  readOnlyHint: true,
@@ -32749,11 +32820,14 @@ var provenanceTools = [
32749
32820
  version: external_exports.string().describe("Exact semver version (e.g. '1.0.0')")
32750
32821
  }),
32751
32822
  handler: async (input) => {
32823
+ if (!input.version || !input.version.trim()) {
32824
+ return { ok: false, status: 400, error: "version is required and must not be empty" };
32825
+ }
32752
32826
  const res = await registryGet(
32753
32827
  `/-/npm/v1/attestations/${encPkg(input.name)}@${encodeURIComponent(input.version)}`
32754
32828
  );
32755
32829
  if (!res.ok) return translateError(res, { pkg: input.name, op: `provenance ${input.version}` });
32756
- const attestations = (res.data.attestations ?? []).map((a) => ({
32830
+ const attestations = (res.data?.attestations ?? []).map((a) => ({
32757
32831
  predicateType: a.predicateType,
32758
32832
  bundle: a.bundle
32759
32833
  }));
@@ -32764,8 +32838,10 @@ var provenanceTools = [
32764
32838
  package: input.name,
32765
32839
  version: input.version,
32766
32840
  attestationCount: attestations.length,
32767
- hasProvenance: attestations.some((a) => a.predicateType.includes("slsa.dev/provenance")),
32768
- hasPublishAttestation: attestations.some((a) => a.predicateType.includes("npmjs.com/attestation")),
32841
+ hasProvenance: attestations.some((a) => a.predicateType.startsWith("https://slsa.dev/provenance")),
32842
+ hasPublishAttestation: attestations.some(
32843
+ (a) => a.predicateType.startsWith("https://npmjs.com/attestation")
32844
+ ),
32769
32845
  attestations
32770
32846
  }
32771
32847
  };
@@ -32801,7 +32877,7 @@ var registryTools = [
32801
32877
  title: "Recent registry changes",
32802
32878
  readOnlyHint: true,
32803
32879
  destructiveHint: false,
32804
- idempotentHint: true,
32880
+ idempotentHint: false,
32805
32881
  openWorldHint: true
32806
32882
  },
32807
32883
  inputSchema: external_exports.object({
@@ -32814,7 +32890,7 @@ var registryTools = [
32814
32890
  replicateGet(`/_changes?limit=${limit}&descending=true`)
32815
32891
  ]);
32816
32892
  if (!changesRes.ok) return translateError(changesRes, { op: "recent_changes" });
32817
- const changes = changesRes.data.results.map((r) => ({
32893
+ const changes = (changesRes.data?.results ?? []).map((r) => ({
32818
32894
  package: r.id,
32819
32895
  rev: r.changes[0]?.rev
32820
32896
  }));
@@ -32822,7 +32898,11 @@ var registryTools = [
32822
32898
  ok: true,
32823
32899
  status: 200,
32824
32900
  data: {
32825
- totalPackages: dbRes.ok ? dbRes.data.doc_count : null,
32901
+ // `registryPackageCount` is the registry-wide doc-count from
32902
+ // replicate.npmjs.com (the entire npm registry, ~3M) -- NOT the
32903
+ // count of returned changes. `changes.length` is the per-call
32904
+ // result size.
32905
+ registryPackageCount: dbRes.data?.doc_count ?? null,
32826
32906
  changes
32827
32907
  }
32828
32908
  };
@@ -32855,7 +32935,7 @@ var registryTools = [
32855
32935
  tool: "mcp_tool: npm_deprecate",
32856
32936
  requiresNpmToken: true,
32857
32937
  messageFormat: {
32858
- preferred: "Renamed to @scope/pkg \u2014 install that instead",
32938
+ preferred: "Renamed to @scope/pkg -- install that instead",
32859
32939
  avoid: "Renamed to @scope/pkg. Install that instead.",
32860
32940
  note: "Period-capital form has triggered 422 on at least one scoped package; use em-dash."
32861
32941
  }
@@ -32921,6 +33001,13 @@ var searchTools = [
32921
33001
  maintenance: external_exports.number().min(0).max(1).optional().describe("Weight for maintenance score (0-1)")
32922
33002
  }),
32923
33003
  handler: async (input) => {
33004
+ if (input.query.trim() === "") {
33005
+ return {
33006
+ ok: false,
33007
+ status: 400,
33008
+ error: "Query cannot be empty. Provide a search term or qualifier (e.g. 'mcp', 'keywords:react')."
33009
+ };
33010
+ }
32924
33011
  const params = new URLSearchParams({ text: input.query });
32925
33012
  if (input.size !== void 0) params.set("size", String(input.size));
32926
33013
  if (input.from !== void 0) params.set("from", String(input.from));
@@ -32929,7 +33016,8 @@ var searchTools = [
32929
33016
  if (input.maintenance !== void 0) params.set("maintenance", String(input.maintenance));
32930
33017
  const res = await registryGet(`/-/v1/search?${params}`);
32931
33018
  if (!res.ok) return translateError(res, { op: `search "${input.query}"` });
32932
- const results = res.data.objects.map((obj) => ({
33019
+ const objects = res.data?.objects ?? [];
33020
+ const results = objects.map((obj) => ({
32933
33021
  name: obj.package.name,
32934
33022
  version: obj.package.version,
32935
33023
  description: obj.package.description,
@@ -32940,7 +33028,7 @@ var searchTools = [
32940
33028
  links: obj.package.links,
32941
33029
  score: obj.score.detail
32942
33030
  }));
32943
- return { ok: true, status: 200, data: { total: res.data.total, results } };
33031
+ return { ok: true, status: 200, data: { total: res.data?.total ?? objects.length, results } };
32944
33032
  }
32945
33033
  }
32946
33034
  ];
@@ -33007,9 +33095,15 @@ var securityTools = [
33007
33095
  dependencies: external_exports.record(external_exports.string(), external_exports.string()).describe('Dependencies to audit as { "package": "version" }, e.g. { "express": "4.17.1" }')
33008
33096
  }),
33009
33097
  handler: async (input) => {
33098
+ const nameErr = validatePackageName(input.name);
33099
+ if (nameErr) return { ok: false, status: 400, error: nameErr };
33100
+ const version3 = input.version ?? "1.0.0";
33101
+ if (!version3.trim()) {
33102
+ return { ok: false, status: 400, error: "version must not be empty" };
33103
+ }
33010
33104
  const body = {
33011
33105
  name: input.name,
33012
- version: input.version ?? "1.0.0",
33106
+ version: version3,
33013
33107
  requires: input.dependencies,
33014
33108
  dependencies: Object.fromEntries(
33015
33109
  Object.entries(input.dependencies).map(([pkg, ver]) => [pkg, { version: ver }])
@@ -33065,12 +33159,20 @@ var trustTools = [
33065
33159
  if (c.type === "github") {
33066
33160
  result.repository = c.claims?.repository;
33067
33161
  const workflowRef = c.claims?.workflow_ref;
33068
- result.workflowFile = workflowRef?.file;
33162
+ if (typeof workflowRef === "string") {
33163
+ result.workflowFile = workflowRef;
33164
+ } else if (workflowRef && typeof workflowRef === "object") {
33165
+ result.workflowFile = workflowRef.file;
33166
+ }
33069
33167
  result.environment = c.claims?.environment;
33070
33168
  } else if (c.type === "gitlab") {
33071
33169
  result.project = c.claims?.project_path;
33072
33170
  const configRef = c.claims?.ci_config_ref_uri;
33073
- result.configFile = configRef?.file;
33171
+ if (typeof configRef === "string") {
33172
+ result.configFile = configRef;
33173
+ } else if (configRef && typeof configRef === "object") {
33174
+ result.configFile = configRef.file;
33175
+ }
33074
33176
  result.environment = c.claims?.environment;
33075
33177
  } else if (c.type === "circleci") {
33076
33178
  result.project = c.claims?.project_id;
@@ -33124,7 +33226,10 @@ var workflowTools = [
33124
33226
  }
33125
33227
  result.authenticated = true;
33126
33228
  result.username = whoamiRes.data.username;
33127
- const profileRes = await registryGetAuth("/-/npm/v1/user");
33229
+ const [profileRes, tokensRes] = await Promise.all([
33230
+ registryGetAuth("/-/npm/v1/user"),
33231
+ registryGetAuth("/-/npm/v1/tokens")
33232
+ ]);
33128
33233
  if (profileRes.ok && profileRes.data) {
33129
33234
  const tfa = profileRes.data.tfa;
33130
33235
  if (tfa && !tfa.pending) {
@@ -33132,8 +33237,9 @@ var workflowTools = [
33132
33237
  } else {
33133
33238
  result.twoFactorAuth = "disabled";
33134
33239
  }
33240
+ } else {
33241
+ result.twoFactorAuth = "fetch-failed";
33135
33242
  }
33136
- const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
33137
33243
  if (tokensRes.ok && tokensRes.data) {
33138
33244
  const tokens = tokensRes.data.objects;
33139
33245
  const hasReadWrite = tokens.some((t) => !t.readonly);
@@ -33144,6 +33250,10 @@ var workflowTools = [
33144
33250
  result.canPublishHeadless = true;
33145
33251
  result.tokenType = "any (2FA disabled)";
33146
33252
  result.recommendation = "2FA is disabled \u2014 any valid token can publish. Consider enabling 2FA for security.";
33253
+ } else if (result.twoFactorAuth === "fetch-failed") {
33254
+ result.canPublishHeadless = null;
33255
+ result.tokenType = "unknown (profile fetch failed)";
33256
+ result.recommendation = "Could not determine 2FA status -- token may lack read permission on /-/npm/v1/user. Verify the token has at least read access and re-run npm_check_auth.";
33147
33257
  } else {
33148
33258
  result.tokenType = "unknown (tokens are redacted in API)";
33149
33259
  result.canPublishHeadless = null;
@@ -33224,7 +33334,10 @@ var workflowTools = [
33224
33334
  });
33225
33335
  }
33226
33336
  if (username) {
33227
- const profileRes = await registryGetAuth("/-/npm/v1/user");
33337
+ const [profileRes, tokensRes] = await Promise.all([
33338
+ registryGetAuth("/-/npm/v1/user"),
33339
+ registryGetAuth("/-/npm/v1/tokens")
33340
+ ]);
33228
33341
  if (profileRes.ok && profileRes.data) {
33229
33342
  const tfa = profileRes.data.tfa;
33230
33343
  if (tfa && !tfa.pending) {
@@ -33247,8 +33360,13 @@ var workflowTools = [
33247
33360
  detail: "2FA is disabled. Any valid token can publish, but 2FA is strongly recommended for security."
33248
33361
  });
33249
33362
  }
33363
+ } else {
33364
+ checks.push({
33365
+ check: "2FA status",
33366
+ status: "warn",
33367
+ detail: "Could not determine 2FA status -- token may lack read permission on /-/npm/v1/user. Verify the token has at least read access and re-run npm_publish_preflight."
33368
+ });
33250
33369
  }
33251
- const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
33252
33370
  if (tokensRes.ok && tokensRes.data) {
33253
33371
  const tokens = tokensRes.data.objects;
33254
33372
  const totalTokens = tokensRes.data.total;
@@ -33395,15 +33513,8 @@ function parseTeamTarget(target) {
33395
33513
  return { scope, team };
33396
33514
  }
33397
33515
  function highestVersion(versions) {
33398
- const parsed = [];
33399
- for (const v of versions) {
33400
- if (v.includes("-")) continue;
33401
- const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
33402
- if (m) parsed.push([Number(m[1]), Number(m[2]), Number(m[3]), v]);
33403
- }
33404
- if (parsed.length === 0) return null;
33405
- parsed.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
33406
- return parsed[parsed.length - 1][3];
33516
+ const stable = versions.filter((v) => !v.includes("-"));
33517
+ return maxSatisfying(stable, "*");
33407
33518
  }
33408
33519
  var writeTools = [
33409
33520
  // ───────────────────────────────────────────────────────
@@ -33421,7 +33532,7 @@ var writeTools = [
33421
33532
  },
33422
33533
  inputSchema: external_exports.object({
33423
33534
  name: external_exports.string().describe("Package name (e.g. '@yawlabs/spend')"),
33424
- message: external_exports.string().describe("Deprecation message. Empty string to clear deprecation (use npm_undeprecate instead)."),
33535
+ message: external_exports.string().describe("Deprecation message. Use npm_undeprecate to clear."),
33425
33536
  versionRange: external_exports.string().optional().describe(
33426
33537
  "Semver range. Omit to deprecate ALL versions. Example: '<1.0.0' or '0.3.x'. Standard semver applies \u2014 bare integers are x-ranges (e.g. '0' means '0.x.x', not exact version 0). For a single version use '=1.2.3'."
33427
33538
  )
@@ -33431,46 +33542,50 @@ var writeTools = [
33431
33542
  if (authErr) return authErr;
33432
33543
  const problem = validateDeprecationMessage(input.message);
33433
33544
  if (problem) return { ok: false, status: 400, error: problem };
33434
- const pRes = await fetchPackument(input.name);
33435
- if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "deprecate (fetch)" });
33436
- const packument = pRes.data;
33437
- const allVersions = Object.keys(packument.versions || {});
33438
33545
  const range = input.versionRange ?? "*";
33439
- const affected = versionsSatisfying(allVersions, range);
33440
- if (affected.length === 0) {
33441
- return {
33442
- ok: false,
33443
- status: 400,
33444
- error: `No versions match range '${range}' for ${input.name}. Published versions: ${allVersions.join(", ") || "(none)"}.`
33445
- };
33446
- }
33447
- for (const v of affected) {
33448
- packument.versions[v].deprecated = input.message;
33449
- }
33450
- if (!packument._rev) {
33546
+ for (let attempt = 0; attempt <= 1; attempt++) {
33547
+ const pRes = await fetchPackument(input.name);
33548
+ if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "deprecate (fetch)" });
33549
+ const packument = pRes.data;
33550
+ const allVersions = Object.keys(packument.versions || {});
33551
+ const affected = versionsSatisfying(allVersions, range);
33552
+ if (affected.length === 0) {
33553
+ return {
33554
+ ok: false,
33555
+ status: 400,
33556
+ error: `No versions match range '${range}' for ${input.name}. Published versions: ${allVersions.join(", ") || "(none)"}.`
33557
+ };
33558
+ }
33559
+ if (!packument._rev) {
33560
+ return {
33561
+ ok: false,
33562
+ status: 500,
33563
+ error: `Packument for ${input.name} missing _rev -- cannot deprecate. Try again; this is usually transient.`
33564
+ };
33565
+ }
33566
+ for (const v of affected) {
33567
+ packument.versions[v].deprecated = input.message;
33568
+ }
33569
+ delete packument._revisions;
33570
+ delete packument._attachments;
33571
+ const putRes = await registryPutAuth(
33572
+ `/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`,
33573
+ packument
33574
+ );
33575
+ if (putRes.status === 409 && attempt === 0) continue;
33576
+ if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "deprecate (write)" });
33451
33577
  return {
33452
- ok: false,
33453
- status: 500,
33454
- error: `Packument for ${input.name} missing _rev -- cannot deprecate. Try again; this is usually transient.`
33578
+ ok: true,
33579
+ status: 200,
33580
+ data: {
33581
+ package: input.name,
33582
+ affectedVersions: affected,
33583
+ totalAffected: affected.length,
33584
+ message: input.message
33585
+ }
33455
33586
  };
33456
33587
  }
33457
- delete packument._revisions;
33458
- delete packument._attachments;
33459
- const putRes = await registryPutAuth(
33460
- `/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`,
33461
- packument
33462
- );
33463
- if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "deprecate (write)" });
33464
- return {
33465
- ok: true,
33466
- status: 200,
33467
- data: {
33468
- package: input.name,
33469
- affectedVersions: affected,
33470
- totalAffected: affected.length,
33471
- message: input.message
33472
- }
33473
- };
33588
+ return { ok: false, status: 409, error: "Conflict after OCC retry -- try again." };
33474
33589
  }
33475
33590
  },
33476
33591
  // ───────────────────────────────────────────────────────
@@ -33585,15 +33700,16 @@ var writeTools = [
33585
33700
  return {
33586
33701
  ok: false,
33587
33702
  status: 500,
33588
- error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish. Try again; this is usually transient.`
33703
+ error: `Packument for ${input.name} missing _rev -- cannot unpublish. Try again; this is usually transient.`
33589
33704
  };
33590
33705
  }
33591
33706
  const tarballUrl = versionData.dist?.tarball;
33592
- const latestBefore = packument["dist-tags"]?.latest;
33707
+ const distTags = packument["dist-tags"] || {};
33708
+ const latestBefore = distTags.latest;
33593
33709
  delete packument.versions[input.version];
33594
- for (const tag of Object.keys(packument["dist-tags"] || {})) {
33595
- if (packument["dist-tags"][tag] === input.version) {
33596
- delete packument["dist-tags"][tag];
33710
+ for (const tag of Object.keys(distTags)) {
33711
+ if (distTags[tag] === input.version) {
33712
+ delete distTags[tag];
33597
33713
  }
33598
33714
  }
33599
33715
  if (latestBefore === input.version) {
@@ -33617,10 +33733,17 @@ var writeTools = [
33617
33733
  const freshRev = freshRes.ok ? freshRes.data._rev : void 0;
33618
33734
  if (freshRev) {
33619
33735
  try {
33620
- const pathname = new URL(tarballUrl).pathname;
33621
- const delRes = await registryDeleteAuth(`${pathname}/-rev/${encodeURIComponent(freshRev)}`);
33622
- tarballDeleted = delRes.ok;
33623
- if (!delRes.ok) tarballError = delRes.error;
33736
+ const tarballParsed = new URL(tarballUrl);
33737
+ const registryOrigin = new URL(
33738
+ (process.env.NPM_REGISTRY || "https://registry.npmjs.org").replace(/\/+$/, "")
33739
+ ).origin;
33740
+ if (tarballParsed.origin !== registryOrigin) {
33741
+ tarballError = `tarball origin ${tarballParsed.origin} does not match registry origin ${registryOrigin} -- tarball DELETE skipped`;
33742
+ } else {
33743
+ const delRes = await registryDeleteAuth(`${tarballParsed.pathname}/-rev/${encodeURIComponent(freshRev)}`);
33744
+ tarballDeleted = delRes.ok;
33745
+ if (!delRes.ok) tarballError = delRes.error;
33746
+ }
33624
33747
  } catch (err) {
33625
33748
  tarballError = err instanceof Error ? err.message : String(err);
33626
33749
  }
@@ -33677,7 +33800,7 @@ var writeTools = [
33677
33800
  return {
33678
33801
  ok: false,
33679
33802
  status: 500,
33680
- error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish.`
33803
+ error: `Packument for ${input.name} missing _rev -- cannot unpublish.`
33681
33804
  };
33682
33805
  }
33683
33806
  const delRes = await registryDeleteAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(rev)}`);
@@ -33712,6 +33835,16 @@ var writeTools = [
33712
33835
  if (authErr) return authErr;
33713
33836
  const tagErr = validateTag(input.tag);
33714
33837
  if (tagErr) return { ok: false, status: 400, error: tagErr };
33838
+ const pRes = await fetchPackument(input.name);
33839
+ if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: `dist-tag set ${input.tag} (fetch)` });
33840
+ const packument = pRes.data;
33841
+ if (!packument.versions?.[input.version]) {
33842
+ return {
33843
+ ok: false,
33844
+ status: 404,
33845
+ error: `Version ${input.version} not found for ${input.name}. Published versions: ${Object.keys(packument.versions || {}).join(", ")}.`
33846
+ };
33847
+ }
33715
33848
  const putRes = await registryPutAuth(
33716
33849
  `/-/package/${encPkg(input.name)}/dist-tags/${encTag(input.tag)}`,
33717
33850
  input.version
@@ -33817,7 +33950,7 @@ var writeTools = [
33817
33950
  return {
33818
33951
  ok: false,
33819
33952
  status: 500,
33820
- error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
33953
+ error: `Packument for ${input.name} missing _rev -- cannot update owners.`
33821
33954
  };
33822
33955
  }
33823
33956
  const maintainers = [...owners, userRecord];
@@ -33864,14 +33997,14 @@ var writeTools = [
33864
33997
  if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "owner_remove (fetch)" });
33865
33998
  const packument = pRes.data;
33866
33999
  const before = packument.maintainers || [];
33867
- if (!before.some((m) => m.name === input.username)) {
34000
+ if (!before.some((m) => m.name.toLowerCase() === input.username.toLowerCase())) {
33868
34001
  return {
33869
34002
  ok: false,
33870
34003
  status: 404,
33871
34004
  error: `${input.username} is not a maintainer of ${input.name}. Current maintainers: ${before.map((m) => m.name).join(", ") || "(none)"}.`
33872
34005
  };
33873
34006
  }
33874
- const after = before.filter((m) => m.name !== input.username);
34007
+ const after = before.filter((m) => m.name.toLowerCase() !== input.username.toLowerCase());
33875
34008
  if (after.length === 0) {
33876
34009
  return {
33877
34010
  ok: false,
@@ -33883,7 +34016,7 @@ var writeTools = [
33883
34016
  return {
33884
34017
  ok: false,
33885
34018
  status: 500,
33886
- error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
34019
+ error: `Packument for ${input.name} missing _rev -- cannot update owners.`
33887
34020
  };
33888
34021
  }
33889
34022
  const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
@@ -33908,7 +34041,7 @@ var writeTools = [
33908
34041
  // ───────────────────────────────────────────────────────
33909
34042
  {
33910
34043
  name: "npm_access_set",
33911
- description: "Set package access level: 'public', 'private', or 'restricted'. Unscoped packages are always public. Private access requires a paid npm account.",
34044
+ description: "Set package access level: 'public' or 'restricted' (private). 'private' is accepted as an alias for 'restricted' for ergonomics -- both map to the registry wire value 'restricted'. Unscoped packages are always public. Restricted access requires a paid npm account.",
33912
34045
  annotations: {
33913
34046
  title: "Set package access level",
33914
34047
  readOnlyHint: false,
@@ -33918,17 +34051,18 @@ var writeTools = [
33918
34051
  },
33919
34052
  inputSchema: external_exports.object({
33920
34053
  name: external_exports.string().describe("Package name"),
33921
- access: external_exports.enum(["public", "private", "restricted"]).describe("Access level")
34054
+ access: external_exports.enum(["public", "private", "restricted"]).describe("Access level ('private' maps to 'restricted' on the wire)")
33922
34055
  }),
33923
34056
  handler: async (input) => {
33924
34057
  const authErr = requireAuth();
33925
34058
  if (authErr) return authErr;
33926
- const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, { access: input.access });
34059
+ const wireAccess = input.access === "private" ? "restricted" : input.access;
34060
+ const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, { access: wireAccess });
33927
34061
  if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set ${input.access}` });
33928
34062
  return {
33929
34063
  ok: true,
33930
34064
  status: 200,
33931
- data: { package: input.name, access: input.access }
34065
+ data: { package: input.name, access: wireAccess }
33932
34066
  };
33933
34067
  }
33934
34068
  },
@@ -33984,12 +34118,16 @@ var writeTools = [
33984
34118
  },
33985
34119
  inputSchema: external_exports.object({
33986
34120
  team: external_exports.string().describe("Team in the form '@scope:team' (e.g. '@yawlabs:devs')"),
33987
- package: external_exports.string().describe("Package name"),
34121
+ package: external_exports.string().describe(
34122
+ "Package name. Field is named 'package' to match the npm CLI (diverges from 'name' used elsewhere in this server)."
34123
+ ),
33988
34124
  permissions: external_exports.enum(["read-only", "read-write"]).describe("Permission level")
33989
34125
  }),
33990
34126
  handler: async (input) => {
33991
34127
  const authErr = requireAuth();
33992
34128
  if (authErr) return authErr;
34129
+ const pkgErr = validatePackageName(input.package);
34130
+ if (pkgErr) return { ok: false, status: 400, error: pkgErr };
33993
34131
  const parsed = parseTeamTarget(input.team);
33994
34132
  if ("error" in parsed) {
33995
34133
  return { ok: false, status: 400, error: parsed.error };
@@ -34022,11 +34160,15 @@ var writeTools = [
34022
34160
  },
34023
34161
  inputSchema: external_exports.object({
34024
34162
  team: external_exports.string().describe("Team in the form '@scope:team'"),
34025
- package: external_exports.string().describe("Package name")
34163
+ package: external_exports.string().describe(
34164
+ "Package name. Field is named 'package' to match the npm CLI (diverges from 'name' used elsewhere in this server)."
34165
+ )
34026
34166
  }),
34027
34167
  handler: async (input) => {
34028
34168
  const authErr = requireAuth();
34029
34169
  if (authErr) return authErr;
34170
+ const pkgErr = validatePackageName(input.package);
34171
+ if (pkgErr) return { ok: false, status: 400, error: pkgErr };
34030
34172
  const parsed = parseTeamTarget(input.team);
34031
34173
  if ("error" in parsed) {
34032
34174
  return { ok: false, status: 400, error: parsed.error };
@@ -34081,7 +34223,7 @@ var writeTools = [
34081
34223
  // ───────────────────────────────────────────────────────
34082
34224
  {
34083
34225
  name: "npm_team_delete",
34084
- description: "Delete a team. Team is passed as '@scope:team'. Revokes all package permissions that team held. Requires confirm: true \u2014 this removes the team and all its package grants in one call.",
34226
+ description: "Delete a team. Team is passed as '@scope:team'. Revokes all package permissions that team held, and team memberships are also removed. Requires confirm: true \u2014 this removes the team and all its package grants in one call. List the team's current grants with npm_team_packages first if you need to preserve them.",
34085
34227
  annotations: {
34086
34228
  title: "Delete team",
34087
34229
  readOnlyHint: false,
@@ -34197,22 +34339,30 @@ var writeTools = [
34197
34339
  inputSchema: external_exports.object({
34198
34340
  org: external_exports.string().describe("Organization name (with or without leading @)"),
34199
34341
  user: external_exports.string().describe("npm username"),
34200
- role: external_exports.enum(["developer", "admin", "owner"]).optional().describe("Role to assign")
34342
+ role: external_exports.enum(["developer", "admin", "owner"]).optional().describe("Role to assign"),
34343
+ confirm: external_exports.literal(true).describe("Must be literally true. Guards against accidental org membership changes.")
34201
34344
  }),
34202
34345
  handler: async (input) => {
34203
34346
  const authErr = requireAuth();
34204
34347
  if (authErr) return authErr;
34348
+ if (input.confirm !== true) {
34349
+ return {
34350
+ ok: false,
34351
+ status: 400,
34352
+ error: "org_member_set requires confirm: true. This adds or changes a user's org role."
34353
+ };
34354
+ }
34205
34355
  const orgErr = validateScope(input.org);
34206
34356
  if (orgErr) return { ok: false, status: 400, error: orgErr };
34207
34357
  const userErr = validateUsername(input.user);
34208
34358
  if (userErr) return { ok: false, status: 400, error: userErr };
34209
- const org = input.org.replace(/^@/, "");
34210
- const user = input.user.replace(/^@/, "");
34211
- const body = { user };
34359
+ const userBare = input.user.replace(/^@/, "");
34360
+ const orgBare = input.org.replace(/^@/, "");
34361
+ const body = { user: userBare };
34212
34362
  if (input.role) body.role = input.role;
34213
- const res = await registryPutAuth(`/-/org/${encScope(org)}/user`, body);
34214
- if (!res.ok) return translateError(res, { op: `org_member_set ${org}/${user}` });
34215
- const data = { org, user };
34363
+ const res = await registryPutAuth(`/-/org/${encScope(input.org)}/user`, body);
34364
+ if (!res.ok) return translateError(res, { op: `org_member_set ${orgBare}/${userBare}` });
34365
+ const data = { org: orgBare, user: userBare };
34216
34366
  if (input.role) data.role = input.role;
34217
34367
  return { ok: true, status: 200, data };
34218
34368
  }
@@ -34249,11 +34399,11 @@ var writeTools = [
34249
34399
  if (orgErr) return { ok: false, status: 400, error: orgErr };
34250
34400
  const userErr = validateUsername(input.user);
34251
34401
  if (userErr) return { ok: false, status: 400, error: userErr };
34252
- const org = input.org.replace(/^@/, "");
34253
- const user = input.user.replace(/^@/, "");
34254
- const res = await registryDeleteAuth(`/-/org/${encScope(org)}/user`, { user });
34255
- if (!res.ok) return translateError(res, { op: `org_member_remove ${org}/${user}` });
34256
- return { ok: true, status: 200, data: { org, removedUser: user } };
34402
+ const orgBare = input.org.replace(/^@/, "");
34403
+ const userBare = input.user.replace(/^@/, "");
34404
+ const res = await registryDeleteAuth(`/-/org/${encScope(input.org)}/user`, { user: userBare });
34405
+ if (!res.ok) return translateError(res, { op: `org_member_remove ${orgBare}/${userBare}` });
34406
+ return { ok: true, status: 200, data: { org: orgBare, removedUser: userBare } };
34257
34407
  }
34258
34408
  },
34259
34409
  // ───────────────────────────────────────────────────────
@@ -34285,6 +34435,13 @@ var writeTools = [
34285
34435
  error: "token_revoke requires confirm: true. Revoking the token in NPM_TOKEN will break the next call."
34286
34436
  };
34287
34437
  }
34438
+ if (!input.tokenKey || !/^[0-9a-f-]{8,}$/i.test(input.tokenKey)) {
34439
+ return {
34440
+ ok: false,
34441
+ status: 400,
34442
+ error: `Invalid token key '${input.tokenKey}'. Expected a UUID (hex characters and dashes, at least 8 characters). Use npm_tokens to list valid keys.`
34443
+ };
34444
+ }
34288
34445
  const res = await registryDeleteAuth(`/-/npm/v1/tokens/token/${encodeURIComponent(input.tokenKey)}`);
34289
34446
  if (!res.ok) return translateError(res, { op: "token_revoke" });
34290
34447
  return { ok: true, status: 200, data: { tokenKey: input.tokenKey, revoked: true } };
@@ -34293,7 +34450,7 @@ var writeTools = [
34293
34450
  ];
34294
34451
 
34295
34452
  // src/index.ts
34296
- var version2 = true ? "0.11.15" : (await null).createRequire(import.meta.url)("../package.json").version;
34453
+ var version2 = true ? "0.12.1" : (await null).createRequire(import.meta.url)("../package.json").version;
34297
34454
  var subcommand = process.argv[2];
34298
34455
  if (subcommand === "version" || subcommand === "--version" || subcommand === "-v" || subcommand === "-V") {
34299
34456
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/npmjs-mcp",
3
- "version": "0.11.15",
3
+ "version": "0.12.1",
4
4
  "mcpName": "io.github.YawLabs/npmjs-mcp",
5
5
  "description": "npm registry MCP server — package intelligence, security audits, and dependency analysis for AI assistants",
6
6
  "license": "MIT",