Skip to content

Commit 7326cca

Browse files
authored
Merge pull request #1061 from cyzus/feat-include-mcp-in-manus-agent
Manus Agent to include MCP tools
2 parents 8592c63 + df9312d commit 7326cca

File tree

6 files changed

+260
-55
lines changed

6 files changed

+260
-55
lines changed

app/agent/manus.py

+100-11
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
1-
from typing import Optional
1+
from typing import Dict, List, Optional
22

33
from pydantic import Field, model_validator
44

55
from app.agent.browser import BrowserContextHelper
66
from app.agent.toolcall import ToolCallAgent
77
from app.config import config
8+
from app.logger import logger
89
from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
910
from app.tool import Terminate, ToolCollection
1011
from app.tool.browser_use_tool import BrowserUseTool
12+
from app.tool.mcp import MCPClients, MCPClientTool
1113
from app.tool.python_execute import PythonExecute
1214
from app.tool.str_replace_editor import StrReplaceEditor
1315

1416

1517
class Manus(ToolCallAgent):
16-
"""A versatile general-purpose agent."""
18+
"""A versatile general-purpose agent with support for both local and MCP tools."""
1719

1820
name: str = "Manus"
19-
description: str = (
20-
"A versatile agent that can solve various tasks using multiple tools"
21-
)
21+
description: str = "A versatile agent that can solve various tasks using multiple tools including MCP-based tools"
2222

2323
system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
2424
next_step_prompt: str = NEXT_STEP_PROMPT
2525

2626
max_observe: int = 10000
2727
max_steps: int = 20
2828

