@yawlabs/npmjs-mcp 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -3
  2. package/dist/index.js +795 -115
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,7 +65,7 @@ Add to `claude_desktop_config.json`:
65
65
  }
66
66
  ```
67
67
 
68
- ## Tools (46)
68
+ ## Tools (63)
69
69
 
70
70
  ### Search
71
71
  - `npm_search` — Search the npm registry with qualifiers (keywords, author, scope)
@@ -139,10 +139,25 @@ These bypass the CLI/2FA friction that causes `npm deprecate` and similar comman
139
139
  - `npm_deprecate` — Deprecate a package or specific versions (validates message format)
140
140
  - `npm_undeprecate` — Clear deprecation
141
141
  - `npm_unpublish_version` — Unpublish a specific version (requires `confirm: true`)
142
+ - `npm_unpublish_package` — Unpublish an entire package (requires `confirm: true`)
142
143
  - `npm_dist_tag_set` — Point a dist-tag at a version
143
144
  - `npm_dist_tag_remove` — Remove a dist-tag (except `latest`)
144
- - `npm_owner_add` — Add a maintainer
145
+ - `npm_owner_add` — Add a maintainer (resolves user via `/-/user/`)
145
146
  - `npm_owner_remove` — Remove a maintainer (prevents lockout)
147
+ - `npm_access_set` — Set public/private/restricted access
148
+ - `npm_access_set_mfa` — Configure 2FA requirement for publish (none/publish/automation)
149
+ - `npm_team_grant` / `npm_team_revoke` — Grant/revoke team permissions on a package
150
+ - `npm_team_create` / `npm_team_delete` — Create/delete a team in an org
151
+ - `npm_team_member_add` / `npm_team_member_remove` — Manage team members
152
+ - `npm_org_member_set` / `npm_org_member_remove` — Add/remove org members, set roles
153
+ - `npm_token_revoke` — Revoke an access token by key (creation requires a password and isn't exposed)
154
+
155
+ ### Webhooks (requires NPM_TOKEN)
156
+ - `npm_hook_add` — Register a webhook on a package, scope, or user
157
+ - `npm_hook_list` — List webhooks (optional package filter)
158
+ - `npm_hook_get` — Fetch a single webhook
159
+ - `npm_hook_update` — Update endpoint/secret of a webhook
160
+ - `npm_hook_remove` — Delete a webhook
146
161
 
147
162
  ### Operation Decision Matrix
148
163
 
@@ -158,7 +173,7 @@ Call `npm_ops_playbook` at the start of any session for the up-to-date matrix.
158
173
 
159
174
  ## Features
160
175
 
161
- - **37 tools** covering search, packages, deps, downloads, security, analysis, auth, orgs, provenance, trust, and publish workflows
176
+ - **63 tools** covering search, packages, deps, downloads, security, analysis, auth, orgs, access, provenance, trust, publish workflows, write operations, and registry webhooks
162
177
  - **No API key required** for read-only tools — authenticated tools opt-in via NPM_TOKEN
163
178
  - **Zero runtime dependencies** — Single bundled file for instant `npx` startup
164
179
  - **Agent-aware publish tools** — Detects non-interactive context, provides human hand-off actions instead of unworkable retries
package/dist/index.js CHANGED
@@ -21015,8 +21015,21 @@ var REGISTRY_URL = "https://registry.npmjs.org";
21015
21015
  var DOWNLOADS_URL = "https://api.npmjs.org";
21016
21016
  var REPLICATE_URL = "https://replicate.npmjs.com";
21017
21017
  var REQUEST_TIMEOUT_MS = 3e4;
21018
+ var PACKAGE_NAME_MAX_LENGTH = 214;
21019
+ var PACKAGE_NAME_PATTERN = /^(?:@[a-zA-Z0-9][a-zA-Z0-9\-_.]*\/)?[a-zA-Z0-9][a-zA-Z0-9\-_.]*$/;
21020
+ function validatePackageName(name) {
21021
+ if (typeof name !== "string" || name.length === 0) return "Package name is empty";
21022
+ if (name.length > PACKAGE_NAME_MAX_LENGTH) {
21023
+ return `Package name exceeds ${PACKAGE_NAME_MAX_LENGTH} characters (got ${name.length}).`;
21024
+ }
21025
+ if (!PACKAGE_NAME_PATTERN.test(name)) {
21026
+ return `Invalid package name '${name}'. Names must start with an alphanumeric character and contain only [a-zA-Z0-9-_.], optionally prefixed with '@scope/' for scoped packages.`;
21027
+ }
21028
+ return null;
21029
+ }
21018
21030
  function encPkg(name) {
21019
- if (!name || name === "@") throw new Error("Invalid package name");
21031
+ const err = validatePackageName(name);
21032
+ if (err) throw new Error(err);
21020
21033
  return name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
21021
21034
  }
21022
21035
  function isAuthenticated() {
@@ -21081,11 +21094,14 @@ function registryPost(path, body) {
21081
21094
  function registryGetAuth(path) {
21082
21095
  return request(REGISTRY_URL, path, { headers: authHeaders() });
21083
21096
  }
21097
+ function registryPostAuth(path, body) {
21098
+ return request(REGISTRY_URL, path, { method: "POST", body, headers: authHeaders() });
21099
+ }
21084
21100
  function registryPutAuth(path, body) {
21085
21101
  return request(REGISTRY_URL, path, { method: "PUT", body, headers: authHeaders() });
21086
21102
  }
21087
- function registryDeleteAuth(path) {
21088
- return request(REGISTRY_URL, path, { method: "DELETE", headers: authHeaders() });
21103
+ function registryDeleteAuth(path, body) {
21104
+ return request(REGISTRY_URL, path, { method: "DELETE", body, headers: authHeaders() });
21089
21105
  }
21090
21106
  function downloadsGet(path) {
21091
21107
  return request(DOWNLOADS_URL, path);
@@ -21229,6 +21245,61 @@ function maxSatisfying(versions, range) {
21229
21245
  return best;
21230
21246
  }
21231
21247
 
21248
+ // src/errors.ts
21249
+ function translateError(res, context) {
21250
+ if (res.ok) return res;
21251
+ const pkgPart = context.pkg ? ` for ${context.pkg}` : "";
21252
+ const opPart = context.op ? ` during ${context.op}` : "";
21253
+ switch (res.status) {
21254
+ case 401:
21255
+ return {
21256
+ ...res,
21257
+ error: `Authentication failed${pkgPart}${opPart}. Your NPM_TOKEN may be invalid, expired, or lack write scope. Create a Granular Access Token with 'Read and write' permission at https://www.npmjs.com/settings/~/tokens, or use a classic Automation token (which bypasses 2FA). Raw: ${res.error}`
21258
+ };
21259
+ case 403:
21260
+ return {
21261
+ ...res,
21262
+ error: `Not authorized${pkgPart}${opPart}. You may not be a maintainer of this package, or the token's scope doesn't include it. Check current maintainers with npm_collaborators or npm_package_access. Raw: ${res.error}`
21263
+ };
21264
+ case 404:
21265
+ return {
21266
+ ...res,
21267
+ error: `Not found${pkgPart}${opPart}. Check the exact package name (scoped packages require the @scope/ prefix). If the version is specified, verify it exists with npm_package. Raw: ${res.error}`
21268
+ };
21269
+ case 422:
21270
+ return {
21271
+ ...res,
21272
+ error: `Registry rejected the request payload${pkgPart}${opPart} (422 Unprocessable Entity). Most common causes: (1) invalid semver range \u2014 validate with npm_package versions first; (2) deprecation message format \u2014 em-dash form works, period-capital form sometimes 422s; (3) account-level 2FA policy requires interactive CLI session. If #3, CLI fallback: \`npm login --auth-type=web\` followed by the npm CLI command. Raw: ${res.error}`
21273
+ };
21274
+ case 429:
21275
+ return {
21276
+ ...res,
21277
+ error: `Rate limited${opPart}. Wait 60 seconds and retry. Raw: ${res.error}`
21278
+ };
21279
+ case 0:
21280
+ return {
21281
+ ...res,
21282
+ error: `Network error${opPart}. Could not reach the registry. Raw: ${res.error}`
21283
+ };
21284
+ default:
21285
+ return res;
21286
+ }
21287
+ }
21288
+ function validateDeprecationMessage(msg) {
21289
+ if (msg.length > 1024) {
21290
+ return "Deprecation message exceeds 1024 characters (registry limit).";
21291
+ }
21292
+ if (msg.length === 0) return null;
21293
+ if (/\.\s+[A-Z]/.test(msg)) {
21294
+ return `Deprecation message contains the "period + space + capital letter" pattern that has triggered 422 Unprocessable Entity on at least one scoped package. The working form uses em-dash and lowercase continuation: e.g. "Renamed to @yawlabs/spend \u2014 install that instead". Pass force: true to bypass this validation.`;
21295
+ }
21296
+ return null;
21297
+ }
21298
+ function versionsMatchingRange(versions, range, maxSatisfying2) {
21299
+ if (range === "*" || range === "") return [...versions];
21300
+ return versions.filter((v) => maxSatisfying2([v], range) === v);
21301
+ }
21302
+
21232
21303
  // src/tools/access.ts
