Skip to content

feat(mcp): upgrade Python SDK to 2.0 - #1025

Open
edis-uipath wants to merge 1 commit into
mainfrom
chore/upgrade-mcp-sdk-latest
Open

feat(mcp): upgrade Python SDK to 2.0#1025
edis-uipath wants to merge 1 commit into
mainfrom
chore/upgrade-mcp-sdk-latest

Conversation

@edis-uipath

Copy link
Copy Markdown
Contributor

Summary

  • upgrade the MCP Python SDK from 1.26.0 to 2.0.0 and remove the unused direct langchain-mcp-adapters dependency
  • replace the copied SDK 1.x Streamable HTTP transport with a thin adapter around the SDK 2 transport while preserving UiPath's asynchronous, externally persisted SessionInfo
  • rebuild session recovery around a fresh transport and ClientSession, including a concurrency guard and recovery for expired persisted session IDs
  • migrate MCP model/error APIs to SDK 2 and add real-transport tests for the 2025 protocol versions, session reuse, recovery, retry exhaustion, caching, and disposal
  • document the SDK changes, UiPath-specific transport behavior, and the current legacy/2026 compatibility boundary

Compatibility

The current low-level UiPath client remains compatible with servers negotiating 2025-03-26, 2025-06-18, or 2025-11-25; all three are covered by tests. A server that also supports the legacy initialize flow connects in legacy mode. Strictly modern 2026-07-28 discovery-only servers require a separate adoption of the SDK 2 high-level auto-discovery client and are explicitly documented as outside this upgrade.

Validation

  • uv lock --check
  • uv run ruff check .
  • uv run mypy src/uipath_langchain/agent/tools/mcp
  • uv run pytest -q

All checks pass locally.

Copilot AI review requested due to automatic review settings August 6, 2026 11:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 direct langchain-mcp-adapters) with corresponding lockfile updates.
  • Replace the local forked Streamable HTTP transport with a session-aware adapter around the SDK 2 transport using httpx2 event hooks.
  • Rework session recovery logic (fresh transport + ClientSession replacement) 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.

Comment on lines +86 to +87
client.event_hooks["request"].remove(apply_session_id)
client.event_hooks["response"].remove(capture_session_id)
Comment on lines +304 to +307
if self._connection_stack is not None:
await self._connection_stack.aclose()
self._connection_stack = None
self._session = None
Comment on lines 316 to 320
else:
detail = (
f"MCP server '{server_slug}' returned an error for tool "
f"'{tool_name}': {error.error.message}"
f"'{tool_name}': {error.message}"
)
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
82.5% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube Cloud

Comment thread pyproject.toml
"jsonpath-ng>=1.7.0",
"mcp==1.26.0",
"langchain-mcp-adapters==0.2.1",
"mcp==2.0.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread uv.lock
[[package]]
name = "mcp"
version = "1.26.0"
version = "2.0.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

3 participants