agent-mcp 0.1.1__py3-none-any.whl → 0.1.3__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.
- agent_mcp/__init__.py +16 -0
- agent_mcp/crewai_mcp_adapter.py +281 -0
- agent_mcp/enhanced_mcp_agent.py +601 -0
- agent_mcp/heterogeneous_group_chat.py +424 -0
- agent_mcp/langchain_mcp_adapter.py +325 -0
- agent_mcp/langgraph_mcp_adapter.py +325 -0
- agent_mcp/mcp_agent.py +632 -0
- agent_mcp/mcp_decorator.py +257 -0
- agent_mcp/mcp_langgraph.py +733 -0
- agent_mcp/mcp_transaction.py +97 -0
- agent_mcp/mcp_transport.py +700 -0
- agent_mcp/mcp_transport_enhanced.py +46 -0
- agent_mcp/proxy_agent.py +24 -0
- agent_mcp-0.1.3.dist-info/METADATA +331 -0
- agent_mcp-0.1.3.dist-info/RECORD +18 -0
- agent_mcp-0.1.3.dist-info/top_level.txt +1 -0
- agent_mcp-0.1.1.dist-info/METADATA +0 -474
- agent_mcp-0.1.1.dist-info/RECORD +0 -5
- agent_mcp-0.1.1.dist-info/top_level.txt +0 -1
- {agent_mcp-0.1.1.dist-info → agent_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {agent_mcp-0.1.1.dist-info → agent_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
agent_mcp/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentMCP - Model Context Protocol for AI Agents
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .mcp_agent import MCPAgent
|
|
6
|
+
from .mcp_decorator import mcp_agent
|
|
7
|
+
from .enhanced_mcp_agent import EnhancedMCPAgent
|
|
8
|
+
from .mcp_transport import MCPTransport, HTTPTransport
|
|
9
|
+
from .heterogeneous_group_chat import HeterogeneousGroupChat
|
|
10
|
+
|
|
11
|
+
# Framework adapters
|
|
12
|
+
from .langchain_mcp_adapter import LangchainMCPAdapter
|
|
13
|
+
from .crewai_mcp_adapter import CrewAIMCPAdapter
|
|
14
|
+
from .langgraph_mcp_adapter import LangGraphMCPAdapter
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.2"
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrewAI MCP Adapter - Adapt CrewAI agents to work with MCP.
|
|
3
|
+
|
|
4
|
+
This module provides an adapter that allows CrewAI agents to work within
|
|
5
|
+
the Model Context Protocol (MCP) framework, enabling them to collaborate
|
|
6
|
+
with agents from other frameworks like Autogen and Langchain.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Dict, Any, Optional, Callable
|
|
11
|
+
from crewai import Agent as CrewAgent
|
|
12
|
+
from fastapi import FastAPI, Request
|
|
13
|
+
from .mcp_agent import MCPAgent
|
|
14
|
+
from .mcp_transport import HTTPTransport
|
|
15
|
+
import uvicorn
|
|
16
|
+
from threading import Thread
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
class CrewAIMCPAdapter(MCPAgent):
|
|
20
|
+
"""
|
|
21
|
+
Adapter for CrewAI agents to work with MCP.
|
|
22
|
+
|
|
23
|
+
This adapter wraps a CrewAI agent and makes it compatible with the MCP framework,
|
|
24
|
+
allowing it to communicate with other agents through the transport layer.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
crewai_agent: CrewAgent,
|
|
31
|
+
process_message: Optional[Callable] = None,
|
|
32
|
+
transport: Optional[HTTPTransport] = None,
|
|
33
|
+
client_mode: bool = True,
|
|
34
|
+
**kwargs
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the CrewAI MCP adapter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name: Name of the agent
|
|
41
|
+
crewai_agent: CrewAI agent to adapt
|
|
42
|
+
process_message: Optional custom message processing function
|
|
43
|
+
transport: Optional transport layer
|
|
44
|
+
client_mode: Whether to run in client mode
|
|
45
|
+
**kwargs: Additional arguments to pass to MCPAgent
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(name=name, **kwargs)
|
|
48
|
+
|
|
49
|
+
self.crewai_agent = crewai_agent
|
|
50
|
+
self.custom_process_message = process_message
|
|
51
|
+
self.transport = transport
|
|
52
|
+
self.client_mode = client_mode
|
|
53
|
+
self.task_queue = asyncio.Queue()
|
|
54
|
+
self.server_ready = asyncio.Event()
|
|
55
|
+
|
|
56
|
+
# Create FastAPI app for server mode
|
|
57
|
+
self.app = FastAPI()
|
|
58
|
+
|
|
59
|
+
@self.app.post("/message")
|
|
60
|
+
async def handle_message(request: Request):
|
|
61
|
+
return await self._handle_message(request)
|
|
62
|
+
|
|
63
|
+
@self.app.on_event("startup")
|
|
64
|
+
async def startup_event():
|
|
65
|
+
self.server_ready.set()
|
|
66
|
+
|
|
67
|
+
self.server_thread = None
|
|
68
|
+
|
|
69
|
+
async def _handle_message(self, request: Request):
|
|
70
|
+
"""Handle incoming HTTP messages"""
|
|
71
|
+
try:
|
|
72
|
+
message = await request.json()
|
|
73
|
+
await self.task_queue.put(message)
|
|
74
|
+
return {"status": "ok"}
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return {"status": "error", "message": str(e)}
|
|
77
|
+
|
|
78
|
+
async def process_messages(self):
|
|
79
|
+
"""Process incoming messages from the transport layer"""
|
|
80
|
+
while True:
|
|
81
|
+
try:
|
|
82
|
+
message, message_id = await self.transport.receive_message()
|
|
83
|
+
print(f"{self.name}: Received message {message_id}: {message}")
|
|
84
|
+
|
|
85
|
+
if message and isinstance(message, dict):
|
|
86
|
+
# Add message_id to message for tracking
|
|
87
|
+
message['message_id'] = message_id
|
|
88
|
+
|
|
89
|
+
# Standardize message structure
|
|
90
|
+
if 'content' not in message and message.get('type') == 'task':
|
|
91
|
+
message = {
|
|
92
|
+
'type': 'task',
|
|
93
|
+
'content': {
|
|
94
|
+
'task_id': message.get('task_id'),
|
|
95
|
+
'description': message.get('description'),
|
|
96
|
+
'type': 'task'
|
|
97
|
+
},
|
|
98
|
+
'message_id': message_id,
|
|
99
|
+
'from': message.get('from', 'unknown')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# --- Idempotency Check ---
|
|
103
|
+
if not super()._should_process_message(message):
|
|
104
|
+
# If skipped, acknowledge and continue
|
|
105
|
+
if message_id and self.transport:
|
|
106
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
107
|
+
print(f"[{self.name}] Acknowledged duplicate task {message.get('task_id')} (msg_id: {message_id})")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if message.get("type") == "task":
|
|
111
|
+
print(f"{self.name}: Queueing task with message_id {message_id}")
|
|
112
|
+
await self.task_queue.put(message)
|
|
113
|
+
elif self.custom_process_message:
|
|
114
|
+
await self.custom_process_message(self, message)
|
|
115
|
+
else:
|
|
116
|
+
print(f"{self.name}: Unknown message type: {message.get('type')}")
|
|
117
|
+
# Acknowledge unknown messages
|
|
118
|
+
if message_id and self.transport:
|
|
119
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
120
|
+
print(f"{self.name}: Acknowledged unknown message {message_id}")
|
|
121
|
+
except asyncio.CancelledError:
|
|
122
|
+
print(f"{self.name}: Message processor cancelled")
|
|
123
|
+
break
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"{self.name}: Error processing message: {e}")
|
|
126
|
+
traceback.print_exc()
|
|
127
|
+
await asyncio.sleep(1)
|
|
128
|
+
|
|
129
|
+
async def process_tasks(self):
|
|
130
|
+
"""Process tasks from the queue using the CrewAI agent"""
|
|
131
|
+
while True:
|
|
132
|
+
try:
|
|
133
|
+
task = await self.task_queue.get()
|
|
134
|
+
task_id = task.get('task', {}).get('task_id')
|
|
135
|
+
message_id = task.get('message_id')
|
|
136
|
+
|
|
137
|
+
print(f"\n{self.name}: Processing task {task_id} with message_id {message_id}")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Extract task details from content or root level
|
|
141
|
+
# Standardized task extraction
|
|
142
|
+
# Unified content extraction with backward compatibility
|
|
143
|
+
task_content = task.get('content', task.get('task', {}))
|
|
144
|
+
task_id = task_content.get('task_id')
|
|
145
|
+
task_description = task_content.get('description')
|
|
146
|
+
|
|
147
|
+
# Validate required fields
|
|
148
|
+
if not all([task_id, task_description]):
|
|
149
|
+
raise ValueError(f"Missing required task fields in message {message_id}")
|
|
150
|
+
message_id = task.get('message_id')
|
|
151
|
+
reply_to = task.get('reply_to')
|
|
152
|
+
|
|
153
|
+
if not task_id or not task_description:
|
|
154
|
+
print(f"[{self.name}] ERROR: Invalid task structure received: {task}")
|
|
155
|
+
# Acknowledge bad tasks
|
|
156
|
+
if message_id and self.transport:
|
|
157
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
158
|
+
print(f"[ERROR] {self.name}: Task missing required fields: {task}")
|
|
159
|
+
self.task_queue.task_done()
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
print(f"\n{self.name}: Processing task {task_id} (from msg {message_id}) Desc: {task_description}")
|
|
163
|
+
|
|
164
|
+
result = await self.execute_task(task_description)
|
|
165
|
+
|
|
166
|
+
# --- Mark task completed (Uses Base Class Method) ---
|
|
167
|
+
super()._mark_task_completed(task_id)
|
|
168
|
+
# --- End mark task completed ---
|
|
169
|
+
|
|
170
|
+
# Send result back if reply_to is specified
|
|
171
|
+
if reply_to:
|
|
172
|
+
await self.transport.send_message(
|
|
173
|
+
reply_to,
|
|
174
|
+
{
|
|
175
|
+
"type": "task_result",
|
|
176
|
+
"task_id": task_id,
|
|
177
|
+
"result": result,
|
|
178
|
+
"sender": self.name,
|
|
179
|
+
"original_message_id": message_id # Include original message ID
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
print(f"{self.name}: Result sent successfully")
|
|
183
|
+
|
|
184
|
+
# Acknowledge task completion
|
|
185
|
+
if message_id:
|
|
186
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
187
|
+
print(f"{self.name}: Task {task_id} acknowledged with message_id {message_id}")
|
|
188
|
+
else:
|
|
189
|
+
print(f"{self.name}: No message_id for task {task_id}, cannot acknowledge")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print(f"{self.name}: Error executing task: {e}")
|
|
192
|
+
traceback.print_exc()
|
|
193
|
+
|
|
194
|
+
# Send error result back if reply_to is specified
|
|
195
|
+
if reply_to:
|
|
196
|
+
await self.transport.send_message(
|
|
197
|
+
task['reply_to'],
|
|
198
|
+
{
|
|
199
|
+
"type": "task_result",
|
|
200
|
+
"task_id": task_id,
|
|
201
|
+
"result": f"Error: {str(e)}",
|
|
202
|
+
"sender": self.name,
|
|
203
|
+
"original_message_id": message_id,
|
|
204
|
+
"error": True
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.task_queue.task_done()
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f"{self.name}: Error processing task: {e}")
|
|
212
|
+
traceback.print_exc()
|
|
213
|
+
await asyncio.sleep(1)
|
|
214
|
+
|
|
215
|
+
async def execute_task(self, task_description: str) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Execute a task using the CrewAI agent.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
task_description: Description of the task to execute
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The result of the task execution
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
# Execute task using CrewAI agent
|
|
227
|
+
result = await asyncio.to_thread(
|
|
228
|
+
self.crewai_agent.execute,
|
|
229
|
+
task_description
|
|
230
|
+
)
|
|
231
|
+
return str(result)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
return f"Error executing task: {e}"
|
|
234
|
+
|
|
235
|
+
def run(self):
|
|
236
|
+
"""Start the message and task processors"""
|
|
237
|
+
if not self.transport:
|
|
238
|
+
raise ValueError(f"{self.name}: No transport configured")
|
|
239
|
+
|
|
240
|
+
# Start the transport server if not in client mode
|
|
241
|
+
if not self.client_mode:
|
|
242
|
+
def run_server():
|
|
243
|
+
config = uvicorn.Config(
|
|
244
|
+
app=self.app,
|
|
245
|
+
host=self.transport.host,
|
|
246
|
+
port=self.transport.port,
|
|
247
|
+
log_level="info"
|
|
248
|
+
)
|
|
249
|
+
server = uvicorn.Server(config)
|
|
250
|
+
server.run()
|
|
251
|
+
|
|
252
|
+
self.server_thread = Thread(target=run_server, daemon=True)
|
|
253
|
+
self.server_thread.start()
|
|
254
|
+
else:
|
|
255
|
+
# In client mode, we're ready immediately
|
|
256
|
+
self.server_ready.set()
|
|
257
|
+
|
|
258
|
+
print(f"{self.name}: Starting message processor...")
|
|
259
|
+
asyncio.create_task(self.process_messages())
|
|
260
|
+
|
|
261
|
+
print(f"{self.name}: Starting task processor...")
|
|
262
|
+
asyncio.create_task(self.process_tasks())
|
|
263
|
+
|
|
264
|
+
async def connect_to_server(self, server_url: str):
|
|
265
|
+
"""Connect to a coordinator server"""
|
|
266
|
+
if not self.client_mode:
|
|
267
|
+
raise ValueError("Agent not configured for client mode")
|
|
268
|
+
|
|
269
|
+
# Wait for server to be ready before connecting
|
|
270
|
+
if not self.server_ready.is_set():
|
|
271
|
+
await asyncio.wait_for(self.server_ready.wait(), timeout=10)
|
|
272
|
+
|
|
273
|
+
# Register with the coordinator
|
|
274
|
+
await self.transport.send_message(
|
|
275
|
+
server_url,
|
|
276
|
+
{
|
|
277
|
+
"type": "register",
|
|
278
|
+
"agent_name": self.name,
|
|
279
|
+
"agent_url": self.transport.get_url()
|
|
280
|
+
}
|
|
281
|
+
)
|