21233
21304
  var accessTools = [
21234
21305
  {
@@ -21248,7 +21319,7 @@ var accessTools = [
21248
21319
  const authErr = requireAuth();
21249
21320
  if (authErr) return authErr;
21250
21321
  const res = await registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`);
21251
- if (!res.ok) return res;
21322
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "collaborators" });
21252
21323
  const collaborators = Object.entries(res.data).map(([username, permissions]) => ({
21253
21324
  username,
21254
21325
  permissions
@@ -21284,7 +21355,7 @@ var accessTools = [
21284
21355
  registryGetAuth(`/-/package/${encPkg(input.name)}/access`),
21285
21356
  registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`)
21286
21357
  ]);
21287
- if (!accessRes.ok && !collabRes.ok) return collabRes;
21358
+ if (!accessRes.ok && !collabRes.ok) return translateError(collabRes, { pkg: input.name, op: "package_access" });
21288
21359
  const result = {
21289
21360
  package: input.name,
21290
21361
  isScoped: input.name.startsWith("@")
@@ -21385,7 +21456,7 @@ var analysisTools = [
21385
21456
  downloadsGet(`/downloads/point/last-week/${encPkg(input.name)}`),
21386
21457
  downloadsGet(`/downloads/point/last-month/${encPkg(input.name)}`)
21387
21458
  ]);
21388
- if (!pkgRes.ok) return pkgRes;
21459
+ if (!pkgRes.ok) return translateError(pkgRes, { pkg: input.name, op: "health" });
21389
21460
  const pkg = pkgRes.data;
21390
21461
  const latest = pkg["dist-tags"]?.latest;
21391
21462
  const latestVersion = latest ? pkg.versions[latest] : void 0;
@@ -21459,7 +21530,7 @@ var analysisTools = [
21459
21530
  }),
21460
21531
  handler: async (input) => {
21461
21532
  const res = await registryGet(`/${encPkg(input.name)}`);
21462
- if (!res.ok) return res;
21533
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "maintainers" });
21463
21534
  const pkg = res.data;
21464
21535
  const publishCounts = {};
21465
21536
  for (const ver of Object.values(pkg.versions)) {
@@ -21495,7 +21566,7 @@ var analysisTools = [
21495
21566
  }),
21496
21567
  handler: async (input) => {
21497
21568
  const res = await registryGet(`/${encPkg(input.name)}`);
21498
- if (!res.ok) return res;
21569
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "release_frequency" });
21499
21570
  const pkg = res.data;
21500
21571
  const limit = input.limit ?? 20;
21501
21572
  const releases = Object.keys(pkg.versions).map((v) => ({ version: v, date: pkg.time[v] })).filter((r) => r.date).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, limit);
@@ -21542,7 +21613,8 @@ var authTools = [
21542
21613
  handler: async () => {
21543
21614
  const authErr = requireAuth();
21544
21615
  if (authErr) return authErr;
21545
- return registryGetAuth("/-/whoami");
21616
+ const res = await registryGetAuth("/-/whoami");
21617
+ return res.ok ? res : translateError(res, { op: "whoami" });
21546
21618
  }
21547
21619
  },
