Topic: version pinning and supply chain security
MCP server version pinning security — lockfile integrity, automated security updates, and supply chain trust
A missing package-lock.json is one of the most common supply chain findings in the SkillAudit corpus. Without a committed lockfile, every npm install resolves to whatever version each dependency published to npm most recently — and a malicious dependency maintainer, a compromised package account, or an honest patch that introduces a breaking vulnerability can silently change what code runs in your MCP server. Five version pinning patterns close this attack surface and ensure reproducible, verifiable builds.
1. Lockfile discipline: commit, use ci, never regenerate casually
The lockfile is the single source of truth for what dependency versions will be installed. It records every package in the transitive dependency graph — not just direct dependencies — pinned to exact resolved versions with cryptographic integrity hashes.
## .gitignore — make sure lockfile is NOT ignored
## (A common mistake is .gitignore'ing package-lock.json)
# WRONG — this ignores the lockfile:
# package-lock.json
# CORRECT — only ignore generated output, not lockfiles:
node_modules/
dist/
build/
## Verify your lockfile is committed:
git ls-files package-lock.json # should print the path
## Use npm ci in all automated contexts:
## Dockerfile
RUN npm ci --production # exact lockfile install, no lockfile update
## CI
- name: Install dependencies
run: npm ci
## NEVER:
# npm install (in CI or Dockerfile — can update lockfile)
# npm install --package-lock-only (recreates lockfile from scratch)
SkillAudit checks that a lockfile is present in the repository root and that the most recent commits include lockfile updates correlated with dependency changes. A package.json update that adds a new dependency without a corresponding lockfile update is flagged as a supply chain gap finding.
2. Exact version specifiers vs ranges
npm version ranges — ^ (allow minor and patch), ~ (allow patch only), or * (allow any) — are convenient for library authors but introduce risk in deployed applications. The lockfile mitigates most of this risk, but exact version specifiers provide an additional defense when the lockfile is regenerated.
// package.json — comparison
// Range specifiers (common but riskier for MCP servers):
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.2.0", // allows 1.x.x updates
"zod": "^3.22.0", // allows 3.x.x updates
"axios": "^1.6.0" // allows 1.x.x updates
}
}
// Exact pins (recommended for security-sensitive deployments):
{
"dependencies": {
"@modelcontextprotocol/sdk": "1.2.4", // exact — requires explicit update
"zod": "3.22.4",
"axios": "1.6.8"
}
}
// To convert all current installs to exact versions:
npm shrinkwrap # creates npm-shrinkwrap.json with exact versions
// Or use npm-exact to pin all current versions:
npx npm-exact
3. Renovate bot for automated security updates
Exact version pinning solves the unpinned-install problem but creates a new problem: dependencies that don't update stay pinned to vulnerable versions forever. Renovate bot automates dependency updates with configurable auto-merge rules — combining the determinism of exact pinning with the currency of automatic updates.
// renovate.json — security-focused configuration for MCP servers
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
// Auto-merge patch updates that pass CI — no manual review needed
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "pr"
},
{
// Prioritize vulnerability fixes: merge within 3 days
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["patch", "minor"],
"schedule": ["at any time"],
"prPriority": 10
},
{
// Security advisories: even higher priority
"vulnerabilityAlerts": {
"labels": ["security"],
"automerge": true,
"schedule": ["at any time"]
}
}
],
// Run npm audit on every PR
"postUpgradeTasks": {
"commands": ["npm audit --audit-level=high"],
"fileFilters": ["package-lock.json"]
}
}
The key Renovate configuration for security: enable vulnerabilityAlerts auto-merge. When a CVE is published for a dependency in your lockfile, Renovate opens a PR within hours, and if CI passes, it auto-merges without requiring human review. This closes the gap between "CVE published" and "dependency updated" from days-to-weeks to hours.
4. npm audit enforcement in CI
npm's built-in audit command checks your lockfile against the npm security advisory database. Running it in CI with a failure threshold ensures that any newly-published advisory for a dependency in your lockfile fails the build, giving you immediate notification and blocking the release pipeline until the dependency is updated.
## .github/workflows/security.yml
name: Security Audit
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 8 * * 1' # Weekly Monday morning run
jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install (lockfile enforced)
run: npm ci
- name: Audit dependencies
run: npm audit --audit-level=high
# Fails build on HIGH or CRITICAL advisories
- name: Check lockfile consistency
run: |
# Verify package-lock.json matches package.json
npm ci --dry-run 2>&1 | grep -v "^npm notice" | head -5
git diff --exit-code package-lock.json # Fail if lockfile would change
5. Package provenance and integrity verification
npm 9+ includes a package provenance feature: packages published with provenance include a signed attestation linking the published tarball to a specific commit in a specific GitHub Actions run. Checking provenance before install verifies that the package you're installing is the one the author intended to publish, not a tampered copy.
## Check provenance for a specific package:
npm audit signatures
## Output example:
# audited 1 package in 0.5s
# 1 package has a verified registry signature
# 1 package has a verified attestation: @modelcontextprotocol/sdk@1.2.4
# Build: https://github.com/anthropics/mcp-sdk/actions/runs/12345678
## In .npmrc — require verified signatures for all installs:
## (npm 9+)
audit-level=high
fund=false
## For publishing your own MCP server with provenance:
## In .github/workflows/publish.yml:
- name: Publish to npm
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
id-token: write # Required for OIDC provenance
Publishing your MCP server with provenance gives your users the ability to verify that the npm package they install came from a specific, auditable CI run — not from a compromised maintainer account or a supply chain attack on the publish step. This is particularly valuable for MCP servers distributed through the Anthropic Skills Directory, where buyers may do provenance spot-checks on high-risk servers.
What SkillAudit checks for version pinning
Version pinning is evaluated as part of the Maintenance sub-score. Key checks:
- Lockfile present:
package-lock.json,yarn.lock, orpnpm-lock.yamlcommitted to the repo — INFO signal (absence is a MEDIUM finding) npm ciin CI: GitHub Actions workflow usesnpm cinotnpm install— positive signal- npm audit in CI: Workflow includes
npm auditwith an--audit-levelflag — positive signal - Renovate or Dependabot configured: Presence of
renovate.jsonor.github/dependabot.yml— positive signal; absence with stale dependencies is a MEDIUM finding - Range specifiers on direct dependencies:
^or*specifiers with no lockfile — HIGH finding; with lockfile — WARN - Outstanding HIGH/CRITICAL advisories: Running npm audit against the committed lockfile reveals existing vulnerabilities — HIGH or CRITICAL finding directly
See the dependency pinning deep-dive for a more detailed treatment of transitive dependency security. For the supply chain trust boundary analysis that version pinning fits into, see the supply chain trust boundaries post.
Check your dependency supply chain
SkillAudit's Maintenance scan checks lockfile presence, audit advisory status, and Renovate/Dependabot configuration. See the full breakdown.
Run a free audit