Skip to content

Conversation

@neubig
Copy link
Contributor

@neubig neubig commented Jan 16, 2026

Summary

Simplified fix for MCP session persistence. Keeps MCP connections open after listing tools instead of reconnecting for each tool call.

Changes

  • utils.py: Keep connection open after _list_tools_and_keep_connected()
  • tool.py: Remove async with from executor since client is already connected
  • client.py: Reverted to original simple implementation

Key Insight

The cleanest fix is proper object lifecycle:

  1. Create ONE client per server
  2. Keep it open after listing tools
  3. Close it when done

No reference counting, no session managers, no global state. Just ~100 lines changed instead of 900+.

Test Results

All 58 MCP tests pass.

Live Testing with Playwright MCP

Verified session persistence with the Playwright MCP server (a stateful browser automation server):

  1. Step 1: Agent navigated to https://www.openhands.dev/blog using browser_navigate
  2. Step 2: Agent used browser_snapshot to verify it was still on the same page

Result: ✅ SUCCESS - The browser state was maintained across multiple tool calls. The agent confirmed it was still on https://www.openhands.dev/blog with the correct page content.

This proves the MCP session persistence fix works correctly - without the fix, the browser context would have been lost when the MCP connection was closed and reopened between tool calls.

Fixes: #1739


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:f762432-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-f762432-python \
  ghcr.io/openhands/agent-server:f762432-python

All tags pushed for this build

ghcr.io/openhands/agent-server:f762432-golang-amd64
ghcr.io/openhands/agent-server:f762432-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:f762432-golang-arm64
ghcr.io/openhands/agent-server:f762432-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:f762432-java-amd64
ghcr.io/openhands/agent-server:f762432-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:f762432-java-arm64
ghcr.io/openhands/agent-server:f762432-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:f762432-python-amd64
ghcr.io/openhands/agent-server:f762432-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:f762432-python-arm64
ghcr.io/openhands/agent-server:f762432-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:f762432-golang
ghcr.io/openhands/agent-server:f762432-java
ghcr.io/openhands/agent-server:f762432-python

About Multi-Architecture Support

  • Each variant tag (e.g., f762432-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., f762432-python-amd64) are also available if needed

This PR fixes the issue where MCP HTTP connections create new sessions per
tool call, breaking session-based authentication.

## Changes

### Core Fix: Persistent MCP Connections
- Modified MCPToolExecutor to maintain persistent connections instead of
  opening/closing for each tool call
- Added connection reuse with reference counting in MCPClient
- Added session ID tracking for potential session resumption

### Session Management Infrastructure
- Added mcp_sessions field to ConversationState for session persistence
- Created MCPSessionManager for centralized session tracking
- Added cleanup logic in LocalConversation.close() to release MCP sessions

### Files Changed
- openhands-sdk/openhands/sdk/mcp/client.py - Added session tracking, reentrant context manager
- openhands-sdk/openhands/sdk/mcp/tool.py - Persistent connection in MCPToolExecutor
- openhands-sdk/openhands/sdk/mcp/session_manager.py - New session manager
- openhands-sdk/openhands/sdk/conversation/state.py - Added mcp_sessions field
- openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py - MCP cleanup

### Tests
- Added comprehensive tests using live MCP servers (no mocks)
- Tests verify connection reuse, session stability, and proper cleanup

Fixes #1739

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/mcp
   client.py50296%77–78
   tool.py891583%57, 71–74, 147, 154, 156–158, 224–225, 230, 238, 266
   utils.py40685%24–25, 28, 31, 84–85
TOTAL16339479270% 

openhands-agent and others added 5 commits January 16, 2026 00:33
- Use getattr for dynamic attribute access in MCPClient._capture_session_id
- Add proper type narrowing with isinstance assertions in tests
- Add Literal import for transport type annotation
- Remove unused variable in test

Co-authored-by: openhands <openhands@all-hands.dev>
Keep MCP connections open after listing tools instead of reconnecting
for each tool call. This is the simplest fix for the session-based
authentication issue.

Changes:
- utils.py: Keep connection open after _list_tools_and_keep_connected()
- tool.py: Remove async with from executor since client is already connected
- client.py: Revert to original simple implementation

