# MCP server updates: treat releases as supply-chain events

An MCP server update is a supply-chain event because it changes executable code that an agent can ask to act. The fact that the server runs locally, uses stdio, or keeps an old tool name does not soften that claim. It may change what data the tool reads, where it sends requests, which defaults it chooses, and how it interprets a prompt-generated argument.

I have seen teams treat agent integrations as harmless plumbing until an update turns a narrow helper into a broad access path. The damage usually begins with a reasonable shortcut: a floating version, a release note skimmed between meetings, and production credentials reused for a quick test. The fix is not a giant approval committee. It is a repeatable adoption gate that pins the artifact, compares observed behavior, retests access, and leaves a rollback route intact.

## An MCP server is executable dependency code

An MCP server is not configuration just because an agent discovers it through a protocol. It is a program with dependencies, startup code, parsers, network clients, and often access to files or external accounts. If you update it, you have changed a program inside an action path that an agent controls through natural-language-derived inputs.

People often blur two separate risks. The first is **artifact integrity**: did you install the exact code you intended to install? The second is **action semantics**: does that exact code still perform the limited action you think it performs? A signed package or a matching checksum helps with the first question. It says little about the second.

That distinction matters when a server adds a seemingly convenient feature. A filesystem tool that once accepted one workspace root may begin resolving symlinks differently. A source-control tool may add automatic remote fetching. An API tool may decide to follow redirects. Each change can preserve the command name, satisfy a test that only checks success, and still alter the data boundary.

Local stdio changes transport exposure, not trust. A stdio server avoids an inbound listening port, which is useful. It still inherits the authority of the process that launches it. If that process can read a home directory, inspect environment variables, or reach the internet, the server may be able to do the same unless the operating system or launcher blocks it.

Treat the update request as you would treat a new build agent plugin or a new command line binary. Ask what code enters the machine, what authority it receives, what it can send out, and how you will prove that the version under review is the version in use.

## Floating versions turn review into theater

A review has no meaning when the deployment command can install a different artifact tomorrow. Version ranges, mutable tags, and uncommitted lockfiles invite that outcome.

For a Node-based server, pin the direct dependency exactly and commit the lockfile. This example blocks the usual caret range that silently accepts later minor releases.

```json
{
  "dependencies": {
    "example-mcp-server": "1.4.2"
  }
}
```

Then install with the lockfile enforced in automation:

```sh
npm ci
npm ls example-mcp-server
```

The second command should print a tree containing the expected version, shaped like this:

```text
project@0.1.0 /work/project
└── example-mcp-server@1.4.2
```

Do not stop at the direct dependency. Inspect the lockfile diff for changed transitive packages, especially packages that run install scripts, parse untrusted input, open network connections, or provide authentication code. A direct package may hold still while a permissive transitive range moves underneath it.

For container distribution, pin a digest rather than a tag:

```text
registry.example/team/mcp-server@sha256:0123456789abcdef...
```

A tag such as `1.4.2` can point to a different image later. A digest identifies one content-addressed image. Pinning does not make the image acceptable. It makes your test result refer to a stable object, which is the minimum needed for a useful review.

For source installs, pin a full commit identifier and record how you obtained it. Do not write a branch name into a bootstrap script and call that pinning. Branches move by design. If the build pulls dependencies during installation, lock those too, or your source pin only covers part of the program that runs.

Keep the prior artifact reference in the same configuration. A rollback that depends on somebody remembering last month's version is not a rollback plan. It is a story someone tells while production access remains exposed.

## Protocol compatibility does not preserve tool behavior

The Model Context Protocol specification defines how clients and servers initialize, advertise capabilities, list tools, and call tools. It does not certify the meaning or safety of a tool named `search_files`, `deploy`, or `send_message`. Protocol compatibility is necessary for a client and server to communicate. It is not proof that the server still has the same authority.

The specification's tools flow makes this visible. A client obtains a tool inventory through `tools/list` and invokes a tool through `tools/call`. Servers may also notify clients that the tool list changed. Use those protocol facts as review inputs, not as a reason to trust a release automatically.

Capture the old and new tool inventories under the same test configuration. Save the raw JSON, then compare it. Names alone are a poor diff. Look at descriptions, input schemas, required fields, enums, defaults described in text, annotations if present, and output structure.

A minimal capture process looks like this:

```text
1. Launch the old server with a disposable test account.
2. Call tools/list and save the full response as tools-old.json.
3. Launch the pinned candidate with the identical configuration.
4. Call tools/list and save tools-new.json.
5. Diff the files, then inspect changed tools by calling them with fixed fixtures.
```

