Security · Supply Chain
MCP server dependency security
An MCP server's security posture is only as strong as its weakest npm dependency. Supply chain attacks — where a legitimate npm package is compromised and updated to include malicious code — have become a primary attack vector for server-side Node.js applications. An MCP server is a particularly attractive target because it runs with file system access, network access, and often database access, and because MCP servers are frequently deployed with elevated permissions to serve AI agents that act on behalf of users. This guide covers the practical steps for dependency hygiene: lockfile verification, CVE scanning in CI, version pinning strategies, automated update PRs, and the incident response procedure when a dependency in your tree is compromised.
TL;DR
Commit package-lock.json and use npm ci (not npm install) in production deployments — this verifies the lockfile against the registry and installs exactly what's locked. Run npm audit --audit-level=high in CI and fail the build on high or critical CVEs. Enable Dependabot or Renovate for automated dependency update PRs. Pin the Node.js version in package.json engines and in your Dockerfile to prevent silent runtime upgrades. Use npm pack --dry-run before publishing if your MCP server is itself distributed as a package. Monitor production availability with AliveMCP — a compromised dependency that causes crashes will surface in the uptime dashboard within 60 seconds.
The lockfile is your supply chain anchor
package-lock.json records the exact version and integrity hash (SHA-512) of every package installed, including transitive dependencies. When you run npm ci, npm verifies each downloaded package against the hash in the lockfile. If any package's content has changed since you last updated the lockfile — even if the version number is the same — npm ci fails. This is your primary defense against the "package content changed without version bump" attack vector used in several high-profile supply chain attacks.
# Production deployment commands
# WRONG: npm install recalculates and updates the lockfile based on semver ranges
npm install # may silently upgrade packages within semver range
# RIGHT: npm ci verifies exact lockfile, fails if lockfile is out of sync
npm ci # installs exact versions from lockfile, no resolution
# In Dockerfile: always use npm ci, never npm install
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev # install production deps only, verify lockfile
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
Commit the lockfile to version control. Never .gitignore package-lock.json. A missing lockfile forces npm install at deploy time, which resolves dependencies from scratch and may install different (potentially compromised) versions than what you tested locally.
CVE scanning in CI
Run npm audit as a required step in your CI pipeline. Configure it to fail the build on high and critical severity vulnerabilities, while allowing moderate and low issues to be tracked and fixed on a schedule:
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
# Fail on high/critical CVEs, warn on moderate/low
- name: npm audit
run: npm audit --audit-level=high
# Also run a more comprehensive scan with Socket Security or Snyk
# for supply chain analysis beyond CVE databases
- name: Socket Security scan
run: npx @socket/cli ci --strict # fails on new dependency risks
npm audit checks against the npm advisory database, which covers known CVEs in packages. It doesn't detect new supply chain attacks (malicious code added to a package without a CVE yet). Tools like Socket Security (socket.dev) and Snyk add supply chain analysis — they detect suspicious package behavior like new network calls, new binary installs, or obfuscated code in a package update.
Version pinning strategies
The tradeoff in version pinning is between security (pinned versions don't automatically pull in a compromised update) and maintenance burden (pinned versions require manual updates). Choose a strategy based on your deployment model:
// package.json version pinning approaches:
// Approach 1: Semver ranges (default) — updates automatically within minor/patch
// Risk: a compromised patch release (e.g., 1.2.4 -> 1.2.5 with malicious code)
// ships automatically on next `npm install`
{
"dependencies": {
"express": "^4.21.0",
"@modelcontextprotocol/sdk": "^1.5.0"
}
}
// Approach 2: Exact pinning — no automatic updates
// Risk: you must manually update to get security fixes
// Mitigated by: Dependabot auto-PRs that update the locked version
{
"dependencies": {
"express": "4.21.2",
"@modelcontextprotocol/sdk": "1.12.0"
}
}
// Approach 3: Use the lockfile for production, ranges for development
// This is the recommended approach: commit package-lock.json and use `npm ci`
// The lockfile pins exact versions; ranges in package.json guide `npm update`
// when you intentionally want to update
{
"dependencies": {
"express": "^4.21.0" // used by `npm update` to resolve new lock entries
}
}
// Then: npm ci in production (uses lockfile), npm update in dev (respects ranges)
The lockfile-as-pin approach is recommended: ranges in package.json express intent, the lockfile captures the actual resolved version, and npm ci enforces the lockfile in production. Automated dependency updates (Dependabot) create PRs that update both the range and the lockfile, so you review updates before they reach production.
Automated dependency updates with Dependabot
Dependabot scans your package.json and package-lock.json weekly and creates pull requests when newer patch or minor versions are available. Configure it to keep your dependencies current without requiring manual intervention:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
time: "09:00"
open-pull-requests-limit: 10
groups:
# Group all MCP SDK updates into a single PR
mcp-sdk:
patterns: ["@modelcontextprotocol/*"]
# Group minor TypeScript toolchain updates
dev-tools:
patterns: ["typescript", "ts-node", "@types/*"]
update-types: ["minor", "patch"]
ignore:
# Pin major version upgrades to manual review
- dependency-name: "*"
update-types: ["version-update:semver-major"]
Dependabot PRs include the changelog for the updated package and the npm audit result after updating. Review these PRs promptly — a dependency update PR that sits open for 30 days is a window where your production server runs a version that Dependabot flagged as having a newer, potentially more secure release.
Responding to a compromised dependency
When a dependency in your tree is discovered to have been compromised (malicious code published to a new version, or an existing version retroactively flagged), the incident response procedure is:
- Assess exposure — run
npm ls <package-name>to confirm the compromised version is in your tree. Check whether the compromised version was deployed to production by comparing your deployment lockfile against the advisory. - Update immediately — if a clean version is available (
npm audit fixor a manual version bump +npm install), update the lockfile and redeploy. If no clean version is available, remove or replace the dependency. - Assess blast radius — what data and credentials did the compromised package have access to? MCP servers run with broad permissions. Rotate any secrets that were accessible in the environment at the time the compromised version was running (
process.env, files in the working directory, mounted secrets). - Audit logs — review your MCP audit log for unusual tool call patterns during the period the compromised version was running. Network-exfiltrating malware typically calls external endpoints; check your firewall egress logs too.
- Notify if required — if your MCP server handles user data, a compromised dependency that ran in production may trigger breach notification obligations under GDPR, CCPA, or similar regulations.
# Incident response commands
# 1. Identify which version you have deployed
npm ls compromised-package
# 2. Check the advisory
npm audit | grep compromised-package
# 3. Update to a clean version
npm update compromised-package
npm audit fix # for transitive dependencies
npm ci # verify the updated lockfile
# 4. Rotate secrets — list env vars that were potentially exposed
printenv | grep -E '(KEY|TOKEN|SECRET|PASSWORD|API)' | cut -d= -f1
# Rotate all of these in your secret store
Related questions
Should I use npm, yarn, or pnpm for MCP server projects?
All three have equivalent supply chain security features (lockfiles, integrity hashes, audit commands). npm is the default and requires no additional tooling. pnpm's content-addressed store and strict package isolation (packages can only import what they explicitly declare in package.json) provide a marginally stronger supply chain guarantee — a package that imports another package it didn't declare gets an error rather than silently resolving it from the shared node_modules. For MCP servers, npm with a committed lockfile and npm ci in production is sufficient. If you already use pnpm or yarn across your organization, use those consistently.
What's a software bill of materials (SBOM) and do I need one?
An SBOM is a machine-readable list of every package in your application's dependency tree, including version and license. It's required by some enterprise customers and government contracts (US Executive Order 14028). Generate one with npm sbom --sbom-format cyclonedx (npm 7+) or cyclonedx-npm --output-file sbom.json. Store the SBOM as a release artifact alongside each deployed build. When a new CVE is published, you can cross-reference against stored SBOMs to identify which deployed versions are affected without waiting for a full audit scan to run.
How do I know if a newly added dependency is trustworthy?
Before adding a new npm package: check its weekly download count and GitHub star count (high traffic packages are more scrutinized), check the last published date (abandoned packages don't receive security patches), look at the maintainers list (packages that recently changed maintainer are higher risk — a common acquisition-attack vector), and review the package's postinstall script in its package.json (postinstall scripts run arbitrary code during npm install). Tools like Socket Security's browser extension add a safety score to npm package pages. Prefer packages with more than 1 million weekly downloads, active maintenance, and no postinstall scripts for critical production dependencies.
Further reading
- MCP server secrets management — rotating credentials after a compromise
- MCP server CI/CD — build pipeline security gates and deploy verification
- MCP server Docker — minimal images and non-root process execution
- MCP server audit logging — detecting anomalous behavior post-compromise
- MCP server environment variables — securing runtime configuration
- AliveMCP — uptime monitoring for MCP servers: compromised dependency crashes surface in 60 seconds