{
  "question": "How do I pin MCP server versions and review what changed before upgrading?",
  "slug": "how-to-pin-mcp-server-versions-and-review-changes-before-upgrading",
  "url": "https://www.rightshold.com/answers/how-to-pin-mcp-server-versions-and-review-changes-before-upgrading",
  "short_answer": "Run every local MCP server at an exact version, make each upgrade a deliberate change, and review the published package and its tool list, not just the changelog, before you switch. In practice: replace npx some-server or @latest with some-server@1.4.2 (uvx some-server@1.4.2 for Python, or a container image by digest); add a release-age delay so brand-new versions aren't installed the day they appear; when an update is offered, read the release notes, diff the two published versions with npm diff, run the new version in a sandbox and compare its tools/list with the one you approved; then change the pin. The changelog alone is not enough: in September 2025 the npm package postmark-mcp, which impersonated Postmark, shipped 15 working versions and then a 1.0.16 whose one added line BCC'd every email to the attacker. Nothing in a version number tells you that; a diff of the published code does.",
  "checklist": [
    "Pin exactly in your client config: an exact version in the args (\"some-server@1.4.2\", never @latest or a bare package name), uvx some-server@1.4.2, or a Docker image referenced by @sha256 digest. For servers you install as dependencies, commit the lockfile.",
    "Add a release-age delay: npm's min-release-age (days), pnpm's minimumReleaseAge (minutes; 1440, one day, by default in current pnpm), and uv's exclude-newer (accepts durations such as \"7 days\") refuse versions newer than the window, which gives the registry and researchers time to catch a malicious release.",
    "Let a bot open the upgrade, not your agent: Dependabot or Renovate turns a new version into a pull request you review. Dependabot applies a 3-day cooldown to version updates by default and lets you set your own with cooldown; security updates skip the cooldown.",
    "Read the release notes and the compare view between the two tags, and look for new tools, new network destinations, new environment variables or credentials, and new install scripts.",
    "Diff what is actually published: npm diff --diff=some-server@1.4.2 --diff=some-server@1.5.0 compares the two registry tarballs. The source repository can look clean while the published package differs, and the package is what runs.",
    "Check provenance: npm audit signatures verifies registry signatures and provenance attestations of installed packages; a package that suddenly loses provenance or changes publisher deserves a closer look.",
    "Diff the tool list before you switch: start the new version in a sandbox with no real credentials, fetch tools/list (the MCP Inspector can do this), and compare names, descriptions and schemas with the hash you stored at approval. A new or changed tool needs a fresh approval.",
    "Change the pin in one reviewed commit, keep the old version one line away for rollback, and log which version each environment runs."
  ],
  "sections": [
    {
      "heading": "What pinning does and doesn't cover",
      "points": [
        "Pinning protects you from a bad release only until you upgrade to it, so the review at upgrade time is the real control; the pin just makes sure there is an upgrade time.",
        "A remote (HTTP) MCP server has no version you can pin: its operator can change its code and tool definitions at any time. For remote servers, re-check the tool-definition hash on every tools/list and keep confirmations on for actions that write, send, pay or delete.",
        "Dependencies of the server change too. A lockfile or a container image by digest pins the whole tree; an exact version in npx args pins only the top-level package."
      ]
    },
    {
      "heading": "Why a delay window helps",
      "points": [
        "pnpm's documentation notes that in most cases malicious releases are discovered and removed from the registry within an hour. A delay of a day or more means you install a version other people have already run, not the one published an hour ago.",
        "Keep a way around the delay for urgent security fixes: npm's min-release-age-exclude, pnpm's minimumReleaseAgeExclude, and Dependabot's security updates, which ignore cooldown."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "This MCP server just released a new version. Has its tool list changed since it was recorded, and what exactly changed?",
    "mcp_call": {
      "tool": "check_tool_integrity",
      "arguments": {
        "server": "<the server's Rightshold slug or id>"
      }
    },
    "also": [
      "The answer is unchanged, changed (with a plain diff of tools added, removed, description changed or schema changed) or unreachable, which is never reported as unchanged. It works for servers registered on Rightshold.",
      "precall_check gives a one-read summary for any https MCP server: registry entry, live integrity probe, invisible-text scan of the definitions it serves now, security flags and tool-name collisions.",
      "The tool-definition fingerprint construction is published at /security/spec, so you can compute the same hash for a local server in your own CI."
    ],
    "scope": "Rightshold compares remote servers' tool definitions with recorded fingerprints. It does not diff npm or PyPI packages, review changelogs, or certify that a version is safe."
  },
  "sources": [
    {
      "title": "Koi Security: First malicious MCP in the wild: the postmark-mcp backdoor",
      "url": "https://www.koi.ai/blog/postmark-mcp-npm-malicious-backdoor-email-theft"
    },
    {
      "title": "Postmark: Information regarding the malicious postmark-mcp package",
      "url": "https://postmarkapp.com/blog/information-regarding-malicious-postmark-mcp-package/"
    },
    {
      "title": "npm docs: config (min-release-age)",
      "url": "https://docs.npmjs.com/cli/v11/using-npm/config"
    },
    {
      "title": "npm docs: npm diff",
      "url": "https://docs.npmjs.com/cli/v11/commands/npm-diff"
    },
    {
      "title": "npm docs: npm audit (audit signatures)",
      "url": "https://docs.npmjs.com/cli/v11/commands/npm-audit"
    },
    {
      "title": "pnpm docs: minimumReleaseAge",
      "url": "https://pnpm.io/settings/dependency-resolution"
    },
    {
      "title": "uv docs: exclude-newer",
      "url": "https://docs.astral.sh/uv/reference/settings/#exclude-newer"
    },
    {
      "title": "uv docs: running tools at a specific version",
      "url": "https://docs.astral.sh/uv/guides/tools/"
    },
    {
      "title": "GitHub docs: Dependabot options reference (cooldown)",
      "url": "https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference"
    },
    {
      "title": "MCP Inspector",
      "url": "https://github.com/modelcontextprotocol/inspector"
    }
  ],
  "related": [
    {
      "title": "How to detect MCP tool description changes (rug pulls)",
      "url": "https://www.rightshold.com/answers/how-to-detect-mcp-tool-description-changes-rug-pull"
    },
    {
      "title": "How to vet an MCP server before installing it",
      "url": "https://www.rightshold.com/answers/how-to-vet-an-mcp-server-before-installing-it"
    },
    {
      "title": "How to tell if a Claude Code or Cursor plugin is safe to install",
      "url": "https://www.rightshold.com/answers/how-to-tell-if-a-claude-code-or-cursor-plugin-is-safe-to-install"
    },
    {
      "title": "Tool-definition fingerprint spec",
      "url": "https://www.rightshold.com/security/spec"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-10-29"
}