@yawlabs/npmjs-mcp 0.12.0 → 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 +226 -107
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31429,7 +31429,13 @@ var accessTools = [
31429
31429
  registryGetAuth(`/-/package/${encPkg(input.name)}/access`),
31430
31430
  registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`)
31431
31431
  ]);
31432
- 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
+ }
31433
31439
  const result = {
31434
31440
  package: input.name,
31435
31441
  isScoped: input.name.startsWith("@")
@@ -31468,6 +31474,10 @@ var analysisTools = [
31468
31474
  packages: external_exports.array(external_exports.string()).min(2).max(5).describe("Package names to compare")
31469
31475
  }),
31470
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
+ }
31471
31481
  const partials = await Promise.all(
31472
31482
  input.packages.map(async (name) => {
31473
31483
  const [pkgRes, dlRes] = await Promise.all([
@@ -31514,9 +31524,11 @@ var analysisTools = [
31514
31524
  // ok:true with no data (api.ts), and dlRes.data!.downloads would throw
31515
31525
  // and crash the whole compare. Degrade to null instead.
31516
31526
  weeklyDownloads: dlRes.ok && dlRes.data ? dlRes.data.downloads : null,
31527
+ // versionCount includes ALL published versions (stable + pre-releases).
31517
31528
  versionCount: versionKeys.length,
31518
31529
  created: pkg.time.created,
31519
31530
  lastPublish: latest ? pkg.time[latest] : void 0,
31531
+ // `deprecated` is false when not deprecated, or the message string when deprecated.
31520
31532
  deprecated: latestVersion?.deprecated ?? false,
31521
31533
  hasReadme: !!(pkg.readme && pkg.readme.length > 0),
31522
31534
  repository: pkg.repository,
@@ -31579,12 +31591,15 @@ var analysisTools = [
31579
31591
  }
31580
31592
  avgDaysBetweenReleases = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
31581
31593
  }
31594
+ const STALE_DAYS = 365;
31595
+ const ACTIVE_DAYS = 90;
31582
31596
  const hasLicense = !!(pkg.license ?? latestVersion?.license);
31583
31597
  const hasReadme = !!(pkg.readme && pkg.readme.length > 0);
31584
31598
  const hasRepo = !!pkg.repository;
31585
31599
  const hasHomepage = !!pkg.homepage;
31586
31600
  const isDeprecated = !!latestVersion?.deprecated;
31587
- const isStale = daysSinceLastPublish !== null && daysSinceLastPublish > 365;
31601
+ const deprecatedMessage = latestVersion?.deprecated ?? false;
31602
+ const isStale = daysSinceLastPublish !== null && daysSinceLastPublish > STALE_DAYS;
31588
31603
  return {
31589
31604
  ok: true,
31590
31605
  status: 200,
@@ -31597,6 +31612,7 @@ var analysisTools = [
31597
31612
  weeklyDownloads: dlWeekRes.ok && dlWeekRes.data ? dlWeekRes.data.downloads : null,
31598
31613
  monthlyDownloads: dlMonthRes.ok && dlMonthRes.data ? dlMonthRes.data.downloads : null,
31599
31614
  maintainerCount: pkg.maintainers?.length ?? 0,
31615
+ // versionCount includes ALL published versions (stable + pre-releases).
31600
31616
  versionCount: versionKeys.length,
31601
31617
  daysSinceLastPublish,
31602
31618
  avgDaysBetweenReleases,
@@ -31606,7 +31622,8 @@ var analysisTools = [
31606
31622
  hasReadme,
31607
31623
  hasRepo,
31608
31624
  hasHomepage,
31609
- isDeprecated,
31625
+ // `isDeprecated` is false when not deprecated, or the message string when deprecated.
31626
+ isDeprecated: deprecatedMessage,
31610
31627
  isStale
31611
31628
  },
31612
31629
  // Holistic single-string verdict layered priority-first: a deprecated
@@ -31616,7 +31633,7 @@ var analysisTools = [
31616
31633
  // on the audit endpoint or a packument with no `latest` to audit) --
31617
31634
  // better to flag the unknown than confidently report ACTIVE on
31618
31635
  // unverified data. Then staleness, recency, and the catch-all.
31619
- 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"
31620
31637
  }
31621
31638
  };
31622
31639
  }
@@ -31690,6 +31707,7 @@ var analysisTools = [
31690
31707
  status: 200,
31691
31708
  data: {
31692
31709
  name: pkg.name,
31710
+ // totalVersions includes ALL published versions (stable + pre-releases).
31693
31711
  totalVersions: Object.keys(pkg.versions).length,
31694
31712
  analyzed: releases.length,
31695
31713
  created: pkg.time.created,
@@ -31797,6 +31815,10 @@ var authTools = [
31797
31815
  tokens: data.objects.map((t) => ({
31798
31816
  key: t.key,
31799
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,
31800
31822
  cidrWhitelist: t.cidr_whitelist,
31801
31823
  created: t.created,
31802
31824
  updated: t.updated
@@ -31941,7 +31963,7 @@ var dependencyTools = [
31941
31963
  inputSchema: external_exports.object({
31942
31964
  name: external_exports.string().describe("Package name"),
31943
31965
  version: external_exports.string().optional().describe("Semver version or dist-tag (default: 'latest')"),
31944
- 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)")
31945
31967
  }),
31946
31968
  handler: async (input) => {
31947
31969
  const maxDepth = input.depth ?? 3;
@@ -31957,7 +31979,18 @@ var dependencyTools = [
31957
31979
  resolved.add(hintKey);
31958
31980
  let pending = packumentCache.get(name);
31959
31981
  if (!pending) {
31960
- 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) => {
31961
31994
  if (!res.ok) {
31962
31995
  warnings.push(`Failed to fetch ${name}: ${res.error}`);
31963
31996
  return null;
@@ -32095,7 +32128,7 @@ var downloadTools = [
32095
32128
  },
32096
32129
  inputSchema: external_exports.object({
32097
32130
  name: external_exports.string().describe("Package name"),
32098
- 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')")
32099
32132
  }),
32100
32133
  handler: async (input) => {
32101
32134
  const period = input.period ?? "last-week";
@@ -32115,7 +32148,7 @@ var downloadTools = [
32115
32148
  },
32116
32149
  inputSchema: external_exports.object({
32117
32150
  name: external_exports.string().describe("Package name"),
32118
- 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')")
32119
32152
  }),
32120
32153
  handler: async (input) => {
32121
32154
  const period = input.period ?? "last-month";
@@ -32139,6 +32172,10 @@ var downloadTools = [
32139
32172
  }),
32140
32173
  handler: async (input) => {
32141
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
+ }
32142
32179
  const scoped = input.packages.filter((p) => p.startsWith("@"));
32143
32180
  if (scoped.length > 0) {
32144
32181
  return {
@@ -32267,7 +32304,17 @@ var hookTools = [
32267
32304
  const authErr = requireAuth();
32268
32305
  if (authErr) return authErr;
32269
32306
  const qs = new URLSearchParams();
32270
- 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
+ }
32271
32318
  if (input.limit !== void 0) qs.set("limit", String(input.limit));
32272
32319
  if (input.offset !== void 0) qs.set("offset", String(input.offset));
32273
32320
  const q = qs.toString();
@@ -32760,7 +32807,7 @@ var packageTools = [
32760
32807
  var provenanceTools = [
32761
32808
  {
32762
32809
  name: "npm_provenance",
32763
- 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.",
32764
32811
  annotations: {
32765
32812
  title: "Package provenance",
32766
32813
  readOnlyHint: true,
@@ -32773,6 +32820,9 @@ var provenanceTools = [
32773
32820
  version: external_exports.string().describe("Exact semver version (e.g. '1.0.0')")
32774
32821
  }),
32775
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
+ }
32776
32826
  const res = await registryGet(
32777
32827
  `/-/npm/v1/attestations/${encPkg(input.name)}@${encodeURIComponent(input.version)}`
32778
32828
  );
@@ -32788,8 +32838,10 @@ var provenanceTools = [
32788
32838
  package: input.name,
32789
32839
  version: input.version,
32790
32840
  attestationCount: attestations.length,
32791
- hasProvenance: attestations.some((a) => a.predicateType.includes("slsa.dev/provenance")),
32792
- 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
+ ),
32793
32845
  attestations
32794
32846
  }
32795
32847
  };
@@ -32825,7 +32877,7 @@ var registryTools = [
32825
32877
  title: "Recent registry changes",
32826
32878
  readOnlyHint: true,
32827
32879
  destructiveHint: false,
32828
- idempotentHint: true,
32880
+ idempotentHint: false,
32829
32881
  openWorldHint: true
32830
32882
  },
32831
32883
  inputSchema: external_exports.object({
@@ -32846,13 +32898,11 @@ var registryTools = [
32846
32898
  ok: true,
32847
32899
  status: 200,
32848
32900
  data: {
32849
- // `totalPackages` is the registry doc-count from replicate.npmjs.com
32850
- // (the entire npm registry, ~3M) -- NOT the count of returned
32851
- // changes. `changes.length` is the per-call result size. The field
32852
- // name reads as if it were a per-call count; this comment pins the
32853
- // semantic so a consumer reading "totalPackages: 50" doesn't assume
32854
- // only 50 packages exist on the registry.
32855
- totalPackages: 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,
32856
32906
  changes
32857
32907
  }
32858
32908
  };
@@ -32885,7 +32935,7 @@ var registryTools = [
32885
32935
  tool: "mcp_tool: npm_deprecate",
32886
32936
  requiresNpmToken: true,
32887
32937
  messageFormat: {
32888
- preferred: "Renamed to @scope/pkg \u2014 install that instead",
32938
+ preferred: "Renamed to @scope/pkg -- install that instead",
32889
32939
  avoid: "Renamed to @scope/pkg. Install that instead.",
32890
32940
  note: "Period-capital form has triggered 422 on at least one scoped package; use em-dash."
32891
32941
  }
@@ -33045,9 +33095,15 @@ var securityTools = [
33045
33095
  dependencies: external_exports.record(external_exports.string(), external_exports.string()).describe('Dependencies to audit as { "package": "version" }, e.g. { "express": "4.17.1" }')
33046
33096
  }),
33047
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
+ }
33048
33104
  const body = {
33049
33105
  name: input.name,
33050
- version: input.version ?? "1.0.0",
33106
+ version: version3,
33051
33107
  requires: input.dependencies,
33052
33108
  dependencies: Object.fromEntries(
33053
33109
  Object.entries(input.dependencies).map(([pkg, ver]) => [pkg, { version: ver }])
@@ -33103,12 +33159,20 @@ var trustTools = [
33103
33159
  if (c.type === "github") {
33104
33160
  result.repository = c.claims?.repository;
33105
33161
  const workflowRef = c.claims?.workflow_ref;
33106
- 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
+ }
33107
33167
  result.environment = c.claims?.environment;
33108
33168
  } else if (c.type === "gitlab") {
33109
33169
  result.project = c.claims?.project_path;
33110
33170
  const configRef = c.claims?.ci_config_ref_uri;
33111
- 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
+ }
33112
33176
  result.environment = c.claims?.environment;
33113
33177
  } else if (c.type === "circleci") {
33114
33178
  result.project = c.claims?.project_id;
@@ -33162,7 +33226,10 @@ var workflowTools = [
33162
33226
  }
33163
33227
  result.authenticated = true;
33164
33228
  result.username = whoamiRes.data.username;
33165
- 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
+ ]);
33166
33233
  if (profileRes.ok && profileRes.data) {
33167
33234
  const tfa = profileRes.data.tfa;
33168
33235
  if (tfa && !tfa.pending) {
@@ -33170,8 +33237,9 @@ var workflowTools = [
33170
33237
  } else {
33171
33238
  result.twoFactorAuth = "disabled";
33172
33239
  }
33240
+ } else {
33241
+ result.twoFactorAuth = "fetch-failed";
33173
33242
  }
33174
- const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
33175
33243
  if (tokensRes.ok && tokensRes.data) {
33176
33244
  const tokens = tokensRes.data.objects;
33177
33245
  const hasReadWrite = tokens.some((t) => !t.readonly);
@@ -33182,6 +33250,10 @@ var workflowTools = [
33182
33250
  result.canPublishHeadless = true;
33183
33251
  result.tokenType = "any (2FA disabled)";
33184
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.";
33185
33257
  } else {
33186
33258
  result.tokenType = "unknown (tokens are redacted in API)";
33187
33259
  result.canPublishHeadless = null;
@@ -33262,7 +33334,10 @@ var workflowTools = [
33262
33334
  });
33263
33335
  }
33264
33336
  if (username) {
33265
- 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
+ ]);
33266
33341
  if (profileRes.ok && profileRes.data) {
33267
33342
  const tfa = profileRes.data.tfa;
33268
33343
  if (tfa && !tfa.pending) {
@@ -33285,8 +33360,13 @@ var workflowTools = [
33285
33360
  detail: "2FA is disabled. Any valid token can publish, but 2FA is strongly recommended for security."
33286
33361
  });
33287
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
+ });
33288
33369
  }
33289
- const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
33290
33370
  if (tokensRes.ok && tokensRes.data) {
33291
33371
  const tokens = tokensRes.data.objects;
33292
33372
  const totalTokens = tokensRes.data.total;
@@ -33433,15 +33513,8 @@ function parseTeamTarget(target) {
33433
33513
  return { scope, team };
33434
33514
  }
33435
33515
  function highestVersion(versions) {
33436
- const parsed = [];
33437
- for (const v of versions) {
33438
- if (v.includes("-")) continue;
33439
- const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
33440
- if (m) parsed.push([Number(m[1]), Number(m[2]), Number(m[3]), v]);
33441
- }
33442
- if (parsed.length === 0) return null;
33443
- parsed.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
33444
- return parsed[parsed.length - 1][3];
33516
+ const stable = versions.filter((v) => !v.includes("-"));
33517
+ return maxSatisfying(stable, "*");
33445
33518
  }
33446
33519
  var writeTools = [
33447
33520
  // ───────────────────────────────────────────────────────
@@ -33459,7 +33532,7 @@ var writeTools = [
33459
33532
  },
33460
33533
  inputSchema: external_exports.object({
33461
33534
  name: external_exports.string().describe("Package name (e.g. '@yawlabs/spend')"),
33462
- 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."),
33463
33536
  versionRange: external_exports.string().optional().describe(
33464
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'."
33465
33538
  )
@@ -33469,46 +33542,50 @@ var writeTools = [
33469
33542
  if (authErr) return authErr;
33470
33543
  const problem = validateDeprecationMessage(input.message);
33471
33544
  if (problem) return { ok: false, status: 400, error: problem };
33472
- const pRes = await fetchPackument(input.name);
33473
- if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "deprecate (fetch)" });
33474
- const packument = pRes.data;
33475
- const allVersions = Object.keys(packument.versions || {});
33476
33545
  const range = input.versionRange ?? "*";
33477
- const affected = versionsSatisfying(allVersions, range);
33478
- if (affected.length === 0) {
33479
- return {
33480
- ok: false,
33481
- status: 400,
33482
- error: `No versions match range '${range}' for ${input.name}. Published versions: ${allVersions.join(", ") || "(none)"}.`
33483
- };
33484
- }
33485
- for (const v of affected) {
33486
- packument.versions[v].deprecated = input.message;
33487
- }
33488
- 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)" });
33489
33577
  return {
33490
- ok: false,
33491
- status: 500,
33492
- 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
+ }
33493
33586
  };
33494
33587
  }
33495
- delete packument._revisions;
33496
- delete packument._attachments;
33497
- const putRes = await registryPutAuth(
33498
- `/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`,
33499
- packument
33500
- );
33501
- if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "deprecate (write)" });
33502
- return {
33503
- ok: true,
33504
- status: 200,
33505
- data: {
33506
- package: input.name,
33507
- affectedVersions: affected,
33508
- totalAffected: affected.length,
33509
- message: input.message
33510
- }
33511
- };
33588
+ return { ok: false, status: 409, error: "Conflict after OCC retry -- try again." };
33512
33589
  }
33513
33590
  },
33514
33591
  // ───────────────────────────────────────────────────────
@@ -33623,15 +33700,16 @@ var writeTools = [
33623
33700
  return {
33624
33701
  ok: false,
33625
33702
  status: 500,
33626
- 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.`
33627
33704
  };
