mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
b45d9663ec
* fix(python): expose client capabilities to MCP servers Add list_roots_callback support to Python connectors to properly advertise the roots capability during MCP initialization handshake. Previously, the Python library was not exposing any client capabilities because the list_roots_callback was not being passed to ClientSession. This fix ensures the roots capability is always advertised, bringing the Python library in line with the TypeScript implementation. Changes: - Add roots and list_roots_callback params to BaseConnector - Add get_roots() and set_roots() methods for managing roots - Pass list_roots_callback to ClientSession in all connectors - Export Root type from connectors module - Update unit tests to expect list_roots_callback parameter Closes MCP-1063 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(python): add roots and list_roots_callback params to connectors - Add roots and list_roots_callback parameters to StdioConnector, HttpConnector, and SandboxConnector constructors - Add comprehensive unit tests for client capabilities exposure - Tests verify roots capability, get_roots(), set_roots(), and that all callbacks are correctly passed to ClientSession 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: fix import sorting and update API documentation - Fix import sorting in test_client_capabilities.py (ruff) - Regenerate API documentation for updated connector classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(python): add list_roots() method to server Context Allow MCP servers to request the list of roots from connected clients via ctx.list_roots(). This follows the same pattern as ctx.sample() and ctx.elicit() for server-to-client requests. * feat(python): add roots params to MCPClient and config - Add roots and list_roots_callback parameters to MCPClient.__init__, from_dict(), and from_config_file() methods - Update create_connector_from_config() to pass roots params to all connector types (Stdio, HTTP, Sandbox) - Allows clients to configure roots at the MCPClient level and have them propagate to all sessions * test(python): add get_client_roots tool to primitive server Add a tool that demonstrates the roots capability by requesting and returning the list of roots from the connected client. Used for integration testing of the roots feature. * test(python): add integration tests for roots capability Add comprehensive integration tests for the roots feature: - test_roots_capability: Verify server can request/receive roots - test_roots_empty_by_default: Verify empty roots when not provided - test_roots_with_custom_callback: Verify custom callback precedence - test_set_roots_updates_server_view: Verify dynamic root updates * test(python): update unit tests for roots parameters Update mock assertions in test_client.py to expect the new roots and list_roots_callback parameters in create_connector_from_config calls. * ci(python): add roots to primitive tests and README badge - Add roots to CI workflow primitive test matrix - Add roots to PR comment report primitives list - Add Roots badge to README Primitives row * feat(docs): enhance MCP client and server documentation for roots feature - Added `roots` and `list_roots_callback` parameters to MCP client initialization documentation. - Updated `create_connector_from_config` documentation to include new parameters. - Introduced `list_roots` method documentation in server context, detailing its usage and return values. - Improved overall clarity and completeness of API reference for better developer guidance. * feat(python): add get_client_roots tool to context example - Introduced a new tool, `get_client_roots`, to the context example, demonstrating how to retrieve and display the list of roots exposed by the client. - Updated documentation to include the new tool and its functionality, enhancing the example's coverage of context features. * feat(docs): add roots documentation for client and server - Introduced new documentation for the `roots` feature, detailing how clients can expose file system roots to MCP servers and how servers can access these roots. - Added examples for configuring roots in the MCP client and using the `list_roots()` method in the server context. - Enhanced overall clarity and usability of the documentation to support developers in implementing the roots functionality. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: pietrozullo <pietro.zullo@gmail.com>
183 lines
7.2 KiB
Python
183 lines
7.2 KiB
Python
"""
|
|
StdIO connector for MCP implementations.
|
|
|
|
This module provides a connector for communicating with MCP implementations
|
|
through the standard input/output streams.
|
|
"""
|
|
|
|
import sys
|
|
from typing import Any
|
|
|
|
from mcp import ClientSession, ErrorData, McpError, StdioServerParameters
|
|
from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
from mcp.types import CONNECTION_CLOSED, Root
|
|
|
|
from mcp_use.client.connectors.base import BaseConnector
|
|
from mcp_use.client.middleware import CallbackClientSession, Middleware
|
|
from mcp_use.client.task_managers import StdioConnectionManager
|
|
from mcp_use.logging import logger
|
|
|
|
|
|
class StdioConnector(BaseConnector):
|
|
"""Connector for MCP implementations using stdio transport.
|
|
|
|
This connector uses the stdio transport to communicate with MCP implementations
|
|
that are executed as child processes. It uses a connection manager to handle
|
|
the proper lifecycle management of the stdio client.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
command: str = "npx",
|
|
args: list[str] | None = None,
|
|
env: dict[str, str] | None = None,
|
|
errlog=sys.stderr,
|
|
sampling_callback: SamplingFnT | None = None,
|
|
elicitation_callback: ElicitationFnT | None = None,
|
|
message_handler: MessageHandlerFnT | None = None,
|
|
logging_callback: LoggingFnT | None = None,
|
|
middleware: list[Middleware] | None = None,
|
|
roots: list[Root] | None = None,
|
|
list_roots_callback: ListRootsFnT | None = None,
|
|
):
|
|
"""Initialize a new stdio connector.
|
|
|
|
Args:
|
|
command: The command to execute.
|
|
args: Optional command line arguments.
|
|
env: Optional environment variables.
|
|
errlog: Stream to write error output to.
|
|
sampling_callback: Optional callback to sample the client.
|
|
elicitation_callback: Optional callback to elicit the client.
|
|
message_handler: Optional callback to handle messages.
|
|
logging_callback: Optional callback to handle log messages.
|
|
middleware: Optional list of middleware.
|
|
roots: Optional initial list of roots to advertise to the server.
|
|
list_roots_callback: Optional custom callback to handle roots/list requests.
|
|
"""
|
|
super().__init__(
|
|
sampling_callback=sampling_callback,
|
|
elicitation_callback=elicitation_callback,
|
|
message_handler=message_handler,
|
|
logging_callback=logging_callback,
|
|
middleware=middleware,
|
|
roots=roots,
|
|
list_roots_callback=list_roots_callback,
|
|
)
|
|
self.command = command
|
|
self.args = args or [] # Ensure args is never None
|
|
self.env = env
|
|
self.errlog = errlog
|
|
|
|
async def connect(self) -> None:
|
|
"""Establish a connection to the MCP implementation."""
|
|
if self._connected:
|
|
logger.debug("Already connected to MCP implementation")
|
|
return
|
|
|
|
logger.debug(f"Connecting to MCP implementation: {self.command}")
|
|
try:
|
|
# Create server parameters
|
|
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
|
|
|
|
# Create and start the connection manager
|
|
self._connection_manager = StdioConnectionManager(server_params, self.errlog)
|
|
read_stream, write_stream = await self._connection_manager.start()
|
|
|
|
# Create the client session
|
|
raw_client_session = ClientSession(
|
|
read_stream,
|
|
write_stream,
|
|
sampling_callback=self.sampling_callback,
|
|
elicitation_callback=self.elicitation_callback,
|
|
list_roots_callback=self.list_roots_callback,
|
|
message_handler=self._internal_message_handler,
|
|
logging_callback=self.logging_callback,
|
|
client_info=self.client_info,
|
|
)
|
|
await raw_client_session.__aenter__()
|
|
|
|
# Wrap with middleware
|
|
self.client_session = CallbackClientSession(
|
|
raw_client_session, self.public_identifier, self.middleware_manager
|
|
)
|
|
|
|
# Mark as connected
|
|
self._connected = True
|
|
logger.debug(f"Successfully connected to MCP implementation: {self.command}")
|
|
|
|
except OSError as e:
|
|
# Process could not be started at all
|
|
logger.error(
|
|
f"Failed to start stdio MCP server {self.public_identifier}"
|
|
f"with command {self.command} and args {self.args}"
|
|
)
|
|
|
|
# Clean up any resources if connection failed
|
|
await self._cleanup_resources()
|
|
|
|
# Re-raise runtime error
|
|
raise RuntimeError(
|
|
"Failed to start stdio MCP server "
|
|
f"'{self.public_identifier}'. "
|
|
f"Ensure '{self.command}' is installed and on PATH. "
|
|
f"Original error: {e}"
|
|
) from e
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to stdio MCP server {self.public_identifier}: {e}")
|
|
await self._cleanup_resources()
|
|
raise
|
|
|
|
async def initialize(self) -> dict[str, Any]:
|
|
"""
|
|
Initialize the MCP session for stdio servers with richer error messages.
|
|
|
|
In particular, wraps McpError(CONNECTION_CLOSED) to include the stdio
|
|
command/args and guidance to inspect stderr.
|
|
"""
|
|
if not self.client_session:
|
|
raise RuntimeError("MCP client is not connected")
|
|
|
|
try:
|
|
# Delegate to BaseConnector.initialize (which handles capabilities + lists)
|
|
return await super().initialize()
|
|
|
|
except McpError as e:
|
|
err = getattr(e, "error", None)
|
|
|
|
# The common case when the server process starts, prints an error,
|
|
# and exits during initialize() (e.g. invalid CLI flag)
|
|
if err is not None and err.code == CONNECTION_CLOSED:
|
|
cmd = f"{self.command} {' '.join(self.args)}".strip()
|
|
|
|
message = (
|
|
f"Failed to initialize stdio MCP server '{self.public_identifier}': "
|
|
"the underlying process closed the connection during initialization.\n"
|
|
f"Command: {cmd}\n"
|
|
"This usually means the server failed to start correctly or crashed "
|
|
"(for example, due to an invalid CLI flag or runtime error).\n"
|
|
"Check the server's stderr output above for details."
|
|
)
|
|
|
|
raise McpError(
|
|
ErrorData(
|
|
code=err.code,
|
|
message=message,
|
|
data={
|
|
"public_identifier": self.public_identifier,
|
|
"command": self.command,
|
|
"args": self.args,
|
|
"phase": "initialize",
|
|
},
|
|
)
|
|
) from e
|
|
|
|
# For other MCP errors, just re-raise
|
|
raise
|
|
|
|
@property
|
|
def public_identifier(self) -> str:
|
|
"""Get the identifier for the connector."""
|
|
return f"stdio:{self.command} {' '.join(self.args)}"
|