agent-mcp 0.1.2__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.2.dist-info/METADATA +0 -475
- agent_mcp-0.1.2.dist-info/RECORD +0 -5
- agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple one-line integration decorator for connecting agents to the AgentMCP network.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import aiohttp
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Optional, Any, Callable, Tuple, Dict
|
|
13
|
+
from .mcp_agent import MCPAgent
|
|
14
|
+
from .mcp_transport import HTTPTransport
|
|
15
|
+
|
|
16
|
+
# Default to environment variable or fallback to localhost
|
|
17
|
+
DEFAULT_MCP_SERVER = os.getenv('MCP_SERVER_URL', "https://mcp-server-ixlfhxquwq-ew.a.run.app")
|
|
18
|
+
|
|
19
|
+
# Set up logging
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Standalone registration function (no longer primary path for decorator, but keep for potential direct use)
|
|
23
|
+
async def register_with_server(agent_id: str, agent_info: dict, server_url: str = DEFAULT_MCP_SERVER):
|
|
24
|
+
"""Register an agent with the MCP server"""
|
|
25
|
+
# Revert to using the default ClientSession
|
|
26
|
+
async with aiohttp.ClientSession() as session:
|
|
27
|
+
async with session.post(
|
|
28
|
+
f"{server_url}/register",
|
|
29
|
+
json={"agent_id": agent_id, "info": agent_info}
|
|
30
|
+
) as response:
|
|
31
|
+
data = await response.json()
|
|
32
|
+
# Parse the response body which is a JSON string
|
|
33
|
+
if isinstance(data, dict) and 'body' in data:
|
|
34
|
+
try:
|
|
35
|
+
body = json.loads(data['body'])
|
|
36
|
+
return body
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
return data
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
class MCPAgentDecorator:
|
|
42
|
+
"""Decorator class to wrap a function as an MCP agent"""
|
|
43
|
+
def __init__(self, agent_function: Callable, agent_class: type, mcp_id: Optional[str] = None, mcp_server: Optional[str] = None, tools: Optional[list] = None, version: Optional[str] = "1.0"):
|
|
44
|
+
# Store original function and configuration
|
|
45
|
+
self._original_agent_function = agent_function
|
|
46
|
+
self._agent_class = agent_class
|
|
47
|
+
self._mcp_id_provided = mcp_id
|
|
48
|
+
self._mcp_server = mcp_server or DEFAULT_MCP_SERVER
|
|
49
|
+
self._tools_funcs = tools or []
|
|
50
|
+
self._mcp_version = version
|
|
51
|
+
|
|
52
|
+
# --- Configuration that will be set on the INSTANCE ---
|
|
53
|
+
# Note: We use a separate __call__ method or similar pattern later
|
|
54
|
+
# to actually create the instance and set these.
|
|
55
|
+
# For now, we define the methods the decorator will add.
|
|
56
|
+
|
|
57
|
+
# Methods to be added to the decorated class
|
|
58
|
+
|
|
59
|
+
def _initialize_mcp_instance(self, instance):
|
|
60
|
+
"""Called when an instance of the decorated class is created."""
|
|
61
|
+
instance._mcp = MCPAgent(
|
|
62
|
+
name=self._agent_class.__name__,
|
|
63
|
+
system_message=None # Or derive from docstring?
|
|
64
|
+
)
|
|
65
|
+
instance._mcp_id = self._mcp_id_provided or str(uuid.uuid4())
|
|
66
|
+
instance._registered_agent_id: Optional[str] = None
|
|
67
|
+
instance._mcp_tools = {}
|
|
68
|
+
instance.transport = HTTPTransport.from_url(self._mcp_server)
|
|
69
|
+
instance.context_store = {} # Simple dict for context
|
|
70
|
+
|
|
71
|
+
# Process tools provided to the decorator
|
|
72
|
+
if self._tools_funcs:
|
|
73
|
+
for tool_func in self._tools_funcs:
|
|
74
|
+
# Ensure the tool_func is bound to the instance if it's a method
|
|
75
|
+
bound_tool_func = tool_func.__get__(instance, self._agent_class)
|
|
76
|
+
|
|
77
|
+
tool_name = getattr(bound_tool_func, '_mcp_tool_name', bound_tool_func.__name__)
|
|
78
|
+
tool_desc = getattr(bound_tool_func, '_mcp_tool_description',
|
|
79
|
+
bound_tool_func.__doc__ or f"Call {tool_name}")
|
|
80
|
+
|
|
81
|
+
instance._mcp_tools[tool_name] = {
|
|
82
|
+
'func': bound_tool_func,
|
|
83
|
+
'description': tool_desc
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async def connect(self): # 'self' here refers to the instance of the decorated class
|
|
87
|
+
"""Connects the decorated agent: registers and starts transport polling."""
|
|
88
|
+
if not hasattr(self, 'transport') or self.transport is None:
|
|
89
|
+
raise RuntimeError("MCP Transport not initialized. Did you call __init__?")
|
|
90
|
+
|
|
91
|
+
agent_info = {
|
|
92
|
+
"name": self._mcp.name,
|
|
93
|
+
"type": self.__class__.__name__, # Use instance's class name
|
|
94
|
+
"tools": list(self._mcp_tools.keys()),
|
|
95
|
+
"version": self._mcp_version # Use the version stored on the instance
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- Begin integrated registration logic (mimicking HTTPTransport) ---
|
|
99
|
+
connector = aiohttp.TCPConnector(ssl=False)
|
|
100
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
101
|
+
register_url = f"{self.transport.remote_url}/register"
|
|
102
|
+
|
|
103
|
+
logger.info(f"Attempting registration for {self._mcp_id} at {register_url}")
|
|
104
|
+
|
|
105
|
+
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
|
106
|
+
try:
|
|
107
|
+
async with session.post(
|
|
108
|
+
register_url,
|
|
109
|
+
json={"agent_id": self._mcp_id, "info": agent_info}
|
|
110
|
+
) as response:
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
data = await response.json()
|
|
113
|
+
logger.debug(f"Raw registration response data: {data}")
|
|
114
|
+
|
|
115
|
+
result = None
|
|
116
|
+
token = None
|
|
117
|
+
if isinstance(data, dict) and 'body' in data:
|
|
118
|
+
try:
|
|
119
|
+
body = json.loads(data['body'])
|
|
120
|
+
result = body
|
|
121
|
+
if isinstance(result, dict) and 'token' in result:
|
|
122
|
+
token = result['token']
|
|
123
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
124
|
+
logger.warning(f"Failed to decode 'body' from registration response: {data.get('body')}. Error: {e}")
|
|
125
|
+
result = data
|
|
126
|
+
else:
|
|
127
|
+
result = data
|
|
128
|
+
|
|
129
|
+
if not token and isinstance(result, dict) and 'token' in result:
|
|
130
|
+
token = result['token']
|
|
131
|
+
|
|
132
|
+
if not token:
|
|
133
|
+
raise ValueError(f"No token could be extracted from registration response: {result}")
|
|
134
|
+
|
|
135
|
+
self._registered_agent_id = result.get('agent_id')
|
|
136
|
+
if not self._registered_agent_id:
|
|
137
|
+
raise ValueError(f"Registration response missing 'agent_id': {result}")
|
|
138
|
+
|
|
139
|
+
print(f"Registered with MCP server (result parsed): {result}")
|
|
140
|
+
|
|
141
|
+
self.transport.token = token
|
|
142
|
+
self.transport.auth_token = token
|
|
143
|
+
print(f"Token set for agent {self._registered_agent_id}")
|
|
144
|
+
|
|
145
|
+
# Connect and start polling for messages
|
|
146
|
+
await self.transport.connect(agent_name=self._registered_agent_id, token=token)
|
|
147
|
+
|
|
148
|
+
except aiohttp.ClientResponseError as e:
|
|
149
|
+
error_body = await response.text()
|
|
150
|
+
logger.error(f"HTTP error during registration: Status={e.status}, Message='{e.message}', URL={e.request_info.url}, Response Body: {error_body[:500]}")
|
|
151
|
+
print(f"HTTP error during registration: {e.status} - {e.message}. Check logs for details.")
|
|
152
|
+
raise
|
|
153
|
+
except aiohttp.ClientConnectionError as e:
|
|
154
|
+
logger.error(f"Connection error during registration to {register_url}: {e}")
|
|
155
|
+
print(f"Connection error during registration: {e}")
|
|
156
|
+
raise
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.exception(f"Unexpected error during registration/connection for agent {self._mcp_id}: {e}")
|
|
159
|
+
print(f"Error during registration/connection: {e}")
|
|
160
|
+
raise
|
|
161
|
+
|
|
162
|
+
async def disconnect(self): # 'self' here refers to the instance
|
|
163
|
+
"""Disconnects the transport layer."""
|
|
164
|
+
if hasattr(self, 'transport') and self.transport:
|
|
165
|
+
await self.transport.disconnect()
|
|
166
|
+
else:
|
|
167
|
+
logger.warning("Attempted to disconnect but transport was not initialized.")
|
|
168
|
+
|
|
169
|
+
def get_id(self) -> Optional[str]: # 'self' here refers to the instance
|
|
170
|
+
"""Returns the agent ID assigned by the server after registration."""
|
|
171
|
+
return self._registered_agent_id
|
|
172
|
+
|
|
173
|
+
async def send_message(self, target: str, message: Any): # 'self' here refers to the instance
|
|
174
|
+
"""Sends a message via the transport layer."""
|
|
175
|
+
if hasattr(self, 'transport') and self.transport:
|
|
176
|
+
await self.transport.send_message(target, message)
|
|
177
|
+
else:
|
|
178
|
+
raise RuntimeError("Transport not initialized, cannot send message.")
|
|
179
|
+
|
|
180
|
+
async def receive_message(self, timeout: float = 1.0) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
181
|
+
"""Receives a message via the transport layer."""
|
|
182
|
+
if hasattr(self, 'transport') and self.transport:
|
|
183
|
+
return await self.transport.receive_message(timeout=timeout)
|
|
184
|
+
else:
|
|
185
|
+
logger.warning("Attempted to receive message but transport was not initialized.")
|
|
186
|
+
return None, None
|
|
187
|
+
|
|
188
|
+
# This method makes the decorator work on classes
|
|
189
|
+
def __call__(self, Cls):
|
|
190
|
+
# Modify the class's __init__ to include our initialization
|
|
191
|
+
original_init = Cls.__init__
|
|
192
|
+
|
|
193
|
+
decorator_self = self # Capture the decorator instance itself
|
|
194
|
+
|
|
195
|
+
def new_init(instance, *args, **kwargs):
|
|
196
|
+
decorator_self._initialize_mcp_instance(instance) # Use decorator's init logic
|
|
197
|
+
original_init(instance, *args, **kwargs) # Call original class __init__
|
|
198
|
+
|
|
199
|
+
# Store the version on the instance too, might be useful
|
|
200
|
+
instance._mcp_version = decorator_self._mcp_version
|
|
201
|
+
|
|
202
|
+
Cls.__init__ = new_init
|
|
203
|
+
|
|
204
|
+
# Add the methods directly to the class
|
|
205
|
+
# Assign the unbound methods from the decorator class itself
|
|
206
|
+
Cls.connect = MCPAgentDecorator.connect
|
|
207
|
+
Cls.disconnect = MCPAgentDecorator.disconnect
|
|
208
|
+
Cls.get_id = MCPAgentDecorator.get_id
|
|
209
|
+
Cls.send_message = MCPAgentDecorator.send_message
|
|
210
|
+
Cls.receive_message = MCPAgentDecorator.receive_message
|
|
211
|
+
|
|
212
|
+
# Add properties for MCP attributes if needed
|
|
213
|
+
# Cls.mcp_tools = property(lambda instance: instance._mcp_tools)
|
|
214
|
+
# Cls.context_store = property(lambda instance: instance.context_store)
|
|
215
|
+
# Cls.mcp_id = property(lambda instance: instance._mcp_id)
|
|
216
|
+
|
|
217
|
+
return Cls
|
|
218
|
+
|
|
219
|
+
# Global decorator instance (adjust if configuration needs to vary per use)
|
|
220
|
+
def mcp_agent(agent_class=None, mcp_id: Optional[str] = None, mcp_server: Optional[str] = None, tools: Optional[list] = None, version: Optional[str] = "1.0"):
|
|
221
|
+
"""Decorator to turn a class into an MCP agent."""
|
|
222
|
+
|
|
223
|
+
if agent_class is None:
|
|
224
|
+
# Called with arguments like @mcp_agent(mcp_id="...")
|
|
225
|
+
return functools.partial(mcp_agent, mcp_id=mcp_id, mcp_server=mcp_server, tools=tools, version=version)
|
|
226
|
+
else:
|
|
227
|
+
# Called as @mcp_agent
|
|
228
|
+
decorator = MCPAgentDecorator(None, agent_class, mcp_id, mcp_server, tools, version)
|
|
229
|
+
return decorator(agent_class) # Apply the decorator logic via __call__
|
|
230
|
+
|
|
231
|
+
def register_tool(name: str, description: Optional[str] = None):
|
|
232
|
+
"""
|
|
233
|
+
Decorator to register a method as an MCP tool.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name (str): Name of the tool
|
|
237
|
+
description (str, optional): Description of what the tool does
|
|
238
|
+
|
|
239
|
+
Usage:
|
|
240
|
+
@register_tool("greet", "Send a greeting message")
|
|
241
|
+
def greet(self, message):
|
|
242
|
+
return f"Hello, {message}!"
|
|
243
|
+
"""
|
|
244
|
+
def decorator(func: Callable) -> Callable:
|
|
245
|
+
@functools.wraps(func)
|
|
246
|
+
def wrapper(self, *args, **kwargs):
|
|
247
|
+
if isinstance(self, MCPAgent):
|
|
248
|
+
return func(self, *args, **kwargs)
|
|
249
|
+
raise TypeError("register_tool can only be used with MCP agents")
|
|
250
|
+
|
|
251
|
+
# Store tool metadata
|
|
252
|
+
wrapper._mcp_tool = True
|
|
253
|
+
wrapper._mcp_tool_name = name
|
|
254
|
+
wrapper._mcp_tool_description = description or func.__doc__ or f"Call {name}"
|
|
255
|
+
|
|
256
|
+
return wrapper
|
|
257
|
+
return decorator
|