Key insight: The cleanest fix is proper object lifecycle - create ONE
client per server, keep it open, close it when done. No reference
counting, no session managers, no global state.

Related issue: #1739

Co-authored-by: openhands <openhands@all-hands.dev>
Multiple tools share the same client, so close() may be called multiple
times during LocalConversation.close() cleanup. Added _closed flag to
ensure safe multiple calls.

Co-authored-by: openhands <openhands@all-hands.dev>
This test uses a server that stores state keyed by MCP session ID.
- Without fix: Each tool call creates new session, state is lost
- With fix: Same session is reused, state is preserved

Test confirms the exact issue reported by user (session-based auth breaking).

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
Copy link
Contributor Author

neubig commented Jan 16, 2026

@OpenHands fix pre-commit

@openhands-ai
Copy link

openhands-ai bot commented Jan 16, 2026

I'm on it! neubig can track my progress at all-hands.dev

- Fix line length issues (E501) by breaking long strings
- Add assertions for executor not None to fix pyright type errors
- Remove unused import MCPToolAction
@openhands-ai
Copy link

openhands-ai bot commented Jan 16, 2026

I've fixed the pre-commit issues in PR #1740. The changes have been pushed to the openhands/fix-mcp-session-persistence branch.

Summary of Changes

Fixed pre-commit failures in tests/sdk/mcp/test_stateful_mcp.py:

  1. Line length issues (E501) - Fixed 3 lines that exceeded 88 characters:

    • Line 72: Split long error message string across multiple lines
    • Line 85: Extracted counter value to a variable before using in f-string
    • Line 218: Shortened comment text
  2. Pyright type errors - Fixed 5 type errors where executor could be None:

    • Added assertions (assert ... is not None) for all 4 tool executors before using them in test_multiple_operations_same_session
  3. Unused import - Removed unused MCPToolAction import (auto-fixed by ruff)

All pre-commit checks now pass:

  • ✅ Format YAML files
  • ✅ Ruff format
  • ✅ Ruff lint
  • ✅ PEP8 style check (pycodestyle)
  • ✅ Type check with pyright
  • ✅ Check import dependency rules
  • ✅ Check Tool subclass registration

View full conversation

@neubig neubig marked this pull request as ready for review January 16, 2026 19:44
@neubig neubig requested a review from jpshackelford January 16, 2026 19:47
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

The session persistence fix works (tests pass), but there are concerns about the approach that should be addressed. Key issues: context manager protocol violation and relying on del for cleanup. Details in inline comments.

@neubig
Copy link
Contributor Author

neubig commented Jan 16, 2026

@OpenHands try to reflect the comments above without making the code overly complex.

@openhands-ai
Copy link

openhands-ai bot commented Jan 16, 2026

I'm on it! neubig can track my progress at all-hands.dev

…xt manager support

- Add explicit connect()/disconnect() methods to MCPClient as alternative to
  calling __aenter__/__aexit__ directly (addresses protocol violation concern)
- Add context manager support (__enter__/__exit__) to MCPToolExecutor for
  deterministic cleanup
- Improve exception handling in utils.py to not mask original exceptions
  when cleanup fails
- Improve error message for disconnected client with more context

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 16, 2026

I've addressed the review comments on PR #1740 without making the code overly complex. The changes have been pushed to the openhands/fix-mcp-session-persistence branch.

Summary of Changes

Addressed all 4 review comments from all-hands-bot:

  1. 🟠 Important - Protocol violation fix: Added explicit connect() and disconnect() methods to MCPClient as a cleaner alternative to calling __aenter__() directly. The _list_tools_and_keep_connected() function now uses await client.connect() instead.

  2. 🟡 Context manager support: Added __enter__ and __exit__ methods to MCPToolExecutor for deterministic cleanup. Users can now write:

    with tool.executor as executor:
        # Use tools
        pass  # Automatic cleanup
  3. 🟡 Exception handling: Improved the exception handler in utils.py to wrap client.sync_close() in a try-except block, ensuring the original exception is preserved if cleanup fails.

  4. 🟢 Better error message: Enhanced the RuntimeError message when the client is disconnected to include more debugging context.

