Spaces:
Build error
Build error
File size: 7,626 Bytes
51ff9e5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
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)
|