agent-mcp 0.1.3__py3-none-any.whl → 0.1.5__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 (57) hide show
  1. agent_mcp/__init__.py +66 -12
  2. agent_mcp/a2a_protocol.py +316 -0
  3. agent_mcp/agent_lightning_library.py +214 -0
  4. agent_mcp/camel_mcp_adapter.py +521 -0
  5. agent_mcp/claude_mcp_adapter.py +195 -0
  6. agent_mcp/cli.py +47 -0
  7. agent_mcp/google_ai_mcp_adapter.py +183 -0
  8. agent_mcp/heterogeneous_group_chat.py +412 -38
  9. agent_mcp/langchain_mcp_adapter.py +176 -43
  10. agent_mcp/llamaindex_mcp_adapter.py +410 -0
  11. agent_mcp/mcp_agent.py +26 -0
  12. agent_mcp/mcp_transport.py +11 -5
  13. agent_mcp/microsoft_agent_framework.py +591 -0
  14. agent_mcp/missing_frameworks.py +435 -0
  15. agent_mcp/openapi_protocol.py +616 -0
  16. agent_mcp/payments.py +804 -0
  17. agent_mcp/pydantic_ai_mcp_adapter.py +628 -0
  18. agent_mcp/registry.py +768 -0
  19. agent_mcp/security.py +864 -0
  20. {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.5.dist-info}/METADATA +173 -49
  21. agent_mcp-0.1.5.dist-info/RECORD +62 -0
  22. {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.5.dist-info}/WHEEL +1 -1
  23. agent_mcp-0.1.5.dist-info/entry_points.txt +4 -0
  24. agent_mcp-0.1.5.dist-info/top_level.txt +3 -0
  25. demos/__init__.py +1 -0
  26. demos/basic/__init__.py +1 -0
  27. demos/basic/framework_examples.py +108 -0
  28. demos/basic/langchain_camel_demo.py +272 -0
  29. demos/basic/simple_chat.py +355 -0
  30. demos/basic/simple_integration_example.py +51 -0
  31. demos/collaboration/collaborative_task_example.py +437 -0
  32. demos/collaboration/group_chat_example.py +130 -0
  33. demos/collaboration/simplified_crewai_example.py +39 -0
  34. demos/comprehensive_framework_demo.py +202 -0
  35. demos/langgraph/autonomous_langgraph_network.py +808 -0
  36. demos/langgraph/langgraph_agent_network.py +415 -0
  37. demos/langgraph/langgraph_collaborative_task.py +619 -0
  38. demos/langgraph/langgraph_example.py +227 -0
  39. demos/langgraph/run_langgraph_examples.py +213 -0
  40. demos/network/agent_network_example.py +381 -0
  41. demos/network/email_agent.py +130 -0
  42. demos/network/email_agent_demo.py +46 -0
  43. demos/network/heterogeneous_network_example.py +216 -0
  44. demos/network/multi_framework_example.py +199 -0
  45. demos/utils/check_imports.py +49 -0
  46. demos/workflows/autonomous_agent_workflow.py +248 -0
  47. demos/workflows/mcp_features_demo.py +353 -0
  48. demos/workflows/run_agent_collaboration_demo.py +63 -0
  49. demos/workflows/run_agent_collaboration_with_logs.py +396 -0
  50. demos/workflows/show_agent_interactions.py +107 -0
  51. demos/workflows/simplified_autonomous_demo.py +74 -0
  52. functions/main.py +144 -0
  53. functions/mcp_network_server.py +513 -0
  54. functions/utils.py +47 -0
  55. agent_mcp-0.1.3.dist-info/RECORD +0 -18
  56. agent_mcp-0.1.3.dist-info/entry_points.txt +0 -2
  57. agent_mcp-0.1.3.dist-info/top_level.txt +0 -1
@@ -10,6 +10,7 @@ from langchain.agents import AgentExecutor
10
10
  from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
11
11
  import traceback
12
12
  import json
13
+ import uuid
13
14
 
14
15
  # --- Setup Logger ---
15
16
  import logging
@@ -66,12 +67,13 @@ class LangchainMCPAdapter(MCPAgent):
66
67
  """Handle incoming messages from other agents"""
67
68
  # First check if type is directly in the message
68
69
  msg_type = message.get("type")
69
-
70
+ logger.info(f"[{self.name}] Raw message: {message}")
71
+
70
72
  # If not, check if it's inside the content field
71
73
  if not msg_type and "content" in message and isinstance(message["content"], dict):
72
74
  msg_type = message["content"].get("type")
73
75
 
74
- sender = message.get("sender", "Unknown")
76
+ sender = self._extract_sender(message)
75
77
  task_id = message.get("task_id") or message.get("content", {}).get("task_id") if isinstance(message.get("content"), dict) else message.get("task_id")
