akseljoonas HF Staff commited on
Commit
0332824
·
1 Parent(s): 190a5f6

preliminary mcp

Browse files
agent/MCP_INTEGRATION.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Integration for HF Agent
2
+
3
+ This agent now supports the Model Context Protocol (MCP), allowing it to connect to and use tools from MCP servers.
4
+
5
+ ## Overview
6
+
7
+ The MCP integration allows the agent to:
8
+ - Connect to multiple MCP servers simultaneously
9
+ - Automatically discover and use tools from connected servers
10
+ - Execute tool calls through the MCP protocol
11
+ - Seamlessly integrate MCP tools with the agent's existing tool system
12
+
13
+ ## Architecture
14
+
15
+ The integration consists of several components:
16
+
17
+ 1. **MCPClient** (`agent/core/mcp_client.py`): Manages connections to MCP servers
18
+ 2. **ToolExecutor** (`agent/core/executor.py`): Executes both MCP and local tools
19
+ 3. **Config** (`agent/config.py`): Stores MCP server configurations
20
+ 4. **Session** (`agent/core/session.py`): Initializes MCP connections and manages lifecycle
21
+
22
+ ## Configuration
23
+
24
+ To use MCP servers with your agent, add them to your configuration file:
25
+
26
+ ```json
27
+ {
28
+ "model_name": "anthropic/claude-sonnet-4-5-20250929",
29
+ "tools": [],
30
+ "system_prompt_path": "",
31
+ "mcp_servers": [
32
+ {
33
+ "name": "weather",
34
+ "command": "python",
35
+ "args": ["path/to/weather_server.py"],
36
+ "env": null
37
+ },
38
+ {
39
+ "name": "filesystem",
40
+ "command": "node",
41
+ "args": ["path/to/filesystem_server.js"],
42
+ "env": {
43
+ "ALLOWED_PATHS": "/home/user/documents"
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ ### Configuration Fields
51
+
52
+ - `name`: Unique identifier for the MCP server
53
+ - `command`: Command to execute the server (`python`, `node`, etc.)
54
+ - `args`: Arguments to pass to the command (path to server script)
55
+ - `env`: (Optional) Environment variables for the server process
56
+
57
+ ## Usage
58
+
59
+ ### Basic Usage
60
+
61
+ ```python
62
+ import asyncio
63
+ from agent.config import Config, load_config
64
+ from agent.core.agent_loop import submission_loop
65
+
66
+ async def main():
67
+ # Load config with MCP servers
68
+ config = load_config("config.json")
69
+
70
+ # Create queues
71
+ submission_queue = asyncio.Queue()
72
+ event_queue = asyncio.Queue()
73
+
74
+ # Start agent loop (MCP connections initialized automatically)
75
+ await submission_loop(submission_queue, event_queue, config)
76
+
77
+ if __name__ == "__main__":
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ### Programmatic Configuration
82
+
83
+ ```python
84
+ from agent.config import Config, MCPServerConfig
85
+
86
+ config = Config(
87
+ model_name="anthropic/claude-sonnet-4-5-20250929",
88
+ tools=[],
89
+ system_prompt_path="",
90
+ mcp_servers=[
91
+ MCPServerConfig(
92
+ name="weather",
93
+ command="python",
94
+ args=["weather_server.py"],
95
+ env=None
96
+ )
97
+ ]
98
+ )
99
+ ```
100
+
101
+ ## How It Works
102
+
103
+ 1. **Initialization**: When the agent loop starts, it calls `session.initialize_mcp()`
104
+ 2. **Connection**: The session connects to all configured MCP servers
105
+ 3. **Tool Discovery**: Tools from all servers are discovered and added to the agent's tool list
106
+ 4. **Tool Naming**: MCP tools are prefixed with their server name (e.g., `weather__get_forecast`)
107
+ 5. **Execution**: When the LLM calls a tool, the ToolExecutor routes it to the appropriate MCP server
108
+ 6. **Cleanup**: When the agent shuts down, all MCP connections are cleaned up properly
109
+
110
+ ## Tool Naming Convention
111
+
112
+ MCP tools are automatically prefixed with their server name to avoid conflicts:
113
+
114
+ - Original tool: `get_forecast`
115
+ - MCP tool name: `weather__get_forecast`
116
+
117
+ This ensures that tools from different servers don't conflict, even if they have the same name.
118
+
119
+ ## Example: Creating a Simple MCP Server
120
+
121
+ Here's a minimal example of an MCP server (save as `calculator_server.py`):
122
+
123
+ ```python
124
+ import asyncio
125
+ from mcp.server import Server, stdio_server
126
+ from mcp.types import Tool, TextContent
127
+
128
+ app = Server("calculator")
129
+
130
+ @app.list_tools()
131
+ async def list_tools() -> list[Tool]:
132
+ return [
133
+ Tool(
134
+ name="add",
135
+ description="Add two numbers",
136
+ inputSchema={
137
+ "type": "object",
138
+ "properties": {
139
+ "a": {"type": "number"},
140
+ "b": {"type": "number"}
141
+ },
142
+ "required": ["a", "b"]
143
+ }
144
+ )
145
+ ]
146
+
147
+ @app.call_tool()
148
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
149
+ if name == "add":
150
+ result = arguments["a"] + arguments["b"]
151
+ return [TextContent(type="text", text=str(result))]
152
+
153
+ raise ValueError(f"Unknown tool: {name}")
154
+
155
+ async def main():
156
+ async with stdio_server() as (read_stream, write_stream):
157
+ await app.run(read_stream, write_stream, app.create_initialization_options())
158
+
159
+ if __name__ == "__main__":
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ ## Troubleshooting
164
+
165
+ ### Server Connection Issues
166
+
167
+ If you see errors connecting to an MCP server:
168
+
169
+ 1. Check that the server script path is correct
170
+ 2. Ensure the command (`python`, `node`) is in your PATH
171
+ 3. Verify the server script is executable
172
+ 4. Check server logs for initialization errors
173
+
174
+ ### Tool Not Found
175
+
176
+ If the agent can't find an MCP tool:
177
+
178
+ 1. Verify the server is connected (check startup logs)
179
+ 2. Check tool naming (should be `servername__toolname`)
180
+ 3. Ensure the server properly implements `list_tools()`
181
+
182
+ ### Performance Considerations
183
+
184
+ - MCP server initialization happens once at startup
185
+ - Tool calls are asynchronous and don't block the agent
186
+ - Multiple servers can be used simultaneously
187
+ - Consider using local tools for high-frequency operations
188
+
189
+ ## Best Practices
190
+
191
+ 1. **Unique Server Names**: Give each MCP server a unique, descriptive name
192
+ 2. **Error Handling**: MCP connection failures are logged but don't crash the agent
193
+ 3. **Resource Cleanup**: Always let the agent shut down gracefully to cleanup connections
194
+ 4. **Testing**: Test MCP servers independently before integrating them
195
+ 5. **Security**: Be cautious with file system and network access in MCP servers
196
+
197
+ ## Future Enhancements
198
+
199
+ Potential improvements to consider:
200
+
201
+ - Dynamic server addition/removal during runtime
202
+ - Server health monitoring and auto-reconnection
203
+ - Tool caching and performance optimization
204
+ - Support for MCP resources and prompts
205
+ - Rate limiting and timeout configuration
agent/config.py CHANGED
@@ -2,12 +2,22 @@ from litellm import Tool
2
  from pydantic import BaseModel
3
 
4
 
 
 
 
 
 
 
 
 
 
5
  class Config(BaseModel):
6
  """Configuration manager"""
7
 
8
  model_name: str
9
  tools: list[Tool]
10
  system_prompt_path: str
 
11
 
12
 
13
  def load_config(config_path: str = "config.json") -> Config:
 
2
  from pydantic import BaseModel
3
 
4
 
5
+ class MCPServerConfig(BaseModel):
6
+ """Configuration for an MCP server"""
7
+
8
+ name: str
9
+ command: str
10
+ args: list[str]
11
+ env: dict[str, str] | None = None
12
+
13
+
14
  class Config(BaseModel):
15
  """Configuration manager"""
16
 
17
  model_name: str
18
  tools: list[Tool]
19
  system_prompt_path: str
20
+ mcp_servers: list[MCPServerConfig] = []
21
 
22
 
23
  def load_config(config_path: str = "config.json") -> Config:
agent/config_mcp_example.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "anthropic/claude-sonnet-4-5-20250929",
3
+ "tools": [],
4
+ "system_prompt_path": "",
5
+ "mcp_servers": [
6
+ {
7
+ "name": "weather",
8
+ "command": "python",
9
+ "args": ["path/to/your/weather_server.py"],
10
+ "env": null
11
+ }
12
+ ]
13
+ }
agent/core/__init__.py CHANGED
@@ -4,5 +4,6 @@ Contains the main agent logic, decision-making, and orchestration
4
  """
5
 
6
  from agent.core.executor import ToolExecutor
 
7
 
8
- __all__ = ["ToolExecutor"]
 
4
  """
5
 
6
  from agent.core.executor import ToolExecutor
7
+ from agent.core.mcp_client import MCPClient, MCPServerConfig
8
 
9
+ __all__ = ["ToolExecutor", "MCPClient", "MCPServerConfig"]
agent/core/agent_loop.py CHANGED
@@ -141,6 +141,40 @@ class Handlers:
141
  return True
142
 
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  async def submission_loop(
145
  submission_queue: asyncio.Queue,
146
  event_queue: asyncio.Queue,
@@ -151,45 +185,31 @@ async def submission_loop(
151
  This is the core of the agent (like submission_loop in codex.rs:1259-1340)
152
  """
153
  session = Session(event_queue, config=config)
154
-
155
  print("🤖 Agent loop started")
156
 
157
- # Main processing loop
158
- while session.is_running:
159
  try:
160
- # Wait for next submission (like rx_sub.recv() in codex.rs:1262)
161
- submission = await submission_queue.get()
162
-
163
- print(f"📨 Received: {submission.operation.op_type.value}")
164
-
165
- # Dispatch to handler based on operation type
166
- op = submission.operation
167
-
168
- if op.op_type == OpType.USER_INPUT:
169
- text = op.data.get("text", "") if op.data else ""
170
- await Handlers.run_agent(session, text, max_iterations=10)
171
-
172
- elif op.op_type == OpType.INTERRUPT:
173
- # im not currently sure what this does lol
174
- await Handlers.interrupt(session)
175
-
176
- elif op.op_type == OpType.COMPACT:
177
- await Handlers.compact(session)
178
 
179
- elif op.op_type == OpType.UNDO:
180
- await Handlers.undo(session)
 
 
181
 
182
- elif op.op_type == OpType.SHUTDOWN:
183
- if await Handlers.shutdown(session):
 
184
  break
185
-
186
- else:
187
- print(f"⚠️ Unknown operation: {op.op_type}")
188
-
189
- except asyncio.CancelledError:
190
- break
191
- except Exception as e:
192
- print(f"❌ Error in agent loop: {e}")
193
- await session.send_event(Event(event_type="error", data={"error": str(e)}))
194
 
195
  print("🛑 Agent loop exited")
 
141
  return True
142
 
143
 
144
+ async def process_submission(session: Session, submission) -> bool:
145
+ """
146
+ Process a single submission and return whether to continue running.
147
+
148
+ Returns:
149
+ bool: True to continue, False to shutdown
150
+ """
151
+ op = submission.operation
152
+ print(f"📨 Received: {op.op_type.value}")
153
+
154
+ if op.op_type == OpType.USER_INPUT:
155
+ text = op.data.get("text", "") if op.data else ""
156
+ await Handlers.run_agent(session, text, max_iterations=10)
157
+ return True
158
+
159
+ if op.op_type == OpType.INTERRUPT:
160
+ await Handlers.interrupt(session)
161
+ return True
162
+
163
+ if op.op_type == OpType.COMPACT:
164
+ await Handlers.compact(session)
165
+ return True
166
+
167
+ if op.op_type == OpType.UNDO:
168
+ await Handlers.undo(session)
169
+ return True
170
+
171
+ if op.op_type == OpType.SHUTDOWN:
172
+ return not await Handlers.shutdown(session)
173
+
174
+ print(f"⚠️ Unknown operation: {op.op_type}")
175
+ return True
176
+
177
+
178
  async def submission_loop(
179
  submission_queue: asyncio.Queue,
180
  event_queue: asyncio.Queue,
 
185
  This is the core of the agent (like submission_loop in codex.rs:1259-1340)
186
  """
187
  session = Session(event_queue, config=config)
 
188
  print("🤖 Agent loop started")
189
 
190
+ # Initialize MCP connections
191
+ if session.config.mcp_servers:
192
  try:
193
+ print(f"Initializing MCP connections for {session.config.mcp_servers}")
194
+ await session.initialize_mcp()
195
+ except Exception as e:
196
+ print(f"⚠️ Error initializing MCP: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ try:
199
+ # Main processing loop
200
+ while session.is_running:
201
+ submission = await submission_queue.get()
202
 
203
+ try:
204
+ should_continue = await process_submission(session, submission)
205
+ if not should_continue:
206
  break
207
+ except asyncio.CancelledError:
208
+ break
209
+ except Exception as e:
210
+ print(f"❌ Error in agent loop: {e}")
211
+ await session.send_event(Event(event_type="error", data={"error": str(e)}))
212
+ finally:
213
+ await session.cleanup()
 
 
214
 
215
  print("🛑 Agent loop exited")
agent/core/executor.py CHANGED
@@ -2,6 +2,7 @@
2
  Task execution engine
3
  """
4
 
 
5
  from typing import Any, List
6
 
7
  from litellm import ChatCompletionMessageToolCall
@@ -18,10 +19,30 @@ class ToolResult(BaseModel):
18
  class ToolExecutor:
19
  """Executes planned tasks using available tools"""
20
 
21
- def __init__(self, tools: List[Any] = None):
22
  self.tools = tools or []
 
23
 
24
  async def execute_tool(self, tool_call: ToolCall) -> ToolResult:
25
  """Execute a single step in the plan"""
26
- # TODO: Implement step execution
27
- return ToolResult(output="", success=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  Task execution engine
3
  """
4
 
5
+ import json
6
  from typing import Any, List
7
 
8
  from litellm import ChatCompletionMessageToolCall
 
19
  class ToolExecutor:
20
  """Executes planned tasks using available tools"""
21
 
22
+ def __init__(self, tools: List[Any] = None, mcp_client=None):
23
  self.tools = tools or []
24
+ self.mcp_client = mcp_client
25
 
26
  async def execute_tool(self, tool_call: ToolCall) -> ToolResult:
27
  """Execute a single step in the plan"""
28
+ tool_name = tool_call.function.name
29
+
30
+ # Parse arguments
31
+ try:
32
+ if isinstance(tool_call.function.arguments, str):
33
+ tool_args = json.loads(tool_call.function.arguments)
34
+ else:
35
+ tool_args = tool_call.function.arguments
36
+ except json.JSONDecodeError as e:
37
+ return ToolResult(
38
+ output=f"Error parsing tool arguments: {str(e)}", success=False
39
+ )
40
+
41
+ # Check if this is an MCP tool (prefixed with server name)
42
+ if self.mcp_client and "__" in tool_name:
43
+ success, result = await self.mcp_client.call_tool(tool_name, tool_args)
44
+ return ToolResult(output=result, success=success)
45
+
46
+ # If not an MCP tool, try local tools
47
+ # TODO: Implement local tool execution
48
+ return ToolResult(output=f"Tool {tool_name} not found", success=False)
agent/core/mcp_client.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP (Model Context Protocol) client integration for the agent
3
+ """
4
+
5
+ from contextlib import AsyncExitStack
6
+ from typing import Optional
7
+
8
+ from mcp import ClientSession, StdioServerParameters
9
+ from mcp.client.stdio import stdio_client
10
+
11
+
12
+ class MCPServerConfig:
13
+ """Configuration for an MCP server"""
14
+
15
+ def __init__(
16
+ self,
17
+ name: str,
18
+ command: str,
19
+ args: list[str],
20
+ env: Optional[dict[str, str]] = None,
21
+ ):
22
+ self.name = name
23
+ self.command = command
24
+ self.args = args
25
+ self.env = env
26
+
27
+
28
+ class MCPClient:
29
+ """
30
+ Manages connections to MCP servers and provides tool access
31
+ """
32
+
33
+ def __init__(self):
34
+ self.sessions: dict[str, ClientSession] = {}
35
+ self.exit_stack = AsyncExitStack()
36
+ self._tools_cache: Optional[list[dict]] = None
37
+
38
+ async def connect_to_server(self, server_config: MCPServerConfig) -> None:
39
+ """
40
+ Connect to an MCP server
41
+
42
+ Args:
43
+ server_config: Configuration for the MCP server
44
+ """
45
+ server_params = StdioServerParameters(
46
+ command=server_config.command,
47
+ args=server_config.args,
48
+ env=server_config.env,
49
+ )
50
+
51
+ stdio_transport = await self.exit_stack.enter_async_context(
52
+ stdio_client(server_params)
53
+ )
54
+ stdio, write = stdio_transport
55
+ session = await self.exit_stack.enter_async_context(ClientSession(stdio, write))
56
+
57
+ await session.initialize()
58
+
59
+ # Store the session
60
+ self.sessions[server_config.name] = session
61
+
62
+ # Invalidate tools cache
63
+ self._tools_cache = None
64
+
65
+ print(f"✅ Connected to MCP server: {server_config.name}")
66
+
67
+ async def list_tools(self) -> list[dict]:
68
+ """
69
+ Get all available tools from all connected servers
70
+
71
+ Returns:
72
+ List of tool definitions compatible with LiteLLM format
73
+ """
74
+ if self._tools_cache is not None:
75
+ return self._tools_cache
76
+
77
+ all_tools = []
78
+
79
+ for server_name, session in self.sessions.items():
80
+ try:
81
+ response = await session.list_tools()
82
+ for tool in response.tools:
83
+ # Convert MCP tool format to LiteLLM tool format
84
+ tool_def = {
85
+ "type": "function",
86
+ "function": {
87
+ "name": f"{server_name}__{tool.name}", # Prefix with server name
88
+ "description": tool.description or "",
89
+ "parameters": tool.inputSchema,
90
+ },
91
+ }
92
+ all_tools.append(tool_def)
93
+ except Exception as e:
94
+ print(f"⚠️ Error listing tools from {server_name}: {e}")
95
+
96
+ self._tools_cache = all_tools
97
+ return all_tools
98
+
99
+ async def call_tool(self, tool_name: str, tool_args: dict) -> tuple[bool, str]:
100
+ """
101
+ Call a tool on the appropriate MCP server
102
+
103
+ Args:
104
+ tool_name: Name of the tool (format: "server_name__tool_name")
105
+ tool_args: Arguments to pass to the tool
106
+
107
+ Returns:
108
+ Tuple of (success, result_content)
109
+ """
110
+ # Parse server name from tool name
111
+ if "__" not in tool_name:
112
+ return False, f"Invalid tool name format: {tool_name}"
113
+
114
+ server_name, actual_tool_name = tool_name.split("__", 1)
115
+
116
+ if server_name not in self.sessions:
117
+ return False, f"Server not found: {server_name}"
118
+
119
+ session = self.sessions[server_name]
120
+
121
+ try:
122
+ result = await session.call_tool(actual_tool_name, tool_args)
123
+
124
+ # Extract content from result
125
+ if hasattr(result, "content"):
126
+ if isinstance(result.content, list):
127
+ # Handle list of content items
128
+ content_parts = []
129
+ for item in result.content:
130
+ if hasattr(item, "text"):
131
+ content_parts.append(item.text)
132
+ else:
133
+ content_parts.append(str(item))
134
+ content = "\n".join(content_parts)
135
+ else:
136
+ content = str(result.content)
137
+ else:
138
+ content = str(result)
139
+
140
+ return True, content
141
+
142
+ except Exception as e:
143
+ return False, f"Error calling tool {tool_name}: {str(e)}"
144
+
145
+ async def cleanup(self) -> None:
146
+ """Clean up all MCP connections"""
147
+ await self.exit_stack.aclose()
148
+ self.sessions.clear()
149
+ self._tools_cache = None
agent/core/session.py CHANGED
@@ -7,6 +7,7 @@ from pydantic import BaseModel
7
  from agent.config import Config
8
  from agent.context_manager.manager import ContextManager
9
  from agent.core import ToolExecutor
 
10
 
11
 
12
  class OpType(Enum):
@@ -41,15 +42,48 @@ class Session:
41
 
42
  def __init__(self, event_queue: asyncio.Queue, config: Config | None = None):
43
  self.context_manager = ContextManager()
44
- self.tool_executor = ToolExecutor()
45
  self.event_queue = event_queue
46
  self.config = config or Config(
47
  model_name="anthropic/claude-sonnet-4-5-20250929",
48
  tools=[],
49
  system_prompt_path="",
50
  )
 
 
 
 
 
51
  self.is_running = True
52
  self.current_task: asyncio.Task | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  async def send_event(self, event: Event) -> None:
55
  """Send event back to client"""
@@ -59,3 +93,7 @@ class Session:
59
  """Interrupt current running task"""
60
  if self.current_task and not self.current_task.done():
61
  self.current_task.cancel()
 
 
 
 
 
7
  from agent.config import Config
8
  from agent.context_manager.manager import ContextManager
9
  from agent.core import ToolExecutor
10
+ from agent.core.mcp_client import MCPClient, MCPServerConfig as MCPServerConfigClass
11
 
12
 
13
  class OpType(Enum):
 
42
 
43
  def __init__(self, event_queue: asyncio.Queue, config: Config | None = None):
44
  self.context_manager = ContextManager()
 
45
  self.event_queue = event_queue
46
  self.config = config or Config(
47
  model_name="anthropic/claude-sonnet-4-5-20250929",
48
  tools=[],
49
  system_prompt_path="",
50
  )
51
+
52
+ # Initialize MCP client
53
+ self.mcp_client = MCPClient()
54
+ self.tool_executor = ToolExecutor(mcp_client=self.mcp_client)
55
+
56
  self.is_running = True
57
  self.current_task: asyncio.Task | None = None
58
+ self._mcp_initialized = False
59
+
60
+ async def initialize_mcp(self) -> None:
61
+ """Initialize MCP server connections"""
62
+ if self._mcp_initialized:
63
+ return
64
+
65
+ for server_config in self.config.mcp_servers:
66
+ try:
67
+ mcp_server_config = MCPServerConfigClass(
68
+ name=server_config.name,
69
+ command=server_config.command,
70
+ args=server_config.args,
71
+ env=server_config.env,
72
+ )
73
+ await self.mcp_client.connect_to_server(mcp_server_config)
74
+ except Exception as e:
75
+ print(f"⚠️ Failed to connect to MCP server {server_config.name}: {e}")
76
+
77
+ # Get MCP tools and merge with config tools
78
+ try:
79
+ mcp_tools = await self.mcp_client.list_tools()
80
+ # Merge with existing tools
81
+ self.config.tools = list(self.config.tools) + mcp_tools
82
+ print(f"📦 Loaded {len(mcp_tools)} tools from MCP servers")
83
+ except Exception as e:
84
+ print(f"⚠️ Error loading MCP tools: {e}")
85
+
86
+ self._mcp_initialized = True
87
 
88
  async def send_event(self, event: Event) -> None:
89
  """Send event back to client"""
 
93
  """Interrupt current running task"""
94
  if self.current_task and not self.current_task.done():
95
  self.current_task.cancel()
96
+
97
+ async def cleanup(self) -> None:
98
+ """Cleanup session resources"""
99
+ await self.mcp_client.cleanup()