@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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/index.js +826 -46
- 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
|
-
|
|
21034
|
-
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
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"]
|
|
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"]
|
|
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 >
|
|
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
|
|
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
|
|
21327
|
-
if (resolved.has(
|
|
21328
|
-
resolved.
|
|
21329
|
-
|
|
21330
|
-
if (!
|
|
21331
|
-
|
|
21332
|
-
|
|
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"]
|
|
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[
|
|
21571
|
+
tree[resolvedKey] = { version: resolvedVersion, dependencies: {} };
|
|
21346
21572
|
return;
|
|
21347
21573
|
}
|
|
21348
21574
|
const deps = versionData.dependencies ?? {};
|
|
21349
|
-
tree[
|
|
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
|
|
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) => !
|
|
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
|
-
|
|
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"]
|
|
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
|
-
|
|
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
|
|
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.
|
|
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