agent-mcp 0.1.3__py3-none-any.whl → 0.1.4__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.
Files changed (44) hide show
  1. agent_mcp/__init__.py +2 -2
  2. agent_mcp/camel_mcp_adapter.py +521 -0
  3. agent_mcp/cli.py +47 -0
  4. agent_mcp/heterogeneous_group_chat.py +412 -38
  5. agent_mcp/langchain_mcp_adapter.py +176 -43
  6. agent_mcp/mcp_agent.py +26 -0
  7. agent_mcp/mcp_transport.py +11 -5
  8. {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.4.dist-info}/METADATA +6 -4
  9. agent_mcp-0.1.4.dist-info/RECORD +49 -0
  10. {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.4.dist-info}/WHEEL +1 -1
  11. agent_mcp-0.1.4.dist-info/entry_points.txt +2 -0
  12. agent_mcp-0.1.4.dist-info/top_level.txt +3 -0
  13. demos/__init__.py +1 -0
  14. demos/basic/__init__.py +1 -0
  15. demos/basic/framework_examples.py +108 -0
  16. demos/basic/langchain_camel_demo.py +272 -0
  17. demos/basic/simple_chat.py +355 -0
  18. demos/basic/simple_integration_example.py +51 -0
  19. demos/collaboration/collaborative_task_example.py +437 -0
  20. demos/collaboration/group_chat_example.py +130 -0
  21. demos/collaboration/simplified_crewai_example.py +39 -0
  22. demos/langgraph/autonomous_langgraph_network.py +808 -0
  23. demos/langgraph/langgraph_agent_network.py +415 -0
  24. demos/langgraph/langgraph_collaborative_task.py +619 -0
  25. demos/langgraph/langgraph_example.py +227 -0
  26. demos/langgraph/run_langgraph_examples.py +213 -0
  27. demos/network/agent_network_example.py +381 -0
  28. demos/network/email_agent.py +130 -0
  29. demos/network/email_agent_demo.py +46 -0
  30. demos/network/heterogeneous_network_example.py +216 -0
  31. demos/network/multi_framework_example.py +199 -0
  32. demos/utils/check_imports.py +49 -0
  33. demos/workflows/autonomous_agent_workflow.py +248 -0
  34. demos/workflows/mcp_features_demo.py +353 -0
  35. demos/workflows/run_agent_collaboration_demo.py +63 -0
  36. demos/workflows/run_agent_collaboration_with_logs.py +396 -0
  37. demos/workflows/show_agent_interactions.py +107 -0
  38. demos/workflows/simplified_autonomous_demo.py +74 -0
  39. functions/main.py +144 -0
  40. functions/mcp_network_server.py +513 -0
  41. functions/utils.py +47 -0
  42. agent_mcp-0.1.3.dist-info/RECORD +0 -18
  43. agent_mcp-0.1.3.dist-info/entry_points.txt +0 -2
  44. agent_mcp-0.1.3.dist-info/top_level.txt +0 -1
agent_mcp/__init__.py CHANGED
@@ -2,6 +2,8 @@
2
2
  AgentMCP - Model Context Protocol for AI Agents