21548
21620
  {
@@ -21560,7 +21632,7 @@ var authTools = [
21560
21632
  const authErr = requireAuth();
21561
21633
  if (authErr) return authErr;
21562
21634
  const res = await registryGetAuth("/-/npm/v1/user");
21563
- if (!res.ok) return res;
21635
+ if (!res.ok) return translateError(res, { op: "profile" });
21564
21636
  const p = res.data;
21565
21637
  return {
21566
21638
  ok: true,
@@ -21604,7 +21676,7 @@ var authTools = [
21604
21676
  const qs = params.toString();
21605
21677
  const path = `/-/npm/v1/tokens${qs ? `?${qs}` : ""}`;
21606
21678
  const res = await registryGetAuth(path);
21607
- if (!res.ok) return res;
21679
+ if (!res.ok) return translateError(res, { op: "tokens" });
21608
21680
  const data = res.data;
21609
21681
  return {
21610
21682
  ok: true,
@@ -21678,7 +21750,7 @@ var authTools = [
21678
21750
  const res = await registryGetAuth(
21679
21751
  `/-/user/org.couchdb.user:${encodeURIComponent(input.username)}/package`
21680
21752
  );
21681
- if (!res.ok) return res;
21753
+ if (!res.ok) return translateError(res, { op: `user_packages ${input.username}` });
21682
21754
  const packages = Object.entries(res.data).map(([name, access]) => ({ name, access }));
21683
21755
  return {
21684
21756
  ok: true,
@@ -21712,7 +21784,7 @@ var dependencyTools = [
21712
21784
  handler: async (input) => {
21713
21785
  const ver = input.version ?? "latest";
21714
21786
  const res = await registryGet(`/${encPkg(input.name)}/${ver}`);
21715
- if (!res.ok) return res;
21787
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `dependencies ${ver}` });
21716
21788
  const v = res.data;
21717
21789
  return {
21718
21790
  ok: true,
@@ -21827,7 +21899,7 @@ var dependencyTools = [
21827
21899
  handler: async (input) => {
21828
21900
  const ver = input.version ?? "latest";
21829
21901
  const res = await registryGet(`/${encPkg(input.name)}/${ver}`);
21830
- if (!res.ok) return res;
21902
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `license_check ${ver}` });
21831
21903
  const pkg = res.data;
21832
21904
  const depEntries = Object.entries(pkg.dependencies ?? {});
21833
21905
  const runLimited = createLimiter(10);
@@ -21885,7 +21957,8 @@ var downloadTools = [
21885
21957
  }),
21886
21958
  handler: async (input) => {
21887
21959
  const period = input.period ?? "last-week";
21888
- return downloadsGet(`/downloads/point/${period}/${encPkg(input.name)}`);
21960
+ const res = await downloadsGet(`/downloads/point/${period}/${encPkg(input.name)}`);
21961
+ return res.ok ? res : translateError(res, { pkg: input.name, op: `downloads ${period}` });
21889
21962
  }
21890
21963
  },
21891
21964
  {
@@ -21904,7 +21977,8 @@ var downloadTools = [
21904
21977
  }),
21905
21978
  handler: async (input) => {
21906
21979
  const period = input.period ?? "last-month";
21907
- return downloadsGet(`/downloads/range/${period}/${encPkg(input.name)}`);
21980
+ const res = await downloadsGet(`/downloads/range/${period}/${encPkg(input.name)}`);
21981
+ return res.ok ? res : translateError(res, { pkg: input.name, op: `downloads_range ${period}` });
21908
21982
  }
21909
21983
  },
21910
21984
  {
@@ -21924,7 +21998,8 @@ var downloadTools = [
21924
21998
  handler: async (input) => {
21925
21999
  const period = input.period ?? "last-week";
21926
22000
  const names = input.packages.map((p) => encPkg(p)).join(",");
21927
- return downloadsGet(`/downloads/point/${period}/${names}`);
22001
+ const res = await downloadsGet(`/downloads/point/${period}/${names}`);
22002
+ return res.ok ? res : translateError(res, { op: `downloads_bulk ${period}` });
21928
22003
  }
21929
22004
  },
21930
22005
  {
@@ -21943,7 +22018,142 @@ var downloadTools = [
21943
22018
  }),
21944
22019
  handler: async (input) => {
21945
22020
  const period = input.period ?? "last-week";
21946
- return downloadsGet(`/versions/${encPkg(input.name)}/${period}`);
22021
+ const res = await downloadsGet(`/versions/${encPkg(input.name)}/${period}`);
22022
+ return res.ok ? res : translateError(res, { pkg: input.name, op: `version_downloads ${period}` });
22023
+ }
22024
+ }
22025
+ ];
22026
+
22027
+ // src/tools/hooks.ts
22028
+ function classifyHookTarget(target) {
22029
+ if (target.startsWith("~")) return { type: "owner", name: target.slice(1) };
22030
+ if (/^@[^/]+$/.test(target)) return { type: "scope", name: target };
22031
+ return { type: "package", name: target };
22032
+ }
22033
+ var hookTools = [
22034
+ {
22035
+ name: "npm_hook_add",
22036
+ description: "Create a registry webhook. Target is 'pkg' or '@scope/pkg' for a package, '@scope' for a scope, or '~user' for a user's packages. Endpoint is the HTTPS URL to POST events to; secret is used to HMAC-sign payloads.",
22037
+ annotations: {
22038
+ title: "Add webhook",
22039
+ readOnlyHint: false,
22040
+ destructiveHint: true,
22041
+ idempotentHint: false,
22042
+ openWorldHint: true
22043
+ },
22044
+ inputSchema: external_exports.object({
22045
+ target: external_exports.string().describe("Hook target: 'pkg', '@scope/pkg', '@scope', or '~user'"),
22046
+ endpoint: external_exports.string().url().describe("HTTPS URL that will receive POST events"),
22047
+ secret: external_exports.string().describe("Secret used to HMAC-sign webhook payloads")
22048
+ }),
22049
+ handler: async (input) => {
22050
+ const authErr = requireAuth();
22051
+ if (authErr) return authErr;
22052
+ const { type, name } = classifyHookTarget(input.target);
22053
+ const res = await registryPostAuth("/-/npm/v1/hooks/hook", {
22054
+ type,
22055
+ name,
22056
+ endpoint: input.endpoint,
22057
+ secret: input.secret
22058
+ });
22059
+ if (!res.ok) return translateError(res, { op: `hook_add ${input.target}` });
22060
+ return { ok: true, status: 200, data: res.data };
22061
+ }
22062
+ },
22063
+ {
22064
+ name: "npm_hook_list",
22065
+ description: "List webhooks. Optionally filter by package name.",
22066
+ annotations: {
22067
+ title: "List webhooks",
22068
+ readOnlyHint: true,
22069
+ destructiveHint: false,
22070
+ idempotentHint: true,
22071
+ openWorldHint: true
22072
+ },
22073
+ inputSchema: external_exports.object({
22074
+ package: external_exports.string().optional().describe("Filter by package name"),
22075
+ limit: external_exports.number().int().optional().describe("Max results"),
22076
+ offset: external_exports.number().int().optional().describe("Pagination offset")
22077
+ }),
22078
+ handler: async (input) => {
22079
+ const authErr = requireAuth();
22080
+ if (authErr) return authErr;
22081
+ const qs = new URLSearchParams();
22082
+ if (input.package) qs.set("package", input.package);
22083
+ if (input.limit !== void 0) qs.set("limit", String(input.limit));
22084
+ if (input.offset !== void 0) qs.set("offset", String(input.offset));
22085
+ const q = qs.toString();
22086
+ const res = await registryGetAuth(`/-/npm/v1/hooks${q ? `?${q}` : ""}`);
22087
+ if (!res.ok) return translateError(res, { op: "hook_list" });
22088
+ return { ok: true, status: 200, data: res.data };
22089
+ }
22090
+ },
22091
+ {
22092
+ name: "npm_hook_get",
22093
+ description: "Get a single webhook by its ID.",
22094
+ annotations: {
22095
+ title: "Get webhook",
22096
+ readOnlyHint: true,
22097
+ destructiveHint: false,
22098
+ idempotentHint: true,
22099
+ openWorldHint: true
22100
+ },
22101
+ inputSchema: external_exports.object({
22102
+ id: external_exports.string().describe("Hook ID (UUID from npm_hook_list)")
22103
+ }),
22104
+ handler: async (input) => {
22105
+ const authErr = requireAuth();
22106
+ if (authErr) return authErr;
22107
+ const res = await registryGetAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`);
22108
+ if (!res.ok) return translateError(res, { op: `hook_get ${input.id}` });
22109
+ return { ok: true, status: 200, data: res.data };
22110
+ }
22111
+ },
22112
+ {
22113
+ name: "npm_hook_update",
22114
+ description: "Update a webhook's endpoint and/or secret.",
22115
+ annotations: {
22116
+ title: "Update webhook",
22117
+ readOnlyHint: false,
22118
+ destructiveHint: true,
22119
+ idempotentHint: true,
22120
+ openWorldHint: true
22121
+ },
22122
+ inputSchema: external_exports.object({
22123
+ id: external_exports.string().describe("Hook ID"),
22124
+ endpoint: external_exports.string().url().describe("New HTTPS URL"),
22125
+ secret: external_exports.string().describe("New signing secret")
22126
+ }),
22127
+ handler: async (input) => {
22128
+ const authErr = requireAuth();
22129
+ if (authErr) return authErr;
22130
+ const res = await registryPutAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`, {
22131
+ endpoint: input.endpoint,
22132
+ secret: input.secret
22133
+ });
22134
+ if (!res.ok) return translateError(res, { op: `hook_update ${input.id}` });
22135
+ return { ok: true, status: 200, data: res.data };
22136
+ }
22137
+ },
22138
+ {
22139
+ name: "npm_hook_remove",
22140
+ description: "Delete a webhook by ID.",
22141
+ annotations: {
22142
+ title: "Remove webhook",
22143
+ readOnlyHint: false,
22144
+ destructiveHint: true,
22145
+ idempotentHint: true,
22146
+ openWorldHint: true
22147
+ },
22148
+ inputSchema: external_exports.object({
22149
+ id: external_exports.string().describe("Hook ID")
22150
+ }),
22151
+ handler: async (input) => {
22152
+ const authErr = requireAuth();
22153
+ if (authErr) return authErr;
22154
+ const res = await registryDeleteAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`);
22155
+ if (!res.ok) return translateError(res, { op: `hook_remove ${input.id}` });
22156
+ return { ok: true, status: 200, data: { id: input.id, removed: true } };
21947
22157
  }
21948
22158
  }
21949
22159
  ];
@@ -21967,7 +22177,7 @@ var orgTools = [
21967
22177
  const authErr = requireAuth();
21968
22178
  if (authErr) return authErr;
21969
22179
  const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/user`);
21970
- if (!res.ok) return res;
22180
+ if (!res.ok) return translateError(res, { op: `org_members ${input.org}` });
21971
22181
  const members = Object.entries(res.data).map(([username, role]) => ({ username, role }));
21972
22182
  return {
21973
22183
  ok: true,
@@ -21997,7 +22207,7 @@ var orgTools = [
21997
22207
  const authErr = requireAuth();
21998
22208
  if (authErr) return authErr;
21999
22209
  const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/package`);
22000
- if (!res.ok) return res;
22210
+ if (!res.ok) return translateError(res, { op: `org_packages ${input.org}` });
22001
22211
  const packages = Object.entries(res.data).map(([name, access]) => ({ name, access }));
22002
22212
  return {
22003
22213
  ok: true,
@@ -22027,7 +22237,7 @@ var orgTools = [
22027
22237
  const authErr = requireAuth();
22028
22238
  if (authErr) return authErr;
22029
22239
  const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/team`);
22030
- if (!res.ok) return res;
22240
+ if (!res.ok) return translateError(res, { op: `org_teams ${input.org}` });
22031
22241
  return {
22032
22242
  ok: true,
22033
22243
  status: 200,
@@ -22059,7 +22269,7 @@ var orgTools = [
22059
22269
  const res = await registryGetAuth(
22060
22270
  `/-/team/${encodeURIComponent(input.org)}/${encodeURIComponent(input.team)}/package`
22061
22271
  );
22062
- if (!res.ok) return res;
22272
+ if (!res.ok) return translateError(res, { op: `team_packages ${input.org}:${input.team}` });
22063
22273
  const packages = Object.entries(res.data).map(([name, permissions]) => ({ name, permissions }));
22064
22274
  return {
22065
22275
  ok: true,
@@ -22092,7 +22302,7 @@ var packageTools = [
22092
22302
  }),
22093
22303
  handler: async (input) => {
22094
22304
  const res = await registryGet(`/${encPkg(input.name)}`);
22095
- if (!res.ok) return res;
22305
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "package" });
22096
22306
  const pkg = res.data;
22097
22307
  const latest = pkg["dist-tags"]?.latest;
22098
22308
  const latestVersion = latest ? pkg.versions[latest] : void 0;
@@ -22136,7 +22346,7 @@ var packageTools = [
22136
22346
  handler: async (input) => {
22137
22347
  const ver = input.version ?? "latest";
22138
22348
  const res = await registryGet(`/${encPkg(input.name)}/${ver}`);
22139
- if (!res.ok) return res;
22349
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `version ${ver}` });
22140
22350
  const v = res.data;
22141
22351
  return {
22142
22352
  ok: true,
@@ -22186,7 +22396,7 @@ var packageTools = [
22186
22396
  }),
22187
22397
  handler: async (input) => {
22188
22398
  const res = await registryGet(`/${encPkg(input.name)}`);
22189
- if (!res.ok) return res;
22399
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "versions" });
22190
22400
  const pkg = res.data;
22191
22401
  const limit = input.limit ?? 50;
22192
22402
  const allVersions = Object.keys(pkg.versions).map((v) => ({
@@ -22224,7 +22434,7 @@ var packageTools = [
22224
22434
  }),
22225
22435
  handler: async (input) => {
22226
22436
  const res = await registryGet(`/${encPkg(input.name)}`);
22227
- if (!res.ok) return res;
22437
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "readme" });
22228
22438
  const readme = res.data.readme;
22229
22439
  if (!readme) {
22230
22440
  return { ok: true, status: 200, data: { name: input.name, readme: "(no readme available)" } };
@@ -22246,7 +22456,8 @@ var packageTools = [
22246
22456
  name: external_exports.string().describe("Package name")
22247
22457
  }),
22248
22458
  handler: async (input) => {
22249
- return registryGet(`/-/package/${encPkg(input.name)}/dist-tags`);
22459
+ const res = await registryGet(`/-/package/${encPkg(input.name)}/dist-tags`);
22460
+ return res.ok ? res : translateError(res, { pkg: input.name, op: "dist_tags" });
22250
22461
  }
22251
22462
  },
22252
22463
  {
@@ -22276,7 +22487,7 @@ var packageTools = [
22276
22487
  registryGet(`/${encPkg(input.name)}/${ver}`),
22277
22488
  registryGet(`/${encPkg(typesPackage)}`)
22278
22489
  ]);
22279
- if (!versionRes.ok) return versionRes;
22490
+ if (!versionRes.ok) return translateError(versionRes, { pkg: input.name, op: `types ${ver}` });
22280
22491
  const v = versionRes.data;
22281
22492
  const hasBuiltinTypes = !!(v.types || v.typings);
22282
22493
  const typesEntry = hasBuiltinTypes ? v.types ?? v.typings : void 0;
@@ -22324,9 +22535,9 @@ var provenanceTools = [
22324
22535
  }),
22325
22536
  handler: async (input) => {
22326
22537
  const res = await registryGet(
22327
- `/-/npm/v1/attestations/${encPkg(input.name)}@${input.version}`
22538
+ `/-/npm/v1/attestations/${encPkg(input.name)}@${encodeURIComponent(input.version)}`
22328
22539
  );
22329
- if (!res.ok) return res;
22540
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `provenance ${input.version}` });
22330
22541
  const attestations = (res.data.attestations ?? []).map((a) => ({
22331
22542
  predicateType: a.predicateType,
22332
22543
  bundle: a.bundle
@@ -22364,7 +22575,8 @@ var registryTools = [
22364
22575
  }),
22365
22576
  handler: async (input) => {
22366
22577
  const period = input.period ?? "last-week";
22367
- return downloadsGet(`/downloads/point/${period}`);
22578
+ const res = await downloadsGet(`/downloads/point/${period}`);
22579
+ return res.ok ? res : translateError(res, { op: `registry_stats ${period}` });
22368
22580
  }
22369
22581
  },
22370
22582
  {
@@ -22386,7 +22598,7 @@ var registryTools = [
22386
22598
  replicateGet("/"),
22387
22599
  replicateGet(`/_changes?limit=${limit}&descending=true`)
22388
22600
  ]);
22389
- if (!changesRes.ok) return changesRes;
22601
+ if (!changesRes.ok) return translateError(changesRes, { op: "recent_changes" });
22390
22602
  const changes = changesRes.data.results.map((r) => ({
22391
22603
  package: r.id,
22392
22604
  rev: r.changes[0]?.rev
@@ -22497,7 +22709,7 @@ var searchTools = [
22497
22709
  if (input.popularity !== void 0) params.set("popularity", String(input.popularity));
22498
22710
  if (input.maintenance !== void 0) params.set("maintenance", String(input.maintenance));
22499
22711
  const res = await registryGet(`/-/v1/search?${params}`);
22500
- if (!res.ok) return res;
22712
+ if (!res.ok) return translateError(res, { op: `search "${input.query}"` });
22501
22713
  const results = res.data.objects.map((obj) => ({
22502
22714
  name: obj.package.name,
22503
22715
  version: obj.package.version,
@@ -22530,7 +22742,34 @@ var securityTools = [
22530
22742
  packages: external_exports.record(external_exports.array(external_exports.string())).describe('Object mapping package names to arrays of version strings, e.g. {"lodash": ["4.17.20"]}')
22531
22743
  }),
22532
22744
  handler: async (input) => {
22533
- return registryPost("/-/npm/v1/security/advisories/bulk", input.packages);
22745
+ const res = await registryPost("/-/npm/v1/security/advisories/bulk", input.packages);
22746
+ if (!res.ok) return translateError(res, { op: "audit" });
22747
+ const advisoriesByPackage = res.data ?? {};
22748
+ const summary = Object.entries(advisoriesByPackage).map(([name, advisories]) => {
22749
+ const list = Array.isArray(advisories) ? advisories : [];
22750
+ const severityCounts = {};
22751
+ for (const adv of list) {
22752
+ const severity = adv?.severity ?? "unknown";
22753
+ severityCounts[severity] = (severityCounts[severity] ?? 0) + 1;
22754
+ }
22755
+ return { name, advisoryCount: list.length, severityCounts };
22756
+ });
22757
+ const queried = Object.keys(input.packages);
22758
+ const vulnerable = summary.filter((s) => s.advisoryCount > 0).map((s) => s.name);
22759
+ const clean = queried.filter((n) => !vulnerable.includes(n));
22760
+ return {
22761
+ ok: true,
22762
+ status: 200,
22763
+ data: {
22764
+ queriedCount: queried.length,
22765
+ vulnerableCount: vulnerable.length,
22766
+ cleanCount: clean.length,
22767
+ vulnerable,
22768
+ clean,
22769
+ summary,
22770
+ advisories: advisoriesByPackage
22771
+ }
22772
+ };
22534
22773
  }
22535
22774
  },
22536
22775
  {
@@ -22557,7 +22796,8 @@ var securityTools = [
22557
22796
  Object.entries(input.dependencies).map(([pkg, ver]) => [pkg, { version: ver }])
22558
22797
  )
22559
22798
  };
22560
- return registryPost("/-/npm/v1/security/audits", body);
22799
+ const res = await registryPost("/-/npm/v1/security/audits", body);
22800
+ return res.ok ? res : translateError(res, { pkg: input.name, op: "audit_deep" });
22561
22801
  }
22562
22802
  },
22563
22803
  {
@@ -22572,7 +22812,8 @@ var securityTools = [
22572
22812
  },
22573
22813
  inputSchema: external_exports.object({}),
22574
22814
  handler: async () => {
22575
- return registryGet("/-/npm/v1/keys");
22815
+ const res = await registryGet("/-/npm/v1/keys");
22816
+ return res.ok ? res : translateError(res, { op: "signing_keys" });
22576
22817
  }
22577
22818
  }
22578
22819
  ];
@@ -22596,7 +22837,7 @@ var trustTools = [
22596
22837
  const authErr = requireAuth();
22597
22838
  if (authErr) return authErr;
22598
22839
  const res = await registryGetAuth(`/-/package/${encPkg(input.name)}/trust`);
22599
- if (!res.ok) return res;
22840
+ if (!res.ok) return translateError(res, { pkg: input.name, op: "trusted_publishers" });
22600
22841
  const configs = (res.data ?? []).map((c) => {
22601
22842
  const result = {
22602
22843
  id: c.id,
@@ -22904,72 +23145,31 @@ var workflowTools = [
22904
23145
  }
22905
23146
  ];
22906
23147
 
22907
- // src/errors.ts
22908
- function translateError(res, context) {
22909
- if (res.ok) return res;
22910
- const pkgPart = context.pkg ? ` for ${context.pkg}` : "";
22911
- const opPart = context.op ? ` during ${context.op}` : "";
22912
- switch (res.status) {
22913
- case 401:
22914
- return {
22915
- ...res,
22916
- error: `Authentication failed${pkgPart}${opPart}. Your NPM_TOKEN may be invalid, expired, or lack write scope. Create a Granular Access Token with 'Read and write' permission at https://www.npmjs.com/settings/~/tokens, or use a classic Automation token (which bypasses 2FA). Raw: ${res.error}`
22917
- };
22918
- case 403:
22919
- return {
22920
- ...res,
22921
- error: `Not authorized${pkgPart}${opPart}. You may not be a maintainer of this package, or the token's scope doesn't include it. Check current maintainers with npm_collaborators or npm_package_access. Raw: ${res.error}`
22922
- };
22923
- case 404:
22924
- return {
22925
- ...res,
22926
- error: `Not found${pkgPart}${opPart}. Check the exact package name (scoped packages require the @scope/ prefix). If the version is specified, verify it exists with npm_package. Raw: ${res.error}`
22927
- };
22928
- case 422:
22929
- return {
22930
- ...res,
22931
- error: `Registry rejected the request payload${pkgPart}${opPart} (422 Unprocessable Entity). Most common causes: (1) invalid semver range \u2014 validate with npm_package versions first; (2) deprecation message format \u2014 em-dash form works, period-capital form sometimes 422s; (3) account-level 2FA policy requires interactive CLI session. If #3, CLI fallback: \`npm login --auth-type=web\` followed by the npm CLI command. Raw: ${res.error}`
22932
- };
22933
- case 429:
22934
- return {
22935
- ...res,
22936
- error: `Rate limited${opPart}. Wait 60 seconds and retry. Raw: ${res.error}`
22937
- };
22938
- case 0:
22939
- return {
22940
- ...res,
22941
- error: `Network error${opPart}. Could not reach the registry. Raw: ${res.error}`
22942
- };
22943
- default:
22944
- return res;
22945
- }
22946
- }
22947
- function validateDeprecationMessage(msg) {
22948
- if (msg.length > 1024) {
22949
- return "Deprecation message exceeds 1024 characters (registry limit).";
22950
- }
22951
- if (msg.length === 0) return null;
22952
- if (/\.\s+[A-Z]/.test(msg)) {
22953
- return `Deprecation message contains the "period + space + capital letter" pattern that has triggered 422 Unprocessable Entity on at least one scoped package. The working form uses em-dash and lowercase continuation: e.g. "Renamed to @yawlabs/spend \u2014 install that instead". Pass force: true to bypass this validation.`;
22954
- }
22955
- return null;
22956
- }
22957
- function versionsMatchingRange(versions, range, maxSatisfying2) {
22958
- if (range === "*" || range === "") return [...versions];
22959
- return versions.filter((v) => maxSatisfying2([v], range) === v);
22960
- }
22961
-
22962
23148
  // src/tools/writes.ts
22963
23149
  async function fetchPackument(pkg) {
22964
23150
  return registryGetAuth(`/${encPkg(pkg)}?write=true`);
22965
23151
  }
23152
+ function parseTeamTarget(target) {
23153
+ const m = target.match(/^@?([^:]+):(.+)$/);
23154
+ return m ? { scope: m[1], team: m[2] } : null;
23155
+ }
23156
+ function highestVersion(versions) {
23157
+ const parsed = [];
23158
+ for (const v of versions) {
23159
+ const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
23160
+ if (m) parsed.push([Number(m[1]), Number(m[2]), Number(m[3]), v]);
23161
+ }
23162
+ if (parsed.length === 0) return null;
23163
+ parsed.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
23164
+ return parsed[parsed.length - 1][3];
23165
+ }
22966
23166
  var writeTools = [
22967
23167
  // ───────────────────────────────────────────────────────
22968
23168
  // npm_deprecate
22969
23169
  // ───────────────────────────────────────────────────────
22970
23170
  {
22971
23171
  name: "npm_deprecate",
22972
- description: "Deprecate a package or specific versions. Shows a warning message on install. Uses the HTTP API with NPM_TOKEN, bypassing the CLI auth friction that causes 422 errors on accounts with 2FA. Message format: prefer em-dash form ('Renamed to @scope/pkg \u2014 install that instead'); period-capital form sometimes triggers 422.",
23172
+ description: "Deprecate a package or specific versions. Shows a warning message on install. Uses the HTTP API with NPM_TOKEN, bypassing the CLI auth friction that causes 422 errors on accounts with 2FA. Message format tip: the period-then-capital pattern ('... install that instead. Thanks.') has 422'd on at least one scoped package; em-dash form is the known-good workaround. Pass force: true to skip the preflight check if you want the exact message as-is.",
22973
23173
  annotations: {
22974
23174
  title: "Deprecate package",
22975
23175
  readOnlyHint: false,
@@ -23072,9 +23272,17 @@ var writeTools = [
23072
23272
  // ───────────────────────────────────────────────────────
23073
23273
  // npm_unpublish_version
23074
23274
  // ───────────────────────────────────────────────────────
23275
+ // Mirrors libnpmpublish/unpublish.js single-version flow:
23276
+ // 1. GET /{pkg}?write=true
23277
+ // 2. mutate: remove version, fix dist-tags, delete _revisions/_attachments
23278
+ // 3. PUT /{pkg}/-rev/{rev}
23279
+ // 4. GET /{pkg}?write=true (fresh rev)
23280
+ // 5. DELETE {tarball-pathname}/-rev/{newRev}
23281
+ // The tarball DELETE is best-effort — if it fails, the version is already unreachable
23282
+ // via the packument (step 3 succeeded), so we report success with a warning.
23075
23283
  {
23076
23284
  name: "npm_unpublish_version",
23077
- description: "Unpublish a specific version of a package. IRREVERSIBLE: once unpublished, the version cannot be re-published and will be blocked for 72 hours. Only works within 72 hours of the original publish for most packages. Requires explicit confirm: true to prevent accidents.",
23285
+ description: "Unpublish a specific version of a package. IRREVERSIBLE: once unpublished, the version cannot be re-published and will be blocked for 72 hours. Only works within 72 hours of the original publish for most packages. Requires explicit confirm: true to prevent accidents. Follows the npm CLI flow (mutate packument + delete tarball). For full-package unpublish use npm_unpublish_package.",
23078
23286
  annotations: {
23079
23287
  title: "Unpublish version",
23080
23288
  readOnlyHint: false,
@@ -23100,33 +23308,118 @@ var writeTools = [
23100
23308
  const pRes = await fetchPackument(input.name);
23101
23309
  if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "unpublish (fetch)" });
23102
23310
  const packument = pRes.data;
23103
- if (!packument.versions[input.version]) {
23311
+ const versionData = packument.versions?.[input.version];
23312
+ if (!versionData) {
23104
23313
  return {
23105
23314
  ok: false,
23106
23315
  status: 404,
23107
- error: `Version ${input.version} not found for ${input.name}. Published versions: ${Object.keys(packument.versions).join(", ")}.`
23316
+ error: `Version ${input.version} not found for ${input.name}. Published versions: ${Object.keys(packument.versions || {}).join(", ")}.`
23108
23317
  };
23109
23318
  }
23319
+ if (!packument._rev) {
23320
+ return {
23321
+ ok: false,
23322
+ status: 500,
23323
+ error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish. Try again; this is usually transient.`
23324
+ };
23325
+ }
23326
+ const tarballUrl = versionData.dist?.tarball;
23327
+ const latestBefore = packument["dist-tags"]?.latest;
23110
23328
  delete packument.versions[input.version];
23111
- for (const tag of Object.keys(packument["dist-tags"])) {
23329
+ for (const tag of Object.keys(packument["dist-tags"] || {})) {
23112
23330
  if (packument["dist-tags"][tag] === input.version) {
23113
23331
  delete packument["dist-tags"][tag];
23114
23332
  }
23115
23333
  }
23116
- const putRes = await registryPutAuth(`/${encPkg(input.name)}`, packument);
23117
- if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "unpublish (write)" });
23334
+ if (latestBefore === input.version) {
23335
+ const newLatest = highestVersion(Object.keys(packument.versions));
23336
+ if (newLatest) packument["dist-tags"].latest = newLatest;
23337
+ }
23338
+ delete packument._revisions;
23339
+ delete packument._attachments;
23340
+ const putRes = await registryPutAuth(
23341
+ `/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`,
23342
+ packument
23343
+ );
23344
+ if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "unpublish (packument PUT)" });
23345
+ let tarballDeleted = false;
23346
+ let tarballError;
23347
+ if (tarballUrl) {
23348
+ const freshRes = await fetchPackument(input.name);
23349
+ const freshRev = freshRes.ok ? freshRes.data._rev : void 0;
23350
+ if (freshRev) {
23351
+ try {
23352
+ const pathname = new URL(tarballUrl).pathname;
23353
+ const delRes = await registryDeleteAuth(`${pathname}/-rev/${encodeURIComponent(freshRev)}`);
23354
+ tarballDeleted = delRes.ok;
23355
+ if (!delRes.ok) tarballError = delRes.error;
23356
+ } catch (err) {
23357
+ tarballError = err instanceof Error ? err.message : String(err);
23358
+ }
23359
+ } else {
23360
+ tarballError = "could not re-fetch packument for tarball DELETE rev";
23361
+ }
23362
+ }
23118
23363
  return {
23119
23364
  ok: true,
23120
23365
  status: 200,
23121
23366
  data: {
23122
23367
  package: input.name,
23123
23368
  unpublishedVersion: input.version,
23124
- remainingVersions: Object.keys(packument.versions)
23369
+ remainingVersions: Object.keys(packument.versions),
23370
+ tarballDeleted,
23371
+ ...tarballError ? { tarballWarning: tarballError } : {}
23125
23372
  }
23126
23373
  };
23127
23374
  }
23128
23375
  },
23129
23376
  // ───────────────────────────────────────────────────────
23377
+ // npm_unpublish_package
23378
+ // ───────────────────────────────────────────────────────
23379
+ {
23380
+ name: "npm_unpublish_package",
23381
+ description: "Unpublish an ENTIRE package (all versions). DELETE /{pkg}/-rev/{rev}. IRREVERSIBLE: the name is blocked for 72 hours and cannot be re-published. For single-version unpublish prefer npm_unpublish_version. Requires confirm: true.",
23382
+ annotations: {
23383
+ title: "Unpublish entire package",
23384
+ readOnlyHint: false,
23385
+ destructiveHint: true,
23386
+ idempotentHint: false,
23387
+ openWorldHint: true
23388
+ },
23389
+ inputSchema: external_exports.object({
23390
+ name: external_exports.string().describe("Package name"),
23391
+ confirm: external_exports.literal(true).describe("Must be literally true. Guards against accidental full unpublish.")
23392
+ }),
23393
+ handler: async (input) => {
23394
+ const authErr = requireAuth();
23395
+ if (authErr) return authErr;
23396
+ if (input.confirm !== true) {
23397
+ return {
23398
+ ok: false,
23399
+ status: 400,
23400
+ error: "Full-package unpublish requires confirm: true. This blocks the name for 72 hours."
23401
+ };
23402
+ }
23403
+ const pRes = await fetchPackument(input.name);
23404
+ if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "unpublish_package (fetch)" });
23405
+ const rev = pRes.data._rev;
23406
+ if (!rev) {
23407
+ return {
23408
+ ok: false,
23409
+ status: 500,
23410
+ error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish.`
23411
+ };
23412
+ }
23413
+ const delRes = await registryDeleteAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(rev)}`);
23414
+ if (!delRes.ok) return translateError(delRes, { pkg: input.name, op: "unpublish_package (DELETE)" });
23415
+ return {
23416
+ ok: true,
23417
+ status: 200,
23418
+ data: { package: input.name, unpublished: true }
23419
+ };
23420
+ }
23421
+ },
23422
+ // ───────────────────────────────────────────────────────
23130
23423
  // npm_dist_tag_set
23131
23424
  // ───────────────────────────────────────────────────────
23132
23425
  {
@@ -23204,9 +23497,13 @@ var writeTools = [
23204
23497
  // ───────────────────────────────────────────────────────
23205
23498
  // npm_owner_add
23206
23499
  // ───────────────────────────────────────────────────────
23500
+ // Mirrors npm CLI owner.js:
23501
+ // 1. GET /-/user/org.couchdb.user:{user} → resolve {name, email}
23502
+ // 2. GET /{pkg}?write=true → packument with _rev
23503
+ // 3. PUT /{pkg}/-rev/{_rev} body {_id, _rev, maintainers}
23207
23504
  {
23208
23505
  name: "npm_owner_add",
23209
- description: "Add a user as a maintainer of a package. They will have publish and write permissions. Use npm_collaborators to verify before adding.",
23506
+ description: "Add a user as a maintainer of a package. They will have publish and write permissions. Resolves the user's email via /-/user/ (no need to supply it). Use npm_collaborators to verify before adding.",
23210
23507
  annotations: {
23211
23508
  title: "Add package owner",
23212
23509
  readOnlyHint: false,
@@ -23216,38 +23513,53 @@ var writeTools = [
23216
23513
  },
23217
23514
  inputSchema: external_exports.object({
23218
23515
  name: external_exports.string().describe("Package name"),
23219
- username: external_exports.string().describe("npm username to add as maintainer"),
23220
- email: external_exports.string().optional().describe("Optional email for the maintainer record (defaults to empty)")
23516
+ username: external_exports.string().describe("npm username to add as maintainer")
23221
23517
  }),
23222
23518
  handler: async (input) => {
23223
23519
  const authErr = requireAuth();
23224
23520
  if (authErr) return authErr;
23521
+ const uRes = await registryGetAuth(
23522
+ `/-/user/org.couchdb.user:${encodeURIComponent(input.username)}`
23523
+ );
23524
+ if (!uRes.ok) return translateError(uRes, { pkg: input.name, op: `owner_add (resolve user ${input.username})` });
23525
+ const userRecord = { name: uRes.data.name, email: uRes.data.email ?? "" };
23225
23526
  const pRes = await fetchPackument(input.name);
23226
23527
  if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "owner_add (fetch)" });
23227
23528
  const packument = pRes.data;
23228
- packument.maintainers = packument.maintainers || [];
23229
- if (packument.maintainers.some((m) => m.name === input.username)) {
23529
+ const owners = packument.maintainers || [];
23530
+ if (owners.some((m) => m.name === userRecord.name)) {
23230
23531
  return {
23231
23532
  ok: true,
23232
23533
  status: 200,
23233
23534
  data: {
23234
23535
  package: input.name,
23235
- username: input.username,
23536
+ username: userRecord.name,
23236
23537
  alreadyOwner: true,
23237
- maintainers: packument.maintainers.map((m) => m.name)
23538
+ maintainers: owners.map((m) => m.name)
23238
23539
  }
23239
23540
  };
23240
23541
  }
23241
- packument.maintainers.push({ name: input.username, email: input.email ?? "" });
23242
- const putRes = await registryPutAuth(`/${encPkg(input.name)}`, packument);
23542
+ if (!packument._rev) {
23543
+ return {
23544
+ ok: false,
23545
+ status: 500,
23546
+ error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
23547
+ };
23548
+ }
23549
+ const maintainers = [...owners, userRecord];
23550
+ const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
23551
+ _id: packument._id,
23552
+ _rev: packument._rev,
23553
+ maintainers
23554
+ });
23243
23555
  if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "owner_add (write)" });
23244
23556
  return {
23245
23557
  ok: true,
23246
23558
  status: 200,
23247
23559
  data: {
23248
23560
  package: input.name,
23249
- addedOwner: input.username,
23250
- maintainers: packument.maintainers.map((m) => m.name)
23561
+ addedOwner: userRecord.name,
23562
+ maintainers: maintainers.map((m) => m.name)
23251
23563
  }
23252
23564
  };
23253
23565
  }
@@ -23291,8 +23603,18 @@ var writeTools = [
23291
23603
  error: `Removing ${input.username} would leave ${input.name} with zero maintainers (lockout). Add another maintainer first with npm_owner_add.`
23292
23604
  };
23293
23605
  }
23294
- packument.maintainers = after;
23295
- const putRes = await registryPutAuth(`/${encPkg(input.name)}`, packument);
23606
+ if (!packument._rev) {
23607
+ return {
23608
+ ok: false,
23609
+ status: 500,
23610
+ error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
23611
+ };
23612
+ }
23613
+ const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
23614
+ _id: packument._id,
23615
+ _rev: packument._rev,
23616
+ maintainers: after
23617
+ });
23296
23618
  if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "owner_remove (write)" });
23297
23619
  return {
23298
23620
  ok: true,
@@ -23304,11 +23626,368 @@ var writeTools = [
23304
23626
  }
23305
23627
  };
23306
23628
  }
23629
+ },
23630
+ // ───────────────────────────────────────────────────────
23631
+ // npm_access_set
23632
+ // ───────────────────────────────────────────────────────
23633
+ {
23634
+ name: "npm_access_set",
23635
+ description: "Set package access level: 'public', 'private', or 'restricted'. Unscoped packages are always public. Private access requires a paid npm account.",
23636
+ annotations: {
23637
+ title: "Set package access level",
23638
+ readOnlyHint: false,
23639
+ destructiveHint: true,
23640
+ idempotentHint: true,
23641
+ openWorldHint: true
23642
+ },
23643
+ inputSchema: external_exports.object({
23644
+ name: external_exports.string().describe("Package name"),
23645
+ access: external_exports.enum(["public", "private", "restricted"]).describe("Access level")
23646
+ }),
23647
+ handler: async (input) => {
23648
+ const authErr = requireAuth();
23649
+ if (authErr) return authErr;
23650
+ const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, { access: input.access });
23651
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set ${input.access}` });
23652
+ return {
23653
+ ok: true,
23654
+ status: 200,
23655
+ data: { package: input.name, access: input.access }
23656
+ };
23657
+ }
23658
+ },
23659
+ // ───────────────────────────────────────────────────────
23660
+ // npm_access_set_mfa
23661
+ // ───────────────────────────────────────────────────────
23662
+ {
23663
+ name: "npm_access_set_mfa",
23664
+ description: "Configure 2FA requirement for publishing: 'none' (off), 'publish' (2FA required), 'automation' (2FA required but automation tokens can bypass).",
23665
+ annotations: {
23666
+ title: "Set package 2FA publish policy",
23667
+ readOnlyHint: false,
23668
+ destructiveHint: true,
23669
+ idempotentHint: true,
23670
+ openWorldHint: true
23671
+ },
23672
+ inputSchema: external_exports.object({
23673
+ name: external_exports.string().describe("Package name"),
23674
+ level: external_exports.enum(["none", "publish", "automation"]).describe("MFA level for publish")
23675
+ }),
23676
+ handler: async (input) => {
23677
+ const authErr = requireAuth();
23678
+ if (authErr) return authErr;
23679
+ let body;
23680
+ if (input.level === "none") {
23681
+ body = { publish_requires_tfa: false };
23682
+ } else if (input.level === "publish") {
23683
+ body = { publish_requires_tfa: true, automation_token_overrides_tfa: false };
23684
+ } else {
23685
+ body = { publish_requires_tfa: true, automation_token_overrides_tfa: true };
23686
+ }
23687
+ const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, body);
23688
+ if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set_mfa ${input.level}` });
23689
+ return {
23690
+ ok: true,
23691
+ status: 200,
23692
+ data: { package: input.name, mfaLevel: input.level }
23693
+ };
23694
+ }
23695
+ },
23696
+ // ───────────────────────────────────────────────────────
23697
+ // npm_team_grant
23698
+ // ───────────────────────────────────────────────────────
23699
+ {
23700
+ name: "npm_team_grant",
23701
+ description: "Grant a team read-only or read-write permission on a package. Scope and team are passed as @scope:team (e.g. '@yawlabs:devs'). Requires org admin or team admin.",
23702
+ annotations: {
23703
+ title: "Grant team package permission",
23704
+ readOnlyHint: false,
23705
+ destructiveHint: true,
23706
+ idempotentHint: true,
23707
+ openWorldHint: true
23708
+ },
23709
+ inputSchema: external_exports.object({
23710
+ team: external_exports.string().describe("Team in the form '@scope:team' (e.g. '@yawlabs:devs')"),
23711
+ package: external_exports.string().describe("Package name"),
23712
+ permissions: external_exports.enum(["read-only", "read-write"]).describe("Permission level")
23713
+ }),
23714
+ handler: async (input) => {
23715
+ const authErr = requireAuth();
23716
+ if (authErr) return authErr;
23717
+ const parsed = parseTeamTarget(input.team);
23718
+ if (!parsed) {
23719
+ return {
23720
+ ok: false,
23721
+ status: 400,
23722
+ error: `Team must be in the form '@scope:team' (got '${input.team}').`
23723
+ };
23724
+ }
23725
+ const { scope, team } = parsed;
23726
+ const res = await registryPutAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, {
23727
+ package: input.package,
23728
+ permissions: input.permissions
23729
+ });
23730
+ if (!res.ok) return translateError(res, { pkg: input.package, op: `team_grant ${input.team}` });
23731
+ return {
23732
+ ok: true,
23733
+ status: 200,
23734
+ data: { team: `@${scope}:${team}`, package: input.package, permissions: input.permissions }
23735
+ };
23736
+ }
23737
+ },
23738
+ // ───────────────────────────────────────────────────────
23739
+ // npm_team_revoke
23740
+ // ───────────────────────────────────────────────────────
23741
+ {
23742
+ name: "npm_team_revoke",
23743
+ description: "Revoke a team's access to a package. Team is passed as '@scope:team'. Does not delete the team itself \u2014 use npm_team_delete for that.",
23744
+ annotations: {
23745
+ title: "Revoke team package permission",
23746
+ readOnlyHint: false,
23747
+ destructiveHint: true,
23748
+ idempotentHint: true,
23749
+ openWorldHint: true
23750
+ },
23751
+ inputSchema: external_exports.object({
23752
+ team: external_exports.string().describe("Team in the form '@scope:team'"),
23753
+ package: external_exports.string().describe("Package name")
23754
+ }),
23755
+ handler: async (input) => {
23756
+ const authErr = requireAuth();
23757
+ if (authErr) return authErr;
23758
+ const parsed = parseTeamTarget(input.team);
23759
+ if (!parsed) {
23760
+ return {
23761
+ ok: false,
23762
+ status: 400,
23763
+ error: `Team must be in the form '@scope:team' (got '${input.team}').`
23764
+ };
23765
+ }
23766
+ const { scope, team } = parsed;
23767
+ const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, {
23768
+ package: input.package
23769
+ });
23770
+ if (!res.ok) return translateError(res, { pkg: input.package, op: `team_revoke ${input.team}` });
23771
+ return {
23772
+ ok: true,
23773
+ status: 200,
23774
+ data: { team: `@${scope}:${team}`, package: input.package, revoked: true }
23775
+ };
23776
+ }
23777
+ },
23778
+ // ───────────────────────────────────────────────────────
23779
+ // npm_team_create
23780
+ // ───────────────────────────────────────────────────────
23781
+ {
23782
+ name: "npm_team_create",
23783
+ description: "Create a team inside an organization. Team is passed as '@scope:team'.",
23784
+ annotations: {
23785
+ title: "Create team",
23786
+ readOnlyHint: false,
23787
+ destructiveHint: true,
23788
+ idempotentHint: false,
23789
+ openWorldHint: true
23790
+ },
23791
+ inputSchema: external_exports.object({
23792
+ team: external_exports.string().describe("Team in the form '@scope:team'"),
23793
+ description: external_exports.string().optional().describe("Optional team description")
23794
+ }),
23795
+ handler: async (input) => {
23796
+ const authErr = requireAuth();
23797
+ if (authErr) return authErr;
23798
+ const parsed = parseTeamTarget(input.team);
23799
+ if (!parsed) {
23800
+ return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
23801
+ }
23802
+ const { scope, team } = parsed;
23803
+ const res = await registryPutAuth(`/-/org/${encodeURIComponent(scope)}/team`, {
23804
+ name: team,
23805
+ description: input.description
23806
+ });
23807
+ if (!res.ok) return translateError(res, { op: `team_create ${input.team}` });
23808
+ return { ok: true, status: 200, data: { team: `@${scope}:${team}`, created: true } };
23809
+ }
23810
+ },
23811
+ // ───────────────────────────────────────────────────────
23812
+ // npm_team_delete
23813
+ // ───────────────────────────────────────────────────────
23814
+ {
23815
+ name: "npm_team_delete",
23816
+ description: "Delete a team. Team is passed as '@scope:team'. Revokes all package permissions that team held.",
23817
+ annotations: {
23818
+ title: "Delete team",
23819
+ readOnlyHint: false,
23820
+ destructiveHint: true,
23821
+ idempotentHint: true,
23822
+ openWorldHint: true
23823
+ },
23824
+ inputSchema: external_exports.object({
23825
+ team: external_exports.string().describe("Team in the form '@scope:team'")
23826
+ }),
23827
+ handler: async (input) => {
23828
+ const authErr = requireAuth();
23829
+ if (authErr) return authErr;
23830
+ const parsed = parseTeamTarget(input.team);
23831
+ if (!parsed) {
23832
+ return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
23833
+ }
23834
+ const { scope, team } = parsed;
23835
+ const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}`);
23836
+ if (!res.ok) return translateError(res, { op: `team_delete ${input.team}` });
23837
+ return { ok: true, status: 200, data: { team: `@${scope}:${team}`, deleted: true } };
23838
+ }
23839
+ },
23840
+ // ───────────────────────────────────────────────────────
23841
+ // npm_team_member_add
23842
+ // ───────────────────────────────────────────────────────
23843
+ {
23844
+ name: "npm_team_member_add",
23845
+ description: "Add a user to a team. Team is '@scope:team'. User must already be in the org.",
23846
+ annotations: {
23847
+ title: "Add team member",
23848
+ readOnlyHint: false,
23849
+ destructiveHint: true,
23850
+ idempotentHint: true,
23851
+ openWorldHint: true
23852
+ },
23853
+ inputSchema: external_exports.object({
23854
+ team: external_exports.string().describe("Team in the form '@scope:team'"),
23855
+ user: external_exports.string().describe("npm username")
23856
+ }),
23857
+ handler: async (input) => {
23858
+ const authErr = requireAuth();
23859
+ if (authErr) return authErr;
23860
+ const parsed = parseTeamTarget(input.team);
23861
+ if (!parsed) {
23862
+ return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
23863
+ }
23864
+ const { scope, team } = parsed;
23865
+ const res = await registryPutAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/user`, {
23866
+ user: input.user
23867
+ });
23868
+ if (!res.ok) return translateError(res, { op: `team_member_add ${input.team}` });
23869
+ return { ok: true, status: 200, data: { team: `@${scope}:${team}`, addedUser: input.user } };
23870
+ }
23871
+ },
23872
+ // ───────────────────────────────────────────────────────
23873
+ // npm_team_member_remove
23874
+ // ───────────────────────────────────────────────────────
23875
+ {
23876
+ name: "npm_team_member_remove",
23877
+ description: "Remove a user from a team. Team is '@scope:team'. User remains in the org.",
23878
+ annotations: {
23879
+ title: "Remove team member",
23880
+ readOnlyHint: false,
23881
+ destructiveHint: true,
23882
+ idempotentHint: true,
23883
+ openWorldHint: true
23884
+ },
23885
+ inputSchema: external_exports.object({
23886
+ team: external_exports.string().describe("Team in the form '@scope:team'"),
23887
+ user: external_exports.string().describe("npm username")
23888
+ }),
23889
+ handler: async (input) => {
23890
+ const authErr = requireAuth();
23891
+ if (authErr) return authErr;
23892
+ const parsed = parseTeamTarget(input.team);
23893
+ if (!parsed) {
23894
+ return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
23895
+ }
23896
+ const { scope, team } = parsed;
23897
+ const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/user`, {
23898
+ user: input.user
23899
+ });
23900
+ if (!res.ok) return translateError(res, { op: `team_member_remove ${input.team}` });
23901
+ return { ok: true, status: 200, data: { team: `@${scope}:${team}`, removedUser: input.user } };
23902
+ }
23903
+ },
23904
+ // ───────────────────────────────────────────────────────
23905
+ // npm_org_member_set
23906
+ // ───────────────────────────────────────────────────────
23907
+ {
23908
+ name: "npm_org_member_set",
23909
+ description: "Add a user to an org or change their role. Roles: 'developer', 'admin', 'owner'. If user is already in the org, updates the role. Omit role to keep existing role.",
23910
+ annotations: {
23911
+ title: "Set org member",
23912
+ readOnlyHint: false,
23913
+ destructiveHint: true,
23914
+ idempotentHint: true,
23915
+ openWorldHint: true
23916
+ },
23917
+ inputSchema: external_exports.object({
23918
+ org: external_exports.string().describe("Organization name (with or without leading @)"),
23919
+ user: external_exports.string().describe("npm username"),
23920
+ role: external_exports.enum(["developer", "admin", "owner"]).optional().describe("Role to assign")
23921
+ }),
23922
+ handler: async (input) => {
23923
+ const authErr = requireAuth();
23924
+ if (authErr) return authErr;
23925
+ const org = input.org.replace(/^@/, "");
23926
+ const user = input.user.replace(/^@/, "");
23927
+ const body = { user };
23928
+ if (input.role) body.role = input.role;
23929
+ const res = await registryPutAuth(`/-/org/${encodeURIComponent(org)}/user`, body);
23930
+ if (!res.ok) return translateError(res, { op: `org_member_set ${org}/${user}` });
23931
+ return { ok: true, status: 200, data: { org, user, role: input.role } };
23932
+ }
23933
+ },
23934
+ // ───────────────────────────────────────────────────────
23935
+ // npm_org_member_remove
23936
+ // ───────────────────────────────────────────────────────
23937
+ {
23938
+ name: "npm_org_member_remove",
23939
+ description: "Remove a user from an org. Their team memberships in that org are also removed.",
23940
+ annotations: {
23941
+ title: "Remove org member",
23942
+ readOnlyHint: false,
23943
+ destructiveHint: true,
23944
+ idempotentHint: true,
23945
+ openWorldHint: true
23946
+ },
23947
+ inputSchema: external_exports.object({
23948
+ org: external_exports.string().describe("Organization name"),
23949
+ user: external_exports.string().describe("npm username")
23950
+ }),
23951
+ handler: async (input) => {
23952
+ const authErr = requireAuth();
23953
+ if (authErr) return authErr;
23954
+ const org = input.org.replace(/^@/, "");
23955
+ const user = input.user.replace(/^@/, "");
23956
+ const res = await registryDeleteAuth(`/-/org/${encodeURIComponent(org)}/user`, { user });
23957
+ if (!res.ok) return translateError(res, { op: `org_member_remove ${org}/${user}` });
23958
+ return { ok: true, status: 200, data: { org, removedUser: user } };
23959
+ }
23960
+ },
23961
+ // ───────────────────────────────────────────────────────
23962
+ // npm_token_revoke
23963
+ // ───────────────────────────────────────────────────────
23964
+ // Note: token CREATION requires a user password (not a token), so it cannot be
23965
+ // performed via NPM_TOKEN alone — we intentionally don't expose npm_token_create.
23966
+ {
23967
+ name: "npm_token_revoke",
23968
+ description: "Revoke an access token by its key (UUID from npm_tokens). Creating tokens is NOT exposed because the endpoint requires the user password \u2014 create via https://www.npmjs.com/settings/~/tokens instead.",
23969
+ annotations: {
23970
+ title: "Revoke access token",
23971
+ readOnlyHint: false,
23972
+ destructiveHint: true,
23973
+ idempotentHint: true,
23974
+ openWorldHint: true
23975
+ },
23976
+ inputSchema: external_exports.object({
23977
+ tokenKey: external_exports.string().describe("Token key (UUID shown by npm_tokens)")
23978
+ }),
23979
+ handler: async (input) => {
23980
+ const authErr = requireAuth();
23981
+ if (authErr) return authErr;
23982
+ const res = await registryDeleteAuth(`/-/npm/v1/tokens/token/${encodeURIComponent(input.tokenKey)}`);
23983
+ if (!res.ok) return translateError(res, { op: "token_revoke" });
23984
+ return { ok: true, status: 200, data: { tokenKey: input.tokenKey, revoked: true } };
23985
+ }
23307
23986
  }
23308
23987
  ];
23309
23988
 
23310
23989
  // src/index.ts
23311
- var version2 = true ? "0.6.0" : (await null).createRequire(import.meta.url)("../package.json").version;
23990
+ var version2 = true ? "0.8.0" : (await null).createRequire(import.meta.url)("../package.json").version;
23312
23991
  var subcommand = process.argv[2];
23313
23992
  if (subcommand === "version" || subcommand === "--version") {
23314
23993
  console.log(version2);
@@ -23328,7 +24007,8 @@ var allTools = [
23328
24007
  ...provenanceTools,
23329
24008
  ...trustTools,
23330
24009
  ...workflowTools,
23331
- ...writeTools
24010
+ ...writeTools,
24011
+ ...hookTools
23332
24012
  ];
23333
24013
  var server = new McpServer({
23334
24014
  name: "@yawlabs/npmjs-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/npmjs-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "npm registry MCP server — package intelligence, security audits, and dependency analysis for AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",