universal-mcp 0.1.1__py3-none-any.whl → 0.1.2__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.
- universal_mcp/applications/__init__.py +23 -28
- universal_mcp/applications/application.py +13 -8
- universal_mcp/applications/e2b/app.py +74 -0
- universal_mcp/applications/firecrawl/app.py +381 -0
- universal_mcp/applications/github/README.md +35 -0
- universal_mcp/applications/github/app.py +133 -100
- universal_mcp/applications/google_calendar/app.py +170 -139
- universal_mcp/applications/google_mail/app.py +185 -160
- universal_mcp/applications/markitdown/app.py +32 -0
- universal_mcp/applications/notion/README.md +32 -0
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +415 -0
- universal_mcp/applications/reddit/app.py +112 -71
- universal_mcp/applications/resend/app.py +3 -8
- universal_mcp/applications/serp/app.py +84 -0
- universal_mcp/applications/tavily/app.py +11 -10
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/cli.py +98 -16
- universal_mcp/config.py +20 -3
- universal_mcp/exceptions.py +1 -3
- universal_mcp/integrations/__init__.py +6 -2
- universal_mcp/integrations/agentr.py +26 -24
- universal_mcp/integrations/integration.py +72 -35
- universal_mcp/servers/__init__.py +21 -1
- universal_mcp/servers/server.py +77 -44
- universal_mcp/stores/__init__.py +15 -2
- universal_mcp/stores/store.py +123 -13
- universal_mcp/utils/__init__.py +1 -0
- universal_mcp/utils/api_generator.py +269 -0
- universal_mcp/utils/docgen.py +360 -0
- universal_mcp/utils/installation.py +17 -2
- universal_mcp/utils/openapi.py +216 -111
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/METADATA +23 -5
- universal_mcp-0.1.2.dist-info/RECORD +40 -0
- universal_mcp-0.1.1.dist-info/RECORD +0 -29
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
|
|
1
|
-
from
|
2
|
-
|
1
|
+
from typing import Any
|
2
|
+
|
3
3
|
from loguru import logger
|
4
|
-
|
4
|
+
|
5
|
+
from universal_mcp.applications.application import APIApplication
|
6
|
+
from universal_mcp.integrations import Integration
|
7
|
+
|
5
8
|
|
6
9
|
class GithubApp(APIApplication):
|
7
10
|
def __init__(self, integration: Integration) -> None:
|
@@ -16,22 +19,22 @@ class GithubApp(APIApplication):
|
|
16
19
|
return credentials["headers"]
|
17
20
|
return {
|
18
21
|
"Authorization": f"Bearer {credentials['access_token']}",
|
19
|
-
"Accept": "application/vnd.github.v3+json"
|
22
|
+
"Accept": "application/vnd.github.v3+json",
|
20
23
|
}
|
21
|
-
|
24
|
+
|
22
25
|
def star_repository(self, repo_full_name: str) -> str:
|
23
26
|
"""Star a GitHub repository
|
24
|
-
|
27
|
+
|
25
28
|
Args:
|
26
29
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
27
|
-
|
30
|
+
|
28
31
|
Returns:
|
29
|
-
|
32
|
+
|
30
33
|
A confirmation message
|
31
34
|
"""
|
32
35
|
url = f"https://api.github.com/user/starred/{repo_full_name}"
|
33
36
|
response = self._put(url, data={})
|
34
|
-
|
37
|
+
|
35
38
|
if response.status_code == 204:
|
36
39
|
return f"Successfully starred repository {repo_full_name}"
|
37
40
|
elif response.status_code == 404:
|
@@ -39,14 +42,13 @@ class GithubApp(APIApplication):
|
|
39
42
|
else:
|
40
43
|
logger.error(response.text)
|
41
44
|
return f"Error starring repository: {response.text}"
|
42
|
-
|
43
45
|
|
44
46
|
def list_commits(self, repo_full_name: str) -> str:
|
45
47
|
"""List recent commits for a GitHub repository
|
46
|
-
|
48
|
+
|
47
49
|
Args:
|
48
50
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
49
|
-
|
51
|
+
|
50
52
|
Returns:
|
51
53
|
A formatted list of recent commits
|
52
54
|
"""
|
@@ -54,27 +56,27 @@ class GithubApp(APIApplication):
|
|
54
56
|
url = f"{self.base_api_url}/{repo_full_name}/commits"
|
55
57
|
response = self._get(url)
|
56
58
|
response.raise_for_status()
|
57
|
-
|
59
|
+
|
58
60
|
commits = response.json()
|
59
61
|
if not commits:
|
60
62
|
return f"No commits found for repository {repo_full_name}"
|
61
|
-
|
63
|
+
|
62
64
|
result = f"Recent commits for {repo_full_name}:\n\n"
|
63
|
-
for commit in commits[:12]: # Limit to 12 commits
|
65
|
+
for commit in commits[:12]: # Limit to 12 commits
|
64
66
|
sha = commit.get("sha", "")[:7]
|
65
|
-
message = commit.get("commit", {}).get("message", "").split(
|
67
|
+
message = commit.get("commit", {}).get("message", "").split("\n")[0]
|
66
68
|
author = commit.get("commit", {}).get("author", {}).get("name", "Unknown")
|
67
|
-
|
69
|
+
|
68
70
|
result += f"- {sha}: {message} (by {author})\n"
|
69
|
-
|
71
|
+
|
70
72
|
return result
|
71
73
|
|
72
74
|
def list_branches(self, repo_full_name: str) -> str:
|
73
75
|
"""List branches for a GitHub repository
|
74
|
-
|
76
|
+
|
75
77
|
Args:
|
76
78
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
77
|
-
|
79
|
+
|
78
80
|
Returns:
|
79
81
|
A formatted list of branches
|
80
82
|
"""
|
@@ -82,25 +84,25 @@ class GithubApp(APIApplication):
|
|
82
84
|
url = f"{self.base_api_url}/{repo_full_name}/branches"
|
83
85
|
response = self._get(url)
|
84
86
|
response.raise_for_status()
|
85
|
-
|
87
|
+
|
86
88
|
branches = response.json()
|
87
89
|
if not branches:
|
88
90
|
return f"No branches found for repository {repo_full_name}"
|
89
|
-
|
91
|
+
|
90
92
|
result = f"Branches for {repo_full_name}:\n\n"
|
91
93
|
for branch in branches:
|
92
94
|
branch_name = branch.get("name", "Unknown")
|
93
95
|
result += f"- {branch_name}\n"
|
94
|
-
|
96
|
+
|
95
97
|
return result
|
96
|
-
|
98
|
+
|
97
99
|
def list_pull_requests(self, repo_full_name: str, state: str = "open") -> str:
|
98
100
|
"""List pull requests for a GitHub repository
|
99
|
-
|
101
|
+
|
100
102
|
Args:
|
101
103
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
102
104
|
state: The state of the pull requests to filter by (open, closed, or all)
|
103
|
-
|
105
|
+
|
104
106
|
Returns:
|
105
107
|
A formatted list of pull requests
|
106
108
|
"""
|
@@ -109,25 +111,35 @@ class GithubApp(APIApplication):
|
|
109
111
|
params = {"state": state}
|
110
112
|
response = self._get(url, params=params)
|
111
113
|
response.raise_for_status()
|
112
|
-
|
114
|
+
|
113
115
|
pull_requests = response.json()
|
114
116
|
if not pull_requests:
|
115
117
|
return f"No pull requests found for repository {repo_full_name} with state '{state}'"
|
116
|
-
|
118
|
+
|
117
119
|
result = f"Pull requests for {repo_full_name} (State: {state}):\n\n"
|
118
120
|
for pr in pull_requests:
|
119
121
|
pr_title = pr.get("title", "No Title")
|
120
122
|
pr_number = pr.get("number", "Unknown")
|
121
123
|
pr_state = pr.get("state", "Unknown")
|
122
124
|
pr_user = pr.get("user", {}).get("login", "Unknown")
|
123
|
-
|
124
|
-
result +=
|
125
|
-
|
125
|
+
|
126
|
+
result += (
|
127
|
+
f"- PR #{pr_number}: {pr_title} (by {pr_user}, Status: {pr_state})\n"
|
128
|
+
)
|
129
|
+
|
126
130
|
return result
|
127
131
|
|
128
|
-
def list_issues(
|
132
|
+
def list_issues(
|
133
|
+
self,
|
134
|
+
repo_full_name: str,
|
135
|
+
state: str = "open",
|
136
|
+
assignee: str = None,
|
137
|
+
labels: str = None,
|
138
|
+
per_page: int = 30,
|
139
|
+
page: int = 1,
|
140
|
+
) -> list[dict[str, Any]]:
|
129
141
|
"""List issues for a GitHub repository
|
130
|
-
|
142
|
+
|
131
143
|
Args:
|
132
144
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
133
145
|
state: State of issues to return (open, closed, all). Default: open
|
@@ -135,35 +147,31 @@ class GithubApp(APIApplication):
|
|
135
147
|
labels: Comma-separated list of label names (e.g. "bug,ui,@high")
|
136
148
|
per_page: The number of results per page (max 100)
|
137
149
|
page: The page number of the results to fetch
|
138
|
-
|
150
|
+
|
139
151
|
Returns:
|
140
152
|
The complete GitHub API response
|
141
153
|
"""
|
142
154
|
repo_full_name = repo_full_name.strip()
|
143
155
|
url = f"{self.base_api_url}/{repo_full_name}/issues"
|
144
|
-
|
145
|
-
params = {
|
146
|
-
|
147
|
-
"per_page": per_page,
|
148
|
-
"page": page
|
149
|
-
}
|
150
|
-
|
156
|
+
|
157
|
+
params = {"state": state, "per_page": per_page, "page": page}
|
158
|
+
|
151
159
|
if assignee:
|
152
160
|
params["assignee"] = assignee
|
153
161
|
if labels:
|
154
162
|
params["labels"] = labels
|
155
|
-
|
163
|
+
|
156
164
|
response = self._get(url, params=params)
|
157
165
|
response.raise_for_status()
|
158
166
|
return response.json()
|
159
167
|
|
160
168
|
def get_pull_request(self, repo_full_name: str, pull_number: int) -> str:
|
161
169
|
"""Get a specific pull request for a GitHub repository
|
162
|
-
|
170
|
+
|
163
171
|
Args:
|
164
172
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
165
173
|
pull_number: The number of the pull request to retrieve
|
166
|
-
|
174
|
+
|
167
175
|
Returns:
|
168
176
|
A formatted string with pull request details
|
169
177
|
"""
|
@@ -171,52 +179,60 @@ class GithubApp(APIApplication):
|
|
171
179
|
url = f"{self.base_api_url}/{repo_full_name}/pulls/{pull_number}"
|
172
180
|
response = self._get(url)
|
173
181
|
response.raise_for_status()
|
174
|
-
|
182
|
+
|
175
183
|
pr = response.json()
|
176
184
|
pr_title = pr.get("title", "No Title")
|
177
185
|
pr_number = pr.get("number", "Unknown")
|
178
186
|
pr_state = pr.get("state", "Unknown")
|
179
187
|
pr_user = pr.get("user", {}).get("login", "Unknown")
|
180
188
|
pr_body = pr.get("body", "No description provided.")
|
181
|
-
|
189
|
+
|
182
190
|
result = (
|
183
191
|
f"Pull Request #{pr_number}: {pr_title}\n"
|
184
192
|
f"Created by: {pr_user}\n"
|
185
193
|
f"Status: {pr_state}\n"
|
186
194
|
f"Description: {pr_body}\n"
|
187
195
|
)
|
188
|
-
|
196
|
+
|
189
197
|
return result
|
190
198
|
|
191
|
-
def create_pull_request(
|
192
|
-
|
193
|
-
|
199
|
+
def create_pull_request(
|
200
|
+
self,
|
201
|
+
repo_full_name: str,
|
202
|
+
head: str,
|
203
|
+
base: str,
|
204
|
+
title: str = None,
|
205
|
+
body: str = None,
|
206
|
+
issue: int = None,
|
207
|
+
maintainer_can_modify: bool = True,
|
208
|
+
draft: bool = False,
|
209
|
+
) -> dict[str, Any]:
|
194
210
|
"""Create a new pull request for a GitHub repository
|
195
|
-
|
211
|
+
|
196
212
|
Args:
|
197
213
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
198
214
|
head: The name of the branch where your changes are implemented
|
199
215
|
base: The name of the branch you want the changes pulled into
|
200
216
|
title: The title of the new pull request (required if issue is not specified)
|
201
217
|
body: The contents of the pull request
|
202
|
-
issue: An issue number to convert to a pull request. If specified, the issue's
|
218
|
+
issue: An issue number to convert to a pull request. If specified, the issue's
|
203
219
|
title, body, and comments will be used for the pull request
|
204
220
|
maintainer_can_modify: Indicates whether maintainers can modify the pull request
|
205
221
|
draft: Indicates whether the pull request is a draft
|
206
|
-
|
222
|
+
|
207
223
|
Returns:
|
208
224
|
The complete GitHub API response
|
209
225
|
"""
|
210
226
|
repo_full_name = repo_full_name.strip()
|
211
227
|
url = f"{self.base_api_url}/{repo_full_name}/pulls"
|
212
|
-
|
228
|
+
|
213
229
|
pull_request_data = {
|
214
230
|
"head": head,
|
215
231
|
"base": base,
|
216
232
|
"maintainer_can_modify": maintainer_can_modify,
|
217
|
-
"draft": draft
|
233
|
+
"draft": draft,
|
218
234
|
}
|
219
|
-
|
235
|
+
|
220
236
|
if issue is not None:
|
221
237
|
pull_request_data["issue"] = issue
|
222
238
|
else:
|
@@ -225,98 +241,107 @@ class GithubApp(APIApplication):
|
|
225
241
|
pull_request_data["title"] = title
|
226
242
|
if body is not None:
|
227
243
|
pull_request_data["body"] = body
|
228
|
-
|
244
|
+
|
229
245
|
response = self._post(url, pull_request_data)
|
230
246
|
response.raise_for_status()
|
231
247
|
return response.json()
|
232
248
|
|
233
|
-
def create_issue(
|
249
|
+
def create_issue(
|
250
|
+
self, repo_full_name: str, title: str, body: str = "", labels=None
|
251
|
+
) -> str:
|
234
252
|
"""Create a new issue in a GitHub repository
|
235
|
-
|
253
|
+
|
236
254
|
Args:
|
237
255
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
238
256
|
title: The title of the issue
|
239
257
|
body: The contents of the issue
|
240
258
|
labels: Labels to associate with this issue. Enter as a comma-separated string
|
241
|
-
(e.g. "bug,enhancement,documentation").
|
242
|
-
NOTE: Only users with push access can set labels for new issues.
|
259
|
+
(e.g. "bug,enhancement,documentation").
|
260
|
+
NOTE: Only users with push access can set labels for new issues.
|
243
261
|
Labels are silently dropped otherwise.
|
244
|
-
|
262
|
+
|
245
263
|
Returns:
|
246
264
|
A confirmation message with the new issue details
|
247
265
|
"""
|
248
266
|
repo_full_name = repo_full_name.strip()
|
249
267
|
url = f"{self.base_api_url}/{repo_full_name}/issues"
|
250
|
-
|
251
|
-
issue_data = {
|
252
|
-
|
253
|
-
"body": body
|
254
|
-
}
|
255
|
-
|
268
|
+
|
269
|
+
issue_data = {"title": title, "body": body}
|
270
|
+
|
256
271
|
if labels:
|
257
272
|
if isinstance(labels, str):
|
258
|
-
labels_list = [
|
273
|
+
labels_list = [
|
274
|
+
label.strip() for label in labels.split(",") if label.strip()
|
275
|
+
]
|
259
276
|
issue_data["labels"] = labels_list
|
260
277
|
else:
|
261
278
|
issue_data["labels"] = labels
|
262
|
-
|
279
|
+
|
263
280
|
response = self._post(url, issue_data)
|
264
281
|
response.raise_for_status()
|
265
|
-
|
282
|
+
|
266
283
|
issue = response.json()
|
267
284
|
issue_number = issue.get("number", "Unknown")
|
268
285
|
issue_url = issue.get("html_url", "")
|
269
|
-
|
270
|
-
return f"Successfully created issue #{issue_number}:\n" \
|
271
|
-
f"Title: {title}\n" \
|
272
|
-
f"URL: {issue_url}"
|
273
286
|
|
274
|
-
|
287
|
+
return (
|
288
|
+
f"Successfully created issue #{issue_number}:\n"
|
289
|
+
f"Title: {title}\n"
|
290
|
+
f"URL: {issue_url}"
|
291
|
+
)
|
292
|
+
|
293
|
+
def list_repo_activities(
|
294
|
+
self, repo_full_name: str, direction: str = "desc", per_page: int = 30
|
295
|
+
) -> str:
|
275
296
|
"""List activities for a GitHub repository
|
276
|
-
|
297
|
+
|
277
298
|
Args:
|
278
299
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
279
300
|
direction: The direction to sort the results by (asc or desc). Default: desc
|
280
301
|
per_page: The number of results per page (max 100). Default: 30
|
281
|
-
|
302
|
+
|
282
303
|
Returns:
|
283
304
|
A formatted list of repository activities
|
284
305
|
"""
|
285
306
|
repo_full_name = repo_full_name.strip()
|
286
307
|
url = f"{self.base_api_url}/{repo_full_name}/activity"
|
287
|
-
|
308
|
+
|
288
309
|
# Build query parameters
|
289
|
-
params = {
|
290
|
-
|
291
|
-
"per_page": per_page
|
292
|
-
}
|
293
|
-
|
310
|
+
params = {"direction": direction, "per_page": per_page}
|
311
|
+
|
294
312
|
response = self._get(url, params=params)
|
295
313
|
response.raise_for_status()
|
296
|
-
|
314
|
+
|
297
315
|
activities = response.json()
|
298
316
|
if not activities:
|
299
317
|
return f"No activities found for repository {repo_full_name}"
|
300
|
-
|
318
|
+
|
301
319
|
result = f"Repository activities for {repo_full_name}:\n\n"
|
302
|
-
|
320
|
+
|
303
321
|
for activity in activities:
|
304
322
|
# Extract common fields
|
305
323
|
timestamp = activity.get("timestamp", "Unknown time")
|
306
324
|
actor_name = "Unknown user"
|
307
325
|
if "actor" in activity and activity["actor"]:
|
308
326
|
actor_name = activity["actor"].get("login", "Unknown user")
|
309
|
-
|
327
|
+
|
310
328
|
# Create a simple description of the activity
|
311
329
|
result += f"- {actor_name} performed an activity at {timestamp}\n"
|
312
|
-
|
330
|
+
|
313
331
|
return result
|
314
332
|
|
315
|
-
def update_issue(
|
316
|
-
|
317
|
-
|
333
|
+
def update_issue(
|
334
|
+
self,
|
335
|
+
repo_full_name: str,
|
336
|
+
issue_number: int,
|
337
|
+
title: str = None,
|
338
|
+
body: str = None,
|
339
|
+
assignee: str = None,
|
340
|
+
state: str = None,
|
341
|
+
state_reason: str = None,
|
342
|
+
) -> dict[str, Any]:
|
318
343
|
"""Update an issue in a GitHub repository
|
319
|
-
|
344
|
+
|
320
345
|
Args:
|
321
346
|
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
322
347
|
issue_number: The number that identifies the issue
|
@@ -325,12 +350,12 @@ class GithubApp(APIApplication):
|
|
325
350
|
assignee: Username to assign to this issue
|
326
351
|
state: State of the issue (open or closed)
|
327
352
|
state_reason: Reason for state change (completed, not_planned, reopened, null)
|
328
|
-
|
353
|
+
|
329
354
|
Returns:
|
330
355
|
The complete GitHub API response
|
331
356
|
"""
|
332
357
|
url = f"{self.base_api_url}/{repo_full_name}/issues/{issue_number}"
|
333
|
-
|
358
|
+
|
334
359
|
update_data = {}
|
335
360
|
if title is not None:
|
336
361
|
update_data["title"] = title
|
@@ -342,13 +367,21 @@ class GithubApp(APIApplication):
|
|
342
367
|
update_data["state"] = state
|
343
368
|
if state_reason is not None:
|
344
369
|
update_data["state_reason"] = state_reason
|
345
|
-
|
370
|
+
|
346
371
|
response = self._patch(url, update_data)
|
347
372
|
response.raise_for_status()
|
348
373
|
return response.json()
|
349
374
|
|
350
375
|
def list_tools(self):
|
351
|
-
return [
|
352
|
-
|
353
|
-
|
354
|
-
|
376
|
+
return [
|
377
|
+
self.star_repository,
|
378
|
+
self.list_commits,
|
379
|
+
self.list_branches,
|
380
|
+
self.list_pull_requests,
|
381
|
+
self.list_issues,
|
382
|
+
self.get_pull_request,
|
383
|
+
self.create_pull_request,
|
384
|
+
self.create_issue,
|
385
|
+
self.update_issue,
|
386
|
+
self.list_repo_activities,
|
387
|
+
]
|