How to Pin MCP Server Versions: npm, PyPI, Docker and Remote
Pin MCP server versions for npm, PyPI and Docker packages, record serverInfo.version and a tools/list snapshot for remote servers, and get change alerts.
TL;DR. To pin MCP server versions, put an exact version in the client config: example-mcp-server@1.4.2 after npx -y, example-mcp-server@1.4.2 (or --from 'example-mcp-server==1.4.2') after uvx, and an @sha256: digest instead of a tag for a Docker image. A remote server cannot be pinned, since it runs where its vendor runs it; what you can do is record the version it reports in io.modelcontextprotocol/serverInfo, keep a copy of its tools/list result, and diff both later. mcp.tc's History tab records version and tool-list changes for the servers it can read, and following a listing emails you when they happen.
What pinning an MCP server means
Pinning means the client starts the same server code every session until you change the config on purpose. For a local server that is a package name plus an exact version, or an image plus a digest. The registries make this hold: the npm docs state that "once a package is published with a given name and version, that specific name and version combination can never be used again, even if it is removed with npm unpublish" (npm publish). PyPI's help page says it refuses file-name reuse so that "a given distribution for a given release for a given project will always resolve to the same file, and cannot be surreptitiously changed one day by the projects maintainer or a malicious party (it can only be removed)". The Docker docs describe a digest as pinning an image "to a specific version in time" (docker image pull).
The MCP Registry's server.json already carries that shape: each package entry has a registryType, an identifier and a version; the reference's examples include an npm entry with "version": "1.0.2", a PyPI entry with "runtimeHint": "uvx" and "version": "0.5.0", and an OCI entry whose identifier ends in :1.0.2 (server.json reference). Setup instructions that copy that version into the client config are pinned; instructions that drop it are not.
A remote server has no version field you control. The vendor deploys new code and every client gets it on the next request. The second half of this post covers what you can record instead.
Why an unpinned config changes under you
An unpinned config resolves the package at run time, so the server a client starts depends on what is published at that moment. The npx docs say that package names "provided without a specifier will be matched with whatever version exists in the local project", and that packages "not present in the local project dependencies" are "installed to a folder in the npm cache". A config in your home directory has no local project, so npx -y example-mcp-server fetches from the registry, and a new release changes what runs next time.
Docker has the same default. If no tag is given, "Docker Engine uses the :latest tag as a default" (docker image pull). A tag is a name the publisher can point at new content; a digest is the content.
A client that shows a dialog when a server's configuration changes has nothing to show here: a new package version behind an unchanged npx -y line is not a configuration change. The config looks identical; the tools behind it can differ.
Pin npm servers run with npx
Append @ and the exact version to the package name, and the npx form npx -- <pkg>[@<version>] [args...] from the npx docs runs that release. First find the current version from the registry:
npm view example-mcp-server version
npm view example-mcp-server versionsnpm view "shows data about a package and prints it to stdout"; the first form defaults to the latest version and versions lists them all (npm view). Then put the version in the config. In Claude Code the command is claude mcp add [options] <name> -- <command> [args...], with --scope project writing a shared .mcp.json (Claude Code MCP docs):
claude mcp add example-server --scope project -- npx -y example-mcp-server@1.4.2The resulting .mcp.json entry, which is also the shape of the generic JSON config mcp.tc shows on each listing:
{
"mcpServers": {
"example-server": {
"command": "npx",
"args": ["-y", "example-mcp-server@1.4.2"]
}
}
}VS Code uses a top-level servers object in .vscode/mcp.json with the same command and args keys (VS Code MCP docs):
{
"servers": {
"example-server": {
"command": "npx",
"args": ["-y", "example-mcp-server@1.4.2"]
}
}
}-y suppresses the install prompt (the npx docs: "A prompt is printed (which can be suppressed by providing either --yes or --no)"). The specifier pins the server package itself; the package's own dependencies are resolved by npm from the ranges in its package.json when it installs into the cache.
If you would rather hold the pin in a lockfile, install the server into a project with npm install -E example-mcp-server@1.4.2. The -E / --save-exact flag means "Dependencies saved to package.json will be configured with an exact version rather than using npm's default semver range operator" (npm install), package-lock.json records the resolved tree, and npx example-mcp-server run from that project matches the locally installed version per the npx rule quoted above.
Pin PyPI servers run with uvx
Use command@<version> for an exact version, the form the uv tools guide documents with the note that "the @ syntax cannot be used for anything other than an exact version". When the command name differs from the package name, or you want a range, use --from with a == pin or a range such as >1.4.0,<1.5.0, the two forms the guide's examples show. For an MCP server:
{
"mcpServers": {
"example-server": {
"command": "uvx",
"args": ["--from", "example-mcp-server==1.4.2", "example-mcp-server"]
}
}
}uvx example-mcp-server@latest asks for the newest release explicitly, which is useful when you want to see what an upgrade would change before you move the pin. PyPI's reuse rule, quoted above, is what makes the pinned file stay the same file.
Pin Docker images by digest
Replace the tag with the digest. Docker prints it at the end of a pull ("Docker prints the digest of the image after the pull has finished", as a Digest: sha256:... line), and docker images --digests lists the digests of local images (docker image ls):
docker pull ghcr.io/example/example-mcp-server:1.4.2
docker images --digests ghcr.io/example/example-mcp-serverThen "a digest takes the place of the tag" in the reference (docker image pull). For a stdio server the config runs the container with -i ("Keep STDIN open even if not attached") and --rm ("Automatically remove the container and its associated anonymous volumes when it exits"), both from the docker run reference:
{
"mcpServers": {
"example-server": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"ghcr.io/example/example-mcp-server@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30"
]
}
}
}The Docker docs add the cost: with a digest, Docker "does therefore not pull updated versions of an image, which may include security updates". A pin is a decision to upgrade by hand, so put a reminder somewhere you will see it.
Remote servers: record the version and the tool list
A remote server cannot be pinned, so the useful move is to write down what it reports and compare later. Under the 2026-07-28 revision there is no initialize handshake; the changelog says every request carries its protocol version in _meta, and "servers SHOULD identify themselves in each result's _meta (io.modelcontextprotocol/serverInfo)". That field is the "Server name and version" (base protocol), and it is the server software's version, which is separate from the protocol version date.
There is also a dedicated call. Servers "MUST implement" server/discover, which returns supportedVersions, capabilities, serverInfo in _meta, and optional instructions (server/discover). One curl gets it, with the headers the Streamable HTTP transport requires on every POST (MCP-Protocol-Version, Mcp-Method, and an Accept header listing both application/json and text/event-stream):
curl -s https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "snapshot", "version": "0.1.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' > discover.jsonChange Mcp-Method and method to tools/list and the same request returns the tool list; save it as tools.json. The server may answer with a single JSON object or an SSE stream (the transport page says the client "MUST support both"), so check which you got before piping it through jq. A server that still speaks a legacy revision expects initialize instead; the transport page tells clients that get a 400 Bad Request whose body is not a recognized modern JSON-RPC error to fall back to initialize. Servers that need sign-in before they list tools need the token on the request too; the tool set "MAY vary by the authorization presented on the request" (tools), so a snapshot taken without a token can differ from what your signed-in client sees.
If you would rather not hand-write the request, npx mcp-tc check https://mcp.example.com/mcp reads a running server the way mcp.tc reads it and prints its serverInfo, its tools with their annotations, and what to fix (the mcp-tc CLI, README; Node.js 20 or later). It runs on your computer and talks to the server's own URL.
Two caveats from the spec itself. serverInfo "is self-reported by the server and is not verified by the protocol"; clients "SHOULD NOT rely on it for security decisions". And the tools/list result now carries ttlMs and cacheScope, a freshness hint for caching, which tells you how long the server thinks the list is good for, not whether it changed.
Diff the snapshot after each update
A diff of two tools/list results shows what an update changed before you approve any tool. Normalize both files first so key order does not create noise:
jq -S '.result.tools' tools-2026-10-01.json > a.json
jq -S '.result.tools' tools.json > b.json
diff a.json b.jsonRead the diff for tool names added or removed, changed description text, changed inputSchema (new parameters, changed defaults or enums, changed parameter descriptions), and changed annotations. The spec asks servers to return tools in a deterministic order, which makes this diff smaller. Treat annotation changes as claims: the tools page says clients "MUST consider tool annotations to be untrusted unless they come from trusted servers". The checks in Is This MCP Server Safe? apply to the new text the same way they applied to the old.
For a local server the diff goes with the version bump: run the new version once with the client's own tooling or npx mcp-tc check against a project directory, diff, then move the pin.
Let mcp.tc's History tab keep the snapshot for you
mcp.tc records version and tool-list changes for every listing it can read, so for a listed server you can read the diff instead of keeping files. On 12 October 2026 the directory listed 808 servers, 558 remote and 250 local, and compared the tool lists of 186 listings every 3 hours; its change history began on 6 October 2026. Every 3 hours it reads the tool list of each listed server whose tools it can read without signing in (remote servers that list tools without sign-in, servers whose tools come from an MCP bundle manifest in their GitHub repository, and mcp.tc's own server), and it checks the npm or PyPI package of local servers for a new version. Added and removed tools, changed descriptions, input and output schemas, annotations and server instructions go on the listing's History tab with the date and time. A change is recorded only after two reads agree, and a listing gets at most one version change in 24 hours (how the history works).
For version bumps that is the practical "is my pin behind?" check: the History tab shows the new npm or PyPI version when mcp.tc's checks find it, or the new serverInfo version a remote server reports. Recent changes lists the newest entries across the directory, with an Atom feed at https://mcp.tc/changes.xml, and each listing has its own feed at https://mcp.tc/i/{slug}/history.xml. Sign in with GitHub or an email code, press Follow on up to 200 listings, and mcp.tc emails you as changes happen (at most one an hour), as a daily summary, or not at all. One email covers every followed listing that changed.
What it cannot see matters as much. Servers that ask for sign-in before they list tools, and local servers without a manifest, have no watched tool list; their History records versions and owner edits only. It cannot see tools a server shows only after sign-in or only to some clients, and a change undone between two checks can be missed. It is not a security review and it approves nothing; it helps you notice that something moved, so that you go and read the diff.
FAQ
Does pinning an npm MCP server also pin its dependencies?
No. A specifier such as example-mcp-server@1.4.2 pins the server package; npm resolves that package's own dependencies from the ranges in its package.json when it installs into the cache. To freeze the whole tree, install the server into a project with npm install -E example-mcp-server@1.4.2, keep the generated package-lock.json, and run npx example-mcp-server from that project, where npx matches the locally installed version.
Can I pin a remote MCP server?
No. A remote server runs on the vendor's infrastructure, and every request gets whatever code is deployed. You can record what it reports: the io.modelcontextprotocol/serverInfo name and version in each result's _meta, the server/discover result, and the tools/list result. Save them, diff them after each change, and follow the listing on mcp.tc so a version or tool-list change found by its checks reaches your inbox.
Where is the server version in the 2026-07-28 spec?
In _meta. The revision removed the initialize handshake, and servers SHOULD put io.modelcontextprotocol/serverInfo (name and version of the server software) in every result's _meta. server/discover, which servers MUST implement, returns it together with supportedVersions and capabilities. The spec notes that this value is self-reported and not verified, and tells implementations not to rely on it for security decisions.
Is a Docker tag like :1.4.2 enough?
A tag is a name the publisher can point at different content later; a digest identifies the content. Docker's docs describe pulling by digest as pinning an image "to a specific version in time", and warn that Docker then "does therefore not pull updated versions of an image, which may include security updates". Use image@sha256:... in the config, note the tag it came from in a comment or commit message, and plan to move the digest yourself.
How do I know when my pinned version is behind?
Run npm view example-mcp-server version or uvx example-mcp-server@latest to see the newest release, or open the listing's History tab on mcp.tc, which records new npm and PyPI versions when its checks find them (at most one version change per listing in 24 hours). Following the listing emails you when that happens. Diff the new tool list against your snapshot before you move the pin.
Should I pin the protocol version too?
The protocol version is chosen per request by the client, not by your config. Under 2026-07-28 every request carries io.modelcontextprotocol/protocolVersion in _meta and the matching MCP-Protocol-Version header, and a server that does not support it answers with the versions it does. Your client handles that negotiation; the thing you pin is the server package or image.
Sources
- docs.npmjs.com/cli/v11/commands/npx
- docs.npmjs.com/cli/v11/commands/npm-view
- docs.npmjs.com/cli/v11/commands/npm-install
- docs.npmjs.com/cli/v11/commands/npm-publish
- docs.astral.sh/uv/guides/tools/
- pypi.org/help/
- docs.docker.com/reference/cli/docker/image/pull/
- docs.docker.com/reference/cli/docker/image/ls/
- docs.docker.com/reference/cli/docker/container/run/
- github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/generic-server-json.md
- code.claude.com/docs/en/mcp
- code.visualstudio.com/docs/copilot/customization/mcp-servers
- modelcontextprotocol.io/specification/2026-07-28/changelog
- modelcontextprotocol.io/specification/2026-07-28/basic/index
- modelcontextprotocol.io/specification/2026-07-28/server/discover
- modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- modelcontextprotocol.io/specification/2026-07-28/server/tools
- github.com/mcptc/mcp-tc