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

162 lines
5.2 KiB
Python

"""Integration tests for the roots capability.
These tests verify that the client properly exposes roots to MCP servers
and that servers can request the list of roots from the client.
"""
import json
import logging
import pytest
from mcp.types import ListRootsResult, Root
from mcp_use.client import MCPClient
logger = logging.getLogger(__name__)
@pytest.mark.asyncio
async def test_roots_capability(primitive_server):
"""Test that a server can request and receive roots from the client."""
# Define roots to expose to the server
roots = [
Root(uri="file:///home/user/project", name="My Project"),
Root(uri="file:///home/user/data"),
]
config = {"mcpServers": {"PrimitiveServer": {"url": f"{primitive_server}/mcp"}}}
client = MCPClient(config, roots=roots)
try:
await client.create_all_sessions()
session = client.get_session("PrimitiveServer")
# Call the get_client_roots tool which requests roots from the client
result = await session.call_tool(name="get_client_roots", arguments={})
content = result.content[0]
logger.info(f"Result: {content}")
# Parse the JSON response
data = json.loads(content.text)
# Verify the roots were received correctly
assert data["count"] == 2
assert len(data["roots"]) == 2
# Check first root (with name)
root1 = data["roots"][0]
assert root1["uri"] == "file:///home/user/project"
assert root1["name"] == "My Project"
# Check second root (without name)
root2 = data["roots"][1]
assert root2["uri"] == "file:///home/user/data"
assert root2["name"] is None
finally:
await client.close_all_sessions()
@pytest.mark.asyncio
async def test_roots_empty_by_default(primitive_server):
"""Test that roots are empty when not provided."""
config = {"mcpServers": {"PrimitiveServer": {"url": f"{primitive_server}/mcp"}}}
client = MCPClient(config)
try:
await client.create_all_sessions()
session = client.get_session("PrimitiveServer")
# Call the get_client_roots tool
result = await session.call_tool(name="get_client_roots", arguments={})
content = result.content[0]
logger.info(f"Result: {content}")
# Parse the JSON response
data = json.loads(content.text)
# Verify no roots were returned
assert data["count"] == 0
assert len(data["roots"]) == 0
finally:
await client.close_all_sessions()
@pytest.mark.asyncio
async def test_roots_with_custom_callback(primitive_server):
"""Test that a custom list_roots_callback can provide dynamic roots."""
# Custom callback that returns different roots
custom_roots = [
Root(uri="file:///custom/workspace", name="Custom Workspace"),
]
async def custom_list_roots_callback(context):
return ListRootsResult(roots=custom_roots)
config = {"mcpServers": {"PrimitiveServer": {"url": f"{primitive_server}/mcp"}}}
client = MCPClient(
config,
roots=[Root(uri="file:///default/path")], # Default roots (should be ignored)
list_roots_callback=custom_list_roots_callback,
)
try:
await client.create_all_sessions()
session = client.get_session("PrimitiveServer")
# Call the get_client_roots tool
result = await session.call_tool(name="get_client_roots", arguments={})
content = result.content[0]
logger.info(f"Result: {content}")
# Parse the JSON response
data = json.loads(content.text)
# Verify custom callback's roots were returned, not default
assert data["count"] == 1
assert data["roots"][0]["uri"] == "file:///custom/workspace"
assert data["roots"][0]["name"] == "Custom Workspace"
finally:
await client.close_all_sessions()
@pytest.mark.asyncio
async def test_set_roots_updates_server_view(primitive_server):
"""Test that set_roots() updates the roots visible to the server."""
# Start with initial roots
initial_roots = [
Root(uri="file:///initial/path", name="Initial"),
]
config = {"mcpServers": {"PrimitiveServer": {"url": f"{primitive_server}/mcp"}}}
client = MCPClient(config, roots=initial_roots)
try:
await client.create_all_sessions()
session = client.get_session("PrimitiveServer")
# Verify initial roots
result = await session.call_tool(name="get_client_roots", arguments={})
data = json.loads(result.content[0].text)
assert data["count"] == 1
assert data["roots"][0]["uri"] == "file:///initial/path"
# Update roots via connector
new_roots = [
Root(uri="file:///updated/path1", name="Updated 1"),
Root(uri="file:///updated/path2", name="Updated 2"),
]
await session.connector.set_roots(new_roots)
# Verify updated roots
result = await session.call_tool(name="get_client_roots", arguments={})
data = json.loads(result.content[0].text)
assert data["count"] == 2
assert data["roots"][0]["uri"] == "file:///updated/path1"
assert data["roots"][1]["uri"] == "file:///updated/path2"
finally:
await client.close_all_sessions()