33628
33705
  }
33629
33706
  const tarballUrl = versionData.dist?.tarball;
33630
- const latestBefore = packument["dist-tags"]?.latest;
33707
+ const distTags = packument["dist-tags"] || {};
33708
+ const latestBefore = distTags.latest;
33631
33709
  delete packument.versions[input.version];
33632
- for (const tag of Object.keys(packument["dist-tags"] || {})) {
33633
- if (packument["dist-tags"][tag] === input.version) {
33634
- delete packument["dist-tags"][tag];
33710
+ for (const tag of Object.keys(distTags)) {
33711
+ if (distTags[tag] === input.version) {
33712
+ delete distTags[tag];
33635
33713
  }
33636
33714
  }
33637
33715
  if (latestBefore === input.version) {
@@ -33655,10 +33733,17 @@ var writeTools = [
33655
33733
  const freshRev = freshRes.ok ? freshRes.data._rev : void 0;
33656
33734
  if (freshRev) {
33657
33735
  try {
33658
- const pathname = new URL(tarballUrl).pathname;
33659
- const delRes = await registryDeleteAuth(`${pathname}/-rev/${encodeURIComponent(freshRev)}`);
33660
- tarballDeleted = delRes.ok;
33661
- 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
+ }
33662
33747
  } catch (err) {
33663
33748
  tarballError = err instanceof Error ? err.message : String(err);
33664
33749
  }
@@ -33715,7 +33800,7 @@ var writeTools = [
33715
33800
  return {
33716
33801
  ok: false,
33717
33802
  status: 500,
33718
- error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish.`
33803
+ error: `Packument for ${input.name} missing _rev -- cannot unpublish.`
33719
33804
  };
33720
33805
  }
33721
33806
  const delRes = await registryDeleteAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(rev)}`);
@@ -33750,6 +33835,16 @@ var writeTools = [
33750
33835
  if (authErr) return authErr;
33751
33836
  const tagErr = validateTag(input.tag);
33752
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
+ }
33753
33848
  const putRes = await registryPutAuth(
33754
33849
  `/-/package/${encPkg(input.name)}/dist-tags/${encTag(input.tag)}`,
33755
33850
  input.version
@@ -33855,7 +33950,7 @@ var writeTools = [
33855
33950
  return {
33856
33951
  ok: false,
33857
33952
  status: 500,
33858
- error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
33953
+ error: `Packument for ${input.name} missing _rev -- cannot update owners.`
33859
33954
  };
33860
33955
  }
33861
33956
  const maintainers = [...owners, userRecord];
@@ -33902,14 +33997,14 @@ var writeTools = [
33902
33997
  if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "owner_remove (fetch)" });
33903
33998
  const packument = pRes.data;
33904
33999
  const before = packument.maintainers || [];
33905
- if (!before.some((m) => m.name === input.username)) {
34000
+ if (!before.some((m) => m.name.toLowerCase() === input.username.toLowerCase())) {
33906
34001
  return {
33907
34002
  ok: false,
33908
34003
  status: 404,
33909
34004
  error: `${input.username} is not a maintainer of ${input.name}. Current maintainers: ${before.map((m) => m.name).join(", ") || "(none)"}.`
33910
34005
  };
33911
34006
  }
33912
- const after = before.filter((m) => m.name !== input.username);
34007
+ const after = before.filter((m) => m.name.toLowerCase() !== input.username.toLowerCase());
33913
34008
  if (after.length === 0) {
33914
34009
  return {
33915
34010
  ok: false,
@@ -33921,7 +34016,7 @@ var writeTools = [
33921
34016
  return {
33922
34017
  ok: false,
33923
34018
  status: 500,
33924
- error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
34019
+ error: `Packument for ${input.name} missing _rev -- cannot update owners.`
33925
34020
  };
33926
34021
  }
33927
34022
  const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
@@ -33946,7 +34041,7 @@ var writeTools = [
33946
34041
  // ───────────────────────────────────────────────────────
33947
34042
  {
33948
34043
  name: "npm_access_set",
33949
- 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.",
33950
34045
  annotations: {
33951
34046
  title: "Set package access level",
33952
34047
  readOnlyHint: false,
@@ -33956,17 +34051,18 @@ var writeTools = [
33956
34051
  },
33957
34052
  inputSchema: external_exports.object({
33958
34053
  name: external_exports.string().describe("Package name"),
33959
- 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)")
33960
34055
  }),
33961
34056
  handler: async (input) => {
33962
34057
  const authErr = requireAuth();
33963
34058
  if (authErr) return authErr;
33964
- 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 });
33965
34061
  if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set ${input.access}` });
33966
34062
  return {
33967
34063
  ok: true,
33968
34064
  status: 200,
33969
- data: { package: input.name, access: input.access }
34065
+ data: { package: input.name, access: wireAccess }
33970
34066
  };
33971
34067
  }
33972
34068
  },
@@ -34022,12 +34118,16 @@ var writeTools = [
34022
34118
  },
34023
34119
  inputSchema: external_exports.object({
34024
34120
  team: external_exports.string().describe("Team in the form '@scope:team' (e.g. '@yawlabs:devs')"),
34025
- 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
+ ),
34026
34124
  permissions: external_exports.enum(["read-only", "read-write"]).describe("Permission level")
34027
34125
  }),
34028
34126
  handler: async (input) => {
34029
34127
  const authErr = requireAuth();
34030
34128
  if (authErr) return authErr;
34129
+ const pkgErr = validatePackageName(input.package);
34130
+ if (pkgErr) return { ok: false, status: 400, error: pkgErr };
34031
34131
  const parsed = parseTeamTarget(input.team);
34032
34132
  if ("error" in parsed) {
34033
34133
  return { ok: false, status: 400, error: parsed.error };
@@ -34060,11 +34160,15 @@ var writeTools = [
34060
34160
  },
34061
34161
  inputSchema: external_exports.object({
34062
34162
  team: external_exports.string().describe("Team in the form '@scope:team'"),
34063
- 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
+ )
34064
34166
  }),
34065
34167
  handler: async (input) => {
34066
34168
  const authErr = requireAuth();
34067
34169
  if (authErr) return authErr;
34170
+ const pkgErr = validatePackageName(input.package);
34171
+ if (pkgErr) return { ok: false, status: 400, error: pkgErr };
34068
34172
  const parsed = parseTeamTarget(input.team);
34069
34173
  if ("error" in parsed) {
34070
34174
  return { ok: false, status: 400, error: parsed.error };
@@ -34235,22 +34339,30 @@ var writeTools = [
34235
34339
  inputSchema: external_exports.object({
34236
34340
  org: external_exports.string().describe("Organization name (with or without leading @)"),
34237
34341
  user: external_exports.string().describe("npm username"),
34238
- 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.")
34239
34344
  }),
34240
34345
  handler: async (input) => {
34241
34346
  const authErr = requireAuth();
34242
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
+ }
34243
34355
  const orgErr = validateScope(input.org);
34244
34356
  if (orgErr) return { ok: false, status: 400, error: orgErr };
34245
34357
  const userErr = validateUsername(input.user);
34246
34358
  if (userErr) return { ok: false, status: 400, error: userErr };
34247
- const org = input.org.replace(/^@/, "");
34248
- const user = input.user.replace(/^@/, "");
34249
- const body = { user };
34359
+ const userBare = input.user.replace(/^@/, "");
34360
+ const orgBare = input.org.replace(/^@/, "");
34361
+ const body = { user: userBare };
34250
34362
  if (input.role) body.role = input.role;
34251
- const res = await registryPutAuth(`/-/org/${encScope(org)}/user`, body);
34252
- if (!res.ok) return translateError(res, { op: `org_member_set ${org}/${user}` });
34253
- 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 };
34254
34366
  if (input.role) data.role = input.role;
34255
34367
  return { ok: true, status: 200, data };
34256
34368
  }
@@ -34287,11 +34399,11 @@ var writeTools = [
34287
34399
  if (orgErr) return { ok: false, status: 400, error: orgErr };
34288
34400
  const userErr = validateUsername(input.user);
34289
34401
  if (userErr) return { ok: false, status: 400, error: userErr };
34290
- const org = input.org.replace(/^@/, "");
34291
- const user = input.user.replace(/^@/, "");
34292
- const res = await registryDeleteAuth(`/-/org/${encScope(org)}/user`, { user });
34293
- if (!res.ok) return translateError(res, { op: `org_member_remove ${org}/${user}` });
34294
- 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 } };
34295
34407
  }
34296
34408
  },
34297
34409
  // ───────────────────────────────────────────────────────
@@ -34323,6 +34435,13 @@ var writeTools = [
34323
34435
  error: "token_revoke requires confirm: true. Revoking the token in NPM_TOKEN will break the next call."
34324
34436
  };
34325
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
+ }
34326
34445
  const res = await registryDeleteAuth(`/-/npm/v1/tokens/token/${encodeURIComponent(input.tokenKey)}`);
34327
34446
  if (!res.ok) return translateError(res, { op: "token_revoke" });
34328
34447
  return { ok: true, status: 200, data: { tokenKey: input.tokenKey, revoked: true } };
@@ -34331,7 +34450,7 @@ var writeTools = [
34331
34450
  ];
34332
34451
 
34333
34452
  // src/index.ts
34334
- var version2 = true ? "0.12.0" : (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;
34335
34454
  var subcommand = process.argv[2];
34336
34455
  if (subcommand === "version" || subcommand === "--version" || subcommand === "-v" || subcommand === "-V") {
34337
34456
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/npmjs-mcp",
3
- "version": "0.12.0",
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",