agentr 0.1.8__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.
- agentr/application.py +31 -9
- agentr/applications/github/app.py +71 -34
- agentr/applications/reddit/app.py +21 -1
- agentr/cli.py +31 -0
- agentr/server.py +8 -2
- agentr/test.py +2 -2
- {agentr-0.1.8.dist-info → agentr-0.1.9.dist-info}/METADATA +3 -3
- {agentr-0.1.8.dist-info → agentr-0.1.9.dist-info}/RECORD +11 -11
- {agentr-0.1.8.dist-info → agentr-0.1.9.dist-info}/WHEEL +0 -0
- {agentr-0.1.8.dist-info → agentr-0.1.9.dist-info}/entry_points.txt +0 -0
- {agentr-0.1.8.dist-info → agentr-0.1.9.dist-info}/licenses/LICENSE +0 -0
agentr/application.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
from abc import ABC
|
2
|
-
|
1
|
+
from abc import ABC
|
3
2
|
from loguru import logger
|
4
3
|
from agentr.exceptions import NotAuthorizedError
|
5
4
|
from agentr.integration import Integration
|
@@ -11,11 +10,25 @@ class Application(ABC):
|
|
11
10
|
"""
|
12
11
|
def __init__(self, name: str, **kwargs):
|
13
12
|
self.name = name
|
13
|
+
self.tools = []
|
14
14
|
|
15
|
-
|
15
|
+
def tool(self):
|
16
|
+
"""Decorator to register a method as a tool.
|
17
|
+
|
18
|
+
This decorator adds the decorated method to the application's tools list
|
19
|
+
and returns the original method unchanged.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
The decorated method
|
23
|
+
"""
|
24
|
+
def decorator(func):
|
25
|
+
if func not in self.tools:
|
26
|
+
self.tools.append(func)
|
27
|
+
return func
|
28
|
+
return decorator
|
29
|
+
|
16
30
|
def list_tools(self):
|
17
|
-
|
18
|
-
|
31
|
+
return self.tools
|
19
32
|
|
20
33
|
class APIApplication(Application):
|
21
34
|
"""
|
@@ -86,9 +99,18 @@ class APIApplication(Application):
|
|
86
99
|
logger.error(f"Error posting {url}: {e}")
|
87
100
|
raise e
|
88
101
|
|
102
|
+
def _patch(self, url, data):
|
103
|
+
try:
|
104
|
+
headers = self._get_headers()
|
105
|
+
response = httpx.patch(url, headers=headers, json=data)
|
106
|
+
response.raise_for_status()
|
107
|
+
return response
|
108
|
+
except NotAuthorizedError as e:
|
109
|
+
logger.warning(f"Authorization needed: {e.message}")
|
110
|
+
raise e
|
111
|
+
except Exception as e:
|
112
|
+
logger.error(f"Error patching {url}: {e}")
|
113
|
+
raise e
|
114
|
+
|
89
115
|
def validate(self):
|
90
|
-
pass
|
91
|
-
|
92
|
-
@abstractmethod
|
93
|
-
def list_tools(self):
|
94
116
|
pass
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from agentr.integration import Integration
|
2
2
|
from agentr.application import APIApplication
|
3
3
|
from loguru import logger
|
4
|
+
from typing import List, Dict, Any
|
4
5
|
|
5
6
|
class GithubApp(APIApplication):
|
6
7
|
def __init__(self, integration: Integration) -> None:
|
@@ -125,40 +126,38 @@ class GithubApp(APIApplication):
|
|
125
126
|
|
126
127
|
return result
|
127
128
|
|
128
|
-
def list_issues(self, repo_full_name: str,
|
129
|
+
def list_issues(self, repo_full_name: str, state: str = "open", assignee: str = None,
|
130
|
+
labels: str = None, per_page: int = 30, page: int = 1) -> List[Dict[str, Any]]:
|
129
131
|
"""List issues for a GitHub repository
|
130
132
|
|
131
133
|
Args:
|
132
134
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
135
|
+
state: State of issues to return (open, closed, all). Default: open
|
136
|
+
assignee: Filter by assignee. Use 'none' for no assignee, '*' for any assignee
|
137
|
+
labels: Comma-separated list of label names (e.g. "bug,ui,@high")
|
133
138
|
per_page: The number of results per page (max 100)
|
134
139
|
page: The page number of the results to fetch
|
135
140
|
|
136
141
|
Returns:
|
137
|
-
|
142
|
+
The complete GitHub API response
|
138
143
|
"""
|
139
144
|
repo_full_name = repo_full_name.strip()
|
140
|
-
url = f"{self.base_api_url}/{repo_full_name}/issues
|
145
|
+
url = f"{self.base_api_url}/{repo_full_name}/issues"
|
146
|
+
|
141
147
|
params = {
|
148
|
+
"state": state,
|
142
149
|
"per_page": per_page,
|
143
150
|
"page": page
|
144
151
|
}
|
145
|
-
response = self._get(url, params=params)
|
146
|
-
response.raise_for_status()
|
147
152
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
result = f"Issues for {repo_full_name} (Page {page}):\n\n"
|
153
|
-
for issue in issues:
|
154
|
-
issue_title = issue.get("issue", {}).get("title", "No Title")
|
155
|
-
issue_number = issue.get("issue", {}).get("number", "Unknown")
|
156
|
-
issue_state = issue.get("issue", {}).get("state", "Unknown")
|
157
|
-
issue_user = issue.get("issue", {}).get("user", {}).get("login", "Unknown")
|
158
|
-
|
159
|
-
result += f"- Issue #{issue_number}: {issue_title} (by {issue_user}, Status: {issue_state})\n"
|
153
|
+
if assignee:
|
154
|
+
params["assignee"] = assignee
|
155
|
+
if labels:
|
156
|
+
params["labels"] = labels
|
160
157
|
|
161
|
-
|
158
|
+
response = self._get(url, params=params)
|
159
|
+
response.raise_for_status()
|
160
|
+
return response.json()
|
162
161
|
|
163
162
|
def get_pull_request(self, repo_full_name: str, pull_number: int) -> str:
|
164
163
|
"""Get a specific pull request for a GitHub repository
|
@@ -191,45 +190,47 @@ class GithubApp(APIApplication):
|
|
191
190
|
|
192
191
|
return result
|
193
192
|
|
194
|
-
def create_pull_request(self, repo_full_name: str,
|
195
|
-
|
193
|
+
def create_pull_request(self, repo_full_name: str, head: str, base: str, title: str = None,
|
194
|
+
body: str = None, issue: int = None, maintainer_can_modify: bool = True,
|
195
|
+
draft: bool = False) -> Dict[str, Any]:
|
196
196
|
"""Create a new pull request for a GitHub repository
|
197
197
|
|
198
198
|
Args:
|
199
199
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
200
|
-
title: The title of the new pull request
|
201
200
|
head: The name of the branch where your changes are implemented
|
202
201
|
base: The name of the branch you want the changes pulled into
|
202
|
+
title: The title of the new pull request (required if issue is not specified)
|
203
203
|
body: The contents of the pull request
|
204
|
+
issue: An issue number to convert to a pull request. If specified, the issue's
|
205
|
+
title, body, and comments will be used for the pull request
|
204
206
|
maintainer_can_modify: Indicates whether maintainers can modify the pull request
|
205
207
|
draft: Indicates whether the pull request is a draft
|
206
208
|
|
207
209
|
Returns:
|
208
|
-
|
210
|
+
The complete GitHub API response
|
209
211
|
"""
|
210
212
|
repo_full_name = repo_full_name.strip()
|
211
213
|
url = f"{self.base_api_url}/{repo_full_name}/pulls"
|
212
214
|
|
213
215
|
pull_request_data = {
|
214
|
-
"title": title,
|
215
216
|
"head": head,
|
216
217
|
"base": base,
|
217
|
-
"body": body,
|
218
218
|
"maintainer_can_modify": maintainer_can_modify,
|
219
219
|
"draft": draft
|
220
220
|
}
|
221
221
|
|
222
|
+
if issue is not None:
|
223
|
+
pull_request_data["issue"] = issue
|
224
|
+
else:
|
225
|
+
if title is None:
|
226
|
+
raise ValueError("Either 'title' or 'issue' must be specified")
|
227
|
+
pull_request_data["title"] = title
|
228
|
+
if body is not None:
|
229
|
+
pull_request_data["body"] = body
|
230
|
+
|
222
231
|
response = self._post(url, pull_request_data)
|
223
232
|
response.raise_for_status()
|
224
|
-
|
225
|
-
pr = response.json()
|
226
|
-
pr_number = pr.get("number", "Unknown")
|
227
|
-
pr_url = pr.get("html_url", "")
|
228
|
-
|
229
|
-
return f"Successfully created pull request #{pr_number}:\n" \
|
230
|
-
f"Title: {title}\n" \
|
231
|
-
f"From: {head} → To: {base}\n" \
|
232
|
-
f"URL: {pr_url}"
|
233
|
+
return response.json()
|
233
234
|
|
234
235
|
def create_issue(self, repo_full_name: str, title: str, body: str = "", labels = None) -> str:
|
235
236
|
"""Create a new issue in a GitHub repository
|
@@ -313,7 +314,43 @@ class GithubApp(APIApplication):
|
|
313
314
|
|
314
315
|
return result
|
315
316
|
|
317
|
+
def update_issue(self, repo_full_name: str, issue_number: int, title: str = None,
|
318
|
+
body: str = None, assignee: str = None, state: str = None,
|
319
|
+
state_reason: str = None) -> Dict[str, Any]:
|
320
|
+
"""Update an issue in a GitHub repository
|
321
|
+
|
322
|
+
Args:
|
323
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
324
|
+
issue_number: The number that identifies the issue
|
325
|
+
title: The title of the issue
|
326
|
+
body: The contents of the issue
|
327
|
+
assignee: Username to assign to this issue
|
328
|
+
state: State of the issue (open or closed)
|
329
|
+
state_reason: Reason for state change (completed, not_planned, reopened, null)
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
The complete GitHub API response
|
333
|
+
"""
|
334
|
+
url = f"{self.base_api_url}/{repo_full_name}/issues/{issue_number}"
|
335
|
+
|
336
|
+
update_data = {}
|
337
|
+
if title is not None:
|
338
|
+
update_data["title"] = title
|
339
|
+
if body is not None:
|
340
|
+
update_data["body"] = body
|
341
|
+
if assignee is not None:
|
342
|
+
update_data["assignee"] = assignee
|
343
|
+
if state is not None:
|
344
|
+
update_data["state"] = state
|
345
|
+
if state_reason is not None:
|
346
|
+
update_data["state_reason"] = state_reason
|
347
|
+
|
348
|
+
response = self._patch(url, update_data)
|
349
|
+
response.raise_for_status()
|
350
|
+
return response.json()
|
351
|
+
|
316
352
|
def list_tools(self):
|
317
353
|
return [self.star_repository, self.list_commits, self.list_branches,
|
318
354
|
self.list_pull_requests, self.list_issues, self.get_pull_request,
|
319
|
-
self.create_pull_request, self.create_issue, self.
|
355
|
+
self.create_pull_request, self.create_issue, self.update_issue,
|
356
|
+
self.list_repo_activities]
|
@@ -1,11 +1,31 @@
|
|
1
|
+
import httpx
|
1
2
|
from agentr.application import APIApplication
|
2
3
|
from agentr.integration import Integration
|
4
|
+
from agentr.exceptions import NotAuthorizedError
|
3
5
|
from loguru import logger
|
4
6
|
|
5
7
|
class RedditApp(APIApplication):
|
6
8
|
def __init__(self, integration: Integration) -> None:
|
7
9
|
super().__init__(name="reddit", integration=integration)
|
8
|
-
self.base_api_url = "https://oauth.reddit.com"
|
10
|
+
self.base_api_url = "https://oauth.reddit.com"
|
11
|
+
|
12
|
+
def _post(self, url, data):
|
13
|
+
try:
|
14
|
+
headers = self._get_headers()
|
15
|
+
response = httpx.post(url, headers=headers, data=data)
|
16
|
+
response.raise_for_status()
|
17
|
+
return response
|
18
|
+
except NotAuthorizedError as e:
|
19
|
+
logger.warning(f"Authorization needed: {e.message}")
|
20
|
+
raise e
|
21
|
+
except httpx.HTTPStatusError as e:
|
22
|
+
if e.response.status_code == 429:
|
23
|
+
return e.response.text or "Rate limit exceeded. Please try again later."
|
24
|
+
else:
|
25
|
+
raise e
|
26
|
+
except Exception as e:
|
27
|
+
logger.error(f"Error posting {url}: {e}")
|
28
|
+
raise e
|
9
29
|
|
10
30
|
def _get_headers(self):
|
11
31
|
if not self.integration:
|
agentr/cli.py
CHANGED
@@ -59,6 +59,8 @@ def install(app_name: str):
|
|
59
59
|
|
60
60
|
with open(config_path, 'r') as f:
|
61
61
|
config = json.load(f)
|
62
|
+
if 'mcpServers' not in config:
|
63
|
+
config['mcpServers'] = {}
|
62
64
|
config['mcpServers']['agentr'] = {
|
63
65
|
"command": "uvx",
|
64
66
|
"args": ["agentr@latest", "run"],
|
@@ -66,6 +68,35 @@ def install(app_name: str):
|
|
66
68
|
"AGENTR_API_KEY": api_key
|
67
69
|
}
|
68
70
|
}
|
71
|
+
with open(config_path, 'w') as f:
|
72
|
+
json.dump(config, f, indent=4)
|
73
|
+
typer.echo("App installed successfully")
|
74
|
+
elif app_name == "cursor":
|
75
|
+
typer.echo(f"Installing mcp server for: {app_name}")
|
76
|
+
|
77
|
+
# Set up Cursor config path
|
78
|
+
config_path = Path.home() / ".cursor/mcp.json"
|
79
|
+
|
80
|
+
# Create config directory if it doesn't exist
|
81
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
82
|
+
|
83
|
+
# Create or load existing config
|
84
|
+
if config_path.exists():
|
85
|
+
with open(config_path, 'r') as f:
|
86
|
+
config = json.load(f)
|
87
|
+
else:
|
88
|
+
config = {}
|
89
|
+
|
90
|
+
if 'mcpServers' not in config:
|
91
|
+
config['mcpServers'] = {}
|
92
|
+
config['mcpServers']['agentr'] = {
|
93
|
+
"command": "uvx",
|
94
|
+
"args": ["agentr@latest", "run"],
|
95
|
+
"env": {
|
96
|
+
"AGENTR_API_KEY": api_key
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
69
100
|
with open(config_path, 'w') as f:
|
70
101
|
json.dump(config, f, indent=4)
|
71
102
|
typer.echo("App installed successfully")
|
agentr/server.py
CHANGED
@@ -80,7 +80,9 @@ class LocalServer(Server):
|
|
80
80
|
if app:
|
81
81
|
tools = app.list_tools()
|
82
82
|
for tool in tools:
|
83
|
-
|
83
|
+
name = app.name + "_" + tool.__name__
|
84
|
+
description = tool.__doc__
|
85
|
+
self.add_tool(tool, name=name, description=description)
|
84
86
|
|
85
87
|
|
86
88
|
|
@@ -114,7 +116,9 @@ class AgentRServer(Server):
|
|
114
116
|
"X-API-KEY": self.api_key
|
115
117
|
}
|
116
118
|
)
|
119
|
+
response.raise_for_status()
|
117
120
|
apps = response.json()
|
121
|
+
|
118
122
|
logger.info(f"Apps: {apps}")
|
119
123
|
return [AppConfig.model_validate(app) for app in apps]
|
120
124
|
|
@@ -125,4 +129,6 @@ class AgentRServer(Server):
|
|
125
129
|
if app:
|
126
130
|
tools = app.list_tools()
|
127
131
|
for tool in tools:
|
128
|
-
|
132
|
+
name = app.name + "_" + tool.__name__
|
133
|
+
description = tool.__doc__
|
134
|
+
self.add_tool(tool, name=name, description=description)
|
agentr/test.py
CHANGED
@@ -7,8 +7,8 @@ async def test():
|
|
7
7
|
tools = await mcp.list_tools()
|
8
8
|
from pprint import pprint
|
9
9
|
pprint(tools)
|
10
|
-
result = await mcp.call_tool("get_today_events", {})
|
11
|
-
print(result)
|
10
|
+
# result = await mcp.call_tool("get_today_events", {})
|
11
|
+
# print(result)
|
12
12
|
|
13
13
|
if __name__ == "__main__":
|
14
14
|
import asyncio
|
@@ -1,8 +1,8 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: agentr
|
3
|
-
Version: 0.1.
|
4
|
-
Summary:
|
5
|
-
Author-email: Manoj Bajaj <
|
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
|
@@ -1,18 +1,18 @@
|
|
1
1
|
agentr/__init__.py,sha256=LOWhgQayrQV7f5ro4rlBJ_6WevhbWIbjAOHnqP7b_-4,30
|
2
|
-
agentr/application.py,sha256=
|
3
|
-
agentr/cli.py,sha256=
|
2
|
+
agentr/application.py,sha256=1y3591Smesk8SOha7fa6aWFey7KNCqPWOP2uutwrk0w,3729
|
3
|
+
agentr/cli.py,sha256=SaJO502Eb-egiu6rXpxlmvDNkXeyYEGNirwSliOsbVI,3906
|
4
4
|
agentr/config.py,sha256=YTygfFJPUuL-epRuILvt5tc5ACzByWIFFNhpFwHlDCE,387
|
5
5
|
agentr/exceptions.py,sha256=hHlyXUZBjG4DfUurvqd0ZiruHC67gbpT6EHKxifwUhg,271
|
6
6
|
agentr/integration.py,sha256=hr2Edd2H-JG_CNe6xgYvfjy2BgKgFrBN2Grn8n4vfGs,5433
|
7
7
|
agentr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
agentr/server.py,sha256=
|
8
|
+
agentr/server.py,sha256=NgqUkTf0h8ot4DEWRQ0nH9winuZLF7E8F-3MPyKa1cs,5233
|
9
9
|
agentr/store.py,sha256=fB3uAaobnWf2ILcDBmg3ToDaqAIPYlLtmHBdpmkcGcI,1585
|
10
|
-
agentr/test.py,sha256=
|
10
|
+
agentr/test.py,sha256=yhTzUUvdqUXug7Og-cOAP2CQo9NPQ2HezLhIaSBcc7s,358
|
11
11
|
agentr/applications/__init__.py,sha256=huqhhfMkSMjcc3eqVAprW03Drr98OHbH2Rh0GwGQHjs,942
|
12
|
-
agentr/applications/github/app.py,sha256=
|
12
|
+
agentr/applications/github/app.py,sha256=ysW1gzP_PjR9sE5TciECziEkYZq2h-I0R6soRhngF-8,14014
|
13
13
|
agentr/applications/google_calendar/app.py,sha256=BlOBmdoEqifuOrG194BgjAvUkBhwvVeGM1HBYKv0bDU,19953
|
14
14
|
agentr/applications/google_mail/app.py,sha256=GWmJwgdpBKyNufsrHp2PkYzXNzyCb7XOHxZPYw6XaBA,23710
|
15
|
-
agentr/applications/reddit/app.py,sha256=
|
15
|
+
agentr/applications/reddit/app.py,sha256=LNkT5lV18lylvghABh_1FFSBcXGo4GwGeH6AiHojXaA,13282
|
16
16
|
agentr/applications/resend/app.py,sha256=ihRzP65bwoNIq3EzBqIghxgLZRxvy-LbHy-rER20ODo,1428
|
17
17
|
agentr/applications/tavily/app.py,sha256=D4FOhm2yxNbuTVHTo3T8ZsuE5AgwK874YutfYx2Njcw,1805
|
18
18
|
agentr/applications/zenquotes/app.py,sha256=4LjYeWdERI8ZMzkajOsDgy9IRTA9plUKem-rW4M03sA,607
|
@@ -23,8 +23,8 @@ agentr/integrations/api_key.py,sha256=cbX_8Je7nX3alIK1g0EqQcyNsNylQRQExGzsUARfyt
|
|
23
23
|
agentr/integrations/base.py,sha256=jZPWwzEHPJxS7onfscp8wwXt0bT66SC9tr75XQaueR4,1536
|
24
24
|
agentr/utils/bridge.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
25
|
agentr/utils/openapi.py,sha256=AEBMAuuDpjmoNjC3KWri6m2Pje0H_h397xE17deLLQg,9563
|
26
|
-
agentr-0.1.
|
27
|
-
agentr-0.1.
|
28
|
-
agentr-0.1.
|
29
|
-
agentr-0.1.
|
30
|
-
agentr-0.1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|