@yawlabs/npmjs-mcp 0.5.0 → 0.7.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/README.md +43 -2
- package/dist/index.js +1142 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Add to `claude_desktop_config.json`:
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
## Tools (
|
|
68
|
+
## Tools (63)
|
|
69
69
|
|
|
70
70
|
### Search
|
|
71
71
|
- `npm_search` — Search the npm registry with qualifiers (keywords, author, scope)
|
|
@@ -103,6 +103,7 @@ Add to `claude_desktop_config.json`:
|
|
|
103
103
|
### Registry
|
|
104
104
|
- `npm_registry_stats` — Total npm-wide download counts
|
|
105
105
|
- `npm_recent_changes` — Recent package publishes from the CouchDB changes feed
|
|
106
|
+
- `npm_ops_playbook` — Canonical recipes for npm operations (call this FIRST when unsure which tool to use)
|
|
106
107
|
|
|
107
108
|
### Provenance
|
|
108
109
|
- `npm_provenance` — Get Sigstore provenance attestations (SLSA, publish)
|
|
@@ -114,6 +115,7 @@ Add to `claude_desktop_config.json`:
|
|
|
114
115
|
- `npm_whoami` — Check authenticated user
|
|
115
116
|
- `npm_profile` — Get profile, email, 2FA status
|
|
116
117
|
- `npm_tokens` — List access tokens
|
|
118
|
+
- `npm_verify_token` — One-call capability check (call this FIRST when debugging write failures)
|
|
117
119
|
- `npm_user_packages` — List packages published by a user
|
|
118
120
|
|
|
119
121
|
### Access (requires NPM_TOKEN)
|
|
@@ -130,9 +132,48 @@ Add to `claude_desktop_config.json`:
|
|
|
130
132
|
- `npm_check_auth` — Auth health check with headless publish feasibility
|
|
131
133
|
- `npm_publish_preflight` — Pre-publish validation checklist
|
|
132
134
|
|
|
135
|
+
### Write Operations (requires NPM_TOKEN with write scope)
|
|
136
|
+
|
|
137
|
+
These bypass the CLI/2FA friction that causes `npm deprecate` and similar commands to 422 locally. All use the HTTP API with your `NPM_TOKEN`.
|
|
138
|
+
|
|
139
|
+
- `npm_deprecate` — Deprecate a package or specific versions (validates message format)
|
|
140
|
+
- `npm_undeprecate` — Clear deprecation
|
|
141
|
+
- `npm_unpublish_version` — Unpublish a specific version (requires `confirm: true`)
|
|
142
|
+
- `npm_unpublish_package` — Unpublish an entire package (requires `confirm: true`)
|
|
143
|
+
- `npm_dist_tag_set` — Point a dist-tag at a version
|
|
144
|
+
- `npm_dist_tag_remove` — Remove a dist-tag (except `latest`)
|
|
145
|
+
- `npm_owner_add` — Add a maintainer (resolves user via `/-/user/`)
|
|
146
|
+
- `npm_owner_remove` — Remove a maintainer (prevents lockout)
|
|
147
|
+
- `npm_access_set` — Set public/private/restricted access
|
|
148
|
+
- `npm_access_set_mfa` — Configure 2FA requirement for publish (none/publish/automation)
|
|
149
|
+
- `npm_team_grant` / `npm_team_revoke` — Grant/revoke team permissions on a package
|
|
150
|
+
- `npm_team_create` / `npm_team_delete` — Create/delete a team in an org
|
|
151
|
+
- `npm_team_member_add` / `npm_team_member_remove` — Manage team members
|
|
152
|
+
- `npm_org_member_set` / `npm_org_member_remove` — Add/remove org members, set roles
|
|
153
|
+
- `npm_token_revoke` — Revoke an access token by key (creation requires a password and isn't exposed)
|
|
154
|
+
|
|
155
|
+
### Webhooks (requires NPM_TOKEN)
|
|
156
|
+
- `npm_hook_add` — Register a webhook on a package, scope, or user
|
|
157
|
+
- `npm_hook_list` — List webhooks (optional package filter)
|
|
158
|
+
- `npm_hook_get` — Fetch a single webhook
|
|
159
|
+
- `npm_hook_update` — Update endpoint/secret of a webhook
|
|
160
|
+
- `npm_hook_remove` — Delete a webhook
|
|
161
|
+
|
|
162
|
+
### Operation Decision Matrix
|
|
163
|
+
|
|
164
|
+
| Operation | Preferred path | Why |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| Read (search/view/stats) | These MCP tools, no auth required | Fast, zero friction |
|
|
167
|
+
| Deprecate / dist-tag / owner | `npm_deprecate`, `npm_dist_tag_*`, `npm_owner_*` | HTTP API, no CLI auth issues |
|
|
168
|
+
| Publish | CI tag-push workflow | Version discipline, provenance, org token |
|
|
169
|
+
| Unpublish | `npm_unpublish_version` (with `confirm: true`) | Safer than CLI; irreversible within 72h |
|
|
170
|
+
| CLI fallback (only if MCP returns 422) | `npm login --auth-type=web` then `npm <op>` | End-user interactive path |
|
|
171
|
+
|
|
172
|
+
Call `npm_ops_playbook` at the start of any session for the up-to-date matrix.
|
|
173
|
+
|
|
133
174
|
## Features
|
|
134
175
|
|
|
135
|
-
- **
|
|
176
|
+
- **63 tools** covering search, packages, deps, downloads, security, analysis, auth, orgs, access, provenance, trust, publish workflows, write operations, and registry webhooks
|
|
136
177
|
- **No API key required** for read-only tools — authenticated tools opt-in via NPM_TOKEN
|
|
137
178
|
- **Zero runtime dependencies** — Single bundled file for instant `npx` startup
|
|
138
179
|
- **Agent-aware publish tools** — Detects non-interactive context, provides human hand-off actions instead of unworkable retries
|
package/dist/index.js
CHANGED
|
@@ -21081,6 +21081,15 @@ function registryPost(path, body) {
|
|
|
21081
21081
|
function registryGetAuth(path) {
|
|
21082
21082
|
return request(REGISTRY_URL, path, { headers: authHeaders() });
|
|
21083
21083
|
}
|
|
21084
|
+
function registryPostAuth(path, body) {
|
|
21085
|
+
return request(REGISTRY_URL, path, { method: "POST", body, headers: authHeaders() });
|
|
21086
|
+
}
|
|
21087
|
+
function registryPutAuth(path, body) {
|
|
21088
|
+
return request(REGISTRY_URL, path, { method: "PUT", body, headers: authHeaders() });
|
|
21089
|
+
}
|
|
21090
|
+
function registryDeleteAuth(path, body) {
|
|
21091
|
+
return request(REGISTRY_URL, path, { method: "DELETE", body, headers: authHeaders() });
|
|
21092
|
+
}
|
|
21084
21093
|
function downloadsGet(path) {
|
|
21085
21094
|
return request(DOWNLOADS_URL, path);
|
|
21086
21095
|
}
|
|
@@ -21616,6 +21625,43 @@ var authTools = [
|
|
|
21616
21625
|
};
|
|
21617
21626
|
}
|
|
21618
21627
|
},
|
|
21628
|
+
{
|
|
21629
|
+
name: "npm_verify_token",
|
|
21630
|
+
description: "Verify the NPM_TOKEN and surface its capabilities \u2014 username, 2FA status, and whether writes are likely to succeed. Call this FIRST when debugging any write failure to rule out auth issues before trying other fixes. Faster than running writes and interpreting 401/403 errors.",
|
|
21631
|
+
annotations: {
|
|
21632
|
+
title: "Verify token capabilities",
|
|
21633
|
+
readOnlyHint: true,
|
|
21634
|
+
destructiveHint: false,
|
|
21635
|
+
idempotentHint: true,
|
|
21636
|
+
openWorldHint: true
|
|
21637
|
+
},
|
|
21638
|
+
inputSchema: external_exports.object({}),
|
|
21639
|
+
handler: async () => {
|
|
21640
|
+
const authErr = requireAuth();
|
|
21641
|
+
if (authErr) return authErr;
|
|
21642
|
+
const [whoami, profile] = await Promise.all([
|
|
21643
|
+
registryGetAuth("/-/whoami"),
|
|
21644
|
+
registryGetAuth("/-/npm/v1/user")
|
|
21645
|
+
]);
|
|
21646
|
+
if (!whoami.ok) {
|
|
21647
|
+
return {
|
|
21648
|
+
...whoami,
|
|
21649
|
+
error: `Token failed /-/whoami check. Token is invalid, expired, or revoked. Create a new one at https://www.npmjs.com/settings/~/tokens. Raw: ${whoami.error}`
|
|
21650
|
+
};
|
|
21651
|
+
}
|
|
21652
|
+
const tfa = profile.ok && profile.data?.tfa ? { enabled: true, mode: profile.data.tfa.mode } : { enabled: false };
|
|
21653
|
+
return {
|
|
21654
|
+
ok: true,
|
|
21655
|
+
status: 200,
|
|
21656
|
+
data: {
|
|
21657
|
+
username: whoami.data?.username,
|
|
21658
|
+
tokenValid: true,
|
|
21659
|
+
tfa,
|
|
21660
|
+
hint: "For write ops, token must have 'Read and write' scope on the target package. Granular Access Tokens require 2FA for writes; Classic Automation tokens bypass 2FA. Check your token's scope at https://www.npmjs.com/settings/~/tokens if writes return 401 or 403."
|
|
21661
|
+
}
|
|
21662
|
+
};
|
|
21663
|
+
}
|
|
21664
|
+
},
|
|
21619
21665
|
{
|
|
21620
21666
|
name: "npm_user_packages",
|
|
21621
21667
|
description: "List all packages published by a specific npm user. Shows package names and the user's access level for each. Requires authentication.",
|
|
@@ -21905,6 +21951,195 @@ var downloadTools = [
|
|
|
21905
21951
|
}
|
|
21906
21952
|
];
|
|
21907
21953
|
|
|
21954
|
+
// src/errors.ts
|
|
21955
|
+
function translateError(res, context) {
|
|
21956
|
+
if (res.ok) return res;
|
|
21957
|
+
const pkgPart = context.pkg ? ` for ${context.pkg}` : "";
|
|
21958
|
+
const opPart = context.op ? ` during ${context.op}` : "";
|
|
21959
|
+
switch (res.status) {
|
|
21960
|
+
case 401:
|
|
21961
|
+
return {
|
|
21962
|
+
...res,
|
|
21963
|
+
error: `Authentication failed${pkgPart}${opPart}. Your NPM_TOKEN may be invalid, expired, or lack write scope. Create a Granular Access Token with 'Read and write' permission at https://www.npmjs.com/settings/~/tokens, or use a classic Automation token (which bypasses 2FA). Raw: ${res.error}`
|
|
21964
|
+
};
|
|
21965
|
+
case 403:
|
|
21966
|
+
return {
|
|
21967
|
+
...res,
|
|
21968
|
+
error: `Not authorized${pkgPart}${opPart}. You may not be a maintainer of this package, or the token's scope doesn't include it. Check current maintainers with npm_collaborators or npm_package_access. Raw: ${res.error}`
|
|
21969
|
+
};
|
|
21970
|
+
case 404:
|
|
21971
|
+
return {
|
|
21972
|
+
...res,
|
|
21973
|
+
error: `Not found${pkgPart}${opPart}. Check the exact package name (scoped packages require the @scope/ prefix). If the version is specified, verify it exists with npm_package. Raw: ${res.error}`
|
|
21974
|
+
};
|
|
21975
|
+
case 422:
|
|
21976
|
+
return {
|
|
21977
|
+
...res,
|
|
21978
|
+
error: `Registry rejected the request payload${pkgPart}${opPart} (422 Unprocessable Entity). Most common causes: (1) invalid semver range \u2014 validate with npm_package versions first; (2) deprecation message format \u2014 em-dash form works, period-capital form sometimes 422s; (3) account-level 2FA policy requires interactive CLI session. If #3, CLI fallback: \`npm login --auth-type=web\` followed by the npm CLI command. Raw: ${res.error}`
|
|
21979
|
+
};
|
|
21980
|
+
case 429:
|
|
21981
|
+
return {
|
|
21982
|
+
...res,
|
|
21983
|
+
error: `Rate limited${opPart}. Wait 60 seconds and retry. Raw: ${res.error}`
|
|
21984
|
+
};
|
|
21985
|
+
case 0:
|
|
21986
|
+
return {
|
|
21987
|
+
...res,
|
|
21988
|
+
error: `Network error${opPart}. Could not reach the registry. Raw: ${res.error}`
|
|
21989
|
+
};
|
|
21990
|
+
default:
|
|
21991
|
+
return res;
|
|
21992
|
+
}
|
|
21993
|
+
}
|
|
21994
|
+
function validateDeprecationMessage(msg) {
|
|
21995
|
+
if (msg.length > 1024) {
|
|
21996
|
+
return "Deprecation message exceeds 1024 characters (registry limit).";
|
|
21997
|
+
}
|
|
21998
|
+
if (msg.length === 0) return null;
|
|
21999
|
+
if (/\.\s+[A-Z]/.test(msg)) {
|
|
22000
|
+
return `Deprecation message contains the "period + space + capital letter" pattern that has triggered 422 Unprocessable Entity on at least one scoped package. The working form uses em-dash and lowercase continuation: e.g. "Renamed to @yawlabs/spend \u2014 install that instead". Pass force: true to bypass this validation.`;
|
|
22001
|
+
}
|
|
22002
|
+
return null;
|
|
22003
|
+
}
|
|
22004
|
+
function versionsMatchingRange(versions, range, maxSatisfying2) {
|
|
22005
|
+
if (range === "*" || range === "") return [...versions];
|
|
22006
|
+
return versions.filter((v) => maxSatisfying2([v], range) === v);
|
|
22007
|
+
}
|
|
22008
|
+
|
|
22009
|
+
// src/tools/hooks.ts
|
|
22010
|
+
function classifyHookTarget(target) {
|
|
22011
|
+
if (target.startsWith("~")) return { type: "owner", name: target.slice(1) };
|
|
22012
|
+
if (/^@[^/]+$/.test(target)) return { type: "scope", name: target };
|
|
22013
|
+
return { type: "package", name: target };
|
|
22014
|
+
}
|
|
22015
|
+
var hookTools = [
|
|
22016
|
+
{
|
|
22017
|
+
name: "npm_hook_add",
|
|
22018
|
+
description: "Create a registry webhook. Target is 'pkg' or '@scope/pkg' for a package, '@scope' for a scope, or '~user' for a user's packages. Endpoint is the HTTPS URL to POST events to; secret is used to HMAC-sign payloads.",
|
|
22019
|
+
annotations: {
|
|
22020
|
+
title: "Add webhook",
|
|
22021
|
+
readOnlyHint: false,
|
|
22022
|
+
destructiveHint: true,
|
|
22023
|
+
idempotentHint: false,
|
|
22024
|
+
openWorldHint: true
|
|
22025
|
+
},
|
|
22026
|
+
inputSchema: external_exports.object({
|
|
22027
|
+
target: external_exports.string().describe("Hook target: 'pkg', '@scope/pkg', '@scope', or '~user'"),
|
|
22028
|
+
endpoint: external_exports.string().url().describe("HTTPS URL that will receive POST events"),
|
|
22029
|
+
secret: external_exports.string().describe("Secret used to HMAC-sign webhook payloads")
|
|
22030
|
+
}),
|
|
22031
|
+
handler: async (input) => {
|
|
22032
|
+
const authErr = requireAuth();
|
|
22033
|
+
if (authErr) return authErr;
|
|
22034
|
+
const { type, name } = classifyHookTarget(input.target);
|
|
22035
|
+
const res = await registryPostAuth("/-/npm/v1/hooks/hook", {
|
|
22036
|
+
type,
|
|
22037
|
+
name,
|
|
22038
|
+
endpoint: input.endpoint,
|
|
22039
|
+
secret: input.secret
|
|
22040
|
+
});
|
|
22041
|
+
if (!res.ok) return translateError(res, { op: `hook_add ${input.target}` });
|
|
22042
|
+
return { ok: true, status: 200, data: res.data };
|
|
22043
|
+
}
|
|
22044
|
+
},
|
|
22045
|
+
{
|
|
22046
|
+
name: "npm_hook_list",
|
|
22047
|
+
description: "List webhooks. Optionally filter by package name.",
|
|
22048
|
+
annotations: {
|
|
22049
|
+
title: "List webhooks",
|
|
22050
|
+
readOnlyHint: true,
|
|
22051
|
+
destructiveHint: false,
|
|
22052
|
+
idempotentHint: true,
|
|
22053
|
+
openWorldHint: true
|
|
22054
|
+
},
|
|
22055
|
+
inputSchema: external_exports.object({
|
|
22056
|
+
package: external_exports.string().optional().describe("Filter by package name"),
|
|
22057
|
+
limit: external_exports.number().int().optional().describe("Max results"),
|
|
22058
|
+
offset: external_exports.number().int().optional().describe("Pagination offset")
|
|
22059
|
+
}),
|
|
22060
|
+
handler: async (input) => {
|
|
22061
|
+
const authErr = requireAuth();
|
|
22062
|
+
if (authErr) return authErr;
|
|
22063
|
+
const qs = new URLSearchParams();
|
|
22064
|
+
if (input.package) qs.set("package", input.package);
|
|
22065
|
+
if (input.limit !== void 0) qs.set("limit", String(input.limit));
|
|
22066
|
+
if (input.offset !== void 0) qs.set("offset", String(input.offset));
|
|
22067
|
+
const q = qs.toString();
|
|
22068
|
+
const res = await registryGetAuth(`/-/npm/v1/hooks${q ? `?${q}` : ""}`);
|
|
22069
|
+
if (!res.ok) return translateError(res, { op: "hook_list" });
|
|
22070
|
+
return { ok: true, status: 200, data: res.data };
|
|
22071
|
+
}
|
|
22072
|
+
},
|
|
22073
|
+
{
|
|
22074
|
+
name: "npm_hook_get",
|
|
22075
|
+
description: "Get a single webhook by its ID.",
|
|
22076
|
+
annotations: {
|
|
22077
|
+
title: "Get webhook",
|
|
22078
|
+
readOnlyHint: true,
|
|
22079
|
+
destructiveHint: false,
|
|
22080
|
+
idempotentHint: true,
|
|
22081
|
+
openWorldHint: true
|
|
22082
|
+
},
|
|
22083
|
+
inputSchema: external_exports.object({
|
|
22084
|
+
id: external_exports.string().describe("Hook ID (UUID from npm_hook_list)")
|
|
22085
|
+
}),
|
|
22086
|
+
handler: async (input) => {
|
|
22087
|
+
const authErr = requireAuth();
|
|
22088
|
+
if (authErr) return authErr;
|
|
22089
|
+
const res = await registryGetAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`);
|
|
22090
|
+
if (!res.ok) return translateError(res, { op: `hook_get ${input.id}` });
|
|
22091
|
+
return { ok: true, status: 200, data: res.data };
|
|
22092
|
+
}
|
|
22093
|
+
},
|
|
22094
|
+
{
|
|
22095
|
+
name: "npm_hook_update",
|
|
22096
|
+
description: "Update a webhook's endpoint and/or secret.",
|
|
22097
|
+
annotations: {
|
|
22098
|
+
title: "Update webhook",
|
|
22099
|
+
readOnlyHint: false,
|
|
22100
|
+
destructiveHint: true,
|
|
22101
|
+
idempotentHint: true,
|
|
22102
|
+
openWorldHint: true
|
|
22103
|
+
},
|
|
22104
|
+
inputSchema: external_exports.object({
|
|
22105
|
+
id: external_exports.string().describe("Hook ID"),
|
|
22106
|
+
endpoint: external_exports.string().url().describe("New HTTPS URL"),
|
|
22107
|
+
secret: external_exports.string().describe("New signing secret")
|
|
22108
|
+
}),
|
|
22109
|
+
handler: async (input) => {
|
|
22110
|
+
const authErr = requireAuth();
|
|
22111
|
+
if (authErr) return authErr;
|
|
22112
|
+
const res = await registryPutAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`, {
|
|
22113
|
+
endpoint: input.endpoint,
|
|
22114
|
+
secret: input.secret
|
|
22115
|
+
});
|
|
22116
|
+
if (!res.ok) return translateError(res, { op: `hook_update ${input.id}` });
|
|
22117
|
+
return { ok: true, status: 200, data: res.data };
|
|
22118
|
+
}
|
|
22119
|
+
},
|
|
22120
|
+
{
|
|
22121
|
+
name: "npm_hook_remove",
|
|
22122
|
+
description: "Delete a webhook by ID.",
|
|
22123
|
+
annotations: {
|
|
22124
|
+
title: "Remove webhook",
|
|
22125
|
+
readOnlyHint: false,
|
|
22126
|
+
destructiveHint: true,
|
|
22127
|
+
idempotentHint: true,
|
|
22128
|
+
openWorldHint: true
|
|
22129
|
+
},
|
|
22130
|
+
inputSchema: external_exports.object({
|
|
22131
|
+
id: external_exports.string().describe("Hook ID")
|
|
22132
|
+
}),
|
|
22133
|
+
handler: async (input) => {
|
|
22134
|
+
const authErr = requireAuth();
|
|
22135
|
+
if (authErr) return authErr;
|
|
22136
|
+
const res = await registryDeleteAuth(`/-/npm/v1/hooks/hook/${encodeURIComponent(input.id)}`);
|
|
22137
|
+
if (!res.ok) return translateError(res, { op: `hook_remove ${input.id}` });
|
|
22138
|
+
return { ok: true, status: 200, data: { id: input.id, removed: true } };
|
|
22139
|
+
}
|
|
22140
|
+
}
|
|
22141
|
+
];
|
|
22142
|
+
|
|
21908
22143
|
// src/tools/orgs.ts
|
|
21909
22144
|
var orgTools = [
|
|
21910
22145
|
{
|
|
@@ -22357,6 +22592,72 @@ var registryTools = [
|
|
|
22357
22592
|
}
|
|
22358
22593
|
};
|
|
22359
22594
|
}
|
|
22595
|
+
},
|
|
22596
|
+
{
|
|
22597
|
+
name: "npm_ops_playbook",
|
|
22598
|
+
description: "Return canonical recipes for common npm operations \u2014 which MCP tool to call for which op, CLI fallbacks when the MCP server can't handle something, and message format guidance. Call this FIRST when you're not sure how to do an npm operation. Prevents reinventing approaches that don't work.",
|
|
22599
|
+
annotations: {
|
|
22600
|
+
title: "npm operations playbook",
|
|
22601
|
+
readOnlyHint: true,
|
|
22602
|
+
destructiveHint: false,
|
|
22603
|
+
idempotentHint: true,
|
|
22604
|
+
openWorldHint: false
|
|
22605
|
+
},
|
|
22606
|
+
inputSchema: external_exports.object({}),
|
|
22607
|
+
handler: async () => ({
|
|
22608
|
+
ok: true,
|
|
22609
|
+
status: 200,
|
|
22610
|
+
data: {
|
|
22611
|
+
read: {
|
|
22612
|
+
search: "mcp_tool: npm_search",
|
|
22613
|
+
view: "mcp_tool: npm_package",
|
|
22614
|
+
downloads: "mcp_tool: npm_downloads",
|
|
22615
|
+
securityAudit: "mcp_tool: npm_audit",
|
|
22616
|
+
auth: "none required"
|
|
22617
|
+
},
|
|
22618
|
+
write: {
|
|
22619
|
+
deprecate: {
|
|
22620
|
+
tool: "mcp_tool: npm_deprecate",
|
|
22621
|
+
requiresNpmToken: true,
|
|
22622
|
+
messageFormat: {
|
|
22623
|
+
preferred: "Renamed to @scope/pkg \u2014 install that instead",
|
|
22624
|
+
avoid: "Renamed to @scope/pkg. Install that instead.",
|
|
22625
|
+
note: "Period-capital form has triggered 422 on at least one scoped package; use em-dash."
|
|
22626
|
+
}
|
|
22627
|
+
},
|
|
22628
|
+
undeprecate: "mcp_tool: npm_undeprecate",
|
|
22629
|
+
unpublishVersion: "mcp_tool: npm_unpublish_version (requires confirm: true)",
|
|
22630
|
+
distTag: "mcp_tool: npm_dist_tag_set / npm_dist_tag_remove",
|
|
22631
|
+
owner: "mcp_tool: npm_owner_add / npm_owner_remove"
|
|
22632
|
+
},
|
|
22633
|
+
publish: {
|
|
22634
|
+
method: "CI tag-push",
|
|
22635
|
+
neverRunLocally: true,
|
|
22636
|
+
steps: [
|
|
22637
|
+
"Bump version in package.json",
|
|
22638
|
+
"git add package.json && git commit -m 'vX.Y.Z'",
|
|
22639
|
+
"git tag vX.Y.Z",
|
|
22640
|
+
"git push origin main --follow-tags",
|
|
22641
|
+
"gh run list --limit 1 to confirm CI published"
|
|
22642
|
+
],
|
|
22643
|
+
why: "Local npm publish bypasses version discipline and often fails on 2FA. CI uses repo-level NPM_TOKEN."
|
|
22644
|
+
},
|
|
22645
|
+
auth: {
|
|
22646
|
+
verifyToken: "mcp_tool: npm_verify_token (first step when debugging write failures)",
|
|
22647
|
+
envVar: "NPM_TOKEN",
|
|
22648
|
+
tokenTypes: {
|
|
22649
|
+
granularAccess: "Requires 2FA for writes. Most common.",
|
|
22650
|
+
classicAutomation: "Bypasses 2FA. Ideal for CI.",
|
|
22651
|
+
classicPublish: "Requires 2FA for writes."
|
|
22652
|
+
}
|
|
22653
|
+
},
|
|
22654
|
+
cliFallback: {
|
|
22655
|
+
when: "When an MCP write op returns 422 despite valid token (rare, account-level 2FA policy)",
|
|
22656
|
+
sequence: ["npm login --auth-type=web", "npm <deprecate|unpublish|dist-tag> <args>"],
|
|
22657
|
+
who: "End user runs in their terminal \u2014 MCP server cannot initiate browser auth."
|
|
22658
|
+
}
|
|
22659
|
+
}
|
|
22660
|
+
})
|
|
22360
22661
|
}
|
|
22361
22662
|
];
|
|
22362
22663
|
|
|
@@ -22795,8 +23096,845 @@ var workflowTools = [
|
|
|
22795
23096
|
}
|
|
22796
23097
|
];
|
|
22797
23098
|
|
|
23099
|
+
// src/tools/writes.ts
|
|
23100
|
+
async function fetchPackument(pkg) {
|
|
23101
|
+
return registryGetAuth(`/${encPkg(pkg)}?write=true`);
|
|
23102
|
+
}
|
|
23103
|
+
function highestVersion(versions) {
|
|
23104
|
+
const parsed = [];
|
|
23105
|
+
for (const v of versions) {
|
|
23106
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
23107
|
+
if (m) parsed.push([Number(m[1]), Number(m[2]), Number(m[3]), v]);
|
|
23108
|
+
}
|
|
23109
|
+
if (parsed.length === 0) return null;
|
|
23110
|
+
parsed.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
|
|
23111
|
+
return parsed[parsed.length - 1][3];
|
|
23112
|
+
}
|
|
23113
|
+
var writeTools = [
|
|
23114
|
+
// ───────────────────────────────────────────────────────
|
|
23115
|
+
// npm_deprecate
|
|
23116
|
+
// ───────────────────────────────────────────────────────
|
|
23117
|
+
{
|
|
23118
|
+
name: "npm_deprecate",
|
|
23119
|
+
description: "Deprecate a package or specific versions. Shows a warning message on install. Uses the HTTP API with NPM_TOKEN, bypassing the CLI auth friction that causes 422 errors on accounts with 2FA. Message format: prefer em-dash form ('Renamed to @scope/pkg \u2014 install that instead'); period-capital form sometimes triggers 422.",
|
|
23120
|
+
annotations: {
|
|
23121
|
+
title: "Deprecate package",
|
|
23122
|
+
readOnlyHint: false,
|
|
23123
|
+
destructiveHint: true,
|
|
23124
|
+
idempotentHint: true,
|
|
23125
|
+
openWorldHint: true
|
|
23126
|
+
},
|
|
23127
|
+
inputSchema: external_exports.object({
|
|
23128
|
+
name: external_exports.string().describe("Package name (e.g. '@yawlabs/tokenmeter-mcp')"),
|
|
23129
|
+
message: external_exports.string().describe("Deprecation message. Empty string to clear deprecation (use npm_undeprecate instead)."),
|
|
23130
|
+
versionRange: external_exports.string().optional().describe("Semver range. Omit to deprecate ALL versions. Example: '<1.0.0' or '0.3.x'."),
|
|
23131
|
+
force: external_exports.boolean().optional().describe("Bypass message format validation (default: false).")
|
|
23132
|
+
}),
|
|
23133
|
+
handler: async (input) => {
|
|
23134
|
+
const authErr = requireAuth();
|
|
23135
|
+
if (authErr) return authErr;
|
|
23136
|
+
if (!input.force) {
|
|
23137
|
+
const problem = validateDeprecationMessage(input.message);
|
|
23138
|
+
if (problem) return { ok: false, status: 400, error: problem };
|
|
23139
|
+
}
|
|
23140
|
+
const pRes = await fetchPackument(input.name);
|
|
23141
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "deprecate (fetch)" });
|
|
23142
|
+
const packument = pRes.data;
|
|
23143
|
+
const allVersions = Object.keys(packument.versions || {});
|
|
23144
|
+
const range = input.versionRange ?? "*";
|
|
23145
|
+
const affected = versionsMatchingRange(allVersions, range, maxSatisfying);
|
|
23146
|
+
if (affected.length === 0) {
|
|
23147
|
+
return {
|
|
23148
|
+
ok: false,
|
|
23149
|
+
status: 400,
|
|
23150
|
+
error: `No versions match range '${range}' for ${input.name}. Published versions: ${allVersions.join(", ") || "(none)"}.`
|
|
23151
|
+
};
|
|
23152
|
+
}
|
|
23153
|
+
for (const v of affected) {
|
|
23154
|
+
packument.versions[v].deprecated = input.message;
|
|
23155
|
+
}
|
|
23156
|
+
const putRes = await registryPutAuth(`/${encPkg(input.name)}`, packument);
|
|
23157
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "deprecate (write)" });
|
|
23158
|
+
return {
|
|
23159
|
+
ok: true,
|
|
23160
|
+
status: 200,
|
|
23161
|
+
data: {
|
|
23162
|
+
package: input.name,
|
|
23163
|
+
affectedVersions: affected,
|
|
23164
|
+
totalAffected: affected.length,
|
|
23165
|
+
message: input.message
|
|
23166
|
+
}
|
|
23167
|
+
};
|
|
23168
|
+
}
|
|
23169
|
+
},
|
|
23170
|
+
// ───────────────────────────────────────────────────────
|
|
23171
|
+
// npm_undeprecate
|
|
23172
|
+
// ───────────────────────────────────────────────────────
|
|
23173
|
+
{
|
|
23174
|
+
name: "npm_undeprecate",
|
|
23175
|
+
description: "Clear the deprecation message from a package or specific versions. Equivalent to npm_deprecate with an empty message but more explicit about intent.",
|
|
23176
|
+
annotations: {
|
|
23177
|
+
title: "Undeprecate package",
|
|
23178
|
+
readOnlyHint: false,
|
|
23179
|
+
destructiveHint: true,
|
|
23180
|
+
idempotentHint: true,
|
|
23181
|
+
openWorldHint: true
|
|
23182
|
+
},
|
|
23183
|
+
inputSchema: external_exports.object({
|
|
23184
|
+
name: external_exports.string().describe("Package name"),
|
|
23185
|
+
versionRange: external_exports.string().optional().describe("Semver range. Omit to undeprecate ALL versions.")
|
|
23186
|
+
}),
|
|
23187
|
+
handler: async (input) => {
|
|
23188
|
+
const authErr = requireAuth();
|
|
23189
|
+
if (authErr) return authErr;
|
|
23190
|
+
const pRes = await fetchPackument(input.name);
|
|
23191
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "undeprecate (fetch)" });
|
|
23192
|
+
const packument = pRes.data;
|
|
23193
|
+
const allVersions = Object.keys(packument.versions || {});
|
|
23194
|
+
const range = input.versionRange ?? "*";
|
|
23195
|
+
const affected = versionsMatchingRange(allVersions, range, maxSatisfying);
|
|
23196
|
+
if (affected.length === 0) {
|
|
23197
|
+
return {
|
|
23198
|
+
ok: false,
|
|
23199
|
+
status: 400,
|
|
23200
|
+
error: `No versions match range '${range}' for ${input.name}.`
|
|
23201
|
+
};
|
|
23202
|
+
}
|
|
23203
|
+
for (const v of affected) {
|
|
23204
|
+
packument.versions[v].deprecated = "";
|
|
23205
|
+
}
|
|
23206
|
+
const putRes = await registryPutAuth(`/${encPkg(input.name)}`, packument);
|
|
23207
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "undeprecate (write)" });
|
|
23208
|
+
return {
|
|
23209
|
+
ok: true,
|
|
23210
|
+
status: 200,
|
|
23211
|
+
data: {
|
|
23212
|
+
package: input.name,
|
|
23213
|
+
affectedVersions: affected,
|
|
23214
|
+
totalAffected: affected.length
|
|
23215
|
+
}
|
|
23216
|
+
};
|
|
23217
|
+
}
|
|
23218
|
+
},
|
|
23219
|
+
// ───────────────────────────────────────────────────────
|
|
23220
|
+
// npm_unpublish_version
|
|
23221
|
+
// ───────────────────────────────────────────────────────
|
|
23222
|
+
// Mirrors libnpmpublish/unpublish.js single-version flow:
|
|
23223
|
+
// 1. GET /{pkg}?write=true
|
|
23224
|
+
// 2. mutate: remove version, fix dist-tags, delete _revisions/_attachments
|
|
23225
|
+
// 3. PUT /{pkg}/-rev/{rev}
|
|
23226
|
+
// 4. GET /{pkg}?write=true (fresh rev)
|
|
23227
|
+
// 5. DELETE {tarball-pathname}/-rev/{newRev}
|
|
23228
|
+
// The tarball DELETE is best-effort — if it fails, the version is already unreachable
|
|
23229
|
+
// via the packument (step 3 succeeded), so we report success with a warning.
|
|
23230
|
+
{
|
|
23231
|
+
name: "npm_unpublish_version",
|
|
23232
|
+
description: "Unpublish a specific version of a package. IRREVERSIBLE: once unpublished, the version cannot be re-published and will be blocked for 72 hours. Only works within 72 hours of the original publish for most packages. Requires explicit confirm: true to prevent accidents. Follows the npm CLI flow (mutate packument + delete tarball). For full-package unpublish use npm_unpublish_package.",
|
|
23233
|
+
annotations: {
|
|
23234
|
+
title: "Unpublish version",
|
|
23235
|
+
readOnlyHint: false,
|
|
23236
|
+
destructiveHint: true,
|
|
23237
|
+
idempotentHint: false,
|
|
23238
|
+
openWorldHint: true
|
|
23239
|
+
},
|
|
23240
|
+
inputSchema: external_exports.object({
|
|
23241
|
+
name: external_exports.string().describe("Package name"),
|
|
23242
|
+
version: external_exports.string().describe("Specific version to unpublish (e.g. '1.2.3')"),
|
|
23243
|
+
confirm: external_exports.literal(true).describe("Must be literally true. Guards against accidental unpublish.")
|
|
23244
|
+
}),
|
|
23245
|
+
handler: async (input) => {
|
|
23246
|
+
const authErr = requireAuth();
|
|
23247
|
+
if (authErr) return authErr;
|
|
23248
|
+
if (input.confirm !== true) {
|
|
23249
|
+
return {
|
|
23250
|
+
ok: false,
|
|
23251
|
+
status: 400,
|
|
23252
|
+
error: "Unpublish requires confirm: true. This op is irreversible within 72 hours."
|
|
23253
|
+
};
|
|
23254
|
+
}
|
|
23255
|
+
const pRes = await fetchPackument(input.name);
|
|
23256
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "unpublish (fetch)" });
|
|
23257
|
+
const packument = pRes.data;
|
|
23258
|
+
const versionData = packument.versions?.[input.version];
|
|
23259
|
+
if (!versionData) {
|
|
23260
|
+
return {
|
|
23261
|
+
ok: false,
|
|
23262
|
+
status: 404,
|
|
23263
|
+
error: `Version ${input.version} not found for ${input.name}. Published versions: ${Object.keys(packument.versions || {}).join(", ")}.`
|
|
23264
|
+
};
|
|
23265
|
+
}
|
|
23266
|
+
if (!packument._rev) {
|
|
23267
|
+
return {
|
|
23268
|
+
ok: false,
|
|
23269
|
+
status: 500,
|
|
23270
|
+
error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish. Try again; this is usually transient.`
|
|
23271
|
+
};
|
|
23272
|
+
}
|
|
23273
|
+
const tarballUrl = versionData.dist?.tarball;
|
|
23274
|
+
const latestBefore = packument["dist-tags"]?.latest;
|
|
23275
|
+
delete packument.versions[input.version];
|
|
23276
|
+
for (const tag of Object.keys(packument["dist-tags"] || {})) {
|
|
23277
|
+
if (packument["dist-tags"][tag] === input.version) {
|
|
23278
|
+
delete packument["dist-tags"][tag];
|
|
23279
|
+
}
|
|
23280
|
+
}
|
|
23281
|
+
if (latestBefore === input.version) {
|
|
23282
|
+
const newLatest = highestVersion(Object.keys(packument.versions));
|
|
23283
|
+
if (newLatest) packument["dist-tags"].latest = newLatest;
|
|
23284
|
+
}
|
|
23285
|
+
delete packument._revisions;
|
|
23286
|
+
delete packument._attachments;
|
|
23287
|
+
const putRes = await registryPutAuth(
|
|
23288
|
+
`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`,
|
|
23289
|
+
packument
|
|
23290
|
+
);
|
|
23291
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "unpublish (packument PUT)" });
|
|
23292
|
+
let tarballDeleted = false;
|
|
23293
|
+
let tarballError;
|
|
23294
|
+
if (tarballUrl) {
|
|
23295
|
+
const freshRes = await fetchPackument(input.name);
|
|
23296
|
+
const freshRev = freshRes.ok ? freshRes.data._rev : void 0;
|
|
23297
|
+
if (freshRev) {
|
|
23298
|
+
try {
|
|
23299
|
+
const pathname = new URL(tarballUrl).pathname;
|
|
23300
|
+
const delRes = await registryDeleteAuth(`${pathname}/-rev/${encodeURIComponent(freshRev)}`);
|
|
23301
|
+
tarballDeleted = delRes.ok;
|
|
23302
|
+
if (!delRes.ok) tarballError = delRes.error;
|
|
23303
|
+
} catch (err) {
|
|
23304
|
+
tarballError = err instanceof Error ? err.message : String(err);
|
|
23305
|
+
}
|
|
23306
|
+
} else {
|
|
23307
|
+
tarballError = "could not re-fetch packument for tarball DELETE rev";
|
|
23308
|
+
}
|
|
23309
|
+
}
|
|
23310
|
+
return {
|
|
23311
|
+
ok: true,
|
|
23312
|
+
status: 200,
|
|
23313
|
+
data: {
|
|
23314
|
+
package: input.name,
|
|
23315
|
+
unpublishedVersion: input.version,
|
|
23316
|
+
remainingVersions: Object.keys(packument.versions),
|
|
23317
|
+
tarballDeleted,
|
|
23318
|
+
...tarballError ? { tarballWarning: tarballError } : {}
|
|
23319
|
+
}
|
|
23320
|
+
};
|
|
23321
|
+
}
|
|
23322
|
+
},
|
|
23323
|
+
// ───────────────────────────────────────────────────────
|
|
23324
|
+
// npm_unpublish_package
|
|
23325
|
+
// ───────────────────────────────────────────────────────
|
|
23326
|
+
{
|
|
23327
|
+
name: "npm_unpublish_package",
|
|
23328
|
+
description: "Unpublish an ENTIRE package (all versions). DELETE /{pkg}/-rev/{rev}. IRREVERSIBLE: the name is blocked for 72 hours and cannot be re-published. For single-version unpublish prefer npm_unpublish_version. Requires confirm: true.",
|
|
23329
|
+
annotations: {
|
|
23330
|
+
title: "Unpublish entire package",
|
|
23331
|
+
readOnlyHint: false,
|
|
23332
|
+
destructiveHint: true,
|
|
23333
|
+
idempotentHint: false,
|
|
23334
|
+
openWorldHint: true
|
|
23335
|
+
},
|
|
23336
|
+
inputSchema: external_exports.object({
|
|
23337
|
+
name: external_exports.string().describe("Package name"),
|
|
23338
|
+
confirm: external_exports.literal(true).describe("Must be literally true. Guards against accidental full unpublish.")
|
|
23339
|
+
}),
|
|
23340
|
+
handler: async (input) => {
|
|
23341
|
+
const authErr = requireAuth();
|
|
23342
|
+
if (authErr) return authErr;
|
|
23343
|
+
if (input.confirm !== true) {
|
|
23344
|
+
return {
|
|
23345
|
+
ok: false,
|
|
23346
|
+
status: 400,
|
|
23347
|
+
error: "Full-package unpublish requires confirm: true. This blocks the name for 72 hours."
|
|
23348
|
+
};
|
|
23349
|
+
}
|
|
23350
|
+
const pRes = await fetchPackument(input.name);
|
|
23351
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "unpublish_package (fetch)" });
|
|
23352
|
+
const rev = pRes.data._rev;
|
|
23353
|
+
if (!rev) {
|
|
23354
|
+
return {
|
|
23355
|
+
ok: false,
|
|
23356
|
+
status: 500,
|
|
23357
|
+
error: `Packument for ${input.name} missing _rev \u2014 cannot unpublish.`
|
|
23358
|
+
};
|
|
23359
|
+
}
|
|
23360
|
+
const delRes = await registryDeleteAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(rev)}`);
|
|
23361
|
+
if (!delRes.ok) return translateError(delRes, { pkg: input.name, op: "unpublish_package (DELETE)" });
|
|
23362
|
+
return {
|
|
23363
|
+
ok: true,
|
|
23364
|
+
status: 200,
|
|
23365
|
+
data: { package: input.name, unpublished: true }
|
|
23366
|
+
};
|
|
23367
|
+
}
|
|
23368
|
+
},
|
|
23369
|
+
// ───────────────────────────────────────────────────────
|
|
23370
|
+
// npm_dist_tag_set
|
|
23371
|
+
// ───────────────────────────────────────────────────────
|
|
23372
|
+
{
|
|
23373
|
+
name: "npm_dist_tag_set",
|
|
23374
|
+
description: "Point a dist-tag (e.g. 'latest', 'beta', 'next') at a specific version. Common uses: promote a beta to latest, roll back latest to a prior version, maintain separate channels.",
|
|
23375
|
+
annotations: {
|
|
23376
|
+
title: "Set dist-tag",
|
|
23377
|
+
readOnlyHint: false,
|
|
23378
|
+
destructiveHint: true,
|
|
23379
|
+
idempotentHint: true,
|
|
23380
|
+
openWorldHint: true
|
|
23381
|
+
},
|
|
23382
|
+
inputSchema: external_exports.object({
|
|
23383
|
+
name: external_exports.string().describe("Package name"),
|
|
23384
|
+
tag: external_exports.string().describe("Dist-tag name (e.g. 'latest', 'beta', 'next')"),
|
|
23385
|
+
version: external_exports.string().describe("Version the tag should point to")
|
|
23386
|
+
}),
|
|
23387
|
+
handler: async (input) => {
|
|
23388
|
+
const authErr = requireAuth();
|
|
23389
|
+
if (authErr) return authErr;
|
|
23390
|
+
const putRes = await registryPutAuth(
|
|
23391
|
+
`/-/package/${encPkg(input.name)}/dist-tags/${encodeURIComponent(input.tag)}`,
|
|
23392
|
+
input.version
|
|
23393
|
+
);
|
|
23394
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: `dist-tag set ${input.tag}` });
|
|
23395
|
+
return {
|
|
23396
|
+
ok: true,
|
|
23397
|
+
status: 200,
|
|
23398
|
+
data: {
|
|
23399
|
+
package: input.name,
|
|
23400
|
+
tag: input.tag,
|
|
23401
|
+
version: input.version
|
|
23402
|
+
}
|
|
23403
|
+
};
|
|
23404
|
+
}
|
|
23405
|
+
},
|
|
23406
|
+
// ───────────────────────────────────────────────────────
|
|
23407
|
+
// npm_dist_tag_remove
|
|
23408
|
+
// ───────────────────────────────────────────────────────
|
|
23409
|
+
{
|
|
23410
|
+
name: "npm_dist_tag_remove",
|
|
23411
|
+
description: "Remove a dist-tag from a package. The 'latest' tag cannot be removed, only reassigned.",
|
|
23412
|
+
annotations: {
|
|
23413
|
+
title: "Remove dist-tag",
|
|
23414
|
+
readOnlyHint: false,
|
|
23415
|
+
destructiveHint: true,
|
|
23416
|
+
idempotentHint: true,
|
|
23417
|
+
openWorldHint: true
|
|
23418
|
+
},
|
|
23419
|
+
inputSchema: external_exports.object({
|
|
23420
|
+
name: external_exports.string().describe("Package name"),
|
|
23421
|
+
tag: external_exports.string().describe("Dist-tag name to remove")
|
|
23422
|
+
}),
|
|
23423
|
+
handler: async (input) => {
|
|
23424
|
+
const authErr = requireAuth();
|
|
23425
|
+
if (authErr) return authErr;
|
|
23426
|
+
if (input.tag === "latest") {
|
|
23427
|
+
return {
|
|
23428
|
+
ok: false,
|
|
23429
|
+
status: 400,
|
|
23430
|
+
error: "The 'latest' tag cannot be removed. Use npm_dist_tag_set to reassign it to a different version."
|
|
23431
|
+
};
|
|
23432
|
+
}
|
|
23433
|
+
const delRes = await registryDeleteAuth(
|
|
23434
|
+
`/-/package/${encPkg(input.name)}/dist-tags/${encodeURIComponent(input.tag)}`
|
|
23435
|
+
);
|
|
23436
|
+
if (!delRes.ok) return translateError(delRes, { pkg: input.name, op: `dist-tag remove ${input.tag}` });
|
|
23437
|
+
return {
|
|
23438
|
+
ok: true,
|
|
23439
|
+
status: 200,
|
|
23440
|
+
data: { package: input.name, removedTag: input.tag }
|
|
23441
|
+
};
|
|
23442
|
+
}
|
|
23443
|
+
},
|
|
23444
|
+
// ───────────────────────────────────────────────────────
|
|
23445
|
+
// npm_owner_add
|
|
23446
|
+
// ───────────────────────────────────────────────────────
|
|
23447
|
+
// Mirrors npm CLI owner.js:
|
|
23448
|
+
// 1. GET /-/user/org.couchdb.user:{user} → resolve {name, email}
|
|
23449
|
+
// 2. GET /{pkg}?write=true → packument with _rev
|
|
23450
|
+
// 3. PUT /{pkg}/-rev/{_rev} body {_id, _rev, maintainers}
|
|
23451
|
+
{
|
|
23452
|
+
name: "npm_owner_add",
|
|
23453
|
+
description: "Add a user as a maintainer of a package. They will have publish and write permissions. Resolves the user's email via /-/user/ (no need to supply it). Use npm_collaborators to verify before adding.",
|
|
23454
|
+
annotations: {
|
|
23455
|
+
title: "Add package owner",
|
|
23456
|
+
readOnlyHint: false,
|
|
23457
|
+
destructiveHint: true,
|
|
23458
|
+
idempotentHint: true,
|
|
23459
|
+
openWorldHint: true
|
|
23460
|
+
},
|
|
23461
|
+
inputSchema: external_exports.object({
|
|
23462
|
+
name: external_exports.string().describe("Package name"),
|
|
23463
|
+
username: external_exports.string().describe("npm username to add as maintainer")
|
|
23464
|
+
}),
|
|
23465
|
+
handler: async (input) => {
|
|
23466
|
+
const authErr = requireAuth();
|
|
23467
|
+
if (authErr) return authErr;
|
|
23468
|
+
const uRes = await registryGetAuth(
|
|
23469
|
+
`/-/user/org.couchdb.user:${encodeURIComponent(input.username)}`
|
|
23470
|
+
);
|
|
23471
|
+
if (!uRes.ok) return translateError(uRes, { pkg: input.name, op: `owner_add (resolve user ${input.username})` });
|
|
23472
|
+
const userRecord = { name: uRes.data.name, email: uRes.data.email ?? "" };
|
|
23473
|
+
const pRes = await fetchPackument(input.name);
|
|
23474
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "owner_add (fetch)" });
|
|
23475
|
+
const packument = pRes.data;
|
|
23476
|
+
const owners = packument.maintainers || [];
|
|
23477
|
+
if (owners.some((m) => m.name === userRecord.name)) {
|
|
23478
|
+
return {
|
|
23479
|
+
ok: true,
|
|
23480
|
+
status: 200,
|
|
23481
|
+
data: {
|
|
23482
|
+
package: input.name,
|
|
23483
|
+
username: userRecord.name,
|
|
23484
|
+
alreadyOwner: true,
|
|
23485
|
+
maintainers: owners.map((m) => m.name)
|
|
23486
|
+
}
|
|
23487
|
+
};
|
|
23488
|
+
}
|
|
23489
|
+
if (!packument._rev) {
|
|
23490
|
+
return {
|
|
23491
|
+
ok: false,
|
|
23492
|
+
status: 500,
|
|
23493
|
+
error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
|
|
23494
|
+
};
|
|
23495
|
+
}
|
|
23496
|
+
const maintainers = [...owners, userRecord];
|
|
23497
|
+
const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
|
|
23498
|
+
_id: packument._id,
|
|
23499
|
+
_rev: packument._rev,
|
|
23500
|
+
maintainers
|
|
23501
|
+
});
|
|
23502
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "owner_add (write)" });
|
|
23503
|
+
return {
|
|
23504
|
+
ok: true,
|
|
23505
|
+
status: 200,
|
|
23506
|
+
data: {
|
|
23507
|
+
package: input.name,
|
|
23508
|
+
addedOwner: userRecord.name,
|
|
23509
|
+
maintainers: maintainers.map((m) => m.name)
|
|
23510
|
+
}
|
|
23511
|
+
};
|
|
23512
|
+
}
|
|
23513
|
+
},
|
|
23514
|
+
// ───────────────────────────────────────────────────────
|
|
23515
|
+
// npm_owner_remove
|
|
23516
|
+
// ───────────────────────────────────────────────────────
|
|
23517
|
+
{
|
|
23518
|
+
name: "npm_owner_remove",
|
|
23519
|
+
description: "Remove a user from a package's maintainer list. Refuses if it would leave the package with zero maintainers (lockout prevention).",
|
|
23520
|
+
annotations: {
|
|
23521
|
+
title: "Remove package owner",
|
|
23522
|
+
readOnlyHint: false,
|
|
23523
|
+
destructiveHint: true,
|
|
23524
|
+
idempotentHint: true,
|
|
23525
|
+
openWorldHint: true
|
|
23526
|
+
},
|
|
23527
|
+
inputSchema: external_exports.object({
|
|
23528
|
+
name: external_exports.string().describe("Package name"),
|
|
23529
|
+
username: external_exports.string().describe("npm username to remove")
|
|
23530
|
+
}),
|
|
23531
|
+
handler: async (input) => {
|
|
23532
|
+
const authErr = requireAuth();
|
|
23533
|
+
if (authErr) return authErr;
|
|
23534
|
+
const pRes = await fetchPackument(input.name);
|
|
23535
|
+
if (!pRes.ok) return translateError(pRes, { pkg: input.name, op: "owner_remove (fetch)" });
|
|
23536
|
+
const packument = pRes.data;
|
|
23537
|
+
const before = packument.maintainers || [];
|
|
23538
|
+
if (!before.some((m) => m.name === input.username)) {
|
|
23539
|
+
return {
|
|
23540
|
+
ok: false,
|
|
23541
|
+
status: 404,
|
|
23542
|
+
error: `${input.username} is not a maintainer of ${input.name}. Current maintainers: ${before.map((m) => m.name).join(", ") || "(none)"}.`
|
|
23543
|
+
};
|
|
23544
|
+
}
|
|
23545
|
+
const after = before.filter((m) => m.name !== input.username);
|
|
23546
|
+
if (after.length === 0) {
|
|
23547
|
+
return {
|
|
23548
|
+
ok: false,
|
|
23549
|
+
status: 400,
|
|
23550
|
+
error: `Removing ${input.username} would leave ${input.name} with zero maintainers (lockout). Add another maintainer first with npm_owner_add.`
|
|
23551
|
+
};
|
|
23552
|
+
}
|
|
23553
|
+
if (!packument._rev) {
|
|
23554
|
+
return {
|
|
23555
|
+
ok: false,
|
|
23556
|
+
status: 500,
|
|
23557
|
+
error: `Packument for ${input.name} missing _rev \u2014 cannot update owners.`
|
|
23558
|
+
};
|
|
23559
|
+
}
|
|
23560
|
+
const putRes = await registryPutAuth(`/${encPkg(input.name)}/-rev/${encodeURIComponent(packument._rev)}`, {
|
|
23561
|
+
_id: packument._id,
|
|
23562
|
+
_rev: packument._rev,
|
|
23563
|
+
maintainers: after
|
|
23564
|
+
});
|
|
23565
|
+
if (!putRes.ok) return translateError(putRes, { pkg: input.name, op: "owner_remove (write)" });
|
|
23566
|
+
return {
|
|
23567
|
+
ok: true,
|
|
23568
|
+
status: 200,
|
|
23569
|
+
data: {
|
|
23570
|
+
package: input.name,
|
|
23571
|
+
removedOwner: input.username,
|
|
23572
|
+
remainingMaintainers: after.map((m) => m.name)
|
|
23573
|
+
}
|
|
23574
|
+
};
|
|
23575
|
+
}
|
|
23576
|
+
},
|
|
23577
|
+
// ───────────────────────────────────────────────────────
|
|
23578
|
+
// npm_access_set
|
|
23579
|
+
// ───────────────────────────────────────────────────────
|
|
23580
|
+
{
|
|
23581
|
+
name: "npm_access_set",
|
|
23582
|
+
description: "Set package access level: 'public', 'private', or 'restricted'. Unscoped packages are always public. Private access requires a paid npm account.",
|
|
23583
|
+
annotations: {
|
|
23584
|
+
title: "Set package access level",
|
|
23585
|
+
readOnlyHint: false,
|
|
23586
|
+
destructiveHint: true,
|
|
23587
|
+
idempotentHint: true,
|
|
23588
|
+
openWorldHint: true
|
|
23589
|
+
},
|
|
23590
|
+
inputSchema: external_exports.object({
|
|
23591
|
+
name: external_exports.string().describe("Package name"),
|
|
23592
|
+
access: external_exports.enum(["public", "private", "restricted"]).describe("Access level")
|
|
23593
|
+
}),
|
|
23594
|
+
handler: async (input) => {
|
|
23595
|
+
const authErr = requireAuth();
|
|
23596
|
+
if (authErr) return authErr;
|
|
23597
|
+
const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, { access: input.access });
|
|
23598
|
+
if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set ${input.access}` });
|
|
23599
|
+
return {
|
|
23600
|
+
ok: true,
|
|
23601
|
+
status: 200,
|
|
23602
|
+
data: { package: input.name, access: input.access }
|
|
23603
|
+
};
|
|
23604
|
+
}
|
|
23605
|
+
},
|
|
23606
|
+
// ───────────────────────────────────────────────────────
|
|
23607
|
+
// npm_access_set_mfa
|
|
23608
|
+
// ───────────────────────────────────────────────────────
|
|
23609
|
+
{
|
|
23610
|
+
name: "npm_access_set_mfa",
|
|
23611
|
+
description: "Configure 2FA requirement for publishing: 'none' (off), 'publish' (2FA required), 'automation' (2FA required but automation tokens can bypass).",
|
|
23612
|
+
annotations: {
|
|
23613
|
+
title: "Set package 2FA publish policy",
|
|
23614
|
+
readOnlyHint: false,
|
|
23615
|
+
destructiveHint: true,
|
|
23616
|
+
idempotentHint: true,
|
|
23617
|
+
openWorldHint: true
|
|
23618
|
+
},
|
|
23619
|
+
inputSchema: external_exports.object({
|
|
23620
|
+
name: external_exports.string().describe("Package name"),
|
|
23621
|
+
level: external_exports.enum(["none", "publish", "automation"]).describe("MFA level for publish")
|
|
23622
|
+
}),
|
|
23623
|
+
handler: async (input) => {
|
|
23624
|
+
const authErr = requireAuth();
|
|
23625
|
+
if (authErr) return authErr;
|
|
23626
|
+
let body;
|
|
23627
|
+
if (input.level === "none") {
|
|
23628
|
+
body = { publish_requires_tfa: false };
|
|
23629
|
+
} else if (input.level === "publish") {
|
|
23630
|
+
body = { publish_requires_tfa: true, automation_token_overrides_tfa: false };
|
|
23631
|
+
} else {
|
|
23632
|
+
body = { publish_requires_tfa: true, automation_token_overrides_tfa: true };
|
|
23633
|
+
}
|
|
23634
|
+
const res = await registryPostAuth(`/-/package/${encPkg(input.name)}/access`, body);
|
|
23635
|
+
if (!res.ok) return translateError(res, { pkg: input.name, op: `access_set_mfa ${input.level}` });
|
|
23636
|
+
return {
|
|
23637
|
+
ok: true,
|
|
23638
|
+
status: 200,
|
|
23639
|
+
data: { package: input.name, mfaLevel: input.level }
|
|
23640
|
+
};
|
|
23641
|
+
}
|
|
23642
|
+
},
|
|
23643
|
+
// ───────────────────────────────────────────────────────
|
|
23644
|
+
// npm_team_grant
|
|
23645
|
+
// ───────────────────────────────────────────────────────
|
|
23646
|
+
{
|
|
23647
|
+
name: "npm_team_grant",
|
|
23648
|
+
description: "Grant a team read-only or read-write permission on a package. Scope and team are passed as @scope:team (e.g. '@yawlabs:devs'). Requires org admin or team admin.",
|
|
23649
|
+
annotations: {
|
|
23650
|
+
title: "Grant team package permission",
|
|
23651
|
+
readOnlyHint: false,
|
|
23652
|
+
destructiveHint: true,
|
|
23653
|
+
idempotentHint: true,
|
|
23654
|
+
openWorldHint: true
|
|
23655
|
+
},
|
|
23656
|
+
inputSchema: external_exports.object({
|
|
23657
|
+
team: external_exports.string().describe("Team in the form '@scope:team' (e.g. '@yawlabs:devs')"),
|
|
23658
|
+
package: external_exports.string().describe("Package name"),
|
|
23659
|
+
permissions: external_exports.enum(["read-only", "read-write"]).describe("Permission level")
|
|
23660
|
+
}),
|
|
23661
|
+
handler: async (input) => {
|
|
23662
|
+
const authErr = requireAuth();
|
|
23663
|
+
if (authErr) return authErr;
|
|
23664
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23665
|
+
if (!m) {
|
|
23666
|
+
return {
|
|
23667
|
+
ok: false,
|
|
23668
|
+
status: 400,
|
|
23669
|
+
error: `Team must be in the form '@scope:team' (got '${input.team}').`
|
|
23670
|
+
};
|
|
23671
|
+
}
|
|
23672
|
+
const [, scope, team] = m;
|
|
23673
|
+
const res = await registryPutAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, {
|
|
23674
|
+
package: input.package,
|
|
23675
|
+
permissions: input.permissions
|
|
23676
|
+
});
|
|
23677
|
+
if (!res.ok) return translateError(res, { pkg: input.package, op: `team_grant ${input.team}` });
|
|
23678
|
+
return {
|
|
23679
|
+
ok: true,
|
|
23680
|
+
status: 200,
|
|
23681
|
+
data: { team: `@${scope}:${team}`, package: input.package, permissions: input.permissions }
|
|
23682
|
+
};
|
|
23683
|
+
}
|
|
23684
|
+
},
|
|
23685
|
+
// ───────────────────────────────────────────────────────
|
|
23686
|
+
// npm_team_revoke
|
|
23687
|
+
// ───────────────────────────────────────────────────────
|
|
23688
|
+
{
|
|
23689
|
+
name: "npm_team_revoke",
|
|
23690
|
+
description: "Revoke a team's access to a package. Team is passed as '@scope:team'. Does not delete the team itself \u2014 use npm_team_delete for that.",
|
|
23691
|
+
annotations: {
|
|
23692
|
+
title: "Revoke team package permission",
|
|
23693
|
+
readOnlyHint: false,
|
|
23694
|
+
destructiveHint: true,
|
|
23695
|
+
idempotentHint: true,
|
|
23696
|
+
openWorldHint: true
|
|
23697
|
+
},
|
|
23698
|
+
inputSchema: external_exports.object({
|
|
23699
|
+
team: external_exports.string().describe("Team in the form '@scope:team'"),
|
|
23700
|
+
package: external_exports.string().describe("Package name")
|
|
23701
|
+
}),
|
|
23702
|
+
handler: async (input) => {
|
|
23703
|
+
const authErr = requireAuth();
|
|
23704
|
+
if (authErr) return authErr;
|
|
23705
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23706
|
+
if (!m) {
|
|
23707
|
+
return {
|
|
23708
|
+
ok: false,
|
|
23709
|
+
status: 400,
|
|
23710
|
+
error: `Team must be in the form '@scope:team' (got '${input.team}').`
|
|
23711
|
+
};
|
|
23712
|
+
}
|
|
23713
|
+
const [, scope, team] = m;
|
|
23714
|
+
const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, {
|
|
23715
|
+
package: input.package
|
|
23716
|
+
});
|
|
23717
|
+
if (!res.ok) return translateError(res, { pkg: input.package, op: `team_revoke ${input.team}` });
|
|
23718
|
+
return {
|
|
23719
|
+
ok: true,
|
|
23720
|
+
status: 200,
|
|
23721
|
+
data: { team: `@${scope}:${team}`, package: input.package, revoked: true }
|
|
23722
|
+
};
|
|
23723
|
+
}
|
|
23724
|
+
},
|
|
23725
|
+
// ───────────────────────────────────────────────────────
|
|
23726
|
+
// npm_team_create
|
|
23727
|
+
// ───────────────────────────────────────────────────────
|
|
23728
|
+
{
|
|
23729
|
+
name: "npm_team_create",
|
|
23730
|
+
description: "Create a team inside an organization. Team is passed as '@scope:team'.",
|
|
23731
|
+
annotations: {
|
|
23732
|
+
title: "Create team",
|
|
23733
|
+
readOnlyHint: false,
|
|
23734
|
+
destructiveHint: true,
|
|
23735
|
+
idempotentHint: false,
|
|
23736
|
+
openWorldHint: true
|
|
23737
|
+
},
|
|
23738
|
+
inputSchema: external_exports.object({
|
|
23739
|
+
team: external_exports.string().describe("Team in the form '@scope:team'"),
|
|
23740
|
+
description: external_exports.string().optional().describe("Optional team description")
|
|
23741
|
+
}),
|
|
23742
|
+
handler: async (input) => {
|
|
23743
|
+
const authErr = requireAuth();
|
|
23744
|
+
if (authErr) return authErr;
|
|
23745
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23746
|
+
if (!m) {
|
|
23747
|
+
return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
|
|
23748
|
+
}
|
|
23749
|
+
const [, scope, team] = m;
|
|
23750
|
+
const res = await registryPutAuth(`/-/org/${encodeURIComponent(scope)}/team`, {
|
|
23751
|
+
name: team,
|
|
23752
|
+
description: input.description
|
|
23753
|
+
});
|
|
23754
|
+
if (!res.ok) return translateError(res, { op: `team_create ${input.team}` });
|
|
23755
|
+
return { ok: true, status: 200, data: { team: `@${scope}:${team}`, created: true } };
|
|
23756
|
+
}
|
|
23757
|
+
},
|
|
23758
|
+
// ───────────────────────────────────────────────────────
|
|
23759
|
+
// npm_team_delete
|
|
23760
|
+
// ───────────────────────────────────────────────────────
|
|
23761
|
+
{
|
|
23762
|
+
name: "npm_team_delete",
|
|
23763
|
+
description: "Delete a team. Team is passed as '@scope:team'. Revokes all package permissions that team held.",
|
|
23764
|
+
annotations: {
|
|
23765
|
+
title: "Delete team",
|
|
23766
|
+
readOnlyHint: false,
|
|
23767
|
+
destructiveHint: true,
|
|
23768
|
+
idempotentHint: true,
|
|
23769
|
+
openWorldHint: true
|
|
23770
|
+
},
|
|
23771
|
+
inputSchema: external_exports.object({
|
|
23772
|
+
team: external_exports.string().describe("Team in the form '@scope:team'")
|
|
23773
|
+
}),
|
|
23774
|
+
handler: async (input) => {
|
|
23775
|
+
const authErr = requireAuth();
|
|
23776
|
+
if (authErr) return authErr;
|
|
23777
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23778
|
+
if (!m) {
|
|
23779
|
+
return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
|
|
23780
|
+
}
|
|
23781
|
+
const [, scope, team] = m;
|
|
23782
|
+
const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}`);
|
|
23783
|
+
if (!res.ok) return translateError(res, { op: `team_delete ${input.team}` });
|
|
23784
|
+
return { ok: true, status: 200, data: { team: `@${scope}:${team}`, deleted: true } };
|
|
23785
|
+
}
|
|
23786
|
+
},
|
|
23787
|
+
// ───────────────────────────────────────────────────────
|
|
23788
|
+
// npm_team_member_add
|
|
23789
|
+
// ───────────────────────────────────────────────────────
|
|
23790
|
+
{
|
|
23791
|
+
name: "npm_team_member_add",
|
|
23792
|
+
description: "Add a user to a team. Team is '@scope:team'. User must already be in the org.",
|
|
23793
|
+
annotations: {
|
|
23794
|
+
title: "Add team member",
|
|
23795
|
+
readOnlyHint: false,
|
|
23796
|
+
destructiveHint: true,
|
|
23797
|
+
idempotentHint: true,
|
|
23798
|
+
openWorldHint: true
|
|
23799
|
+
},
|
|
23800
|
+
inputSchema: external_exports.object({
|
|
23801
|
+
team: external_exports.string().describe("Team in the form '@scope:team'"),
|
|
23802
|
+
user: external_exports.string().describe("npm username")
|
|
23803
|
+
}),
|
|
23804
|
+
handler: async (input) => {
|
|
23805
|
+
const authErr = requireAuth();
|
|
23806
|
+
if (authErr) return authErr;
|
|
23807
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23808
|
+
if (!m) {
|
|
23809
|
+
return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
|
|
23810
|
+
}
|
|
23811
|
+
const [, scope, team] = m;
|
|
23812
|
+
const res = await registryPutAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/user`, {
|
|
23813
|
+
user: input.user
|
|
23814
|
+
});
|
|
23815
|
+
if (!res.ok) return translateError(res, { op: `team_member_add ${input.team}` });
|
|
23816
|
+
return { ok: true, status: 200, data: { team: `@${scope}:${team}`, addedUser: input.user } };
|
|
23817
|
+
}
|
|
23818
|
+
},
|
|
23819
|
+
// ───────────────────────────────────────────────────────
|
|
23820
|
+
// npm_team_member_remove
|
|
23821
|
+
// ───────────────────────────────────────────────────────
|
|
23822
|
+
{
|
|
23823
|
+
name: "npm_team_member_remove",
|
|
23824
|
+
description: "Remove a user from a team. Team is '@scope:team'. User remains in the org.",
|
|
23825
|
+
annotations: {
|
|
23826
|
+
title: "Remove team member",
|
|
23827
|
+
readOnlyHint: false,
|
|
23828
|
+
destructiveHint: true,
|
|
23829
|
+
idempotentHint: true,
|
|
23830
|
+
openWorldHint: true
|
|
23831
|
+
},
|
|
23832
|
+
inputSchema: external_exports.object({
|
|
23833
|
+
team: external_exports.string().describe("Team in the form '@scope:team'"),
|
|
23834
|
+
user: external_exports.string().describe("npm username")
|
|
23835
|
+
}),
|
|
23836
|
+
handler: async (input) => {
|
|
23837
|
+
const authErr = requireAuth();
|
|
23838
|
+
if (authErr) return authErr;
|
|
23839
|
+
const m = input.team.match(/^@?([^:]+):(.+)$/);
|
|
23840
|
+
if (!m) {
|
|
23841
|
+
return { ok: false, status: 400, error: `Team must be in the form '@scope:team' (got '${input.team}').` };
|
|
23842
|
+
}
|
|
23843
|
+
const [, scope, team] = m;
|
|
23844
|
+
const res = await registryDeleteAuth(`/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/user`, {
|
|
23845
|
+
user: input.user
|
|
23846
|
+
});
|
|
23847
|
+
if (!res.ok) return translateError(res, { op: `team_member_remove ${input.team}` });
|
|
23848
|
+
return { ok: true, status: 200, data: { team: `@${scope}:${team}`, removedUser: input.user } };
|
|
23849
|
+
}
|
|
23850
|
+
},
|
|
23851
|
+
// ───────────────────────────────────────────────────────
|
|
23852
|
+
// npm_org_member_set
|
|
23853
|
+
// ───────────────────────────────────────────────────────
|
|
23854
|
+
{
|
|
23855
|
+
name: "npm_org_member_set",
|
|
23856
|
+
description: "Add a user to an org or change their role. Roles: 'developer', 'admin', 'owner'. If user is already in the org, updates the role. Omit role to keep existing role.",
|
|
23857
|
+
annotations: {
|
|
23858
|
+
title: "Set org member",
|
|
23859
|
+
readOnlyHint: false,
|
|
23860
|
+
destructiveHint: true,
|
|
23861
|
+
idempotentHint: true,
|
|
23862
|
+
openWorldHint: true
|
|
23863
|
+
},
|
|
23864
|
+
inputSchema: external_exports.object({
|
|
23865
|
+
org: external_exports.string().describe("Organization name (with or without leading @)"),
|
|
23866
|
+
user: external_exports.string().describe("npm username"),
|
|
23867
|
+
role: external_exports.enum(["developer", "admin", "owner"]).optional().describe("Role to assign")
|
|
23868
|
+
}),
|
|
23869
|
+
handler: async (input) => {
|
|
23870
|
+
const authErr = requireAuth();
|
|
23871
|
+
if (authErr) return authErr;
|
|
23872
|
+
const org = input.org.replace(/^@/, "");
|
|
23873
|
+
const user = input.user.replace(/^@/, "");
|
|
23874
|
+
const body = { user };
|
|
23875
|
+
if (input.role) body.role = input.role;
|
|
23876
|
+
const res = await registryPutAuth(`/-/org/${encodeURIComponent(org)}/user`, body);
|
|
23877
|
+
if (!res.ok) return translateError(res, { op: `org_member_set ${org}/${user}` });
|
|
23878
|
+
return { ok: true, status: 200, data: { org, user, role: input.role } };
|
|
23879
|
+
}
|
|
23880
|
+
},
|
|
23881
|
+
// ───────────────────────────────────────────────────────
|
|
23882
|
+
// npm_org_member_remove
|
|
23883
|
+
// ───────────────────────────────────────────────────────
|
|
23884
|
+
{
|
|
23885
|
+
name: "npm_org_member_remove",
|
|
23886
|
+
description: "Remove a user from an org. Their team memberships in that org are also removed.",
|
|
23887
|
+
annotations: {
|
|
23888
|
+
title: "Remove org member",
|
|
23889
|
+
readOnlyHint: false,
|
|
23890
|
+
destructiveHint: true,
|
|
23891
|
+
idempotentHint: true,
|
|
23892
|
+
openWorldHint: true
|
|
23893
|
+
},
|
|
23894
|
+
inputSchema: external_exports.object({
|
|
23895
|
+
org: external_exports.string().describe("Organization name"),
|
|
23896
|
+
user: external_exports.string().describe("npm username")
|
|
23897
|
+
}),
|
|
23898
|
+
handler: async (input) => {
|
|
23899
|
+
const authErr = requireAuth();
|
|
23900
|
+
if (authErr) return authErr;
|
|
23901
|
+
const org = input.org.replace(/^@/, "");
|
|
23902
|
+
const user = input.user.replace(/^@/, "");
|
|
23903
|
+
const res = await registryDeleteAuth(`/-/org/${encodeURIComponent(org)}/user`, { user });
|
|
23904
|
+
if (!res.ok) return translateError(res, { op: `org_member_remove ${org}/${user}` });
|
|
23905
|
+
return { ok: true, status: 200, data: { org, removedUser: user } };
|
|
23906
|
+
}
|
|
23907
|
+
},
|
|
23908
|
+
// ───────────────────────────────────────────────────────
|
|
23909
|
+
// npm_token_revoke
|
|
23910
|
+
// ───────────────────────────────────────────────────────
|
|
23911
|
+
// Note: token CREATION requires a user password (not a token), so it cannot be
|
|
23912
|
+
// performed via NPM_TOKEN alone — we intentionally don't expose npm_token_create.
|
|
23913
|
+
{
|
|
23914
|
+
name: "npm_token_revoke",
|
|
23915
|
+
description: "Revoke an access token by its key (UUID from npm_tokens). Creating tokens is NOT exposed because the endpoint requires the user password \u2014 create via https://www.npmjs.com/settings/~/tokens instead.",
|
|
23916
|
+
annotations: {
|
|
23917
|
+
title: "Revoke access token",
|
|
23918
|
+
readOnlyHint: false,
|
|
23919
|
+
destructiveHint: true,
|
|
23920
|
+
idempotentHint: true,
|
|
23921
|
+
openWorldHint: true
|
|
23922
|
+
},
|
|
23923
|
+
inputSchema: external_exports.object({
|
|
23924
|
+
tokenKey: external_exports.string().describe("Token key (UUID shown by npm_tokens)")
|
|
23925
|
+
}),
|
|
23926
|
+
handler: async (input) => {
|
|
23927
|
+
const authErr = requireAuth();
|
|
23928
|
+
if (authErr) return authErr;
|
|
23929
|
+
const res = await registryDeleteAuth(`/-/npm/v1/tokens/token/${encodeURIComponent(input.tokenKey)}`);
|
|
23930
|
+
if (!res.ok) return translateError(res, { op: "token_revoke" });
|
|
23931
|
+
return { ok: true, status: 200, data: { tokenKey: input.tokenKey, revoked: true } };
|
|
23932
|
+
}
|
|
23933
|
+
}
|
|
23934
|
+
];
|
|
23935
|
+
|
|
22798
23936
|
// src/index.ts
|
|
22799
|
-
var version2 = true ? "0.
|
|
23937
|
+
var version2 = true ? "0.7.0" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
22800
23938
|
var subcommand = process.argv[2];
|
|
22801
23939
|
if (subcommand === "version" || subcommand === "--version") {
|
|
22802
23940
|
console.log(version2);
|
|
@@ -22815,7 +23953,9 @@ var allTools = [
|
|
|
22815
23953
|
...accessTools,
|
|
22816
23954
|
...provenanceTools,
|
|
22817
23955
|
...trustTools,
|
|
22818
|
-
...workflowTools
|
|
23956
|
+
...workflowTools,
|
|
23957
|
+
...writeTools,
|
|
23958
|
+
...hookTools
|
|
22819
23959
|
];
|
|
22820
23960
|
var server = new McpServer({
|
|
22821
23961
|
name: "@yawlabs/npmjs-mcp",
|
package/package.json
CHANGED