Files Changed

  • client.py: +16 lines (added connect() and disconnect() methods)
  • tool.py: +10 lines (added context manager support)
  • utils.py: +7/-7 lines (use connect() method, improved exception handling)

All 61 MCP tests pass and all pre-commit checks pass.

View full conversation

@neubig
Copy link
Contributor Author

neubig commented Jan 18, 2026

@OpenHands double-check the comment by jpshackleford and fix it

@openhands-ai
Copy link

openhands-ai bot commented Jan 18, 2026

I'm on it! neubig can track my progress at all-hands.dev

Address jpshackelford's review comment: move self._closed = True to after
the cleanup operations so that if cleanup fails, future calls to sync_close()
can retry. This is safe because both underlying close methods are idempotent.

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 18, 2026

I've addressed jpshackelford's review comment on PR #1740. The change has been pushed to the openhands/fix-mcp-session-persistence branch.

Summary of Changes

Fixed the sync_close() method in client.py per jpshackelford's review comment:

  • Before: self._closed = True was set before the cleanup operations, which meant if cleanup failed, future calls to sync_close() would return early without retrying.

  • After: self._closed = True is now set after the cleanup operations succeed. This allows retries if cleanup fails, which is safe because both underlying close methods (AsyncExecutor.close() and fastmcp.Client.close()) are idempotent.

Verification

  • ✅ All 61 MCP tests pass
  • ✅ All pre-commit checks pass
  • ✅ Changes pushed to the PR branch

The fix is minimal (4 lines added, 1 line removed) and directly addresses the reviewer's concern about cleanup reliability.

View full conversation

@neubig
Copy link
Contributor Author

neubig commented Jan 19, 2026

@OpenHands please clean up the remaining unresolved comments and resolve them

@openhands-ai
Copy link

openhands-ai bot commented Jan 19, 2026

I'm on it! neubig can track my progress at all-hands.dev

- utils.py: Log warning when MCP client cleanup fails during error handling
- test_stateful_mcp.py: Log exception when server thread encounters an error

This addresses the CodeQL 'empty except' warnings while preserving the
original exception handling behavior.

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 19, 2026

I've addressed the remaining unresolved comments on PR #1740. The changes have been pushed to the openhands/fix-mcp-session-persistence branch.

Summary of Changes

Fixed the two "empty except" CodeQL warnings from github-code-quality[bot]:

  1. openhands-sdk/openhands/sdk/mcp/utils.py (line 99):

    • Changed empty except Exception: pass to log a warning when MCP client cleanup fails during error handling
    • Uses logger.warning("Failed to close MCP client during error cleanup", exc_info=close_exc)
  2. tests/sdk/mcp/test_stateful_mcp.py (line 117):

    • Changed empty except Exception: pass to log the exception when the server thread encounters an error
    • Uses logging.exception("Error in SessionStatefulMCPServer thread")

Verification

  • ✅ All 61 MCP tests pass
  • ✅ All pre-commit checks pass
  • ✅ Changes pushed to the PR branch

The fix is minimal (6 lines added, 3 lines removed) and directly addresses the CodeQL warnings while preserving the original exception handling behavior.

View full conversation

@neubig neubig requested a review from jpshackelford January 19, 2026 18:25
@enyst
Copy link
Collaborator

enyst commented Jan 21, 2026

@OpenHands Do a /codereview-roasted on this PR.

@openhands-ai
Copy link

openhands-ai bot commented Jan 21, 2026

I'm on it! enyst can track my progress at all-hands.dev

@openhands-ai
Copy link

openhands-ai bot commented Jan 21, 2026

You asked for a /codereview-roasted of PR #1740 (“Fix MCP HTTP session persistence across tool calls”). I did a read-through of the diff against origin/main (no code changes made) and delivered a blunt review focused on fundamentals:

  • CRITICAL: The fix keeps connections open but doesn’t define ownership/lifecycle, pushing “remember to close it” onto users via “close any executor when done,” which is a recipe for leaks and surprising breakage across shared tools.
  • CRITICAL: MCPToolExecutor.close() closes a shared client, making it a built-in foot-gun.
  • IMPROVEMENTS: The new integration tests are heavy and potentially flaky (threaded servers, sleep() startup, incomplete shutdown), and mocks are drifting by subclassing MCPClient while skipping its init.
  • STYLE: Minor nit (docstring placement inside function body).