3
3
  """
4
4
 
5
+ __version__ = "0.1.4"
6
+
5
7
  from .mcp_agent import MCPAgent
6
8
  from .mcp_decorator import mcp_agent
7
9
  from .enhanced_mcp_agent import EnhancedMCPAgent
@@ -12,5 +14,3 @@ from .heterogeneous_group_chat import HeterogeneousGroupChat
12
14
  from .langchain_mcp_adapter import LangchainMCPAdapter
13
15
  from .crewai_mcp_adapter import CrewAIMCPAdapter
14
16
  from .langgraph_mcp_adapter import LangGraphMCPAdapter
15
-
16
- __version__ = "0.1.2"
@@ -0,0 +1,521 @@
1
+ """
2
+ Adapter for Camel AI agents to work with MCP.
3
+ """
4
+
5
+ import asyncio
6
+ from typing import Dict, Any, Optional
7
+ from .mcp_agent import MCPAgent
8
+ from .mcp_transport import MCPTransport
9
+ from camel.agents import ChatAgent # Import the core Camel AI agent
10
+ import traceback
11
+ import json
12
+ import uuid
13
+
14
+ # --- Setup Logger ---
15
+ import logging
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
17
+ logger = logging.getLogger(__name__)
18
+ # --- End Logger Setup ---
19
+
20
+ class CamelMCPAdapter(MCPAgent):
21
+ """Adapter for Camel AI ChatAgent to work with MCP"""
22
+
23
+ def __init__(self,
24
+ name: str,
25
+ transport: Optional[MCPTransport] = None,
26
+ client_mode: bool = False,
27
+ camel_agent: ChatAgent = None, # Expect a Camel ChatAgent instance
28
+ system_message: str = "",
29
+ **kwargs):
30
+ # Set default system message if none provided and agent doesn't have one
31
+ if not system_message and camel_agent and hasattr(camel_agent, 'system_message') and not camel_agent.system_message:
32
+ system_message = "I am a Camel AI agent ready to assist."
33
+ elif not system_message:
34
+ system_message = "I am a Camel AI agent ready to assist."
35
+
36
+ # Initialize parent with system message
37
+ # Use provided system_message or agent's if available
38
+ effective_system_message = system_message or (camel_agent.system_message.content if camel_agent and camel_agent.system_message else "Camel AI Assistant")
39
+ super().__init__(name=name, system_message=effective_system_message, **kwargs)
40
+
41
+ # Set instance attributes
42
+ self.transport = transport
43
+ self.client_mode = client_mode
44
+ self.camel_agent = camel_agent # Store the Camel ChatAgent
45
+ self.task_queue = asyncio.Queue()
46
+ self._task_processor = None
47
+ self._message_processor = None
48
+ self._processed_tasks = set() # For idempotency check
49
+
50
+ if not self.camel_agent:
51
+ raise ValueError("A camel.agents.ChatAgent instance must be provided.")
52
+
53
+ async def connect_to_server(self, server_url: str):
54
+ """Connect to another agent's server"""
55
+ if not self.client_mode or not self.transport:
56
+ raise ValueError("Agent not configured for client mode")
57
+
58
+ # Register with the server
59
+ registration = {
60
+ "type": "registration",
61
+ "agent_id": self.mcp_id,
62
+ "name": self.name,
63
+ "capabilities": [] # Define capabilities based on agent if needed
64
+ }
65
+
66
+ response = await self.transport.send_message(server_url, registration)
67
+ if response.get("status") == "ok":
68
+ print(f"Successfully connected to server at {server_url}")
69
+ # transport message format
70
+ async def handle_incoming_message(self, message: Dict[str, Any], message_id: Optional[str] = None):
71
+ """Handle incoming messages from other agents"""
72
+ # First check if type is directly in the message
73
+ msg_type = message.get("type")
74
+ logger.info(f"[{self.name}] Raw message: {message}")
75
+
76
+ # If not, check if it's inside the content field
77
+ if not msg_type and "content" in message and isinstance(message["content"], dict):
78
+ msg_type = message["content"].get("type")
79
+
80
+ # Extract sender with nested JSON support
81
+ sender = self._extract_sender(message)
82
+ task_id = message.get("task_id") or message.get("content", {}).get("task_id") if isinstance(message.get("content"), dict) else message.get("task_id")
83
+ logger.info(f"[{self.name}] Received message (ID: {message_id}) of type '{msg_type}' from {sender} (Task ID: {task_id})")
84
+
85
+ # --- Idempotency Check ---
86
+ if not super()._should_process_message(message):
87
+ # If skipped, acknowledge and stop
88
+ if message_id and self.transport:
89
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
90
+ logger.info(f"[{self.name}] Acknowledged duplicate task {task_id} (msg_id: {message_id})")
91
+ return
92
+ # --- End Idempotency Check ---
93
+
94
+ if msg_type == "task":
95
+ logger.info(f"[{self.name}] Queueing task {task_id} (message_id: {message_id}) from {sender}")
96
+ content = message.get("content", {})
97
+ # Ensure task_id and description are retrieved correctly from nested content if necessary
98
+ current_task_id = content.get("task_id", message.get("task_id"))
99
+ description = content.get("description", message.get("description"))
100
+ reply_to = content.get("reply_to", message.get("reply_to"))
101
+
102
+ if not current_task_id or not description:
103
+ logger.error(f"[{self.name}] Task message missing required fields (task_id or description): {message}")
104
+ # Acknowledge if possible, even if invalid, to prevent reprocessing
105
+ if message_id and self.transport:
106
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
107
+ return
108
+
109
+ # Add message_id to task context for later acknowledgement
110
+ task_context = {
111
+ "type": "task", # Explicitly set type for process_tasks
112
+ "task_id": current_task_id,
113
+ "description": description,
114
+ "reply_to": reply_to,
115
+ "sender": sender,
116
+ "message_id": message_id, # Store message_id for acknowledgement
117
+ "original_message": message # Store original for context if needed
118
+ }
119
+
120
+ logger.debug(f"[{self.name}] Queueing task context: {task_context}")
121
+ await self.task_queue.put(task_context)
122
+ logger.debug(f"[{self.name}] Successfully queued task {current_task_id}")
123
+
124
+ elif msg_type == "task_result":
125
+ # Received a result, treat it as the next step in the conversation
126
+ result_content = message.get("result")
127
+
128
+ # --- Robust extraction for various formats ---
129
+ content = message.get("content")
130
+ if result_content is None and content is not None:
131
+ # 1. Try content["result"]
132
+ if isinstance(content, dict) and "result" in content:
133
+ result_content = content["result"]
134
+ # 2. Try content["text"] as JSON
135
+ elif isinstance(content, dict) and "text" in content:
136
+ text_val = content["text"]
137
+ if isinstance(text_val, str):
138
+ try:
139
+ parsed = json.loads(text_val)
140
+ if isinstance(parsed, dict) and "result" in parsed:
141
+ result_content = parsed["result"]
142
+ except Exception:
143
+ pass
144
+ # 3. Try content itself as JSON string
145
+ elif isinstance(content, str):
146
+ try:
147
+ parsed = json.loads(content)
148
+ if isinstance(parsed, dict) and "result" in parsed:
149
+ result_content = parsed["result"]
150
+ except Exception:
151
+ pass
152
+ # 4. Fallback: use content["text"] as plain string
153
+ if result_content is None and isinstance(content, dict) and "text" in content:
154
+ result_content = content["text"]
155
+
156
+ # Add direct parsing of content["text"] structure
157
+ if isinstance(result_content, str):
158
+ try:
159
+ text_content = json.loads(result_content)
160
+ if isinstance(text_content, dict):
161
+ result_content = text_content
162
+ except json.JSONDecodeError:
163
+ pass
164
+
165
+ # Handle JSON string content
166
+ if isinstance(result_content, str):
167
+ try:
168
+ result_content = json.loads(result_content)
169
+ except json.JSONDecodeError:
170
+ pass
171
+
172
+ original_task_id = (
173
+ (result_content.get("task_id") if isinstance(result_content, dict) else None)
174
+ or message.get("task_id")
175
+ )
176
+ logger.info(f"[{self.name}] Received task_result from {sender} for task {original_task_id}. Content: '{str(result_content)[:100]}...'")
177
+
178
+ if not result_content:
179
+ logger.warning(f"[{self.name}] Received task_result from {sender} with empty content.")
180
+ # Acknowledge the result message even if content is empty
181
+ if message_id and self.transport:
182
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
183
+ return
184
+
185
+ # Create a *new* task for this agent based on the received result
186
+ new_task_id = f"conv_{uuid.uuid4()}" # Generate a new ID for this conversational turn
187
+ new_task_context = {
188
+ "type": "task", # Still a task for this agent to process
189
+ "task_id": new_task_id,
190
+ "description": str(result_content), # The result becomes the new input/description
191
+ "reply_to": message.get("reply_to") or result_content.get("reply_to"),
192
+ "sender": sender, # This agent is the conceptual sender of this internal task
193
+ "message_id": message_id # Carry over original message ID for acknowledgement
194
+ }
195
+
196
+ logger.info(f"[{self.name}] Queueing new conversational task {new_task_id} based on result from {sender}")
197
+ await self.task_queue.put(new_task_context)
198
+ logger.debug(f"[{self.name}] Successfully queued new task {new_task_id}")
199
+
200
+ else:
201
+ logger.warning(f"[{self.name}] Received unknown message type: {msg_type}. Message: {message}")
202
+ # Acknowledge non-task messages immediately if they have an ID
203
+ if message_id and self.transport:
204
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
205
+
206
+ # message processor loop making sure the message is a dictionary and then processing it
207
+ async def process_messages(self):
208
+ logger.info(f"[{self.name}] Message processor loop started.")
209
+ while True:
210
+ try:
211
+ logger.debug(f"[{self.name}] Waiting for message from transport...")
212
+ # Get message from transport (without agent_name parameter)
213
+ message, message_id = await self.transport.receive_message()
214
+ logger.debug(f"[{self.name}] Received raw message from transport: {message} (ID: {message_id})")
215
+
216
+
217
+ if message is None:
218
+ logger.debug(f"[{self.name}] Received None message, continuing loop...")
219
+ await asyncio.sleep(0.1) # Avoid busy-waiting
220
+ continue
221
+
222
+ # Ensure message is a dictionary before proceeding
223
+ if not isinstance(message, dict):
224
+ logger.error(f"[{self.name}] Received non-dict message: {message} (type: {type(message)}), skipping.")
225
+ # Attempt to acknowledge if possible
226
+ if message_id and self.transport:
227
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
228
+ continue
229
+
230
+ logger.info(f"[{self.name}] Processing message {message_id}: {message}")
231
+ await self.handle_incoming_message(message, message_id)
232
+
233
+ except asyncio.CancelledError:
234
+ logger.info(f"[{self.name}] Message processor cancelled.")
235
+ break
236
+ except Exception as e:
237
+ logger.error(f"[{self.name}] Error in message processor: {e}", exc_info=True)
238
+ traceback.print_exc()
239
+ # Avoid immediate exit on error, maybe add a delay
240
+ await asyncio.sleep(1)
241
+ logger.info(f"[{self.name}] Message processor loop finished.")
242
+
243
+
244
+ async def process_tasks(self):
245
+ logger.info(f"[{self.name}] Task processor loop started.")
246
+ while True:
247
+ try:
248
+ logger.debug(f"[{self.name}] Waiting for task from queue...")
249
+ task_context = await self.task_queue.get()
250
+ logger.info(f"\n[{self.name}] Dequeued item from task queue: {task_context}")
251
+
252
+ if not isinstance(task_context, dict):
253
+ logger.error(f"[{self.name}] Task item is not a dictionary: {task_context}")
254
+ self.task_queue.task_done()
255
+ continue
256
+
257
+ # Extract task details from the context dictionary
258
+ task_desc = task_context.get("description")
259
+ task_id = task_context.get("task_id")
260
+ reply_to = task_context.get("reply_to")
261
+ message_id = task_context.get("message_id") # Retrieve message_id for ack
262
+ task_type = task_context.get("type") # Should be 'task'
263
+
264
+ logger.info(f"[{self.name}] Processing task details:")
265
+ logger.info(f" - Task ID: {task_id}")
266
+ logger.info(f" - Type: {task_type}")
267
+ logger.info(f" - Reply To: {reply_to}")
268
+ logger.info(f" - Description: {str(task_desc)[:100]}...")
269
+ logger.info(f" - Original Message ID: {message_id}")
270
+
271
+ if not task_desc or not task_id:
272
+ logger.error(f"[{self.name}] Task context missing description or task_id: {task_context}")
273
+ self.task_queue.task_done()
274
+ # Acknowledge message even if task context is bad
275
+ if message_id and self.transport:
276
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
277
+ logger.warning(f"[{self.name}] Acknowledged message {message_id} for invalid task context.")
278
+ continue
279
+
280
+ # Ensure it's actually a task we should process
281
+ if task_type != "task":
282
+ logger.error(f"[{self.name}] Invalid item type in task queue: {task_type}. Item: {task_context}")
283
+ self.task_queue.task_done()
284
+ if message_id and self.transport:
285
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
286
+ continue
287
+
288
+ logger.info(f"[{self.name}] Starting execution of task {task_id}: '{str(task_desc)[:50]}...'" )
289
+
290
+ # --- Execute task using Camel AI agent ---
291
+ result_str = None
292
+ try:
293
+ logger.debug(f"[{self.name}] Calling camel_agent.astep with task description...")
294
+ # Use astep for asynchronous execution if available, otherwise step
295
+ if hasattr(self.camel_agent, 'astep'):
296
+ response = await self.camel_agent.astep(task_desc)
297
+ else:
298
+ # Fallback to synchronous step in executor if astep not available
299
+ # Note: This blocks the task processor loop. Consider running in executor.
300
+ logger.warning(f"[{self.name}] Camel agent does not have 'astep'. Using synchronous 'step'. This may block.")
301
+ # For safety, run sync 'step' in an executor to avoid blocking asyncio loop
302
+ loop = asyncio.get_running_loop()
303
+ response = await loop.run_in_executor(None, self.camel_agent.step, task_desc)
304
+
305
+
306
+ # Extract content from the response object
307
+ # Assuming response has a 'msg' attribute with 'content' based on docs example
308
+ if response and hasattr(response, 'msg') and hasattr(response.msg, 'content'):
309
+ result_str = response.msg.content
310
+ logger.debug(f"[{self.name}] Camel agent execution successful. Result: '{str(result_str)[:100]}...'" )
311
+ elif response and hasattr(response, 'messages') and response.messages:
312
+ # Handle cases where response might be a list of messages (e.g., RolePlaying)
313
+ # Take the last message content as the result for simplicity
314
+ last_msg = response.messages[-1]
315
+ if hasattr(last_msg, 'content'):
316
+ result_str = last_msg.content
317
+ logger.debug(f"[{self.name}] Camel agent execution successful (from messages). Result: '{str(result_str)[:100]}...'" )
318
+ else:
319
+ logger.warning(f"[{self.name}] Camel agent response's last message has no 'content'. Response: {response}")
320
+ result_str = str(response) # Fallback
321
+ elif response:
322
+ logger.warning(f"[{self.name}] Camel agent response format unexpected. Using str(response). Response: {response}")
323
+ result_str = str(response) # Fallback to string representation
324
+ else:
325
+ logger.warning(f"[{self.name}] Camel agent returned None or empty response.")
326
+ result_str = "Agent returned no response."
327
+
328
+
329
+ except Exception as e:
330
+ logger.error(f"[{self.name}] Camel agent execution failed for task {task_id}: {e}", exc_info=True)
331
+ traceback.print_exc()
332
+ result_str = f"Agent execution failed due to an error: {str(e)}"
333
+
334
+ # Ensure result is always a string before sending
335
+ if not isinstance(result_str, str):
336
+ try:
337
+ result_str = json.dumps(result_str) # Try serializing complex types
338
+ except (TypeError, OverflowError):
339
+ result_str = str(result_str) # Fallback
340
+
341
+ logger.info(f"[{self.name}] Sending result for task {task_id} to {reply_to}")
342
+ # --- Send the result back ---
343
+ if reply_to and self.transport:
344
+ try:
345
+ # Extract target agent name from reply_to (assuming format http://.../agent_name)
346
+ target_agent_name = reply_to # Default if parsing fails
347
+ try:
348
+ target_agent_name = reply_to.split('/')[-1]
349
+ if not target_agent_name: # Handle trailing slash case
350
+ target_agent_name = reply_to.split('/')[-2]
351
+ except IndexError:
352
+ logger.warning(f"[{self.name}] Could not reliably extract agent name from reply_to URL: {reply_to}. Using full URL.")
353
+
354
+
355
+ logger.debug(f"[{self.name}] Sending result to target agent: {target_agent_name} (extracted from {reply_to})")
356
+
357
+ await self.transport.send_message(
358
+ target_agent_name, # Send to the specific agent name/ID
359
+ {
360
+ "type": "task_result",
361
+ "task_id": task_id,
362
+ "result": result_str,
363
+ "sender": self.name,
364
+ "reply_to": self.name, # Add this so the receiving agent knows where to send follow-up messages
365
+ "original_message_id": message_id # Include original message ID for tracing/ack
366
+ }
367
+ )
368
+ logger.debug(f"[{self.name}] Result sent successfully for task {task_id}")
369
+
370
+ # Acknowledge task completion *after* sending result, using message_id
371
+ if message_id:
372
+ await self.transport.acknowledge_message(self.name, message_id)
373
+ logger.info(f"[{self.name}] Task {task_id} acknowledged via message_id {message_id}")
374
+ else:
375
+ logger.warning(f"[{self.name}] No message_id found for task {task_id} in context, cannot acknowledge transport message.")
376
+
377
+ except Exception as send_error:
378
+ logger.error(f"[{self.name}] Failed to send result or acknowledge for task {task_id}: {send_error}", exc_info=True)
379
+ traceback.print_exc()
380
+ # Decide if task should be retried or marked done despite send failure
381
+ # For now, mark done to avoid loop, but log error clearly
382
+ else:
383
+ logger.warning(f"[{self.name}] No reply_to address or transport configured for task {task_id}. Cannot send result or acknowledge.")
384
+ # If no reply_to, we might still need to acknowledge if we have a message_id
385
+ if message_id and self.transport:
386
+ logger.warning(f"[{self.name}] Acknowledging message {message_id} for task {task_id} even though result wasn't sent (no reply_to)." )
387
+ await self.transport.acknowledge_message(self.name, message_id)
388
+
389
+
390
+ # Mark task as completed internally *after* processing and attempting send/ack
391
+ super()._mark_task_completed(task_id) # Call base class method
392
+
393
+ self.task_queue.task_done()
394
+ logger.info(f"[{self.name}] Task {task_id} fully processed and marked done.")
395
+
396
+ except asyncio.CancelledError:
397
+ logger.info(f"[{self.name}] Task processor cancelled.")
398
+ break
399
+ except Exception as e:
400
+ logger.error(f"[{self.name}] Unhandled error in task processing loop: {e}", exc_info=True)
401
+ traceback.print_exc()
402
+ # Ensure task_done is called even in unexpected error to prevent queue blockage
403
+ try:
404
+ self.task_queue.task_done()
405
+ except ValueError: # Already done
406
+ pass
407
+ await asyncio.sleep(1) # Prevent fast error loop
408
+
409
+ logger.info(f"[{self.name}] Task processor loop finished.")
410
+
411
+ # --- Helper methods inherited or overridden from MCPAgent ---
412
+ # _should_process_message and _mark_task_completed are inherited from MCPAgent
413
+ # but shown here for clarity from the original langchain adapter template.
414
+ # We rely on the super() calls within handle_incoming_message and process_tasks.
415
+
416
+ # def _should_process_message(self, message: Dict[str, Any]) -> bool:
417
+ # """Check if a message should be processed based on idempotency"""
418
+ # # Implementation relies on MCPAgent's base method via super()
419
+
420
+ # def _mark_task_completed(self, task_id: str) -> None:
421
+ # """Mark a task as completed for idempotency tracking"""
422
+ # # Implementation relies on MCPAgent's base method via super()
423
+
424
+ async def run(self):
425
+ """Start the agent's message and task processing loops."""
426
+ if not self.transport:
427
+ raise ValueError("Transport must be configured to run the agent.")
428
+
429
+ logger.info(f"[{self.name}] Starting CamelMCPAdapter run loop...")
430
+
431
+ # Start the message processing loop
432
+ self._message_processor = asyncio.create_task(self.process_messages())
433
+ # Start the task processing loop
434
+ self._task_processor = asyncio.create_task(self.process_tasks())
435
+
436
+ try:
437
+ # Keep running until tasks are cancelled
438
+ await asyncio.gather(self._message_processor, self._task_processor)
439
+ except asyncio.CancelledError:
440
+ logger.info(f"[{self.name}] Agent run loop cancelled.")
441
+ finally:
442
+ logger.info(f"[{self.name}] Agent run loop finished.")
443
+
444
+ async def stop(self):
445
+ """Stop the agent's processing loops gracefully."""
446
+ logger.info(f"[{self.name}] Stopping agent...")
447
+ if self._message_processor and not self._message_processor.done():
448
+ self._message_processor.cancel()
449
+ if self._task_processor and not self._task_processor.done():
450
+ self._task_processor.cancel()
451
+
452
+ # Wait for tasks to finish cancelling
453
+ try:
454
+ await asyncio.gather(self._message_processor, self._task_processor, return_exceptions=True)
455
+ except asyncio.CancelledError:
456
+ pass # Expected
457
+ logger.info(f"[{self.name}] Agent stopped.")
458
+
459
+ # Example Usage (for demonstration purposes, would normally be in a separate script)
460
+ # async def main():
461
+ # # Requires setting up a transport (e.g., MCPTransport)
462
+ # # Requires creating a Camel ChatAgent instance
463
+ # from camel.models import ModelFactory
464
+ # from camel.types import ModelType
465
+ # import os
466
+ #
467
+ # # Ensure API key is set
468
+ # # os.environ["OPENAI_API_KEY"] = "your_key_here"
469
+ #
470
+ # # 1. Create a Camel ChatAgent
471
+ # model = ModelFactory.create(model_type=ModelType.GPT_4O_MINI)
472
+ # camel_agent_instance = ChatAgent(system_message="You are a helpful geography expert.", model=model)
473
+ #
474
+ # # 2. Create a transport (replace with actual transport implementation)
475
+ # class MockTransport(MCPTransport): # Replace with your actual transport
476
+ # async def send_message(self, target: str, message: Dict[str, Any]):
477
+ # print(f"[MockTransport] Sending to {target}: {message}")
478
+ # # Simulate response for registration
479
+ # if message.get("type") == "registration":
480
+ # return {"status": "ok"}
481
+ # return {"status": "sent"}
482
+ # async def receive_message(self):
483
+ # print("[MockTransport] receive_message called, simulating task...")
484
+ # await asyncio.sleep(5) # Simulate delay
485
+ # # Simulate receiving a task message
486
+ # return {
487
+ # "type": "task",
488
+ # "task_id": "task-123",
489
+ # "description": "What is the capital of France?",
490
+ # "sender": "user_agent",
491
+ # "reply_to": "http://localhost:8000/user_agent" # Example reply URL
492
+ # }, "msg-abc" # Message and ID
493
+ # async def acknowledge_message(self, agent_id: str, message_id: str):
494
+ # print(f"[MockTransport] Acknowledging message {message_id} for agent {agent_id}")
495
+ # async def connect(self): pass
496
+ # async def disconnect(self): pass
497
+ #
498
+ # transport_instance = MockTransport()
499
+ #
500
+ # # 3. Create the adapter
501
+ # adapter = CamelMCPAdapter(
502
+ # name="CamelGeographyAgent",
503
+ # transport=transport_instance,
504
+ # camel_agent=camel_agent_instance
505
+ # )
506
+ #
507
+ # # 4. Run the adapter
508
+ # print("Starting CamelMCPAdapter...")
509
+ # try:
510
+ # await adapter.run()
511
+ # except KeyboardInterrupt:
512
+ # await adapter.stop()
513
+ # print("Adapter finished.")
514
+ #
515
+ # if __name__ == "__main__":
516
+ # # Note: Requires OPENAI_API_KEY environment variable to be set
517
+ # # if os.getenv("OPENAI_API_KEY"):
518
+ # # asyncio.run(main())
519
+ # # else:
520
+ # # print("Please set the OPENAI_API_KEY environment variable to run the example.")
521
+ # pass # Keep example code commented out in the main file
agent_mcp/cli.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ Command-line interface for the Agent MCP package.
3
+ """
4
+ import argparse
5
+ import logging
6
+ from typing import Optional
7
+
8
+ def main():
9
+ """Entry point for the command-line interface."""
10
+ parser = argparse.ArgumentParser(description="Agent MCP - Multi-agent Collaboration Platform")
11
+ parser.add_argument(
12
+ "--version",
13
+ action="store_true",
14
+ help="Show version information"
15
+ )
16
+ parser.add_argument(
17
+ "-v",
18
+ "--verbose",
19
+ action="count",
20
+ default=0,
21
+ help="Increase verbosity (use -vv for debug level)"
22
+ )
23
+
24
+ args = parser.parse_args()
25
+
26
+ # Configure logging
27
+ log_level = logging.WARNING
28
+ if args.verbose == 1:
29
+ log_level = logging.INFO
30
+ elif args.verbose >= 2:
31
+ log_level = logging.DEBUG
32
+
33
+ logging.basicConfig(
34
+ level=log_level,
35
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
36
+ )
37
+
38
+ if args.version:
39
+ from agent_mcp import __version__
40
+ print(f"Agent MCP version {__version__}")
41
+ return
42
+
43
+ # Default action (you can add more commands here)
44
+ print("Agent MCP - Use --help for usage information")
45
+
46
+ if __name__ == "__main__":
47
+ main()