Backup-bdg's picture
Upload 964 files
51ff9e5 verified
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.controller.agent import Agent
from openhands.core.config.mcp_config import (
MCPConfig,
MCPSHTTPServerConfig,
MCPSSEServerConfig,
)
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
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.memory.memory import Memory
from openhands.runtime.base import Runtime
def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[dict]:
"""
Converts a list of MCPClient instances to ChatCompletionToolParam format
that can be used by CodeActAgent.
Args:
mcp_clients: List of MCPClient instances or None
Returns:
List of dicts of tools ready to be used by CodeActAgent
"""
if mcp_clients is None:
logger.warning('mcp_clients is None, returning empty list')
return []
all_mcp_tools = []
try:
for client in mcp_clients:
# Each MCPClient has an mcp_clients property that is a ToolCollection
# The ToolCollection has a to_params method that converts tools to ChatCompletionToolParam format
for tool in client.tools:
mcp_tools = tool.to_param()
all_mcp_tools.append(mcp_tools)
except Exception as e:
logger.error(f'Error in convert_mcp_clients_to_tools: {e}')
return []
return all_mcp_tools
async def create_mcp_clients(
sse_servers: list[MCPSSEServerConfig],
shttp_servers: list[MCPSHTTPServerConfig],
conversation_id: str | None = None,
) -> list[MCPClient]:
import sys
# Skip MCP clients on Windows
if sys.platform == 'win32':
logger.info(
'MCP functionality is disabled on Windows, skipping client creation'
)
return []
servers: list[MCPSSEServerConfig | MCPSHTTPServerConfig] = sse_servers.copy()
servers.extend(shttp_servers.copy())
if not servers:
return []
mcp_clients = []
for server in servers:
is_shttp = isinstance(server, MCPSHTTPServerConfig)
connection_type = 'SHTTP' if is_shttp else 'SSE'
logger.info(
f'Initializing MCP agent for {server} with {connection_type} connection...'
)
client = MCPClient()
try:
await client.connect_http(server, conversation_id=conversation_id)
# Only add the client to the list after a successful connection
mcp_clients.append(client)
except Exception as e:
logger.error(f'Failed to connect to {server}: {str(e)}', exc_info=True)
return mcp_clients
async def fetch_mcp_tools_from_config(
mcp_config: MCPConfig, conversation_id: str | None = None
) -> list[dict]:
"""
Retrieves the list of MCP tools from the MCP clients.
Args:
mcp_config: The MCP configuration
conversation_id: Optional conversation ID to associate with the MCP clients
Returns:
A list of tool dictionaries. Returns an empty list if no connections could be established.
"""
import sys
# Skip MCP tools on Windows
if sys.platform == 'win32':
logger.info('MCP functionality is disabled on Windows, skipping tool fetching')
return []
mcp_clients = []
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.sse_servers, mcp_config.shttp_servers, conversation_id
)
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)
except Exception as e:
logger.error(f'Error fetching MCP tools: {str(e)}')
return []
logger.debug(f'MCP tools: {mcp_tools}')
return mcp_tools
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
Returns:
The observation from the MCP server
"""
import sys
from openhands.events.observation import ErrorObservation
# Skip MCP tools on Windows
if sys.platform == 'win32':
logger.info('MCP functionality is disabled on Windows')
return ErrorObservation('MCP functionality is not available on Windows')
if not mcp_clients:
raise ValueError('No MCP clients found')
logger.debug(f'MCP action received: {action}')
# 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}')
# 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=json.dumps(response.model_dump(mode='json')),
name=action.name,
arguments=action.arguments,
)
async def add_mcp_tools_to_agent(
agent: 'Agent', runtime: Runtime, memory: 'Memory', app_config: OpenHandsConfig
):
"""
Add MCP tools to an agent.
"""
import sys
# Skip MCP tools on Windows
if sys.platform == 'win32':
logger.info('MCP functionality is disabled on Windows, skipping MCP tools')
agent.set_mcp_tools([])
return
assert runtime.runtime_initialized, (
'Runtime must be initialized before adding MCP tools'
)
extra_stdio_servers = []
# Add microagent MCP tools if available
mcp_config: MCPConfig = app_config.mcp
microagent_mcp_configs = memory.get_microagent_mcp_tools()
for mcp_config in microagent_mcp_configs:
if mcp_config.sse_servers:
logger.warning(
'Microagent MCP config contains SSE servers, it is not yet supported.'
)
if mcp_config.stdio_servers:
for stdio_server in mcp_config.stdio_servers:
# Check if this stdio server is already in the config
if stdio_server not in extra_stdio_servers:
extra_stdio_servers.append(stdio_server)
logger.info(f'Added microagent stdio server: {stdio_server.name}')
# Add the runtime as another MCP server
updated_mcp_config = runtime.get_mcp_config(extra_stdio_servers)
# 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)