Skip to content

Agent Setup (MCP Server)

@pkgplease/agent ships a single binary, pkgpls. It does three things:

  1. Bootstraps a repo (pkgpls agent init) so every npm project in it installs through https://npm.pkgpls.com, and so a coding agent harness started in that repo already has the MCP server configured.
  2. 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 .npmrc authenticated for as long as the session lives.
  3. 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.

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.

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.

Terminal window
pkgpls login # prompts for the SVC/developer token
pkgpls login --token pkgpls_dev_…
# or just export PKGPLEASE_SVC_TOKEN in your shell — either works

An SVC token comes from Settings → Service Tokens; a developer token from Settings or the Slack /issue-token command — either is valid here.

Terminal window
npx -y -p @pkgplease/agent pkgpls token create --type provisioner

This 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:

Terminal window
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.

Terminal window
cd your-project
npx -y -p @pkgplease/agent pkgpls agent init

Safe 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 marked registry= block to each one’s .npmrc.
  • Writes one project-level .mcp.json at 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.json if its entries still point at another registry host, without touching it — see --package-lock-only below.

Run it again any time; it’s idempotent — a second run changes nothing.

Terminal window
pkgpls agent init --package-lock-only

A 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.

Terminal window
npx -y @pkgplease/agent agent mcp start

That 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.

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.

# <!-- 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.com

That’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.

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.

There’s no long-lived MCP server in a build step, so use the daemon:

Terminal window
pkgpls agent daemon --once # exchange once at boot, then exit
pkgpls agent daemon # exchange, then stay alive keeping .npmrc current

daemon 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.

Terminal window
PKGPLEASE_PROVISIONER_FILE=/run/secrets/pkgpls pkgpls agent daemon --once

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.

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:

Terminal window
AGENT_REPO_PATH=/path/to/pkgplease/agent pnpm check:agent-env-vars

from 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.

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.

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.

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.

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.

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.

  • A provisioner is only ever printed by pkgpls token create, and that command refuses to run outside a real interactive terminal — the one place a pkgpls_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 .npmrc and .mcp.json hold 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 stays 0600 and is re-chmod’d after every write.
  • ~/.npmrc’s credential half is never printed either. A pkgpls_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_expires exists for platforms that genuinely cannot refresh, and gives up the main property that makes an agent token safe to hand out.