universal-mcp 0.1.0__py3-none-any.whl → 0.1.1rc1__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 (35) 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 +131 -77
  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 -45
  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 +104 -0
  29. universal_mcp/utils/openapi.py +202 -104
  30. {universal_mcp-0.1.0.dist-info → universal_mcp-0.1.1rc1.dist-info}/METADATA +47 -5
  31. universal_mcp-0.1.1rc1.dist-info/RECORD +37 -0
  32. universal_mcp/applications/agentr.py +0 -0
  33. universal_mcp-0.1.0.dist-info/RECORD +0 -29
  34. {universal_mcp-0.1.0.dist-info → universal_mcp-0.1.1rc1.dist-info}/WHEEL +0 -0
  35. {universal_mcp-0.1.0.dist-info → universal_mcp-0.1.1rc1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from loguru import logger
4
+
1
5
  from universal_mcp.applications.application import APIApplication
2
6
  from universal_mcp.integrations import Integration
3
- from loguru import logger
4
- from datetime import datetime, timedelta
7
+
5
8
 
6
9
  class GoogleCalendarApp(APIApplication):
7
10
  def __init__(self, integration: Integration) -> None:
@@ -16,22 +19,22 @@ class GoogleCalendarApp(APIApplication):
16
19
  return credentials["headers"]
17
20
  return {
18
21
  "Authorization": f"Bearer {credentials['access_token']}",
19
- "Accept": "application/json"
22
+ "Accept": "application/json",
20
23
  }
21
-
24
+
22
25
  def _format_datetime(self, dt_string: str) -> str:
23
26
  """Format a datetime string from ISO format to a human-readable format.
24
-
27
+
25
28
  Args:
26
29
  dt_string: A datetime string in ISO format (e.g., "2023-06-01T10:00:00Z")
27
-
30
+
28
31
  Returns:
29
32
  A formatted datetime string (e.g., "2023-06-01 10:00 AM") or the original string with
30
33
  "(All day)" appended if it's just a date
31
34
  """
32
35
  if not dt_string or dt_string == "Unknown":
33
36
  return "Unknown"
34
-
37
+
35
38
  # Check if it's just a date (all-day event) or a datetime
36
39
  if "T" in dt_string:
37
40
  # It's a datetime - parse and format it
@@ -39,10 +42,10 @@ class GoogleCalendarApp(APIApplication):
39
42
  # Handle Z (UTC) suffix by replacing with +00:00 timezone
40
43
  if dt_string.endswith("Z"):
41
44
  dt_string = dt_string.replace("Z", "+00:00")
42
-
45
+
43
46
  # Parse the ISO datetime string
44
47
  dt = datetime.fromisoformat(dt_string)
45
-
48
+
46
49
  # Format to a more readable form
47
50
  return dt.strftime("%Y-%m-%d %I:%M %p")
48
51
  except ValueError:
@@ -52,61 +55,67 @@ class GoogleCalendarApp(APIApplication):
52
55
  else:
53
56
  # It's just a date (all-day event)
54
57
  return f"{dt_string} (All day)"
55
-
56
- def get_today_events(self, days: int = 1, max_results: int = None, time_zone: str = None) -> str:
58
+
59
+ def get_today_events(
60
+ self, days: int = 1, max_results: int = None, time_zone: str = None
61
+ ) -> str:
57
62
  """Get events from your Google Calendar for today or a specified number of days
58
-
63
+
59
64
  Args:
60
65
  days: Number of days to retrieve events for (default: 1, which is just today)
61
66
  max_results: Maximum number of events to return (optional)
62
67
  time_zone: Time zone used in the response (optional, default is calendar's time zone)
63
-
68
+
64
69
  Returns:
65
70
  A formatted list of events or an error message
66
71
  """
67
72
  # Get today's date in ISO format
68
73
  today = datetime.utcnow().date()
69
74
  end_date = today + timedelta(days=days)
70
-
75
+
71
76
  # Format dates for API
72
77
  time_min = f"{today.isoformat()}T00:00:00Z"
73
78
  time_max = f"{end_date.isoformat()}T00:00:00Z"
74
-
79
+
75
80
  url = f"{self.base_api_url}/events"
76
-
81
+
77
82
  # Build query parameters
78
83
  params = {
79
84
  "timeMin": time_min,
80
85
  "timeMax": time_max,
81
86
  "singleEvents": "true",
82
- "orderBy": "startTime"
87
+ "orderBy": "startTime",
83
88
  }
84
-
89
+
85
90
  if max_results is not None:
86
91
  params["maxResults"] = max_results
87
-
92
+
88
93
  if time_zone:
89
94
  params["timeZone"] = time_zone
90
-
95
+
91
96
  date_range = "today" if days == 1 else f"the next {days} days"
92
97
  logger.info(f"Retrieving calendar events for {date_range}")
93
-
98
+
94
99
  response = self._get(url, params=params)
95
100
  response.raise_for_status()
96
-
101
+
97
102
  events = response.json().get("items", [])
98
103
  if not events:
99
104
  return f"No events scheduled for {date_range}."
100
-
105
+
101
106
  result = f"Events for {date_range}:\n\n"
102
107
  for event in events:
103
108
  # Extract event date and time
104
109
  start = event.get("start", {})
105
- event_date = start.get("date", start.get("dateTime", "")).split("T")[0] if "T" in start.get("dateTime", "") else start.get("date", "")
106
-
110
+ event_date = (
111
+ start.get("date", start.get("dateTime", "")).split("T")[0]
112
+ if "T" in start.get("dateTime", "")
113
+ else start.get("date", "")
114
+ )
115
+
107
116
  # Extract and format time
108
117
  start_time = start.get("dateTime", start.get("date", "All day"))
109
-
118
+
110
119
  # Format the time display
111
120
  if "T" in start_time: # It's a datetime
112
121
  formatted_time = self._format_datetime(start_time)
@@ -115,71 +124,74 @@ class GoogleCalendarApp(APIApplication):
115
124
  time_display = formatted_time
116
125
  else:
117
126
  # Extract just the time part
118
- time_display = formatted_time.split(" ")[1] + " " + formatted_time.split(" ")[2]
127
+ time_display = (
128
+ formatted_time.split(" ")[1]
129
+ + " "
130
+ + formatted_time.split(" ")[2]
131
+ )
119
132
  else: # It's an all-day event
120
- if days > 1:
121
- time_display = f"{event_date} (All day)"
122
- else:
123
- time_display = "All day"
124
-
133
+ time_display = f"{event_date} (All day)" if days > 1 else "All day"
134
+
125
135
  # Get event details
126
136
  summary = event.get("summary", "Untitled event")
127
137
  event_id = event.get("id", "No ID")
128
-
138
+
129
139
  result += f"- {time_display}: {summary} (ID: {event_id})\n"
130
-
140
+
131
141
  return result
132
142
 
133
- def get_event(self, event_id: str, max_attendees: int = None, time_zone: str = None) -> str:
143
+ def get_event(
144
+ self, event_id: str, max_attendees: int = None, time_zone: str = None
145
+ ) -> str:
134
146
  """Get a specific event from your Google Calendar by ID
135
-
147
+
136
148
  Args:
137
149
  event_id: The ID of the event to retrieve
138
150
  max_attendees: Optional. The maximum number of attendees to include in the response
139
151
  time_zone: Optional. Time zone used in the response (default is calendar's time zone)
140
-
152
+
141
153
  Returns:
142
154
  A formatted event details or an error message
143
155
  """
144
156
  url = f"{self.base_api_url}/events/{event_id}"
145
-
157
+
146
158
  # Build query parameters
147
159
  params = {}
148
160
  if max_attendees is not None:
149
161
  params["maxAttendees"] = max_attendees
150
162
  if time_zone:
151
163
  params["timeZone"] = time_zone
152
-
164
+
153
165
  logger.info(f"Retrieving calendar event with ID: {event_id}")
154
-
166
+
155
167
  response = self._get(url, params=params)
156
168
  response.raise_for_status()
157
-
169
+
158
170
  event = response.json()
159
-
171
+
160
172
  # Extract event details
161
173
  summary = event.get("summary", "Untitled event")
162
174
  description = event.get("description", "No description")
163
175
  location = event.get("location", "No location specified")
164
-
176
+
165
177
  # Format dates
166
178
  start = event.get("start", {})
167
179
  end = event.get("end", {})
168
-
180
+
169
181
  start_time = start.get("dateTime", start.get("date", "Unknown"))
170
182
  end_time = end.get("dateTime", end.get("date", "Unknown"))
171
-
183
+
172
184
  # Format datetimes using the helper function
173
185
  start_formatted = self._format_datetime(start_time)
174
186
  end_formatted = self._format_datetime(end_time)
175
-
187
+
176
188
  # Get creator and organizer
177
189
  creator = event.get("creator", {}).get("email", "Unknown")
178
190
  organizer = event.get("organizer", {}).get("email", "Unknown")
179
-
191
+
180
192
  # Check if it's a recurring event
181
193
  recurrence = "Yes" if "recurrence" in event else "No"
182
-
194
+
183
195
  # Get attendees if any
184
196
  attendees = event.get("attendees", [])
185
197
  attendee_info = ""
@@ -189,17 +201,17 @@ class GoogleCalendarApp(APIApplication):
189
201
  email = attendee.get("email", "No email")
190
202
  name = attendee.get("displayName", email)
191
203
  response_status = attendee.get("responseStatus", "Unknown")
192
-
204
+
193
205
  status_mapping = {
194
206
  "accepted": "Accepted",
195
207
  "declined": "Declined",
196
208
  "tentative": "Maybe",
197
- "needsAction": "Not responded"
209
+ "needsAction": "Not responded",
198
210
  }
199
-
211
+
200
212
  formatted_status = status_mapping.get(response_status, response_status)
201
213
  attendee_info += f" {i}. {name} ({email}) - {formatted_status}\n"
202
-
214
+
203
215
  # Format the response
204
216
  result = f"Event: {summary}\n"
205
217
  result += f"ID: {event_id}\n"
@@ -210,14 +222,22 @@ class GoogleCalendarApp(APIApplication):
210
222
  result += f"Organizer: {organizer}\n"
211
223
  result += f"Recurring: {recurrence}\n"
212
224
  result += attendee_info
213
-
225
+
214
226
  return result
215
227
 
216
- def list_events(self, max_results: int = 10, time_min: str = None, time_max: str = None,
217
- q: str = None, order_by: str = "startTime", single_events: bool = True,
218
- time_zone: str = None, page_token: str = None) -> str:
228
+ def list_events(
229
+ self,
230
+ max_results: int = 10,
231
+ time_min: str = None,
232
+ time_max: str = None,
233
+ q: str = None,
234
+ order_by: str = "startTime",
235
+ single_events: bool = True,
236
+ time_zone: str = None,
237
+ page_token: str = None,
238
+ ) -> str:
219
239
  """List events from your Google Calendar with various filtering options
220
-
240
+
221
241
  Args:
222
242
  max_results: Maximum number of events to return (default: 10, max: 2500)
223
243
  time_min: Start time (ISO format, e.g. '2023-12-01T00:00:00Z') - defaults to now if not specified
@@ -227,19 +247,19 @@ class GoogleCalendarApp(APIApplication):
227
247
  single_events: Whether to expand recurring events (default: True)
228
248
  time_zone: Time zone used in the response (default is calendar's time zone)
229
249
  page_token: Token for retrieving a specific page of results
230
-
250
+
231
251
  Returns:
232
252
  A formatted list of events or an error message
233
253
  """
234
254
  url = f"{self.base_api_url}/events"
235
-
255
+
236
256
  # Build query parameters
237
257
  params = {
238
258
  "maxResults": max_results,
239
259
  "singleEvents": str(single_events).lower(),
240
- "orderBy": order_by
260
+ "orderBy": order_by,
241
261
  }
242
-
262
+
243
263
  # Set time boundaries if provided, otherwise default to now for time_min
244
264
  if time_min:
245
265
  params["timeMin"] = time_min
@@ -247,145 +267,151 @@ class GoogleCalendarApp(APIApplication):
247
267
  # Default to current time if not specified
248
268
  now = datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time
249
269
  params["timeMin"] = now
250
-
270
+
251
271
  if time_max:
252
272
  params["timeMax"] = time_max
253
-
273
+
254
274
  # Add optional filters if provided
255
275
  if q:
256
276
  params["q"] = q
257
-
277
+
258
278
  if time_zone:
259
279
  params["timeZone"] = time_zone
260
-
280
+
261
281
  if page_token:
262
282
  params["pageToken"] = page_token
263
-
283
+
264
284
  logger.info(f"Retrieving calendar events with params: {params}")
265
-
285
+
266
286
  response = self._get(url, params=params)
267
287
  response.raise_for_status()
268
-
288
+
269
289
  data = response.json()
270
290
  events = data.get("items", [])
271
-
291
+
272
292
  if not events:
273
293
  return "No events found matching your criteria."
274
-
294
+
275
295
  # Extract calendar information
276
296
  calendar_summary = data.get("summary", "Your Calendar")
277
297
  time_zone_info = data.get("timeZone", "Unknown")
278
-
298
+
279
299
  result = f"Events from {calendar_summary} (Time Zone: {time_zone_info}):\n\n"
280
-
300
+
281
301
  # Process and format each event
282
302
  for i, event in enumerate(events, 1):
283
303
  # Get basic event details
284
304
  event_id = event.get("id", "No ID")
285
305
  summary = event.get("summary", "Untitled event")
286
-
306
+
287
307
  # Get event times and format them
288
308
  start = event.get("start", {})
289
309
  start_time = start.get("dateTime", start.get("date", "Unknown"))
290
-
310
+
291
311
  # Format the start time using the helper function
292
312
  start_formatted = self._format_datetime(start_time)
293
-
313
+
294
314
  # Get location if available
295
315
  location = event.get("location", "No location specified")
296
-
316
+
297
317
  # Check if it's a recurring event
298
318
  is_recurring = "recurrence" in event
299
319
  recurring_info = " (Recurring)" if is_recurring else ""
300
-
320
+
301
321
  # Format the event information
302
322
  result += f"{i}. {summary}{recurring_info}\n"
303
323
  result += f" ID: {event_id}\n"
304
324
  result += f" When: {start_formatted}\n"
305
325
  result += f" Where: {location}\n"
306
-
326
+
307
327
  # Add a separator between events
308
328
  if i < len(events):
309
329
  result += "\n"
310
-
330
+
311
331
  # Add pagination info if available
312
332
  if "nextPageToken" in data:
313
333
  next_token = data.get("nextPageToken")
314
- result += f"\nMore events available. Use page_token='{next_token}' to see more."
315
-
334
+ result += (
335
+ f"\nMore events available. Use page_token='{next_token}' to see more."
336
+ )
337
+
316
338
  return result
317
339
 
318
340
  def quick_add_event(self, text: str, send_updates: str = "none") -> str:
319
341
  """Create a calendar event using natural language description
320
-
342
+
321
343
  This method allows you to quickly create an event using a simple text string,
322
344
  similar to how you would add events in the Google Calendar UI.
323
-
345
+
324
346
  Args:
325
347
  text: Text describing the event (e.g., "Meeting with John at Coffee Shop tomorrow 3pm-4pm")
326
348
  send_updates: Who should receive notifications - "all", "externalOnly", or "none" (default)
327
-
349
+
328
350
  Returns:
329
351
  A confirmation message with the created event details or an error message
330
352
  """
331
353
  url = f"{self.base_api_url}/events/quickAdd"
332
-
354
+
333
355
  # Use params argument instead of manually constructing URL
334
- params = {
335
- "text": text,
336
- "sendUpdates": send_updates
337
- }
338
-
356
+ params = {"text": text, "sendUpdates": send_updates}
357
+
339
358
  logger.info(f"Creating event via quickAdd: '{text}'")
340
-
359
+
341
360
  # Pass params to _post method
342
361
  response = self._post(url, data=None, params=params)
343
362
  response.raise_for_status()
344
-
363
+
345
364
  event = response.json()
346
-
365
+
347
366
  # Extract event details
348
367
  event_id = event.get("id", "Unknown")
349
368
  summary = event.get("summary", "Untitled event")
350
-
369
+
351
370
  # Format dates
352
371
  start = event.get("start", {})
353
372
  end = event.get("end", {})
354
-
373
+
355
374
  start_time = start.get("dateTime", start.get("date", "Unknown"))
356
375
  end_time = end.get("dateTime", end.get("date", "Unknown"))
357
-
376
+
358
377
  # Format datetimes using the helper function
359
378
  start_formatted = self._format_datetime(start_time)
360
379
  end_formatted = self._format_datetime(end_time)
361
-
380
+
362
381
  # Get location if available
363
382
  location = event.get("location", "No location specified")
364
-
383
+
365
384
  # Format the confirmation message
366
- result = f"Successfully created event!\n\n"
385
+ result = "Successfully created event!\n\n"
367
386
  result += f"Summary: {summary}\n"
368
387
  result += f"When: {start_formatted}"
369
-
388
+
370
389
  # Only add end time if it's different from start (for all-day events they might be the same)
371
390
  if start_formatted != end_formatted:
372
391
  result += f" to {end_formatted}"
373
-
392
+
374
393
  result += f"\nWhere: {location}\n"
375
394
  result += f"Event ID: {event_id}\n"
376
-
395
+
377
396
  # Add a note about viewing the event
378
397
  result += f"\nUse get_event('{event_id}') to see full details."
379
-
398
+
380
399
  return result
381
-
382
- def get_event_instances(self, event_id: str, max_results: int = 25, time_min: str = None,
383
- time_max: str = None, time_zone: str = None, show_deleted: bool = False,
384
- page_token: str = None) -> str:
400
+
401
+ def get_event_instances(
402
+ self,
403
+ event_id: str,
404
+ max_results: int = 25,
405
+ time_min: str = None,
406
+ time_max: str = None,
407
+ time_zone: str = None,
408
+ show_deleted: bool = False,
409
+ page_token: str = None,
410
+ ) -> str:
385
411
  """Get all instances of a recurring event
386
-
412
+
387
413
  This method retrieves all occurrences of a recurring event within a specified time range.
388
-
414
+
389
415
  Args:
390
416
  event_id: ID of the recurring event
391
417
  max_results: Maximum number of event instances to return (default: 25, max: 2500)
@@ -394,94 +420,99 @@ class GoogleCalendarApp(APIApplication):
394
420
  time_zone: Time zone used in the response (default is calendar's time zone)
395
421
  show_deleted: Whether to include deleted instances (default: False)
396
422
  page_token: Token for retrieving a specific page of results
397
-
423
+
398
424
  Returns:
399
425
  A formatted list of event instances or an error message
400
426
  """
401
427
  url = f"{self.base_api_url}/events/{event_id}/instances"
402
-
428
+
403
429
  # Build query parameters
404
- params = {
405
- "maxResults": max_results,
406
- "showDeleted": str(show_deleted).lower()
407
- }
408
-
430
+ params = {"maxResults": max_results, "showDeleted": str(show_deleted).lower()}
431
+
409
432
  # Add optional parameters if provided
410
433
  if time_min:
411
434
  params["timeMin"] = time_min
412
-
435
+
413
436
  if time_max:
414
437
  params["timeMax"] = time_max
415
-
438
+
416
439
  if time_zone:
417
440
  params["timeZone"] = time_zone
418
-
441
+
419
442
  if page_token:
420
443
  params["pageToken"] = page_token
421
-
444
+
422
445
  logger.info(f"Retrieving instances of recurring event with ID: {event_id}")
423
-
446
+
424
447
  response = self._get(url, params=params)
425
448
  response.raise_for_status()
426
-
449
+
427
450
  data = response.json()
428
451
  instances = data.get("items", [])
429
-
452
+
430
453
  if not instances:
431
454
  return f"No instances found for recurring event with ID: {event_id}"
432
-
455
+
433
456
  # Extract event summary from the first instance
434
457
  parent_summary = instances[0].get("summary", "Untitled recurring event")
435
-
458
+
436
459
  result = f"Instances of recurring event: {parent_summary}\n\n"
437
-
460
+
438
461
  # Process and format each instance
439
462
  for i, instance in enumerate(instances, 1):
440
463
  # Get instance ID and status
441
464
  instance_id = instance.get("id", "No ID")
442
465
  status = instance.get("status", "confirmed")
443
-
466
+
444
467
  # Format status for display
445
468
  status_display = ""
446
469
  if status == "cancelled":
447
470
  status_display = " [CANCELLED]"
448
471
  elif status == "tentative":
449
472
  status_display = " [TENTATIVE]"
450
-
473
+
451
474
  # Get instance time
452
475
  start = instance.get("start", {})
453
476
  original_start_time = instance.get("originalStartTime", {})
454
-
477
+
455
478
  # Determine if this is a modified instance
456
479
  is_modified = original_start_time and "dateTime" in original_start_time
457
480
  modified_indicator = " [MODIFIED]" if is_modified else ""
458
-
481
+
459
482
  # Get the time information
460
483
  start_time = start.get("dateTime", start.get("date", "Unknown"))
461
-
484
+
462
485
  # Format the time using the helper function
463
486
  formatted_time = self._format_datetime(start_time)
464
-
487
+
465
488
  # Format the instance information
466
489
  result += f"{i}. {formatted_time}{status_display}{modified_indicator}\n"
467
490
  result += f" Instance ID: {instance_id}\n"
468
-
491
+
469
492
  # Show original start time if modified
470
493
  if is_modified:
471
- orig_time = original_start_time.get("dateTime", original_start_time.get("date", "Unknown"))
494
+ orig_time = original_start_time.get(
495
+ "dateTime", original_start_time.get("date", "Unknown")
496
+ )
472
497
  orig_formatted = self._format_datetime(orig_time)
473
498
  result += f" Original time: {orig_formatted}\n"
474
-
499
+
475
500
  # Add a separator between instances
476
501
  if i < len(instances):
477
502
  result += "\n"
478
-
503
+
479
504
  # Add pagination info if available
480
505
  if "nextPageToken" in data:
481
506
  next_token = data.get("nextPageToken")
482
507
  result += f"\nMore instances available. Use page_token='{next_token}' to see more."
483
-
508
+
484
509
  return result
485
-
510
+
486
511
  def list_tools(self):
487
- return [self.get_event, self.get_today_events, self.list_events, self.quick_add_event, self.get_event_instances]
512
+ return [
513
+ self.get_event,
514
+ self.get_today_events,
515
+ self.list_events,
516
+ self.quick_add_event,
517
+ self.get_event_instances,
518
+ ]