agentr 0.1.7__py3-none-any.whl → 0.1.9__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,60 @@
1
+ from abc import ABC, abstractmethod
2
+ import os
3
+
4
+ from loguru import logger
5
+ from agentr.exceptions import NotAuthorizedError
6
+ from agentr.store import Store
7
+ import httpx
8
+
9
+
10
+
11
+
12
+ class Integration(ABC):
13
+ """Abstract base class for handling application integrations and authentication.
14
+
15
+ This class defines the interface for different types of integrations that handle
16
+ authentication and authorization with external services.
17
+
18
+ Args:
19
+ name: The name identifier for this integration
20
+ store: Optional Store instance for persisting credentials and other data
21
+
22
+ Attributes:
23
+ name: The name identifier for this integration
24
+ store: Store instance for persisting credentials and other data
25
+ """
26
+ def __init__(self, name: str, store: Store = None):
27
+ self.name = name
28
+ self.store = store
29
+
30
+ @abstractmethod
31
+ def authorize(self):
32
+ """Authorize the integration.
33
+
34
+ Returns:
35
+ str: Authorization URL.
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def get_credentials(self):
41
+ """Get credentials for the integration.
42
+
43
+ Returns:
44
+ dict: Credentials for the integration.
45
+
46
+ Raises:
47
+ NotAuthorizedError: If credentials are not found.
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def set_credentials(self, credentials: dict):
53
+ """Set credentials for the integration.
54
+
55
+ Args:
56
+ credentials: Credentials for the integration.
57
+ """
58
+ pass
59
+
60
+
agentr/server.py CHANGED
@@ -2,11 +2,15 @@ from abc import ABC, abstractmethod
2
2
  import httpx
3
3
  from mcp.server.fastmcp import FastMCP
4
4
  from agentr.applications import app_from_name
5
+ from agentr.exceptions import NotAuthorizedError
5
6
  from agentr.integration import AgentRIntegration, ApiKeyIntegration
6
7
  from agentr.store import EnvironmentStore, MemoryStore
7
8
  from agentr.config import AppConfig, IntegrationConfig, StoreConfig
8
9
  from loguru import logger
9
10
  import os
11
+ from typing import Any
12
+ from mcp.types import TextContent
13
+ from mcp.server.fastmcp.exceptions import ToolError
10
14
 
11
15
  class Server(FastMCP, ABC):
12
16
  """
@@ -21,6 +25,17 @@ class Server(FastMCP, ABC):
21
25
  def _load_apps(self):
22
26
  pass
23
27
 
28
+ async def call_tool(self, name: str, arguments: dict[str, Any]):
29
+ """Call a tool by name with arguments."""
30
+ try:
31
+ result = await super().call_tool(name, arguments)
32
+ return result
33
+ except ToolError as e:
34
+ raised_error = e.__cause__
35
+ if isinstance(raised_error, NotAuthorizedError):
36
+ return [TextContent(type="text", text=raised_error.message)]
37
+ else:
38
+ raise e
24
39
 
25
40
  class LocalServer(Server):
26
41
  """
@@ -65,7 +80,9 @@ class LocalServer(Server):
65
80
  if app:
66
81
  tools = app.list_tools()
67
82
  for tool in tools:
68
- self.add_tool(tool)
83
+ name = app.name + "_" + tool.__name__
84
+ description = tool.__doc__
85
+ self.add_tool(tool, name=name, description=description)
69
86
 
70
87
 
71
88
 
@@ -99,7 +116,9 @@ class AgentRServer(Server):
99
116
  "X-API-KEY": self.api_key
100
117
  }
101
118
  )
119
+ response.raise_for_status()
102
120
  apps = response.json()
121
+
103
122
  logger.info(f"Apps: {apps}")
104
123
  return [AppConfig.model_validate(app) for app in apps]
105
124
 
@@ -110,4 +129,6 @@ class AgentRServer(Server):
110
129
  if app:
111
130
  tools = app.list_tools()
112
131
  for tool in tools:
113
- self.add_tool(tool)
132
+ name = app.name + "_" + tool.__name__
133
+ description = tool.__doc__
134
+ self.add_tool(tool, name=name, description=description)
agentr/test.py CHANGED
@@ -1,39 +1,14 @@
1
- from agentr.server import LocalServer
2
- from agentr.store import MemoryStore
1
+ from agentr.server import AgentRServer
3
2
 
