Skip to content

fix(mcp): stdio/unix transports skip initialize handshake (#890)#935

Merged
zmanian merged 1 commit intonearai:stagingfrom
reidliu41:fix/890-mcp-stdio-init
Mar 11, 2026
Merged

fix(mcp): stdio/unix transports skip initialize handshake (#890)#935
zmanian merged 1 commit intonearai:stagingfrom
reidliu41:fix/890-mcp-stdio-init

Conversation

@reidliu41
Copy link
Copy Markdown
Contributor

Summary

fixes #890

  • Always call initialize() before list_tools()/call_tool(), removing the session_manager.is_some() guard that caused stdio/unix clients to skip the MCP protocol handshake entirely
  • Add local AtomicBool flag for idempotent initialization when no session manager is present
  • Fire-and-forget JSON-RPC notifications (id=None) in stdio/unix transports instead of registering a pending response that would block for 30s waiting on a reply that never comes
  • Fix mcp test panic on stdio/unix servers by using create_client_from_config() instead of new_with_config() which asserts HTTP-only transport

Change Type

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI/Infrastructure
  • Security
  • Dependencies

Linked Issue

Validation

  • cargo fmt
  • cargo clippy --all --benches --tests --examples --all-features
  • Relevant tests pass:
  • Manual testing:

Security Impact

Database Impact

Blast Radius

Rollback Plan


Review track:

fixes nearai#890

  - Always call initialize() before list_tools()/call_tool(), removing
    the session_manager.is_some() guard that caused stdio/unix clients
    to skip the MCP protocol handshake entirely
  - Add local AtomicBool flag for idempotent initialization when no
    session manager is present
  - Fire-and-forget JSON-RPC notifications (id=None) in stdio/unix
    transports instead of registering a pending response that would
    block for 30s waiting on a reply that never comes
  - Fix mcp test panic on stdio/unix servers by using
    create_client_from_config() instead of new_with_config() which
    asserts HTTP-only transport
@github-actions github-actions bot added scope: channel/cli TUI / CLI channel scope: tool/mcp MCP client size: M 50-199 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: experienced 6-19 merged PRs labels Mar 11, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical bug in the MCP client where stdio and Unix socket transports were failing to properly initialize due to a conditional guard. The changes ensure that the initialize handshake is always performed, regardless of the presence of a session manager, and introduces an idempotent mechanism to prevent redundant initialization. Furthermore, it optimizes the handling of JSON-RPC notifications by treating them as fire-and-forget, preventing unnecessary blocking. These fixes improve the robustness and reliability of MCP client interactions, particularly for non-HTTP transports, and resolve a related test failure.

Highlights

  • Initialization Logic: Ensured initialize() is always called for MCP clients, removing a guard that caused stdio/unix transports to skip the handshake.
  • Idempotent Initialization: Introduced a local AtomicBool flag for idempotent initialization when no session manager is present.
  • JSON-RPC Notifications: Implemented fire-and-forget handling for JSON-RPC notifications (id=None) in stdio/unix transports to prevent blocking.
  • Test Fix: Corrected a test panic in MCP tests by using create_client_from_config() for non-HTTP transports.
Changelog
  • src/cli/mcp.rs
    • Imported McpProcessManager and create_client_from_config for improved client creation.
    • Updated test_server to use create_client_from_config for unauthenticated clients, resolving a test panic related to HTTP-only assertions.
  • src/tools/mcp/client.rs
    • Added an AtomicBool field initialized to McpClient to track initialization status locally.
    • Modified initialize method to check the local initialized flag for idempotency and to set it upon successful initialization.
    • Removed the session_manager.is_some() guard from list_tools and call_tool to ensure initialize is always invoked.
    • Updated Clone implementation for McpClient to correctly copy the initialized state.
    • Expanded test cases to include a regression test for idempotent stdio client initialization and adjusted an existing test to account for multiple requests during initialization.
  • src/tools/mcp/stdio_transport.rs
    • Implemented fire-and-forget logic for JSON-RPC notifications (requests with id: None) in send_request, preventing the transport from waiting for a non-existent response.
    • Refactored pending.remove calls to use a consistent id variable.
  • src/tools/mcp/unix_transport.rs
    • Implemented fire-and-forget logic for JSON-RPC notifications (requests with id: None) in send_request, preventing the transport from waiting for a non-existent response.
    • Refactored pending.remove calls to use a consistent id variable.
Activity
  • No specific activity (comments, reviews, progress) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request refactors the MCP client initialization to ensure idempotency across all transport types (HTTP, stdio, unix sockets) by introducing an initialized flag in McpClient and adjusting the send_request method to handle JSON-RPC notifications as fire-and-forget. This change also involves using a new client factory function in test_server. However, the review identified two issues: the test_server function leaks child processes for stdio-based servers due to local McpProcessManager creation without proper cleanup, potentially leading to resource exhaustion, and it hardcodes the user_id to "default" when creating clients, which could lead to user isolation issues and potential security vulnerabilities.

// No OAuth and no tokens - try unauthenticated
McpClient::new_with_config(server.clone())
// Use the factory to dispatch on transport type (HTTP, stdio, unix)
let process_manager = Arc::new(McpProcessManager::new());
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.

security-medium medium

A new McpProcessManager is created locally within the test_server function and dropped when the function returns. Since McpProcessManager and StdioMcpTransport do not automatically kill child processes on drop (and kill_on_drop(true) is not used in the transport implementation), every call to test_server for a stdio-based server will leak a running child process, potentially leading to resource exhaustion.

&session_manager,
&process_manager,
None,
"default",
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.

security-medium medium

The user_id argument passed to create_client_from_config is hardcoded to "default", ignoring the user_id parameter provided to the test_server function. While secrets is currently passed as None in this specific call, this pattern breaks user isolation and could lead to incorrect context usage or security bypasses if the factory logic is extended.

References
  1. Tools that interact with user-owned resources, such as sandbox jobs, must verify that the authenticated user ID in the tool context matches the resource owner's ID (e.g., by using a ContextManager) before performing any read or write operations to prevent unauthorized cross-user access.

Copy link
Copy Markdown
Collaborator

@zmanian zmanian left a comment

Choose a reason for hiding this comment

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

Review: fix(mcp): stdio/unix transports skip initialize handshake (#890)

Verdict: APPROVE

Core fix is correct and well-structured across 4 files.

Root cause and fix

The session_manager.is_some() guard meant stdio/unix clients (which don't use session managers) never called initialize(). The fix removes this guard and adds a local AtomicBool for idempotency when no session manager is present. Relaxed ordering is fine -- worst case is a redundant no-op initialize call.

Fire-and-forget notifications

Correctly handles JSON-RPC notifications (no id) as fire-and-forget in both stdio_transport.rs and unix_transport.rs. Per the JSON-RPC spec, notifications expect no response -- the old code registered a pending response that would block for 30s waiting on a reply that never comes.

Other changes

  • cli/mcp.rs test_server now uses create_client_from_config() factory -- correct dispatch for all transport types
  • The id variable extraction removes redundant .unwrap_or(0) calls -- nice cleanup
  • Two good regression tests covering auto-initialization and idempotency

Re: automated review concerns

  1. Child process leak in test_server: McpProcessManager is created locally and dropped when the function returns. This is pre-existing behavior, not introduced by this PR.
  2. Hardcoded user_id = "default": test_server is a local diagnostic CLI command, not multi-user. Fine here.

Clone behavior

The initialized flag in the Clone impl copies the current value, so cloned clients that were already initialized won't re-initialize. This is correct -- they share the same transport and the server has already been initialized.

No blocking issues found.

@reidliu41
Copy link
Copy Markdown
Contributor Author

I see it's already approved — is this good to merge?

@zmanian zmanian merged commit c8cac09 into nearai:staging Mar 11, 2026
9 checks passed
@ironclaw-ci ironclaw-ci bot mentioned this pull request Mar 12, 2026
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
nearai#935)

fixes nearai#890

  - Always call initialize() before list_tools()/call_tool(), removing
    the session_manager.is_some() guard that caused stdio/unix clients
    to skip the MCP protocol handshake entirely
  - Add local AtomicBool flag for idempotent initialization when no
    session manager is present
  - Fire-and-forget JSON-RPC notifications (id=None) in stdio/unix
    transports instead of registering a pending response that would
    block for 30s waiting on a reply that never comes
  - Fix mcp test panic on stdio/unix servers by using
    create_client_from_config() instead of new_with_config() which
    asserts HTTP-only transport
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: experienced 6-19 merged PRs risk: medium Business logic, config, or moderate-risk modules scope: channel/cli TUI / CLI channel scope: tool/mcp MCP client size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stdio MCP transport sends tools/list before initialize handshake

2 participants