Files
Luigi Pederzani b45d9663ec feat(python): introduce roots support in MCPClient and Server (#751)
* 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>
2026-01-12 11:16:09 +01:00

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)}"