traia-iatp 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +30 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +161 -0
- traia_iatp/mcp/__init__.py +15 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/mcp_agent_template.py +422 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +56 -0
- traia_iatp/mcp/templates/README.md.j2 +212 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +326 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +26 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +94 -0
- traia_iatp/mcp/templates/server.py.j2 +240 -0
- traia_iatp/mcp/traia_mcp_adapter.py +381 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +839 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +215 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +450 -0
- traia_iatp/server/templates/agent.py.j2 +80 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +264 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +67 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +76 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +190 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.1.dist-info/METADATA +414 -0
- traia_iatp-0.1.1.dist-info/RECORD +72 -0
- traia_iatp-0.1.1.dist-info/WHEEL +5 -0
- traia_iatp-0.1.1.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.1.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.1.dist-info/top_level.txt +1 -0
traia_iatp/mcp/client.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""MCP client wrapper for connecting to MCP servers with streamable-http support."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Optional, List, AsyncIterator
|
|
6
|
+
import httpx
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from ..core.models import MCPServer, MCPServerType
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPClient:
|
|
15
|
+
"""Wrapper for MCP client connections with streamable-http support."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, mcp_server: MCPServer):
|
|
18
|
+
self.mcp_server = mcp_server
|
|
19
|
+
self._available_tools: List[Dict[str, Any]] = []
|
|
20
|
+
self._http_client: Optional[httpx.AsyncClient] = None
|
|
21
|
+
self._connected = False
|
|
22
|
+
|
|
23
|
+
async def connect(self) -> None:
|
|
24
|
+
"""Connect to the MCP server using streamable-http."""
|
|
25
|
+
try:
|
|
26
|
+
if self.mcp_server.server_type != MCPServerType.STREAMABLE_HTTP:
|
|
27
|
+
raise ValueError(f"Only streamable-http is supported, got: {self.mcp_server.server_type}")
|
|
28
|
+
|
|
29
|
+
await self._connect_streamable_http()
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Failed to connect to MCP server {self.mcp_server.name}: {e}")
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
async def _connect_streamable_http(self) -> None:
|
|
36
|
+
"""Connect using streamable-http for real-time updates."""
|
|
37
|
+
url = str(self.mcp_server.url)
|
|
38
|
+
|
|
39
|
+
logger.info(f"Connecting to MCP server {self.mcp_server.name} via streamable-http at {url}")
|
|
40
|
+
|
|
41
|
+
# Initialize HTTP client for persistent connection
|
|
42
|
+
self._http_client = httpx.AsyncClient(
|
|
43
|
+
timeout=httpx.Timeout(30.0, connect=10.0),
|
|
44
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
45
|
+
http2=True # Enable HTTP/2 for better streaming support
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Test connection and get available tools
|
|
49
|
+
try:
|
|
50
|
+
response = await self._http_client.get(f"{url}/tools")
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
tools_data = response.json()
|
|
53
|
+
self._available_tools = tools_data.get("tools", [])
|
|
54
|
+
self._connected = True
|
|
55
|
+
logger.info(f"Connected to {self.mcp_server.name}, found {len(self._available_tools)} tools")
|
|
56
|
+
|
|
57
|
+
# Update capabilities in the MCP server model
|
|
58
|
+
self.mcp_server.capabilities = [tool["name"] for tool in self._available_tools]
|
|
59
|
+
except Exception as e:
|
|
60
|
+
await self._http_client.aclose()
|
|
61
|
+
self._http_client = None
|
|
62
|
+
raise RuntimeError(f"Failed to connect to MCP server: {e}")
|
|
63
|
+
|
|
64
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
65
|
+
"""Call a tool on the MCP server."""
|
|
66
|
+
if not self._connected or not self._http_client:
|
|
67
|
+
# Reconnect if needed
|
|
68
|
+
await self.connect()
|
|
69
|
+
|
|
70
|
+
# Find the tool
|
|
71
|
+
tool = next((t for t in self._available_tools if t["name"] == tool_name), None)
|
|
72
|
+
if not tool:
|
|
73
|
+
raise ValueError(f"Tool {tool_name} not found")
|
|
74
|
+
|
|
75
|
+
# Call the tool
|
|
76
|
+
url = str(self.mcp_server.url).rstrip('/') + '/call'
|
|
77
|
+
|
|
78
|
+
response = await self._http_client.post(
|
|
79
|
+
url,
|
|
80
|
+
json={"name": tool_name, "input": arguments}
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return response.json()
|
|
84
|
+
|
|
85
|
+
async def call_tool_streaming(self, tool_name: str, arguments: Dict[str, Any]) -> AsyncIterator[Any]:
|
|
86
|
+
"""Call a tool with streaming response support."""
|
|
87
|
+
if not self._connected:
|
|
88
|
+
await self.connect()
|
|
89
|
+
|
|
90
|
+
# Stream the tool call response
|
|
91
|
+
async for chunk in self._stream_tool_call(tool_name, arguments):
|
|
92
|
+
yield chunk
|
|
93
|
+
|
|
94
|
+
async def _stream_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> AsyncIterator[Any]:
|
|
95
|
+
"""Stream a tool call response for streamable-http connections."""
|
|
96
|
+
if not self._http_client:
|
|
97
|
+
raise RuntimeError("HTTP client not initialized")
|
|
98
|
+
|
|
99
|
+
url = str(self.mcp_server.url).rstrip('/') + '/call'
|
|
100
|
+
|
|
101
|
+
# Make streaming request
|
|
102
|
+
async with self._http_client.stream(
|
|
103
|
+
"POST",
|
|
104
|
+
url,
|
|
105
|
+
json={"name": tool_name, "input": arguments},
|
|
106
|
+
headers={"Accept": "text/event-stream"}
|
|
107
|
+
) as response:
|
|
108
|
+
async for line in response.aiter_lines():
|
|
109
|
+
if line.startswith("data: "):
|
|
110
|
+
data = line[6:] # Remove "data: " prefix
|
|
111
|
+
if data:
|
|
112
|
+
try:
|
|
113
|
+
yield json.loads(data)
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
logger.warning(f"Failed to parse streaming data: {data}")
|
|
116
|
+
|
|
117
|
+
async def list_tools(self) -> List[Dict[str, Any]]:
|
|
118
|
+
"""List available tools."""
|
|
119
|
+
if not self._connected:
|
|
120
|
+
await self.connect()
|
|
121
|
+
|
|
122
|
+
return self._available_tools
|
|
123
|
+
|
|
124
|
+
async def disconnect(self) -> None:
|
|
125
|
+
"""Disconnect from the MCP server."""
|
|
126
|
+
self._connected = False
|
|
127
|
+
self._available_tools = []
|
|
128
|
+
|
|
129
|
+
if self._http_client:
|
|
130
|
+
await self._http_client.aclose()
|
|
131
|
+
self._http_client = None
|
|
132
|
+
|
|
133
|
+
async def health_check(self) -> bool:
|
|
134
|
+
"""Check if the MCP server connection is healthy."""
|
|
135
|
+
try:
|
|
136
|
+
if not self._connected or not self._http_client:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# Try to ping the server
|
|
140
|
+
response = await self._http_client.get(f"{self.mcp_server.url}/health")
|
|
141
|
+
return response.status_code == 200
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning(f"Health check failed for {self.mcp_server.name}: {e}")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
async def __aenter__(self):
|
|
147
|
+
"""Async context manager entry."""
|
|
148
|
+
await self.connect()
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
152
|
+
"""Async context manager exit."""
|
|
153
|
+
await self.disconnect()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class MCPToolWrapper:
|
|
157
|
+
"""Wrapper to expose MCP tools as CrewAI-compatible tools with connection pooling."""
|
|
158
|
+
|
|
159
|
+
# Class-level connection pool
|
|
160
|
+
_connection_pool: Dict[str, MCPClient] = {}
|
|
161
|
+
_pool_lock = asyncio.Lock()
|
|
162
|
+
|
|
163
|
+
def __init__(self, mcp_server: MCPServer, tool_name: str, tool_description: str):
|
|
164
|
+
self.mcp_server = mcp_server
|
|
165
|
+
self.tool_name = tool_name
|
|
166
|
+
self.description = tool_description
|
|
167
|
+
self.name = tool_name
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
async def get_or_create_client(cls, mcp_server: MCPServer) -> MCPClient:
|
|
171
|
+
"""Get or create a client from the connection pool."""
|
|
172
|
+
server_key = f"{mcp_server.name}:{mcp_server.url}"
|
|
173
|
+
|
|
174
|
+
async with cls._pool_lock:
|
|
175
|
+
if server_key not in cls._connection_pool:
|
|
176
|
+
# Create new client
|
|
177
|
+
client = MCPClient(mcp_server)
|
|
178
|
+
await client.connect()
|
|
179
|
+
cls._connection_pool[server_key] = client
|
|
180
|
+
else:
|
|
181
|
+
# Check if existing client is healthy
|
|
182
|
+
client = cls._connection_pool[server_key]
|
|
183
|
+
if not await client.health_check():
|
|
184
|
+
# Reconnect if unhealthy
|
|
185
|
+
await client.disconnect()
|
|
186
|
+
await client.connect()
|
|
187
|
+
|
|
188
|
+
return cls._connection_pool[server_key]
|
|
189
|
+
|
|
190
|
+
async def __call__(self, **kwargs) -> Any:
|
|
191
|
+
"""Execute the MCP tool using pooled connection."""
|
|
192
|
+
client = await self.get_or_create_client(self.mcp_server)
|
|
193
|
+
return await client.call_tool(self.tool_name, kwargs)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
async def cleanup_pool(cls):
|
|
197
|
+
"""Clean up all connections in the pool."""
|
|
198
|
+
async with cls._pool_lock:
|
|
199
|
+
for client in cls._connection_pool.values():
|
|
200
|
+
await client.disconnect()
|
|
201
|
+
cls._connection_pool.clear()
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
MCP Agent Template
|
|
4
|
+
|
|
5
|
+
This template provides a structured way to create agents that use MCP server tools.
|
|
6
|
+
It offers a simplified interface for creating specialized agents that can leverage
|
|
7
|
+
any MCP server by providing the server configuration.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Automatic detection of authentication requirements
|
|
11
|
+
- Support for both authenticated and non-authenticated MCP servers
|
|
12
|
+
- Flexible agent creation with optional tool filtering
|
|
13
|
+
- Health checks for MCP servers
|
|
14
|
+
|
|
15
|
+
Authentication:
|
|
16
|
+
The template automatically detects if an MCP server requires authentication
|
|
17
|
+
based on metadata fields:
|
|
18
|
+
- requires_api_key: boolean indicating if authentication is needed
|
|
19
|
+
- api_key_header: the header name to use (default: "Authorization")
|
|
20
|
+
- headers: dictionary containing the actual API key
|
|
21
|
+
|
|
22
|
+
Usage for MCP Agents:
|
|
23
|
+
1. Import the MCPAgentBuilder from this module
|
|
24
|
+
2. Create your specialized agent(s) with the builder
|
|
25
|
+
3. Create tasks for your agents
|
|
26
|
+
4. Run your agents as a CrewAI crew with MCP server configuration
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```python
|
|
30
|
+
from mcp_agent_template import MCPAgentBuilder, run_with_mcp_tools, MCPServerInfo
|
|
31
|
+
|
|
32
|
+
# Create MCP server info (you would get this from registry or configuration)
|
|
33
|
+
mcp_server = MCPServerInfo(
|
|
34
|
+
id="weather-123",
|
|
35
|
+
name="weather-mcp",
|
|
36
|
+
url="http://localhost:8080",
|
|
37
|
+
description="Weather information MCP server",
|
|
38
|
+
server_type="streamable-http",
|
|
39
|
+
capabilities=["get_weather", "get_forecast"],
|
|
40
|
+
metadata={},
|
|
41
|
+
tags=["weather", "api"]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# For authenticated servers, include auth info in metadata:
|
|
45
|
+
# metadata={
|
|
46
|
+
# "requires_api_key": True,
|
|
47
|
+
# "api_key_header": "Authorization",
|
|
48
|
+
# "headers": {"Authorization": "Bearer YOUR_API_KEY"}
|
|
49
|
+
# }
|
|
50
|
+
|
|
51
|
+
# Create an agent for the MCP server
|
|
52
|
+
analyst = MCPAgentBuilder.create_agent(
|
|
53
|
+
role="Weather Analyst",
|
|
54
|
+
goal="Analyze weather conditions and provide forecasts",
|
|
55
|
+
backstory="You are an expert meteorologist...",
|
|
56
|
+
verbose=True
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create task for the agent
|
|
60
|
+
task = Task(
|
|
61
|
+
description="Analyze current weather conditions in New York...",
|
|
62
|
+
expected_output="A comprehensive weather report...",
|
|
63
|
+
agent=analyst
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Run the agent with MCP tools
|
|
67
|
+
result = run_with_mcp_tools([task], mcp_server=mcp_server)
|
|
68
|
+
print(result)
|
|
69
|
+
```
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
import os
|
|
73
|
+
import sys
|
|
74
|
+
import json
|
|
75
|
+
import argparse
|
|
76
|
+
import logging
|
|
77
|
+
from typing import List, Dict, Any, Optional, Union
|
|
78
|
+
from dataclasses import dataclass
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
# Import CrewAI components
|
|
82
|
+
from crewai import Agent, Task, Crew, Process, LLM
|
|
83
|
+
from crewai_tools import MCPServerAdapter
|
|
84
|
+
|
|
85
|
+
# Import our custom adapter for API key support
|
|
86
|
+
from .traia_mcp_adapter import create_mcp_adapter, create_mcp_adapter_with_auth
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
# Create default LLM instance
|
|
92
|
+
DEFAULT_LLM = LLM(model="openai/gpt-4.1", temperature=0.7)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class MCPServerInfo:
|
|
97
|
+
"""Information about an MCP server."""
|
|
98
|
+
id: str
|
|
99
|
+
name: str
|
|
100
|
+
url: str
|
|
101
|
+
description: str
|
|
102
|
+
server_type: str
|
|
103
|
+
capabilities: List[str]
|
|
104
|
+
metadata: Dict[str, Any]
|
|
105
|
+
tags: List[str]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MCPServerConfig:
|
|
109
|
+
"""Configuration for an MCP server - used for utility agency creation."""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
name: str,
|
|
114
|
+
url: str,
|
|
115
|
+
description: str,
|
|
116
|
+
server_type: str = "streamable-http", # Only streamable-http is supported
|
|
117
|
+
capabilities: List[str] = None,
|
|
118
|
+
metadata: Dict[str, Any] = None
|
|
119
|
+
):
|
|
120
|
+
self.name = name
|
|
121
|
+
self.url = url
|
|
122
|
+
self.description = description
|
|
123
|
+
self.server_type = server_type
|
|
124
|
+
self.capabilities = capabilities or []
|
|
125
|
+
self.metadata = metadata or {}
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
128
|
+
"""Convert to dictionary for storage."""
|
|
129
|
+
return {
|
|
130
|
+
"name": self.name,
|
|
131
|
+
"url": self.url,
|
|
132
|
+
"description": self.description,
|
|
133
|
+
"server_type": self.server_type,
|
|
134
|
+
"capabilities": self.capabilities,
|
|
135
|
+
"metadata": self.metadata
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class MCPAgentBuilder:
|
|
140
|
+
"""
|
|
141
|
+
Builder class for creating agents that use MCP server tools.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Class variable to store tool subsets for agents (using agent id as key)
|
|
145
|
+
_agent_tool_subsets = {}
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def create_agent(
|
|
149
|
+
role: str,
|
|
150
|
+
goal: str,
|
|
151
|
+
backstory: str,
|
|
152
|
+
verbose: bool = True,
|
|
153
|
+
allow_delegation: bool = False,
|
|
154
|
+
llm: LLM = None,
|
|
155
|
+
tools_subset: List[str] = None
|
|
156
|
+
) -> Agent:
|
|
157
|
+
"""
|
|
158
|
+
Create a CrewAI agent for use with MCP tools.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
role: The role of the agent
|
|
162
|
+
goal: The primary goal of the agent
|
|
163
|
+
backstory: Background story for the agent
|
|
164
|
+
verbose: Whether to enable verbose output
|
|
165
|
+
allow_delegation: Whether to allow the agent to delegate tasks
|
|
166
|
+
llm: The LLM instance to use (defaults to gpt-4.1 with temperature 0.7)
|
|
167
|
+
tools_subset: Optional list of specific tool names to include (if None, all tools are included)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
CrewAI Agent configured for MCP tools
|
|
171
|
+
"""
|
|
172
|
+
# Use specified LLM or default
|
|
173
|
+
if llm is None:
|
|
174
|
+
llm = DEFAULT_LLM
|
|
175
|
+
|
|
176
|
+
# We'll set tools later when run_with_mcp_tools is called
|
|
177
|
+
# This is because tools require the MCP server connection
|
|
178
|
+
agent = Agent(
|
|
179
|
+
role=role,
|
|
180
|
+
goal=goal,
|
|
181
|
+
backstory=backstory,
|
|
182
|
+
verbose=verbose,
|
|
183
|
+
allow_delegation=allow_delegation,
|
|
184
|
+
llm=llm
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Store the tools_subset in the class dictionary
|
|
188
|
+
if tools_subset is not None:
|
|
189
|
+
MCPAgentBuilder._agent_tool_subsets[id(agent)] = tools_subset
|
|
190
|
+
|
|
191
|
+
return agent
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def get_tools_subset(agent: Agent) -> Optional[List[str]]:
|
|
195
|
+
"""Get the tools subset for a specific agent"""
|
|
196
|
+
return MCPAgentBuilder._agent_tool_subsets.get(id(agent))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def check_server_health(server_info: MCPServerInfo, api_key: Optional[str] = None) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Check if the MCP server is running and healthy by attempting to connect
|
|
202
|
+
and list available tools.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
server_info: MCPServerInfo object with server details
|
|
206
|
+
api_key: Optional API key for authenticated servers
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if the server is healthy, False otherwise
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# Check if authentication is required
|
|
213
|
+
requires_api_key = server_info.metadata.get("requires_api_key", False)
|
|
214
|
+
api_key_header = server_info.metadata.get("api_key_header", "Authorization")
|
|
215
|
+
|
|
216
|
+
# Create appropriate adapter
|
|
217
|
+
if requires_api_key and api_key:
|
|
218
|
+
# Use the provided API key directly (user provides raw key without Bearer prefix)
|
|
219
|
+
adapter = create_mcp_adapter_with_auth(
|
|
220
|
+
url=server_info.url,
|
|
221
|
+
api_key=api_key,
|
|
222
|
+
auth_header=api_key_header,
|
|
223
|
+
auth_prefix="Bearer" # We add the Bearer prefix
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
# No authentication required or no API key provided
|
|
227
|
+
adapter = create_mcp_adapter(url=server_info.url)
|
|
228
|
+
|
|
229
|
+
# Try to connect and list tools
|
|
230
|
+
with adapter as mcp_tools:
|
|
231
|
+
tools = list(mcp_tools)
|
|
232
|
+
print(f"✓ MCP server '{server_info.name}' is healthy ({len(tools)} tools available)")
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print(f"✗ MCP server '{server_info.name}' health check failed: {e}")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run_with_mcp_tools(
|
|
241
|
+
tasks: List[Task],
|
|
242
|
+
mcp_server: MCPServerInfo,
|
|
243
|
+
agents: Optional[List[Agent]] = None,
|
|
244
|
+
process: Process = Process.sequential,
|
|
245
|
+
verbose: bool = True,
|
|
246
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
247
|
+
skip_health_check: bool = False,
|
|
248
|
+
api_key: Optional[str] = None
|
|
249
|
+
) -> Any:
|
|
250
|
+
"""
|
|
251
|
+
Run tasks with agents that have access to MCP server tools.
|
|
252
|
+
|
|
253
|
+
NOTE ON AUTHENTICATION:
|
|
254
|
+
This function automatically detects if the MCP server requires authentication
|
|
255
|
+
based on the server's metadata. If authentication is required, you must provide
|
|
256
|
+
your API key using the api_key parameter.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
tasks: List of tasks to run
|
|
260
|
+
mcp_server: MCPServerInfo object with server details
|
|
261
|
+
agents: Optional list of agents (if None, will use agents from tasks)
|
|
262
|
+
process: CrewAI process type (sequential or hierarchical)
|
|
263
|
+
verbose: Whether to enable verbose output
|
|
264
|
+
inputs: Optional inputs for the crew
|
|
265
|
+
skip_health_check: Skip server health check
|
|
266
|
+
api_key: Optional API key for authenticated MCP servers
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Result from the crew execution
|
|
270
|
+
"""
|
|
271
|
+
# Check if the server is healthy (unless skipped)
|
|
272
|
+
if not skip_health_check:
|
|
273
|
+
# Pass the API key for health check if authentication is required
|
|
274
|
+
if not check_server_health(mcp_server, api_key):
|
|
275
|
+
print(f"MCP server '{mcp_server.name}' is not healthy.")
|
|
276
|
+
print(f"Server URL: {mcp_server.url}")
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
|
|
279
|
+
# Check if authentication is required
|
|
280
|
+
requires_api_key = mcp_server.metadata.get("requires_api_key", False)
|
|
281
|
+
api_key_header = mcp_server.metadata.get("api_key_header", "Authorization")
|
|
282
|
+
|
|
283
|
+
# Create appropriate adapter
|
|
284
|
+
if requires_api_key:
|
|
285
|
+
if not api_key:
|
|
286
|
+
print(f"\n⚠️ WARNING: MCP server '{mcp_server.name}' requires authentication")
|
|
287
|
+
print(f"Expected header: {api_key_header}")
|
|
288
|
+
print("But no API key was provided.")
|
|
289
|
+
print("\nTo provide authentication:")
|
|
290
|
+
print("Pass your API key using the 'api_key' parameter")
|
|
291
|
+
print("Example: run_with_mcp_tools(tasks, mcp_server, api_key='YOUR_API_KEY')")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
# Use the provided API key directly (user provides raw key without Bearer prefix)
|
|
295
|
+
adapter = create_mcp_adapter_with_auth(
|
|
296
|
+
url=mcp_server.url,
|
|
297
|
+
api_key=api_key,
|
|
298
|
+
auth_header=api_key_header,
|
|
299
|
+
auth_prefix="Bearer" # We add the Bearer prefix
|
|
300
|
+
)
|
|
301
|
+
print(f"\n🔐 Using authenticated connection (header: {api_key_header})")
|
|
302
|
+
else:
|
|
303
|
+
# No authentication required
|
|
304
|
+
adapter = create_mcp_adapter(url=mcp_server.url)
|
|
305
|
+
print("\n🔓 Using standard connection (no authentication)")
|
|
306
|
+
|
|
307
|
+
# Get agents from tasks if not provided
|
|
308
|
+
if agents is None:
|
|
309
|
+
agents = [task.agent for task in tasks]
|
|
310
|
+
# Remove duplicates while preserving order
|
|
311
|
+
seen = set()
|
|
312
|
+
agents = [agent for agent in agents if not (agent in seen or seen.add(agent))]
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
# Use context manager for MCP server connection
|
|
316
|
+
with adapter as all_tools:
|
|
317
|
+
print(f"Connected to MCP server '{mcp_server.name}'")
|
|
318
|
+
print(f"Available tools: {[tool.name for tool in all_tools]}")
|
|
319
|
+
|
|
320
|
+
# Assign tools to each agent based on their tools_subset if defined
|
|
321
|
+
for agent in agents:
|
|
322
|
+
# Get the tools subset from the class dictionary
|
|
323
|
+
tools_subset = MCPAgentBuilder.get_tools_subset(agent)
|
|
324
|
+
if tools_subset:
|
|
325
|
+
# Filter tools by name if a subset is specified
|
|
326
|
+
agent.tools = [tool for tool in all_tools if tool.name in tools_subset]
|
|
327
|
+
print(f"Agent '{agent.role}' assigned tools: {[tool.name for tool in agent.tools]}")
|
|
328
|
+
else:
|
|
329
|
+
# Use all tools if no subset is specified
|
|
330
|
+
agent.tools = all_tools
|
|
331
|
+
print(f"Agent '{agent.role}' assigned all available tools")
|
|
332
|
+
|
|
333
|
+
# Create and run the crew
|
|
334
|
+
crew = Crew(
|
|
335
|
+
agents=agents,
|
|
336
|
+
tasks=tasks,
|
|
337
|
+
verbose=verbose,
|
|
338
|
+
process=process
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Kickoff the crew with inputs
|
|
342
|
+
result = crew.kickoff(inputs=inputs or {})
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"Error during execution: {e}")
|
|
347
|
+
import traceback
|
|
348
|
+
traceback.print_exc()
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Example usage when running this file directly
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
# This example shows how to use the template without registry
|
|
355
|
+
print("MCP Agent Template Example")
|
|
356
|
+
print("=" * 80)
|
|
357
|
+
|
|
358
|
+
# Example MCP server configuration (would normally come from registry or config)
|
|
359
|
+
example_server = MCPServerInfo(
|
|
360
|
+
id="example-123",
|
|
361
|
+
name="example-mcp",
|
|
362
|
+
url="http://localhost:8080/mcp/", # Add trailing slash
|
|
363
|
+
description="Example MCP server for demonstration",
|
|
364
|
+
server_type="streamable-http",
|
|
365
|
+
capabilities=["example_tool1", "example_tool2"],
|
|
366
|
+
metadata={},
|
|
367
|
+
tags=["example", "demo"]
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Example of MCP server that requires authentication:
|
|
371
|
+
# authenticated_server = MCPServerInfo(
|
|
372
|
+
# id="news-456",
|
|
373
|
+
# name="newsapi-mcp",
|
|
374
|
+
# url="http://localhost:8000/mcp/", # Add trailing slash
|
|
375
|
+
# description="NewsAPI MCP server",
|
|
376
|
+
# server_type="streamable-http",
|
|
377
|
+
# capabilities=["search_news", "get_headlines"],
|
|
378
|
+
# metadata={
|
|
379
|
+
# "requires_api_key": True,
|
|
380
|
+
# "api_key_header": "Authorization",
|
|
381
|
+
# "headers": {
|
|
382
|
+
# "Authorization": "Bearer YOUR_API_KEY" # Client API key
|
|
383
|
+
# }
|
|
384
|
+
# },
|
|
385
|
+
# tags=["news", "api"]
|
|
386
|
+
# )
|
|
387
|
+
|
|
388
|
+
print(f"Using MCP Server: {example_server.name}")
|
|
389
|
+
print(f"Description: {example_server.description}")
|
|
390
|
+
print(f"URL: {example_server.url}")
|
|
391
|
+
print(f"Capabilities: {example_server.capabilities}")
|
|
392
|
+
print()
|
|
393
|
+
|
|
394
|
+
# Create an example agent
|
|
395
|
+
analyst = MCPAgentBuilder.create_agent(
|
|
396
|
+
role="Example Analyst",
|
|
397
|
+
goal="Demonstrate the usage of MCP tools",
|
|
398
|
+
backstory="""
|
|
399
|
+
You are an expert in using MCP server tools.
|
|
400
|
+
Your job is to demonstrate how to use the available tools effectively.
|
|
401
|
+
""",
|
|
402
|
+
verbose=True
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Create a task
|
|
406
|
+
demo_task = Task(
|
|
407
|
+
description="""
|
|
408
|
+
Use the available MCP tools to perform a simple demonstration.
|
|
409
|
+
Show what the tools can do and provide a summary.
|
|
410
|
+
""",
|
|
411
|
+
expected_output="""
|
|
412
|
+
A demonstration report showing the capabilities of the MCP tools.
|
|
413
|
+
""",
|
|
414
|
+
agent=analyst
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
print("Note: This is a template example. To run actual MCP tools:")
|
|
418
|
+
print("1. Ensure an MCP server is running at the specified URL")
|
|
419
|
+
print("2. Get the server configuration from the registry or config")
|
|
420
|
+
print("3. Create agents and tasks specific to your use case")
|
|
421
|
+
print("4. Run with: run_with_mcp_tools([task], mcp_server)")
|
|
422
|
+
print("=" * 80)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Use Python 3.12 slim image as base
|
|
2
|
+
FROM python:3.12-slim
|
|
3
|
+
|
|
4
|
+
# Install system dependencies
|
|
5
|
+
RUN apt-get update && apt-get install -y \
|
|
6
|
+
curl \
|
|
7
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
8
|
+
|
|
9
|
+
# Install uv
|
|
10
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
11
|
+
RUN chmod +x /usr/local/bin/uv
|
|
12
|
+
|
|
13
|
+
# Create non-root user
|
|
14
|
+
RUN useradd -m -u 1000 appuser
|
|
15
|
+
|
|
16
|
+
# Create app directory and set ownership
|
|
17
|
+
RUN mkdir -p /app && chown -R appuser:appuser /app
|
|
18
|
+
|
|
19
|
+
# Set working directory
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
# Switch to non-root user
|
|
23
|
+
USER appuser
|
|
24
|
+
|
|
25
|
+
# Copy project files
|
|
26
|
+
COPY --chown=appuser:appuser . .
|
|
27
|
+
|
|
28
|
+
# Install Python dependencies
|
|
29
|
+
RUN uv venv .venv && \
|
|
30
|
+
uv pip install -r pyproject.toml
|
|
31
|
+
|
|
32
|
+
# Set environment variables
|
|
33
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
34
|
+
ENV PYTHONPATH=/app
|
|
35
|
+
ENV HOST=0.0.0.0
|
|
36
|
+
ENV PYTHONUNBUFFERED=1
|
|
37
|
+
ENV UV_SYSTEM_PYTHON=1
|
|
38
|
+
ENV STAGE=MAINNET
|
|
39
|
+
ENV PORT=8000
|
|
40
|
+
ENV LOG_LEVEL=INFO
|
|
41
|
+
{% if environment_variables %}
|
|
42
|
+
# Additional environment variables
|
|
43
|
+
{% for env_var in environment_variables %}
|
|
44
|
+
ENV {{ env_var.name }}="{{ env_var.value }}"
|
|
45
|
+
{% endfor %}
|
|
46
|
+
{% endif %}
|
|
47
|
+
|
|
48
|
+
# Health check
|
|
49
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
50
|
+
CMD curl -f http://localhost:${PORT:-8000}/health || exit 1
|
|
51
|
+
|
|
52
|
+
# Expose port (uses PORT environment variable with default)
|
|
53
|
+
EXPOSE ${PORT:-8000}
|
|
54
|
+
|
|
55
|
+
# Run the application
|
|
56
|
+
CMD ["uv", "run", "python", "server.py"]
|