mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
feat(MCP): MCP refactor, support stdio, and running MCP server in runtime (#7911)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Calvin Smith <email@cjsmith.io>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user