Checklist against your request:

  • Reviewed the PR changes on branch openhands/fix-mcp-session-persistence
  • Provided “roasted” but actionable feedback
  • Did not modify code, commit, or push (none requested for this review-only task)

No further action taken since this was a review request only.

View full conversation

@enyst
Copy link
Collaborator

enyst commented Jan 21, 2026

(OpenHands-GPT-5.2 speaking)

🔴 Taste rating: Needs improvement

You fixed the symptom (session persistence) by… changing who’s responsible for not leaking connections. Congrats, now every caller gets to play “guess when to close the socket”.

This PR is close to good taste (“one client, keep it open”), but the lifecycle/ownership story is still sloppy and the tests are doing a lot of cargo-cult threading to prove something that should be structurally obvious.


[CRITICAL ISSUES] (must fix)

Ownership is unclear → leaks are inevitable

File: openhands-sdk/openhands/sdk/mcp/utils.py (create_mcp_tools, _list_tools_and_keep_connected)

You create one MCPClient in create_mcp_tools() and return a list of tools that share it. Then the docstring effectively says: “Call close() on any tool’s executor when done.”

That’s not an API, that’s a dare.

Why it’s bad

  • Call close() too early: you break other tools silently.
  • Never call it: you leak sessions/threads until process exit.
  • Call it twice: you’re relying on idempotency and best-effort cleanup.

Concrete improvement

  • Return an explicit owner/handle, e.g. (tools, client) or a small MCPToolset object with .tools and .close().
  • Or make create_mcp_tools() itself a context manager (with create_mcp_tools(...) as tools:) so ownership is explicit and impossible to “forget”.

Executor exposes close() that nukes a shared client

File: openhands-sdk/openhands/sdk/mcp/tool.py (MCPToolExecutor.close)

MCPToolExecutor.close() calls self.client.sync_close() even though the client is shared across tools. That’s a foot-gun built into the public surface area.

Why it’s bad
The executor is conceptually “one tool call mechanism”. It should not be the owner of the transport for all tools on that server.

Concrete improvement

  • Remove close() from the per-tool executor, or make it a no-op.
  • Put “close the client” on the shared owner object.
  • If you insist on keeping it: at least rename to close_shared_client() and document loudly that it affects all tools.

connect() calling __aenter__() is still a protocol smell

File: openhands-sdk/openhands/sdk/mcp/client.py (MCPClient.connect)

You wrapped the “don’t call magic methods directly” concern by introducing connect()… which calls __aenter__() anyway.

Why it’s bad
It’s still mixing lifecycle protocols. If the upstream context manager semantics change, this wrapper is now coupled to internals.

Concrete improvement

  • If the underlying library has real connect/open methods, call those.
  • If it doesn’t: your wrapper should implement connection logic, not re-expose __aenter__ under a different name.

[IMPROVEMENT OPPORTUNITIES] (should fix)

Server stop is fake and startup uses sleep()

File: tests/sdk/mcp/test_mcp_session_persistence.py (LiveMCPTestServer)

You call loop.stop(), don’t close the server cleanly, don’t join the thread, and you sleep(0.5) to “wait for server start”.

Why it’s bad
This is nondeterministic and will flake on slow CI.

Concrete improvement

  • Use an Event to signal “server is ready”.
  • Provide a real shutdown mechanism (or at least join the thread with a timeout).

Way too much test to validate a simple invariant

File: tests/sdk/mcp/test_stateful_mcp.py

285 lines to prove “connection isn’t reopened” is… not elegant. It’s integration-y, slow, and brittle.

Concrete improvement

  • Keep one integration test like this, but also add a tight unit-level assertion by instrumenting session creation/transport connect and asserting it happens once per server.

Mocks bypass base init but pretend to be real

Files: tests/sdk/mcp/test_mcp_tool.py, test_mcp_tool_immutability.py, test_mcp_security_risk.py

The tests subclass MCPClient, skip the parent constructor, then manually reintroduce internal attributes.

Concrete improvement

  • Prefer a duck-typed fake implementing the minimal protocol used by the tool layer.
  • Or call the real initializer with a dummy transport.

