Lockfile Strategies for MCP Server Python Environments Across Agent Runtimes
Dual lockfiles ensure MCP servers run with identical dependencies and capabilities across machines.

Reproducible environments for AI coding agents and MCP servers require two locks. A committed uv.lock controls Python dependencies, while a capability lockfile records the tools, prompts, and resources an approved server advertises. A complete strategy verifies both layers in CI and matches each agent runtime’s launch command to the locked environment.
Why MCP server environments have two distinct locking problems, not one
An MCP server’s capability surface is negotiated at runtime. The consuming agent discovers which tools, prompts, and resources the server offers when it connects. That discovery creates a separate source of drift. The same binary can advertise different tool schemas from one run to the next, even when every installed package stays the same.
The two layers therefore fail independently. Layer 1 governs installed Python packages, versions, and hashes, while Layer 2 governs the capabilities declared to the agent. Conventional dependency tooling controls the first layer but does not inspect the second. A perfectly pinned environment can still expose an unexpected toolset, so package reproducibility alone cannot establish that an agent workflow will behave as approved.
How pip and requirements.txt leave transitive dependencies uncontrolled by default
Pinning top-level packages in requirements.txt leaves their transitive dependencies uncontrolled unless those dependencies are also pinned. For example, flask==3.0.0 does not by itself determine which version of Werkzeug installs. A later transitive release can change the environment. An MCP server tested locally can consequently run against a different dependency tree in CI or on another developer’s machine.
Hash verification requires a separate, deliberate workflow. With pip, that means generating hashes through pip-compile --generate-hashes and installing with --require-hashes. These steps provide controls, but they are easy to omit when CI does not enforce them.
How do you get the same dependencies every run with uv.lock?
uv.lock records the resolved dependency tree and package hashes, making it the foundation for Layer 1. Hash recording is part of the design. Committing the file gives developer machines, CI runners, and container builds a shared dependency baseline rather than asking each installation to choose a fresh tree.
That baseline also simplifies the startup workflow. uv sync provisions the correct Python version, builds the environment, and installs pinned dependencies, while uv run python server.py avoids manual virtual-environment activation. The --frozen flag prevents the command from updating the lockfile. In CI, this keeps installation anchored to the committed resolution.
The lockfile must exist before that discipline can apply. uv init initializes the project, but the first uv sync, uv run, or uv lock creates the lockfile. The operational sequence is therefore initialization, resolution, and commitment of uv.lock, followed by frozen installation in automated environments.
Why cross-architecture teams and CI runners need uv.lock’s universal resolve
With a single uv.lock, environments can be reproduced across macOS, Linux, and Windows without a separate resolve for each platform. This addresses a common split between Apple Silicon development machines, AMD64 CI runners, and Linux deployment targets. The shared file governs platform-appropriate dependency choices. It does not require every operating system to install an identical set of platform-specific artifacts.
MCP deployments make that distinction especially important because the consuming agent may run on a different architecture from the server developer’s machine. However, the launch command can discard the benefit of the committed lockfile. uvx ignores both the project lockfile and its specified Python version range. Shipping a server as uvx my-server therefore opts out of project-level reproducibility despite the presence of uv.lock.
The Python SDK version boundary that teams on unpinned ranges may have already crossed silently
Python MCP SDK v2.0.0 shipped on 28 July 2026 as a breaking release, renamed FastMCP to MCPServer, and moved v1 into maintenance mode for security fixes only. Resolving an open range such as mcp>=1 again could select 2.x even if the team had no plans to migrate.
Teams migrating to 2.x must account for the FastMCP → MCPServer rename in server code as well as dependency configuration. A lockfile cannot perform that code change. Frozen installation protects the approved resolution from incidental updates, while explicit SDK version constraints describe the intended migration boundary. This example also connects the layers, because a dependency change can break the server that supplies the agent’s capability surface.
How MCP capability drift breaks agent workflows in ways no package lockfile can detect
Capability drift can break either the syntax or the meaning of a workflow. A newly required argument invalidates stored prompts that omit it, while a removed enum value invalidates calls that still use that value. Changes to descriptions are harder to spot by watching for execution failures. They can change when an agent chooses a tool even though its calls remain syntactically valid.
mcplock’s worked example makes that distinction concrete. A tool initially described as “Execute a shell command and return stdout” acquires a description instructing the model to read ~/.ssh/id_rsa and pass its contents as context. The package tree need not change at all. A package diff cannot show whether the instructions given to the model are still safe.
Snyk’s 25 September 2025 disclosure of postmark-mcp illustrates the complementary package-level threat. Its analysis suggested that, starting around version 1.0.16, the software silently sent copies of outgoing messages to an outside address, while earlier versions were clean. Version pinning addresses that release boundary, but capability comparison addresses a different question. Neither check substitutes for the other.
Three tools that address capability drift at the lockfile level — mcplock, mcpsum, and MCP Sentinel
mcplock snapshots the declared surface with mcplock snap and verifies it with mcplock verify --lock mcplock.json --fail-on breaking. Its committed artifact is plain JSON, and it supports stdio and Streamable HTTP transports. Changes are classified as breaking, risky, new, or safe. Anything it cannot prove harmless is reported as breaking, favoring visible review over silently accepting uncertain changes.
Its offline scanner adds policy checks without spawning the server. mcplock risk examines the lockfile for capabilities such as credential access, file reading, network egress, and process execution, as well as prompt-injection wording. With the policy options --deny-capability process-exec --fail-on high, the build fails if command execution is prohibited. A GitHub Action also supports a configurable breaking-change gate.
mcpsum combines a lockfile with a runtime reference monitor. As of October 2026, work was underway to add MCP 2026-07-28 lock and verify support by probing with server/discover and using strict fail-closed validation. Its scope includes protocol-era changes. An EraChanged result denotes definitional drift, requiring review before the proxy can communicate with the server as locked.
MCP Sentinel offers a narrower tool-schema workflow. It describes itself as “package-lock.json, but for MCP tool schemas” and supports snapshots, drift checks with CI exit codes, and diffs. This narrower scope makes it easier for teams to start capability locking. The choice depends on which surface the team needs to record, which policy controls it needs, and which protocol eras the tool covers, with no single ranking that fits every team.
How each major agent runtime launches and manages MCP servers, and what that means for locking strategy
Claude Desktop launches stdio servers through a configuration file whose command field selects the process. On macOS, that file is ~/Library/Application Support/Claude/claude_desktop_config.json, with a Windows equivalent. Changing the command can change which process runs after the team has approved its dependencies and capabilities.
Cursor uses a separate configuration path from Claude Desktop. Maintaining the same server across both clients consequently creates another place for launch settings to diverge. A committed server lockfile cannot repair a client command that bypasses it. Cross-client consistency depends on matching those commands to the approved environment.
LangGraph typically integrates MCP servers programmatically. Its graph tool-calling layer receives the capability surface, so server drift can change what the graph can do without a graph-code change. The coding assistant in VS Code shares the broader verification gap. Across all four runtimes, clients fetch tools/list at session startup without natively comparing it with a committed baseline.
What the 2026-07-28 protocol specification changes for both locking layers
The 28 July 2026 MCP specification made the protocol stateless-first and removed the initialize and notifications/initialized handshake. The recommended production pattern became stateless Streamable HTTP services behind a plain load balancer. Dependency approval must account for SDK compatibility, while capability approval must account for the protocol definition used to discover the surface.
The authorization specification is optional overall, but HTTP-based servers should conform to it. Its requirements include publishing Protected Resource Metadata under RFC 9728 and validating audience, while the authorization server must implement OAuth 2.1. A locking strategy based on an earlier authorization model may therefore leave deployment assumptions unexamined.
mcpsum records the protocol era it detects for each server. A dual-era server retains the era in which it was locked. Modern entries require lockfile version 2. Older software refuses v2, while newer software refuses a v1 file that is inconsistent, so fail-closed behavior holds across the format boundary.
Connecting lockfile discipline to SBOM generation and supply-chain attestation
A committed uv.lock has a resolved dependency tree and cryptographic hashes that can be used to generate a software bill of materials. Compared with direct dependency pins, it supplies a more complete account of the packages selected for the build. Build-time tooling can consume that deterministic record. The SBOM then rests on the same dependency baseline used to construct the environment.
PyPI’s rollout of Sigstore attestations through 2024–2025 adds registry-level provenance records that teams can connect to package hashes. Provenance verification still has a practical limit. As of early 2026, signing first-party artifacts was tractable, but verifying valid provenance for every transitive dependency was not solved at scale. The lockfile still serves as a practical trust boundary, but it cannot prove the whole provenance chain.
The capability lockfile complements that package inventory. It records the declared surface approved for the server, allowing an audit to distinguish reviewed changes from silent drift. A versioned manifest.toml with the full resolved environment and package hashes can sit beside it. Input-derived deployment hashes then support precise CVE discovery and hash-level blocking.
How platform engineers can enforce both locking layers as CI gates rather than developer conventions
Platform teams should commit uv.lock, run uv sync --frozen in CI, and reject approved configurations that launch through lockfile-bypassing patterns such as uvx my-server or npx -y my-server. Documentation alone is not an enforcement mechanism.
For the capability gate, teams should snapshot each server at first approval and commit mcplock.json beside its code. Running mcplock verify --lock mcplock.json --fail-on breaking then returns exit code 1 on breaking drift. Container deployments need the same discipline below Python. SHA-256 base-image references and locked dependencies with hashes extend the approved baseline to the image layer.
An inventory record should identify the owner, transport, SDK version, authentication method, credentials held, exposed tools, tools_list_sha256, and approval date. When a hash changes, it should go through re-review before an agent gets access. Platform teams should also treat the environment manifest as the unit of promotion. Declarative generations and atomic rollback allow a failed CVE check to lead back to a known clean environment.
A practical checklist for teams building or inheriting an MCP server Python environment today
The starting sequence is package stabilization, capability approval, and CI enforcement. Initialize the uv project, generate and commit uv.lock, and add uv sync --frozen to every pipeline. Pin the intended MCP SDK version explicitly. Then audit Claude Desktop, Cursor, VS Code, and LangGraph launch settings for unpinned uvx or npx -y invocations, and pin container base images by SHA-256 digest.
Next, snapshot every approved server with mcplock or the equivalent in mcpsum or MCP Sentinel. Commit the capability lockfile and gate breaking changes. Maintain tools_list_sha256 and an approval date in the inventory, route changed hashes to review, and run offline risk checks before production. Record each server’s protocol era so that definitional changes cannot pass as routine updates.
The two lockfiles are complementary: the package lockfile governs what installs, while the capability lockfile governs what the agent is offered. Either one alone leaves a gap in the environment that consumes and executes agent workflows.


