# 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.

Published 2026-10-12 by mcp.tc. Canonical: https://mcp.tc/blog/pin-mcp-server-versions

**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](https://docs.npmjs.com/cli/v11/commands/npm-publish)). [PyPI's help page](https://pypi.org/help/) 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](https://docs.docker.com/reference/cli/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](https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/generic-server-json.md)). 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](https://docs.npmjs.com/cli/v11/commands/npx) 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](https://docs.docker.com/reference/cli/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](https://docs.npmjs.com/cli/v11/commands/npx) runs that release. First find the current version from the registry:

```bash
npm view example-mcp-server version
npm view example-mcp-server versions
```

`npm 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](https://docs.npmjs.com/cli/v11/commands/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](https://code.claude.com/docs/en/mcp)):

```bash
claude mcp add example-server --scope project -- npx -y example-mcp-server@1.4.2
```

The resulting `.mcp.json` entry, which is also the shape of the generic JSON config mcp.tc shows on each listing:

```json
{
  "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](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)):

```json
{
  "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](https://docs.npmjs.com/cli/v11/commands/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](https://docs.astral.sh/uv/guides/tools/) 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:

```json
{
  "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](https://docs.docker.com/reference/cli/docker/image/ls/)):

```bash
docker pull ghcr.io/example/example-mcp-server:1.4.2
docker images --digests ghcr.io/example/example-mcp-server
```

Then "a digest takes the place of the tag" in the reference ([docker image pull](https://docs.docker.com/reference/cli/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](https://docs.docker.com/reference/cli/docker/container/run/):

```json
{
  "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](https://modelcontextprotocol.io/specification/2026-07-28/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](https://modelcontextprotocol.io/specification/2026-07-28/basic/index)), 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](https://modelcontextprotocol.io/specification/2026-07-28/server/discover)). One `curl` gets it, with the headers the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) requires on every POST (`MCP-Protocol-Version`, `Mcp-Method`, and an `Accept` header listing both `application/json` and `text/event-stream`):

```bash
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.json
```

Change `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](https://modelcontextprotocol.io/specification/2026-07-28/server/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](https://mcp.tc/cli), [README](https://github.com/mcptc/mcp-tc); 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:

```bash
jq -S '.result.tools' tools-2026-10-01.json > a.json
jq -S '.result.tools' tools.json > b.json
diff a.json b.json
```

Read 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?](https://mcp.tc/blog/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](https://mcp.tc/i/mcp-tc#history)), 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](https://mcp.tc/docs/changes)).

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](https://mcp.tc/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

- <https://docs.npmjs.com/cli/v11/commands/npx>
- <https://docs.npmjs.com/cli/v11/commands/npm-view>
- <https://docs.npmjs.com/cli/v11/commands/npm-install>
- <https://docs.npmjs.com/cli/v11/commands/npm-publish>
- <https://docs.astral.sh/uv/guides/tools/>
- <https://pypi.org/help/>
- <https://docs.docker.com/reference/cli/docker/image/pull/>
- <https://docs.docker.com/reference/cli/docker/image/ls/>
- <https://docs.docker.com/reference/cli/docker/container/run/>
- <https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/generic-server-json.md>
- <https://code.claude.com/docs/en/mcp>
- <https://code.visualstudio.com/docs/copilot/customization/mcp-servers>
- <https://modelcontextprotocol.io/specification/2026-07-28/changelog>
- <https://modelcontextprotocol.io/specification/2026-07-28/basic/index>
- <https://modelcontextprotocol.io/specification/2026-07-28/server/discover>
- <https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http>
- <https://modelcontextprotocol.io/specification/2026-07-28/server/tools>
- <https://github.com/mcptc/mcp-tc>
