feat(mcp): upgrade Python SDK to 2.0 - #1025
Conversation
There was a problem hiding this comment.
Pull request overview
Upgrades the MCP Python SDK integration in uipath_langchain.agent.tools.mcp to SDK 2.0.0, replacing the previously copied Streamable HTTP transport with a thin adapter and updating the client/tool layers (plus tests/docs) to the new SDK APIs while preserving UiPath’s externally persisted SessionInfo behavior.
Changes:
- Bump dependency pin to
mcp==2.0.0(and remove directlangchain-mcp-adapters) with corresponding lockfile updates. - Replace the local forked Streamable HTTP transport with a session-aware adapter around the SDK 2 transport using
httpx2event hooks. - Rework session recovery logic (fresh transport +
ClientSessionreplacement) and update tests/docs to exercise real SDK 2 transport behavior.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Updates resolved dependency graph for MCP SDK 2.0 (httpx2, mcp-types, etc.) and removes langchain-mcp-adapters. |
pyproject.toml |
Pins mcp==2.0.0 and drops the direct langchain-mcp-adapters dependency. |
src/uipath_langchain/agent/tools/mcp/streamable_http.py |
Replaces ~800-line fork with a small adapter that syncs mcp-session-id via httpx2 request/response hooks. |
src/uipath_langchain/agent/tools/mcp/mcp_client.py |
Migrates to SDK 2 (MCPError, httpx2) and replaces recovery with “close connection + reopen transport/session” semantics. |
src/uipath_langchain/agent/tools/mcp/mcp_tool.py |
Updates SDK 2 model field names (input_schema/output_schema) and MCP error mapping. |
src/uipath_langchain/agent/tools/mcp/claude.md |
Updates implementation documentation for the new adapter/recovery model. |
tests/agent/tools/test_mcp/test_mcp_client.py |
Replaces prior mocks with real SDK 2 transport over httpx2.MockTransport to validate negotiation/recovery/cache/disposal. |
tests/agent/tools/test_mcp/test_mcp_tool.py |
Updates imports/types for SDK 2 and aligns schema assertions with snake_case fields. |
tests/agent/tools/test_mcp/claude.md |
Updates test strategy documentation for the new endpoint + transport mocking pattern. |
docs/mcp-sdk-2-upgrade.md |
Adds a focused upgrade review doc explaining SDK 2 changes, compatibility boundaries, and recovery behavior. |
| client.event_hooks["request"].remove(apply_session_id) | ||
| client.event_hooks["response"].remove(capture_session_id) |
| if self._connection_stack is not None: | ||
| await self._connection_stack.aclose() | ||
| self._connection_stack = None | ||
| self._session = None |
| else: | ||
| detail = ( | ||
| f"MCP server '{server_slug}' returned an error for tool " | ||
| f"'{tool_name}': {error.error.message}" | ||
| f"'{tool_name}': {error.message}" | ||
| ) |
| "jsonpath-ng>=1.7.0", | ||
| "mcp==1.26.0", | ||
| "langchain-mcp-adapters==0.2.1", | ||
| "mcp==2.0.0", |
There was a problem hiding this comment.
Restore compatibility with langchain-mcp-adapters before pinning MCP 2.0. The repository’s own simple-local-mcp integration resolves langchain-mcp-adapters==0.3.1 with this pin, then fails at import because the adapter imports RequestContext removed by MCP 2. I reproduced the same failure with uv run --with langchain-mcp-adapters==0.3.1 ..., and the alpha/cloud/staging integration jobs all fail on it. The adapter metadata only says mcp>=1.24.0, so dependency resolution cannot protect downstream users. Land a compatible adapter/migration for this supported path, or prevent the incompatible combination from resolving.
| async with sdk_streamable_http_client( | ||
| url, | ||
| http_client=client, | ||
| terminate_on_close=terminate_on_close, |
There was a problem hiding this comment.
Preserve the full restored-session lifecycle instead of only injecting its ID into requests. The SDK transport delegated to here keeps a separate in-memory session_id; when SessionInfo starts with a persisted ID and initialization is skipped, that SDK field remains None. I reproduced a successful tool call followed by disposal with terminate_on_close=True: no DELETE was sent (delete_count == 0) and the persisted ID remained set, whereas the replaced transport terminated through SessionInfo. The fresh ClientSession also has no adopted negotiation state, so the restored-session request carried no mcp-protocol-version. Explicitly hydrate/adopt the required session state or implement termination/version handling for restored IDs, and cover both the protocol header and DELETE path.
| if self._session_info: | ||
| await self._session_info.set_session_id(None) | ||
| await self._initialize_session() | ||
| await self._open_connection() |
There was a problem hiding this comment.
Keep the client recoverable when opening the replacement connection fails. I reproduced _open_connection() raising MCPError(CONNECTION_CLOSED, ...) here: _open_connection clears _session, but _client_initialized remains True; every later operation then fails in _ensure_session() with RuntimeError("MCP client initialized without a session") instead of rebuilding the client. The handshake failure also escapes from inside the retry handler, so the configured retry loop does not repair this state. Reset the initialized state on reopen failure or make _ensure_session() rebuild when the session is absent, with a focused failed-recovery-then-next-call test.
| [[package]] | ||
| name = "mcp" | ||
| version = "1.26.0" | ||
| version = "2.0.0" |
There was a problem hiding this comment.
Bump the uipath-langchain package version and regenerate the lockfile with that version. This PR changes runtime session/recovery behavior and takes a new major MCP dependency, but pyproject.toml and the local-package lock entry still publish 0.15.3. Every merge publishes immediately, so leaving the package version unchanged makes these changes ride another release and prevents an independent rollback.
43b7ac8 to
ecbeb94
Compare
2332347 to
a4ac560
Compare
14ab75c to
33d22a2
Compare
Upgrade the MCP Python SDK from 1.26.0 to 2.0.0 and replace the copied SDK 1.x
Streamable HTTP transport with a thin adapter over the upstream one, cutting
`streamable_http.py` from roughly 800 lines to 130. Fixes and new protocol
behavior upstream now arrive without hand-merging a fork.
Preserve UiPath's externally persisted `SessionInfo` on top of the upstream
transport: MCP 2 removed the transport's `get_session_id` callback, so two
`httpx2` event hooks carry the behavior instead -- one loading the persisted ID
onto each request, one persisting an ID the server returns. Restored sessions
are validated and adopted without a second initialize, and terminated on
disposal when configured.
Make session recovery correct under concurrency and failure. MCP 2 makes
`ClientSession.initialize()` idempotent, so recovery replaces the transport and
session rather than re-initializing, guarded so a late failure from a
superseded session cannot tear down its replacement. Closing a failed
connection is best-effort, and a failed replacement no longer poisons the
client: the next operation reopens. Recover from the SDK's canonical
`Session not found` response, and disambiguate the bare HTTP 404 that a
restored-but-stale session ID produces.
Retain compatibility with the previously accepted `httpx.Timeout` API by
converting all four phase values for the MCP 2 transport, and declare a
directly tested, bounded `httpx2` dependency.
Replace the incompatible `langchain-mcp-adapters` dependency -- it imports
`RequestContext`, which MCP 2 removed -- with a tested first-party
session-to-LangChain tool converter, and migrate the local and remote samples
to MCP 2 public APIs.
Add `testcases/simple-http-mcp`, an integration testcase hosting MCP over
Streamable HTTP on real sockets. It drives UiPath's own adapter rather than the
raw SDK transport, covering what the unit tests can only reach through
`httpx2.MockTransport`: a genuine `MCPServer` at 2025-11-25, a pinned endpoint
at 2025-06-18, a modern-only 2026-07-28 server asserted to fail, and the exact
API surface `uipath-agents-python` depends on, including persisted-session
resume. It is LLM-free, so the matrix carries no model nondeterminism.
Add unit tripwires pinning the external SDK constraints that decide what this
client can negotiate, each naming the follow-up it unblocks when it fails.
BREAKING CHANGE: `McpClient` is a public low-level API that deliberately keeps
returning the MCP SDK's raw result models, and SDK 2.0 renamed their Python
attributes to snake case. Callers that read them directly must update:
CallToolResult.isError -> is_error
CallToolResult.structuredContent -> structured_content
Tool.inputSchema -> input_schema
Tool.outputSchema -> output_schema
ListToolsResult.nextCursor -> next_cursor
The wire format is unchanged: these became snake_case fields carrying the old
names as serialization aliases, so `model_dump(by_alias=True)` still emits
camelCase. Note that plain `model_dump()` now emits snake_case keys, which is
the one form of this break that fails silently rather than raising.
Removing the vendored transport also drops the unused public names it carried,
including `streamablehttp_client`, `StreamableHTTPTransport`, `RequestContext`,
`StreamableHTTPError` and `ResumptionError`.
Strictly modern 2026-07-28 discovery-only support remains a separate follow-up:
it needs the high-level client, and it forces a decision about what
`SessionInfo` means once session IDs are dropped.
Co-Authored-By: Ion Mincu <ion.mincu@uipath.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33d22a2 to
426c5f3
Compare
|



Summary
1.26.0to2.0.0and replace the copied SDK 1.x Streamable HTTP transport with a thin SDK 2 adapterSessionInfo, including restored-session protocol adoption, version headers, idempotent persistence, termination, and recovery from the SDK server's canonicalSession not foundresponsehttpx.TimeoutAPIlangchain-mcp-adapterssample dependency with a tested first-party active-session tool converter, and migrate local and remote samples to MCP 2 public APIshttpx2dependency and release the public raw-model attribute changes asuipath-langchain0.17.0Compatibility
The low-level UiPath client remains compatible with servers negotiating
2025-03-26,2025-06-18, or2025-11-25; all three versions are covered by real-transport tests. Externally restored sessions are validated and adopted without another initialize request, then terminated on disposal when configured.A server that also supports the legacy initialize flow connects in legacy mode. Strictly modern
2026-07-28discovery-only support remains a separate, uncommitted follow-up layer and is not part of this PR.Migration guide
McpClientintentionally continues to return the MCP SDK's raw result models. Their Python attributes changed from camelCase to snake_case in SDK 2, so this PR bumps the package to0.17.0and documents the exact mappings.Validation
uv lock --checkuv run just lintuv run ruff format --check .uv run mypy --config-file pyproject.toml .uv run pytest— 2,672 passeduv run pytest -o addopts='' tests/agent/tools/test_mcp— 79 passedsimple-local-mcpdependency resolution dry run withoutlangchain-mcp-adaptersuv build— 0.17.0 sdist and wheelorigin/main— 93%Development Package
uipath pack --nolockto get the latest dev build from this PR (requires version range).