Suppose a server keeps a tool named `read_project_file`. The old schema requires a relative `path`. The new version accepts `path` plus an optional `root`, and its description says it can use an environment default when `root` is omitted. That is an access change even if every existing client call still works. An agent can now produce an argument that reaches outside the workspace if the server fails to constrain the default.

Descriptions deserve more scrutiny than many engineers give them. Agents use them to decide when and how to call a tool. A new description that says "Use this to inspect any local file needed for debugging" can broaden actual agent behavior before any source code bug appears. The tool implementation may still reject unsafe paths, but you should test that claim rather than infer it from a friendly sentence.

Also compare failure behavior. A tool that previously rejected an ambiguous argument might now guess. Guessing can feel helpful in an interactive command line. In agent execution, it turns uncertain intent into an action.

## Release notes are evidence, not a review

Release notes tell you what maintainers chose to mention. They do not enumerate every changed dependency, changed default, or altered error path. Read them, but verify the areas that matter to your installation.

Start with the release artifact and its provenance. Identify the package version, image digest, source commit, and installation command. Then inspect the source diff when the project makes it available. Pay close attention to code around process spawning, path handling, HTTP clients, credential loading, telemetry, update checks, and startup scripts. Those locations decide authority more often than the tool handler itself.

Package manager documentation gives a useful warning here. npm documents that lifecycle scripts can run during installation. That means an update review starts before the server process launches. If your workflow installs a package on a developer machine with personal credentials and broad filesystem access, an install script already has a meaningful opportunity to cause harm.

Use a clean environment for candidate installation. A disposable virtual machine or dedicated test account is better than a developer's normal workstation. Give it an empty home directory, a separate package cache where practical, and only the test credentials required for the exercise. Record the commands you ran so another person can repeat the result.

Reject the lazy argument that open source removes this work. Public source lets you inspect more. It does not inspect itself, and it does not freeze transitive dependencies or prove that your package registry delivered the source you reviewed.

The opposite mistake is demanding a line-by-line audit for every patch release. Most teams will skip that burden, then return to blind updates. Match review depth to authority. A formatting helper with no network access deserves less effort than a server that can read repositories, execute shell commands, or call a cloud API. The process must be strict where consequences are high and fast enough that people will actually use it.

## Test denied paths before happy paths

A successful `tools/call` proves only that the server can do something. It says nothing about where it stops. Access testing should begin at the boundary you expect the server to enforce.

Build a small fixture set that includes allowed inputs and deliberately disallowed inputs. Keep it versioned with the server configuration. For a project-file tool, test a normal file, a parent-directory traversal attempt, an absolute path, a symlink that points outside the fixture root, a nonexistent file, and an unreadable file. For an HTTP tool, test an approved host, an unapproved host, a redirect to an unapproved host, a private address if that matters to your environment, and a request with a malformed method or header.

The expected result must be specific. "It failed" is not enough. A tool might fail only because a network route happened to be down. Write outcomes such as:

- It reads `fixtures/app/config.json` and returns its expected content.
- It rejects `../outside.txt` before opening a file.
- It rejects a symlink whose resolved target leaves the fixture root.
- It refuses the redirect before sending credentials to the new host.
- It returns a structured error without printing secrets in diagnostic output.

This is where updates surprise experienced teams. A refactor replaces a path library, a default changes, or an error handler now logs the request object. The happy path keeps passing. The boundary test catches the regression.

Retest authority separately from syntax. A tool can correctly parse a restricted path yet use a credential that gained broader scope since the last review. Use a test identity that lacks access to a known protected object, then confirm the server cannot retrieve or alter it. If the server supports different account profiles, test each profile rather than assuming the most restricted profile represents them all.

Watch outbound behavior while you test. A network monitor, controlled DNS record, proxy log, or isolated network can show destinations that tool output does not reveal. You do not need elaborate surveillance for every server. You do need to know whether an update contacts a new host, follows redirects, or sends error reports that include request context.

## Agents magnify small schema changes

A human sees a new optional field and pauses. An agent sees a new optional field in a tool description and may try it repeatedly across a long task. This is why behavior review must cover the agent's decision surface, not just the server's API surface.

Test with representative prompts after direct fixture tests. Use prompts that reflect real work but constrain the test environment: inspect a repository, fetch a known issue, update a dummy record, or connect to a nonproduction host. Collect the actual tool calls. Compare the old and new runs for call count, arguments, error recovery, and any action the agent attempts after a failure.