4
- store = MemoryStore()
5
- apps_list = [
6
- {
7
- "name": "tavily",
8
- "integration": {
9
- "name": "tavily_api_key",
10
- "type": "api_key",
11
- "store": {
12
- "type": "environment",
13
- }
14
- },
15
- },
16
- {
17
- "name": "zenquotes",
18
- "integration": None
19
- },
20
- {
21
- "name": "github",
22
- "integration": {
23
- "name": "github",
24
- "type": "agentr",
25
- }
26
- }
27
- ]
28
- mcp = LocalServer(name="Test Server", description="Test Server", apps_list=apps_list)
29
3
 
4
+ mcp = AgentRServer(name="Test Server", description="Test Server")
30
5
 
31
6
  async def test():
32
7
  tools = await mcp.list_tools()
33
8
  from pprint import pprint
34
9
  pprint(tools)
35
- result = await mcp.call_tool("star_repository", {"repo_full_name": "manojbajaj95/config"})
36
- print(result)
10
+ # result = await mcp.call_tool("get_today_events", {})
11
+ # print(result)
37
12
 
38
13
  if __name__ == "__main__":
39
14
  import asyncio
agentr/utils/openapi.py CHANGED
@@ -1,6 +1,26 @@
1
1
  import json
2
2
  import yaml
3
+ import re
3
4
  from pathlib import Path
5
+ from typing import Dict, Any
6
+
7
+
8
+ def convert_to_snake_case(identifier: str) -> str:
9
+ """
10
+ Convert a camelCase or PascalCase identifier to snake_case.
11
+
12
+ Args:
13
+ identifier (str): The string to convert
14
+
15
+ Returns:
16
+ str: The converted snake_case string
17
+ """
18
+ if not identifier:
19
+ return identifier
20
+ # Add underscore between lowercase and uppercase letters
21
+ result = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', identifier)
22
+ # Convert to lowercase
23
+ return result.lower()
4
24
 
5
25
 
6
26
  def load_schema(path: Path):
@@ -14,6 +34,7 @@ def load_schema(path: Path):
14
34
  else:
15
35
  return json.load(f)
16
36
 
37
+
17
38
  def generate_api_client(schema):
18
39
  """
19
40
  Generate a Python API client class from an OpenAPI schema.
@@ -25,25 +46,62 @@ def generate_api_client(schema):
25
46
  str: A string containing the Python code for the API client class.
26
47
  """
27
48
  methods = []
49
+ method_names = []
50
+
51
+ # Extract API info for naming and base URL
52
+ info = schema.get('info', {})
53
+ api_title = info.get('title', 'API')
54
+
55
+ # Get base URL from servers array if available
56
+ base_url = ""
57
+ servers = schema.get('servers', [])
58
+ if servers and isinstance(servers, list) and 'url' in servers[0]:
59
+ base_url = servers[0]['url'].rstrip('/')
60
+
61
+ # Create a clean class name from API title
62
+ if api_title:
63
+ # Convert API title to a clean class name
64
+ base_name = "".join(word.capitalize() for word in api_title.split())
65
+ clean_name = ''.join(c for c in base_name if c.isalnum())
66
+ class_name = f"{clean_name}App"
67
+ else:
68
+ class_name = "APIClient"
28
69
 
29
70
  # Iterate over paths and their operations
30
71
  for path, path_info in schema.get('paths', {}).items():
31
72
  for method in path_info:
32
73
  if method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']:
33
74
  operation = path_info[method]
34
- method_code = generate_method_code(path, method, operation)
75
+ method_code, func_name = generate_method_code(path, method, operation)
35
76
  methods.append(method_code)
77
+ method_names.append(func_name)
78
+
79
+ # Generate list_tools method with all the function names
80
+ tools_list = ",\n ".join([f"self.{name}" for name in method_names])
81
+ list_tools_method = f""" def list_tools(self):
82
+ return [
83
+ {tools_list}
84
+ ]"""
85
+
86
+ # Generate class imports
87
+ imports = [
88
+ "from agentr.application import APIApplication",
89
+ "from agentr.integration import Integration"
90
+ ]
36
91
 
37
92
  # Construct the class code
38
93
  class_code = (
39
- "import requests\n\n"
40
- "class APIClient:\n"
41
- " def __init__(self, base_url):\n"
42
- " self.base_url = base_url\n\n" +
43
- '\n\n'.join(methods)
94
+ "\n".join(imports) + "\n\n"
95
+ f"class {class_name}(APIApplication):\n"
96
+ f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
97
+ f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
98
+ f" self.base_url = \"{base_url}\"\n\n" +
99
+ '\n\n'.join(methods) + "\n\n" +
100
+ list_tools_method + "\n"
44
101
  )