29+
# MCP clients for remote tool access
30+
mcp_clients: MCPClients = Field(default_factory=MCPClients)
31+
2932
# Add general-purpose tools to the tool collection
3033
available_tools: ToolCollection = Field(
3134
default_factory=lambda: ToolCollection(
@@ -34,16 +37,107 @@ class Manus(ToolCallAgent):
3437
)
3538

3639
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
37-
3840
browser_context_helper: Optional[BrowserContextHelper] = None
3941

42+
# Track connected MCP servers
43+
connected_servers: Dict[str, str] = Field(
44+
default_factory=dict
45+
) # server_id -> url/command
46+
_initialized: bool = False
47+
4048
@model_validator(mode="after")
4149
def initialize_helper(self) -> "Manus":
50+
"""Initialize basic components synchronously."""
4251
self.browser_context_helper = BrowserContextHelper(self)
4352
return self
4453

54+
@classmethod
55+
async def create(cls, **kwargs) -> "Manus":
56+
"""Factory method to create and properly initialize a Manus instance."""
57+
instance = cls(**kwargs)
58+
await instance.initialize_mcp_servers()
59+
instance._initialized = True
60+
return instance
61+
62+
async def initialize_mcp_servers(self) -> None:
63+
"""Initialize connections to configured MCP servers."""
64+
for server_id, server_config in config.mcp_config.servers.items():
65+
try:
66+
if server_config.type == "sse":
67+
if server_config.url:
68+
await self.connect_mcp_server(server_config.url, server_id)
69+
logger.info(
70+
f"Connected to MCP server {server_id} at {server_config.url}"
71+
)
72+
elif server_config.type == "stdio":
73+
if server_config.command:
74+
await self.connect_mcp_server(
75+
server_config.command,
76+
server_id,
77+
use_stdio=True,
78+
stdio_args=server_config.args,
79+
)
80+
logger.info(
81+
f"Connected to MCP server {server_id} using command {server_config.command}"
82+
)
83+
except Exception as e:
84+
logger.error(f"Failed to connect to MCP server {server_id}: {e}")
85+
86+
async def connect_mcp_server(
87+
self,
88+
server_url: str,
89+
server_id: str = "",
90+
use_stdio: bool = False,
91+
stdio_args: List[str] = None,
92+
) -> None:
93+
"""Connect to an MCP server and add its tools."""
94+
if use_stdio:
95+
await self.mcp_clients.connect_stdio(
96+
server_url, stdio_args or [], server_id
97+
)
98+
self.connected_servers[server_id or server_url] = server_url
99+
else:
100+
await self.mcp_clients.connect_sse(server_url, server_id)
101+
self.connected_servers[server_id or server_url] = server_url
102+
103+
# Update available tools with only the new tools from this server
104+
new_tools = [
105+
tool for tool in self.mcp_clients.tools if tool.server_id == server_id
106+
]
107+
self.available_tools.add_tools(*new_tools)
108+
109+
async def disconnect_mcp_server(self, server_id: str = "") -> None:
110+
"""Disconnect from an MCP server and remove its tools."""
111+
await self.mcp_clients.disconnect(server_id)
112+
if server_id:
113+
self.connected_servers.pop(server_id, None)
114+
else:
115+
self.connected_servers.clear()
116+
117+
# Rebuild available tools without the disconnected server's tools
118+
base_tools = [
119+
tool
120+
for tool in self.available_tools.tools
121+
if not isinstance(tool, MCPClientTool)
122+
]
123+
self.available_tools = ToolCollection(*base_tools)
124+
self.available_tools.add_tools(*self.mcp_clients.tools)
125+
126+
async def cleanup(self):
127+
"""Clean up Manus agent resources."""
128+
if self.browser_context_helper:
129+
await self.browser_context_helper.cleanup_browser()
130+
# Disconnect from all MCP servers only if we were initialized
131+
if self._initialized:
132+
await self.disconnect_mcp_server()
133+
self._initialized = False
134+
45135
async def think(self) -> bool:
46136
"""Process current state and decide next actions with appropriate context."""
137+
if not self._initialized:
138+
await self.initialize_mcp_servers()
139+
self._initialized = True
140+
47141
original_prompt = self.next_step_prompt
48142
recent_messages = self.memory.messages[-3:] if self.memory.messages else []
49143
browser_in_use = any(
@@ -64,8 +158,3 @@ async def think(self) -> bool:
64158
self.next_step_prompt = original_prompt
65159

66160
return result
67-
68-
async def cleanup(self):
69-
"""Clean up Manus agent resources."""
70-
if self.browser_context_helper:
71-
await self.browser_context_helper.cleanup_browser()

app/config.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import threading
23
import tomllib
34
from pathlib import Path
@@ -98,12 +99,51 @@ class SandboxSettings(BaseModel):
9899
)
99100

100101

102+
class MCPServerConfig(BaseModel):
103+
"""Configuration for a single MCP server"""
104+
105+
type: str = Field(..., description="Server connection type (sse or stdio)")
106+
url: Optional[str] = Field(None, description="Server URL for SSE connections")
107+
command: Optional[str] = Field(None, description="Command for stdio connections")
108+
args: List[str] = Field(
109+
default_factory=list, description="Arguments for stdio command"
110+
)
111+
112+
101113
class MCPSettings(BaseModel):
102114
"""Configuration for MCP (Model Context Protocol)"""
103115

104116
server_reference: str = Field(
105117
"app.mcp.server", description="Module reference for the MCP server"
106118
)
119+
servers: Dict[str, MCPServerConfig] = Field(
120+
default_factory=dict, description="MCP server configurations"
121+
)
122+
123+
@classmethod
124+
def load_server_config(cls) -> Dict[str, MCPServerConfig]:
125+
"""Load MCP server configuration from JSON file"""
126+
config_path = PROJECT_ROOT / "config" / "mcp.json"
127+
128+
try:
129+
config_file = config_path if config_path.exists() else None
130+
if not config_file:
131+
return {}
132+
133+
with config_file.open() as f:
134+
data = json.load(f)
135+
servers = {}
136+
137+
for server_id, server_config in data.get("mcpServers", {}).items():
138+
servers[server_id] = MCPServerConfig(
139+
type=server_config["type"],
140+
url=server_config.get("url"),
141+
command=server_config.get("command"),
142+
args=server_config.get("args", []),
143+
)
144+
return servers
145+
except Exception as e:
146+
raise ValueError(f"Failed to load MCP server config: {e}")
107147

108148

109149
class AppConfig(BaseModel):
@@ -223,9 +263,11 @@ def _load_initial_config(self):
223263
mcp_config = raw_config.get("mcp", {})
224264
mcp_settings = None
225265
if mcp_config:
266+
# Load server configurations from JSON
267+
mcp_config["servers"] = MCPSettings.load_server_config()
226268
mcp_settings = MCPSettings(**mcp_config)
227269
else:
228-
mcp_settings = MCPSettings()
270+
mcp_settings = MCPSettings(servers=MCPSettings.load_server_config())
229271

230272
config_dict = {
231273
"llm": {

0 commit comments

Comments
 (0)