[STYLE NOTES] (minor)

Docstring placement inside method body

File: openhands-sdk/openhands/sdk/mcp/tool.py (call_tool)

You put a docstring inside the method body. It’s legal, but reads like an accident.

Fix
Move it to the normal docstring position (right under def call_tool).


VERDICT

Needs rework before merge — the persistence idea is fine, but resource ownership is undefined, and undefined ownership is how you get leaks and “why did my tools suddenly stop working?” bugs.

Key insight: You solved persistence by keeping the connection open, but you didn’t solve lifecycle; you just pushed the burden onto the user via “close any executor when done”. That’s not a contract—it’s a trap.

@enyst
Copy link
Collaborator

enyst commented Jan 21, 2026

  • HUMAN Q: I don't understand your criticism. I think the goal of the PR was to keep state per tool?

  • ANSWER: The goal is not “keep state per tool”. The goal (per the issue/PR title) is keep state per MCP server session, shared across all tools that talk to that server.

    Why:

    • In MCP, state lives on the server side and is keyed to a connection/session (e.g., auth token stored in the session, browser context in Playwright MCP, counters, etc.).
    • If you create a new session per tool call (or per tool), you effectively reset that state constantly.
    • Multiple tools from the same MCP server (e.g., browser_navigate, browser_snapshot) must share the same underlying client connection/session or they won’t see each other’s state.

    So the “correct” shape is:

    • 1 client/session per server (shared)
    • many tools bound to that shared client
    • explicit lifecycle for closing that shared client when you’re done

    My review criticism was: the PR moves toward “shared session per server” (good), but the API makes it unclear who owns closing that one shared connection, and it exposes a per-tool executor.close() that can accidentally kill the shared session for all tools.

@neubig
Copy link
Contributor Author

neubig commented Jan 21, 2026

@OpenHands consider this criticism, if the alternative seems reasonable, implement it and make sure that tests still pass and that you can still demonstrate that the playwright MCP based live testing works.

@openhands-ai
Copy link

openhands-ai bot commented Jan 21, 2026

I'm on it! neubig can track my progress at all-hands.dev

Address review criticism about unclear ownership and foot-gun API:

1. Create MCPToolset class that owns the MCP client connection
   - Provides .tools property to access the list of tools
   - Provides .client property for advanced use cases
   - Provides .close() method for explicit cleanup
   - Supports context manager protocol for automatic cleanup

2. Update create_mcp_tools() to return MCPToolset instead of list
   - Clear ownership: the toolset owns the connection
   - Recommended usage: with create_mcp_tools(config) as toolset:
   - Manual usage: toolset = create_mcp_tools(config); try/finally

3. Remove close() from MCPToolExecutor
   - Eliminates the foot-gun that could close shared client
   - Cleanup is now only through MCPToolset.close()

4. Update all tests to use new MCPToolset API
   - Use context manager for automatic cleanup
   - All 61 MCP tests pass

This addresses the key criticism: 'You solved persistence by keeping
the connection open, but you didn't solve lifecycle; you just pushed
the burden onto the user via "close any executor when done".'

Now the API is clear: MCPToolset owns the connection, use it as a
context manager or call close() when done.

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 21, 2026

Summary

I've implemented the review criticism by introducing an MCPToolset class for explicit lifecycle management. The changes have been pushed to the openhands/fix-mcp-session-persistence branch.

Changes Made

1. Created MCPToolset class (utils.py):

  • Owns the MCP client connection with clear ownership semantics
  • Provides .tools property to access the list of tools
  • Provides .client property for advanced use cases
  • Provides .close() method for explicit cleanup
  • Supports context manager protocol (with create_mcp_tools(...) as toolset:)
  • Supports iteration, indexing, and len() for convenience

2. Updated create_mcp_tools() return type:

  • Now returns MCPToolset instead of list[MCPToolDefinition]
  • Clear ownership: the toolset owns the connection
  • Recommended usage: with create_mcp_tools(config) as toolset:

3. Removed close() from MCPToolExecutor:

  • Eliminates the foot-gun that could close the shared client from per-tool executor
  • Cleanup is now only through MCPToolset.close()

4. Updated all tests to use the new API with context managers

