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.
@@ -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