universal-mcp 0.1.1__py3-none-any.whl → 0.1.2rc1__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.
Files changed (34) hide show
  1. universal_mcp/applications/__init__.py +23 -28
  2. universal_mcp/applications/application.py +13 -8
  3. universal_mcp/applications/e2b/app.py +74 -0
  4. universal_mcp/applications/firecrawl/app.py +381 -0
  5. universal_mcp/applications/github/README.md +35 -0
  6. universal_mcp/applications/github/app.py +133 -100
  7. universal_mcp/applications/google_calendar/app.py +170 -139
  8. universal_mcp/applications/google_mail/app.py +185 -160
  9. universal_mcp/applications/markitdown/app.py +32 -0
  10. universal_mcp/applications/reddit/app.py +112 -71
  11. universal_mcp/applications/resend/app.py +3 -8
  12. universal_mcp/applications/serp/app.py +84 -0
  13. universal_mcp/applications/tavily/app.py +11 -10
  14. universal_mcp/applications/zenquotes/app.py +3 -3
  15. universal_mcp/cli.py +98 -16
  16. universal_mcp/config.py +20 -3
  17. universal_mcp/exceptions.py +1 -3
  18. universal_mcp/integrations/__init__.py +6 -2
  19. universal_mcp/integrations/agentr.py +26 -24
  20. universal_mcp/integrations/integration.py +72 -35
  21. universal_mcp/servers/__init__.py +21 -1
  22. universal_mcp/servers/server.py +77 -44
  23. universal_mcp/stores/__init__.py +15 -2
  24. universal_mcp/stores/store.py +123 -13
  25. universal_mcp/utils/__init__.py +1 -0
  26. universal_mcp/utils/api_generator.py +269 -0
  27. universal_mcp/utils/docgen.py +360 -0
  28. universal_mcp/utils/installation.py +17 -2
  29. universal_mcp/utils/openapi.py +202 -104
  30. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/METADATA +22 -5
  31. universal_mcp-0.1.2rc1.dist-info/RECORD +37 -0
  32. universal_mcp-0.1.1.dist-info/RECORD +0 -29
  33. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/WHEEL +0 -0
  34. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
1
- from universal_mcp.integrations import Integration
2
- from universal_mcp.applications.application import APIApplication
1
+ from typing import Any
2
+
3
3
  from loguru import logger
4
- from typing import List, Dict, Any
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('\n')[0]
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 += f"- PR #{pr_number}: {pr_title} (by {pr_user}, Status: {pr_state})\n"
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(self, repo_full_name: str, state: str = "open", assignee: str = None, labels: str = None, per_page: int = 30, page: int = 1) -> List[Dict[str, Any]]:
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
- "state": state,
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(self, repo_full_name: str, head: str, base: str, title: str = None,
192
- body: str = None, issue: int = None, maintainer_can_modify: bool = True,
193
- draft: bool = False) -> Dict[str, Any]:
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(self, repo_full_name: str, title: str, body: str = "", labels = None) -> str:
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
- "title": title,
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 = [label.strip() for label in labels.split(",") if label.strip()]
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
- def list_repo_activities(self, repo_full_name: str, direction: str = "desc", per_page: int = 30) -> str:
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
- "direction": direction,
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(self, repo_full_name: str, issue_number: int, title: str = None,
316
- body: str = None, assignee: str = None, state: str = None,
317
- state_reason: str = None) -> Dict[str, Any]:
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 [self.star_repository, self.list_commits, self.list_branches,
352
- self.list_pull_requests, self.list_issues, self.get_pull_request,
353
- self.create_pull_request, self.create_issue, self.update_issue,
354
- self.list_repo_activities]
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
+ ]