Do not confuse prompt injection resistance with update safety. A server can have perfect input validation and still change its intended authority. Conversely, a server can keep identical behavior while a new tool description persuades an agent to request actions more often. You need both layers in view.

A useful test prompt asks for a boundary that should remain closed. For example: "Find the build configuration for this sample project. Do not inspect files outside the project directory." If the candidate server attempts a parent path, follows a symlink out of the tree, or asks for a broader root, you have evidence that the update changed the decision surface.

Keep the client version fixed during this comparison. Updating the MCP client and server together makes an unexpected result hard to attribute. First test the candidate server against the current client. If you also plan a client update, test that as a separate change with the server held fixed. Combined upgrades save a little calendar time and cost much more debugging time.

## Credentials belong outside the server process when possible

A server that reads a long-lived API token from its own environment carries that token through its process lifetime and often through child processes, crash reports, and careless debug logging. It also makes every update a test of the server's secret handling. Reduce that exposure before you ask an agent to use the server.

Use separate credentials for development, testing, and production. Scope each credential to the few actions the server needs. Rotate test credentials after an investigation or a broad test run. This is ordinary operational discipline, but agent workflows make the consequences sharper because the caller can generate unusual arguments at high volume.

Sallyport keeps API and SSH credentials in its encrypted vault and performs the HTTP or SSH action without handing the secret to the agent. That design narrows one major review question: you still review the MCP-facing action and its requested access, but the agent does not receive reusable credential material.

Do not mistake a credential boundary for an approval of server behavior. A gateway can prevent an agent from reading a token while the action itself still writes the wrong record or contacts the wrong endpoint. Review the request shape, destination, account scope, and result handling as separate concerns.

For high-consequence actions, require a deliberate approval at the action boundary. Sallyport can require approval on every use for individual credentials, which is useful when a tool can deploy, modify a production record, or access a sensitive host. Repeated prompts can make people click through without reading, so reserve per-call approval for actions where a mistaken call has a meaningful cost.

## Record the adoption decision where engineers can find it

A release becomes hard to investigate when nobody can answer four plain questions: which artifact ran, who approved it, what did they test, and what version can replace it if it fails. Put that record beside the configuration that launches the server.

A concise adoption record can fit in a repository file or change request:

```text
Server: example-mcp-server
Previous artifact: example-mcp-server@1.4.1
Candidate artifact: example-mcp-server@1.4.2
Resolved digest or lockfile revision: recorded in commit 8f31c2a
Reviewed changes: tools/list diff, dependency diff, release notes
Tests: path boundaries passed; redirect boundary passed; restricted account denied
Approved access: test API account only
Rollback artifact: example-mcp-server@1.4.1
Owner: team name or responsible engineer
```

Do not record secrets, full request bodies with customer data, or copied authorization headers. The record should prove the decision, not create a second secret store.

The audit trail should separate agent runs from individual actions. A run answers which agent process received permission. An action answers what it then attempted. Those are different questions during an incident. If you use an action gateway, preserve logs that let you revoke a run quickly and inspect each call later. Sallyport's Sessions and Activity journals are built around that separation and draw from a hash-chained encrypted audit log that `sp audit verify` can verify offline.

Do not rely on a single chat approval. Chat threads lose context, edits hide history, and the package reference rarely survives intact. A checked-in record ties the stated test to the exact configuration that later deploys.

## A short adoption gate beats emergency rollback

Teams adopt unsafe updates when the safe path is vague or slow. Make the gate short enough for a patch release and strict enough that it catches authority changes.

Use this sequence before a developer changes the shared agent configuration:

1. Resolve and pin the candidate artifact, including the lockfile or image digest.
2. Install it in a clean test environment with a restricted test identity.
3. Diff the raw `tools/list` output and inspect changed schemas, descriptions, and defaults.
4. Run the boundary fixtures and representative agent prompts, then inspect outbound destinations and logs.
5. Commit the adoption record and retain the prior artifact as the rollback target.

This gate does not promise that an update has no defect. Nothing honest can promise that. It does force the team to test the code they will run, under access similar to the access they intend to grant, before an agent finds the changed behavior in a live task.

The first change to make is usually embarrassingly small: replace the floating server version in the shared configuration with an exact artifact reference, then commit the evidence for it. That single move turns an informal update into a decision you can reproduce, challenge, and reverse.
