diff --git a/docs/modules/usage/mcp.md b/docs/modules/usage/mcp.md new file mode 100644 index 0000000000..de475b6462 --- /dev/null +++ b/docs/modules/usage/mcp.md @@ -0,0 +1,96 @@ +# Model Context Protocol (MCP) + +:::note +This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you to extend the agent's capabilities with custom tools. +::: + +## Overview + +Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate with external tool servers. These servers can provide additional functionality to the agent, such as specialized data processing, external API access, or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io). + +## Configuration + +MCP configuration is defined in the `[mcp]` section of your `config.toml` file. + +### Configuration Example + +```toml +[mcp] +# SSE Servers - External servers that communicate via Server-Sent Events +sse_servers = [ + # Basic SSE server with just a URL + "http://example.com:8080/mcp", + + # SSE server with API key authentication + {url="https://secure-example.com/mcp", api_key="your-api-key"} +] + +# Stdio Servers - Local processes that communicate via standard input/output +stdio_servers = [ + # Basic stdio server + {name="fetch", command="uvx", args=["mcp-server-fetch"]}, + + # Stdio server with environment variables + { + name="data-processor", + command="python", + args=["-m", "my_mcp_server"], + env={ + "DEBUG": "true", + "PORT": "8080" + } + } +] +``` + +## Configuration Options + +### SSE Servers + +SSE servers are configured using either a string URL or an object with the following properties: + +- `url` (required) + - Type: `str` + - Description: The URL of the SSE server + +- `api_key` (optional) + - Type: `str` + - Default: `None` + - Description: API key for authentication with the SSE server + +### Stdio Servers + +Stdio servers are configured using an object with the following properties: + +- `name` (required) + - Type: `str` + - Description: A unique name for the server + +- `command` (required) + - Type: `str` + - Description: The command to run the server + +- `args` (optional) + - Type: `list of str` + - Default: `[]` + - Description: Command-line arguments to pass to the server + +- `env` (optional) + - Type: `dict of str to str` + - Default: `{}` + - Description: Environment variables to set for the server process + +## How MCP Works + +When OpenHands starts, it: + +1. Reads the MCP configuration from `config.toml` +2. Connects to any configured SSE servers +3. Starts any configured stdio servers +4. Registers the tools provided by these servers with the agent + +The agent can then use these tools just like any built-in tool. When the agent calls an MCP tool: + +1. OpenHands routes the call to the appropriate MCP server +2. The server processes the request and returns a response +3. OpenHands converts the response to an observation and presents it to the agent diff --git a/openhands/agenthub/codeact_agent/codeact_agent.py b/openhands/agenthub/codeact_agent/codeact_agent.py index 84cb70e9be..98e1d85cda 100644 --- a/openhands/agenthub/codeact_agent/codeact_agent.py +++ b/openhands/agenthub/codeact_agent/codeact_agent.py @@ -177,38 +177,30 @@ class CodeActAgent(Agent): } params['tools'] = self.tools - if self.mcp_tools: - # Only add tools with unique names - existing_names = {tool['function']['name'] for tool in params['tools']} - unique_mcp_tools = [ - tool - for tool in self.mcp_tools - if tool['function']['name'] not in existing_names - ] - - if self.llm.config.model == 'gemini-2.5-pro-preview-03-25': - logger.info( - f'Removing the default fields from the MCP tools for {self.llm.config.model} ' - "since it doesn't support them and the request would crash." - ) - # prevent mutation of input tools - unique_mcp_tools = copy.deepcopy(unique_mcp_tools) - # Strip off default fields that cause errors with gemini-preview - for tool in unique_mcp_tools: - if 'function' in tool and 'parameters' in tool['function']: - if 'properties' in tool['function']['parameters']: - for prop_name, prop in tool['function']['parameters'][ - 'properties' - ].items(): - if 'default' in prop: - del prop['default'] - - params['tools'] += unique_mcp_tools + # Special handling for Gemini model which doesn't support default fields + if self.llm.config.model == 'gemini-2.5-pro-preview-03-25': + logger.info( + f'Removing the default fields from tools for {self.llm.config.model} ' + "since it doesn't support them and the request would crash." + ) + # prevent mutation of input tools + params['tools'] = copy.deepcopy(params['tools']) + # Strip off default fields that cause errors with gemini-preview + for tool in params['tools']: + if 'function' in tool and 'parameters' in tool['function']: + if 'properties' in tool['function']['parameters']: + for prop_name, prop in tool['function']['parameters'][ + 'properties' + ].items(): + if 'default' in prop: + del prop['default'] # log to litellm proxy if possible params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)} response = self.llm.completion(**params) logger.debug(f'Response from LLM: {response}') - actions = self.response_to_actions_fn(response) + actions = self.response_to_actions_fn( + response, mcp_tool_names=list(self.mcp_tools.keys()) + ) logger.debug(f'Actions after response_to_actions: {actions}') for action in actions: self.pending_actions.append(action) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 023acca660..bbf291cdf4 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -37,10 +37,9 @@ from openhands.events.action import ( IPythonRunCellAction, MessageAction, ) -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.event import FileEditSource, FileReadSource from openhands.events.tool import ToolCallMetadata -from openhands.mcp import MCPClientTool def combine_thought(action: Action, thought: str) -> Action: @@ -53,7 +52,9 @@ def combine_thought(action: Action, thought: str) -> Action: return action -def response_to_actions(response: ModelResponse) -> list[Action]: +def response_to_actions( + response: ModelResponse, mcp_tool_names: list[str] | None = None +) -> list[Action]: actions: list[Action] = [] assert len(response.choices) == 1, 'Only one choice is supported for now' choice = response.choices[0] @@ -195,12 +196,12 @@ def response_to_actions(response: ModelResponse) -> list[Action]: action = BrowseURLAction(url=arguments['url']) # ================================================ - # McpAction (MCP) + # MCPAction (MCP) # ================================================ - elif tool_call.function.name.endswith(MCPClientTool.postfix()): - action = McpAction( - name=tool_call.function.name.removesuffix(MCPClientTool.postfix()), - arguments=tool_call.function.arguments, + elif mcp_tool_names and tool_call.function.name in mcp_tool_names: + action = MCPAction( + name=tool_call.function.name, + arguments=arguments, ) else: raise FunctionCallNotExistsError( diff --git a/openhands/agenthub/readonly_agent/function_calling.py b/openhands/agenthub/readonly_agent/function_calling.py index 183c330b5b..f327ddf0a5 100644 --- a/openhands/agenthub/readonly_agent/function_calling.py +++ b/openhands/agenthub/readonly_agent/function_calling.py @@ -36,6 +36,7 @@ from openhands.events.action import ( BrowseURLAction, CmdRunAction, FileReadAction, + MCPAction, MessageAction, ) from openhands.events.event import FileReadSource @@ -102,7 +103,9 @@ def glob_to_cmdrun(pattern: str, path: str = '.') -> str: return echo_cmd + complete_cmd -def response_to_actions(response: ModelResponse) -> list[Action]: +def response_to_actions( + response: ModelResponse, mcp_tool_names: list[str] | None = None +) -> list[Action]: actions: list[Action] = [] assert len(response.choices) == 1, 'Only one choice is supported for now' choice = response.choices[0] @@ -198,6 +201,15 @@ def response_to_actions(response: ModelResponse) -> list[Action]: ) action = BrowseURLAction(url=arguments['url']) + # ================================================ + # MCPAction (MCP) + # ================================================ + elif mcp_tool_names and tool_call.function.name in mcp_tool_names: + action = MCPAction( + name=tool_call.function.name, + arguments=arguments, + ) + else: raise FunctionCallNotExistsError( f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.' diff --git a/openhands/controller/agent.py b/openhands/controller/agent.py index ca5908adfc..b5e70209ad 100644 --- a/openhands/controller/agent.py +++ b/openhands/controller/agent.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: from openhands.core.config import AgentConfig from openhands.events.action import Action from openhands.events.action.message import SystemMessageAction +from litellm import ChatCompletionToolParam + from openhands.core.exceptions import ( AgentAlreadyRegisteredError, AgentNotRegisteredError, @@ -42,7 +44,7 @@ class Agent(ABC): self.config = config self._complete = False self.prompt_manager: 'PromptManager' | None = None - self.mcp_tools: list[dict] = [] + self.mcp_tools: dict[str, ChatCompletionToolParam] = {} self.tools: list = [] def get_system_message(self) -> 'SystemMessageAction | None': @@ -160,4 +162,18 @@ class Agent(ABC): Args: - mcp_tools (list[dict]): The list of MCP tools. """ - self.mcp_tools = mcp_tools + logger.info( + f"Setting {len(mcp_tools)} MCP tools for agent {self.name}: {[tool['function']['name'] for tool in mcp_tools]}" + ) + for tool in mcp_tools: + _tool = ChatCompletionToolParam(**tool) + if _tool['function']['name'] in self.mcp_tools: + logger.warning( + f"Tool {_tool['function']['name']} already exists, skipping" + ) + continue + self.mcp_tools[_tool['function']['name']] = _tool + self.tools.append(_tool) + logger.info( + f"Tools updated for agent {self.name}, total {len(self.tools)}: {[tool['function']['name'] for tool in self.tools]}" + ) diff --git a/openhands/core/cli.py b/openhands/core/cli.py index dbbcdc521d..cd07bc66fa 100644 --- a/openhands/core/cli.py +++ b/openhands/core/cli.py @@ -54,7 +54,7 @@ from openhands.events.observation import ( AgentStateChangedObservation, ) from openhands.io import read_task -from openhands.mcp import fetch_mcp_tools_from_config +from openhands.mcp import add_mcp_tools_to_agent from openhands.memory.condenser.impl.llm_summarizing_condenser import ( LLMSummarizingCondenserConfig, ) @@ -112,8 +112,6 @@ async def run_session( ) agent = create_agent(config) - mcp_tools = await fetch_mcp_tools_from_config(config.mcp) - agent.set_mcp_tools(mcp_tools) runtime = create_runtime( config, sid=sid, @@ -209,6 +207,7 @@ async def run_session( event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, str(uuid4())) await runtime.connect() + await add_mcp_tools_to_agent(agent, runtime, config.mcp) # Initialize repository if needed repo_directory = None diff --git a/openhands/core/config/__init__.py b/openhands/core/config/__init__.py index e927c4f3b0..c43bca2408 100644 --- a/openhands/core/config/__init__.py +++ b/openhands/core/config/__init__.py @@ -7,6 +7,7 @@ from openhands.core.config.config_utils import ( ) from openhands.core.config.extended_config import ExtendedConfig from openhands.core.config.llm_config import LLMConfig +from openhands.core.config.mcp_config import MCPConfig from openhands.core.config.sandbox_config import SandboxConfig from openhands.core.config.security_config import SecurityConfig from openhands.core.config.utils import ( @@ -26,6 +27,7 @@ __all__ = [ 'OH_MAX_ITERATIONS', 'AgentConfig', 'AppConfig', + 'MCPConfig', 'LLMConfig', 'SandboxConfig', 'SecurityConfig', diff --git a/openhands/core/config/mcp_config.py b/openhands/core/config/mcp_config.py index 2f362c81b0..a5fe534925 100644 --- a/openhands/core/config/mcp_config.py +++ b/openhands/core/config/mcp_config.py @@ -1,28 +1,59 @@ -from typing import List from urllib.parse import urlparse from pydantic import BaseModel, Field, ValidationError +class MCPSSEServerConfig(BaseModel): + """Configuration for a single MCP server. + + Attributes: + url: The server URL + api_key: Optional API key for authentication + """ + + url: str + api_key: str | None = None + + +class MCPStdioServerConfig(BaseModel): + """Configuration for a MCP server that uses stdio. + + Attributes: + name: The name of the server + command: The command to run the server + args: The arguments to pass to the server + env: The environment variables to set for the server + """ + + name: str + command: str + args: list[str] = Field(default_factory=list) + env: dict[str, str] = Field(default_factory=dict) + + class MCPConfig(BaseModel): """Configuration for MCP (Message Control Protocol) settings. Attributes: - mcp_servers: List of MCP SSE (Server-Sent Events) server URLs. + sse_servers: List of MCP SSE server configs + stdio_servers: List of MCP stdio server configs. These servers will be added to the MCP Router running inside runtime container. """ - mcp_servers: List[str] = Field(default_factory=list) + sse_servers: list[MCPSSEServerConfig] = Field(default_factory=list) + stdio_servers: list[MCPStdioServerConfig] = Field(default_factory=list) model_config = {'extra': 'forbid'} def validate_servers(self) -> None: """Validate that server URLs are valid and unique.""" + urls = [server.url for server in self.sse_servers] + # Check for duplicate server URLs - if len(set(self.mcp_servers)) != len(self.mcp_servers): + if len(set(urls)) != len(urls): raise ValueError('Duplicate MCP server URLs are not allowed') # Validate URLs - for url in self.mcp_servers: + for url in urls: try: result = urlparse(url) if not all([result.scheme, result.netloc]): @@ -44,11 +75,32 @@ class MCPConfig(BaseModel): mcp_mapping: dict[str, MCPConfig] = {} try: + # Convert all entries in sse_servers to MCPSSEServerConfig objects + if 'sse_servers' in data: + servers = [] + for server in data['sse_servers']: + if isinstance(server, dict): + servers.append(MCPSSEServerConfig(**server)) + else: + # Convert string URLs to MCPSSEServerConfig objects with no API key + servers.append(MCPSSEServerConfig(url=server)) + data['sse_servers'] = servers + + # Convert all entries in stdio_servers to MCPStdioServerConfig objects + if 'stdio_servers' in data: + servers = [] + for server in data['stdio_servers']: + servers.append(MCPStdioServerConfig(**server)) + data['stdio_servers'] = servers + # Create SSE config if present mcp_config = MCPConfig.model_validate(data) mcp_config.validate_servers() # Create the main MCP config - mcp_mapping['mcp'] = cls(mcp_servers=mcp_config.mcp_servers) + mcp_mapping['mcp'] = cls( + sse_servers=mcp_config.sse_servers, + stdio_servers=mcp_config.stdio_servers, + ) except ValidationError as e: raise ValueError(f'Invalid MCP configuration: {e}') diff --git a/openhands/core/main.py b/openhands/core/main.py index 14f8312ecd..8c44260bbe 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -30,7 +30,7 @@ from openhands.events.action.action import Action from openhands.events.event import Event from openhands.events.observation import AgentStateChangedObservation from openhands.io import read_input, read_task -from openhands.mcp import fetch_mcp_tools_from_config +from openhands.mcp import add_mcp_tools_to_agent from openhands.memory.memory import Memory from openhands.runtime.base import Runtime from openhands.utils.async_utils import call_async_from_sync @@ -96,8 +96,6 @@ async def run_controller( if agent is None: agent = create_agent(config) - mcp_tools = await fetch_mcp_tools_from_config(config.mcp) - agent.set_mcp_tools(mcp_tools) # when the runtime is created, it will be connected and clone the selected repository repo_directory = None @@ -118,6 +116,8 @@ async def run_controller( selected_repository=config.sandbox.selected_repo, ) + await add_mcp_tools_to_agent(agent, runtime, config.mcp) + event_stream = runtime.event_stream # when memory is created, it will load the microagents from the selected repository diff --git a/openhands/events/action/__init__.py b/openhands/events/action/__init__.py index d97be959a7..a30a5545a5 100644 --- a/openhands/events/action/__init__.py +++ b/openhands/events/action/__init__.py @@ -15,7 +15,7 @@ from openhands.events.action.files import ( FileReadAction, FileWriteAction, ) -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.action.message import MessageAction, SystemMessageAction __all__ = [ @@ -37,5 +37,5 @@ __all__ = [ 'ActionConfirmationStatus', 'AgentThinkAction', 'RecallAction', - 'McpAction', + 'MCPAction', ] diff --git a/openhands/events/action/mcp.py b/openhands/events/action/mcp.py index 6c0a77a77b..de18c30138 100644 --- a/openhands/events/action/mcp.py +++ b/openhands/events/action/mcp.py @@ -1,14 +1,14 @@ -from dataclasses import dataclass -from typing import ClassVar +from dataclasses import dataclass, field +from typing import Any, ClassVar from openhands.core.schema import ActionType from openhands.events.action.action import Action, ActionSecurityRisk @dataclass -class McpAction(Action): +class MCPAction(Action): name: str - arguments: str | None = None + arguments: dict[str, Any] = field(default_factory=dict) thought: str = '' action: str = ActionType.MCP runnable: ClassVar[bool] = True @@ -24,7 +24,7 @@ class McpAction(Action): ) def __str__(self) -> str: - ret = '**McpAction**\n' + ret = '**MCPAction**\n' if self.thought: ret += f'THOUGHT: {self.thought}\n' ret += f'NAME: {self.name}\n' diff --git a/openhands/events/observation/__init__.py b/openhands/events/observation/__init__.py index 7e574b32be..2334cc0095 100644 --- a/openhands/events/observation/__init__.py +++ b/openhands/events/observation/__init__.py @@ -21,6 +21,7 @@ from openhands.events.observation.files import ( FileReadObservation, FileWriteObservation, ) +from openhands.events.observation.mcp import MCPObservation from openhands.events.observation.observation import Observation from openhands.events.observation.reject import UserRejectObservation from openhands.events.observation.success import SuccessObservation diff --git a/openhands/events/serialization/action.py b/openhands/events/serialization/action.py index 6b3c1f096c..3d2debbefa 100644 --- a/openhands/events/serialization/action.py +++ b/openhands/events/serialization/action.py @@ -22,7 +22,7 @@ from openhands.events.action.files import ( FileReadAction, FileWriteAction, ) -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.action.message import MessageAction, SystemMessageAction actions = ( @@ -43,7 +43,7 @@ actions = ( MessageAction, SystemMessageAction, CondensationAction, - McpAction, + MCPAction, ) ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined] diff --git a/openhands/mcp/__init__.py b/openhands/mcp/__init__.py index 383384c566..e345266166 100644 --- a/openhands/mcp/__init__.py +++ b/openhands/mcp/__init__.py @@ -1,9 +1,7 @@ from openhands.mcp.client import MCPClient -from openhands.mcp.tool import ( - BaseTool, - MCPClientTool, -) +from openhands.mcp.tool import MCPClientTool from openhands.mcp.utils import ( + add_mcp_tools_to_agent, call_tool_mcp, convert_mcp_clients_to_tools, create_mcp_clients, @@ -14,8 +12,8 @@ __all__ = [ 'MCPClient', 'convert_mcp_clients_to_tools', 'create_mcp_clients', - 'BaseTool', 'MCPClientTool', 'fetch_mcp_tools_from_config', 'call_tool_mcp', + 'add_mcp_tools_to_agent', ] diff --git a/openhands/mcp/client.py b/openhands/mcp/client.py index 71d7623632..6a51c8eeac 100644 --- a/openhands/mcp/client.py +++ b/openhands/mcp/client.py @@ -7,7 +7,7 @@ from mcp.client.sse import sse_client from pydantic import BaseModel, Field from openhands.core.logger import openhands_logger as logger -from openhands.mcp.tool import BaseTool, MCPClientTool +from openhands.mcp.tool import MCPClientTool class MCPClient(BaseModel): @@ -18,13 +18,15 @@ class MCPClient(BaseModel): session: Optional[ClientSession] = None exit_stack: AsyncExitStack = AsyncExitStack() description: str = 'MCP client tools for server interaction' - tools: List[BaseTool] = Field(default_factory=list) - tool_map: Dict[str, BaseTool] = Field(default_factory=dict) + tools: List[MCPClientTool] = Field(default_factory=list) + tool_map: Dict[str, MCPClientTool] = Field(default_factory=dict) class Config: arbitrary_types_allowed = True - async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None: + async def connect_sse( + self, server_url: str, api_key: str | None = None, timeout: float = 30.0 + ) -> None: """Connect to an MCP server using SSE transport. Args: @@ -41,7 +43,8 @@ class MCPClient(BaseModel): async def connect_with_timeout(): streams_context = sse_client( url=server_url, - timeout=timeout, # Pass the timeout to sse_client + headers={'Authorization': f'Bearer {api_key}'} if api_key else None, + timeout=timeout, ) streams = await self.exit_stack.enter_async_context(streams_context) self.session = await self.exit_stack.enter_async_context( @@ -92,7 +95,10 @@ class MCPClient(BaseModel): """Call a tool on the MCP server.""" if tool_name not in self.tool_map: raise ValueError(f'Tool {tool_name} not found.') - return await self.tool_map[tool_name].execute(**args) + # The MCPClientTool is primarily for metadata; use the session to call the actual tool. + if not self.session: + raise RuntimeError('Client session is not available.') + return await self.session.call_tool(name=tool_name, arguments=args) async def disconnect(self) -> None: """Disconnect from the MCP server and clean up resources.""" diff --git a/openhands/mcp/tool.py b/openhands/mcp/tool.py index 71d73f046a..3bee8029c6 100644 --- a/openhands/mcp/tool.py +++ b/openhands/mcp/tool.py @@ -1,54 +1,26 @@ -from abc import ABC, abstractmethod -from typing import Dict, Optional +from typing import Dict -from mcp import ClientSession -from mcp.types import CallToolResult, TextContent, Tool +from mcp.types import Tool -class BaseTool(ABC, Tool): - @classmethod - def postfix(cls) -> str: - return '_mcp_tool_call' +class MCPClientTool(Tool): + """ + Represents a tool proxy that can be called on the MCP server from the client side. + + This version doesn't store a session reference, as sessions are created on-demand + by the MCPClient for each operation. + """ class Config: arbitrary_types_allowed = True - @abstractmethod - async def execute(self, **kwargs) -> CallToolResult: - """Execute the tool with given parameters.""" - def to_param(self) -> Dict: """Convert tool to function call format.""" return { 'type': 'function', 'function': { - 'name': self.name + self.postfix(), + 'name': self.name, 'description': self.description, 'parameters': self.inputSchema, }, } - - -class MCPClientTool(BaseTool): - """Represents a tool proxy that can be called on the MCP server from the client side.""" - - session: Optional[ClientSession] = None - - async def execute(self, **kwargs) -> CallToolResult: - """Execute the tool by making a remote call to the MCP server.""" - if not self.session: - return CallToolResult( - content=[TextContent(text='Not connected to MCP server', type='text')], - isError=True, - ) - - try: - result = await self.session.call_tool(self.name, kwargs) - return result - except Exception as e: - return CallToolResult( - content=[ - TextContent(text=f'Error executing tool: {str(e)}', type='text') - ], - isError=True, - ) diff --git a/openhands/mcp/utils.py b/openhands/mcp/utils.py index e6fd1403ca..60a10d5c44 100644 --- a/openhands/mcp/utils.py +++ b/openhands/mcp/utils.py @@ -1,11 +1,16 @@ import json +from typing import TYPE_CHECKING -from openhands.core.config.mcp_config import MCPConfig +if TYPE_CHECKING: + from openhands.controller.agent import Agent + +from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig from openhands.core.logger import openhands_logger as logger -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.observation.mcp import MCPObservation from openhands.events.observation.observation import Observation from openhands.mcp.client import MCPClient +from openhands.runtime.base import Runtime def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[dict]: @@ -38,19 +43,19 @@ def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[di async def create_mcp_clients( - mcp_servers: list[str], + sse_servers: list[MCPSSEServerConfig], ) -> list[MCPClient]: mcp_clients: list[MCPClient] = [] # Initialize SSE connections - if mcp_servers: - for server_url in mcp_servers: + if sse_servers: + for server_url in sse_servers: logger.info( f'Initializing MCP agent for {server_url} with SSE connection...' ) client = MCPClient() try: - await client.connect_sse(server_url) + await client.connect_sse(server_url.url, api_key=server_url.api_key) # Only add the client to the list after a successful connection mcp_clients.append(client) logger.info(f'Connected to MCP server {server_url} via SSE') @@ -77,14 +82,16 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]: mcp_tools = [] try: logger.debug(f'Creating MCP clients with config: {mcp_config}') + # Create clients - this will fetch tools but not maintain active connections mcp_clients = await create_mcp_clients( - mcp_config.mcp_servers, + mcp_config.sse_servers, ) if not mcp_clients: logger.debug('No MCP clients were successfully connected') return [] + # Convert tools to the format expected by the agent mcp_tools = convert_mcp_clients_to_tools(mcp_clients) # Always disconnect clients to clean up resources @@ -93,6 +100,7 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]: await mcp_client.disconnect() except Exception as disconnect_error: logger.error(f'Error disconnecting MCP client: {str(disconnect_error)}') + except Exception as e: logger.error(f'Error fetching MCP tools: {str(e)}') return [] @@ -101,13 +109,13 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]: return mcp_tools -async def call_tool_mcp(mcp_clients: list[MCPClient], action: McpAction) -> Observation: +async def call_tool_mcp(mcp_clients: list[MCPClient], action: MCPAction) -> Observation: """ Call a tool on an MCP server and return the observation. Args: + mcp_clients: The list of MCP clients to execute the action on action: The MCP action to execute - sse_mcp_servers: List of SSE MCP server URLs Returns: The observation from the MCP server @@ -116,20 +124,55 @@ async def call_tool_mcp(mcp_clients: list[MCPClient], action: McpAction) -> Obse raise ValueError('No MCP clients found') logger.debug(f'MCP action received: {action}') - # Find the MCP agent that has the matching tool name + + # Find the MCP client that has the matching tool name matching_client = None logger.debug(f'MCP clients: {mcp_clients}') logger.debug(f'MCP action name: {action.name}') + for client in mcp_clients: logger.debug(f'MCP client tools: {client.tools}') if action.name in [tool.name for tool in client.tools]: matching_client = client break + if matching_client is None: raise ValueError(f'No matching MCP agent found for tool name: {action.name}') + logger.debug(f'Matching client: {matching_client}') - args_dict = json.loads(action.arguments) if action.arguments else {} - response = await matching_client.call_tool(action.name, args_dict) + + # Call the tool - this will create a new connection internally + response = await matching_client.call_tool(action.name, action.arguments) logger.debug(f'MCP response: {response}') - return MCPObservation(content=f'MCP result:{response.model_dump(mode="json")}') + return MCPObservation(content=json.dumps(response.model_dump(mode='json'))) + + +async def add_mcp_tools_to_agent( + agent: 'Agent', runtime: Runtime, mcp_config: MCPConfig +): + """ + Add MCP tools to an agent. + """ + from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, # inline import to avoid circular import + ) + + assert isinstance( + runtime, ActionExecutionClient + ), 'Runtime must be an instance of ActionExecutionClient' + assert ( + runtime.runtime_initialized + ), 'Runtime must be initialized before adding MCP tools' + + # Add the runtime as another MCP server + updated_mcp_config = runtime.get_updated_mcp_config() + # Fetch the MCP tools + mcp_tools = await fetch_mcp_tools_from_config(updated_mcp_config) + + logger.info( + f"Loaded {len(mcp_tools)} MCP tools: {[tool['function']['name'] for tool in mcp_tools]}" + ) + + # Set the MCP tools on the agent + agent.set_mcp_tools(mcp_tools) diff --git a/openhands/memory/conversation_memory.py b/openhands/memory/conversation_memory.py index c87b90fce6..46d43a416a 100644 --- a/openhands/memory/conversation_memory.py +++ b/openhands/memory/conversation_memory.py @@ -19,7 +19,7 @@ from openhands.events.action import ( IPythonRunCellAction, MessageAction, ) -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.action.message import SystemMessageAction from openhands.events.event import Event, RecallType from openhands.events.observation import ( @@ -184,7 +184,7 @@ class ConversationMemory: - BrowseInteractiveAction: For browsing the web - AgentFinishAction: For ending the interaction - MessageAction: For sending messages - - McpAction: For interacting with the MCP server + - MCPAction: For interacting with the MCP server pending_tool_call_action_messages: Dictionary mapping response IDs to their corresponding messages. Used in function calling mode to track tool calls that are waiting for their results. @@ -210,7 +210,7 @@ class ConversationMemory: FileReadAction, BrowseInteractiveAction, BrowseURLAction, - McpAction, + MCPAction, ), ) or (isinstance(action, CmdRunAction) and action.source == 'agent'): tool_metadata = action.tool_call_metadata diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index d91126202c..fa59b69cc9 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -8,6 +8,8 @@ NOTE: this will be executed inside the docker sandbox. import argparse import asyncio import base64 +import json +import logging import mimetypes import os import shutil @@ -23,6 +25,8 @@ from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile from fastapi.exceptions import RequestValidationError from fastapi.responses import FileResponse, JSONResponse from fastapi.security import APIKeyHeader +from mcpm import MCPRouter, RouterConfig +from mcpm.router.router import logger as mcp_router_logger from openhands_aci.editor.editor import OHEditor from openhands_aci.editor.exceptions import ToolError from openhands_aci.editor.results import ToolResult @@ -68,6 +72,9 @@ from openhands.runtime.utils.runtime_init import init_user_and_working_directory from openhands.runtime.utils.system_stats import get_system_stats from openhands.utils.async_utils import call_sync_from_async, wait_all +# Set MCP router logger to the same level as the main logger +mcp_router_logger.setLevel(logger.getEffectiveLevel()) + class ActionRequest(BaseModel): action: dict @@ -572,10 +579,15 @@ if __name__ == '__main__': plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore client: ActionExecutor | None = None + mcp_router: MCPRouter | None = None + MCP_ROUTER_PROFILE_PATH = os.path.join( + os.path.dirname(__file__), 'mcp', 'config.json' + ) @asynccontextmanager async def lifespan(app: FastAPI): - global client + global client, mcp_router + logger.info('Initializing ActionExecutor...') client = ActionExecutor( plugins_to_load, work_dir=args.working_dir, @@ -584,9 +596,70 @@ if __name__ == '__main__': browsergym_eval_env=args.browsergym_eval_env, ) await client.ainit() + logger.info('ActionExecutor initialized.') + + # Initialize and mount MCP Router + logger.info('Initializing MCP Router...') + mcp_router = MCPRouter( + profile_path=MCP_ROUTER_PROFILE_PATH, + router_config=RouterConfig( + api_key=SESSION_API_KEY, + auth_enabled=bool(SESSION_API_KEY), + ), + ) + allowed_origins = ['*'] + sse_app = await mcp_router.get_sse_server_app( + allow_origins=allowed_origins, include_lifespan=False + ) + + # Check for route conflicts before mounting + main_app_routes = {route.path for route in app.routes} + sse_app_routes = {route.path for route in sse_app.routes} + conflicting_routes = main_app_routes.intersection(sse_app_routes) + + if conflicting_routes: + logger.error(f'Route conflicts detected: {conflicting_routes}') + raise RuntimeError( + f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}' + ) + + app.mount('/', sse_app) + logger.info( + f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}' + ) + + # Additional debug logging + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Main app routes:') + for route in main_app_routes: + logger.debug(f' {route}') + logger.debug('MCP SSE server app routes:') + for route in sse_app_routes: + logger.debug(f' {route}') + yield + # Clean up & release the resources - client.close() + logger.info('Shutting down MCP Router...') + if mcp_router: + try: + await mcp_router.shutdown() + logger.info('MCP Router shutdown successfully.') + except Exception as e: + logger.error(f'Error shutting down MCP Router: {e}', exc_info=True) + else: + logger.info('MCP Router instance not found for shutdown.') + + logger.info('Closing ActionExecutor...') + if client: + try: + client.close() + logger.info('ActionExecutor closed successfully.') + except Exception as e: + logger.error(f'Error closing ActionExecutor: {e}', exc_info=True) + else: + logger.info('ActionExecutor instance not found for closing.') + logger.info('Shutdown complete.') app = FastAPI(lifespan=lifespan) @@ -663,6 +736,51 @@ if __name__ == '__main__': detail=traceback.format_exc(), ) + @app.post('/update_mcp_server') + async def update_mcp_server(request: Request): + assert mcp_router is not None + assert os.path.exists(MCP_ROUTER_PROFILE_PATH) + + # Use synchronous file operations outside of async function + def read_profile(): + with open(MCP_ROUTER_PROFILE_PATH, 'r') as f: + return json.load(f) + + current_profile = read_profile() + assert 'default' in current_profile + assert isinstance(current_profile['default'], list) + + # Get the request body + mcp_tools_to_sync = await request.json() + if not isinstance(mcp_tools_to_sync, list): + raise HTTPException( + status_code=400, detail='Request must be a list of MCP tools to sync' + ) + + logger.info( + f'Updating MCP server to: {json.dumps(mcp_tools_to_sync, indent=2)}.\nPrevious profile: {json.dumps(current_profile, indent=2)}' + ) + current_profile['default'] = mcp_tools_to_sync + + # Use synchronous file operations outside of async function + def write_profile(profile): + with open(MCP_ROUTER_PROFILE_PATH, 'w') as f: + json.dump(profile, f) + + write_profile(current_profile) + + # Manually reload the profile and update the servers + mcp_router.profile_manager.reload() + servers_wait_for_update = mcp_router.get_unique_servers() + await mcp_router.update_servers(servers_wait_for_update) + logger.info( + f'MCP router updated successfully with unique servers: {servers_wait_for_update}' + ) + + return JSONResponse( + status_code=200, content={'detail': 'MCP server updated successfully'} + ) + @app.post('/upload_file') async def upload_file( file: UploadFile, destination: str = '/', recursive: bool = False diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index ceb04f407a..00ca450aa5 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -30,7 +30,7 @@ from openhands.events.action import ( FileWriteAction, IPythonRunCellAction, ) -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.event import Event from openhands.events.observation import ( AgentThinkObservation, @@ -282,9 +282,8 @@ class Runtime(FileEditRuntimeMixin): assert event.timeout is not None try: await self._export_latest_git_provider_tokens(event) - if isinstance(event, McpAction): - # we don't call call_tool_mcp impl directly because there can be other action ActionExecutionClient - observation: Observation = await getattr(self, McpAction.action)(event) + if isinstance(event, MCPAction): + observation: Observation = await self.call_tool_mcp(event) else: observation = await call_sync_from_async(self.run_action, event) except Exception as e: @@ -571,7 +570,7 @@ class Runtime(FileEditRuntimeMixin): pass @abstractmethod - async def call_tool_mcp(self, action: McpAction) -> Observation: + async def call_tool_mcp(self, action: MCPAction) -> Observation: pass # ==================================================================== diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index f780ab63c5..91469de9ee 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -1,3 +1,4 @@ +import asyncio import os import tempfile import threading @@ -10,6 +11,7 @@ import httpx from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from openhands.core.config import AppConfig +from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig from openhands.core.exceptions import ( AgentRuntimeTimeoutError, ) @@ -27,7 +29,7 @@ from openhands.events.action import ( ) from openhands.events.action.action import Action from openhands.events.action.files import FileEditSource -from openhands.events.action.mcp import McpAction +from openhands.events.action.mcp import MCPAction from openhands.events.observation import ( AgentThinkObservation, ErrorObservation, @@ -38,16 +40,12 @@ from openhands.events.observation import ( from openhands.events.serialization import event_to_dict, observation_from_dict from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS from openhands.integrations.provider import PROVIDER_TOKEN_TYPE -from openhands.mcp import MCPClient, create_mcp_clients -from openhands.mcp import call_tool_mcp as call_tool_mcp_handler from openhands.runtime.base import Runtime from openhands.runtime.plugins import PluginRequirement from openhands.runtime.utils.request import send_request -from openhands.utils.async_utils import call_async_from_sync from openhands.utils.http_session import HttpSession from openhands.utils.tenacity_stop import stop_if_should_exit - def _is_retryable_error(exception): return isinstance( exception, (httpx.RemoteProtocolError, httpcore.RemoteProtocolError) @@ -79,7 +77,6 @@ class ActionExecutionClient(Runtime): self._runtime_initialized: bool = False self._runtime_closed: bool = False self._vscode_token: str | None = None # initial dummy value - self.mcp_clients: list[MCPClient] | None = None super().__init__( config, event_stream, @@ -329,19 +326,59 @@ class ActionExecutionClient(Runtime): def browse_interactive(self, action: BrowseInteractiveAction) -> Observation: return self.send_action_for_execution(action) - async def call_tool_mcp(self, action: McpAction) -> Observation: - if self.mcp_clients is None: - self.log( - 'debug', - f'Creating MCP clients with servers: {self.config.mcp.mcp_servers}', - ) - self.mcp_clients = await create_mcp_clients(self.config.mcp.mcp_servers) - return await call_tool_mcp_handler(self.mcp_clients, action) + def get_updated_mcp_config(self) -> MCPConfig: + # Add the runtime as another MCP server + updated_mcp_config = self.config.mcp.model_copy() + # Send a request to the action execution server to updated MCP config + stdio_tools = [ + server.model_dump(mode='json') + for server in updated_mcp_config.stdio_servers + ] + self.log('debug', f'Updating MCP server to: {stdio_tools}') + response = self._send_action_server_request( + 'POST', + f'{self.action_execution_server_url}/update_mcp_server', + json=stdio_tools, + timeout=10, + ) + if response.status_code != 200: + raise RuntimeError(f'Failed to update MCP server: {response.text}') - async def aclose(self) -> None: - if self.mcp_clients: - for client in self.mcp_clients: - await client.disconnect() + # No API key by default. Child runtime can override this when appropriate + updated_mcp_config.sse_servers.append( + MCPSSEServerConfig( + url=self.action_execution_server_url.rstrip('/') + '/sse', api_key=None + ) + ) + self.log( + 'debug', + f'Updated MCP config by adding runtime as another server: {updated_mcp_config}', + ) + return updated_mcp_config + + async def call_tool_mcp(self, action: MCPAction) -> Observation: + # Import here to avoid circular imports + from openhands.mcp.utils import create_mcp_clients, call_tool_mcp as call_tool_mcp_handler + + # Get the updated MCP config + updated_mcp_config = self.get_updated_mcp_config() + self.log( + 'debug', + f'Creating MCP clients with servers: {updated_mcp_config.sse_servers}', + ) + + # Create clients for this specific operation + mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers) + + # Call the tool and return the result + # No need for try/finally since disconnect() is now just resetting state + result = await call_tool_mcp_handler(mcp_clients, action) + + # Reset client state (no active connections to worry about) + for client in mcp_clients: + await client.disconnect() + + return result def close(self) -> None: # Make sure we don't close the session multiple times @@ -350,4 +387,3 @@ class ActionExecutionClient(Runtime): return self._runtime_closed = True self.session.close() - call_async_from_sync(self.aclose) diff --git a/openhands/runtime/mcp/config.json b/openhands/runtime/mcp/config.json new file mode 100644 index 0000000000..6bb5967d80 --- /dev/null +++ b/openhands/runtime/mcp/config.json @@ -0,0 +1,3 @@ +{ + "default": [] +} diff --git a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 index a381edc11a..b0c08560c2 100644 --- a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 +++ b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 @@ -34,10 +34,12 @@ RUN apt-get update && \ {% if 'ubuntu' in base_image %} RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \ npm install -g corepack && corepack enable yarn && \ - curl -fsSL --compressed https://install.python-poetry.org | python - && \ - curl -LsSf https://astral.sh/uv/install.sh | sh + curl -fsSL --compressed https://install.python-poetry.org | python - {% endif %} +# Install uv (required by MCP) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + # Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \ (if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi) diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index 2e2596e935..bd58a9b7c4 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -18,6 +18,7 @@ from openhands.events.event import Event, EventSource from openhands.events.stream import EventStream from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler from openhands.integrations.service_types import Repository +from openhands.mcp import add_mcp_tools_to_agent from openhands.memory.memory import Memory from openhands.microagent.microagent import BaseMicroagent from openhands.runtime import get_runtime_cls @@ -124,6 +125,11 @@ class AgentSession: selected_branch=selected_branch, ) + # NOTE: this needs to happen before controller is created + # so MCP tools can be included into the SystemMessageAction + if self.runtime and runtime_connected: + await add_mcp_tools_to_agent(agent, self.runtime, config.mcp) + if replay_json: initial_message = self._run_replay( initial_message, @@ -148,6 +154,7 @@ class AgentSession: repo_directory = None if self.runtime and runtime_connected and selected_repository: repo_directory = selected_repository.full_name.split('/')[-1] + self.memory = await self._create_memory( selected_repository=selected_repository, repo_directory=repo_directory, diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index 40f57df837..2898857367 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -25,7 +25,6 @@ from openhands.events.observation.error import ErrorObservation from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.stream import EventStreamSubscriber from openhands.llm.llm import LLM -from openhands.mcp import fetch_mcp_tools_from_config from openhands.server.session.agent_session import AgentSession from openhands.server.session.conversation_init_data import ConversationInitData from openhands.storage.data_models.settings import Settings @@ -147,9 +146,7 @@ class Session: self.logger.info(f'Enabling default condenser: {default_condenser_config}') agent_config.condenser = default_condenser_config - mcp_tools = await fetch_mcp_tools_from_config(self.config.mcp) agent = Agent.get_cls(agent_cls)(llm, agent_config) - agent.set_mcp_tools(mcp_tools) git_provider_tokens = None selected_repository = None diff --git a/poetry.lock b/poetry.lock index 8441137417..536d425aed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1144,7 +1144,7 @@ version = "1.2.2.post1" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, @@ -1162,6 +1162,28 @@ typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", " uv = ["uv (>=0.1.18)"] virtualenv = ["virtualenv (>=20.0.35)"] +[[package]] +name = "cachecontrol" +version = "0.14.3" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae"}, + {file = "cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11"}, +] + +[package.dependencies] +filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} +msgpack = ">=0.5.2,<2.0.0" +requests = ">=2.16.0" + +[package.extras] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] + [[package]] name = "cachetools" version = "5.5.2" @@ -1393,6 +1415,22 @@ files = [ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, +] + +[package.dependencies] +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=3.0.0,<4.0.0" + [[package]] name = "click" version = "8.1.8" @@ -1431,7 +1469,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "comm" @@ -1629,6 +1667,18 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] + [[package]] name = "cryptography" version = "44.0.2" @@ -1920,7 +1970,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" -groups = ["dev", "evaluation"] +groups = ["main", "dev", "evaluation"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -1973,6 +2023,134 @@ files = [ {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, ] +[[package]] +name = "duckdb" +version = "1.2.2" +description = "DuckDB in-process database" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "duckdb-1.2.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6e5e6c333b550903ff11919ed1154c60c9b9d935db51afdb263babe523a8a69e"}, + {file = "duckdb-1.2.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:c1fcbc579de8e4fa7e34242fd6f419c1a39520073b1fe0c29ed6e60ed5553f38"}, + {file = "duckdb-1.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:690885060c4140922ffa2f6935291c6e74ddad0ca2cf33bff66474ce89312ab3"}, + {file = "duckdb-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a382782980643f5ee827990b76f079b22f47786509061c0afac28afaa5b8bf5"}, + {file = "duckdb-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c33345570ed8c50c9fe340c2767470115cc02d330f25384104cfad1f6e54f5"}, + {file = "duckdb-1.2.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b744f8293ce649d802a9eabbf88e4930d672cf9de7d4fc9af5d14ceaeeec5805"}, + {file = "duckdb-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8680e81b0c77be9fc968c1dd4cd38395c34b18bb693cbfc7b7742c18221cc9b"}, + {file = "duckdb-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:fb41f2035a70378b3021f724bb08b047ca4aa475850a3744c442570054af3c52"}, + {file = "duckdb-1.2.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:081110ffbc9d53c9740ef55482c93b97db2f8030d681d1658827d2e94f77da03"}, + {file = "duckdb-1.2.2-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:53a154dbc074604036a537784ce5d1468edf263745a4363ca06fdb922f0d0a99"}, + {file = "duckdb-1.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0353f80882c066f7b14451852395b7a360f3d4846a10555c4268eb49144ea11c"}, + {file = "duckdb-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b134a5002757af1ae44a9ae26c2fe963ffa09eb47a62779ce0c5eeb44bfc2f28"}, + {file = "duckdb-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd9c434127fd1575694e1cf19a393bed301f5d6e80b4bcdae80caa368a61a678"}, + {file = "duckdb-1.2.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:890f58855d127c25bc3a53f4c24b27e79391c4468c4fcc99bc10d87b5d4bd1c4"}, + {file = "duckdb-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a5002305cdd4e76c94b61b50abc5e3f4e32c9cb81116960bb4b74acbbc9c6c8"}, + {file = "duckdb-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:cdb9999c6a109aa31196cdd22fc58a810a3d35d08181a25d1bf963988e89f0a5"}, + {file = "duckdb-1.2.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f745379f44ad302560688855baaed9739c03b37a331338eda6a4ac655e4eb42f"}, + {file = "duckdb-1.2.2-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:087713fc5958cae5eb59097856b3deaae0def021660c8f2052ec83fa8345174a"}, + {file = "duckdb-1.2.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:a1f96395319c447a31b9477881bd84b4cb8323d6f86f21ceaef355d22dd90623"}, + {file = "duckdb-1.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6aba3bc0acf4f8d52b94f7746c3b0007b78b517676d482dc516d63f48f967baf"}, + {file = "duckdb-1.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5c1556775a9ebaa49b5c8d64718f155ac3e05b34a49e9c99443cf105e8b0371"}, + {file = "duckdb-1.2.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d625cc7d2faacfb2fc83ebbe001ae75dda175b3d8dce6a51a71c199ffac3627a"}, + {file = "duckdb-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:73263f81545c5cb4360fbaf7b22a493e55ddf88fadbe639c43efb7bc8d7554c4"}, + {file = "duckdb-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b1c0c4d737fd2ab9681e4e78b9f361e0a827916a730e84fa91e76dca451b14d5"}, + {file = "duckdb-1.2.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:fb9a2c77236fae079185a990434cb9d8432902488ba990235c702fc2692d2dcd"}, + {file = "duckdb-1.2.2-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:d8bb89e580cb9a3aaf42e4555bf265d3db9446abfb118e32150e1a5dfa4b5b15"}, + {file = "duckdb-1.2.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:88916d7f0532dc926bed84b50408c00dcbe6d2097d0de93c3ff647d8d57b4f83"}, + {file = "duckdb-1.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30bece4f58a6c7bb0944a02dd1dc6de435a9daf8668fa31a9fe3a9923b20bd65"}, + {file = "duckdb-1.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd2c6373b8b54474724c2119f6939c4568c428e1d0be5bcb1f4e3d7f1b7c8bb"}, + {file = "duckdb-1.2.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f688a8b0df7030c5a28ca6072817c1f090979e08d28ee5912dee37c26a7d0c"}, + {file = "duckdb-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26e9c349f56f7c99341b5c79bbaff5ba12a5414af0261e79bf1a6a2693f152f6"}, + {file = "duckdb-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1aec7102670e59d83512cf47d32a6c77a79df9df0294c5e4d16b6259851e2e9"}, + {file = "duckdb-1.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b374e7e2c474d6cd65fd80a94ff7263baec4be14ea193db4076d54eab408f9"}, + {file = "duckdb-1.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0fc6512d26eac83521938d7de65645ec08b04c2dc7807d4e332590c667e9d78"}, + {file = "duckdb-1.2.2-cp37-cp37m-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b451d16c3931fdbc235a12a39217a2faa03fa7c84c8560e65bc9b706e876089"}, + {file = "duckdb-1.2.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:f3f8e09029ae47d3b904d32a03149ffc938bb3fb8a3048dc7b2d0f2ab50e0f56"}, + {file = "duckdb-1.2.2-cp38-cp38-macosx_12_0_universal2.whl", hash = "sha256:cee19d0c5bcb143b851ebd3ffc91e3445c5c3ee3cc0106edd882dd5b4091d5c0"}, + {file = "duckdb-1.2.2-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:c0f86c5e4ab7d4007ca0baa1707486daa38869c43f552a56e9cd2a28d431c2ae"}, + {file = "duckdb-1.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378ef6a3d1a8b50da5a89376cc0cc6f131102d4a27b4b3adef10b20f7a6ea49f"}, + {file = "duckdb-1.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b985d13e161c27e8b947af28658d460925bade61cb5d7431b8258a807cc83752"}, + {file = "duckdb-1.2.2-cp38-cp38-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446a5db77caeb155bcc0874c162a51f6d023af4aa2563fffbdec555db7402a35"}, + {file = "duckdb-1.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:0c1a3496695c7220ac83dde02fc1cf174359c8072a6880050c8ae6b5c62a2635"}, + {file = "duckdb-1.2.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:25ac669180f88fecca20f300b898e191f81aa674d51dde8a328bdeb28a572ab0"}, + {file = "duckdb-1.2.2-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:d42e7e545d1059e6b73d0f0baa9ae34c90684bfd8c862e70b0d8ab92e01e0e3f"}, + {file = "duckdb-1.2.2-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:f3ce127bcecc723f1c7bddbc57f0526d11128cb05bfd81ffcd5e69e2dd5a1624"}, + {file = "duckdb-1.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2418937adb9d6d0ca823bd385b914495294db27bc2963749d54af6708757f679"}, + {file = "duckdb-1.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d41f899ce7979e7b3f9097ebce70da5c659db2d81d08c07a72b2b50f869859"}, + {file = "duckdb-1.2.2-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85e90a9c5307cf4d9151844e60c80f492618ea6e9b71081020e7d462e071ac8f"}, + {file = "duckdb-1.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:df8c8a4ec998139b8507213c44c50e24f62a36af1cfded87e8972173dc9f8baf"}, + {file = "duckdb-1.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6507ad2445cd3479853fb6473164b5eb5b22446d283c9892cfbbd0a85c5f361d"}, + {file = "duckdb-1.2.2.tar.gz", hash = "sha256:1e53555dece49201df08645dbfa4510c86440339889667702f936b7d28d39e43"}, +] + +[[package]] +name = "dulwich" +version = "0.22.8" +description = "Python Git Library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dulwich-0.22.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546176d18b8cc0a492b0f23f07411e38686024cffa7e9d097ae20512a2e57127"}, + {file = "dulwich-0.22.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d2434dd72b2ae09b653c9cfe6764a03c25cfbd99fbbb7c426f0478f6fb1100f"}, + {file = "dulwich-0.22.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8318bc0921d42e3e69f03716f983a301b5ee4c8dc23c7f2c5bbb28581257a9"}, + {file = "dulwich-0.22.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7a0f96a2a87f3b4f7feae79d2ac6b94107d6b7d827ac08f2f331b88c8f597a1"}, + {file = "dulwich-0.22.8-cp310-cp310-win32.whl", hash = "sha256:432a37b25733202897b8d67cdd641688444d980167c356ef4e4dd15a17a39a24"}, + {file = "dulwich-0.22.8-cp310-cp310-win_amd64.whl", hash = "sha256:f3a15e58dac8b8a76073ddca34e014f66f3672a5540a99d49ef6a9c09ab21285"}, + {file = "dulwich-0.22.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0852edc51cff4f4f62976bdaa1d82f6ef248356c681c764c0feb699bc17d5782"}, + {file = "dulwich-0.22.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:826aae8b64ac1a12321d6b272fc13934d8f62804fda2bc6ae46f93f4380798eb"}, + {file = "dulwich-0.22.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ae726f923057d36cdbb9f4fb7da0d0903751435934648b13f1b851f0e38ea1"}, + {file = "dulwich-0.22.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6987d753227f55cf75ba29a8dab69d1d83308ce483d7a8c6d223086f7a42e125"}, + {file = "dulwich-0.22.8-cp311-cp311-win32.whl", hash = "sha256:7757b4a2aad64c6f1920082fc1fccf4da25c3923a0ae7b242c08d06861dae6e1"}, + {file = "dulwich-0.22.8-cp311-cp311-win_amd64.whl", hash = "sha256:12b243b7e912011c7225dc67480c313ac8d2990744789b876016fb593f6f3e19"}, + {file = "dulwich-0.22.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d81697f74f50f008bb221ab5045595f8a3b87c0de2c86aa55be42ba97421f3cd"}, + {file = "dulwich-0.22.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bff1da8e2e6a607c3cb45f5c2e652739589fe891245e1d5b770330cdecbde41"}, + {file = "dulwich-0.22.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9969099e15b939d3936f8bee8459eaef7ef5a86cd6173393a17fe28ca3d38aff"}, + {file = "dulwich-0.22.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:017152c51b9a613f0698db28c67cf3e0a89392d28050dbf4f4ac3f657ea4c0dc"}, + {file = "dulwich-0.22.8-cp312-cp312-win32.whl", hash = "sha256:ee70e8bb8798b503f81b53f7a103cb869c8e89141db9005909f79ab1506e26e9"}, + {file = "dulwich-0.22.8-cp312-cp312-win_amd64.whl", hash = "sha256:dc89c6f14dcdcbfee200b0557c59ae243835e42720be143526d834d0e53ed3af"}, + {file = "dulwich-0.22.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbade3342376be1cd2409539fe1b901d2d57a531106bbae204da921ef4456a74"}, + {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71420ffb6deebc59b2ce875e63d814509f9c1dc89c76db962d547aebf15670c7"}, + {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a626adbfac44646a125618266a24133763bdc992bf8bd0702910d67e6b994443"}, + {file = "dulwich-0.22.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f1476c9c4e4ede95714d06c4831883a26680e37b040b8b6230f506e5ba39f51"}, + {file = "dulwich-0.22.8-cp313-cp313-win32.whl", hash = "sha256:b2b31913932bb5bd41658dd398b33b1a2d4d34825123ad54e40912cfdfe60003"}, + {file = "dulwich-0.22.8-cp313-cp313-win_amd64.whl", hash = "sha256:7a44e5a61a7989aca1e301d39cfb62ad2f8853368682f524d6e878b4115d823d"}, + {file = "dulwich-0.22.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9cd0c67fb44a38358b9fcabee948bf11044ef6ce7a129e50962f54c176d084e"}, + {file = "dulwich-0.22.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b79b94726c3f4a9e5a830c649376fd0963236e73142a4290bac6bc9fc9cb120"}, + {file = "dulwich-0.22.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16bbe483d663944972e22d64e1f191201123c3b5580fbdaac6a4f66bfaa4fc11"}, + {file = "dulwich-0.22.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e02d403af23d93dc1f96eb2408e25efd50046e38590a88c86fa4002adc9849b0"}, + {file = "dulwich-0.22.8-cp39-cp39-win32.whl", hash = "sha256:8bdd9543a77fb01be704377f5e634b71f955fec64caa4a493dc3bfb98e3a986e"}, + {file = "dulwich-0.22.8-cp39-cp39-win_amd64.whl", hash = "sha256:3b6757c6b3ba98212b854a766a4157b9cb79a06f4e1b06b46dec4bd834945b8e"}, + {file = "dulwich-0.22.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7bb18fa09daa1586c1040b3e2777d38d4212a5cdbe47d384ba66a1ac336fcc4c"}, + {file = "dulwich-0.22.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2fda8e87907ed304d4a5962aea0338366144df0df60f950b8f7f125871707f"}, + {file = "dulwich-0.22.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1748cd573a0aee4d530bc223a23ccb8bb5b319645931a37bd1cfb68933b720c1"}, + {file = "dulwich-0.22.8-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a631b2309feb9a9631eabd896612ba36532e3ffedccace57f183bb868d7afc06"}, + {file = "dulwich-0.22.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:00e7d9a3d324f9e0a1b27880eec0e8e276ff76519621b66c1a429ca9eb3f5a8d"}, + {file = "dulwich-0.22.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f8aa3de93201f9e3e40198725389aa9554a4ee3318a865f96a8e9bc9080f0b25"}, + {file = "dulwich-0.22.8-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e8da9dd8135884975f5be0563ede02179240250e11f11942801ae31ac293f37"}, + {file = "dulwich-0.22.8-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc5ce2435fb3abdf76f1acabe48f2e4b3f7428232cadaef9daaf50ea7fa30ee"}, + {file = "dulwich-0.22.8-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:982b21cc3100d959232cadb3da0a478bd549814dd937104ea50f43694ec27153"}, + {file = "dulwich-0.22.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6bde2b13a05cc0ec2ecd4597a99896663544c40af1466121f4d046119b874ce3"}, + {file = "dulwich-0.22.8-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6d446cb7d272a151934ad4b48ba691f32486d5267cf2de04ee3b5e05fc865326"}, + {file = "dulwich-0.22.8-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f6338e6cf95cd76a0191b3637dc3caed1f988ae84d8e75f876d5cd75a8dd81a"}, + {file = "dulwich-0.22.8-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e004fc532ea262f2d5f375068101ca4792becb9d4aa663b050f5ac31fda0bb5c"}, + {file = "dulwich-0.22.8-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bfdbc6fa477dee00d04e22d43a51571cd820cfaaaa886f0f155b8e29b3e3d45"}, + {file = "dulwich-0.22.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ae900c8e573f79d714c1d22b02cdadd50b64286dd7203028f0200f82089e4950"}, + {file = "dulwich-0.22.8-py3-none-any.whl", hash = "sha256:ffc7a02e62b72884de58baaa3b898b7f6427893e79b1289ffa075092efe59181"}, + {file = "dulwich-0.22.8.tar.gz", hash = "sha256:701547310415de300269331abe29cb5717aa1ea377af826bf513d0adfb1c209b"}, +] + +[package.dependencies] +urllib3 = ">=1.25" + +[package.extras] +dev = ["mypy (==1.15.0)", "ruff (==0.9.7)"] +fastimport = ["fastimport"] +https = ["urllib3 (>=1.24.1)"] +paramiko = ["paramiko"] +pgp = ["gpg"] + [[package]] name = "e2b" version = "1.3.3" @@ -2165,7 +2343,7 @@ version = "2.21.1" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, @@ -2191,6 +2369,21 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "findpython" +version = "0.6.3" +description = "A utility to find python versions on your system" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "findpython-0.6.3-py3-none-any.whl", hash = "sha256:a85bb589b559cdf1b87227cc233736eb7cad894b9e68021ee498850611939ebc"}, + {file = "findpython-0.6.3.tar.gz", hash = "sha256:5863ea55556d8aadc693481a14ac4f3624952719efc1c5591abb0b4a9e965c94"}, +] + +[package.dependencies] +packaging = ">=20" + [[package]] name = "flake8" version = "7.2.0" @@ -2664,7 +2857,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -3515,6 +3708,18 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "installer" +version = "0.7.0" +description = "A library for installing Python wheels." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, +] + [[package]] name = "ipykernel" version = "6.29.5" @@ -3645,6 +3850,64 @@ files = [ {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, + {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + [[package]] name = "jedi" version = "0.19.2" @@ -3665,6 +3928,23 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + [[package]] name = "jinja2" version = "3.1.6" @@ -4128,6 +4408,35 @@ files = [ {file = "jupyterlab_widgets-3.0.14.tar.gz", hash = "sha256:bad03e59546869f026e537e0d170e454259e6dc7048e14041707ca31e523c8a1"}, ] +[[package]] +name = "keyring" +version = "25.6.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, + {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, +] + +[package.dependencies] +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -4824,6 +5133,30 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] +[[package]] +name = "mcpm" +version = "1.8.0" +description = "MCPM - Model Context Protocol Manager" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mcpm-1.8.0-py3-none-any.whl", hash = "sha256:d7ca53fb67535cb3072e6c17d0cd07d740ca0df3b73e05c46a8f7708584b5299"}, + {file = "mcpm-1.8.0.tar.gz", hash = "sha256:8faaf3e6377b37c0bd5cb6656f2569a41daf7ade25842656462da66e9821c445"}, +] + +[package.dependencies] +click = ">=8.1.3" +duckdb = ">=1.2.2" +mcp = ">=1.6.0" +prompt-toolkit = ">=3.0.0" +psutil = ">=7.0.0" +pydantic = ">=2.5.1" +requests = ">=2.28.0" +rich = ">=12.0.0" +ruamel-yaml = ">=0.18.10" +watchfiles = ">=1.0.4" + [[package]] name = "mdurl" version = "0.1.2" @@ -4909,6 +5242,18 @@ types-toml = "*" typing_extensions = ">=4.6,<5.0" watchfiles = "*" +[[package]] +name = "more-itertools" +version = "10.7.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, + {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -4927,6 +5272,80 @@ docs = ["sphinx"] gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] tests = ["pytest (>=4.6)"] +[[package]] +name = "msgpack" +version = "1.1.0" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, + {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, + {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, + {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, + {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + [[package]] name = "multidict" version = "6.1.0" @@ -5905,6 +6324,27 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pbs-installer" +version = "2025.4.9" +description = "Installer for Python Build Standalone" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pbs_installer-2025.4.9-py3-none-any.whl", hash = "sha256:af110b398248584422f46760ce1e3793622fe3fbcde47aacd22e35baf8c3db1d"}, + {file = "pbs_installer-2025.4.9.tar.gz", hash = "sha256:15755bc94769a544af5dda155f973c70caf76f0e70b21f3c8a8ed506f102f88f"}, +] + +[package.dependencies] +httpx = {version = ">=0.27.0,<1", optional = true, markers = "extra == \"download\""} +zstandard = {version = ">=0.21.0", optional = true, markers = "extra == \"install\""} + +[package.extras] +all = ["pbs-installer[download,install]"] +download = ["httpx (>=0.27.0,<1)"] +install = ["zstandard (>=0.21.0)"] + [[package]] name = "pexpect" version = "4.9.0" @@ -6009,6 +6449,21 @@ tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "ole typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] +[[package]] +name = "pkginfo" +version = "1.12.1.2" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, + {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov", "wheel"] + [[package]] name = "platformdirs" version = "4.3.6" @@ -6064,6 +6519,53 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "poetry" +version = "2.1.2" +description = "Python dependency management and packaging made easy." +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "poetry-2.1.2-py3-none-any.whl", hash = "sha256:df7dfe7e5f9cd50ed3b8d1a013afcc379645f66d7e9aa43728689e34fb016216"}, + {file = "poetry-2.1.2.tar.gz", hash = "sha256:6a0694645ee24ba93cb94254db66e47971344562ddd5578e82bf35e572bc546d"}, +] + +[package.dependencies] +build = ">=1.2.1,<2.0.0" +cachecontrol = {version = ">=0.14.0,<0.15.0", extras = ["filecache"]} +cleo = ">=2.1.0,<3.0.0" +dulwich = ">=0.22.6,<0.23.0" +fastjsonschema = ">=2.18.0,<3.0.0" +findpython = ">=0.6.2,<0.7.0" +installer = ">=0.7.0,<0.8.0" +keyring = ">=25.1.0,<26.0.0" +packaging = ">=24.0" +pbs-installer = {version = ">=2025.1.6,<2026.0.0", extras = ["download", "install"]} +pkginfo = ">=1.12,<2.0" +platformdirs = ">=3.0.0,<5" +poetry-core = "2.1.2" +pyproject-hooks = ">=1.0.0,<2.0.0" +requests = ">=2.26,<3.0" +requests-toolbelt = ">=1.0.0,<2.0.0" +shellingham = ">=1.5,<2.0" +tomlkit = ">=0.11.4,<1.0.0" +trove-classifiers = ">=2022.5.19" +virtualenv = ">=20.26.6,<21.0.0" +xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} + +[[package]] +name = "poetry-core" +version = "2.1.2" +description = "Poetry PEP 517 Build Backend" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "poetry_core-2.1.2-py3-none-any.whl", hash = "sha256:ecb1e8f7d4f071a21cd0feb8c19bd1aec80de6fb0e82aa9d809a591e544431b4"}, + {file = "poetry_core-2.1.2.tar.gz", hash = "sha256:f9dbbbd0ebf9755476a1d57f04b30e9aecf71ca9dc2fcd4b17aba92c0002aa04"}, +] + [[package]] name = "portalocker" version = "3.1.1" @@ -6880,7 +7382,7 @@ version = "1.2.0" description = "Wrappers to call pyproject.toml-based build backend hooks." optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, @@ -7202,6 +7704,19 @@ files = [ ] markers = {main = "sys_platform == \"win32\"", evaluation = "sys_platform == \"win32\" or platform_system == \"Windows\"", runtime = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pywinpty" version = "2.0.15" @@ -7454,7 +7969,7 @@ version = "3.12.2" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.9" -groups = ["testgeneval"] +groups = ["main", "testgeneval"] files = [ {file = "rapidfuzz-3.12.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b9a75e0385a861178adf59e86d6616cbd0d5adca7228dc9eeabf6f62cf5b0b1"}, {file = "rapidfuzz-3.12.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6906a7eb458731e3dd2495af1d0410e23a21a2a2b7ced535e6d5cd15cb69afc5"}, @@ -7757,6 +8272,21 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "retry" version = "0.9.2" @@ -7962,6 +8492,82 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruamel-yaml" +version = "0.18.10" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version == \"3.12\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +] + [[package]] name = "ruff" version = "0.11.7" @@ -8243,6 +8849,23 @@ dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "sys_platform == \"linux\"" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "send2trash" version = "1.8.3" @@ -8885,6 +9508,18 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + [[package]] name = "torch" version = "2.4.1" @@ -9275,7 +9910,7 @@ description = "A language and compiler for custom Deep Learning operations" optional = false python-versions = "*" groups = ["evaluation"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\"" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version == \"3.12\"" files = [ {file = "triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a"}, {file = "triton-3.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ce8520437c602fb633f1324cc3871c47bee3b67acf9756c1a66309b60e3216c"}, @@ -9292,6 +9927,18 @@ build = ["cmake (>=3.20)", "lit"] tests = ["autopep8", "flake8", "isort", "llnl-hatchet", "numpy", "pytest", "scipy (>=1.7.1)"] tutorials = ["matplotlib", "pandas", "tabulate"] +[[package]] +name = "trove-classifiers" +version = "2025.4.28.22" +description = "Canonical source for classifiers on PyPI (pypi.org)." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "trove_classifiers-2025.4.28.22-py3-none-any.whl", hash = "sha256:fdb453fefa3a0da9c18b8d390846e6df7e961e8924703559ea9be07ec99c0925"}, + {file = "trove_classifiers-2025.4.28.22.tar.gz", hash = "sha256:42bef4957a74fe7724b8310dafd4b23e0a71406a4812cf4dfd65e2ee34f1943d"}, +] + [[package]] name = "typer" version = "0.15.2" @@ -9537,7 +10184,7 @@ version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" -groups = ["dev", "evaluation"] +groups = ["main", "dev", "evaluation"] files = [ {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, @@ -9932,6 +10579,93 @@ files = [ [package.dependencies] h11 = ">=0.9.0,<1" +[[package]] +name = "xattr" +version = "1.1.4" +description = "Python wrapper for extended filesystem attributes" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "xattr-1.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb85b6249e9f3ea10cbb56df1021d43f4027212f0d004304bc9075dc7f54769"}, + {file = "xattr-1.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a848ab125c0fafdc501ccd83b4c9018bba576a037a4ca5960a22f39e295552e"}, + {file = "xattr-1.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:467ee77471d26ae5187ee7081b82175b5ca56ead4b71467ec2e6119d1b08beed"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd35f46cb0154f7033f9d5d0960f226857acb0d1e0d71fd7af18ed84663007c"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d956478e9bb98a1efd20ebc6e5703497c1d2d690d5a13c4df4abf59881eed50"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f25dfdcd974b700fb04a40e14a664a80227ee58e02ea062ac241f0d7dc54b4e"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33b63365c1fcbc80a79f601575bac0d6921732e0245b776876f3db3fcfefe22d"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:544542be95c9b49e211f0a463758f200de88ba6d5a94d3c4f42855a484341acd"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac14c9893f3ea046784b7702be30889b200d31adcd2e6781a8a190b6423f9f2d"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bb4bbe37ba95542081890dd34fa5347bef4651e276647adaa802d5d0d7d86452"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da489ecef798705f9a39ea8cea4ead0d1eeed55f92c345add89740bd930bab6"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:798dd0cbe696635a6f74b06fc430818bf9c3b24314e1502eadf67027ab60c9b0"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2b6361626efad5eb5a6bf8172c6c67339e09397ee8140ec41258737bea9681"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7fa20a0c9ce022d19123b1c5b848d00a68b837251835a7929fe041ee81dcd0"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20eeb08e2c57fc7e71f050b1cfae35cbb46105449853a582bf53fd23c5379e"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:477370e75821bded901487e5e752cffe554d1bd3bd4839b627d4d1ee8c95a093"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a8682091cd34a9f4a93c8aaea4101aae99f1506e24da00a3cc3dd2eca9566f21"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e079b3b1a274ba2121cf0da38bbe5c8d2fb1cc49ecbceb395ce20eb7d69556d"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ae6579dea05bf9f335a082f711d5924a98da563cac72a2d550f5b940c401c0e9"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd6038ec9df2e67af23c212693751481d5f7e858156924f14340376c48ed9ac7"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:608b2877526674eb15df4150ef4b70b7b292ae00e65aecaae2f192af224be200"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54dad1a6a998c6a23edfd25e99f4d38e9b942d54e518570044edf8c767687ea"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0dab6ff72bb2b508f3850c368f8e53bd706585012676e1f71debba3310acde8"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a3c54c6af7cf09432b2c461af257d5f4b1cb2d59eee045f91bacef44421a46d"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e346e05a158d554639fbf7a0db169dc693c2d2260c7acb3239448f1ff4a9d67f"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3ff6d9e2103d0d6e5fcd65b85a2005b66ea81c0720a37036445faadc5bbfa424"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a2ee4563c6414dfec0d1ac610f59d39d5220531ae06373eeb1a06ee37cd193f"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878df1b38cfdadf3184ad8c7b0f516311128d5597b60ac0b3486948953658a83"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c9b8350244a1c5454f93a8d572628ff71d7e2fc2f7480dcf4c4f0e8af3150fe"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a46bf48fb662b8bd745b78bef1074a1e08f41a531168de62b5d7bd331dadb11a"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83fc3c07b583777b1dda6355329f75ca6b7179fe0d1002f1afe0ef96f7e3b5de"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6308b19cff71441513258699f0538394fad5d66e1d324635207a97cb076fd439"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48c00ddc15ddadc9c729cd9504dabf50adb3d9c28f647d4ac9a3df45a046b1a0"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a06136196f26293758e1b244200b73156a0274af9a7349fa201c71c7af3bb9e8"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8fc2631a3c6cfcdc71f7f0f847461839963754e76a2015de71e7e71e3304abc0"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6e1e835f9c938d129dd45e7eb52ebf7d2d6816323dab93ce311bf331f7d2328"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:60dea2d369a6484e8b7136224fc2971e10e2c46340d83ab780924afe78c90066"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85c2b778b09d919523f80f244d799a142302582d76da18903dc693207c4020b0"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ee0abba9e1b890d39141714ff43e9666864ca635ea8a5a2194d989e6b17fe862"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e4174ba7f51f46b95ea7918d907c91cd579575d59e6a2f22ca36a0551026737"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2b05e52e99d82d87528c54c2c5c8c5fb0ba435f85ac6545511aeea136e49925"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a3696fad746be37de34eb73c60ea67144162bd08106a5308a90ce9dea9a3287"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a3a7149439a26b68904c14fdc4587cde4ac7d80303e9ff0fefcfd893b698c976"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:507b36a126ce900dbfa35d4e2c2db92570c933294cba5d161ecd6a89f7b52f43"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9392b417b54923e031041940d396b1d709df1d3779c6744454e1f1c1f4dad4f5"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e9f00315e6c02943893b77f544776b49c756ac76960bea7cb8d7e1b96aefc284"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c8f98775065260140efb348b1ff8d50fd66ddcbf0c685b76eb1e87b380aaffb3"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b471c6a515f434a167ca16c5c15ff34ee42d11956baa749173a8a4e385ff23e7"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee0763a1b7ceb78ba2f78bee5f30d1551dc26daafcce4ac125115fa1def20519"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:099e6e9ce7999b403d36d9cf943105a3d25d8233486b54ec9d1b78623b050433"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e56faef9dde8d969f0d646fb6171883693f88ae39163ecd919ec707fbafa85"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:328156d4e594c9ae63e1072503c168849e601a153ad37f0290743544332d6b6f"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a57a55a27c7864d6916344c9a91776afda6c3b8b2209f8a69b79cdba93fbe128"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3c19cdde08b040df1e99d2500bf8a9cff775ab0e6fa162bf8afe6d84aa93ed04"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c72667f19d3a9acf324aed97f58861d398d87e42314731e7c6ab3ac7850c971"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:67ae934d75ea2563fc48a27c5945749575c74a6de19fdd38390917ddcb0e4f24"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1b0c348dd8523554dc535540d2046c0c8a535bb086561d8359f3667967b6ca"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22284255d2a8e8f3da195bd8e8d43ce674dbc7c38d38cb6ecfb37fae7755d31f"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b38aac5ef4381c26d3ce147ca98fba5a78b1e5bcd6be6755b4908659f2705c6d"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:803f864af528f6f763a5be1e7b1ccab418e55ae0e4abc8bda961d162f850c991"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:40354ebfb5cecd60a5fbb9833a8a452d147486b0ffec547823658556625d98b5"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2abaf5d06be3361bfa8e0db2ee123ba8e92beab5bceed5e9d7847f2145a32e04"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e638e5ffedc3565242b5fa3296899d35161bad771f88d66277b58f03a1ba9fe"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0597e919d116ec39997804288d77bec3777228368efc0f2294b84a527fc4f9c2"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee9455c501d19f065527afda974418b3ef7c61e85d9519d122cd6eb3cb7a00"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:89ed62ce430f5789e15cfc1ccabc172fd8b349c3a17c52d9e6c64ecedf08c265"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b824f4b9259cd8bb6e83c4873cf8bf080f6e4fa034a02fe778e07aba8d345"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fba66faa0016dfc0af3dd7ac5782b5786a1dfb851f9f3455e266f94c2a05a04"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec4b0c3e0a7bcd103f3cf31dd40c349940b2d4223ce43d384a3548992138ef1"}, + {file = "xattr-1.1.4.tar.gz", hash = "sha256:b7b02ecb2270da5b7e7deaeea8f8b528c17368401c2b9d5f63e91f545b45d372"}, +] + +[package.dependencies] +cffi = ">=1.16.0" + +[package.extras] +test = ["pytest"] + [[package]] name = "xlsxwriter" version = "3.2.2" @@ -10268,7 +11002,120 @@ docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] test = ["coverage[toml]", "zope.event", "zope.testing"] testing = ["coverage[toml]", "zope.event", "zope.testing"] +[[package]] +name = "zstandard" +version = "0.23.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, + {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, + {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, + {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, + {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, + {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, + {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, + {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, + {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, + {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, + {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, + {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, + {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, + {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, + {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "e8326a1441d5ce74c017755566e1e0d865551712290c00202d257c931b7dc5bd" +content-hash = "e6c8ca80310e604ba506015337fb7bff3e212f7e507a0277ffda4d02a80cd217" diff --git a/pyproject.toml b/pyproject.toml index 98d7374065..f4d8d25152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,8 @@ mcp = "1.6.0" python-json-logger = "^3.2.1" playwright = "^1.51.0" prompt-toolkit = "^3.0.50" +mcpm = "1.8.0" +poetry = "^2.1.2" anyio = "4.9.0" [tool.poetry.group.dev.dependencies] @@ -99,6 +101,7 @@ gevent = ">=24.2.1,<26.0.0" concurrency = ["gevent"] + [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -128,6 +131,7 @@ ignore = ["D1"] convention = "google" + [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*" diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py index bb0c1eca69..f29681ccd8 100644 --- a/tests/runtime/conftest.py +++ b/tests/runtime/conftest.py @@ -7,7 +7,7 @@ import time import pytest from pytest import TempPathFactory -from openhands.core.config import AppConfig, load_app_config +from openhands.core.config import AppConfig, MCPConfig, load_app_config from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream from openhands.runtime.base import Runtime @@ -214,6 +214,7 @@ def _load_runtime( force_rebuild_runtime: bool = False, runtime_startup_env_vars: dict[str, str] | None = None, docker_runtime_kwargs: dict[str, str] | None = None, + override_mcp_config: MCPConfig | None = None, ) -> tuple[Runtime, AppConfig]: sid = 'rt_' + str(random.randint(100000, 999999)) @@ -256,6 +257,9 @@ def _load_runtime( config.sandbox.base_container_image = base_container_image config.sandbox.runtime_container_image = None + if override_mcp_config is not None: + config.mcp = override_mcp_config + file_store = get_file_store(config.file_store, config.file_store_path) event_stream = EventStream(sid, file_store) diff --git a/tests/runtime/test_mcp_action.py b/tests/runtime/test_mcp_action.py new file mode 100644 index 0000000000..c487047470 --- /dev/null +++ b/tests/runtime/test_mcp_action.py @@ -0,0 +1,78 @@ +"""Bash-related tests for the DockerRuntime, which connects to the ActionExecutor running in the sandbox.""" + +import json +import os + +import pytest +from conftest import ( + _load_runtime, +) + +import openhands +from openhands.core.config import MCPConfig +from openhands.core.config.mcp_config import MCPStdioServerConfig +from openhands.core.logger import openhands_logger as logger +from openhands.events.action import CmdRunAction, MCPAction +from openhands.events.observation import CmdOutputObservation, MCPObservation + +# ============================================================================================================================ +# Bash-specific tests +# ============================================================================================================================ + + +def test_default_activated_tools(): + project_root = os.path.dirname(openhands.__file__) + mcp_config_path = os.path.join(project_root, 'runtime', 'mcp', 'config.json') + assert os.path.exists( + mcp_config_path + ), f'MCP config file not found at {mcp_config_path}' + with open(mcp_config_path, 'r') as f: + mcp_config = json.load(f) + assert 'default' in mcp_config + # no tools are always activated yet + assert len(mcp_config['default']) == 0 + + +@pytest.mark.asyncio +async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands): + mcp_stdio_server_config = MCPStdioServerConfig( + name='fetch', command='uvx', args=['mcp-server-fetch'] + ) + override_mcp_config = MCPConfig(stdio_servers=[mcp_stdio_server_config]) + runtime, config = _load_runtime( + temp_dir, runtime_cls, run_as_openhands, override_mcp_config=override_mcp_config + ) + + # Test browser server + action_cmd = CmdRunAction(command='python3 -m http.server 8000 > server.log 2>&1 &') + logger.info(action_cmd, extra={'msg_type': 'ACTION'}) + obs = runtime.run_action(action_cmd) + logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + + assert isinstance(obs, CmdOutputObservation) + assert obs.exit_code == 0 + assert '[1]' in obs.content + + action_cmd = CmdRunAction(command='sleep 3 && cat server.log') + logger.info(action_cmd, extra={'msg_type': 'ACTION'}) + obs = runtime.run_action(action_cmd) + logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + assert obs.exit_code == 0 + + mcp_action = MCPAction(name='fetch', arguments={'url': 'http://localhost:8000'}) + obs = await runtime.call_tool_mcp(mcp_action) + logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + assert isinstance( + obs, MCPObservation + ), 'The observation should be a MCPObservation.' + + result_json = json.loads(obs.content) + assert not result_json['isError'] + assert len(result_json['content']) == 1 + assert result_json['content'][0]['type'] == 'text' + assert ( + result_json['content'][0]['text'] + == 'Contents of http://localhost:8000/:\n---\n\n* \n\n---' + ) + + runtime.close() diff --git a/tests/unit/test_agent_controller.py b/tests/unit/test_agent_controller.py index 7c2b7bfb81..4b55898fa5 100644 --- a/tests/unit/test_agent_controller.py +++ b/tests/unit/test_agent_controller.py @@ -32,6 +32,9 @@ from openhands.llm import LLM from openhands.llm.metrics import Metrics, TokenUsage from openhands.memory.memory import Memory from openhands.runtime.base import Runtime +from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, +) from openhands.storage.memory import InMemoryFileStore @@ -83,8 +86,12 @@ def test_event_stream(): @pytest.fixture def mock_runtime() -> Runtime: + from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, + ) + runtime = MagicMock( - spec=Runtime, + spec=ActionExecutionClient, event_stream=test_event_stream, ) return runtime @@ -233,7 +240,7 @@ async def test_run_controller_with_fatal_error( mock_agent.llm.metrics = Metrics() mock_agent.llm.config = config.get_llm_config() - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) def on_event(event: Event): if isinstance(event, CmdRunAction): @@ -298,7 +305,7 @@ async def test_run_controller_stop_with_stuck( mock_agent.llm.metrics = Metrics() mock_agent.llm.config = config.get_llm_config() - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) def on_event(event: Event): if isinstance(event, CmdRunAction): @@ -660,7 +667,7 @@ async def test_run_controller_max_iterations_has_metrics( mock_agent.step = agent_step_fn - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) def on_event(event: Event): if isinstance(event, CmdRunAction): @@ -1064,7 +1071,7 @@ async def test_run_controller_with_memory_error(test_event_stream, mock_agent): mock_agent.step = agent_step_fn - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) runtime.event_stream = event_stream # Create a real Memory instance diff --git a/tests/unit/test_agent_session.py b/tests/unit/test_agent_session.py index e955682c23..7d43894b1b 100644 --- a/tests/unit/test_agent_session.py +++ b/tests/unit/test_agent_session.py @@ -11,7 +11,9 @@ from openhands.events import EventStream, EventStreamSubscriber from openhands.llm import LLM from openhands.llm.metrics import Metrics from openhands.memory.memory import Memory -from openhands.runtime.base import Runtime +from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, +) from openhands.server.session.agent_session import AgentSession from openhands.storage.memory import InMemoryFileStore @@ -58,7 +60,7 @@ async def test_agent_session_start_with_no_state(mock_agent): ) # Create a mock runtime and set it up - mock_runtime = MagicMock(spec=Runtime) + mock_runtime = MagicMock(spec=ActionExecutionClient) # Mock the runtime creation to set up the runtime attribute async def mock_create_runtime(*args, **kwargs): @@ -141,7 +143,7 @@ async def test_agent_session_start_with_restored_state(mock_agent): ) # Create a mock runtime and set it up - mock_runtime = MagicMock(spec=Runtime) + mock_runtime = MagicMock(spec=ActionExecutionClient) # Mock the runtime creation to set up the runtime attribute async def mock_create_runtime(*args, **kwargs): diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 9896495376..6208f69b2e 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -123,7 +123,7 @@ def mock_settings_store(): @patch('openhands.core.cli.display_runtime_initialization_message') @patch('openhands.core.cli.display_initialization_animation') @patch('openhands.core.cli.create_agent') -@patch('openhands.core.cli.fetch_mcp_tools_from_config') +@patch('openhands.core.cli.add_mcp_tools_to_agent') @patch('openhands.core.cli.create_runtime') @patch('openhands.core.cli.create_controller') @patch('openhands.core.cli.create_memory') @@ -137,7 +137,7 @@ async def test_run_session_without_initial_action( mock_create_memory, mock_create_controller, mock_create_runtime, - mock_fetch_mcp_tools, + mock_add_mcp_tools, mock_create_agent, mock_display_animation, mock_display_runtime_init, @@ -154,9 +154,6 @@ async def test_run_session_without_initial_action( mock_agent = AsyncMock() mock_create_agent.return_value = mock_agent - mock_mcp_tools = [] - mock_fetch_mcp_tools.return_value = mock_mcp_tools - mock_runtime = AsyncMock() mock_runtime.event_stream = MagicMock() mock_create_runtime.return_value = mock_runtime @@ -193,8 +190,9 @@ async def test_run_session_without_initial_action( mock_display_runtime_init.assert_called_once_with('local') mock_display_animation.assert_called_once() mock_create_agent.assert_called_once_with(mock_config) - mock_fetch_mcp_tools.assert_called_once() - mock_agent.set_mcp_tools.assert_called_once_with(mock_mcp_tools) + mock_add_mcp_tools.assert_called_once_with( + mock_agent, mock_runtime, mock_config.mcp + ) mock_create_runtime.assert_called_once() mock_create_controller.assert_called_once() mock_create_memory.assert_called_once() @@ -213,7 +211,7 @@ async def test_run_session_without_initial_action( @patch('openhands.core.cli.display_runtime_initialization_message') @patch('openhands.core.cli.display_initialization_animation') @patch('openhands.core.cli.create_agent') -@patch('openhands.core.cli.fetch_mcp_tools_from_config') +@patch('openhands.core.cli.add_mcp_tools_to_agent') @patch('openhands.core.cli.create_runtime') @patch('openhands.core.cli.create_controller') @patch('openhands.core.cli.create_memory') @@ -227,7 +225,7 @@ async def test_run_session_with_initial_action( mock_create_memory, mock_create_controller, mock_create_runtime, - mock_fetch_mcp_tools, + mock_add_mcp_tools, mock_create_agent, mock_display_animation, mock_display_runtime_init, @@ -244,9 +242,6 @@ async def test_run_session_with_initial_action( mock_agent = AsyncMock() mock_create_agent.return_value = mock_agent - mock_mcp_tools = [] - mock_fetch_mcp_tools.return_value = mock_mcp_tools - mock_runtime = AsyncMock() mock_runtime.event_stream = MagicMock() mock_create_runtime.return_value = mock_runtime diff --git a/tests/unit/test_mcp_action_observation.py b/tests/unit/test_mcp_action_observation.py new file mode 100644 index 0000000000..2dd8796a0d --- /dev/null +++ b/tests/unit/test_mcp_action_observation.py @@ -0,0 +1,108 @@ +import json + +from openhands.core.schema import ActionType, ObservationType +from openhands.events.action.mcp import MCPAction +from openhands.events.observation.mcp import MCPObservation + + +def test_mcp_action_creation(): + """Test creating an MCPAction.""" + action = MCPAction(name='test_tool', arguments={'arg1': 'value1', 'arg2': 42}) + + assert action.name == 'test_tool' + assert action.arguments == {'arg1': 'value1', 'arg2': 42} + assert action.action == ActionType.MCP + assert action.thought == '' + assert action.runnable is True + assert action.security_risk is None + + +def test_mcp_action_with_thought(): + """Test creating an MCPAction with a thought.""" + action = MCPAction( + name='test_tool', + arguments={'arg1': 'value1', 'arg2': 42}, + thought='This is a test thought', + ) + + assert action.name == 'test_tool' + assert action.arguments == {'arg1': 'value1', 'arg2': 42} + assert action.thought == 'This is a test thought' + + +def test_mcp_action_message(): + """Test the message property of MCPAction.""" + action = MCPAction(name='test_tool', arguments={'arg1': 'value1', 'arg2': 42}) + + message = action.message + assert 'test_tool' in message + assert 'arg1' in message + assert 'value1' in message + assert '42' in message + + +def test_mcp_action_str_representation(): + """Test the string representation of MCPAction.""" + action = MCPAction( + name='test_tool', + arguments={'arg1': 'value1', 'arg2': 42}, + thought='This is a test thought', + ) + + str_repr = str(action) + assert 'MCPAction' in str_repr + assert 'THOUGHT: This is a test thought' in str_repr + assert 'NAME: test_tool' in str_repr + assert 'ARGUMENTS:' in str_repr + assert 'arg1' in str_repr + assert 'value1' in str_repr + assert '42' in str_repr + + +def test_mcp_observation_creation(): + """Test creating an MCPObservation.""" + observation = MCPObservation( + content=json.dumps({'result': 'success', 'data': 'test data'}) + ) + + assert observation.content == json.dumps({'result': 'success', 'data': 'test data'}) + assert observation.observation == ObservationType.MCP + + +def test_mcp_observation_message(): + """Test the message property of MCPObservation.""" + observation = MCPObservation( + content=json.dumps({'result': 'success', 'data': 'test data'}) + ) + + message = observation.message + assert message == json.dumps({'result': 'success', 'data': 'test data'}) + assert 'result' in message + assert 'success' in message + assert 'data' in message + assert 'test data' in message + + +def test_mcp_action_with_complex_arguments(): + """Test MCPAction with complex nested arguments.""" + complex_args = { + 'simple_arg': 'value', + 'number_arg': 42, + 'boolean_arg': True, + 'nested_arg': {'inner_key': 'inner_value', 'inner_list': [1, 2, 3]}, + 'list_arg': ['a', 'b', 'c'], + } + + action = MCPAction(name='complex_tool', arguments=complex_args) + + assert action.name == 'complex_tool' + assert action.arguments == complex_args + assert action.arguments['nested_arg']['inner_key'] == 'inner_value' + assert action.arguments['list_arg'] == ['a', 'b', 'c'] + + # Check that the message contains the complex arguments + message = action.message + assert 'complex_tool' in message + assert 'nested_arg' in message + assert 'inner_key' in message + assert 'inner_value' in message diff --git a/tests/unit/test_mcp_config.py b/tests/unit/test_mcp_config.py index f069b3f4da..bac7a2e833 100644 --- a/tests/unit/test_mcp_config.py +++ b/tests/unit/test_mcp_config.py @@ -1,23 +1,33 @@ import pytest +from pydantic import ValidationError -from openhands.core.config.mcp_config import MCPConfig +from openhands.core.config.mcp_config import ( + MCPConfig, + MCPSSEServerConfig, + MCPStdioServerConfig, +) def test_valid_sse_config(): """Test a valid SSE configuration.""" - config = MCPConfig(mcp_servers=['http://server1:8080', 'http://server2:8080']) + config = MCPConfig( + sse_servers=[ + MCPSSEServerConfig(url='http://server1:8080'), + MCPSSEServerConfig(url='http://server2:8080'), + ] + ) config.validate_servers() # Should not raise any exception def test_empty_sse_config(): """Test SSE configuration with empty servers list.""" - config = MCPConfig(mcp_servers=[]) + config = MCPConfig(sse_servers=[]) config.validate_servers() def test_invalid_sse_url(): """Test SSE configuration with invalid URL format.""" - config = MCPConfig(mcp_servers=['not_a_url']) + config = MCPConfig(sse_servers=[MCPSSEServerConfig(url='not_a_url')]) with pytest.raises(ValueError) as exc_info: config.validate_servers() assert 'Invalid URL' in str(exc_info.value) @@ -25,7 +35,12 @@ def test_invalid_sse_url(): def test_duplicate_sse_urls(): """Test SSE configuration with duplicate server URLs.""" - config = MCPConfig(mcp_servers=['http://server1:8080', 'http://server1:8080']) + config = MCPConfig( + sse_servers=[ + MCPSSEServerConfig(url='http://server1:8080'), + MCPSSEServerConfig(url='http://server1:8080'), + ] + ) with pytest.raises(ValueError) as exc_info: config.validate_servers() assert 'Duplicate MCP server URLs are not allowed' in str(exc_info.value) @@ -34,17 +49,18 @@ def test_duplicate_sse_urls(): def test_from_toml_section_valid(): """Test creating config from valid TOML section.""" data = { - 'mcp_servers': ['http://server1:8080'], + 'sse_servers': ['http://server1:8080'], } result = MCPConfig.from_toml_section(data) assert 'mcp' in result - assert result['mcp'].mcp_servers == ['http://server1:8080'] + assert len(result['mcp'].sse_servers) == 1 + assert result['mcp'].sse_servers[0].url == 'http://server1:8080' def test_from_toml_section_invalid_sse(): """Test creating config from TOML section with invalid SSE URL.""" data = { - 'mcp_servers': ['not_a_url'], + 'sse_servers': ['not_a_url'], } with pytest.raises(ValueError) as exc_info: MCPConfig.from_toml_section(data) @@ -54,10 +70,125 @@ def test_from_toml_section_invalid_sse(): def test_complex_urls(): """Test SSE configuration with complex URLs.""" config = MCPConfig( - mcp_servers=[ - 'https://user:pass@server1:8080/path?query=1', - 'wss://server2:8443/ws', - 'http://subdomain.example.com:9090', + sse_servers=[ + MCPSSEServerConfig(url='https://user:pass@server1:8080/path?query=1'), + MCPSSEServerConfig(url='wss://server2:8443/ws'), + MCPSSEServerConfig(url='http://subdomain.example.com:9090'), ] ) config.validate_servers() # Should not raise any exception + + +def test_mcp_sse_server_config_with_api_key(): + """Test MCPSSEServerConfig with API key.""" + config = MCPSSEServerConfig(url='http://server1:8080', api_key='test-api-key') + assert config.url == 'http://server1:8080' + assert config.api_key == 'test-api-key' + + +def test_mcp_sse_server_config_without_api_key(): + """Test MCPSSEServerConfig without API key.""" + config = MCPSSEServerConfig(url='http://server1:8080') + assert config.url == 'http://server1:8080' + assert config.api_key is None + + +def test_mcp_stdio_server_config_basic(): + """Test basic MCPStdioServerConfig.""" + config = MCPStdioServerConfig(name='test-server', command='python') + assert config.name == 'test-server' + assert config.command == 'python' + assert config.args == [] + assert config.env == {} + + +def test_mcp_stdio_server_config_with_args_and_env(): + """Test MCPStdioServerConfig with args and env.""" + config = MCPStdioServerConfig( + name='test-server', + command='python', + args=['-m', 'server'], + env={'DEBUG': 'true', 'PORT': '8080'}, + ) + assert config.name == 'test-server' + assert config.command == 'python' + assert config.args == ['-m', 'server'] + assert config.env == {'DEBUG': 'true', 'PORT': '8080'} + + +def test_mcp_config_with_stdio_servers(): + """Test MCPConfig with stdio servers.""" + stdio_server = MCPStdioServerConfig( + name='test-server', + command='python', + args=['-m', 'server'], + env={'DEBUG': 'true'}, + ) + config = MCPConfig(stdio_servers=[stdio_server]) + assert len(config.stdio_servers) == 1 + assert config.stdio_servers[0].name == 'test-server' + assert config.stdio_servers[0].command == 'python' + assert config.stdio_servers[0].args == ['-m', 'server'] + assert config.stdio_servers[0].env == {'DEBUG': 'true'} + + +def test_from_toml_section_with_stdio_servers(): + """Test creating config from TOML section with stdio servers.""" + data = { + 'sse_servers': ['http://server1:8080'], + 'stdio_servers': [ + { + 'name': 'test-server', + 'command': 'python', + 'args': ['-m', 'server'], + 'env': {'DEBUG': 'true'}, + } + ], + } + result = MCPConfig.from_toml_section(data) + assert 'mcp' in result + assert len(result['mcp'].sse_servers) == 1 + assert result['mcp'].sse_servers[0].url == 'http://server1:8080' + assert len(result['mcp'].stdio_servers) == 1 + assert result['mcp'].stdio_servers[0].name == 'test-server' + assert result['mcp'].stdio_servers[0].command == 'python' + assert result['mcp'].stdio_servers[0].args == ['-m', 'server'] + assert result['mcp'].stdio_servers[0].env == {'DEBUG': 'true'} + + +def test_mcp_config_with_both_server_types(): + """Test MCPConfig with both SSE and stdio servers.""" + sse_server = MCPSSEServerConfig(url='http://server1:8080', api_key='test-api-key') + stdio_server = MCPStdioServerConfig( + name='test-server', + command='python', + args=['-m', 'server'], + env={'DEBUG': 'true'}, + ) + config = MCPConfig(sse_servers=[sse_server], stdio_servers=[stdio_server]) + assert len(config.sse_servers) == 1 + assert config.sse_servers[0].url == 'http://server1:8080' + assert config.sse_servers[0].api_key == 'test-api-key' + assert len(config.stdio_servers) == 1 + assert config.stdio_servers[0].name == 'test-server' + assert config.stdio_servers[0].command == 'python' + + +def test_mcp_config_model_validation_error(): + """Test MCPConfig validation error with invalid data.""" + with pytest.raises(ValidationError): + # Missing required 'url' field + MCPSSEServerConfig() + + with pytest.raises(ValidationError): + # Missing required 'name' and 'command' fields + MCPStdioServerConfig() + + +def test_mcp_config_extra_fields_forbidden(): + """Test that extra fields are forbidden in MCPConfig.""" + with pytest.raises(ValidationError): + MCPConfig(extra_field='value') + + # Note: The nested models don't have 'extra': 'forbid' set, so they allow extra fields + # We're only testing the main MCPConfig class here diff --git a/tests/unit/test_mcp_timeout.py b/tests/unit/test_mcp_timeout.py index 8bdc932294..8d0ed4b7f5 100644 --- a/tests/unit/test_mcp_timeout.py +++ b/tests/unit/test_mcp_timeout.py @@ -3,7 +3,7 @@ from unittest import mock import pytest -from openhands.core.config.mcp_config import MCPConfig +from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig from openhands.mcp import MCPClient, create_mcp_clients, fetch_mcp_tools_from_config @@ -24,10 +24,13 @@ async def test_sse_connection_timeout(): # Mock the MCPClient constructor to return our mock with mock.patch('openhands.mcp.utils.MCPClient', return_value=mock_client): # Create a list of server URLs to test - servers = ['http://server1:8080', 'http://server2:8080'] + servers = [ + MCPSSEServerConfig(url='http://server1:8080'), + MCPSSEServerConfig(url='http://server2:8080'), + ] # Call create_mcp_clients with the server URLs - clients = await create_mcp_clients(mcp_servers=servers) + clients = await create_mcp_clients(sse_servers=servers) # Verify that no clients were successfully connected assert len(clients) == 0 @@ -46,7 +49,7 @@ async def test_fetch_mcp_tools_with_timeout(): mock_config = mock.MagicMock(spec=MCPConfig) # Configure the mock config - mock_config.mcp_servers = ['http://server1:8080'] + mock_config.sse_servers = ['http://server1:8080'] # Mock create_mcp_clients to return an empty list (simulating all connections failing) with mock.patch('openhands.mcp.utils.create_mcp_clients', return_value=[]): @@ -64,7 +67,7 @@ async def test_mixed_connection_results(): mock_config = mock.MagicMock(spec=MCPConfig) # Configure the mock config - mock_config.mcp_servers = ['http://server1:8080', 'http://server2:8080'] + mock_config.sse_servers = ['http://server1:8080', 'http://server2:8080'] # Create a successful client successful_client = mock.MagicMock(spec=MCPClient) diff --git a/tests/unit/test_mcp_utils.py b/tests/unit/test_mcp_utils.py new file mode 100644 index 0000000000..fd45900610 --- /dev/null +++ b/tests/unit/test_mcp_utils.py @@ -0,0 +1,165 @@ +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +# Import the module, not the functions directly to avoid circular imports +import openhands.mcp.utils +from openhands.core.config.mcp_config import MCPSSEServerConfig +from openhands.events.action.mcp import MCPAction +from openhands.events.observation.mcp import MCPObservation + + +@pytest.mark.asyncio +async def test_create_mcp_clients_empty(): + """Test creating MCP clients with empty server list.""" + clients = await openhands.mcp.utils.create_mcp_clients([]) + assert clients == [] + + +@pytest.mark.asyncio +@patch('openhands.mcp.utils.MCPClient') +async def test_create_mcp_clients_success(mock_mcp_client): + """Test successful creation of MCP clients.""" + # Setup mock + mock_client_instance = AsyncMock() + mock_mcp_client.return_value = mock_client_instance + mock_client_instance.connect_sse = AsyncMock() + + # Test with two servers + server_configs = [ + MCPSSEServerConfig(url='http://server1:8080'), + MCPSSEServerConfig(url='http://server2:8080', api_key='test-key'), + ] + + clients = await openhands.mcp.utils.create_mcp_clients(server_configs) + + # Verify + assert len(clients) == 2 + assert mock_mcp_client.call_count == 2 + + # Check that connect_sse was called with correct parameters + mock_client_instance.connect_sse.assert_any_call( + 'http://server1:8080', api_key=None + ) + mock_client_instance.connect_sse.assert_any_call( + 'http://server2:8080', api_key='test-key' + ) + + +@pytest.mark.asyncio +@patch('openhands.mcp.utils.MCPClient') +async def test_create_mcp_clients_connection_failure(mock_mcp_client): + """Test handling of connection failures when creating MCP clients.""" + # Setup mock + mock_client_instance = AsyncMock() + mock_mcp_client.return_value = mock_client_instance + + # First connection succeeds, second fails + mock_client_instance.connect_sse.side_effect = [ + None, # Success + Exception('Connection failed'), # Failure + ] + mock_client_instance.disconnect = AsyncMock() + + server_configs = [ + MCPSSEServerConfig(url='http://server1:8080'), + MCPSSEServerConfig(url='http://server2:8080'), + ] + + clients = await openhands.mcp.utils.create_mcp_clients(server_configs) + + # Verify only one client was successfully created + assert len(clients) == 1 + assert mock_client_instance.disconnect.call_count == 1 + + +def test_convert_mcp_clients_to_tools_empty(): + """Test converting empty MCP clients list to tools.""" + tools = openhands.mcp.utils.convert_mcp_clients_to_tools(None) + assert tools == [] + + tools = openhands.mcp.utils.convert_mcp_clients_to_tools([]) + assert tools == [] + + +def test_convert_mcp_clients_to_tools(): + """Test converting MCP clients to tools.""" + # Create mock clients with tools + mock_client1 = MagicMock() + mock_client2 = MagicMock() + + # Create mock tools + mock_tool1 = MagicMock() + mock_tool1.to_param.return_value = {'function': {'name': 'tool1'}} + + mock_tool2 = MagicMock() + mock_tool2.to_param.return_value = {'function': {'name': 'tool2'}} + + mock_tool3 = MagicMock() + mock_tool3.to_param.return_value = {'function': {'name': 'tool3'}} + + # Set up the clients with their tools + mock_client1.tools = [mock_tool1, mock_tool2] + mock_client2.tools = [mock_tool3] + + # Convert to tools + tools = openhands.mcp.utils.convert_mcp_clients_to_tools( + [mock_client1, mock_client2] + ) + + # Verify + assert len(tools) == 3 + assert tools[0] == {'function': {'name': 'tool1'}} + assert tools[1] == {'function': {'name': 'tool2'}} + assert tools[2] == {'function': {'name': 'tool3'}} + + +@pytest.mark.asyncio +async def test_call_tool_mcp_no_clients(): + """Test calling MCP tool with no clients.""" + action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) + + with pytest.raises(ValueError, match='No MCP clients found'): + await openhands.mcp.utils.call_tool_mcp([], action) + + +@pytest.mark.asyncio +async def test_call_tool_mcp_no_matching_client(): + """Test calling MCP tool with no matching client.""" + # Create mock client without the requested tool + mock_client = MagicMock() + mock_client.tools = [MagicMock(name='other_tool')] + + action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) + + with pytest.raises(ValueError, match='No matching MCP agent found for tool name'): + await openhands.mcp.utils.call_tool_mcp([mock_client], action) + + +@pytest.mark.asyncio +async def test_call_tool_mcp_success(): + """Test successful MCP tool call.""" + # Create mock client with the requested tool + mock_client = MagicMock() + mock_tool = MagicMock() + # Set the name attribute properly for the tool + mock_tool.name = 'test_tool' + mock_client.tools = [mock_tool] + + # Setup response + mock_response = MagicMock() + mock_response.model_dump.return_value = {'result': 'success'} + + # Setup call_tool method + mock_client.call_tool = AsyncMock(return_value=mock_response) + + action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) + + # Call the function + observation = await openhands.mcp.utils.call_tool_mcp([mock_client], action) + + # Verify + assert isinstance(observation, MCPObservation) + assert json.loads(observation.content) == {'result': 'success'} + mock_client.call_tool.assert_called_once_with('test_tool', {'arg1': 'value1'}) diff --git a/tests/unit/test_memory.py b/tests/unit/test_memory.py index 60bd3359c9..627220bccd 100644 --- a/tests/unit/test_memory.py +++ b/tests/unit/test_memory.py @@ -21,7 +21,9 @@ from openhands.events.stream import EventStream from openhands.llm import LLM from openhands.llm.metrics import Metrics from openhands.memory.memory import Memory -from openhands.runtime.base import Runtime +from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, +) from openhands.storage.memory import InMemoryFileStore @@ -77,7 +79,7 @@ def mock_agent(): async def test_memory_on_event_exception_handling(memory, event_stream, mock_agent): """Test that exceptions in Memory.on_event are properly handled via status callback.""" # Create a mock runtime - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) runtime.event_stream = event_stream # Mock Memory method to raise an exception @@ -106,7 +108,7 @@ async def test_memory_on_workspace_context_recall_exception_handling( ): """Test that exceptions in Memory._on_workspace_context_recall are properly handled via status callback.""" # Create a mock runtime - runtime = MagicMock(spec=Runtime) + runtime = MagicMock(spec=ActionExecutionClient) runtime.event_stream = event_stream # Mock Memory._on_workspace_context_recall to raise an exception