76
78
  logger.info(f"[{self.name}] Received message (ID: {message_id}) of type '{msg_type}' from {sender} (Task ID: {task_id})")
77
79
 
@@ -87,24 +89,118 @@ class LangchainMCPAdapter(MCPAgent):
87
89
  if msg_type == "task":
88
90
  logger.info(f"[{self.name}] Queueing task {task_id} (message_id: {message_id}) from {sender}")
89
91
  content = message.get("content", {})
90
- task_id = content.get("task_id") or message.get("task_id")
92
+ current_task_id = content.get("task_id") or message.get("task_id") # Handle potential nesting
91
93
  description = content.get("description") or message.get("description")
92
- reply_to = content.get("reply_to")
93
-
94
- if not task_id or not description:
95
- print(f"[ERROR] {self.name}: Task message missing required fields: {message}")
94
+ reply_to = content.get("reply_to") or message.get("reply_to")
95
+
96
+ if not current_task_id or not description:
97
+ logger.error(f"[{self.name}] Task message missing required fields: {message}")
98
+ # Acknowledge if possible to prevent reprocessing bad message
99
+ if message_id and self.transport:
100
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
96
101
  return
97
-
98
- # Add message_id to task
102
+
103
+ # Add message_id to task context for processing
99
104
  message['message_id'] = message_id
100
-
101
- # Queue task for async processing
102
- print(f"[DEBUG] {self.name}: Queueing task {task_id} with message_id {message_id} for processing")
105
+
106
+ #task_context = {
107
+ # "type": "task", # Ensure type is explicitly set for process_tasks
108
+ # "task_id": current_task_id,
109
+ # "description": description,
110
+ # "reply_to": reply_to,
111
+ # "sender": sender,
112
+ # "message_id": message_id
113
+ #}
114
+ #logger.debug(f"[{self.name}] Queueing task context: {task_context}")
115
+ logger.debug(f"[DEBUG] {self.name}: Queueing task {task_id} with message_id {message_id} for processing")
116
+
103
117
  await self.task_queue.put(message)
104
- print(f"[DEBUG] {self.name}: Successfully queued task {task_id}")
105
- else:
106
- print(f"[WARN] {self.name}: Received unknown message type: {msg_type}")
118
+ logger.debug(f"[{self.name}] Successfully queued task {current_task_id}")
119
+
120
+ elif msg_type == "task_result":
121
+ # Received a result, treat it as the next step in the conversation
122
+ result_content = message.get("result")
123
+
124
+ # --- Robust extraction for various formats ---
125
+ content = message.get("content")
126
+ if result_content is None and content is not None:
127
+ # 1. Try content["result"]
128
+ if isinstance(content, dict) and "result" in content:
129
+ result_content = content["result"]
130
+ # 2. Try content["text"] as JSON
131
+ elif isinstance(content, dict) and "text" in content:
132
+ text_val = content["text"]
133
+ if isinstance(text_val, str):
134
+ try:
135
+ parsed = json.loads(text_val)
136
+ if isinstance(parsed, dict) and "result" in parsed:
137
+ result_content = parsed["result"]
138
+ except Exception:
139
+ pass
140
+ # 3. Try content itself as JSON string
141
+ elif isinstance(content, str):
142
+ try:
143
+ parsed = json.loads(content)
144
+ if isinstance(parsed, dict) and "result" in parsed:
145
+ result_content = parsed["result"]
146
+ except Exception:
147
+ pass
148
+ # 4. Fallback: use content["text"] as plain string
149
+ if result_content is None and isinstance(content, dict) and "text" in content:
150
+ result_content = content["text"]
151
+
152
+ # Handle JSON string content
153
+ if isinstance(result_content, str):
154
+ try:
155
+ result_content = json.loads(result_content)
156
+ except json.JSONDecodeError:
157
+ pass
158
+
159
+ # Direct parsing of content["text"] structure
160
+ if isinstance(result_content, str):
161
+ try:
162
+ text_content = json.loads(result_content)
163
+ if isinstance(text_content, dict):
164
+ result_content = text_content
165
+ except json.JSONDecodeError:
166
+ pass
167
+
168
+ # --- End Robust extraction ---
169
+ original_task_id = (
170
+ (result_content.get("task_id") if isinstance(result_content, dict) else None)
171
+ or message.get("task_id")
172
+ )
173
+ logger.info(f"[{self.name}] Received task_result from {sender} for task {original_task_id}. Content: '{str(result_content)[:100]}...'")
174
+
175
+ if not result_content:
176
+ logger.warning(f"[{self.name}] Received task_result from {sender} with empty content.")
107
177
 
