1
- from typing import Optional
1
+ from typing import Dict , List , Optional
2
2
3
3
from pydantic import Field , model_validator
4
4
5
5
from app .agent .browser import BrowserContextHelper
6
6
from app .agent .toolcall import ToolCallAgent
7
7
from app .config import config
8
+ from app .logger import logger
8
9
from app .prompt .manus import NEXT_STEP_PROMPT , SYSTEM_PROMPT
9
10
from app .tool import Terminate , ToolCollection
10
11
from app .tool .browser_use_tool import BrowserUseTool
12
+ from app .tool .mcp import MCPClients , MCPClientTool
11
13
from app .tool .python_execute import PythonExecute
12
14
from app .tool .str_replace_editor import StrReplaceEditor
13
15
14
16
15
17
class Manus (ToolCallAgent ):
16
- """A versatile general-purpose agent."""
18
+ """A versatile general-purpose agent with support for both local and MCP tools ."""
17
19
18
20
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"
22
22
23
23
system_prompt : str = SYSTEM_PROMPT .format (directory = config .workspace_root )
24
24
next_step_prompt : str = NEXT_STEP_PROMPT
25
25
26
26
max_observe : int = 10000
27
27
max_steps : int = 20
28
28
29
+ # MCP clients for remote tool access
30
+ mcp_clients : MCPClients = Field (default_factory = MCPClients )
31
+
29
32
# Add general-purpose tools to the tool collection
30
33
available_tools : ToolCollection = Field (
31
34
default_factory = lambda : ToolCollection (
@@ -34,16 +37,107 @@ class Manus(ToolCallAgent):
34
37
)
35
38
36
39
special_tool_names : list [str ] = Field (default_factory = lambda : [Terminate ().name ])
37
-
38
40
browser_context_helper : Optional [BrowserContextHelper ] = None
39
41
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
+
40
48
@model_validator (mode = "after" )
41
49
def initialize_helper (self ) -> "Manus" :
50
+ """Initialize basic components synchronously."""
42
51
self .browser_context_helper = BrowserContextHelper (self )
43
52
return self
44
53
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
+
45
135
async def think (self ) -> bool :
46
136
"""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
+
47
141
original_prompt = self .next_step_prompt
48
142
recent_messages = self .memory .messages [- 3 :] if self .memory .messages else []
49
143
browser_in_use = any (
@@ -64,8 +158,3 @@ async def think(self) -> bool:
64
158
self .next_step_prompt = original_prompt
65
159
66
160
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 ()
0 commit comments