Skip to content

MCP Python SDK 2.0 Breaks Unpinned uv-run Servers

TL;DR — A custom MCP server written with a PEP 723 inline script header (# /// script ... dependencies = ["mcp"] ///) and launched via uv run server.py will silently resolve whatever the latest mcp package is at run time. When mcp shipped a breaking 2.0.0 release, every unpinned install started crashing on startup with AttributeError: 'Server' object has no attribute 'list_tools' — even though the exact same script had worked for months on machines that installed it earlier. Fix: pin the dependency (mcp<2.0.0, or whatever major version your code targets) in the script's inline metadata.


Symptom

A local MCP server, launched by Claude Desktop as a stdio child process ("command": "uv", "args": ["run", "/path/to/server.py"]), fails to start. The Claude Desktop MCP log shows something like:

Installed 28 packages in 16ms
Traceback (most recent call last):
  File "/path/to/server.py", line 41, in <module>
    @server.list_tools()
     ^^^^^^^^^^^^^^^^^
AttributeError: 'Server' object has no attribute 'list_tools'
Server transport closed unexpectedly, this is likely due to the process exiting early.

The same server, same script, same command — works fine on other machines that installed it weeks or months earlier.


Why this happens

uv run on a script with PEP 723 inline metadata (the # /// script header block) resolves dependencies fresh, every time it can't find a matching cached environment — which includes the very first run on a new machine. If the script's dependencies list has no version constraint (just ["mcp"]), uv grabs whatever the latest published release is at that moment.

The mcp Python SDK (the official Model Context Protocol server/client library) shipped a major version bump to 2.0.0, which removed the decorator-based tool-registration API (@server.list_tools(), @server.call_tool()) in favor of a different handler-registration model (add_request_handler, etc.). Any server script written against the 1.x API breaks immediately on 2.0.0 — not with a deprecation warning, with a hard AttributeError at import time, because the decorated method no longer exists on the Server class at all.

Machines that installed the server before the 2.0.0 release keep working, because uv's per-script environment cache already has a resolved 1.x environment and won't silently upgrade it on every run. Machines that install (or fully reset the cache) after the breaking release hit it immediately, with no code change on their end.

This is a general trap, not specific to any one server: any unpinned dependency in a PEP 723 script header is a live wire — the script's behavior depends on the state of the package index at the moment uv run happens to (re-)resolve it, which is invisible from reading the code and can change without warning.


Diagnostic chain

  1. Confirm it's a resolution issue, not a code issue. Check whether the exact same script works on another machine. If it does, the code is fine — something about dependency resolution differs.
  2. Check what version actually got installed:
    uv run --with mcp python3 -c "import importlib.metadata; print(importlib.metadata.version('mcp'))"
    
  3. Check the package's release history for a recent major bump:
    curl -s https://pypi.org/pypi/mcp/json | python3 -c "
    import json,sys
    d = json.load(sys.stdin)
    rows = [(f[0]['upload_time'], v) for v, f in d['releases'].items() if f]
    rows.sort()
    for date, v in rows[-20:]: print(date, v)
    "
    
    Look for a major-version jump close to the failure date.
  4. Confirm the API actually changed:
    uv run --with mcp python3 -c "
    from mcp.server import Server
    print([m for m in dir(Server) if not m.startswith('_')])
    "
    
    Compare against what the failing script expects (list_tools, call_tool, etc.).

Fix

Pin the dependency in the script's inline metadata to the last major version the code was written against:

# /// script
# requires-python = ">=3.11"
# dependencies = ["mcp<2.0.0"]
# ///

Verify with a clean run (no traceback, process just sits waiting on stdio as an MCP server should):

cd /path/to/server-dir
(uv run server.py < /dev/null > /tmp/test.log 2>&1 & echo $! > /tmp/test.pid; sleep 4; kill $(cat /tmp/test.pid) 2>/dev/null); cat /tmp/test.log

(No timeout command on stock macOS — the background-and-kill pattern above works without it.)


Prevention

  • Always pin dependencies in PEP 723 script headers, even loosely (mcp<2.0.0, mcp>=1.20,<2.0) — an unpinned dependency in a single-file script is worse than in a normal project, because there's no lockfile to make the resolution reproducible across machines and time.
  • If a fleet of machines runs the same uv-launched script, re-verify it after any machine's uv cache is cleared or the script is freshly installed elsewhere — a pin protects future installs, but doesn't retroactively guarantee every existing install already resolved safely before the breaking release shipped.
  • When debugging "works on machine A, fails on machine B" for identical code, dependency resolution drift (this pattern) should be one of the first things checked — it produces confusing errors that look like a code bug but aren't.