Skip to content

refactor(mcp): migrate from MCP SDK v1 to v2.0.0b1#112

Draft
jonpspri wants to merge 5 commits into
0.1.xfrom
0.1.x-mcp-2.0.0
Draft

refactor(mcp): migrate from MCP SDK v1 to v2.0.0b1#112
jonpspri wants to merge 5 commits into
0.1.xfrom
0.1.x-mcp-2.0.0

Conversation

@jonpspri

@jonpspri jonpspri commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates all MCP client and server code from MCP Python SDK v1 → v2.0.0b1, applying every breaking API change.

Closes: #129

Breaking Changes Applied

v1 v2
streamablehttp_client streamable_http_client
McpError MCPError
mcp.types mcp_types (separate package)
FastMCP MCPServer
SSLCapableFastMCP SSLCapableMCPServer
MCPServer(host=..., port=...) host/port passed to run methods only
streamable_http_client returns (read, write, get_session_id) returns (read, write)

Behavioral Changes

  • Session termination: removed manual __terminate_http_session(); v2 SDK handles DELETE via terminate_on_close=True
  • HTTP client: replaced _http_client_factory pattern with pre-built httpx.AsyncClient
  • Transport security: transport_security now stored as instance attr and passed to streamable_http_app() instead of __init__
  • Config access: self.settings.host/portself.server_config.host/port

Removed

  • _session_id, _get_session_id, _http_client_factory, __terminate_http_session
  • Obsolete test classes (TestTerminateHTTPSession, TestTerminateHTTPSessionErrors, TestHTTPClientFactory)

Tests

All 190 MCP unit tests pass.

Dependencies

  • mcp == 2.0.0b1
  • mcp-types == 2.0.0b1

Migration guide reference

https://py.sdk.modelcontextprotocol.io/v2/migration/

@jonpspri
jonpspri requested review from araujof and terylt as code owners July 3, 2026 23:43
Breaking changes applied:
- streamablehttp_client → streamable_http_client (renamed import)
- McpError → MCPError (exception renamed)
- mcp.types → mcp_types (split into separate package)
- FastMCP → MCPServer (base class renamed)
- SSLCapableFastMCP → SSLCapableMCPServer
- MCPServer.__init__ no longer accepts host/port/transport_security;
  these are passed to run methods and streamable_http_app() instead
- streamable_http_client returns 2-tuple (read, write) instead of 3-tuple;
  session ID tracking and manual termination removed (terminate_on_close=True)
- Remove _http_client_factory; use pre-built httpx.AsyncClient directly
- Replace self.settings.host/port with self.server_config.host/port

Deprecations removed:
- __terminate_http_session(), _session_id, _get_session_id, _http_client_factory

Tests: 190 passing
araujof added 3 commits July 13, 2026 18:37
- Fix spurious whitespace in runtime.py f-strings and dict values
- Replace broken server.settings.host/port doctests with server.server_config.*
- Simplify getattr(_transport_security) to direct attribute access
- Update stale "Fast MCP server" docstring and class Examples indentation
- Update scaffold template to mcp==2.0.0b1 and mcp-types==2.0.0b1
- Remove unused ErrorData import in test_client_reconnect.py
- Remove dead plugin._session_id = None test setup
- Rename http_client_instance to http_client

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
…d http client

- runtime.py: pass host=server_config.host to streamable_http_app(). Without
  it the v2 SDK defaults host to 127.0.0.1 and auto-applies a localhost-only
  DNS rebinding allowlist when transport_security is None, causing HTTP 421 on
  every non-localhost request for servers bound to 0.0.0.0.
- client.py: enter the caller-provided httpx.AsyncClient into the exit stack.
  The v2 SDK does not close a caller-provided client, so it leaked on every
  connection attempt. Entered before the transport so LIFO teardown fires the
  terminate_on_close DELETE while the client is still open.
- test_runtime_coverage.py: set _transport_security on object.__new__ servers
  that bypass __init__, now required after dropping the getattr fallback.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
- Split test_invoke_hook_reconnects_on_session_terminated: the session-terminated
  success path (reconnect then retry succeeds) was dead code shadowed by a second
  plugin setup. Restore it as a real assertion and re-add the separate
  no-reconnect-on-other-errors test that had been merged away.
- Assert streamable_http_client is called with a pre-built http_client instance
  and terminate_on_close=True (the v2 API change and DELETE-on-close behavior
  that replaced the removed __terminate_http_session).
- Assert UDS servers populate _transport_security with DNS rebinding protection,
  non-UDS servers leave it unset, and run_streamable_http_async forwards both
  transport_security and the real bind host to streamable_http_app().

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>

@araujof araujof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good migration overall! Reviewed the v2 migration and pushed fixes for everything actionable. Two real bugs found (both fixed), plus a batch of cleanups and test gaps.

