Supply Chain · npm Security

MCP server npm publish security

Publishing an MCP server to npm gives every developer who installs it a copy of your code. It also creates a high-value target: whoever can publish a new version under your package name can inject code into every system that auto-updates your package. npm account takeover is the most direct path to a supply chain attack on an MCP server with an active user base.

How npm account takeover enables malicious version injection

The attack chain is straightforward: phish or credential-stuff a maintainer's npm account → log in → npm publish a new patch version with injected malicious code → every consumer with ^ or ~ version ranges pulls the compromised version on next install. The attack is especially effective against MCP servers because:

The ua-parser-js incident (2021) saw a maintainer's account phished and three versions published with malicious postinstall scripts that installed cryptominer and password-stealing software. The package had 7 million weekly downloads. Compromised MCP servers with similar reach could exfiltrate API keys from every agent that installs them.

Defense 1: Enable 2FA on your npm account

npm supports two 2FA modes: auth-only (login requires 2FA) and auth-and-writes (login AND publish/unpublish require 2FA). For MCP server maintainers, use auth-and-writes — it means even an attacker with your stolen password cannot publish a new version without your TOTP code:

# Enable 2FA for auth and write operations
npm profile enable-2fa auth-and-writes

# Verify 2FA is enabled
npm profile get 2fa

npm now also supports passkeys as a 2FA method, which are phishing-resistant (unlike TOTP codes, which can be stolen by real-time phishing proxies). Prefer a passkey or hardware security key over TOTP for highest protection.

Defense 2: Use granular automation tokens for CI

Many MCP server maintainers store a legacy npm token in their CI environment to enable automated publishing. Legacy tokens have full account permissions — they can publish, unpublish, and change account settings. A leaked CI token (from a public repository, a logging misconfiguration, or a compromised CI environment) gives an attacker full npm account control.

Replace legacy tokens with granular automation tokens scoped to specific packages and operations:

# Create a granular token via npm website:
# npmjs.com → Account Settings → Access Tokens → Generate New Token → Granular Access Token
# Settings:
#   - Packages: select only YOUR_PACKAGE_NAME
#   - Permissions: read and write (publish only)
#   - No org permissions
#   - Expiration: 90 days (rotate on each release)

# Use in GitHub Actions as a secret
# In .github/workflows/release.yml:
- name: Publish to npm
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
  run: npm publish --access public

Granular tokens limit blast radius: a leaked token can only publish to the specific package it was scoped to, and cannot modify account settings or access other packages.

Defense 3: Enable npm provenance attestation

npm provenance (available since npm 9.5, supported on GitHub Actions, GitLab CI, and other OIDC-supporting CI providers) links your published package to the specific CI run that built it, via a Sigstore signature. Consumers can verify that the version they install was built by your CI pipeline, not by someone who gained access to your npm token:

# In GitHub Actions, add --provenance flag to npm publish
- name: Publish to npm with provenance
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
  run: npm publish --provenance --access public

# The published package will show attestation data
# Consumers can verify with:
npm info YOUR_PACKAGE_NAME dist.attestations

When provenance is enabled, the npm registry page for your package displays a "Provenance" section linking to the exact GitHub Actions workflow run, commit SHA, and repository that published the version. This provides consumers with tamper-evident evidence of build provenance.

Defense 4: Configure package access settings

# Restrict who can publish (team accounts)
npm access set status=public @your-org/your-mcp-server
npm access set mfa=require @your-org/your-mcp-server

# List all maintainers — remove any you don't recognize
npm owner ls your-mcp-server

# Remove a maintainer
npm owner rm suspicious-username your-mcp-server

Defense 5: Monitor your package for unexpected new versions

Set up monitoring to alert you when a new version of your package is published — so you know immediately if someone publishes a version you didn't authorize:

# Simple GitHub Actions scheduled check
name: Monitor npm package versions
on:
  schedule:
    - cron: '0 */6 * * *'  # every 6 hours

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check latest version
        run: |
          LATEST=$(npm info your-mcp-server dist-tags.latest)
          EXPECTED="1.2.3"  # update this on each release
          if [ "$LATEST" != "$EXPECTED" ]; then
            echo "ALERT: Unexpected version on npm: $LATEST (expected $EXPECTED)"
            exit 1
          fi

What consumers can do

As a consumer of MCP servers published to npm, you can reduce your exposure by:

SkillAudit findings

CRITICAL npm account does not require 2FA for write operations — account takeover enables direct malicious version injection
HIGH CI pipeline uses a legacy npm token (full account permissions) rather than a granular token scoped to the package
HIGH No provenance attestation on published versions — consumers cannot verify CI built the package
MEDIUM Multiple maintainers with publish access, some with no recent activity — stale accounts increase takeover risk

Run a SkillAudit scan on your published MCP server to check for provenance attestation, 2FA status (via npm API), and token scope signals. SkillAudit's supply chain axis also checks for lockfile presence and npm audit findings. See also: MCP server supply chain risk deep-dive and MCP server SBOM security.