178
+ # Acknowledge the result message even if content is empty
179
+ if message_id and self.transport:
180
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
181
+ return
182
+
183
+ # Create a *new* task for this agent based on the received result
184
+ #new_task_id = f"conv_{uuid.uuid4()}" # Generate a new ID for this conversational turn
185
+ #new_task_context = {
186
+ # "type": "task", # Still a task for this agent to process
187
+ # "task_id": new_task_id,
188
+ # "description": str(result_content), # The result becomes the new input/description
189
+ # "reply_to": message.get("reply_to") or result_content.get("reply_to"),
190
+ # "sender": sender, # This agent is the conceptual sender of this internal task
191
+ # "message_id": message_id # Carry over original message ID for acknowledgement
192
+ #}
193
+
194
+ #logger.info(f"[{self.name}] Queueing new conversational task {new_task_id} based on result from {sender}")
195
+ #await self.task_queue.put(new_task_context)
196
+ #logger.debug(f"[{self.name}] Successfully queued new task {new_task_id}")
197
+
198
+ else:
199
+ logger.warning(f"[{self.name}] Received unknown message type: {msg_type}. Message: {message}")
200
+ # Acknowledge other message types immediately if they have an ID
201
+ #if message_id and self.transport:
202
+ # asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
203
+
108
204
  async def _handle_task(self, message: Dict[str, Any]):
109
205
  """Handle incoming task"""
110
206
  print(f"{self.name}: Received task: {message}")
@@ -112,14 +208,14 @@ class LangchainMCPAdapter(MCPAgent):
112
208
  return {"status": "ok"}
113
209
 
114
210
  async def process_messages(self):
115
- print(f"[{self.name}] Message processor loop started.")
211
+ logger.info(f"[{self.name}] Message processor loop started.")
116
212
  while True:
117
213
  try:
118
- print(f"[{self.name}] Waiting for message from queue...")
214
+ logger.debug(f"[{self.name}] Waiting for message from transport...")
215
+ # Pass agent name to receive_message
119
216
  message, message_id = await self.transport.receive_message()
120
- print(f"{self.name}: Processing message {message_id}: {message}")
121
-
122
- # Skip None messages
217
+ logger.debug(f"[{self.name}] Received raw message from transport: {message} (ID: {message_id})")
218
+
123
219
  if message is None:
124
220
  print(f"[{self.name}] Received None message, skipping...")
125
221
  continue
@@ -143,48 +239,77 @@ class LangchainMCPAdapter(MCPAgent):
143
239
  try:
144
240
  print(f"[{self.name}] Waiting for task from queue...")
145
241
  task = await self.task_queue.get()
146
- print(f"\n[{self.name}] Got task from queue: {task}")
242
+ print(f"\n[{self.name}] Got item from queue: {task}")
147
243
 
148
244
  if not isinstance(task, dict):
149
- print(f"[ERROR] {self.name}: Task is not a dictionary: {task}")
245
+ print(f"[ERROR] {self.name}: Task item is not a dictionary: {task}")
150
246
  self.task_queue.task_done()
151
247
  continue
152
248
 
153
- # Get task details from content field if present
154
- content = task.get("content", {})
155
- task_desc = content.get("description") or task.get("description")
156
- task_id = content.get("task_id") or task.get("task_id")
157
- task_type = content.get("type") or task.get("type")
158
- reply_to = content.get("reply_to") or task.get("reply_to")
159
-
160
- print(f"[DEBUG] {self.name}: Task details:")
161
- print(f" - Type: {task_type}")
249
+ # Extract task details (handle both original message format and task_context format)
250
+ task_desc = task.get("description")
251
+ task_id = task.get("task_id")
252
+ task_type = task.get("type") # Should always be 'task' if queued correctly
253
+ reply_to = task.get("reply_to")
254
+ message_id = task.get("message_id") # For acknowledgement
255
+ sender = self._extract_sender(task)
256
+ # Fallback for nested content (less likely now but safe)
257
+ if not task_desc and isinstance(task.get("content"), dict):
258
+ content = task.get("content", {})
259
+ task_desc = content.get("description")
260
+ if not task_id: task_id = content.get("task_id")
261
+ if not task_type: task_type = content.get("type")
262
+ if not reply_to: reply_to = content.get("reply_to")
263
+ if not sender: sender = content.get("sender", "from")
264
+
265
+ print(f"[DEBUG] {self.name}: Processing task details:")
162
266
  print(f" - Task ID: {task_id}")
267
+ print(f" - Type: {task_type}")
268
+ print(f" - Sender: {sender}")
163
269
  print(f" - Reply To: {reply_to}")
164
- print(f" - Description: {task_desc}")
270
+ print(f" - Description: {str(task_desc)[:100]}...")
271
+ print(f" - Original Message ID: {message_id}")
165
272
 
166
273
  if not task_desc or not task_id:
167
- print(f"[ERROR] {self.name}: Task is missing description or task_id")
274
+ print(f"[ERROR] {self.name}: Task is missing description or task_id: {task}")
168
275
  self.task_queue.task_done()