Fixes pushed

Bugs

  • runtime.py — HTTP 421 for non-localhost servers. streamable_http_app() was called without host=, so the v2 SDK defaulted to host="127.0.0.1" and auto-applied a localhost-only DNS rebinding allowlist whenever transport_security was None. Any server bound to 0.0.0.0 would have rejected every non-localhost request with HTTP 421. Now passes host=self.server_config.host. Verified against the real SDK: with host="0.0.0.0" no allowlist is applied; localhost binds stay protected.
  • client.py — httpx client leak. The v2 SDK does not close a caller-provided http_client (streamable_http.py:567 skips it when client_provided=True), so the client created each connection attempt leaked. Now entered into the exit stack right after creation — before the transport — so LIFO teardown fires the terminate_on_close DELETE while the client is still open, then closes the client. No leak on success or failure.

Cleanups:

  • Fixed spurious find-replace whitespace in 6 runtime.py f-strings/dict values.
  • Corrected 8 broken doctests (server.settings.host/portserver.server_config.*; those fields don't exist on MCPServer v2).
  • Simplified getattr(self, '_transport_security', None) to direct attribute access.
  • Updated stale "Fast MCP" docstring text and class Examples: indentation.
  • Updated the plugin scaffold template to mcp==2.0.0b1 + mcp-types==2.0.0b1 (was still on v1, which would have generated plugins incompatible with the v2 client).
  • Removed unused ErrorData import and dead plugin._session_id = None test setup; renamed http_client_instancehttp_client.

Test coverage for the gaps:

  • Split test_invoke_hook_reconnects_on_session_terminated — the session-terminated success path was dead code shadowed by a second plugin setup, so the reconnect-then-retry path was never actually tested. Restored it and re-added the separate no-reconnect-on-other-errors test.
  • Added a test asserting streamable_http_client is called with a pre-built http_client and terminate_on_close=True (the v2 API change + DELETE-on-close behavior that replaced the removed __terminate_http_session).
  • Added tests that UDS servers populate _transport_security with DNS rebinding protection, non-UDS servers leave it unset, and run_streamable_http_async forwards both transport_security and the real bind host to streamable_http_app().

Questions

  1. Dependency pinspyproject.toml pins mcp==2.0.0b1 and mcp-types==2.0.0b1 exactly. In a library's install_requires, an exact pin to a pre-release becomes a hard constraint on every downstream consumer, and once mcp 2.0.0 stable ships, consumers are blocked from upgrading until we cut a new release. Do you want to relax these to a range (e.g. >=2.0.0b1,<3), or is the exact pin intentional for beta stability until GA? I updated the scaffold template to match whatever we land on here.

  2. MIGRATION_MCP_V2.md — the 524-line strategy doc is committed at the repo root. It's useful as a record, but at the root next to README/CHANGELOG a future contributor might read its "planned" sections as outstanding work. Keep it at root, move it under docs/, or drop it now that the migration is done?

@araujof araujof self-assigned this Jul 13, 2026
@araujof araujof added enhancement New feature or request Python 0.1.x labels Jul 13, 2026
@araujof araujof added this to CPEX Jul 13, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in CPEX Jul 13, 2026
@araujof araujof added this to the 0.1.2 milestone Jul 14, 2026
@jonpspri

Copy link
Copy Markdown
Contributor Author

In replay to questions above:

Dependency pins — pyproject.toml pins mcp==2.0.0b1 and mcp-types==2.0.0b1 exactly. In a library's install_requires, an exact pin to a pre-release becomes a hard constraint on every downstream consumer, and once mcp 2.0.0 stable ships, consumers are blocked from upgrading until we cut a new release. Do you want to relax these to a range (e.g. >=2.0.0b1,<3), or is the exact pin intentional for beta stability until GA? I updated the scaffold template to match whatever we land on here.

Yes, the dependency pins are only in place until 2.0.0 is GA, at which point PyPI versioning will take over again and they can be relaxed to a range. We will not push this PR prior to GA.

MIGRATION_MCP_V2.md — the 524-line strategy doc is committed at the repo root. It's useful as a record, but at the root next to README/CHANGELOG a future contributor might read its "planned" sections as outstanding work. Keep it at root, move it under docs/, or drop it now that the migration is done?

Same response. We will remove this document from the PR prior to merge -- the squash will take it out of git history.

I'm switching the PR to draft for now... didn't realize it wasn't that way already.

@jonpspri
jonpspri marked this pull request as draft July 19, 2026 14:58
The b2 release replaces httpx with httpx2 internally. Update the MCP
transport client factory in client.py to use httpx2.AsyncClient and
related types. Adjust the corresponding test mock path accordingly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

0.1.x enhancement New feature or request Python

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

[CHORE]: Migrate MCP client/server from SDK v1 to v2.0.0b1

2 participants