Merge branch 'main' into feat-incorporate-file-system-tools
This commit is contained in:
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../core/.claude/skills/building-agents
|
||||
@@ -62,3 +62,5 @@ __pycache__/
|
||||
.cache/
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
exports/*
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"agent-builder": {
|
||||
"command": "python",
|
||||
"args": ["-m", "framework.mcp.agent_builder_server"],
|
||||
"cwd": "/home/timothy/oss/hive/core"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,80 +81,48 @@ docker compose up
|
||||
Traditional agent frameworks require you to manually design workflows, define agent interactions, and handle failures reactively. Aden flips this paradigm—**you describe outcomes, and the system builds itself**.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph USER["👤 User"]
|
||||
GOAL[("🎯 Define Goal<br/>(Natural Language)")]
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph CODING["🤖 Coding Agent"]
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
GENERATE["Generate Agent Graph"]
|
||||
CONNECTION["Create Connection Code"]
|
||||
TESTGEN["Generate Test Cases"]
|
||||
EVOLVE["Evolve on Failure"]
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph WORKERS["⚙️ Worker Agents"]
|
||||
direction TB
|
||||
subgraph NODE1["SDK-Wrapped Node"]
|
||||
N1_MEM["Memory (STM/LTM)"]
|
||||
N1_TOOLS["Tools Access"]
|
||||
N1_LLM["LLM Integration"]
|
||||
N1_MON["Monitoring"]
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
subgraph NODE2["SDK-Wrapped Node"]
|
||||
N2_MEM["Memory (STM/LTM)"]
|
||||
N2_TOOLS["Tools Access"]
|
||||
N2_LLM["LLM Integration"]
|
||||
N2_MON["Monitoring"]
|
||||
end
|
||||
HITL["🙋 Human-in-the-Loop<br/>Intervention Points"]
|
||||
end
|
||||
|
||||
subgraph CONTROL["🎛️ Hive Control Plane"]
|
||||
direction TB
|
||||
BUDGET["Budget & Cost Control"]
|
||||
POLICY["Policy Management"]
|
||||
METRICS["Real-time Metrics"]
|
||||
MCP["19 MCP Tools"]
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
subgraph STORAGE["💾 Storage Layer"]
|
||||
TSDB[("TimescaleDB<br/>Metrics & Events")]
|
||||
MONGO[("MongoDB<br/>Policies")]
|
||||
POSTGRES[("PostgreSQL<br/>Users & Config")]
|
||||
end
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
subgraph DASHBOARD["📊 Dashboard (Honeycomb)"]
|
||||
ANALYTICS["Analytics & KPIs"]
|
||||
AGENTS["Agent Monitoring"]
|
||||
COSTS["Cost Tracking"]
|
||||
end
|
||||
|
||||
GOAL --> GENERATE
|
||||
GENERATE --> CONNECTION
|
||||
CONNECTION --> TESTGEN
|
||||
TESTGEN --> NODE1
|
||||
TESTGEN --> NODE2
|
||||
|
||||
NODE1 <--> NODE2
|
||||
NODE1 & NODE2 --> HITL
|
||||
|
||||
NODE1 & NODE2 -->|Events| CONTROL
|
||||
CONTROL -->|Policies| NODE1 & NODE2
|
||||
CONTROL <-->|WebSocket| DASHBOARD
|
||||
|
||||
CONTROL --> STORAGE
|
||||
|
||||
NODE1 & NODE2 -->|Failure Data| EVOLVE
|
||||
EVOLVE -->|Updated Graph| GENERATE
|
||||
|
||||
style USER fill:#e8f5e9,stroke:#2e7d32
|
||||
style CODING fill:#e3f2fd,stroke:#1565c0
|
||||
style WORKERS fill:#fff3e0,stroke:#ef6c00
|
||||
style CONTROL fill:#fce4ec,stroke:#c2185b
|
||||
style STORAGE fill:#f3e5f5,stroke:#7b1fa2
|
||||
style DASHBOARD fill:#e0f7fa,stroke:#00838f
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### The Aden Advantage
|
||||
|
||||
@@ -433,6 +433,161 @@ Goal(
|
||||
**Good goals**: Specific, measurable, constrained
|
||||
**Bad goals**: Vague, unmeasurable, no boundaries
|
||||
|
||||
## Integrating External Tools (MCP Servers)
|
||||
|
||||
Before adding nodes, you can register MCP servers to make their tools available to your agent.
|
||||
|
||||
### Using aden-tools in the Hive Monorepo
|
||||
|
||||
The hive monorepo includes `aden-tools` which provides web search, web scraping, and file operations.
|
||||
|
||||
**Step 1: Register the MCP Server**
|
||||
|
||||
After creating your session, register aden-tools:
|
||||
|
||||
```python
|
||||
# Using MCP tools
|
||||
add_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args='["mcp_server.py", "--stdio"]',
|
||||
cwd="../aden-tools" # Relative to core/ directory
|
||||
)
|
||||
```
|
||||
|
||||
**Expected response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"server": {
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.server"],
|
||||
"cwd": "../aden-tools"
|
||||
},
|
||||
"tools_discovered": 6,
|
||||
"tools": [
|
||||
"web_search",
|
||||
"web_scrape",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"pdf_read",
|
||||
"example_tool"
|
||||
],
|
||||
"note": "MCP server 'aden-tools' registered with 6 tools..."
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: List Available Tools** (optional verification)
|
||||
|
||||
```python
|
||||
list_mcp_tools(server_name="aden-tools")
|
||||
```
|
||||
|
||||
This shows detailed information about each tool including parameters.
|
||||
|
||||
**Step 3: Use Tools in Your Nodes**
|
||||
|
||||
Now you can reference these tools in `llm_tool_use` nodes:
|
||||
|
||||
```python
|
||||
add_node(
|
||||
node_id="web_searcher",
|
||||
name="Web Searcher",
|
||||
description="Search the web for information",
|
||||
node_type="llm_tool_use",
|
||||
input_keys='["query"]',
|
||||
output_keys='["search_results"]',
|
||||
tools='["web_search"]', # ← Tool from aden-tools
|
||||
system_prompt="Search for {query} using web_search tool"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Export Creates mcp_servers.json**
|
||||
|
||||
When you export your agent with `export_graph()`, the MCP server configuration is automatically saved:
|
||||
|
||||
```
|
||||
exports/my-agent/
|
||||
├── agent.json # Agent specification
|
||||
├── README.md # Documentation
|
||||
└── mcp_servers.json # ← MCP configuration (auto-generated)
|
||||
```
|
||||
|
||||
The `mcp_servers.json` file ensures the agent can access aden-tools when run later.
|
||||
|
||||
### Available aden-tools
|
||||
|
||||
| Tool | Description | Key Parameters |
|
||||
|------|-------------|----------------|
|
||||
| `web_search` | Search the web using Brave Search API | `query`, `num_results`, `country` |
|
||||
| `web_scrape` | Extract text content from a webpage | `url`, `selector`, `include_links` |
|
||||
| `file_read` | Read file contents | `path` |
|
||||
| `file_write` | Write content to files | `path`, `content` |
|
||||
| `pdf_read` | Extract text from PDF files | `path` |
|
||||
|
||||
### MCP Server Management
|
||||
|
||||
List registered servers:
|
||||
```python
|
||||
list_mcp_servers()
|
||||
```
|
||||
|
||||
Remove a server:
|
||||
```python
|
||||
remove_mcp_server(name="aden-tools")
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Register early**: Call `add_mcp_server` right after `create_session` and before defining nodes
|
||||
2. **Verify tools**: Use `list_mcp_tools` to see available tools and their parameters
|
||||
3. **Minimal tools**: Only include tools a node actually needs in its `tools` list
|
||||
4. **Test nodes**: Use `test_node` to verify tool access works before building the full graph
|
||||
|
||||
### Example: Research Agent with aden-tools
|
||||
|
||||
```python
|
||||
# 1. Create session
|
||||
create_session(name="research-agent")
|
||||
|
||||
# 2. Register aden-tools
|
||||
add_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args='["mcp_server.py", "--stdio"]',
|
||||
cwd="../aden-tools"
|
||||
)
|
||||
|
||||
# 3. Verify tools
|
||||
list_mcp_tools(server_name="aden-tools")
|
||||
|
||||
# 4. Define goal
|
||||
set_goal(
|
||||
goal_id="research",
|
||||
name="Research Agent",
|
||||
description="Gather and synthesize information",
|
||||
success_criteria='[...]',
|
||||
constraints='[...]'
|
||||
)
|
||||
|
||||
# 5. Add node that uses web_search
|
||||
add_node(
|
||||
node_id="searcher",
|
||||
name="Information Searcher",
|
||||
node_type="llm_tool_use",
|
||||
input_keys='["topic"]',
|
||||
output_keys='["search_results"]',
|
||||
tools='["web_search"]', # From aden-tools
|
||||
system_prompt="Search for information about {topic}"
|
||||
)
|
||||
|
||||
# 6. Continue building...
|
||||
```
|
||||
|
||||
## Adding Nodes
|
||||
|
||||
Each node does one thing:
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
"agent-builder": {
|
||||
"command": "python",
|
||||
"args": ["-m", "framework.mcp.agent_builder_server"],
|
||||
"cwd": "/home/timothy/aden/worker-bee"
|
||||
"cwd": "/home/timothy/oss/hive/core"
|
||||
},
|
||||
"aden-tools": {
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "/Users/guangjitang/acho/hive/aden-tools"
|
||||
"cwd": "/home/timothy/oss/hive/aden-tools"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
# Agent Builder MCP Tools - MCP Integration Guide
|
||||
|
||||
This guide explains how to use the new MCP integration tools in the agent builder MCP server.
|
||||
|
||||
## Overview
|
||||
|
||||
The agent builder now supports registering external MCP servers as tool sources. This allows you to:
|
||||
|
||||
1. Register MCP servers (like aden-tools) during agent building
|
||||
2. Discover available tools from those servers
|
||||
3. Use those tools in your agent nodes
|
||||
4. Automatically generate `mcp_servers.json` configuration on export
|
||||
|
||||
## New MCP Tools
|
||||
|
||||
### `add_mcp_server`
|
||||
|
||||
Register an MCP server as a tool source for your agent.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string, required): Unique name for the MCP server
|
||||
- `transport` (string, required): Transport type - "stdio" or "http"
|
||||
- `command` (string): Command to run (for stdio transport)
|
||||
- `args` (string): JSON array of command arguments (for stdio)
|
||||
- `cwd` (string): Working directory (for stdio)
|
||||
- `env` (string): JSON object of environment variables (for stdio)
|
||||
- `url` (string): Server URL (for http transport)
|
||||
- `headers` (string): JSON object of HTTP headers (for http)
|
||||
- `description` (string): Description of the MCP server
|
||||
|
||||
**Example - STDIO:**
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": "[\"mcp_server.py\", \"--stdio\"]",
|
||||
"cwd": "../aden-tools",
|
||||
"description": "Aden tools for web search and file operations"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example - HTTP:**
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
"arguments": {
|
||||
"name": "remote-tools",
|
||||
"transport": "http",
|
||||
"url": "http://localhost:4001",
|
||||
"description": "Remote tool server"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"server": {
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"description": "Aden tools..."
|
||||
},
|
||||
"tools_discovered": 6,
|
||||
"tools": [
|
||||
"web_search",
|
||||
"web_scrape",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"pdf_read",
|
||||
"example_tool"
|
||||
],
|
||||
"total_mcp_servers": 1,
|
||||
"note": "MCP server 'aden-tools' registered with 6 tools. These tools can now be used in llm_tool_use nodes."
|
||||
}
|
||||
```
|
||||
|
||||
### `list_mcp_servers`
|
||||
|
||||
List all registered MCP servers.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"mcp_servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"description": "Aden tools..."
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### `list_mcp_tools`
|
||||
|
||||
List tools available from registered MCP servers.
|
||||
|
||||
**Parameters:**
|
||||
- `server_name` (string, optional): Name of specific server to list tools from. If omitted, lists tools from all servers.
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "list_mcp_tools",
|
||||
"arguments": {
|
||||
"server_name": "aden-tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"tools_by_server": {
|
||||
"aden-tools": [
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "Search the web for information using Brave Search API...",
|
||||
"parameters": ["query", "num_results", "country"]
|
||||
},
|
||||
{
|
||||
"name": "web_scrape",
|
||||
"description": "Scrape and extract text content from a webpage...",
|
||||
"parameters": ["url", "selector", "include_links", "max_length"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_tools": 6,
|
||||
"note": "Use these tool names in the 'tools' parameter when adding llm_tool_use nodes"
|
||||
}
|
||||
```
|
||||
|
||||
### `remove_mcp_server`
|
||||
|
||||
Remove a registered MCP server.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string, required): Name of the MCP server to remove
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "remove_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"removed": "aden-tools",
|
||||
"remaining_servers": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow Example
|
||||
|
||||
Here's a complete workflow for building an agent with MCP tools:
|
||||
|
||||
### 1. Create Session
|
||||
```json
|
||||
{
|
||||
"name": "create_session",
|
||||
"arguments": {
|
||||
"name": "web-research-agent"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register MCP Server
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": "[\"mcp_server.py\", \"--stdio\"]",
|
||||
"cwd": "../aden-tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. List Available Tools
|
||||
```json
|
||||
{
|
||||
"name": "list_mcp_tools",
|
||||
"arguments": {
|
||||
"server_name": "aden-tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Set Goal
|
||||
```json
|
||||
{
|
||||
"name": "set_goal",
|
||||
"arguments": {
|
||||
"goal_id": "web-research",
|
||||
"name": "Web Research Agent",
|
||||
"description": "Search the web and summarize findings",
|
||||
"success_criteria": "[{\"id\": \"search-success\", \"description\": \"Successfully retrieve search results\", \"metric\": \"results_count\", \"target\": \">= 3\", \"weight\": 1.0}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Node with MCP Tool
|
||||
```json
|
||||
{
|
||||
"name": "add_node",
|
||||
"arguments": {
|
||||
"node_id": "web-searcher",
|
||||
"name": "Web Search",
|
||||
"description": "Search the web for information",
|
||||
"node_type": "llm_tool_use",
|
||||
"input_keys": "[\"query\"]",
|
||||
"output_keys": "[\"search_results\"]",
|
||||
"system_prompt": "Search for {query} using the web_search tool",
|
||||
"tools": "[\"web_search\"]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `web_search` is now available because we registered the aden-tools MCP server!
|
||||
|
||||
### 6. Export Agent
|
||||
```json
|
||||
{
|
||||
"name": "export_graph",
|
||||
"arguments": {}
|
||||
}
|
||||
```
|
||||
|
||||
The export will create:
|
||||
- `exports/web-research-agent/agent.json` - Agent specification
|
||||
- `exports/web-research-agent/README.md` - Documentation
|
||||
- `exports/web-research-agent/mcp_servers.json` - **MCP server configuration** ✨
|
||||
|
||||
## MCP Configuration File
|
||||
|
||||
When you export an agent with registered MCP servers, an `mcp_servers.json` file is automatically created:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"description": "Aden tools for web search and file operations"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This file is automatically loaded by the AgentRunner when the agent is executed, making the MCP tools available at runtime.
|
||||
|
||||
## Using the Exported Agent
|
||||
|
||||
Once exported, load and run the agent normally:
|
||||
|
||||
```python
|
||||
from framework.runner.runner import AgentRunner
|
||||
|
||||
# Load agent - MCP servers auto-load from mcp_servers.json
|
||||
runner = AgentRunner.load("exports/web-research-agent")
|
||||
|
||||
# Run with input
|
||||
result = await runner.run({"query": "latest AI breakthroughs"})
|
||||
|
||||
# The web_search tool from aden-tools is automatically available!
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Discoverable Tools**: See what tools are available before using them
|
||||
2. **Validation**: Connection is tested when registering the server
|
||||
3. **Automatic Configuration**: No manual file editing required
|
||||
4. **Documentation**: README includes MCP server information
|
||||
5. **Runtime Ready**: Exported agents work immediately with configured tools
|
||||
|
||||
## Common MCP Servers
|
||||
|
||||
### aden-tools
|
||||
Provides:
|
||||
- `web_search` - Brave Search API integration
|
||||
- `web_scrape` - Web page content extraction
|
||||
- `file_read` / `file_write` - File operations
|
||||
- `pdf_read` - PDF text extraction
|
||||
|
||||
### Custom MCP Servers
|
||||
You can register any MCP server that follows the Model Context Protocol specification.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Failed to connect to MCP server"
|
||||
|
||||
- Verify the `command` and `args` are correct
|
||||
- Check that the server is accessible at the specified path/URL
|
||||
- Ensure any required environment variables are set
|
||||
- For STDIO: verify the command can be executed from the `cwd`
|
||||
- For HTTP: verify the server is running and accessible
|
||||
|
||||
### Tools not appearing
|
||||
|
||||
- Use `list_mcp_tools` to verify tools were discovered
|
||||
- Check the tool names match exactly (case-sensitive)
|
||||
- Ensure the MCP server is still registered (`list_mcp_servers`)
|
||||
|
||||
### Export doesn't include mcp_servers.json
|
||||
|
||||
- Verify you registered at least one MCP server
|
||||
- Check `get_session_status` to see `mcp_servers_count > 0`
|
||||
- Re-export the agent after registering servers
|
||||
@@ -0,0 +1,361 @@
|
||||
# MCP Integration Guide
|
||||
|
||||
This guide explains how to integrate Model Context Protocol (MCP) servers with the Hive Core Framework, enabling agents to use tools from external MCP servers.
|
||||
|
||||
## Overview
|
||||
|
||||
The framework provides built-in support for MCP servers, allowing you to:
|
||||
|
||||
- **Register MCP servers** via STDIO or HTTP transport
|
||||
- **Auto-discover tools** from registered servers
|
||||
- **Use MCP tools** seamlessly in your agents
|
||||
- **Manage multiple MCP servers** simultaneously
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Register an MCP Server Programmatically
|
||||
|
||||
```python
|
||||
from framework.runner.runner import AgentRunner
|
||||
|
||||
# Load your agent
|
||||
runner = AgentRunner.load("exports/my-agent")
|
||||
|
||||
# Register aden-tools MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="/path/to/aden-tools"
|
||||
)
|
||||
|
||||
# Tools are now available to your agent
|
||||
result = await runner.run({"input": "data"})
|
||||
```
|
||||
|
||||
### 2. Use Configuration File
|
||||
|
||||
Create `mcp_servers.json` in your agent folder:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The framework will automatically load and register these servers when you load the agent:
|
||||
|
||||
```python
|
||||
runner = AgentRunner.load("exports/my-agent") # MCP servers auto-loaded
|
||||
```
|
||||
|
||||
## Transport Types
|
||||
|
||||
### STDIO Transport
|
||||
|
||||
Best for local MCP servers running as subprocesses:
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="local-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "my_tools.server", "--stdio"],
|
||||
cwd="/path/to/my-tools",
|
||||
env={
|
||||
"API_KEY": "your-key-here"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
- `command`: Executable to run (e.g., "python", "node")
|
||||
- `args`: List of command-line arguments
|
||||
- `cwd`: Working directory for the process
|
||||
- `env`: Environment variables (optional)
|
||||
|
||||
### HTTP Transport
|
||||
|
||||
Best for remote MCP servers or containerized deployments:
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="remote-tools",
|
||||
transport="http",
|
||||
url="http://localhost:4001",
|
||||
headers={
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
- `url`: Base URL of the MCP server
|
||||
- `headers`: HTTP headers to include (optional)
|
||||
|
||||
## Using MCP Tools in Agents
|
||||
|
||||
Once registered, MCP tools are available just like any other tool:
|
||||
|
||||
### In Node Specifications
|
||||
|
||||
```python
|
||||
from framework.builder.workflow import WorkflowBuilder
|
||||
|
||||
builder = WorkflowBuilder()
|
||||
|
||||
# Add a node that uses MCP tools
|
||||
builder.add_node(
|
||||
node_id="researcher",
|
||||
name="Web Researcher",
|
||||
node_type="llm_tool_use",
|
||||
system_prompt="Research the topic using web_search",
|
||||
tools=["web_search"], # Tool from aden-tools MCP server
|
||||
input_keys=["topic"],
|
||||
output_keys=["findings"]
|
||||
)
|
||||
```
|
||||
|
||||
### In Agent.json
|
||||
|
||||
Tools from MCP servers can be referenced in your agent.json just like built-in tools:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "searcher",
|
||||
"name": "Web Searcher",
|
||||
"node_type": "llm_tool_use",
|
||||
"system_prompt": "Search for information about {topic}",
|
||||
"tools": ["web_search", "web_scrape"],
|
||||
"input_keys": ["topic"],
|
||||
"output_keys": ["results"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools from aden-tools
|
||||
|
||||
When you register the `aden-tools` MCP server, the following tools become available:
|
||||
|
||||
- **web_search**: Search the web using Brave Search API
|
||||
- **web_scrape**: Scrape content from a URL
|
||||
- **file_read**: Read file contents
|
||||
- **file_write**: Write content to a file
|
||||
- **pdf_read**: Extract text from PDF files
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Some MCP tools require environment variables. You can pass them in the configuration:
|
||||
|
||||
### Via Programmatic Registration
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
env={
|
||||
"BRAVE_SEARCH_API_KEY": os.environ["BRAVE_SEARCH_API_KEY"]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Via Configuration File
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"env": {
|
||||
"BRAVE_SEARCH_API_KEY": "${BRAVE_SEARCH_API_KEY}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The framework will substitute `${VAR_NAME}` with values from the environment.
|
||||
|
||||
## Multiple MCP Servers
|
||||
|
||||
You can register multiple MCP servers to access different sets of tools:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools"
|
||||
},
|
||||
{
|
||||
"name": "database-tools",
|
||||
"transport": "http",
|
||||
"url": "http://localhost:5001"
|
||||
},
|
||||
{
|
||||
"name": "analytics-tools",
|
||||
"transport": "http",
|
||||
"url": "http://analytics-server:6001"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All tools from all servers will be available to your agent.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use STDIO for Development
|
||||
|
||||
STDIO transport is easier to debug and doesn't require managing server processes:
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="dev-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "my_tools.server", "--stdio"]
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Use HTTP for Production
|
||||
|
||||
HTTP transport is better for:
|
||||
- Containerized deployments
|
||||
- Shared tools across multiple agents
|
||||
- Remote tool execution
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="prod-tools",
|
||||
transport="http",
|
||||
url="http://tools-service:8000"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Handle Cleanup
|
||||
|
||||
Always clean up MCP connections when done:
|
||||
|
||||
```python
|
||||
try:
|
||||
runner = AgentRunner.load("exports/my-agent")
|
||||
runner.register_mcp_server(...)
|
||||
result = await runner.run(input_data)
|
||||
finally:
|
||||
runner.cleanup() # Disconnects all MCP servers
|
||||
```
|
||||
|
||||
Or use context manager:
|
||||
|
||||
```python
|
||||
async with AgentRunner.load("exports/my-agent") as runner:
|
||||
runner.register_mcp_server(...)
|
||||
result = await runner.run(input_data)
|
||||
# Automatic cleanup
|
||||
```
|
||||
|
||||
### 4. Tool Name Conflicts
|
||||
|
||||
If multiple MCP servers provide tools with the same name, the last registered server wins. To avoid conflicts:
|
||||
|
||||
- Use unique tool names in your MCP servers
|
||||
- Register servers in priority order (most important last)
|
||||
- Use separate agents for different tool sets
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Errors
|
||||
|
||||
If you get connection errors with STDIO transport:
|
||||
|
||||
1. Check that the command and path are correct
|
||||
2. Verify the MCP server starts successfully standalone
|
||||
3. Check environment variables are set correctly
|
||||
4. Look at stderr output for error messages
|
||||
|
||||
### Tool Not Found
|
||||
|
||||
If a tool is registered but not found:
|
||||
|
||||
1. Verify the server registered successfully (check logs)
|
||||
2. List available tools: `runner._tool_registry.get_registered_names()`
|
||||
3. Check tool name spelling in your node configuration
|
||||
|
||||
### HTTP Server Not Responding
|
||||
|
||||
If HTTP transport fails:
|
||||
|
||||
1. Verify the server is running: `curl http://localhost:4001/health`
|
||||
2. Check firewall settings
|
||||
3. Verify the URL and port are correct
|
||||
|
||||
## Example: Full Agent with MCP Tools
|
||||
|
||||
Here's a complete example of an agent that uses MCP tools:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from framework.runner.runner import AgentRunner
|
||||
|
||||
async def main():
|
||||
# Create agent path
|
||||
agent_path = Path("exports/web-research-agent")
|
||||
|
||||
# Load agent
|
||||
runner = AgentRunner.load(agent_path)
|
||||
|
||||
# Register MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
env={
|
||||
"BRAVE_SEARCH_API_KEY": "your-api-key"
|
||||
}
|
||||
)
|
||||
|
||||
# Run agent
|
||||
result = await runner.run({
|
||||
"query": "latest developments in quantum computing"
|
||||
})
|
||||
|
||||
print(f"Research complete: {result}")
|
||||
|
||||
# Cleanup
|
||||
runner.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP_SERVER_GUIDE.md](MCP_SERVER_GUIDE.md) - Building your own MCP servers
|
||||
- [examples/mcp_integration_example.py](examples/mcp_integration_example.py) - More examples
|
||||
- [examples/mcp_servers.json](examples/mcp_servers.json) - Example configuration
|
||||
+208
-37
@@ -64,7 +64,7 @@ To use the agent builder with Claude Desktop or other MCP clients, add this to y
|
||||
"agent-builder": {
|
||||
"command": "python",
|
||||
"args": ["-m", "framework.mcp.agent_builder_server"],
|
||||
"cwd": "/path/to/goal-agent"
|
||||
"cwd": "/path/to/hive/core"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,48 +75,144 @@ The MCP server provides tools for:
|
||||
- Defining goals with success criteria
|
||||
- Adding nodes (llm_generate, llm_tool_use, router, function)
|
||||
- Connecting nodes with edges
|
||||
- **Registering MCP servers as tool sources** ✨
|
||||
- **Discovering tools from MCP servers** ✨
|
||||
- Validating and exporting agent graphs
|
||||
- Testing nodes and full agent graphs
|
||||
|
||||
## Quick Start
|
||||
When you register an MCP server during agent building, the tools from that server become available to your agent, and an `mcp_servers.json` configuration file is automatically created on export.
|
||||
|
||||
### Calculator Agent
|
||||
See [MCP_SERVER_GUIDE.md](MCP_SERVER_GUIDE.md) for agent builder instructions and [MCP_BUILDER_TOOLS_GUIDE.md](MCP_BUILDER_TOOLS_GUIDE.md) for MCP integration tools.
|
||||
|
||||
Run an LLM-powered calculator:
|
||||
## MCP Tool Integration
|
||||
|
||||
```bash
|
||||
# Single calculation
|
||||
python -m framework calculate "2 + 3 * 4"
|
||||
The framework also supports **connecting to MCP servers as tool providers**, allowing your agents to use tools from external MCP servers (like aden-tools). This enables you to extend your agents with powerful external capabilities.
|
||||
|
||||
# Interactive mode
|
||||
python -m framework interactive
|
||||
### Quick Example
|
||||
|
||||
# Analyze runs with Builder
|
||||
python -m framework analyze calculator
|
||||
```python
|
||||
from framework.runner.runner import AgentRunner
|
||||
|
||||
# Load an agent
|
||||
runner = AgentRunner.load("exports/task-planner")
|
||||
|
||||
# Register an MCP server with tools
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["mcp_server.py", "--stdio"],
|
||||
cwd="../aden-tools"
|
||||
)
|
||||
|
||||
# Tools from the MCP server are now available to your agent
|
||||
result = await runner.run({"query": "Search for AI news"})
|
||||
```
|
||||
|
||||
### Using the Runtime
|
||||
### Auto-loading MCP Servers
|
||||
|
||||
Create `mcp_servers.json` in your agent folder:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
MCP servers will be automatically loaded when you load the agent.
|
||||
|
||||
### Available Tools from aden-tools
|
||||
|
||||
When you register the aden-tools MCP server, these tools become available:
|
||||
- `web_search` - Search the web using Brave Search API
|
||||
- `web_scrape` - Extract content from web pages
|
||||
- `file_read` - Read file contents
|
||||
- `file_write` - Write content to files
|
||||
- `pdf_read` - Extract text from PDF files
|
||||
|
||||
See [MCP_INTEGRATION_GUIDE.md](MCP_INTEGRATION_GUIDE.md) for detailed instructions on MCP tool integration.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Running Agents
|
||||
|
||||
The framework comes with pre-built example agents in the `exports/` directory:
|
||||
|
||||
```bash
|
||||
# List available agents
|
||||
python -m framework list exports/
|
||||
|
||||
# Show agent information
|
||||
python -m framework info exports/task-planner
|
||||
|
||||
# Run an agent
|
||||
python -m framework run exports/task-planner --input '{"objective": "Build a web scraper"}'
|
||||
|
||||
# Interactive shell mode (with human-in-the-loop approval)
|
||||
python -m framework shell exports/task-planner
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
- `run` - Execute an exported agent with given input
|
||||
- `info` - Display agent details (goal, nodes, edges, success criteria)
|
||||
- `validate` - Check that an agent is valid and runnable
|
||||
- `list` - List all exported agents in a directory
|
||||
- `dispatch` - Route requests to multiple agents using the orchestrator
|
||||
- `shell` - Start an interactive session with an agent
|
||||
|
||||
### Building Agents Programmatically
|
||||
|
||||
You can build agents using the MCP server (recommended) or programmatically:
|
||||
|
||||
```python
|
||||
from framework import Runtime
|
||||
|
||||
runtime = Runtime("/path/to/storage")
|
||||
# Initialize runtime with storage path
|
||||
runtime = Runtime("./storage")
|
||||
|
||||
# Start a run
|
||||
run_id = runtime.start_run("my_goal", "Description of what we're doing")
|
||||
# Start a run for a goal
|
||||
run_id = runtime.start_run(
|
||||
goal_id="data-processor",
|
||||
goal_description="Process data with quality checks",
|
||||
input_data={"dataset": "customers.csv"}
|
||||
)
|
||||
|
||||
# Set the current node context
|
||||
runtime.set_node("processor-node")
|
||||
|
||||
# Record a decision
|
||||
decision_id = runtime.decide(
|
||||
intent="Choose how to process the data",
|
||||
options=[
|
||||
{"id": "fast", "description": "Quick processing", "pros": ["Fast"], "cons": ["Less accurate"]},
|
||||
{"id": "thorough", "description": "Detailed processing", "pros": ["Accurate"], "cons": ["Slower"]},
|
||||
{
|
||||
"id": "fast",
|
||||
"description": "Quick processing",
|
||||
"action_type": "tool_call",
|
||||
"pros": ["Fast"],
|
||||
"cons": ["Less accurate"]
|
||||
},
|
||||
{
|
||||
"id": "thorough",
|
||||
"description": "Detailed processing",
|
||||
"action_type": "tool_call",
|
||||
"pros": ["Accurate"],
|
||||
"cons": ["Slower"]
|
||||
},
|
||||
],
|
||||
chosen="thorough",
|
||||
reasoning="Accuracy is more important for this task"
|
||||
)
|
||||
|
||||
# Record the outcome
|
||||
# Record the outcome of the decision
|
||||
runtime.record_outcome(
|
||||
decision_id=decision_id,
|
||||
success=True,
|
||||
@@ -125,58 +221,133 @@ runtime.record_outcome(
|
||||
)
|
||||
|
||||
# End the run
|
||||
runtime.end_run(success=True, narrative="Successfully processed all data")
|
||||
runtime.end_run(
|
||||
success=True,
|
||||
narrative="Successfully processed all data",
|
||||
output_data={"total_processed": 100}
|
||||
)
|
||||
```
|
||||
|
||||
### Analyzing with Builder
|
||||
### Analyzing Agent Behavior with Builder
|
||||
|
||||
The BuilderQuery interface allows you to analyze agent runs and identify improvements:
|
||||
|
||||
```python
|
||||
from framework import BuilderQuery
|
||||
|
||||
query = BuilderQuery("/path/to/storage")
|
||||
# Initialize Builder query interface
|
||||
query = BuilderQuery("./storage")
|
||||
|
||||
# Find patterns across runs
|
||||
patterns = query.find_patterns("my_goal")
|
||||
print(f"Success rate: {patterns.success_rate:.1%}")
|
||||
# Find patterns across runs for a goal
|
||||
patterns = query.find_patterns("data-processor")
|
||||
if patterns:
|
||||
print(f"Success rate: {patterns.success_rate:.1%}")
|
||||
print(f"Runs analyzed: {patterns.run_count}")
|
||||
|
||||
# Analyze a failure
|
||||
analysis = query.analyze_failure("run_123")
|
||||
print(f"Root cause: {analysis.root_cause}")
|
||||
print(f"Suggestions: {analysis.suggestions}")
|
||||
# Show problematic nodes
|
||||
for node_id, failure_rate in patterns.problematic_nodes:
|
||||
print(f"Node '{node_id}' has {failure_rate:.1%} failure rate")
|
||||
|
||||
# Get improvement recommendations
|
||||
suggestions = query.suggest_improvements("my_goal")
|
||||
# Analyze a specific failure
|
||||
analysis = query.analyze_failure("run_20260119_143022_abc123")
|
||||
if analysis:
|
||||
print(f"Failure point: {analysis.failure_point}")
|
||||
print(f"Root cause: {analysis.root_cause}")
|
||||
print(f"\nSuggestions:")
|
||||
for suggestion in analysis.suggestions:
|
||||
print(f" - {suggestion}")
|
||||
|
||||
# Get improvement recommendations for a goal
|
||||
suggestions = query.suggest_improvements("data-processor")
|
||||
for s in suggestions:
|
||||
print(f"[{s['priority']}] {s['recommendation']}")
|
||||
print(f" Reason: {s['reason']}")
|
||||
|
||||
# Get performance metrics for a specific node
|
||||
perf = query.get_node_performance("processor-node")
|
||||
print(f"Node: {perf['node_id']}")
|
||||
print(f"Success rate: {perf['success_rate']:.1%}")
|
||||
print(f"Avg latency: {perf['avg_latency_ms']:.0f}ms")
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The framework consists of several layers:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Human Engineer │ ← Supervision, approval
|
||||
│ Human Engineer │ ← Supervision, approval via HITL
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Builder LLM │ ← Analyzes runs, suggests improvements
|
||||
│ Builder LLM │ ← Analyzes runs, suggests improvements (via MCP)
|
||||
│ (BuilderQuery) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Agent LLM │ ← Executes tasks, records decisions
|
||||
│ (Runtime) │
|
||||
│ Agent Graph │ ← Node-based execution flow
|
||||
│ (AgentRunner) │ (llm_generate, llm_tool_use, router, function)
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Runtime │ ← Records decisions, outcomes, problems
|
||||
│ (Decision DB) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Graph-Based Agents
|
||||
|
||||
Agents are defined as directed graphs with:
|
||||
- **Nodes**: Execution steps (llm_generate, llm_tool_use, router, function)
|
||||
- **Edges**: Control flow between nodes, including conditional routing
|
||||
- **Goal**: What the agent is designed to accomplish with success criteria
|
||||
- **Constraints**: Hard and soft limits on agent behavior
|
||||
|
||||
### Decision Recording
|
||||
|
||||
- **Decision**: The atomic unit of agent behavior. Captures intent, options, choice, and reasoning.
|
||||
- **Run**: A complete execution with all decisions and outcomes.
|
||||
- **Runtime**: Interface agents use to record their behavior.
|
||||
- **BuilderQuery**: Interface Builder uses to analyze agent behavior.
|
||||
- **Outcome**: Result of executing a decision (success/failure, latency, tokens, state changes)
|
||||
- **Run**: A complete execution trace with all decisions and outcomes
|
||||
- **Problem**: Issues reported during execution with severity and suggested fixes
|
||||
|
||||
### Analysis & Improvement
|
||||
|
||||
- **Runtime**: Interface agents use to record their behavior during execution
|
||||
- **BuilderQuery**: Interface for analyzing agent runs and identifying patterns
|
||||
- **PatternAnalysis**: Cross-run analysis showing success rates, common failures, problematic nodes
|
||||
- **FailureAnalysis**: Deep dive into why a specific run failed with suggestions
|
||||
|
||||
### Human-in-the-Loop (HITL)
|
||||
|
||||
- **Approval Callbacks**: Nodes can require human approval before execution
|
||||
- **Interactive Shell**: Chat-like interface for running agents with approval prompts
|
||||
- **Session State**: Agents can pause and resume based on user input
|
||||
|
||||
### Multi-Agent Orchestration
|
||||
|
||||
- **AgentOrchestrator**: Dispatch requests to multiple agents
|
||||
- **Agent Discovery**: Automatically discover and register agents from a directory
|
||||
- **Dispatch Strategy**: Route requests to the most appropriate agent(s)
|
||||
|
||||
## Example Agents
|
||||
|
||||
The `exports/` directory contains example agents you can run or use as templates:
|
||||
|
||||
- **task-planner**: Breaks down complex objectives into actionable tasks with dependencies
|
||||
- **research-summary-agent**: Conducts research and generates summaries
|
||||
- **outbound-sales-agent**: Handles outbound sales workflows
|
||||
- **youtube-comments-research**: Analyzes YouTube comments for insights
|
||||
|
||||
Each agent includes:
|
||||
- `agent.json`: Graph definition with nodes, edges, goal, and constraints
|
||||
- `README.md`: Agent documentation
|
||||
- `tools.py` (optional): Custom tool implementations
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- pydantic >= 2.0
|
||||
- anthropic >= 0.40.0 (for LLM-powered agents)
|
||||
- mcp, fastmcp (optional, for MCP server)
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example: Integrating MCP Servers with the Core Framework
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Register MCP servers programmatically
|
||||
2. Use MCP tools in agents
|
||||
3. Load MCP servers from configuration files
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from framework.runner.runner import AgentRunner
|
||||
|
||||
|
||||
async def example_1_programmatic_registration():
|
||||
"""Example 1: Register MCP server programmatically"""
|
||||
print("\n=== Example 1: Programmatic MCP Server Registration ===\n")
|
||||
|
||||
# Load an existing agent
|
||||
runner = AgentRunner.load("exports/task-planner")
|
||||
|
||||
# Register aden-tools MCP server via STDIO
|
||||
num_tools = runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
)
|
||||
|
||||
print(f"Registered {num_tools} tools from aden-tools MCP server")
|
||||
|
||||
# List all available tools
|
||||
tools = runner._tool_registry.get_tools()
|
||||
print(f"\nAvailable tools: {list(tools.keys())}")
|
||||
|
||||
# Run the agent with MCP tools available
|
||||
result = await runner.run({
|
||||
"objective": "Search for 'Claude AI' and summarize the top 3 results"
|
||||
})
|
||||
|
||||
print(f"\nAgent result: {result}")
|
||||
|
||||
# Cleanup
|
||||
runner.cleanup()
|
||||
|
||||
|
||||
async def example_2_http_transport():
|
||||
"""Example 2: Connect to MCP server via HTTP"""
|
||||
print("\n=== Example 2: HTTP MCP Server Connection ===\n")
|
||||
|
||||
# First, start the aden-tools MCP server in HTTP mode:
|
||||
# cd aden-tools && python mcp_server.py --port 4001
|
||||
|
||||
runner = AgentRunner.load("exports/task-planner")
|
||||
|
||||
# Register aden-tools via HTTP
|
||||
num_tools = runner.register_mcp_server(
|
||||
name="aden-tools-http",
|
||||
transport="http",
|
||||
url="http://localhost:4001",
|
||||
)
|
||||
|
||||
print(f"Registered {num_tools} tools from HTTP MCP server")
|
||||
|
||||
# Cleanup
|
||||
runner.cleanup()
|
||||
|
||||
|
||||
async def example_3_config_file():
|
||||
"""Example 3: Load MCP servers from configuration file"""
|
||||
print("\n=== Example 3: Load from Configuration File ===\n")
|
||||
|
||||
# Create a test agent folder with mcp_servers.json
|
||||
test_agent_path = Path("exports/task-planner")
|
||||
|
||||
# Copy example config (in practice, you'd place this in your agent folder)
|
||||
import shutil
|
||||
shutil.copy(
|
||||
"examples/mcp_servers.json",
|
||||
test_agent_path / "mcp_servers.json"
|
||||
)
|
||||
|
||||
# Load agent - MCP servers will be auto-discovered
|
||||
runner = AgentRunner.load(test_agent_path)
|
||||
|
||||
# Tools are automatically available
|
||||
tools = runner._tool_registry.get_tools()
|
||||
print(f"Available tools: {list(tools.keys())}")
|
||||
|
||||
# Cleanup
|
||||
runner.cleanup()
|
||||
|
||||
# Clean up the test config
|
||||
(test_agent_path / "mcp_servers.json").unlink()
|
||||
|
||||
|
||||
async def example_4_custom_agent_with_mcp_tools():
|
||||
"""Example 4: Build custom agent that uses MCP tools"""
|
||||
print("\n=== Example 4: Custom Agent with MCP Tools ===\n")
|
||||
|
||||
from framework.builder.workflow import WorkflowBuilder
|
||||
|
||||
# Create a workflow builder
|
||||
builder = WorkflowBuilder()
|
||||
|
||||
# Define goal
|
||||
builder.set_goal(
|
||||
goal_id="web-researcher",
|
||||
name="Web Research Agent",
|
||||
description="Search the web and summarize findings"
|
||||
)
|
||||
|
||||
# Add success criteria
|
||||
builder.add_success_criterion(
|
||||
"search-results",
|
||||
"Successfully retrieve at least 3 web search results"
|
||||
)
|
||||
builder.add_success_criterion(
|
||||
"summary",
|
||||
"Provide a clear, concise summary of the findings"
|
||||
)
|
||||
|
||||
# Add nodes that will use MCP tools
|
||||
builder.add_node(
|
||||
node_id="web-searcher",
|
||||
name="Web Search",
|
||||
description="Search the web for information",
|
||||
node_type="llm_tool_use",
|
||||
system_prompt="Search for {query} and return the top results. Use the web_search tool.",
|
||||
tools=["web_search"], # This tool comes from aden-tools MCP server
|
||||
input_keys=["query"],
|
||||
output_keys=["search_results"],
|
||||
)
|
||||
|
||||
builder.add_node(
|
||||
node_id="summarizer",
|
||||
name="Summarize Results",
|
||||
description="Summarize the search results",
|
||||
node_type="llm_generate",
|
||||
system_prompt="Summarize the following search results in 2-3 sentences: {search_results}",
|
||||
input_keys=["search_results"],
|
||||
output_keys=["summary"],
|
||||
)
|
||||
|
||||
# Connect nodes
|
||||
builder.add_edge("web-searcher", "summarizer")
|
||||
|
||||
# Set entry point
|
||||
builder.set_entry("web-searcher")
|
||||
builder.set_terminal("summarizer")
|
||||
|
||||
# Export the agent
|
||||
export_path = Path("exports/web-research-agent")
|
||||
export_path.mkdir(parents=True, exist_ok=True)
|
||||
builder.export(export_path)
|
||||
|
||||
# Load and register MCP server
|
||||
runner = AgentRunner.load(export_path)
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
result = await runner.run({"query": "latest AI breakthroughs 2026"})
|
||||
|
||||
print(f"\nAgent completed with result:\n{result}")
|
||||
|
||||
# Cleanup
|
||||
runner.cleanup()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples"""
|
||||
print("=" * 60)
|
||||
print("MCP Integration Examples")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Run examples
|
||||
await example_1_programmatic_registration()
|
||||
# await example_2_http_transport() # Requires HTTP server running
|
||||
# await example_3_config_file()
|
||||
# await example_4_custom_agent_with_mcp_tools()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError running example: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"description": "Aden tools including web search, file operations, and PDF reading",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"env": {
|
||||
"BRAVE_SEARCH_API_KEY": "${BRAVE_SEARCH_API_KEY}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aden-tools-http",
|
||||
"description": "Aden tools via HTTP (for Docker deployments)",
|
||||
"transport": "http",
|
||||
"url": "http://localhost:4001",
|
||||
"headers": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -392,10 +392,10 @@ class LLMNode(NodeProtocol):
|
||||
|
||||
def executor(tool_use: ToolUse) -> ToolResult:
|
||||
logger.info(f" 🔧 Tool call: {tool_use.name}({', '.join(f'{k}={v}' for k, v in tool_use.input.items())})")
|
||||
result = self.tool_executor(ctx, tool_use)
|
||||
result = self.tool_executor(tool_use)
|
||||
# Truncate long results
|
||||
result_str = str(result.output)[:150]
|
||||
if len(str(result.output)) > 150:
|
||||
result_str = str(result.content)[:150]
|
||||
if len(str(result.content)) > 150:
|
||||
result_str += "..."
|
||||
logger.info(f" ✓ Tool result: {result_str}")
|
||||
return result
|
||||
|
||||
@@ -31,6 +31,7 @@ class BuildSession:
|
||||
self.goal: Goal | None = None
|
||||
self.nodes: list[NodeSpec] = []
|
||||
self.edges: list[EdgeSpec] = []
|
||||
self.mcp_servers: list[dict] = [] # MCP server configurations
|
||||
|
||||
|
||||
# Global session
|
||||
@@ -309,6 +310,155 @@ def add_edge(
|
||||
}, default=str)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_node(
|
||||
node_id: Annotated[str, "ID of the node to update"],
|
||||
name: Annotated[str, "Updated human-readable name"] = "",
|
||||
description: Annotated[str, "Updated description"] = "",
|
||||
node_type: Annotated[str, "Updated type: llm_generate, llm_tool_use, router, or function"] = "",
|
||||
input_keys: Annotated[str, "Updated JSON array of input keys"] = "",
|
||||
output_keys: Annotated[str, "Updated JSON array of output keys"] = "",
|
||||
system_prompt: Annotated[str, "Updated instructions for LLM nodes"] = "",
|
||||
tools: Annotated[str, "Updated JSON array of tool names"] = "",
|
||||
routes: Annotated[str, "Updated JSON object mapping conditions to target node IDs"] = "",
|
||||
) -> str:
|
||||
"""Update an existing node in the agent graph. Only provided fields will be updated."""
|
||||
session = get_session()
|
||||
|
||||
# Find the node
|
||||
node = None
|
||||
for n in session.nodes:
|
||||
if n.id == node_id:
|
||||
node = n
|
||||
break
|
||||
|
||||
if not node:
|
||||
return json.dumps({"valid": False, "errors": [f"Node '{node_id}' not found"]})
|
||||
|
||||
# Update fields if provided
|
||||
if name:
|
||||
node.name = name
|
||||
if description:
|
||||
node.description = description
|
||||
if node_type:
|
||||
node.node_type = node_type
|
||||
if input_keys:
|
||||
node.input_keys = json.loads(input_keys)
|
||||
if output_keys:
|
||||
node.output_keys = json.loads(output_keys)
|
||||
if system_prompt:
|
||||
node.system_prompt = system_prompt
|
||||
if tools:
|
||||
node.tools = json.loads(tools)
|
||||
if routes:
|
||||
node.routes = json.loads(routes)
|
||||
|
||||
# Validate
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if node.node_type == "llm_tool_use" and not node.tools:
|
||||
errors.append(f"Node '{node_id}' of type llm_tool_use must specify tools")
|
||||
if node.node_type == "router" and not node.routes:
|
||||
errors.append(f"Router node '{node_id}' must specify routes")
|
||||
if node.node_type in ("llm_generate", "llm_tool_use") and not node.system_prompt:
|
||||
warnings.append(f"LLM node '{node_id}' should have a system_prompt")
|
||||
|
||||
return json.dumps({
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"node": node.model_dump(),
|
||||
"total_nodes": len(session.nodes),
|
||||
"approval_required": True,
|
||||
"approval_question": {
|
||||
"component_type": "node",
|
||||
"component_name": node.name,
|
||||
"question": f"Do you approve this updated {node.node_type} node: {node.name}?",
|
||||
"header": "Approve Node Update",
|
||||
"options": [
|
||||
{
|
||||
"label": "✓ Approve (Recommended)",
|
||||
"description": f"Updated node '{node.name}' looks good"
|
||||
},
|
||||
{
|
||||
"label": "✗ Reject & Modify",
|
||||
"description": "Need to change node configuration"
|
||||
},
|
||||
{
|
||||
"label": "⏸ Pause & Review",
|
||||
"description": "I need more time to review this update"
|
||||
}
|
||||
]
|
||||
}
|
||||
}, default=str)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def delete_node(
|
||||
node_id: Annotated[str, "ID of the node to delete"],
|
||||
) -> str:
|
||||
"""Delete a node from the agent graph. Also removes all edges connected to this node."""
|
||||
session = get_session()
|
||||
|
||||
# Find the node
|
||||
node_idx = None
|
||||
for i, n in enumerate(session.nodes):
|
||||
if n.id == node_id:
|
||||
node_idx = i
|
||||
break
|
||||
|
||||
if node_idx is None:
|
||||
return json.dumps({"valid": False, "errors": [f"Node '{node_id}' not found"]})
|
||||
|
||||
# Remove the node
|
||||
removed_node = session.nodes.pop(node_idx)
|
||||
|
||||
# Remove all edges connected to this node
|
||||
removed_edges = [e.id for e in session.edges if e.source == node_id or e.target == node_id]
|
||||
session.edges = [
|
||||
e for e in session.edges
|
||||
if not (e.source == node_id or e.target == node_id)
|
||||
]
|
||||
|
||||
return json.dumps({
|
||||
"valid": True,
|
||||
"deleted_node": removed_node.model_dump(),
|
||||
"removed_edges": removed_edges,
|
||||
"total_nodes": len(session.nodes),
|
||||
"total_edges": len(session.edges),
|
||||
"message": f"Node '{node_id}' and {len(removed_edges)} connected edge(s) removed"
|
||||
}, default=str)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def delete_edge(
|
||||
edge_id: Annotated[str, "ID of the edge to delete"],
|
||||
) -> str:
|
||||
"""Delete an edge from the agent graph."""
|
||||
session = get_session()
|
||||
|
||||
# Find the edge
|
||||
edge_idx = None
|
||||
for i, e in enumerate(session.edges):
|
||||
if e.id == edge_id:
|
||||
edge_idx = i
|
||||
break
|
||||
|
||||
if edge_idx is None:
|
||||
return json.dumps({"valid": False, "errors": [f"Edge '{edge_id}' not found"]})
|
||||
|
||||
# Remove the edge
|
||||
removed_edge = session.edges.pop(edge_idx)
|
||||
|
||||
return json.dumps({
|
||||
"valid": True,
|
||||
"deleted_edge": removed_edge.model_dump(),
|
||||
"total_edges": len(session.edges),
|
||||
"message": f"Edge '{edge_id}' removed: {removed_edge.source} → {removed_edge.target}"
|
||||
}, default=str)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def validate_graph() -> str:
|
||||
"""Validate the complete graph. Checks for unreachable nodes, missing connections, and context flow."""
|
||||
@@ -324,6 +474,18 @@ def validate_graph() -> str:
|
||||
errors.append("No nodes defined")
|
||||
return json.dumps({"valid": False, "errors": errors})
|
||||
|
||||
# === DETECT PAUSE/RESUME ARCHITECTURE ===
|
||||
# Identify pause nodes (nodes marked as PAUSE in description)
|
||||
pause_nodes = [n.id for n in session.nodes if "PAUSE" in n.description.upper()]
|
||||
|
||||
# Identify resume entry points (nodes marked as RESUME ENTRY POINT in description)
|
||||
resume_entry_points = [n.id for n in session.nodes if "RESUME" in n.description.upper() and "ENTRY" in n.description.upper()]
|
||||
|
||||
is_pause_resume_agent = len(pause_nodes) > 0 or len(resume_entry_points) > 0
|
||||
|
||||
if is_pause_resume_agent:
|
||||
warnings.append(f"Pause/resume architecture detected. Pause nodes: {pause_nodes}, Resume entry points: {resume_entry_points}")
|
||||
|
||||
# Find entry node (no incoming edges)
|
||||
entry_candidates = []
|
||||
for node in session.nodes:
|
||||
@@ -332,7 +494,8 @@ def validate_graph() -> str:
|
||||
|
||||
if not entry_candidates:
|
||||
errors.append("No entry node found (all nodes have incoming edges)")
|
||||
elif len(entry_candidates) > 1:
|
||||
elif len(entry_candidates) > 1 and not is_pause_resume_agent:
|
||||
# Multiple entry points are expected for pause/resume agents
|
||||
warnings.append(f"Multiple entry candidates: {entry_candidates}")
|
||||
|
||||
# Find terminal nodes (no outgoing edges)
|
||||
@@ -347,7 +510,13 @@ def validate_graph() -> str:
|
||||
# Check reachability
|
||||
if entry_candidates:
|
||||
reachable = set()
|
||||
to_visit = [entry_candidates[0]]
|
||||
|
||||
# For pause/resume agents, start from ALL entry points (including resume)
|
||||
if is_pause_resume_agent:
|
||||
to_visit = list(entry_candidates) # All nodes without incoming edges
|
||||
else:
|
||||
to_visit = [entry_candidates[0]] # Just the primary entry
|
||||
|
||||
while to_visit:
|
||||
current = to_visit.pop()
|
||||
if current in reachable:
|
||||
@@ -363,7 +532,14 @@ def validate_graph() -> str:
|
||||
|
||||
unreachable = [n.id for n in session.nodes if n.id not in reachable]
|
||||
if unreachable:
|
||||
errors.append(f"Unreachable nodes: {unreachable}")
|
||||
# For pause/resume agents, nodes might be reachable only from resume entry points
|
||||
if is_pause_resume_agent:
|
||||
# Filter out resume entry points from unreachable list
|
||||
unreachable_non_resume = [n for n in unreachable if n not in resume_entry_points]
|
||||
if unreachable_non_resume:
|
||||
warnings.append(f"Nodes unreachable from primary entry (may be resume-only nodes): {unreachable_non_resume}")
|
||||
else:
|
||||
errors.append(f"Unreachable nodes: {unreachable}")
|
||||
|
||||
# === CONTEXT FLOW VALIDATION ===
|
||||
# Build dependency map (node_id -> list of nodes it depends on)
|
||||
@@ -433,27 +609,64 @@ def validate_graph() -> str:
|
||||
node = nodes_by_id.get(node_id)
|
||||
deps = dependencies.get(node_id, [])
|
||||
|
||||
# Check if this is a resume entry point
|
||||
is_resume_entry = node_id in resume_entry_points
|
||||
|
||||
if not deps:
|
||||
# Entry node - inputs must come from initial runtime context
|
||||
context_warnings.append(
|
||||
f"Node '{node_id}' requires inputs {missing} from initial context. "
|
||||
f"Ensure these are provided when running the agent."
|
||||
)
|
||||
if is_resume_entry:
|
||||
context_warnings.append(
|
||||
f"Resume entry node '{node_id}' requires inputs {missing} from resumed invocation context. "
|
||||
f"These will be provided by the runtime when resuming (e.g., user's answers)."
|
||||
)
|
||||
else:
|
||||
context_warnings.append(
|
||||
f"Node '{node_id}' requires inputs {missing} from initial context. "
|
||||
f"Ensure these are provided when running the agent."
|
||||
)
|
||||
else:
|
||||
# Find which dependency could provide each missing input
|
||||
suggestions = []
|
||||
for key in missing:
|
||||
# Check if any existing node produces this
|
||||
producers = [n.id for n in session.nodes if key in n.output_keys]
|
||||
if producers:
|
||||
suggestions.append(f"'{key}' is produced by {producers} - add dependency edge")
|
||||
else:
|
||||
suggestions.append(f"'{key}' is not produced by any node - add a node that outputs it")
|
||||
# Check if this is a common external input key for resume nodes
|
||||
external_input_keys = ["input", "user_response", "user_input", "answer", "answers"]
|
||||
unproduced_external = [k for k in missing if k in external_input_keys]
|
||||
|
||||
context_errors.append(
|
||||
f"Node '{node_id}' requires {missing} but dependencies {deps} don't provide them. "
|
||||
f"Suggestions: {'; '.join(suggestions)}"
|
||||
)
|
||||
if is_resume_entry and unproduced_external:
|
||||
# Resume entry points can receive external inputs from resumed invocations
|
||||
other_missing = [k for k in missing if k not in external_input_keys]
|
||||
|
||||
if unproduced_external:
|
||||
context_warnings.append(
|
||||
f"Resume entry node '{node_id}' expects external inputs {unproduced_external} from resumed invocation. "
|
||||
f"These will be injected by the runtime when the user responds."
|
||||
)
|
||||
|
||||
if other_missing:
|
||||
# Still need to check other keys
|
||||
suggestions = []
|
||||
for key in other_missing:
|
||||
producers = [n.id for n in session.nodes if key in n.output_keys]
|
||||
if producers:
|
||||
suggestions.append(f"'{key}' is produced by {producers} - ensure edge exists")
|
||||
else:
|
||||
suggestions.append(f"'{key}' is not produced - add node or include in external inputs")
|
||||
|
||||
context_errors.append(
|
||||
f"Resume node '{node_id}' requires {other_missing} but dependencies {deps} don't provide them. "
|
||||
f"Suggestions: {'; '.join(suggestions)}"
|
||||
)
|
||||
else:
|
||||
# Non-resume node or no external input keys - standard validation
|
||||
suggestions = []
|
||||
for key in missing:
|
||||
producers = [n.id for n in session.nodes if key in n.output_keys]
|
||||
if producers:
|
||||
suggestions.append(f"'{key}' is produced by {producers} - add dependency edge")
|
||||
else:
|
||||
suggestions.append(f"'{key}' is not produced by any node - add a node that outputs it")
|
||||
|
||||
context_errors.append(
|
||||
f"Node '{node_id}' requires {missing} but dependencies {deps} don't provide them. "
|
||||
f"Suggestions: {'; '.join(suggestions)}"
|
||||
)
|
||||
|
||||
errors.extend(context_errors)
|
||||
warnings.extend(context_warnings)
|
||||
@@ -466,6 +679,10 @@ def validate_graph() -> str:
|
||||
"terminal_nodes": terminal_candidates,
|
||||
"node_count": len(session.nodes),
|
||||
"edge_count": len(session.edges),
|
||||
"pause_resume_detected": is_pause_resume_agent,
|
||||
"pause_nodes": pause_nodes,
|
||||
"resume_entry_points": resume_entry_points,
|
||||
"all_entry_points": entry_candidates,
|
||||
"context_flow": {
|
||||
node_id: list(keys) for node_id, keys in available_context.items()
|
||||
} if available_context else None,
|
||||
@@ -584,6 +801,18 @@ def _generate_readme(session: BuildSession, export_data: dict, all_tools: set) -
|
||||
|
||||
{chr(10).join(f"- `{tool}`" for tool in sorted(all_tools)) if all_tools else "No tools required"}
|
||||
|
||||
{"## MCP Tool Sources" if session.mcp_servers else ""}
|
||||
|
||||
{chr(10).join(f'''### {s["name"]} ({s["transport"]})
|
||||
{s.get("description", "")}
|
||||
|
||||
**Configuration:**
|
||||
''' + (f'''- Command: `{s.get("command")}`
|
||||
- Args: `{s.get("args")}`
|
||||
- Working Directory: `{s.get("cwd")}`''' if s["transport"] == "stdio" else f'''- URL: `{s.get("url")}`''') for s in session.mcp_servers) if session.mcp_servers else ""}
|
||||
|
||||
{"Tools from these MCP servers are automatically loaded when the agent runs." if session.mcp_servers else ""}
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
@@ -754,30 +983,51 @@ def export_graph() -> str:
|
||||
with open(readme_path, "w") as f:
|
||||
f.write(readme_content)
|
||||
|
||||
# Write mcp_servers.json if MCP servers are configured
|
||||
mcp_servers_path = None
|
||||
mcp_servers_size = 0
|
||||
if session.mcp_servers:
|
||||
mcp_config = {
|
||||
"servers": session.mcp_servers
|
||||
}
|
||||
mcp_servers_path = exports_dir / "mcp_servers.json"
|
||||
with open(mcp_servers_path, "w") as f:
|
||||
json.dump(mcp_config, f, indent=2)
|
||||
mcp_servers_size = mcp_servers_path.stat().st_size
|
||||
|
||||
# Get file sizes
|
||||
agent_json_size = agent_json_path.stat().st_size
|
||||
readme_size = readme_path.stat().st_size
|
||||
|
||||
files_written = {
|
||||
"agent_json": {
|
||||
"path": str(agent_json_path),
|
||||
"size_bytes": agent_json_size,
|
||||
},
|
||||
"readme": {
|
||||
"path": str(readme_path),
|
||||
"size_bytes": readme_size,
|
||||
},
|
||||
}
|
||||
|
||||
if mcp_servers_path:
|
||||
files_written["mcp_servers"] = {
|
||||
"path": str(mcp_servers_path),
|
||||
"size_bytes": mcp_servers_size,
|
||||
}
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"agent": export_data["agent"],
|
||||
"files_written": {
|
||||
"agent_json": {
|
||||
"path": str(agent_json_path),
|
||||
"size_bytes": agent_json_size,
|
||||
},
|
||||
"readme": {
|
||||
"path": str(readme_path),
|
||||
"size_bytes": readme_size,
|
||||
},
|
||||
},
|
||||
"files_written": files_written,
|
||||
"graph": graph_spec,
|
||||
"goal": session.goal.model_dump(),
|
||||
"evaluation_rules": _evaluation_rules,
|
||||
"required_tools": list(all_tools),
|
||||
"node_count": len(session.nodes),
|
||||
"edge_count": len(edges_list),
|
||||
"note": f"Agent exported to {exports_dir}. Files: agent.json, README.md",
|
||||
"mcp_servers_count": len(session.mcp_servers),
|
||||
"note": f"Agent exported to {exports_dir}. Files: agent.json, README.md" + (", mcp_servers.json" if session.mcp_servers else ""),
|
||||
}, default=str, indent=2)
|
||||
|
||||
|
||||
@@ -792,8 +1042,253 @@ def get_session_status() -> str:
|
||||
"goal_name": session.goal.name if session.goal else None,
|
||||
"node_count": len(session.nodes),
|
||||
"edge_count": len(session.edges),
|
||||
"mcp_servers_count": len(session.mcp_servers),
|
||||
"nodes": [n.id for n in session.nodes],
|
||||
"edges": [(e.source, e.target) for e in session.edges],
|
||||
"mcp_servers": [s["name"] for s in session.mcp_servers],
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add_mcp_server(
|
||||
name: Annotated[str, "Unique name for the MCP server"],
|
||||
transport: Annotated[str, "Transport type: 'stdio' or 'http'"],
|
||||
command: Annotated[str, "Command to run (for stdio transport)"] = "",
|
||||
args: Annotated[str, "JSON array of command arguments (for stdio)"] = "[]",
|
||||
cwd: Annotated[str, "Working directory (for stdio)"] = "",
|
||||
env: Annotated[str, "JSON object of environment variables (for stdio)"] = "{}",
|
||||
url: Annotated[str, "Server URL (for http transport)"] = "",
|
||||
headers: Annotated[str, "JSON object of HTTP headers (for http)"] = "{}",
|
||||
description: Annotated[str, "Description of the MCP server"] = "",
|
||||
) -> str:
|
||||
"""
|
||||
Register an MCP server as a tool source for this agent.
|
||||
|
||||
The MCP server will be saved in mcp_servers.json when the agent is exported,
|
||||
and tools from this server will be available to the agent at runtime.
|
||||
|
||||
Example for stdio:
|
||||
add_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args='["mcp_server.py", "--stdio"]',
|
||||
cwd="../aden-tools"
|
||||
)
|
||||
|
||||
Example for http:
|
||||
add_mcp_server(
|
||||
name="remote-tools",
|
||||
transport="http",
|
||||
url="http://localhost:4001"
|
||||
)
|
||||
"""
|
||||
session = get_session()
|
||||
|
||||
# Validate transport
|
||||
if transport not in ["stdio", "http"]:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"Invalid transport '{transport}'. Must be 'stdio' or 'http'"
|
||||
})
|
||||
|
||||
# Check for duplicate
|
||||
if any(s["name"] == name for s in session.mcp_servers):
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"MCP server '{name}' already registered"
|
||||
})
|
||||
|
||||
# Parse JSON inputs
|
||||
try:
|
||||
args_list = json.loads(args)
|
||||
env_dict = json.loads(env)
|
||||
headers_dict = json.loads(headers)
|
||||
except json.JSONDecodeError as e:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"Invalid JSON: {e}"
|
||||
})
|
||||
|
||||
# Validate required fields
|
||||
errors = []
|
||||
if transport == "stdio" and not command:
|
||||
errors.append("command is required for stdio transport")
|
||||
if transport == "http" and not url:
|
||||
errors.append("url is required for http transport")
|
||||
|
||||
if errors:
|
||||
return json.dumps({"success": False, "errors": errors})
|
||||
|
||||
# Build server config
|
||||
server_config = {
|
||||
"name": name,
|
||||
"transport": transport,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
if transport == "stdio":
|
||||
server_config["command"] = command
|
||||
server_config["args"] = args_list
|
||||
if cwd:
|
||||
server_config["cwd"] = cwd
|
||||
if env_dict:
|
||||
server_config["env"] = env_dict
|
||||
else: # http
|
||||
server_config["url"] = url
|
||||
if headers_dict:
|
||||
server_config["headers"] = headers_dict
|
||||
|
||||
# Try to connect and discover tools
|
||||
try:
|
||||
from framework.runner.mcp_client import MCPClient, MCPServerConfig
|
||||
|
||||
mcp_config = MCPServerConfig(
|
||||
name=name,
|
||||
transport=transport,
|
||||
command=command if transport == "stdio" else None,
|
||||
args=args_list if transport == "stdio" else [],
|
||||
env=env_dict,
|
||||
cwd=cwd if cwd else None,
|
||||
url=url if transport == "http" else None,
|
||||
headers=headers_dict,
|
||||
description=description,
|
||||
)
|
||||
|
||||
with MCPClient(mcp_config) as client:
|
||||
tools = client.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
|
||||
# Add to session
|
||||
session.mcp_servers.append(server_config)
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"server": server_config,
|
||||
"tools_discovered": len(tool_names),
|
||||
"tools": tool_names,
|
||||
"total_mcp_servers": len(session.mcp_servers),
|
||||
"note": f"MCP server '{name}' registered with {len(tool_names)} tools. These tools can now be used in llm_tool_use nodes.",
|
||||
}, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"Failed to connect to MCP server: {str(e)}",
|
||||
"suggestion": "Check that the command/url is correct and the server is accessible"
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_mcp_servers() -> str:
|
||||
"""List all registered MCP servers for this agent."""
|
||||
session = get_session()
|
||||
|
||||
if not session.mcp_servers:
|
||||
return json.dumps({
|
||||
"mcp_servers": [],
|
||||
"total": 0,
|
||||
"note": "No MCP servers registered. Use add_mcp_server to add tool sources."
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"mcp_servers": session.mcp_servers,
|
||||
"total": len(session.mcp_servers),
|
||||
}, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_mcp_tools(
|
||||
server_name: Annotated[str, "Name of the MCP server to list tools from"] = "",
|
||||
) -> str:
|
||||
"""
|
||||
List tools available from registered MCP servers.
|
||||
|
||||
If server_name is provided, lists tools from that specific server.
|
||||
Otherwise, lists all tools from all registered servers.
|
||||
"""
|
||||
session = get_session()
|
||||
|
||||
if not session.mcp_servers:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "No MCP servers registered"
|
||||
})
|
||||
|
||||
# Filter servers if name provided
|
||||
servers_to_query = session.mcp_servers
|
||||
if server_name:
|
||||
servers_to_query = [s for s in session.mcp_servers if s["name"] == server_name]
|
||||
if not servers_to_query:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"MCP server '{server_name}' not found"
|
||||
})
|
||||
|
||||
all_tools = {}
|
||||
|
||||
for server_config in servers_to_query:
|
||||
try:
|
||||
from framework.runner.mcp_client import MCPClient, MCPServerConfig
|
||||
|
||||
mcp_config = MCPServerConfig(
|
||||
name=server_config["name"],
|
||||
transport=server_config["transport"],
|
||||
command=server_config.get("command"),
|
||||
args=server_config.get("args", []),
|
||||
env=server_config.get("env", {}),
|
||||
cwd=server_config.get("cwd"),
|
||||
url=server_config.get("url"),
|
||||
headers=server_config.get("headers", {}),
|
||||
description=server_config.get("description", ""),
|
||||
)
|
||||
|
||||
with MCPClient(mcp_config) as client:
|
||||
tools = client.list_tools()
|
||||
|
||||
all_tools[server_config["name"]] = [
|
||||
{
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"parameters": list(t.input_schema.get("properties", {}).keys()),
|
||||
}
|
||||
for t in tools
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
all_tools[server_config["name"]] = {
|
||||
"error": f"Failed to connect: {str(e)}"
|
||||
}
|
||||
|
||||
total_tools = sum(len(tools) if isinstance(tools, list) else 0 for tools in all_tools.values())
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"tools_by_server": all_tools,
|
||||
"total_tools": total_tools,
|
||||
"note": "Use these tool names in the 'tools' parameter when adding llm_tool_use nodes",
|
||||
}, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def remove_mcp_server(
|
||||
name: Annotated[str, "Name of the MCP server to remove"],
|
||||
) -> str:
|
||||
"""Remove a registered MCP server."""
|
||||
session = get_session()
|
||||
|
||||
for i, server in enumerate(session.mcp_servers):
|
||||
if server["name"] == name:
|
||||
session.mcp_servers.pop(i)
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"removed": name,
|
||||
"remaining_servers": len(session.mcp_servers)
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"MCP server '{name}' not found"
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
"""MCP Client for connecting to Model Context Protocol servers.
|
||||
|
||||
This module provides a client for connecting to MCP servers and invoking their tools.
|
||||
Supports both STDIO and HTTP transports using the official MCP Python SDK.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
"""Configuration for an MCP server connection."""
|
||||
|
||||
name: str
|
||||
transport: Literal["stdio", "http"]
|
||||
|
||||
# For STDIO transport
|
||||
command: str | None = None
|
||||
args: list[str] = field(default_factory=list)
|
||||
env: dict[str, str] = field(default_factory=dict)
|
||||
cwd: str | None = None
|
||||
|
||||
# For HTTP transport
|
||||
url: str | None = None
|
||||
headers: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# Optional metadata
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPTool:
|
||||
"""A tool available from an MCP server."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
input_schema: dict[str, Any]
|
||||
server_name: str
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""
|
||||
Client for communicating with MCP servers.
|
||||
|
||||
Supports both STDIO and HTTP transports using the official MCP SDK.
|
||||
Manages the connection lifecycle and provides methods to list and invoke tools.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MCPServerConfig):
|
||||
"""
|
||||
Initialize the MCP client.
|
||||
|
||||
Args:
|
||||
config: Server configuration
|
||||
"""
|
||||
self.config = config
|
||||
self._session = None
|
||||
self._read_stream = None
|
||||
self._write_stream = None
|
||||
self._http_client: httpx.Client | None = None
|
||||
self._tools: dict[str, MCPTool] = {}
|
||||
self._connected = False
|
||||
|
||||
def _run_async(self, coro):
|
||||
"""
|
||||
Run an async coroutine, handling both sync and async contexts.
|
||||
|
||||
Args:
|
||||
coro: Coroutine to run
|
||||
|
||||
Returns:
|
||||
Result of the coroutine
|
||||
"""
|
||||
try:
|
||||
# Try to get the current event loop
|
||||
asyncio.get_running_loop()
|
||||
# If we're here, we're in an async context
|
||||
# Create a new thread to run the coroutine
|
||||
import threading
|
||||
|
||||
result = None
|
||||
exception = None
|
||||
|
||||
def run_in_thread():
|
||||
nonlocal result, exception
|
||||
try:
|
||||
new_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(new_loop)
|
||||
try:
|
||||
result = new_loop.run_until_complete(coro)
|
||||
finally:
|
||||
new_loop.close()
|
||||
except Exception as e:
|
||||
exception = e
|
||||
|
||||
thread = threading.Thread(target=run_in_thread)
|
||||
thread.start()
|
||||
thread.join()
|
||||
|
||||
if exception:
|
||||
raise exception
|
||||
return result
|
||||
except RuntimeError:
|
||||
# No event loop running, we can use asyncio.run
|
||||
return asyncio.run(coro)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect to the MCP server."""
|
||||
if self._connected:
|
||||
return
|
||||
|
||||
if self.config.transport == "stdio":
|
||||
self._connect_stdio()
|
||||
elif self.config.transport == "http":
|
||||
self._connect_http()
|
||||
else:
|
||||
raise ValueError(f"Unsupported transport: {self.config.transport}")
|
||||
|
||||
# Discover tools
|
||||
self._discover_tools()
|
||||
self._connected = True
|
||||
|
||||
def _connect_stdio(self) -> None:
|
||||
"""Connect to MCP server via STDIO transport using MCP SDK."""
|
||||
if not self.config.command:
|
||||
raise ValueError("command is required for STDIO transport")
|
||||
|
||||
try:
|
||||
# Import MCP SDK
|
||||
from mcp import StdioServerParameters
|
||||
|
||||
# Create server parameters
|
||||
server_params = StdioServerParameters(
|
||||
command=self.config.command,
|
||||
args=self.config.args,
|
||||
env=self.config.env or None,
|
||||
cwd=self.config.cwd,
|
||||
)
|
||||
|
||||
# Store for later use in async context
|
||||
self._server_params = server_params
|
||||
|
||||
logger.info(f"Connected to MCP server '{self.config.name}' via STDIO")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to connect to MCP server: {e}")
|
||||
|
||||
def _connect_http(self) -> None:
|
||||
"""Connect to MCP server via HTTP transport."""
|
||||
if not self.config.url:
|
||||
raise ValueError("url is required for HTTP transport")
|
||||
|
||||
self._http_client = httpx.Client(
|
||||
base_url=self.config.url,
|
||||
headers=self.config.headers,
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
response = self._http_client.get("/health")
|
||||
response.raise_for_status()
|
||||
logger.info(f"Connected to MCP server '{self.config.name}' via HTTP at {self.config.url}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Health check failed for MCP server '{self.config.name}': {e}")
|
||||
# Continue anyway, server might not have health endpoint
|
||||
|
||||
def _discover_tools(self) -> None:
|
||||
"""Discover available tools from the MCP server."""
|
||||
try:
|
||||
if self.config.transport == "stdio":
|
||||
tools_list = self._run_async(self._list_tools_stdio_async())
|
||||
else:
|
||||
tools_list = self._list_tools_http()
|
||||
|
||||
self._tools = {}
|
||||
for tool_data in tools_list:
|
||||
tool = MCPTool(
|
||||
name=tool_data["name"],
|
||||
description=tool_data.get("description", ""),
|
||||
input_schema=tool_data.get("inputSchema", {}),
|
||||
server_name=self.config.name,
|
||||
)
|
||||
self._tools[tool.name] = tool
|
||||
|
||||
logger.info(f"Discovered {len(self._tools)} tools from '{self.config.name}': {list(self._tools.keys())}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to discover tools from '{self.config.name}': {e}")
|
||||
raise
|
||||
|
||||
async def _list_tools_stdio_async(self) -> list[dict]:
|
||||
"""List tools via STDIO protocol using MCP SDK."""
|
||||
from mcp import ClientSession
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
async with stdio_client(self._server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# Initialize the session
|
||||
await session.initialize()
|
||||
|
||||
# List tools
|
||||
response = await session.list_tools()
|
||||
|
||||
# Convert tools to dict format
|
||||
tools_list = []
|
||||
for tool in response.tools:
|
||||
tools_list.append({
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"inputSchema": tool.inputSchema,
|
||||
})
|
||||
|
||||
return tools_list
|
||||
|
||||
def _list_tools_http(self) -> list[dict]:
|
||||
"""List tools via HTTP protocol."""
|
||||
if not self._http_client:
|
||||
raise RuntimeError("HTTP client not initialized")
|
||||
|
||||
try:
|
||||
# Use MCP over HTTP protocol
|
||||
response = self._http_client.post(
|
||||
"/mcp/v1",
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if "error" in data:
|
||||
raise RuntimeError(f"MCP error: {data['error']}")
|
||||
|
||||
return data.get("result", {}).get("tools", [])
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to list tools via HTTP: {e}")
|
||||
|
||||
def list_tools(self) -> list[MCPTool]:
|
||||
"""
|
||||
Get list of available tools.
|
||||
|
||||
Returns:
|
||||
List of MCPTool objects
|
||||
"""
|
||||
if not self._connected:
|
||||
self.connect()
|
||||
|
||||
return list(self._tools.values())
|
||||
|
||||
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
||||
"""
|
||||
Invoke a tool on the MCP server.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to invoke
|
||||
arguments: Tool arguments
|
||||
|
||||
Returns:
|
||||
Tool result
|
||||
"""
|
||||
if not self._connected:
|
||||
self.connect()
|
||||
|
||||
if tool_name not in self._tools:
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
if self.config.transport == "stdio":
|
||||
return self._run_async(self._call_tool_stdio_async(tool_name, arguments))
|
||||
else:
|
||||
return self._call_tool_http(tool_name, arguments)
|
||||
|
||||
async def _call_tool_stdio_async(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
||||
"""Call tool via STDIO protocol using MCP SDK."""
|
||||
from mcp import ClientSession
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
async with stdio_client(self._server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# Initialize the session
|
||||
await session.initialize()
|
||||
|
||||
# Call tool
|
||||
result = await session.call_tool(tool_name, arguments=arguments)
|
||||
|
||||
# Extract content
|
||||
if result.content:
|
||||
# MCP returns content as a list of content items
|
||||
if len(result.content) > 0:
|
||||
content_item = result.content[0]
|
||||
# Check if it's a text content item
|
||||
if hasattr(content_item, 'text'):
|
||||
return content_item.text
|
||||
elif hasattr(content_item, 'data'):
|
||||
return content_item.data
|
||||
return result.content
|
||||
|
||||
return None
|
||||
|
||||
def _call_tool_http(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
||||
"""Call tool via HTTP protocol."""
|
||||
if not self._http_client:
|
||||
raise RuntimeError("HTTP client not initialized")
|
||||
|
||||
try:
|
||||
response = self._http_client.post(
|
||||
"/mcp/v1",
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if "error" in data:
|
||||
raise RuntimeError(f"Tool execution error: {data['error']}")
|
||||
|
||||
return data.get("result", {}).get("content", [])
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to call tool via HTTP: {e}")
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from the MCP server."""
|
||||
if self._http_client:
|
||||
self._http_client.close()
|
||||
self._http_client = None
|
||||
|
||||
self._connected = False
|
||||
logger.info(f"Disconnected from MCP server '{self.config.name}'")
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit."""
|
||||
self.disconnect()
|
||||
@@ -210,6 +210,11 @@ class AgentRunner:
|
||||
if tools_path.exists():
|
||||
self._tool_registry.discover_from_module(tools_path)
|
||||
|
||||
# Auto-discover MCP servers from mcp_servers.json
|
||||
mcp_config_path = agent_path / "mcp_servers.json"
|
||||
if mcp_config_path.exists():
|
||||
self._load_mcp_servers_from_config(mcp_config_path)
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
@@ -283,6 +288,67 @@ class AgentRunner:
|
||||
"""
|
||||
return self._tool_registry.discover_from_module(module_path)
|
||||
|
||||
def register_mcp_server(
|
||||
self,
|
||||
name: str,
|
||||
transport: str,
|
||||
**config_kwargs,
|
||||
) -> int:
|
||||
"""
|
||||
Register an MCP server and discover its tools.
|
||||
|
||||
Args:
|
||||
name: Server name
|
||||
transport: "stdio" or "http"
|
||||
**config_kwargs: Additional configuration (command, args, url, etc.)
|
||||
|
||||
Returns:
|
||||
Number of tools registered from this server
|
||||
|
||||
Example:
|
||||
# Register STDIO MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="/path/to/aden-tools"
|
||||
)
|
||||
|
||||
# Register HTTP MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
transport="http",
|
||||
url="http://localhost:4001"
|
||||
)
|
||||
"""
|
||||
server_config = {
|
||||
"name": name,
|
||||
"transport": transport,
|
||||
**config_kwargs,
|
||||
}
|
||||
return self._tool_registry.register_mcp_server(server_config)
|
||||
|
||||
def _load_mcp_servers_from_config(self, config_path: Path) -> None:
|
||||
"""
|
||||
Load and register MCP servers from a configuration file.
|
||||
|
||||
Args:
|
||||
config_path: Path to mcp_servers.json file
|
||||
"""
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
servers = config.get("servers", [])
|
||||
for server_config in servers:
|
||||
try:
|
||||
self._tool_registry.register_mcp_server(server_config)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to register MCP server '{server_config.get('name', 'unknown')}': {e}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load MCP servers config from {config_path}: {e}")
|
||||
|
||||
def set_approval_callback(self, callback: Callable) -> None:
|
||||
"""
|
||||
Set a callback for human-in-the-loop approval during execution.
|
||||
@@ -631,6 +697,9 @@ Respond with JSON only:
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up resources."""
|
||||
# Clean up MCP client connections
|
||||
self._tool_registry.cleanup()
|
||||
|
||||
if self._temp_dir:
|
||||
self._temp_dir.cleanup()
|
||||
self._temp_dir = None
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import importlib.util
|
||||
import inspect
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from framework.llm.provider import Tool, ToolUse, ToolResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegisteredTool:
|
||||
@@ -25,11 +28,13 @@ class ToolRegistry:
|
||||
Tool Discovery Order:
|
||||
1. Built-in tools (if any)
|
||||
2. tools.py in agent folder
|
||||
3. Manually registered tools
|
||||
3. MCP servers
|
||||
4. Manually registered tools
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: dict[str, RegisteredTool] = {}
|
||||
self._mcp_clients: list[Any] = [] # List of MCPClient instances
|
||||
|
||||
def register(
|
||||
self,
|
||||
@@ -222,6 +227,129 @@ class ToolRegistry:
|
||||
"""Check if a tool is registered."""
|
||||
return name in self._tools
|
||||
|
||||
def register_mcp_server(
|
||||
self,
|
||||
server_config: dict[str, Any],
|
||||
) -> int:
|
||||
"""
|
||||
Register an MCP server and discover its tools.
|
||||
|
||||
Args:
|
||||
server_config: MCP server configuration dict with keys:
|
||||
- name: Server name (required)
|
||||
- transport: "stdio" or "http" (required)
|
||||
- command: Command to run (for stdio)
|
||||
- args: Command arguments (for stdio)
|
||||
- env: Environment variables (for stdio)
|
||||
- cwd: Working directory (for stdio)
|
||||
- url: Server URL (for http)
|
||||
- headers: HTTP headers (for http)
|
||||
- description: Server description (optional)
|
||||
|
||||
Returns:
|
||||
Number of tools registered from this server
|
||||
"""
|
||||
try:
|
||||
from framework.runner.mcp_client import MCPClient, MCPServerConfig
|
||||
|
||||
# Build config object
|
||||
config = MCPServerConfig(
|
||||
name=server_config["name"],
|
||||
transport=server_config["transport"],
|
||||
command=server_config.get("command"),
|
||||
args=server_config.get("args", []),
|
||||
env=server_config.get("env", {}),
|
||||
cwd=server_config.get("cwd"),
|
||||
url=server_config.get("url"),
|
||||
headers=server_config.get("headers", {}),
|
||||
description=server_config.get("description", ""),
|
||||
)
|
||||
|
||||
# Create and connect client
|
||||
client = MCPClient(config)
|
||||
client.connect()
|
||||
|
||||
# Store client for cleanup
|
||||
self._mcp_clients.append(client)
|
||||
|
||||
# Register each tool
|
||||
count = 0
|
||||
for mcp_tool in client.list_tools():
|
||||
# Convert MCP tool to framework Tool
|
||||
tool = self._convert_mcp_tool_to_framework_tool(mcp_tool)
|
||||
|
||||
# Create executor that calls the MCP server
|
||||
def make_mcp_executor(client_ref: MCPClient, tool_name: str):
|
||||
def executor(inputs: dict) -> Any:
|
||||
try:
|
||||
result = client_ref.call_tool(tool_name, inputs)
|
||||
# MCP tools return content array, extract the result
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
if isinstance(result[0], dict) and "text" in result[0]:
|
||||
return result[0]["text"]
|
||||
return result[0]
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"MCP tool '{tool_name}' execution failed: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
return executor
|
||||
|
||||
self.register(
|
||||
mcp_tool.name,
|
||||
tool,
|
||||
make_mcp_executor(client, mcp_tool.name),
|
||||
)
|
||||
count += 1
|
||||
|
||||
logger.info(f"Registered {count} tools from MCP server '{config.name}'")
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register MCP server: {e}")
|
||||
return 0
|
||||
|
||||
def _convert_mcp_tool_to_framework_tool(self, mcp_tool: Any) -> Tool:
|
||||
"""
|
||||
Convert an MCP tool to a framework Tool.
|
||||
|
||||
Args:
|
||||
mcp_tool: MCPTool object
|
||||
|
||||
Returns:
|
||||
Framework Tool object
|
||||
"""
|
||||
# Extract parameters from MCP input schema
|
||||
input_schema = mcp_tool.input_schema
|
||||
properties = input_schema.get("properties", {})
|
||||
required = input_schema.get("required", [])
|
||||
|
||||
# Convert to framework Tool format
|
||||
tool = Tool(
|
||||
name=mcp_tool.name,
|
||||
description=mcp_tool.description,
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
},
|
||||
)
|
||||
|
||||
return tool
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up all MCP client connections."""
|
||||
for client in self._mcp_clients:
|
||||
try:
|
||||
client.disconnect()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error disconnecting MCP client: {e}")
|
||||
self._mcp_clients.clear()
|
||||
|
||||
def __del__(self):
|
||||
"""Destructor to ensure cleanup."""
|
||||
self.cleanup()
|
||||
|
||||
|
||||
def tool(
|
||||
description: str | None = None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Core dependencies
|
||||
pydantic>=2.0
|
||||
anthropic>=0.40.0
|
||||
httpx>=0.27.0
|
||||
|
||||
# MCP server dependencies
|
||||
mcp
|
||||
|
||||
Reference in New Issue
Block a user