Skip to content

Cap mcp below 2.0 so fresh installs start - #317

Closed
alex-clickhouse wants to merge 1 commit into
mainfrom
alex-clickhouse/pin-mcp-below-2
Closed

Cap mcp below 2.0 so fresh installs start#317
alex-clickhouse wants to merge 1 commit into
mainfrom
alex-clickhouse/pin-mcp-below-2

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Fixes #316.

What

Declares mcp explicitly in pyproject.toml with an upper bound: mcp>=1.23,<2.
One dependency line plus the comment explaining why the bound exists and when to
lift it. Nothing else changes.

Why

mcp 2.0.0 removed the module-level request_ctx contextvar from
mcp.server.lowlevel.server. nerve/mcp_server/http.py imports it at module
scope (line 32, used in _resolve_client_info and
_bound_identity_from_request), so a fresh install dies at startup with the
ImportError in the issue.

mcp was never declared in pyproject.toml — it arrived transitively through
claude-agent-sdk, whose constraint is mcp<3.0.0,>=1.23.0. Nothing else in
the tree caps it, so once 2.0.0 landed on PyPI (2026-07-28, four minutes after
1.29.0) every fresh resolve picked it up.

Declaring it explicitly is also the right hygiene independent of this bug: three
modules under nerve/mcp_server/ import mcp directly, and an upper bound on an
otherwise-transitive dependency can only be expressed by declaring it. The floor
mirrors claude-agent-sdk's own >=1.23.0, so nothing is narrowed except the
top end.

Why CI never caught it

.github/workflows/ci.yml keys its uv cache on pyproject.toml
(cache-dependency-glob). The restored cache carried stale index metadata, so
runs kept resolving the old version — run 32061993270
(main, 2026-08-17, green) installed mcp==1.29.0 three weeks after 2.0.0 shipped.
CI was green on stale metadata while every genuinely fresh install broke.

Usefully, touching pyproject.toml changes that cache key — so this PR's own run
performs a real resolve rather than restoring the stale one.

That blind spot is worth closing separately (dependency drift stays invisible
until pyproject.toml happens to change), but it's a CI-config change and not
this PR's job.

Testing

Verified both directions in a throwaway Python 3.13 venv, resolving with
--refresh so no stale index metadata could mask the result.

With the bound — resolves mcp==1.29.0, the newest 1.x:

  • uv pip install -e ".[test]" --refreshmcp==1.29.0 (and no longer pulls the 2.x-only mcp-types)
  • from mcp.server.lowlevel.server import request_ctx → OK
  • import nerve.gateway.server (the nerve start path) → OK
  • pytest tests/ -q3303 passed in 107s

Negative control, forcing mcp==2.0.0 into the same venv to confirm the
bound is load-bearing rather than cosmetic:

  • import nerve.mcp_server.http → reproduces the issue's ImportError exactly
  • import nerve.gateway.server → same ImportError, i.e. startup is what breaks
  • pytest tests/ --co → 8 collection errors (test_mcp_loopback, test_mcp_session_binding, test_satellite_sessions, test_server_periodic_loops, …)

Frontend is untouched by a Python dependency change, so npm run build was not
run locally; CI covers it.

Follow-up

This is a stopgap. claude-agent-sdk already allows mcp<3, so the real work is
porting nerve/mcp_server/ from request_ctx to the 2.x request-context API
(2.0 keeps a ServerRequestContext class but no module-level contextvar). The
comment on the pin says as much so the bound doesn't quietly become permanent.

🤖 Generated with Claude Code

mcp 2.0.0 removed the module-level `request_ctx` contextvar from
mcp.server.lowlevel.server. nerve/mcp_server/http.py imports it at module
scope, so a fresh install fails at startup with

    ImportError: cannot import name 'request_ctx'
    from 'mcp.server.lowlevel.server'

mcp was never declared in pyproject.toml — it arrived transitively via
claude-agent-sdk, whose constraint is `mcp<3.0.0,>=1.23.0`. Nothing else in
the tree caps it, so once 2.0.0 landed on PyPI every fresh resolve picked it
up. Declaring mcp explicitly is also the correct hygiene here: three modules
under nerve/mcp_server/ import it directly, and an upper bound on an
otherwise-transitive dependency can only be expressed by declaring it.

CI stayed green through this because .github/workflows/ci.yml keys its uv
cache on pyproject.toml; the restored cache carried stale index metadata, so
runs kept resolving 1.29.0 while genuinely fresh installs broke. Touching
pyproject.toml changes that key, so this PR's own run does a real resolve.

The bound is a stopgap, not the destination — claude-agent-sdk already
allows mcp<3, so porting nerve/mcp_server/ to the 2.x request-context API
is separate follow-up work.

Fixes #316

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alex-clickhouse

Copy link
Copy Markdown
Collaborator Author

CI result, confirming the fresh-resolve claim above with the run log rather than inference:

Set up uv   Trying to restore uv cache from GitHub Actions cache with key:
            setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-0f1311bf22…
Set up uv   No GitHub Actions cache found for key: setup-uv-1-…-0f1311bf22…
Install     Resolved 120 packages in 1.44s
Install      + mcp==1.29.0

So run 32243134290
resolved on a cold uv cache — setup-uv keys exactly, with no restore-keys
prefix fallback, so changing pyproject.toml produced a true miss rather than a
stale restore. That makes this run the same code path the issue reporter hit on a
fresh install, and it picked mcp==1.29.0:

  • Backend tests (Python 3.13) — pass, 1m45s
  • Frontend build (Vite) — pass, 35s

Worth stating plainly for the record: green CI on its own could not have
distinguished the bound working from a stale cache handing back 1.29.0 anyway,
since 1.29.0 is the correct answer either way. The cold-cache log line is what
makes it real evidence, alongside the local --refresh negative control in the
description (no bound → mcp==2.0.0 → the issue's ImportError).

@alex-clickhouse

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #319, which does the real port. This cap was only ever a
stopgap, and with the cut-over to mcp>=2 it has no future — keeping it open would
just mean merging a constraint that the very next PR removes.

Worth noting what closing this actually required, because it wasn't only the PR:
the cap commit 0b90148 was in the history of #318, #319 and #320, so it would
have shipped to main regardless (nerve merges rather than squashes). I've rebuilt
all three branches on top of the port instead, so that commit is gone entirely and
each PR now shows only its own change. Fixes #316 moved from this commit to
#319, which is now the thing that actually closes the issue.

The trade-off to be aware of: main stays broken for fresh installs until #319
merges, where merging this would have fixed it in one line today. That's the right
call given #319 is verified and green — but if the port stalls in review, this is
worth reviving rather than leaving users on a broken install.

@alex-clickhouse
alex-clickhouse deleted the alex-clickhouse/pin-mcp-below-2 branch August 19, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fresh install broken by mcp 2.0.0 — ImportError: cannot import name 'request_ctx' from 'mcp.server.lowlevel.server'

1 participant