Spaces:
Running
Running
Commit ·
0332824
1
Parent(s): 190a5f6
preliminary mcp
Browse files- agent/MCP_INTEGRATION.md +205 -0
- agent/config.py +10 -0
- agent/config_mcp_example.json +13 -0
- agent/core/__init__.py +2 -1
- agent/core/agent_loop.py +54 -34
- agent/core/executor.py +24 -3
- agent/core/mcp_client.py +149 -0
- agent/core/session.py +39 -1
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 |
-
#
|
| 158 |
-
|
| 159 |
try:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
print(f"
|
| 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 |
-
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
|
|
|
| 184 |
break
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 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 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|