276
+ # Acknowledge if possible
277
+ #if message_id and self.transport:
278
+ # asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
169
279
  continue
170
280
 
281
+ # We only queue tasks now, so this check might be redundant but safe
171
282
  if task_type != "task":
172
- print(f"[ERROR] {self.name}: Invalid task type: {task_type}")
283
+ print(f"[ERROR] {self.name}: Invalid item type received in task queue: {task_type}. Item: {task}")
173
284
  self.task_queue.task_done()
285
+ #if message_id and self.transport:
286
+ # asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
174
287
  continue
175
288
 
176
289
  print(f"[DEBUG] {self.name}: Starting execution of task {task_id}")
177
290
  # Execute task using Langchain agent
178
291
  try:
179
- print(f"[DEBUG] {self.name}: Calling agent_executor.arun with task description")
180
- # Execute the task using the Langchain agent executor
181
- result = await self.agent_executor.arun(task_desc)
182
- print(f"[DEBUG] {self.name}: Agent execution completed. Result type: {type(result)}")
292
+ print(f"[DEBUG] {self.name}: Calling agent_executor.ainvoke with task description")
293
+ # Execute the task using the Langchain agent executor's ainvoke method
294
+ # Pass input AND agent_name in a dictionary matching the prompt's input variables
295
+ input_data = {
296
+ "input": task_desc,
297
+ "agent_name": self.name # Add agent_name here to indicate this agent as the executor (who's currently executing the task)
298
+ }
299
+ result_dict = await self.agent_executor.ainvoke(input_data)
300
+ print(f"[DEBUG] {self.name}: Agent execution completed. Full result: {result_dict}")
301
+ # Extract the final output string, typically under the 'output' key
302
+ if isinstance(result_dict, dict) and 'output' in result_dict:
303
+ result = result_dict['output']
304
+ print(f"[DEBUG] {self.name}: Extracted output: {result}")
305
+ else:
306
+ logger.warning(f"[{self.name}] Could not find 'output' key in agent result: {result_dict}. Using full dict as string.")
307
+ result = str(result_dict)
183
308
  except Exception as e:
184
309
  print(f"[ERROR] {self.name}: Agent execution failed: {e}")
185
310
  print(f"[ERROR] {self.name}: Error type: {type(e)}")
186
311
  traceback.print_exc() # Print the full traceback for detailed debugging
187
- # Provide a user-friendly error message as the result
312
+ # Assign error message to result variable for graceful failure
188
313
  result = f"Agent execution failed due to an error: {str(e)}"
189
314
 
190
315
  # Ensure result is always a string before sending
@@ -202,11 +327,20 @@ class LangchainMCPAdapter(MCPAgent):
202
327
  try:
203
328
  # --- FIX: Extract agent name from reply_to URL ---
204
329
  try:
205
- target_agent_name = reply_to.split('/')[-1]
330
+ # Handle both URL paths and direct agent names
331
+ if '/' in reply_to:
332
+ target_agent_name = reply_to.split('/')[-1]
333
+ else:
334
+ target_agent_name = reply_to
206
335
  except IndexError:
207
- print(f"[ERROR] {self.name}: Could not extract agent name from reply_to URL: {reply_to}")
336
+ print(f"[ERROR] {self.name}: Could not extract agent name from reply_to: {reply_to}")
208
337
  target_agent_name = reply_to # Fallback, though likely wrong
209
338
 
339
+ print(f"[DEBUG] Conversation Routing - Original sender: {reply_to}, Current agent: {self.name}, Final reply_to: {reply_to}")
340
+ print(f"[DEBUG] Derived target agent: {target_agent_name} from reply_to: {reply_to}")
341
+ print(f"[DEBUG] TASK_MESSAGE: {task}")
342
+ print(f"[DEBUG] Message Chain - From: {sender} -> To: {self.name} -> ReplyTo: {reply_to}")
343
+
210
344
  print(f"[DEBUG] {self.name}: Sending result to target agent: {target_agent_name} (extracted from {reply_to})")
211
345
  # --- END FIX ---
212
346
 
@@ -217,13 +351,12 @@ class LangchainMCPAdapter(MCPAgent):
217
351
  "task_id": task_id,
218
352
  "result": result_str,
219
353
  "sender": self.name,
220
- "original_message_id": task.get('message_id') # Include original message ID
354
+ "original_message_id": message_id # Include original message ID
221
355
  }
222
356
  )
223
357
  print(f"[DEBUG] {self.name}: Result sent successfully")
224
358
 
225
359
  # Acknowledge task completion using message_id
226
- message_id = task.get('message_id')
227
360
  if message_id:
228
361
  await self.transport.acknowledge_message(self.name, message_id)
229
362
  print(f"[DEBUG] {self.name}: Task {task_id} acknowledged with message_id {message_id}")