45
102
  return class_code
46
103
 
104
+
47
105
  def generate_method_code(path, method, operation):
48
106
  """
49
107
  Generate the code for a single API method.
@@ -54,11 +112,13 @@ def generate_method_code(path, method, operation):
54
112
  operation (dict): The operation details from the schema.
55
113
 
56
114
  Returns:
57
- str: The Python code for the method.
115
+ tuple: (method_code, func_name) - The Python code for the method and its name.
58
116
  """
59
117
  # Determine function name
60
118
  if 'operationId' in operation:
61
- func_name = operation['operationId']
119
+ raw_name = operation['operationId']
120
+ cleaned_name = raw_name.replace('.', '_').replace('-', '_')
121
+ func_name = convert_to_snake_case(cleaned_name)
62
122
  else:
63
123
  # Generate name from path and method
64
124
  path_parts = path.strip('/').split('/')
@@ -82,43 +142,71 @@ def generate_method_code(path, method, operation):
82
142
  args.append(param['name'])
83
143
  else:
84
144
  args.append(f"{param['name']}=None")
145
+
85
146
  if has_body:
86
- args.append('body' if body_required else 'body=None')
87
- signature = f"def {func_name}(self, {', '.join(args)}):"
147
+ args.append('request_body' if body_required else 'request_body=None')
148
+
149
+ signature = f" def {func_name}(self, {', '.join(args)}) -> Dict[str, Any]:"
88
150
 
89
151
  # Build method body
90
152
  body_lines = []
91
153
 
154
+ # Validate required parameters
155
+ for param in parameters:
156
+ if param.get('required', False):
157
+ body_lines.append(f" if {param['name']} is None:")
158
+ body_lines.append(f" raise ValueError(\"Missing required parameter '{param['name']}'\")")
159
+
160
+ # Validate required body
161
+ if has_body and body_required:
162
+ body_lines.append(" if request_body is None:")
163
+ body_lines.append(" raise ValueError(\"Missing required request body\")")
164
+
92
165
  # Path parameters
93
166
  path_params = [p for p in parameters if p['in'] == 'path']
94
167
  path_params_dict = ', '.join([f"'{p['name']}': {p['name']}" for p in path_params])
95
- body_lines.append(f" path_params = {{{path_params_dict}}}")
168
+ body_lines.append(f" path_params = {{{path_params_dict}}}")
169
+
170
+ # Format URL
171
+ body_lines.append(f" url = f\"{{self.base_url}}{path}\".format_map(path_params)")
96
172
 
97
173
  # Query parameters
98
174
  query_params = [p for p in parameters if p['in'] == 'query']
99
175
  query_params_items = ', '.join([f"('{p['name']}', {p['name']})" for p in query_params])
100
176
  body_lines.append(
101
- f" query_params = {{k: v for k, v in [{query_params_items}] if v is not None}}"
177
+ f" query_params = {{k: v for k, v in [{query_params_items}] if v is not None}}"
102
178
  )
103
179
 
104
- # Format URL
105
- body_lines.append(f" url = f\"{{self.base_url}}{path}\".format_map(path_params)")
106
-
107
- # Make HTTP request
108
- method_func = method.lower()
180
+ # Request body handling for JSON
109
181
  if has_body:
110
- body_lines.append(" if body is not None:")
111
- body_lines.append(f" response = requests.{method_func}(url, params=query_params, json=body)")
112
- body_lines.append(" else:")
113
- body_lines.append(f" response = requests.{method_func}(url, params=query_params)")
182
+ body_lines.append(" json_body = request_body if request_body is not None else None")
183
+
184
+ # Make HTTP request using the proper method
185
+ method_lower = method.lower()
186
+ if method_lower == 'get':
187
+ body_lines.append(" response = self._get(url, params=query_params)")
188
+ elif method_lower == 'post':
189
+ if has_body:
190
+ body_lines.append(" response = self._post(url, data=json_body)")
191
+ else:
192
+ body_lines.append(" response = self._post(url, data={})")
193
+ elif method_lower == 'put':
194
+ if has_body:
195
+ body_lines.append(" response = self._put(url, data=json_body)")
196
+ else:
197
+ body_lines.append(" response = self._put(url, data={})")
198
+ elif method_lower == 'delete':
199
+ body_lines.append(" response = self._delete(url)")
114
200
  else:
115
- body_lines.append(f" response = requests.{method_func}(url, params=query_params)")
201
+ body_lines.append(f" response = self._{method_lower}(url, data={{}})")
116
202
 
117
203
  # Handle response
118
- body_lines.append(" response.raise_for_status()")
119
- body_lines.append(" return response.json()")
204
+ body_lines.append(" response.raise_for_status()")
205
+ body_lines.append(" return response.json()")
120
206
 
121
- return signature + '\n' + '\n'.join(body_lines)
207
+ method_code = signature + '\n' + '\n'.join(body_lines)
208
+ return method_code, func_name
209
+
122
210
 
123
211
  # Example usage
124
212
  if __name__ == "__main__":
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentr
3
- Version: 0.1.7
4
- Summary: A python framework to build MCP servers
5
- Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
3
+ Version: 0.1.9
4
+ Summary: An MCP middleware to connect to 400+ apps
5
+ Author-email: Manoj Bajaj <manoj@agentr.dev>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
8
8
  Requires-Dist: loguru>=0.7.3
@@ -0,0 +1,30 @@
1
+ agentr/__init__.py,sha256=LOWhgQayrQV7f5ro4rlBJ_6WevhbWIbjAOHnqP7b_-4,30
2
+ agentr/application.py,sha256=1y3591Smesk8SOha7fa6aWFey7KNCqPWOP2uutwrk0w,3729
3
+ agentr/cli.py,sha256=SaJO502Eb-egiu6rXpxlmvDNkXeyYEGNirwSliOsbVI,3906
4
+ agentr/config.py,sha256=YTygfFJPUuL-epRuILvt5tc5ACzByWIFFNhpFwHlDCE,387
5
+ agentr/exceptions.py,sha256=hHlyXUZBjG4DfUurvqd0ZiruHC67gbpT6EHKxifwUhg,271
6
+ agentr/integration.py,sha256=hr2Edd2H-JG_CNe6xgYvfjy2BgKgFrBN2Grn8n4vfGs,5433
7
+ agentr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ agentr/server.py,sha256=NgqUkTf0h8ot4DEWRQ0nH9winuZLF7E8F-3MPyKa1cs,5233
9
+ agentr/store.py,sha256=fB3uAaobnWf2ILcDBmg3ToDaqAIPYlLtmHBdpmkcGcI,1585
10
+ agentr/test.py,sha256=yhTzUUvdqUXug7Og-cOAP2CQo9NPQ2HezLhIaSBcc7s,358
11
+ agentr/applications/__init__.py,sha256=huqhhfMkSMjcc3eqVAprW03Drr98OHbH2Rh0GwGQHjs,942
12
+ agentr/applications/github/app.py,sha256=ysW1gzP_PjR9sE5TciECziEkYZq2h-I0R6soRhngF-8,14014
13
+ agentr/applications/google_calendar/app.py,sha256=BlOBmdoEqifuOrG194BgjAvUkBhwvVeGM1HBYKv0bDU,19953
14
+ agentr/applications/google_mail/app.py,sha256=GWmJwgdpBKyNufsrHp2PkYzXNzyCb7XOHxZPYw6XaBA,23710
15
+ agentr/applications/reddit/app.py,sha256=LNkT5lV18lylvghABh_1FFSBcXGo4GwGeH6AiHojXaA,13282
16
+ agentr/applications/resend/app.py,sha256=ihRzP65bwoNIq3EzBqIghxgLZRxvy-LbHy-rER20ODo,1428
17
+ agentr/applications/tavily/app.py,sha256=D4FOhm2yxNbuTVHTo3T8ZsuE5AgwK874YutfYx2Njcw,1805
18
+ agentr/applications/zenquotes/app.py,sha256=4LjYeWdERI8ZMzkajOsDgy9IRTA9plUKem-rW4M03sA,607
19
+ agentr/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
20
+ agentr/integrations/__init__.py,sha256=_jGeykaUjY97rklbUIGbY7oWQXhtYGRUdYszNRw_NOg,232
21
+ agentr/integrations/agentr.py,sha256=MWUHItiRvs-9PNi8jcPHXgJ-cxCGVpzIpW7VNWWIoAs,3364
22
+ agentr/integrations/api_key.py,sha256=cbX_8Je7nX3alIK1g0EqQcyNsNylQRQExGzsUARfytU,552
23
+ agentr/integrations/base.py,sha256=jZPWwzEHPJxS7onfscp8wwXt0bT66SC9tr75XQaueR4,1536
24
+ agentr/utils/bridge.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ agentr/utils/openapi.py,sha256=AEBMAuuDpjmoNjC3KWri6m2Pje0H_h397xE17deLLQg,9563
26
+ agentr-0.1.9.dist-info/METADATA,sha256=zk7QncR1lOritziohTCOJrPhZFm62ubrAztkdmeNxO8,4384
27
+ agentr-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ agentr-0.1.9.dist-info/entry_points.txt,sha256=13fGFeVhgF6_8T-VFiIkNxYO7gDQaUwwTcUNWdvaLQg,42
29
+ agentr-0.1.9.dist-info/licenses/LICENSE,sha256=CPslwL9mT3MH-lEljRJQHKe646096G-szURVmOD18Lc,1063
30
+ agentr-0.1.9.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- agentr/__init__.py,sha256=LOWhgQayrQV7f5ro4rlBJ_6WevhbWIbjAOHnqP7b_-4,30
2
- agentr/application.py,sha256=r2o4PP0uDBFTkziseuOh7AobcVqnEWDoYJxN_UA7UVo,2932
3
- agentr/cli.py,sha256=z0QOdyiBcypNE_npN2lYIkEcYZXG8Ji0027IcEuhDTs,2880
4
- agentr/config.py,sha256=YTygfFJPUuL-epRuILvt5tc5ACzByWIFFNhpFwHlDCE,387
5
- agentr/exceptions.py,sha256=hHlyXUZBjG4DfUurvqd0ZiruHC67gbpT6EHKxifwUhg,271
6
- agentr/integration.py,sha256=BmtPpn2lXV7i6k6Uqu5g-LWpD9wRiMz7R3SgztLAktc,2663
7
- agentr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- agentr/server.py,sha256=vg9YlApxlLvxcIn9h_Rz2IgatXv7aBj0pTTb6XWMRN4,4279
9
- agentr/store.py,sha256=fB3uAaobnWf2ILcDBmg3ToDaqAIPYlLtmHBdpmkcGcI,1585
10
- agentr/test.py,sha256=wtupsEMrn-Q9Ocjlhelk4YrVSrWMM5vwMBhnvepEaHo,917
11
- agentr/applications/__init__.py,sha256=huqhhfMkSMjcc3eqVAprW03Drr98OHbH2Rh0GwGQHjs,942
12
- agentr/applications/github/app.py,sha256=N-f9TX5btcUDr19H-eTG7NlFlb25fGENLZ07fFfQBg4,17301
13
- agentr/applications/google_calendar/app.py,sha256=3mpqP0MsTYsJyZDF2Skgs1VLfLRr6E7OSOhbcAQ_L3E,25710
14
- agentr/applications/google_mail/app.py,sha256=GWmJwgdpBKyNufsrHp2PkYzXNzyCb7XOHxZPYw6XaBA,23710
15
- agentr/applications/reddit/app.py,sha256=iNkSRVGScj7Mae3e8N-Mkov_hvZrvH5NynbpO8mKToQ,842
16
- agentr/applications/resend/app.py,sha256=ihRzP65bwoNIq3EzBqIghxgLZRxvy-LbHy-rER20ODo,1428
17
- agentr/applications/tavily/app.py,sha256=D4FOhm2yxNbuTVHTo3T8ZsuE5AgwK874YutfYx2Njcw,1805
18
- agentr/applications/zenquotes/app.py,sha256=4LjYeWdERI8ZMzkajOsDgy9IRTA9plUKem-rW4M03sA,607
19
- agentr/utils/bridge.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- agentr/utils/openapi.py,sha256=DT4jcWue_Fx0J-wg7T53w3uMb5CIzn4u2QswmtpSFVU,6280
21
- agentr-0.1.7.dist-info/METADATA,sha256=BUoooDf1WgcRVE-rVBD3W78W-FrhSGnvAmhbYbuV-TY,4388
22
- agentr-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- agentr-0.1.7.dist-info/entry_points.txt,sha256=13fGFeVhgF6_8T-VFiIkNxYO7gDQaUwwTcUNWdvaLQg,42
24
- agentr-0.1.7.dist-info/licenses/LICENSE,sha256=CPslwL9mT3MH-lEljRJQHKe646096G-szURVmOD18Lc,1063
25
- agentr-0.1.7.dist-info/RECORD,,
File without changes