Spaces:
Build error
Build error
import os | |
from typing import TYPE_CHECKING | |
from urllib.parse import urlparse | |
from pydantic import BaseModel, Field, ValidationError, model_validator | |
if TYPE_CHECKING: | |
from openhands.core.config.openhands_config import OpenHandsConfig | |
from openhands.core.logger import openhands_logger as logger | |
from openhands.utils.import_utils import get_impl | |
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) | |
def __eq__(self, other): | |
"""Override equality operator to compare server configurations. | |
Two server configurations are considered equal if they have the same | |
name, command, args, and env values. The order of args is important, | |
but the order of env variables is not. | |
""" | |
if not isinstance(other, MCPStdioServerConfig): | |
return False | |
return ( | |
self.name == other.name | |
and self.command == other.command | |
and self.args == other.args | |
and set(self.env.items()) == set(other.env.items()) | |
) | |
class MCPSHTTPServerConfig(BaseModel): | |
url: str | |
api_key: str | None = None | |
class MCPConfig(BaseModel): | |
"""Configuration for MCP (Message Control Protocol) settings. | |
Attributes: | |
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. | |
""" | |
sse_servers: list[MCPSSEServerConfig] = Field(default_factory=list) | |
stdio_servers: list[MCPStdioServerConfig] = Field(default_factory=list) | |
shttp_servers: list[MCPSHTTPServerConfig] = Field(default_factory=list) | |
model_config = {'extra': 'forbid'} | |
def _normalize_servers(servers_data: list[dict | str]) -> list[dict]: | |
"""Helper method to normalize SSE server configurations.""" | |
normalized = [] | |
for server in servers_data: | |
if isinstance(server, str): | |
normalized.append({'url': server}) | |
else: | |
normalized.append(server) | |
return normalized | |
def convert_string_urls(cls, data): | |
"""Convert string URLs to MCPSSEServerConfig objects.""" | |
if isinstance(data, dict): | |
if 'sse_servers' in data: | |
data['sse_servers'] = cls._normalize_servers(data['sse_servers']) | |
if 'shttp_servers' in data: | |
data['shttp_servers'] = cls._normalize_servers(data['shttp_servers']) | |
return data | |
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(urls)) != len(urls): | |
raise ValueError('Duplicate MCP server URLs are not allowed') | |
# Validate URLs | |
for url in urls: | |
try: | |
result = urlparse(url) | |
if not all([result.scheme, result.netloc]): | |
raise ValueError(f'Invalid URL format: {url}') | |
except Exception as e: | |
raise ValueError(f'Invalid URL {url}: {str(e)}') | |
def from_toml_section(cls, data: dict) -> dict[str, 'MCPConfig']: | |
""" | |
Create a mapping of MCPConfig instances from a toml dictionary representing the [mcp] section. | |
The configuration is built from all keys in data. | |
Returns: | |
dict[str, MCPConfig]: A mapping where the key "mcp" corresponds to the [mcp] configuration | |
""" | |
# Initialize the result mapping | |
mcp_mapping: dict[str, MCPConfig] = {} | |
try: | |
# Convert all entries in sse_servers to MCPSSEServerConfig objects | |
if 'sse_servers' in data: | |
data['sse_servers'] = cls._normalize_servers(data['sse_servers']) | |
servers = [] | |
for server in data['sse_servers']: | |
servers.append(MCPSSEServerConfig(**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 | |
if 'shttp_servers' in data: | |
data['shttp_servers'] = cls._normalize_servers(data['shttp_servers']) | |
servers = [] | |
for server in data['shttp_servers']: | |
servers.append(MCPSHTTPServerConfig(**server)) | |
data['shttp_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( | |
sse_servers=mcp_config.sse_servers, | |
stdio_servers=mcp_config.stdio_servers, | |
) | |
except ValidationError as e: | |
raise ValueError(f'Invalid MCP configuration: {e}') | |
return mcp_mapping | |
class OpenHandsMCPConfig: | |
def add_search_engine(app_config: 'OpenHandsConfig') -> MCPStdioServerConfig | None: | |
"""Add search engine to the MCP config""" | |
if ( | |
app_config.search_api_key | |
and app_config.search_api_key.get_secret_value().startswith('tvly-') | |
): | |
logger.info('Adding search engine to MCP config') | |
return MCPStdioServerConfig( | |
name='tavily', | |
command='npx', | |
args=['-y', '[email protected]'], | |
env={'TAVILY_API_KEY': app_config.search_api_key.get_secret_value()}, | |
) | |
else: | |
logger.warning('No search engine API key found, skipping search engine') | |
# Do not add search engine to MCP config in SaaS mode since it will be added by the OpenHands server | |
return None | |
def create_default_mcp_server_config( | |
host: str, config: 'OpenHandsConfig', user_id: str | None = None | |
) -> tuple[MCPSHTTPServerConfig, list[MCPStdioServerConfig]]: | |
""" | |
Create a default MCP server configuration. | |
Args: | |
host: Host string | |
config: OpenHandsConfig | |
Returns: | |
tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]: A tuple containing the default SSE server configuration and a list of MCP stdio server configurations | |
""" | |
stdio_servers = [] | |
search_engine_stdio_server = OpenHandsMCPConfig.add_search_engine(config) | |
if search_engine_stdio_server: | |
stdio_servers.append(search_engine_stdio_server) | |
shttp_servers = MCPSHTTPServerConfig(url=f'http://{host}/mcp/mcp', api_key=None) | |
return shttp_servers, stdio_servers | |
openhands_mcp_config_cls = os.environ.get( | |
'OPENHANDS_MCP_CONFIG_CLS', | |
'openhands.core.config.mcp_config.OpenHandsMCPConfig', | |
) | |
OpenHandsMCPConfigImpl = get_impl(OpenHandsMCPConfig, openhands_mcp_config_cls) | |