Verification

  • ✅ All 61 MCP tests pass
  • ✅ All pre-commit checks pass
  • ✅ Changes pushed to the PR branch

Key Insight Addressed

The review criticism was: "You solved persistence by keeping the connection open, but you didn't solve lifecycle; you just pushed the burden onto the user via 'close any executor when done'."

Now the API is clear:

  • MCPToolset owns the connection
  • Use it as a context manager for automatic cleanup
  • Or call toolset.close() when done manually

The PR is updated at: #1740

View full conversation

@neubig neubig marked this pull request as draft January 21, 2026 15:34
Remove MCPToolset class and consolidate to one connection management approach:

1. create_mcp_tools() returns list[MCPToolDefinition]
2. Tools share a persistent MCP client connection for session state
3. Connection is cleaned up via executor.close() which is:
   - Called by Conversation.close() on all tools automatically
   - Idempotent (safe to call multiple times from shared tools)
   - Also cleaned up via __del__ as fallback

Internal helper _create_stateful_toolset() establishes the persistent
connection and creates tools that share it.

All 61 MCP tests pass. Playwright MCP live testing verified session persistence.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig force-pushed the openhands/fix-mcp-session-persistence branch from 5101547 to 5e50b4b Compare January 21, 2026 15:58
- Add MCPToolset class that owns the MCP client connection
- Support context manager for automatic cleanup
- Make MCPToolset iterable (acts like a list)
- Remove close() from MCPToolExecutor (MCPToolset handles cleanup)
- Update all tests to use context manager pattern

Usage:
    with create_mcp_tools(config) as toolset:
        for tool in toolset:
            # use tool
    # Connection automatically closed

Co-authored-by: openhands <openhands@all-hands.dev>
- Remove unused disconnect() method from MCPClient
- Fix broad exception handling: use BaseException instead of Exception
- Remove redundant is_connected() check after connect()
- Add __del__ to MCPToolset for GC safety
- Restore explanatory comment about MCPToolAction
- Extract MockMCPClient to shared conftest.py
- Consolidate duplicate test servers into reusable MCPTestServer

Reduces code duplication across test files (-182 lines).

Co-authored-by: openhands <openhands@all-hands.dev>
Instead of a separate MCPToolset wrapper class, the MCPClient now:
- Has a 'tools' property containing the list of tools
- Is a sync context manager (with client:)
- Is iterable/indexable like a list

Usage:
    with create_mcp_tools(config) as client:
        for tool in client:  # or client.tools
            ...
    # Connection automatically closed

This eliminates the MCPToolset class entirely (-60 lines).

Co-authored-by: openhands <openhands@all-hands.dev>
The core fix: keep the MCP connection open across tool calls instead of
reconnecting each time (which lost session state like auth tokens).

Changes to client.py:
- Add _tools list and tools property
- Add connect() method
- Add _closed flag for idempotent sync_close()
- Add sync context manager (__enter__/__exit__)
- Add iteration support (__iter__/__len__/__getitem__)

Changes to tool.py:
- Remove 'async with self.client:' - use already-connected client
- Check is_connected() and raise RuntimeError if not

Changes to utils.py:
- New _connect_and_list_tools() that keeps connection open
- create_mcp_tools() returns MCPClient (iterable, so backward compatible)
- Add cleanup on error (sync_close in except blocks)

Usage:
    with create_mcp_tools(config) as client:
        for tool in client:  # client is iterable
            ...
    # Connection closed automatically

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 22, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Agent Server
    • Pre-commit checks

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1740 at branch `openhands/fix-mcp-session-persistence`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Consistent with other MCP tests that define their server fixtures inline.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig marked this pull request as ready for review January 22, 2026 20:48
@neubig neubig requested a review from enyst January 22, 2026 20:49
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

This PR successfully implements MCP session persistence, but there are several issues that need attention, particularly around resource management and API design. See inline comments for details.

- Update MCPClient docstring to clarify lifecycle management
- Return copy of tools list from tools property to prevent modification
- Add logging when sync_close fails during error cleanup

Co-authored-by: openhands <openhands@all-hands.dev>
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.

MCP HTTP connections create new sessions per tool call, breaking session-based auth

6 participants