@yawlabs/npmjs-mcp 0.1.0 → 0.3.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/index.js +826 -46
  4. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yaw Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @yawlabs/npmjs-mcp
2
+
3
+ MCP server for the [npm](https://www.npmjs.com) registry. Package intelligence, security audits, dependency analysis, and org management from any MCP-compatible AI assistant.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx @yawlabs/npmjs-mcp
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ No API key is required for read-only tools (search, packages, downloads, security, analysis). For authenticated tools (auth, access, orgs, hooks), set your npm token:
14
+
15
+ ```bash
16
+ export NPM_TOKEN="your-token"
17
+ ```
18
+
19
+ ### Claude Code
20
+
21
+ Add to your MCP config:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "npmjs": {
27
+ "command": "npx",
28
+ "args": ["-y", "@yawlabs/npmjs-mcp"]
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ With authentication:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "npmjs": {
40
+ "command": "npx",
41
+ "args": ["-y", "@yawlabs/npmjs-mcp"],
42
+ "env": {
43
+ "NPM_TOKEN": "your-token"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### Claude Desktop
51
+
52
+ Add to `claude_desktop_config.json`:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "npmjs": {
58
+ "command": "npx",
59
+ "args": ["-y", "@yawlabs/npmjs-mcp"],
60
+ "env": {
61
+ "NPM_TOKEN": "your-token"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Tools (35)
69
+
70
+ ### Search
71
+ - `npm_search` — Search the npm registry with qualifiers (keywords, author, scope)
72
+
73
+ ### Packages
74
+ - `npm_package` — Get package metadata (description, dist-tags, maintainers, license)
75
+ - `npm_version` — Get detailed metadata for a specific version
76
+ - `npm_versions` — List all published versions with dates
77
+ - `npm_readme` — Get README content
78
+ - `npm_dist_tags` — Get dist-tags (latest, next, beta, etc)
79
+
80
+ ### Dependencies
81
+ - `npm_dependencies` — Get dependency lists (prod, dev, peer, optional)
82
+ - `npm_dep_tree` — Resolve transitive dependency tree (configurable depth)
83
+ - `npm_license_check` — Check licenses of a package and its direct deps
84
+
85
+ ### Downloads
86
+ - `npm_downloads` — Get total download count for a period
87
+ - `npm_downloads_range` — Get daily download breakdown
88
+ - `npm_downloads_bulk` — Compare downloads for up to 128 packages
89
+ - `npm_version_downloads` — Per-version download counts
90
+
91
+ ### Security
92
+ - `npm_audit` — Check packages for known vulnerabilities
93
+ - `npm_audit_deep` — Full audit with CVSS scores, CWEs, fix recommendations
94
+ - `npm_signing_keys` — Get registry ECDSA signing keys
95
+
96
+ ### Analysis
97
+ - `npm_compare` — Compare 2-5 packages side-by-side
98
+ - `npm_health` — Assess maintenance, downloads, security, deprecation status
99
+ - `npm_maintainers` — Get maintainers and their publish history
100
+ - `npm_release_frequency` — Analyze release cadence and gaps
101
+
102
+ ### Registry
103
+ - `npm_registry_stats` — Total npm-wide download counts
104
+ - `npm_recent_changes` — Recent package publishes from the CouchDB changes feed
105
+
106
+ ### Provenance
107
+ - `npm_provenance` — Get Sigstore provenance attestations (SLSA, publish)
108
+
109
+ ### Auth (requires NPM_TOKEN)
110
+ - `npm_whoami` — Check authenticated user
111
+ - `npm_profile` — Get profile, email, 2FA status
112
+ - `npm_tokens` — List access tokens
113
+
114
+ ### Access (requires NPM_TOKEN)
115
+ - `npm_collaborators` — List package collaborators and permissions
116
+ - `npm_package_access` — Get package access settings
117
+
118
+ ### Organizations (requires NPM_TOKEN)
119
+ - `npm_org_members` — List org members and roles
120
+ - `npm_org_packages` — List org packages
121
+ - `npm_org_teams` — List org teams
122
+ - `npm_team_packages` — List team package permissions
123
+
124
+ ### Hooks (requires NPM_TOKEN)
125
+ - `npm_hooks` — List npm webhooks
126
+
127
+ ### Workflows
128
+ - `npm_check_auth` — Auth health check with headless publish feasibility
129
+ - `npm_publish_preflight` — Pre-publish validation checklist
130
+
131
+ ## Features
132
+
133
+ - **35 tools** covering search, packages, deps, downloads, security, analysis, auth, orgs, provenance, and publish workflows
134
+ - **No API key required** for read-only tools — authenticated tools opt-in via NPM_TOKEN
135
+ - **Zero runtime dependencies** — Single bundled file for instant `npx` startup
136
+ - **Agent-aware publish tools** — Detects non-interactive context, provides human hand-off actions instead of unworkable retries
137
+ - **MCP annotations** — Every tool declares read-only, destructive, and idempotent hints
138
+
139
+ ## License
140
+
141
+ MIT
package/dist/index.js CHANGED
@@ -21016,8 +21016,24 @@ var DOWNLOADS_URL = "https://api.npmjs.org";
21016
21016
  var REPLICATE_URL = "https://replicate.npmjs.com";
21017
21017
  var REQUEST_TIMEOUT_MS = 3e4;
21018
21018
  function encPkg(name) {
21019
+ if (!name || name === "@") throw new Error("Invalid package name");
21019
21020
  return name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
21020
21021
  }
21022
+ function isAuthenticated() {
21023
+ return !!process.env.NPM_TOKEN;
21024
+ }
21025
+ function requireAuth() {
21026
+ if (isAuthenticated()) return null;
21027
+ return {
21028
+ ok: false,
21029
+ status: 401,
21030
+ error: "No NPM_TOKEN configured. Set the NPM_TOKEN environment variable to use authenticated endpoints. Create a token at https://www.npmjs.com/settings/~/tokens \u2014 use a Granular Access Token for CI/CD (automation tokens bypass 2FA)."
21031
+ };
21032
+ }
21033
+ function authHeaders() {
21034
+ const token = process.env.NPM_TOKEN;
21035
+ return token ? { Authorization: `Bearer ${token}` } : {};
21036
+ }
21021
21037
  async function request(baseUrl, path, options) {
21022
21038
  const method = options?.method ?? "GET";
21023
21039
  const headers = {
@@ -21030,21 +21046,26 @@ async function request(baseUrl, path, options) {
21030
21046
  fetchBody = JSON.stringify(options.body);
21031
21047
  }
21032
21048
  const url = `${baseUrl}${path}`;
21033
- const res = await fetch(url, {
21034
- method,
21035
- headers,
21036
- body: fetchBody,
21037
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21038
- });
21039
- if (!res.ok) {
21040
- const errorBody = await res.text();
21041
- return { ok: false, status: res.status, error: errorBody };
21042
- }
21043
- if (res.status === 204 || res.headers.get("content-length") === "0") {
21044
- return { ok: true, status: res.status };
21049
+ try {
21050
+ const res = await fetch(url, {
21051
+ method,
21052
+ headers,
21053
+ body: fetchBody,
21054
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21055
+ });
21056
+ if (!res.ok) {
21057
+ const errorBody = await res.text();
21058
+ return { ok: false, status: res.status, error: errorBody };
21059
+ }
21060
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
21061
+ return { ok: true, status: res.status };
21062
+ }
21063
+ const data = await res.json();
21064
+ return { ok: true, status: res.status, data };
21065
+ } catch (err) {
21066
+ const message = err instanceof Error ? err.message : String(err);
21067
+ return { ok: false, status: 0, error: message };
21045
21068
  }
21046
- const data = await res.json();
21047
- return { ok: true, status: res.status, data };
21048
21069
  }
21049
21070
  function registryGet(path) {
21050
21071
  return request(REGISTRY_URL, path);
@@ -21057,6 +21078,9 @@ function registryGetAbbreviated(path) {
21057
21078
  function registryPost(path, body) {
21058
21079
  return request(REGISTRY_URL, path, { method: "POST", body });
21059
21080
  }
21081
+ function registryGetAuth(path) {
21082
+ return request(REGISTRY_URL, path, { headers: authHeaders() });
21083
+ }
21060
21084
  function downloadsGet(path) {
21061
21085
  return request(DOWNLOADS_URL, path);
21062
21086
  }
@@ -21064,6 +21088,84 @@ function replicateGet(path) {
21064
21088
  return request(REPLICATE_URL, path);
21065
21089
  }
21066
21090
 
21091
+ // src/tools/access.ts
21092
+ var accessTools = [
21093
+ {
21094
+ name: "npm_collaborators",
21095
+ description: "Get all users who have access to a package and their permission levels (read-only, read-write). Useful for verifying who can publish to a package before setting up CI/CD.",
21096
+ annotations: {
21097
+ title: "Package collaborators",
21098
+ readOnlyHint: true,
21099
+ destructiveHint: false,
21100
+ idempotentHint: true,
21101
+ openWorldHint: true
21102
+ },
21103
+ inputSchema: external_exports.object({
21104
+ name: external_exports.string().describe("Package name (e.g. 'express' or '@yawlabs/npmjs-mcp')")
21105
+ }),
21106
+ handler: async (input) => {
21107
+ const authErr = requireAuth();
21108
+ if (authErr) return authErr;
21109
+ const res = await registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`);
21110
+ if (!res.ok) return res;
21111
+ const collaborators = Object.entries(res.data).map(([username, permissions]) => ({
21112
+ username,
21113
+ permissions
21114
+ }));
21115
+ return {
21116
+ ok: true,
21117
+ status: 200,
21118
+ data: {
21119
+ package: input.name,
21120
+ collaboratorCount: collaborators.length,
21121
+ collaborators
21122
+ }
21123
+ };
21124
+ }
21125
+ },
21126
+ {
21127
+ name: "npm_package_access",
21128
+ description: "Get package access settings \u2014 visibility (public/private), whether publish requires 2FA, and whether automation tokens can bypass 2FA. Critical for understanding why CI publishing fails: if publish_requires_tfa is true but automation_token_overrides_tfa is false, automation tokens cannot publish.",
21129
+ annotations: {
21130
+ title: "Package access settings",
21131
+ readOnlyHint: true,
21132
+ destructiveHint: false,
21133
+ idempotentHint: true,
21134
+ openWorldHint: true
21135
+ },
21136
+ inputSchema: external_exports.object({
21137
+ name: external_exports.string().describe("Package name (e.g. 'express' or '@yawlabs/npmjs-mcp')")
21138
+ }),
21139
+ handler: async (input) => {
21140
+ const authErr = requireAuth();
21141
+ if (authErr) return authErr;
21142
+ const [accessRes, collabRes] = await Promise.all([
21143
+ registryGetAuth(`/-/package/${encPkg(input.name)}/access`),
21144
+ registryGetAuth(`/-/package/${encPkg(input.name)}/collaborators`)
21145
+ ]);
21146
+ if (!accessRes.ok && !collabRes.ok) return collabRes;
21147
+ const result = {
21148
+ package: input.name,
21149
+ isScoped: input.name.startsWith("@")
21150
+ };
21151
+ if (input.name.startsWith("@")) {
21152
+ result.scope = input.name.split("/")[0];
21153
+ result.hint = "Scoped packages belong to an org. Use npm_org_packages to see all packages in the org, and npm_tokens to check if you have a token scoped to this org.";
21154
+ }
21155
+ if (accessRes.ok) {
21156
+ result.access = accessRes.data;
21157
+ }
21158
+ if (collabRes.ok) {
21159
+ result.collaborators = Object.entries(collabRes.data).map(([username, permissions]) => ({
21160
+ username,
21161
+ permissions
21162
+ }));
21163
+ }
21164
+ return { ok: true, status: 200, data: result };
21165
+ }
21166
+ }
21167
+ ];
21168
+
21067
21169
  // src/tools/analysis.ts
21068
21170
  var analysisTools = [
21069
21171
  {
@@ -21093,8 +21195,8 @@ var analysisTools = [
21093
21195
  return { name, error: pkgRes.error };
21094
21196
  }
21095
21197
  const pkg = pkgRes.data;
21096
- const latest = pkg["dist-tags"].latest;
21097
- const latestVersion = pkg.versions[latest];
21198
+ const latest = pkg["dist-tags"]?.latest;
21199
+ const latestVersion = latest ? pkg.versions[latest] : void 0;
21098
21200
  const versionKeys = Object.keys(pkg.versions);
21099
21201
  return {
21100
21202
  name,
@@ -21105,7 +21207,7 @@ var analysisTools = [
21105
21207
  weeklyDownloads: dlRes.ok ? dlRes.data.downloads : null,
21106
21208
  versionCount: versionKeys.length,
21107
21209
  created: pkg.time.created,
21108
- lastPublish: pkg.time[latest],
21210
+ lastPublish: latest ? pkg.time[latest] : void 0,
21109
21211
  deprecated: latestVersion?.deprecated ?? false,
21110
21212
  hasReadme: !!(pkg.readme && pkg.readme.length > 0),
21111
21213
  repository: pkg.repository,
@@ -21138,8 +21240,8 @@ var analysisTools = [
21138
21240
  ]);
21139
21241
  if (!pkgRes.ok) return pkgRes;
21140
21242
  const pkg = pkgRes.data;
21141
- const latest = pkg["dist-tags"].latest;
21142
- const latestVersion = pkg.versions[latest];
21243
+ const latest = pkg["dist-tags"]?.latest;
21244
+ const latestVersion = latest ? pkg.versions[latest] : void 0;
21143
21245
  const versionKeys = Object.keys(pkg.versions);
21144
21246
  const publishDates = versionKeys.map((v) => pkg.time[v]).filter(Boolean).map((d) => new Date(d).getTime()).sort((a, b) => b - a);
21145
21247
  const now = Date.now();
@@ -21154,7 +21256,7 @@ var analysisTools = [
21154
21256
  avgDaysBetweenReleases = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
21155
21257
  }
21156
21258
  const hasLicense = !!(pkg.license ?? latestVersion?.license);
21157
- const hasReadme = !!(pkg.readme && pkg.readme.length > 100);
21259
+ const hasReadme = !!(pkg.readme && pkg.readme.length > 0);
21158
21260
  const hasRepo = !!pkg.repository;
21159
21261
  const hasHomepage = !!pkg.homepage;
21160
21262
  const isDeprecated = !!latestVersion?.deprecated;
@@ -21266,6 +21368,105 @@ var analysisTools = [
21266
21368
  }
21267
21369
  ];
21268
21370
 
21371
+ // src/tools/auth.ts
21372
+ var authTools = [
21373
+ {
21374
+ name: "npm_whoami",
21375
+ description: "Check the currently authenticated npm user. Verifies the NPM_TOKEN is valid and returns the associated username. Essential for debugging auth issues before publishing.",
21376
+ annotations: {
21377
+ title: "Check npm auth",
21378
+ readOnlyHint: true,
21379
+ destructiveHint: false,
21380
+ idempotentHint: true,
21381
+ openWorldHint: true
21382
+ },
21383
+ inputSchema: external_exports.object({}),
21384
+ handler: async () => {
21385
+ const authErr = requireAuth();
21386
+ if (authErr) return authErr;
21387
+ return registryGetAuth("/-/whoami");
21388
+ }
21389
+ },
21390
+ {
21391
+ name: "npm_profile",
21392
+ description: "Get the authenticated user's npm profile \u2014 name, email, 2FA status, creation date. Useful for checking whether 2FA is enabled (which affects token requirements for publishing).",
21393
+ annotations: {
21394
+ title: "Get npm profile",
21395
+ readOnlyHint: true,
21396
+ destructiveHint: false,
21397
+ idempotentHint: true,
21398
+ openWorldHint: true
21399
+ },
21400
+ inputSchema: external_exports.object({}),
21401
+ handler: async () => {
21402
+ const authErr = requireAuth();
21403
+ if (authErr) return authErr;
21404
+ const res = await registryGetAuth("/-/npm/v1/user");
21405
+ if (!res.ok) return res;
21406
+ const p = res.data;
21407
+ return {
21408
+ ok: true,
21409
+ status: 200,
21410
+ data: {
21411
+ name: p.name,
21412
+ email: p.email,
21413
+ emailVerified: p.email_verified,
21414
+ fullname: p.fullname,
21415
+ tfa: p.tfa ? { enabled: true, mode: p.tfa.mode, pending: p.tfa.pending } : { enabled: false },
21416
+ homepage: p.homepage,
21417
+ github: p.github,
21418
+ twitter: p.twitter,
21419
+ created: p.created,
21420
+ updated: p.updated,
21421
+ cidrWhitelist: p.cidr_whitelist
21422
+ }
21423
+ };
21424
+ }
21425
+ },
21426
+ {
21427
+ name: "npm_tokens",
21428
+ description: "List all access tokens for the authenticated npm user. Shows token type, creation date, CIDR restrictions, and read-only status. Critical for finding reusable automation/granular tokens that cover your org scope \u2014 avoids the common mistake of creating duplicate tokens or using publish tokens in CI (which still require OTP).",
21429
+ annotations: {
21430
+ title: "List npm tokens",
21431
+ readOnlyHint: true,
21432
+ destructiveHint: false,
21433
+ idempotentHint: true,
21434
+ openWorldHint: true
21435
+ },
21436
+ inputSchema: external_exports.object({
21437
+ page: external_exports.number().min(0).optional().describe("Page number for pagination (default: 0)"),
21438
+ perPage: external_exports.number().min(1).max(100).optional().describe("Results per page (default: 25)")
21439
+ }),
21440
+ handler: async (input) => {
21441
+ const authErr = requireAuth();
21442
+ if (authErr) return authErr;
21443
+ const params = new URLSearchParams();
21444
+ if (input.page !== void 0) params.set("page", String(input.page));
21445
+ if (input.perPage !== void 0) params.set("perPage", String(input.perPage));
21446
+ const qs = params.toString();
21447
+ const path = `/-/npm/v1/tokens${qs ? `?${qs}` : ""}`;
21448
+ const res = await registryGetAuth(path);
21449
+ if (!res.ok) return res;
21450
+ const data = res.data;
21451
+ return {
21452
+ ok: true,
21453
+ status: 200,
21454
+ data: {
21455
+ total: data.total,
21456
+ tokens: data.objects.map((t) => ({
21457
+ token: t.token,
21458
+ key: t.key,
21459
+ readonly: t.readonly,
21460
+ cidrWhitelist: t.cidr_whitelist,
21461
+ created: t.created,
21462
+ updated: t.updated
21463
+ }))
21464
+ }
21465
+ };
21466
+ }
21467
+ }
21468
+ ];
21469
+
21269
21470
  // src/tools/dependencies.ts
21270
21471
  var dependencyTools = [
21271
21472
  {
@@ -21320,33 +21521,58 @@ var dependencyTools = [
21320
21521
  }),
21321
21522
  handler: async (input) => {
21322
21523
  const maxDepth = input.depth ?? 3;
21323
- const resolved = /* @__PURE__ */ new Map();
21524
+ const MAX_CONCURRENT = 10;
21525
+ const packumentCache = /* @__PURE__ */ new Map();
21526
+ const resolved = /* @__PURE__ */ new Set();
21324
21527
  const tree = {};
21528
+ const warnings = [];
21529
+ let active = 0;
21530
+ const queue = [];
21531
+ function runLimited(fn) {
21532
+ return new Promise((resolve2, reject) => {
21533
+ const run = () => {
21534
+ active++;
21535
+ fn().then(resolve2, reject).finally(() => {
21536
+ active--;
21537
+ if (queue.length > 0) queue.shift()();
21538
+ });
21539
+ };
21540
+ if (active < MAX_CONCURRENT) run();
21541
+ else queue.push(run);
21542
+ });
21543
+ }
21325
21544
  async function resolve(name, versionHint, currentDepth) {
21326
- const key = `${name}@${versionHint}`;
21327
- if (resolved.has(key) || currentDepth > maxDepth) return;
21328
- resolved.set(key, versionHint);
21329
- const res = await registryGetAbbreviated(`/${encPkg(name)}`);
21330
- if (!res.ok) {
21331
- tree[key] = { version: versionHint, dependencies: {} };
21332
- return;
21545
+ const hintKey = `${name}@${versionHint}`;
21546
+ if (resolved.has(hintKey) || currentDepth > maxDepth) return;
21547
+ resolved.add(hintKey);
21548
+ let pkg = packumentCache.get(name);
21549
+ if (!pkg) {
21550
+ const res = await runLimited(() => registryGetAbbreviated(`/${encPkg(name)}`));
21551
+ if (!res.ok) {
21552
+ warnings.push(`Failed to fetch ${name}: ${res.error}`);
21553
+ tree[hintKey] = { version: versionHint, dependencies: {} };
21554
+ return;
21555
+ }
21556
+ pkg = res.data;
21557
+ packumentCache.set(name, pkg);
21333
21558
  }
21334
- const pkg = res.data;
21335
21559
  let resolvedVersion;
21336
21560
  if (pkg.versions[versionHint]) {
21337
21561
  resolvedVersion = versionHint;
21338
21562
  } else if (pkg["dist-tags"][versionHint]) {
21339
21563
  resolvedVersion = pkg["dist-tags"][versionHint];
21340
21564
  } else {
21341
- resolvedVersion = pkg["dist-tags"].latest ?? Object.keys(pkg.versions).pop() ?? versionHint;
21565
+ resolvedVersion = pkg["dist-tags"]?.latest ?? versionHint;
21342
21566
  }
21567
+ const resolvedKey = `${name}@${resolvedVersion}`;
21568
+ if (tree[resolvedKey]) return;
21343
21569
  const versionData = pkg.versions[resolvedVersion];
21344
21570
  if (!versionData) {
21345
- tree[key] = { version: resolvedVersion, dependencies: {} };
21571
+ tree[resolvedKey] = { version: resolvedVersion, dependencies: {} };
21346
21572
  return;
21347
21573
  }
21348
21574
  const deps = versionData.dependencies ?? {};
21349
- tree[`${name}@${resolvedVersion}`] = { version: resolvedVersion, dependencies: deps };
21575
+ tree[resolvedKey] = { version: resolvedVersion, dependencies: deps };
21350
21576
  if (currentDepth < maxDepth) {
21351
21577
  const tasks = Object.entries(deps).map(([depName, depRange]) => resolve(depName, depRange, currentDepth + 1));
21352
21578
  await Promise.all(tasks);
@@ -21360,7 +21586,8 @@ var dependencyTools = [
21360
21586
  root: `${input.name}@${input.version ?? "latest"}`,
21361
21587
  depth: maxDepth,
21362
21588
  totalPackages: Object.keys(tree).length,
21363
- tree
21589
+ tree,
21590
+ ...warnings.length > 0 ? { warnings } : {}
21364
21591
  }
21365
21592
  };
21366
21593
  }
@@ -21377,7 +21604,10 @@ var dependencyTools = [
21377
21604
  },
21378
21605
  inputSchema: external_exports.object({
21379
21606
  name: external_exports.string().describe("Package name"),
21380
- version: external_exports.string().optional().describe("Semver version or dist-tag (default: 'latest')")
21607
+ version: external_exports.string().optional().describe("Semver version or dist-tag (default: 'latest')"),
21608
+ allowed: external_exports.array(external_exports.string()).optional().describe(
21609
+ "SPDX license identifiers to treat as allowed (default: MIT, ISC, BSD-2-Clause, BSD-3-Clause, Apache-2.0, 0BSD, Unlicense)"
21610
+ )
21381
21611
  }),
21382
21612
  handler: async (input) => {
21383
21613
  const ver = input.version ?? "latest";
@@ -21385,26 +21615,45 @@ var dependencyTools = [
21385
21615
  if (!res.ok) return res;
21386
21616
  const pkg = res.data;
21387
21617
  const deps = Object.keys(pkg.dependencies ?? {});
21618
+ const MAX_CONCURRENT = 10;
21619
+ let active = 0;
21620
+ const queue = [];
21621
+ function runLimited(fn) {
21622
+ return new Promise((resolve, reject) => {
21623
+ const run = () => {
21624
+ active++;
21625
+ fn().then(resolve, reject).finally(() => {
21626
+ active--;
21627
+ if (queue.length > 0) queue.shift()();
21628
+ });
21629
+ };
21630
+ if (active < MAX_CONCURRENT) run();
21631
+ else queue.push(run);
21632
+ });
21633
+ }
21388
21634
  const depLicenses = await Promise.all(
21389
21635
  deps.map(async (depName) => {
21390
- const depRes = await registryGet(`/${encPkg(depName)}/latest`);
21636
+ const depRes = await runLimited(() => registryGet(`/${encPkg(depName)}/latest`));
21391
21637
  return {
21392
21638
  name: depName,
21393
21639
  license: depRes.ok ? depRes.data?.license ?? "UNKNOWN" : "FETCH_ERROR"
21394
21640
  };
21395
21641
  })
21396
21642
  );
21397
- const PERMISSIVE = /* @__PURE__ */ new Set(["MIT", "ISC", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0", "0BSD", "Unlicense"]);
21643
+ const allowedSet = new Set(
21644
+ input.allowed ?? ["MIT", "ISC", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0", "0BSD", "Unlicense"]
21645
+ );
21398
21646
  const results = [
21399
21647
  { name: pkg.name, version: pkg.version, license: pkg.license ?? "UNKNOWN" },
21400
21648
  ...depLicenses.map((d) => ({ name: d.name, version: "latest", license: d.license }))
21401
21649
  ];
21402
- const flagged = results.filter((r) => !PERMISSIVE.has(r.license));
21650
+ const flagged = results.filter((r) => !allowedSet.has(r.license));
21403
21651
  return {
21404
21652
  ok: true,
21405
21653
  status: 200,
21406
21654
  data: {
21407
21655
  total: results.length,
21656
+ allowed: [...allowedSet],
21408
21657
  flagged: flagged.length,
21409
21658
  packages: results,
21410
21659
  issues: flagged.length > 0 ? flagged : void 0
@@ -21485,10 +21734,190 @@ var downloadTools = [
21485
21734
  openWorldHint: true
21486
21735
  },
21487
21736
  inputSchema: external_exports.object({
21488
- name: external_exports.string().describe("Package name")
21737
+ name: external_exports.string().describe("Package name"),
21738
+ period: external_exports.string().optional().describe("Period: 'last-day', 'last-week', 'last-month' (default: 'last-week')")
21739
+ }),
21740
+ handler: async (input) => {
21741
+ const period = input.period ?? "last-week";
21742
+ return downloadsGet(`/versions/${encPkg(input.name)}/${period}`);
21743
+ }
21744
+ }
21745
+ ];
21746
+
21747
+ // src/tools/hooks.ts
21748
+ var hookTools = [
21749
+ {
21750
+ name: "npm_hooks",
21751
+ description: "List all npm webhooks configured for the authenticated user. Shows hook type (package, scope, or owner), endpoint URL, delivery status, and last response code.",
21752
+ annotations: {
21753
+ title: "List npm hooks",
21754
+ readOnlyHint: true,
21755
+ destructiveHint: false,
21756
+ idempotentHint: true,
21757
+ openWorldHint: true
21758
+ },
21759
+ inputSchema: external_exports.object({
21760
+ package: external_exports.string().optional().describe("Filter hooks by package name"),
21761
+ limit: external_exports.number().min(1).max(100).optional().describe("Max results (default: 25)"),
21762
+ offset: external_exports.number().min(0).optional().describe("Pagination offset")
21763
+ }),
21764
+ handler: async (input) => {
21765
+ const authErr = requireAuth();
21766
+ if (authErr) return authErr;
21767
+ const params = new URLSearchParams();
21768
+ if (input.package) params.set("package", input.package);
21769
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
21770
+ if (input.offset !== void 0) params.set("offset", String(input.offset));
21771
+ const qs = params.toString();
21772
+ const path = `/-/npm/v1/hooks${qs ? `?${qs}` : ""}`;
21773
+ const res = await registryGetAuth(path);
21774
+ if (!res.ok) return res;
21775
+ const data = res.data;
21776
+ return {
21777
+ ok: true,
21778
+ status: 200,
21779
+ data: {
21780
+ total: data.total,
21781
+ hooks: data.objects.map((h) => ({
21782
+ id: h.id,
21783
+ type: h.type,
21784
+ name: h.name,
21785
+ endpoint: h.endpoint,
21786
+ status: h.status,
21787
+ lastDelivery: h.last_delivery,
21788
+ lastResponseCode: h.response_code,
21789
+ created: h.created,
21790
+ updated: h.updated
21791
+ }))
21792
+ }
21793
+ };
21794
+ }
21795
+ }
21796
+ ];
21797
+
21798
+ // src/tools/orgs.ts
21799
+ var orgTools = [
21800
+ {
21801
+ name: "npm_org_members",
21802
+ description: "List all members of an npm organization with their roles (owner, admin, developer). Requires authentication as an org member.",
21803
+ annotations: {
21804
+ title: "List org members",
21805
+ readOnlyHint: true,
21806
+ destructiveHint: false,
21807
+ idempotentHint: true,
21808
+ openWorldHint: true
21809
+ },
21810
+ inputSchema: external_exports.object({
21811
+ org: external_exports.string().describe("Organization name (without @ prefix)")
21812
+ }),
21813
+ handler: async (input) => {
21814
+ const authErr = requireAuth();
21815
+ if (authErr) return authErr;
21816
+ const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/user`);
21817
+ if (!res.ok) return res;
21818
+ const members = Object.entries(res.data).map(([username, role]) => ({ username, role }));
21819
+ return {
21820
+ ok: true,
21821
+ status: 200,
21822
+ data: {
21823
+ org: input.org,
21824
+ memberCount: members.length,
21825
+ members
21826
+ }
21827
+ };
21828
+ }
21829
+ },
21830
+ {
21831
+ name: "npm_org_packages",
21832
+ description: "List all packages accessible to an npm organization with their access levels. Shows what the org owns or has been granted access to.",
21833
+ annotations: {
21834
+ title: "List org packages",
21835
+ readOnlyHint: true,
21836
+ destructiveHint: false,
21837
+ idempotentHint: true,
21838
+ openWorldHint: true
21839
+ },
21840
+ inputSchema: external_exports.object({
21841
+ org: external_exports.string().describe("Organization name (without @ prefix)")
21842
+ }),
21843
+ handler: async (input) => {
21844
+ const authErr = requireAuth();
21845
+ if (authErr) return authErr;
21846
+ const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/package`);
21847
+ if (!res.ok) return res;
21848
+ const packages = Object.entries(res.data).map(([name, access]) => ({ name, access }));
21849
+ return {
21850
+ ok: true,
21851
+ status: 200,
21852
+ data: {
21853
+ org: input.org,
21854
+ packageCount: packages.length,
21855
+ packages
21856
+ }
21857
+ };
21858
+ }
21859
+ },
21860
+ {
21861
+ name: "npm_org_teams",
21862
+ description: "List all teams within an npm organization. Requires authentication as an org member.",
21863
+ annotations: {
21864
+ title: "List org teams",
21865
+ readOnlyHint: true,
21866
+ destructiveHint: false,
21867
+ idempotentHint: true,
21868
+ openWorldHint: true
21869
+ },
21870
+ inputSchema: external_exports.object({
21871
+ org: external_exports.string().describe("Organization name (without @ prefix)")
21872
+ }),
21873
+ handler: async (input) => {
21874
+ const authErr = requireAuth();
21875
+ if (authErr) return authErr;
21876
+ const res = await registryGetAuth(`/-/org/${encodeURIComponent(input.org)}/team`);
21877
+ if (!res.ok) return res;
21878
+ return {
21879
+ ok: true,
21880
+ status: 200,
21881
+ data: {
21882
+ org: input.org,
21883
+ teamCount: res.data.length,
21884
+ teams: res.data
21885
+ }
21886
+ };
21887
+ }
21888
+ },
21889
+ {
21890
+ name: "npm_team_packages",
21891
+ description: "List all packages a specific team has access to and their permission levels (read-only or read-write). Useful for auditing team permissions.",
21892
+ annotations: {
21893
+ title: "List team packages",
21894
+ readOnlyHint: true,
21895
+ destructiveHint: false,
21896
+ idempotentHint: true,
21897
+ openWorldHint: true
21898
+ },
21899
+ inputSchema: external_exports.object({
21900
+ org: external_exports.string().describe("Organization name (without @ prefix)"),
21901
+ team: external_exports.string().describe("Team name")
21489
21902
  }),
21490
21903
  handler: async (input) => {
21491
- return downloadsGet(`/versions/${encPkg(input.name)}/last-week`);
21904
+ const authErr = requireAuth();
21905
+ if (authErr) return authErr;
21906
+ const res = await registryGetAuth(
21907
+ `/-/team/${encodeURIComponent(input.org)}/${encodeURIComponent(input.team)}/package`
21908
+ );
21909
+ if (!res.ok) return res;
21910
+ const packages = Object.entries(res.data).map(([name, permissions]) => ({ name, permissions }));
21911
+ return {
21912
+ ok: true,
21913
+ status: 200,
21914
+ data: {
21915
+ org: input.org,
21916
+ team: input.team,
21917
+ packageCount: packages.length,
21918
+ packages
21919
+ }
21920
+ };
21492
21921
  }
21493
21922
  }
21494
21923
  ];
@@ -21512,8 +21941,8 @@ var packageTools = [
21512
21941
  const res = await registryGet(`/${encPkg(input.name)}`);
21513
21942
  if (!res.ok) return res;
21514
21943
  const pkg = res.data;
21515
- const latest = pkg["dist-tags"].latest;
21516
- const latestVersion = pkg.versions[latest];
21944
+ const latest = pkg["dist-tags"]?.latest;
21945
+ const latestVersion = latest ? pkg.versions[latest] : void 0;
21517
21946
  return {
21518
21947
  ok: true,
21519
21948
  status: 200,
@@ -21553,7 +21982,39 @@ var packageTools = [
21553
21982
  }),
21554
21983
  handler: async (input) => {
21555
21984
  const ver = input.version ?? "latest";
21556
- return registryGet(`/${encPkg(input.name)}/${ver}`);
21985
+ const res = await registryGet(`/${encPkg(input.name)}/${ver}`);
21986
+ if (!res.ok) return res;
21987
+ const v = res.data;
21988
+ return {
21989
+ ok: true,
21990
+ status: 200,
21991
+ data: {
21992
+ name: v.name,
21993
+ version: v.version,
21994
+ description: v.description,
21995
+ license: v.license,
21996
+ author: v.author,
21997
+ maintainers: v.maintainers,
21998
+ homepage: v.homepage,
21999
+ repository: v.repository,
22000
+ bugs: v.bugs,
22001
+ keywords: v.keywords,
22002
+ engines: v.engines,
22003
+ dependencies: v.dependencies ?? {},
22004
+ devDependencies: v.devDependencies ?? {},
22005
+ peerDependencies: v.peerDependencies ?? {},
22006
+ optionalDependencies: v.optionalDependencies ?? {},
22007
+ deprecated: v.deprecated ?? false,
22008
+ dist: {
22009
+ shasum: v.dist.shasum,
22010
+ integrity: v.dist.integrity,
22011
+ tarball: v.dist.tarball,
22012
+ fileCount: v.dist.fileCount,
22013
+ unpackedSize: v.dist.unpackedSize
22014
+ },
22015
+ publisher: v._npmUser?.name
22016
+ }
22017
+ };
21557
22018
  }
21558
22019
  },
21559
22020
  {
@@ -21633,6 +22094,47 @@ var packageTools = [
21633
22094
  }
21634
22095
  ];
21635
22096
 
22097
+ // src/tools/provenance.ts
22098
+ var provenanceTools = [
22099
+ {
22100
+ name: "npm_provenance",
22101
+ 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.",
22102
+ annotations: {
22103
+ title: "Package provenance",
22104
+ readOnlyHint: true,
22105
+ destructiveHint: false,
22106
+ idempotentHint: true,
22107
+ openWorldHint: true
22108
+ },
22109
+ inputSchema: external_exports.object({
22110
+ name: external_exports.string().describe("Package name (e.g. '@anthropic-ai/sdk')"),
22111
+ version: external_exports.string().describe("Exact semver version (e.g. '1.0.0')")
22112
+ }),
22113
+ handler: async (input) => {
22114
+ const res = await registryGet(
22115
+ `/-/npm/v1/attestations/${encPkg(input.name)}@${input.version}`
22116
+ );
22117
+ if (!res.ok) return res;
22118
+ const attestations = (res.data.attestations ?? []).map((a) => ({
22119
+ predicateType: a.predicateType,
22120
+ bundle: a.bundle
22121
+ }));
22122
+ return {
22123
+ ok: true,
22124
+ status: 200,
22125
+ data: {
22126
+ package: input.name,
22127
+ version: input.version,
22128
+ attestationCount: attestations.length,
22129
+ hasProvenance: attestations.some((a) => a.predicateType.includes("slsa.dev/provenance")),
22130
+ hasPublishAttestation: attestations.some((a) => a.predicateType.includes("npmjs.com/attestation")),
22131
+ attestations
22132
+ }
22133
+ };
22134
+ }
22135
+ }
22136
+ ];
22137
+
21636
22138
  // src/tools/registry.ts
21637
22139
  var registryTools = [
21638
22140
  {
@@ -21668,7 +22170,6 @@ var registryTools = [
21668
22170
  }),
21669
22171
  handler: async (input) => {
21670
22172
  const limit = input.limit ?? 25;
21671
- const infoRes = await replicateGet("/_db_updates");
21672
22173
  const dbRes = await replicateGet("/");
21673
22174
  if (!dbRes.ok) return dbRes;
21674
22175
  const since = dbRes.data.update_seq - limit;
@@ -21726,7 +22227,7 @@ var searchTools = [
21726
22227
  description: obj.package.description,
21727
22228
  date: obj.package.date,
21728
22229
  license: obj.package.license,
21729
- publisher: obj.package.publisher?.username,
22230
+ publisher: obj.package.publisher,
21730
22231
  keywords: obj.package.keywords,
21731
22232
  links: obj.package.links,
21732
22233
  score: obj.score.detail,
@@ -21801,8 +22302,281 @@ var securityTools = [
21801
22302
  }
21802
22303
  ];
21803
22304
 
22305
+ // src/tools/workflows.ts
22306
+ var workflowTools = [
22307
+ {
22308
+ name: "npm_check_auth",
22309
+ description: "Quick auth health check \u2014 returns structured data about npm auth status, token capability, and whether headless (CI/agent) publishing is possible. Run this BEFORE attempting any publish operation. Returns canPublishHeadless boolean and a clear recommendation.\n\nMCP servers are called by AI agents which CANNOT open browsers or enter OTP codes. This tool detects that and provides the exact terminal command for the human to run instead of suggesting unworkable retries.",
22310
+ annotations: {
22311
+ title: "Check npm auth health",
22312
+ readOnlyHint: true,
22313
+ destructiveHint: false,
22314
+ idempotentHint: true,
22315
+ openWorldHint: true
22316
+ },
22317
+ inputSchema: external_exports.object({}),
22318
+ handler: async () => {
22319
+ const result = {
22320
+ authenticated: false,
22321
+ username: null,
22322
+ twoFactorAuth: "unknown",
22323
+ tokenType: "unknown",
22324
+ canPublishHeadless: false,
22325
+ recommendation: null
22326
+ };
22327
+ if (!isAuthenticated()) {
22328
+ result.recommendation = 'No NPM_TOKEN configured. Run "npm login" in your terminal, or create a token at https://www.npmjs.com/settings/~/tokens';
22329
+ return { ok: true, status: 200, data: result };
22330
+ }
22331
+ const whoamiRes = await registryGetAuth("/-/whoami");
22332
+ if (!whoamiRes.ok) {
22333
+ result.recommendation = `Token is set but invalid (HTTP ${whoamiRes.status}). It may be expired or revoked. Create a new token at https://www.npmjs.com/settings/~/tokens`;
22334
+ return { ok: true, status: 200, data: result };
22335
+ }
22336
+ result.authenticated = true;
22337
+ result.username = whoamiRes.data.username;
22338
+ const profileRes = await registryGetAuth("/-/npm/v1/user");
22339
+ if (profileRes.ok && profileRes.data) {
22340
+ const tfa = profileRes.data.tfa;
22341
+ if (tfa && !tfa.pending) {
22342
+ result.twoFactorAuth = tfa.mode;
22343
+ } else {
22344
+ result.twoFactorAuth = "disabled";
22345
+ }
22346
+ }
22347
+ const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
22348
+ if (tokensRes.ok && tokensRes.data) {
22349
+ const tokens = tokensRes.data.objects;
22350
+ const hasReadWrite = tokens.some((t) => !t.readonly);
22351
+ result.tokenCount = tokensRes.data.total;
22352
+ result.hasReadWriteTokens = hasReadWrite;
22353
+ }
22354
+ if (result.twoFactorAuth === "disabled") {
22355
+ result.canPublishHeadless = true;
22356
+ result.tokenType = "any (2FA disabled)";
22357
+ result.recommendation = "2FA is disabled \u2014 any valid token can publish. Consider enabling 2FA for security.";
22358
+ } else {
22359
+ result.tokenType = "unknown (tokens are redacted in API)";
22360
+ result.canPublishHeadless = null;
22361
+ result.recommendation = `2FA is enabled (${result.twoFactorAuth}). Headless publishing ONLY works with automation/granular tokens. Publish tokens from ~/.npmrc ALWAYS require OTP and WILL fail in CI/agent contexts. If publish fails with EOTP, your token is a publish token \u2014 you need an automation token.`;
22362
+ result.ifPublishFails = {
22363
+ errorType: "2FA_REQUIRED",
22364
+ humanAction: {
22365
+ label: "Publish from your terminal (one-time)",
22366
+ command: "npm publish --access public --auth-type=web",
22367
+ context: "Run in an INTERACTIVE terminal \u2014 a browser will open for 2FA. Do NOT run through piped/agent runners."
22368
+ },
22369
+ permanentFix: {
22370
+ label: "Set up automation token (permanent fix for CI/agents)",
22371
+ url: "https://www.npmjs.com/settings/~/tokens",
22372
+ instructions: "Create a 'Granular Access Token' with publish permissions scoped to your org/packages. Then set it as your CI secret or run: npm config set //registry.npmjs.org/:_authToken=<token>"
22373
+ }
22374
+ };
22375
+ }
22376
+ return { ok: true, status: 200, data: result };
22377
+ }
22378
+ },
22379
+ {
22380
+ name: "npm_publish_preflight",
22381
+ description: "Comprehensive pre-publish validation \u2014 run before publishing ANY npm package. Returns an actionable checklist with pass/fail/warn for each item.\n\nASSUMES NON-INTERACTIVE CONTEXT BY DEFAULT because MCP servers are called by AI agents that:\n- CANNOT open browsers (so --auth-type=web is useless)\n- CANNOT enter OTP codes\n- CANNOT retry with 2FA \u2014 this is a hand-off to the human\n\nChecks: auth token validity, 2FA requirements, token type inference, org-level token reuse, package name availability, maintainer access, scoped package settings.\n\nWhen issues are found, returns structured actions with exact commands for the HUMAN to run in their terminal \u2014 never suggests actions an agent cannot perform.",
22382
+ annotations: {
22383
+ title: "Publish preflight check",
22384
+ readOnlyHint: true,
22385
+ destructiveHint: false,
22386
+ idempotentHint: true,
22387
+ openWorldHint: true
22388
+ },
22389
+ inputSchema: external_exports.object({
22390
+ name: external_exports.string().describe("Package name to publish (e.g. '@yawlabs/npmjs-mcp')")
22391
+ }),
22392
+ handler: async (input) => {
22393
+ const checks = [];
22394
+ const actions = [];
22395
+ const isScoped = input.name.startsWith("@");
22396
+ const scope = isScoped ? input.name.split("/")[0] : null;
22397
+ let username = null;
22398
+ let twoFactorAuth = null;
22399
+ let canPublishHeadless = null;
22400
+ if (!isAuthenticated()) {
22401
+ checks.push({
22402
+ check: "NPM_TOKEN configured",
22403
+ status: "fail",
22404
+ detail: "No NPM_TOKEN environment variable set. Publishing requires authentication."
22405
+ });
22406
+ actions.push({
22407
+ label: "Login interactively",
22408
+ command: "npm login",
22409
+ context: "Run in your terminal to create a publish token"
22410
+ });
22411
+ actions.push({
22412
+ label: "Create automation token (for CI/agents)",
22413
+ url: "https://www.npmjs.com/settings/~/tokens",
22414
+ context: `Create a Granular Access Token with publish permissions${scope ? ` scoped to ${scope}` : ""}, then set NPM_TOKEN in your environment`
22415
+ });
22416
+ } else {
22417
+ const whoamiRes = await registryGetAuth("/-/whoami");
22418
+ if (!whoamiRes.ok) {
22419
+ checks.push({
22420
+ check: "NPM_TOKEN valid",
22421
+ status: "fail",
22422
+ detail: `Token rejected (HTTP ${whoamiRes.status}). Expired or revoked.`
22423
+ });
22424
+ actions.push({
22425
+ label: "Create new token",
22426
+ url: "https://www.npmjs.com/settings/~/tokens",
22427
+ context: "Your current token is invalid. Create a new Granular Access Token."
22428
+ });
22429
+ } else {
22430
+ username = whoamiRes.data.username;
22431
+ checks.push({
22432
+ check: "NPM_TOKEN valid",
22433
+ status: "pass",
22434
+ detail: `Authenticated as: ${username}`
22435
+ });
22436
+ }
22437
+ if (username) {
22438
+ const profileRes = await registryGetAuth("/-/npm/v1/user");
22439
+ if (profileRes.ok && profileRes.data) {
22440
+ const tfa = profileRes.data.tfa;
22441
+ if (tfa && !tfa.pending) {
22442
+ twoFactorAuth = tfa.mode;
22443
+ checks.push({
22444
+ check: "2FA status",
22445
+ status: "info",
22446
+ detail: `2FA is enabled (mode: ${tfa.mode}). In this non-interactive context:
22447
+ - Automation/granular tokens: can publish (bypass 2FA)
22448
+ - Publish tokens (from ~/.npmrc): WILL FAIL with EOTP error
22449
+ - --auth-type=web: IMPOSSIBLE (needs browser)
22450
+ - OTP codes: IMPOSSIBLE (agent can't enter them)`
22451
+ });
22452
+ } else {
22453
+ twoFactorAuth = "disabled";
22454
+ canPublishHeadless = true;
22455
+ checks.push({
22456
+ check: "2FA status",
22457
+ status: "warn",
22458
+ detail: "2FA is disabled. Any valid token can publish, but 2FA is strongly recommended for security."
22459
+ });
22460
+ }
22461
+ }
22462
+ const tokensRes = await registryGetAuth("/-/npm/v1/tokens");
22463
+ if (tokensRes.ok && tokensRes.data) {
22464
+ const tokens = tokensRes.data.objects;
22465
+ const totalTokens = tokensRes.data.total;
22466
+ const readWriteTokens = tokens.filter((t) => !t.readonly);
22467
+ if (twoFactorAuth && twoFactorAuth !== "disabled") {
22468
+ if (readWriteTokens.length > 0) {
22469
+ checks.push({
22470
+ check: "Token capability",
22471
+ status: "warn",
22472
+ detail: `Found ${readWriteTokens.length} read-write token(s) out of ${totalTokens} total. Cannot determine token type from API (tokens are redacted). If publish fails with EOTP, your active token is a publish-type token \u2014 you need an automation/granular token.`
22473
+ });
22474
+ canPublishHeadless = null;
22475
+ } else {
22476
+ checks.push({
22477
+ check: "Token capability",
22478
+ status: "fail",
22479
+ detail: "No read-write tokens found. You need a token with publish permissions."
22480
+ });
22481
+ canPublishHeadless = false;
22482
+ }
22483
+ }
22484
+ if (isScoped && scope) {
22485
+ checks.push({
22486
+ check: "Org-scope token reuse",
22487
+ status: "info",
22488
+ detail: `${input.name} is under ${scope}. You have ${totalTokens} token(s). If any are granular tokens scoped to ${scope}, they cover ALL packages under that scope \u2014 check your tokens at https://www.npmjs.com/settings/~/tokens before creating duplicates.`
22489
+ });
22490
+ }
22491
+ }
22492
+ }
22493
+ }
22494
+ const pkgRes = await registryGet(`/${encPkg(input.name)}`);
22495
+ if (!pkgRes.ok && pkgRes.status === 404) {
22496
+ checks.push({
22497
+ check: "Package name available",
22498
+ status: "pass",
22499
+ detail: `"${input.name}" is not taken \u2014 you can publish it as a new package.`
22500
+ });
22501
+ if (isScoped) {
22502
+ checks.push({
22503
+ check: "First publish access flag",
22504
+ status: "info",
22505
+ detail: "First publish of a scoped package requires --access public (otherwise it defaults to restricted/paid)."
22506
+ });
22507
+ }
22508
+ } else if (pkgRes.ok && pkgRes.data) {
22509
+ const pkg = pkgRes.data;
22510
+ const maintainers = pkg.maintainers?.map((m) => m.name) ?? [];
22511
+ const isMaintainer = username ? maintainers.includes(username) : null;
22512
+ if (isMaintainer === true) {
22513
+ checks.push({
22514
+ check: "Maintainer access",
22515
+ status: "pass",
22516
+ detail: `You (${username}) are a maintainer of ${input.name}.`
22517
+ });
22518
+ } else if (isMaintainer === false) {
22519
+ checks.push({
22520
+ check: "Maintainer access",
22521
+ status: "fail",
22522
+ detail: `You (${username}) are NOT a maintainer. Maintainers: ${maintainers.join(", ")}. You need to be added as a maintainer or collaborator.`
22523
+ });
22524
+ } else {
22525
+ checks.push({
22526
+ check: "Package exists",
22527
+ status: "info",
22528
+ detail: `"${input.name}" exists. Maintainers: ${maintainers.join(", ") || "unknown"}. Authenticate to check your access.`
22529
+ });
22530
+ }
22531
+ }
22532
+ if (twoFactorAuth && twoFactorAuth !== "disabled" && canPublishHeadless !== true) {
22533
+ actions.push({
22534
+ label: "Publish from your terminal (one-time)",
22535
+ command: "npm publish --access public --auth-type=web",
22536
+ context: "Run this in an INTERACTIVE terminal (not piped through an agent or ! runner). A browser will open for 2FA."
22537
+ });
22538
+ actions.push({
22539
+ label: "Set up automation token (permanent fix for CI/agents)",
22540
+ url: "https://www.npmjs.com/settings/~/tokens",
22541
+ context: `Create a 'Granular Access Token' with publish permissions${scope ? ` scoped to ${scope} packages` : ""}. This bypasses 2FA for headless publishing.`
22542
+ });
22543
+ }
22544
+ const failures = checks.filter((c) => c.status === "fail");
22545
+ const warnings = checks.filter((c) => c.status === "warn");
22546
+ const passes = checks.filter((c) => c.status === "pass");
22547
+ let summary;
22548
+ if (failures.length > 0) {
22549
+ summary = `BLOCKED: ${failures.length} issue(s) must be resolved before publishing.`;
22550
+ } else if (canPublishHeadless === false) {
22551
+ summary = "BLOCKED: No suitable token for headless publishing. See actions below.";
22552
+ } else if (canPublishHeadless === null && twoFactorAuth !== "disabled") {
22553
+ summary = "UNCERTAIN: 2FA is enabled and token type cannot be verified. Publishing may fail with EOTP. If it does, this is a hand-off to the human \u2014 do NOT retry. See actions below.";
22554
+ } else if (warnings.length > 0) {
22555
+ summary = `READY with ${warnings.length} warning(s). Review before proceeding.`;
22556
+ } else {
22557
+ summary = "READY to publish. All checks passed.";
22558
+ }
22559
+ const response = {
22560
+ package: input.name,
22561
+ context: "non-interactive (MCP/agent)",
22562
+ summary,
22563
+ canPublishHeadless,
22564
+ passCount: passes.length,
22565
+ warnCount: warnings.length,
22566
+ failCount: failures.length,
22567
+ checks
22568
+ };
22569
+ if (actions.length > 0) {
22570
+ response.humanActions = actions;
22571
+ response.agentNote = "The actions below are for the HUMAN to run in their terminal. Do NOT attempt these through piped runners, ! commands, or automation. Do NOT retry the publish. Do NOT suggest --otp. Present these actions to the user and wait for them to complete the step manually.";
22572
+ }
22573
+ return { ok: true, status: 200, data: response };
22574
+ }
22575
+ }
22576
+ ];
22577
+
21804
22578
  // src/index.ts
21805
- var version2 = true ? "0.1.0" : (await null).createRequire(import.meta.url)("../package.json").version;
22579
+ var version2 = true ? "0.3.0" : (await null).createRequire(import.meta.url)("../package.json").version;
21806
22580
  var subcommand = process.argv[2];
21807
22581
  if (subcommand === "version" || subcommand === "--version") {
21808
22582
  console.log(version2);
@@ -21815,7 +22589,13 @@ var allTools = [
21815
22589
  ...downloadTools,
21816
22590
  ...securityTools,
21817
22591
  ...analysisTools,
21818
- ...registryTools
22592
+ ...registryTools,
22593
+ ...authTools,
22594
+ ...orgTools,
22595
+ ...accessTools,
22596
+ ...provenanceTools,
22597
+ ...hookTools,
22598
+ ...workflowTools
21819
22599
  ];
21820
22600
  var server = new McpServer({
21821
22601
  name: "@yawlabs/npmjs-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/npmjs-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.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>",