Agent Setup (MCP Server)
@pkgplease/agent ships a single binary, pkgpls. It does three things:
- Bootstraps a repo (
pkgpls agent init) so every npm project in it installs throughhttps://npm.pkgpls.com, and so a coding agent harness started in that repo already has the MCP server configured. - Runs the MCP server (
pkgpls agent mcp start) that a harness launches automatically — it exchanges your provisioner for a short-lived agent token and keeps.npmrcauthenticated for as long as the session lives. - Exposes four read-and-request tools so an agent can ask why a package is blocked and raise an approval instead of thrashing.
The MCP server is a cooperative fast path, not the enforcement mechanism. The registry gate described in Registry & Tokens holds whether or not an agent ever calls a tool. Running this server makes a blocked install explainable and recoverable; it does not make anything more or less permitted.
The credential model
Section titled “The credential model”Three tiers, and a hard boundary after the first one: nothing under
pkgpls agent — the namespace everything agent-facing lives in — ever
touches the SVC tier. pkgpls agent commands take a provisioner as input;
the agent token below it is minted and managed entirely by the MCP server
itself.
| Tier | Prefix | Where it lives | Who creates it |
|---|---|---|---|
| SVC | pkgpls_svc_… (or a developer token) |
your shell’s persistent environment, or ~/.config/pkgpls/credentials via pkgpls login |
you |
| Provisioner | pkgpls_prv_… |
your shell’s persistent environment, as PKGPLEASE_PRV_TOKEN |
pkgpls token create --type provisioner |
| Agent token | pkgpls_agt_… |
.npmrc’s _authToken line, and in the server’s memory |
minted per session by the MCP server, from the provisioner |
SVC token. Human-held, and its only job in this whole flow is to gate one
command: pkgpls token create. It is never read by anything under
pkgpls agent, never reachable from an agent’s tool surface, and never
appears in a transcript — the one place a provisioner is ever printed is a
command that is, by definition, run by a human in their own terminal.
Provisioner. The credential an agent is actually allowed to hold. Once
exported into your shell profile, it’s present in every terminal, every
session, every agent sandbox that inherits your environment — on this
machine, for every pkgPlease-enabled repo, forever, until you rotate it. This
is what makes pkgpls agent init a once-per-repo action instead of something
you re-run every time you open a new session.
Agent token. Minted at server startup by exchanging the provisioner. Default TTL is 1 hour, and the API caps any requested TTL at 24 hours. The server re-mints at 80% of the token’s lifetime — the same fraction, and for the same reason, that kubelet uses for projected service account tokens: it leaves room for retries before anything actually expires. A failed background refresh is not fatal; the current token is still good for the remaining 20% and the next call retries.
Quick path
Section titled “Quick path”1. Get a provisioner (once, ever, per machine)
Section titled “1. Get a provisioner (once, ever, per machine)”Without a global install, run pkgpls as
npx -y -p @pkgplease/agent pkgpls <command> — the guide writes just
pkgpls <command> from here on.
pkgpls login # prompts for the SVC/developer tokenpkgpls login --token pkgpls_dev_…# or just export PKGPLEASE_SVC_TOKEN in your shell — either worksAn SVC token comes from Settings → Service Tokens; a developer token
from Settings or the Slack /issue-token command — either is valid
here.
npx -y -p @pkgplease/agent pkgpls token create --type provisionerThis is the one command that prints a provisioner. Add the line it gives you
to your shell profile (~/.zshrc or similar) and restart your shell:
export PKGPLEASE_PRV_TOKEN=pkgpls_prv_…token create refuses to run unless both stdin and stderr are a real TTY —
so it won’t accidentally run (and print a secret) from a non-interactive
shell an agent inherited.
2. Configure a repo
Section titled “2. Configure a repo”cd your-projectnpx -y -p @pkgplease/agent pkgpls agent initSafe for an agent to run itself — it reads only PKGPLEASE_PRV_TOKEN, never
mints anything, never touches your SVC-tier credential. In one pass it:
- Walks the repo for every npm project — any directory with its own
package.json, not just an npm-workspaces root — and writes a markedregistry=block to each one’s.npmrc. - Writes one project-level
.mcp.jsonat the directory you ran it from, templated as"PKGPLEASE_PRV_TOKEN": "${PKGPLEASE_PRV_TOKEN}"— never a raw token on disk. - Exchanges your provisioner and verifies the registry is actually live for every project root it configured, rather than trusting that the files it wrote are correct. A 401 here means the provisioner itself was rejected; anything else means the registry didn’t resolve where expected.
- Flags a stale
package-lock.jsonif its entries still point at another registry host, without touching it — see--package-lock-onlybelow.
Run it again any time; it’s idempotent — a second run changes nothing.
pkgpls agent init --package-lock-onlyA separate, explicit mode: skips .npmrc/.mcp.json writing entirely, and
instead rewrites each stale lockfile’s foreign-registry tarball URLs to the
configured registry, then runs npm install so the lockfile actually
re-resolves through pkgPlease.
3. The MCP server itself
Section titled “3. The MCP server itself”npx -y @pkgplease/agent agent mcp startThat is the exact command pkgpls agent init writes into .mcp.json — it
starts the MCP server on stdio, so it will appear to hang when you run it by
hand. That is correct; you do not normally run it yourself, the harness
does.
The pkgpls CLI
Section titled “The pkgpls CLI”Commands split into two groups by what credential they’re allowed to touch — the split is the enforcement mechanism, not a convention.
Human/SVC-tier — never reachable from an agent:
| Command | What it does |
|---|---|
pkgpls login [--token <t>] |
Validates an SVC/developer credential against the API, then stores it at ~/.config/pkgpls/credentials (mode 0600). Prompts with echo disabled when --token is omitted; requires a TTY to prompt. |
pkgpls token create --type provisioner |
Mints a provisioner using the resolved SVC credential and prints it once, with the exact export line to add to a shell profile. Refuses to run unless both stdin and stderr are a real TTY. |
Agent-tier — provably PKGPLEASE_PRV_TOKEN-only:
| Command | What it does |
|---|---|
pkgpls agent init [--package-lock-only] |
Configures every npm project in the current directory tree: .npmrc, one project-level .mcp.json, and live registry verification. Requires PKGPLEASE_PRV_TOKEN. Idempotent. |
pkgpls agent mcp start |
Starts the MCP server over stdio. Requires PKGPLEASE_PRV_TOKEN. |
pkgpls agent daemon [--once] |
Exchanges at boot and keeps .npmrc current for sandboxes with no long-lived MCP server. Requires PKGPLEASE_PRV_TOKEN. |
Unchanged, outside the namespace split:
| Command | What it does |
|---|---|
pkgpls ensure-credential |
Ensures .npmrc holds a currently-valid agent token, then exits. Cheap enough to run on every session start: it seeds from the on-disk token cache and makes no network call unless the token is past 80% of its TTL, malformed, or minted for a different registry. |
pkgpls conformance-setup <harness-id> --token <t> |
Test scaffolding: writes only the .npmrc registry= line and one named harness’s MCP block — never a credential, never a hook — to reproduce the fresh-session starting condition. |
pkgpls conformance-check |
Polls .npmrc until both halves are present and uncommented, or a timeout elapses. Flags: --npmrc <path>, --registry <url>, --timeout-ms (default 300000), --interval-ms (default 2000). |
Every command under pkgpls agent shares one credential-resolution
function: read PKGPLEASE_PRV_TOKEN, require the pkgpls_prv_ prefix, fail
loudly otherwise — one input, one prefix, one error message, for anything an
agent can run. Running bare npx -y @pkgplease/agent with no subcommand
prints usage rather than starting anything.
The .npmrc block
Section titled “The .npmrc block”# <!-- pkgplease:npmrc-instructions --># This project installs npm packages through the pkgPlease registry.# Agents/CI: set PKGPLEASE_PRV_TOKEN in your shell environment once# (see https://docs.pkgplease.com/guides/agent-mcp-setup/).# Humans: run `pkgpls token create --type provisioner` once, export the# result, then `pkgpls agent init` in each project.# The line below is managed by pkgpls — don't hand-edit it.registry=https://npm.pkgpls.comThat’s what pkgpls agent init writes to every npm project’s own .npmrc —
project-level, secret-free, safe to commit. Re-running init replaces the
whole marked block by its comment markers, not just the registry= line, so
it stays idempotent across releases. A bare pre-existing registry= line
from before this format is migrated into the block in place.
This project file is deliberately separate from the credential half of
.npmrc, written by the running MCP server into ~/.npmrc:
//npm.pkgpls.com/:_authToken=pkgpls_agt_…The server owns exactly that one line: every other line in your ~/.npmrc
is left untouched and in order, and the file is replaced atomically via a
temp file and a rename, because npm may read it at any moment and a torn
.npmrc is an auth failure you cannot diagnose. Refresh rewrites this file
rather than an environment variable, because an env var is fixed at process
spawn and no refresher can reach an already-running shell.
Project-level .mcp.json
Section titled “Project-level .mcp.json”pkgpls agent init writes one .mcp.json at the directory you ran it from
— not a global, per-user harness config — because the whole point is that a
repo only needs this run once, and the config travels with the repo:
{ "mcpServers": { "pkgplease": { "command": "npx", "args": ["-y", "@pkgplease/agent", "agent", "mcp", "start"], "env": { "PKGPLEASE_PRV_TOKEN": "${PKGPLEASE_PRV_TOKEN}" } } }}Templated, never a raw value — since the provisioner lives durably in your shell profile, there’s never a reason to also duplicate it as plaintext in a repo-local file.
This is currently written only for Claude Code, the one harness whose
project-level MCP config convention (.mcp.json, JSON, mcpServers key) has
actually been verified. Configuring any other harness today is manual — add
the block above to that harness’s own project- or user-level config file
yourself. A wrong guess at an unverified path would write a file nothing
reads, which is worse than writing nothing, so pkgpls agent init doesn’t
attempt one.
Sandboxes and CI
Section titled “Sandboxes and CI”There’s no long-lived MCP server in a build step, so use the daemon:
pkgpls agent daemon --once # exchange once at boot, then exitpkgpls agent daemon # exchange, then stay alive keeping .npmrc currentdaemon treats 401/403 as permanent — the credential itself is bad,
and retrying only delays the moment a human finds out — so it comments out
the _authToken line with a stated reason and exits 1. 5xx and network
errors are transient and get exponential backoff (1s doubling to a 60s
ceiling), because commenting out a working sandbox’s auth over a blip would
be worse than the blip.
Delivering the provisioner as a mounted secret
Section titled “Delivering the provisioner as a mounted secret”Set PKGPLEASE_PROVISIONER_FILE to a path holding the provisioner. daemon
reads it into PKGPLEASE_PRV_TOKEN and unlinks the file once the exchange
succeeds — from that point the durable credential lives only in the
daemon’s memory, so an agent process compromised later cannot exfiltrate it
off disk. This is Vault Agent’s behavior, and for its reason.
PKGPLEASE_PROVISIONER_FILE=/run/secrets/pkgpls pkgpls agent daemon --oncePlatforms that cannot refresh
Section titled “Platforms that cannot refresh”Some platforms take a credential once at configuration time and have no way
to update it; given a TTL, their token is dead before the sandbox is ever
used. For those, the provisioning API accepts never_expires: true and
issues an agent token with no expiry, marked long_lived in the token’s
metadata and in the audit event. Such tokens appear in
Settings → Agent Credentials → Agent Tokens as “Never · reduced
security”.
The trade-off is the whole point of an agent token: a credential that never expires cannot be rotated away from a leak, only revoked after one is noticed. Use it only where refresh is genuinely impossible, and prefer a short-lived token everywhere else.
Environment variables
Section titled “Environment variables”This table is a manual mirror of what pkgplease/agent — a separate repo —
actually reads in src/config.ts, src/credentials/manager.ts, and
src/pkgpls.ts. Nothing keeps the two in sync automatically, so whenever
pkgplease/agent cuts a release that touches those files, re-run:
AGENT_REPO_PATH=/path/to/pkgplease/agent pnpm check:agent-env-varsfrom this repo (AGENT_REPO_PATH defaults to ../agent, a sibling checkout
of this repo). It greps that repo’s source for PKGPLEASE_* reads and diffs
the result against this table’s `PKGPLEASE_...` rows, printing any var
that’s missing from the docs or any table row no longer read by the agent.
Update the table below to resolve any drift it reports.
| Variable | Default | Effect |
|---|---|---|
PKGPLEASE_PRV_TOKEN |
none (required for every pkgpls agent command) |
The minting credential. Must be prefixed pkgpls_prv_; anything else is a startup error. This is the only credential input anything agent-facing accepts. |
PKGPLEASE_SVC_TOKEN |
none | Read only by pkgpls token create, as a fallback when no stored operator credential exists from pkgpls login. Never read by anything under pkgpls agent. |
PKGPLEASE_API_URL |
https://api.pkgplease.com |
The pkgPlease API the tools and the token exchange call. |
PKGPLEASE_REGISTRY_URL |
https://npm.pkgpls.com |
The registry the _authToken line is keyed to, and the value pkgpls agent init writes as registry=. |
PKGPLEASE_MANAGE_NPMRC |
on | Set to 0, false, or no to stop the server writing .npmrc at all — for environments where something else already owns the file. |
PKGPLEASE_NPMRC_SCOPE |
global |
local scopes the credential to PKGPLEASE_NPMRC_PATH instead of ~/.npmrc, so multiple pkgPlease projects on one machine stop sharing a credential. A local scope with no path is treated as global rather than as an error. pkgpls agent init doesn’t set this for you — add both vars to .mcp.json’s own env block yourself if you want local scope. |
PKGPLEASE_NPMRC_PATH |
~/.npmrc |
The credential file under local scope. |
PKGPLEASE_RUNTIME |
unknown |
A free-form label stamped onto every minted token, so an operator can see in the audit log what asked for it. |
PKGPLEASE_PROVISIONER_FILE |
unset | Read by pkgpls agent daemon only: a file holding the provisioner, unlinked once spent. |
The four MCP tools
Section titled “The four MCP tools”Every tool authenticates with the same pkgpls_agt_ token as the registry
and calls the pkgPlease API over HTTP. None of them can decide anything.
The server reads policy and raises requests; approving, denying, endorsing,
and changing thresholds are all outside its reach by construction.
check_package(name, version)
Section titled “check_package(name, version)”Would this package be allowed under this org’s policy? Returns the
decision, the reason, and the signal breakdown — not a global score. If the
version you asked for is behind npm’s latest release, the response also
carries newer_version_available and a note, a hint that the version may
have come from stale training data rather than from a policy decision.
A refusal returned here contains decision, reason, the pack and
block_class that matched, and — where one applies — a remediation naming
a compliant version or an endorsed peer package to use instead. It will
never contain a way to bypass the decision. There is no override field,
no flag, no undocumented parameter that satisfies a block from this tool:
overriding is a human affordance, performed by a person on the
dashboard, never by an agent. If you are
looking for the field that unblocks an install, stop — it does not exist by
design, and an agent that hits a block can only raise a request through
request_approval.
suggest_allowed(cohort)
Section titled “suggest_allowed(cohort)”Given a cohort (a kind of package whose members substitute for one another,
such as logging or http-client), returns what this org lets you use for it
in two tiers. endorsed is the standard the org has declared, with the
version range it covers and the reason it gave. permitted lists other
reviewed members its policy currently allows. A member the org would block,
ask about, or steer away from is never listed; excluded counts them.
The tool’s cohort parameter is a closed list, loaded when the MCP server
starts, and each value carries a name and description. The full list is also
available from GET /api/v1/cohorts/npm. A slug that isn’t in the vocabulary
is rejected with the valid list.
request_approval(name, version, justification)
Section titled “request_approval(name, version, justification)”Raises a pending approval request carrying the agent’s own reasoning, which is shown to the human who reviews it in the Approvals queue. This never approves anything. Returns the approval id.
approval_status(id)
Section titled “approval_status(id)”Polls a previously raised approval so the agent can resume after an ask-and-park block.
No tool mints, refreshes, or returns a credential value. The agent finds its tooling already authenticated and never handles the credential at all, which keeps secrets out of the model’s context entirely.
Troubleshooting
Section titled “Troubleshooting”pkgpls agent init says “no npm project (no package.json) found under
this directory.”
It walked the directory tree from where you ran it and found nothing with
its own package.json (skipping node_modules, .git, dist, .turbo,
.next, vendor, and hidden directories). Run it from your repo’s root, or
check that this is actually an npm project.
A FAILED line after pkgpls agent init with a 401.
The provisioner itself was rejected during the exchange step — the run stops
there and never gets to per-project verification. Run
pkgpls token create --type provisioner again for a fresh one, export it,
and retry pkgpls agent init.
A FAILED line naming the wrong registry host.
Verification resolved a package, but not through npm.pkgpls.com — check
that PKGPLEASE_REGISTRY_URL isn’t set to something unexpected, and that
nothing else on the machine (an .npmrc outside the project, a proxy) is
overriding the registry for that project root.
A commented-out _authToken line with a # pkgPlease: reason.
Refresh failed permanently — the credential was rejected with a 401 or 403,
not a transient error — and the daemon or MCP server commented the line out
with the reason inline rather than leaving you an undiagnosable npm failure.
Mint a fresh provisioner and restart. The server never un-comments a line it
finds commented out — a later successful mint appends a fresh credential
line beside it — so the commented one and its reason stay as a record of
what happened.
PKGPLEASE_PRV_TOKEN has an unrecognized prefix (…).
Only pkgpls_prv_ is valid there. If you see this, double check for a
copy-paste mistake (quotes included in the export, trailing whitespace, the
wrong variable). An SVC or developer token cannot be used as an agent
credential — mint a provisioner with pkgpls token create --type provisioner instead.
pkgpls token create refuses to run.
It only runs in a real interactive terminal (stdin and stderr must both be a
TTY) — this is deliberate, so an agent that happened to inherit
PKGPLEASE_SVC_TOKEN from your shell can’t trigger it and get a provisioner
printed into its own transcript. Run it yourself, directly, in your own
terminal.
pnpm ignores ${VAR} in a project .npmrc.
pnpm does not expand ${VAR} syntax from a project-level .npmrc. This
doesn’t affect the MCP server, which writes a literal token into the
user-level file — but if you are hand-writing a credential line for a human
workflow, put it in ~/.npmrc or substitute the literal token. See
Getting Started.
Security notes
Section titled “Security notes”- A provisioner is only ever printed by
pkgpls token create, and that command refuses to run outside a real interactive terminal — the one place apkgpls_prv_value is allowed to appear in output, deliberately fenced off from anything an agent can trigger. - No tool exposes a credential to the model. None of the four MCP tools can mint, refresh, or return a token value. The agent finds its tooling already authenticated.
- Project
.npmrcand.mcp.jsonhold no secret, so they’re written with normal file permissions and are safe to commit — the actual provisioner only ever lives in your shell environment or~/.npmrc’s credential half, which stays0600and is re-chmod’d after every write. ~/.npmrc’s credential half is never printed either. Apkgpls_prv_value in a transcript is a logged, replayable secret; the exchange for a short-lived agent token happens entirely server-side.- Prefer expiring agent tokens.
never_expiresexists for platforms that genuinely cannot refresh, and gives up the main property that makes an agent token safe to hand out.