Skip to content

fix(deps): drop fastmcp for the official mcp SDK; pin urllib3 for CVE fixes#324

Merged
mbektas merged 3 commits into
plmbr:mainfrom
pjdoland:chore/deps-cve-bumps
May 19, 2026
Merged

fix(deps): drop fastmcp for the official mcp SDK; pin urllib3 for CVE fixes#324
mbektas merged 3 commits into
plmbr:mainfrom
pjdoland:chore/deps-cve-bumps

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

@pjdoland pjdoland commented May 19, 2026

Summary

Two related fixes:

  1. NBI's pyproject.toml on main is currently un-installable on a fresh Python 3.14 environment. litellm>=1.83.7 (the floor that picks up CVE-2026-42203 / CVE-2026-42208 / CVE-2026-42271 fixes) hard-pins python-dotenv==1.0.1, and every fastmcp>=2.11.2 release requires python-dotenv>=1.1.0. The constraints are mutually unsatisfiable; pip install -e . reports ResolutionImpossible. Verified in a clean venv.

    This PR drops the fastmcp dep entirely and routes mcp_manager.py through a thin local shim implemented against the official mcp Python SDK, which lists python-dotenv only as an optional cli extra and is already in NBI's dep tree via claude-agent-sdk. Fresh installs succeed.

  2. urllib3 2.6.3 has two open CVEs. Pinned urllib3>=2.7.0 to pick up CVE-2026-44431 (cross-origin redirect handling) and CVE-2026-44432 (streaming API). Reaches NBI via requests, which doesn't cap the upper bound, so a single lower bound in NBI's pyproject is enough.

Solution

fastmcp → mcp swap (notebook_intelligence/mcp_client.py, new file):

  • Thin shim exposing the subset of fastmcp's surface NBI used (Client, StdioTransport, StreamableHttpTransport) implemented on top of the official mcp SDK. Constructor signatures and method shapes match fastmcp's so the call sites in mcp_manager.py change in two lines (the two imports).
  • Return values are unchanged: the underlying Pydantic models are the same in both libraries, since fastmcp itself is a thin layer over mcp.
  • pyproject.toml: fastmcp>=2.11.2mcp>=1.27.0. The python-dotenv resolution now lands on 1.0.1 (forced by litellm; CVE-2026-28684 affects only set_key / unset_key write paths which neither NBI nor any other dep exercises).

urllib3 pin (pyproject.toml):

  • One added line: "urllib3>=2.7.0". Comment explains the transitive path through requests and which CVEs the bump addresses.

Testing

  • tests/test_mcp_client_shim.py (5 new cases): constructor parity, programmer-error guards (methods-before-context, unsupported transport), and an end-to-end smoke test against a real stdio MCP server (spawns a subprocess, drives ping / list_tools / call_tool / list_prompts / get_prompt, asserts the exact attribute paths mcp_manager.py reads off the responses). Plus a cleanup-on-failure case that pins the stdio subprocess unwinds cleanly when initialization fails.
  • Full pytest suite: 1012 passed. TS gates clean. End-to-end stdio test runs in under a second.
  • Fresh-venv install verification: python -m venv /tmp/fresh && /tmp/fresh/bin/pip install -e . on Python 3.14 now succeeds. Previously: ResolutionImpossible.

Why this is the right temporary fix

The principled fix lives upstream in BerriAI/litellm#25231 (relax the python-dotenv pin from ==1.0.1 to >=1.0.1). That PR has been open since 2026-04-06 with no maintainer engagement, and a parallel CVE-fix bump (#26435) was closed without merge. NBI can't ship a working install while waiting on upstream.

The shim is small (~120 lines), tested end-to-end, and the docstring + pyproject comment both point at the upstream PR to revert to once it lands. The official mcp SDK is a stable Anthropic-maintained library, not a vendored fork — there's no maintenance burden beyond keeping the shim's method signatures aligned with mcp's public API.

Tracked upstream:

Risks

  • None practically expected. The full pytest suite stays green. The end-to-end shim test exercises every method mcp_manager.py calls. JupyterLab's MCP integration paths haven't been touched.
  • The shim is a new module to learn; the docstring lays out exactly why it exists and how to revert. Two imports in mcp_manager.py change shape; the consumer behavior is byte-for-byte identical.

…44432

`pip-audit` against the installed dep graph reports two open
urllib3 CVEs in 2.6.3:

- CVE-2026-44431 (GHSA-qccp-gfcp-xxvc): when following cross-origin
  redirects via urllib3's high-level APIs, the redirect handling
  could leak credentials or headers across origins.
- CVE-2026-44432 (GHSA-mf9v-mfxr-j63j): the streaming API mishandles
  some I/O states.

Both are fixed in urllib3 2.7.0. urllib3 reaches NBI transitively
through `requests` (tiktoken's dep), and neither `requests` nor
tiktoken caps the urllib3 upper bound, so a >=2.7.0 lower bound
here is enough to pull the fix on fresh installs.

Documents the remaining open litellm CVE (CVE-2026-40217, fix in
1.83.10) inline next to the existing pin. 1.83.10 has not yet been
released to PyPI; the vulnerable endpoint is in the litellm proxy
server which NBI does not run, so the gap is latent-not-exposed.
Re-bump the lower bound when 1.83.10 ships.

JS-side `yarn npm audit` reports 21 advisories, all transitive
through `@jupyterlab/*`, `webpack`, `eslint`, and `jest`. None are
NBI's direct deps; pinning them via yarn `resolutions` would risk
breaking JupyterLab compatibility. Tracked as a follow-up for the
maintainer to decide on a per-advisory basis after the upstream
JupyterLab packages release fixed versions.

Verified: `pip-audit` after `pip install 'urllib3>=2.7.0' --upgrade`
shows the two CVEs cleared. Full test suite green (1007 passed).
@pjdoland pjdoland added this to the 5.0.x milestone May 19, 2026
@pjdoland pjdoland added the bug Something isn't working label May 19, 2026
NBI's pyproject was un-installable on a fresh Python 3.14 because
`litellm>=1.83.7` (the floor that picks up CVE-2026-42203 /
CVE-2026-42208 / CVE-2026-42271 fixes) hard-pins
`python-dotenv==1.0.1`, and every `fastmcp>=2.11.2` release requires
`python-dotenv>=1.1.0`. Mutually unsatisfiable; pip reports
`ResolutionImpossible`. Upstream litellm has closed a CVE-fix bump
(BerriAI/litellm#26435) and stalled on a pin-relaxation PR
(BerriAI/litellm#25231) for ~7 weeks with no signal of movement.

This commit drops the `fastmcp` dep entirely and routes
`mcp_manager.py` through a thin local shim
(`notebook_intelligence/mcp_client.py`) implemented against the
official `mcp` Python SDK. The SDK lists `python-dotenv` only as an
optional `cli` extra, so it can coexist with litellm's hard
`==1.0.1` and the resolver succeeds. `mcp` was already in NBI's
dep tree (transitively via `claude-agent-sdk`); promoted to a
direct `>=1.27.0` pin so the version is anchored.

The shim exposes the small fastmcp surface NBI used (`Client`,
`StdioTransport`, `StreamableHttpTransport`) with constructor
signatures and method shapes identical to fastmcp's. Return values
are the underlying mcp Pydantic models — the same shapes fastmcp
returned, since fastmcp itself is a thin layer over the SDK.
mcp_manager.py's two imports change; nothing else in the consumer
moves.

Tests (`tests/test_mcp_client_shim.py`, 5 cases):
- Constructor field-name parity for both transports.
- Methods-before-context-entry surface as RuntimeError.
- Unsupported transport types surface as TypeError.
- End-to-end against a real stdio MCP server: ping, list_tools,
  call_tool, list_prompts, get_prompt. Asserts the exact attribute
  paths mcp_manager.py reads (tool.name / tool.inputSchema /
  prompt.arguments[i].required / get_prompt_result.messages[i].
  content.text). A future SDK refactor that returns an envelope
  where a list was expected (or vice versa) flunks the test rather
  than silently breaking the consumer.
- Context cleanup on initialize failure: the stdio subprocess
  unwinds cleanly when the server exits before the handshake.

Verification: fresh `python -m venv && pip install -e .` on
Python 3.14 succeeds (previously failed with ResolutionImpossible).
Full pytest suite: 1012 passed. TS gates clean. The shim's
end-to-end test runs the real stdio handshake in <1s.

Temporary in spirit: once BerriAI/litellm#25231 lands and the
fastmcp clients NBI used become installable alongside it again,
this shim can revert to a direct `fastmcp` import and the local
file can be deleted. Both the new module's docstring and the
pyproject comment point at that upstream PR.
@pjdoland pjdoland changed the title chore(deps): pin urllib3>=2.7.0 for two open CVEs fix(deps): drop fastmcp for the official mcp SDK; pin urllib3 for CVE fixes May 19, 2026
Three fix-before-merge items from the parallel review pass.

1. Test-arch flagged that no async test in the repo has a timeout.
   ``mcp_client.py``'s stdio-subprocess tests rely on the underlying
   ``mcp`` SDK's pipes; if a pipe ever deadlocks, the test would
   hang forever and CI would kill the whole job rather than
   surfacing the specific test. Added ``pytest-timeout`` to the test
   extras and a 30s default via ``[tool.pytest.ini_options].timeout``
   so any hang surfaces as a per-test failure with a stack trace.

2. Test-arch and async-resource both flagged that
   ``test_context_cleanup_on_initialize_failure`` only asserted
   ``pytest.raises(Exception)``, which would still pass if the shim
   silently leaked the stdio subprocess. The whole point of that
   test is the cleanup path. Now uses ``psutil`` to enumerate the
   pytest process's children, filters to those running the broken
   server script, and asserts the list is empty before and after the
   failing connect. A 2-second poll budget covers OS-level SIGCHLD
   lag; in practice the assertion resolves under 50ms locally. If
   the shim regresses to leaking subprocesses, the test now fails
   loudly with the leaked PIDs.

3. Code-reuse and code-quality both flagged the ``_current_exc()``
   helper as one-line indirection over ``sys.exc_info()`` with one
   caller. Inlined and lifted the ``sys`` import to module top.
   Moved the explanation (why we forward exc_info to the exit-stack
   cleanup) to the comment at the call site.

Deferred follow-up items the reviewers flagged, none security- or
correctness-relevant:
- Drop ``StdioTransport`` dataclass and let ``Client`` accept
  ``StdioServerParameters`` directly. Code-reuse win at the eventual
  revert, not worth churn while the shim is temporary.
- Move ``_SERVER_SCRIPT`` to ``tests/fixtures/mcp_echo_server.py``
  for editor-side syntax highlighting and import-time validation.
- Parametrize ``_require_session`` coverage across all five methods
  (currently only ``list_tools`` is tested for the not-connected
  guard).
- HTTP transport (``streamablehttp_client``) is exercised by the
  constructor test but not end-to-end. Would need a mock ASGI
  server to add cleanly; NBI's own usage is stdio-dominant.
- Document the env=={} -> None collapse at mcp_client.py for
  callers wanting clean-env semantics.
- Reference counting on the ``Client`` if reentrant context use
  ever becomes a live code path (today it's dead in mcp_manager).
@mbektas mbektas merged commit 3163488 into plmbr:main May 19, 2026
4 checks passed
pjdoland added a commit to pjdoland/notebook-intelligence that referenced this pull request May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22
and expands it to cover everything merged into upstream/main after
PR plmbr#287's docs refresh. Bumps package.json to 5.0.0.

CHANGELOG additions cover the post-plmbr#287 surface:

- Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace
  details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286),
  JSON-paste path in Add MCP server (plmbr#285).
- Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode
  (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332).
- Chat sidebar and agentic UX: workspace @-mention in Claude mode
  (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt
  away from over-eager notebook creation (plmbr#336).
- Skills: multi-manifest support (plmbr#321), tracks-upstream for user-
  imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291).
- Accessibility: full sub-section covering plmbr#305-plmbr#320.
- Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323),
  0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP
  config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS
  auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK
  swap (plmbr#324).
- Fixed: session listing unification (plmbr#310), session preview unwrap
  (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler
  leak (plmbr#294).
- Removed: fastmcp dependency, history.jsonl session gate.

Adds a Migration note covering the five behavior changes operators
should review before upgrading from 4.x: fastmcp swap, path
sandboxes, history.jsonl gate removal, workspace @-mention pointer
shape, and the Copilot WebSocket auth/origin tightening.

Two reviewer rounds (six personas each) applied:
- Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the
  plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes,
  and the stale `fastmcp==2.x.*` recommendation in the admin guide.
- Round 2 caught the missing plmbr#301 migration bullet, missing version-
  matrix 5.0.x row, missing README TOC entry, and a couple of style
  nits (sub-heading overpromise, orphan bullet).

Skipped (deferred to future PRs):
- README first-run tour mention.
- Admin guide HTTP kill-switch row in Failure-modes table.
- Terminal drag-drop trust-model precision update after plmbr#327.
- Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not
  AES-GCM).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants