universal-mcp 0.1.7rc1__py3-none-any.whl → 0.1.8__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 (61) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/ahrefs/README.md +76 -0
  4. universal_mcp/applications/ahrefs/app.py +2291 -0
  5. universal_mcp/applications/application.py +95 -5
  6. universal_mcp/applications/calendly/README.md +78 -0
  7. universal_mcp/applications/calendly/__init__.py +0 -0
  8. universal_mcp/applications/calendly/app.py +1195 -0
  9. universal_mcp/applications/coda/README.md +133 -0
  10. universal_mcp/applications/coda/__init__.py +0 -0
  11. universal_mcp/applications/coda/app.py +3671 -0
  12. universal_mcp/applications/e2b/app.py +14 -28
  13. universal_mcp/applications/figma/README.md +74 -0
  14. universal_mcp/applications/figma/__init__.py +0 -0
  15. universal_mcp/applications/figma/app.py +1261 -0
  16. universal_mcp/applications/firecrawl/app.py +38 -35
  17. universal_mcp/applications/github/app.py +127 -85
  18. universal_mcp/applications/google_calendar/app.py +62 -138
  19. universal_mcp/applications/google_docs/app.py +47 -52
  20. universal_mcp/applications/google_drive/app.py +119 -113
  21. universal_mcp/applications/google_mail/app.py +124 -50
  22. universal_mcp/applications/google_sheet/app.py +89 -91
  23. universal_mcp/applications/markitdown/app.py +9 -8
  24. universal_mcp/applications/notion/app.py +254 -134
  25. universal_mcp/applications/perplexity/app.py +13 -41
  26. universal_mcp/applications/reddit/app.py +94 -85
  27. universal_mcp/applications/resend/app.py +12 -13
  28. universal_mcp/applications/{serp → serpapi}/app.py +14 -25
  29. universal_mcp/applications/tavily/app.py +11 -18
  30. universal_mcp/applications/wrike/README.md +71 -0
  31. universal_mcp/applications/wrike/__init__.py +0 -0
  32. universal_mcp/applications/wrike/app.py +1372 -0
  33. universal_mcp/applications/youtube/README.md +82 -0
  34. universal_mcp/applications/youtube/__init__.py +0 -0
  35. universal_mcp/applications/youtube/app.py +1428 -0
  36. universal_mcp/applications/zenquotes/app.py +12 -2
  37. universal_mcp/exceptions.py +9 -2
  38. universal_mcp/integrations/__init__.py +24 -1
  39. universal_mcp/integrations/agentr.py +27 -4
  40. universal_mcp/integrations/integration.py +146 -32
  41. universal_mcp/logger.py +3 -56
  42. universal_mcp/servers/__init__.py +6 -14
  43. universal_mcp/servers/server.py +201 -146
  44. universal_mcp/stores/__init__.py +7 -2
  45. universal_mcp/stores/store.py +103 -40
  46. universal_mcp/tools/__init__.py +3 -0
  47. universal_mcp/tools/adapters.py +43 -0
  48. universal_mcp/tools/func_metadata.py +213 -0
  49. universal_mcp/tools/tools.py +342 -0
  50. universal_mcp/utils/docgen.py +325 -119
  51. universal_mcp/utils/docstring_parser.py +179 -0
  52. universal_mcp/utils/dump_app_tools.py +33 -23
  53. universal_mcp/utils/installation.py +201 -10
  54. universal_mcp/utils/openapi.py +229 -46
  55. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
  56. universal_mcp-0.1.8.dist-info/RECORD +81 -0
  57. universal_mcp-0.1.7rc1.dist-info/RECORD +0 -58
  58. /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
  59. /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
  60. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
  61. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,6 @@ from typing import Any, Literal
2
2
 
3
3
  from universal_mcp.applications.application import APIApplication
4
4
  from universal_mcp.integrations import Integration
5
- from loguru import logger
6
5
 
7
6
 
8
7
  class PerplexityApp(APIApplication):
@@ -11,34 +10,6 @@ class PerplexityApp(APIApplication):
11
10
  self.api_key: str | None = None
12
11
  self.base_url = "https://api.perplexity.ai"
13
12
 
14
- def _set_api_key(self):
15
- if self.api_key:
16
- return
17
-
18
- if not self.integration:
19
- raise ValueError("Integration is None. Cannot retrieve Perplexity API Key.")
20
-
21
- credentials = self.integration.get_credentials()
22
- if not credentials or "apiKey" not in credentials:
23
- raise ValueError(
24
- f"Failed to retrieve Perplexity API Key using integration '{self.integration.name}'. "
25
- )
26
-
27
- # if not isinstance(credentials, str) or not credentials.strip():
28
- # raise ValueError(
29
- # f"Invalid credential format received for Perplexity API Key via integration '{self.integration.name}'. "
30
- # )
31
- self.api_key = credentials["apiKey"]
32
-
33
- def _get_headers(self) -> dict[str, str]:
34
- self._set_api_key()
35
- logger.info(f"Perplexity API Key: {self.api_key}")
36
- return {
37
- "Authorization": f"Bearer {self.api_key}",
38
- "Content-Type": "application/json",
39
- "Accept": "application/json",
40
- }
41
-
42
13
  def chat(
43
14
  self,
44
15
  query: str,
@@ -54,34 +25,35 @@ class PerplexityApp(APIApplication):
54
25
  system_prompt: str = "Be precise and concise.",
55
26
  ) -> dict[str, Any] | str:
56
27
  """
57
- Sends a query to a Perplexity Sonar online model and returns the response.
58
-
59
- This uses the chat completions endpoint, suitable for conversational queries
60
- and leveraging Perplexity's online capabilities.
28
+ Initiates a chat completion request to generate AI responses using various models with customizable parameters.
61
29
 
62
30
  Args:
63
- query: The user's query or message.
64
- model: The specific Perplexity model to use (e.g., "r1-1776","sonar","sonar-pro","sonar-reasoning","sonar-reasoning-pro", "sonar-deep-research").Defaults to 'sonar'.
65
- temperature: Sampling temperature for the response generation (e.g., 0.7).
66
- system_prompt: An optional system message to guide the model's behavior.
31
+ query: The input text/prompt to send to the chat model
32
+ model: The model to use for chat completion. Options include 'r1-1776', 'sonar', 'sonar-pro', 'sonar-reasoning', 'sonar-reasoning-pro', 'sonar-deep-research'. Defaults to 'sonar'
33
+ temperature: Controls randomness in the model's output. Higher values make output more random, lower values more deterministic. Defaults to 1
34
+ system_prompt: Initial system message to guide the model's behavior. Defaults to 'Be precise and concise.'
67
35
 
68
36
  Returns:
69
- A dictionary containing 'content' (str) and 'citations' (list) on success, or a string containing an error message on failure.
37
+ A dictionary containing the generated content and citations, with keys 'content' (str) and 'citations' (list), or a string in some cases
38
+
39
+ Raises:
40
+ AuthenticationError: Raised when API authentication fails due to missing or invalid credentials
41
+ HTTPError: Raised when the API request fails or returns an error status
42
+
43
+ Tags:
44
+ chat, generate, ai, completion, important
70
45
  """
71
46
  endpoint = f"{self.base_url}/chat/completions"
72
-
73
47
  messages = []
74
48
  if system_prompt:
75
49
  messages.append({"role": "system", "content": system_prompt})
76
50
  messages.append({"role": "user", "content": query})
77
-
78
51
  payload = {
79
52
  "model": model,
80
53
  "messages": messages,
81
54
  "temperature": temperature,
82
55
  # "max_tokens": 512,
83
56
  }
84
-
85
57
  data = self._post(endpoint, data=payload)
86
58
  response = data.json()
87
59
  content = response["choices"][0]["message"]["content"]
@@ -45,48 +45,48 @@ class RedditApp(APIApplication):
45
45
  def get_subreddit_posts(
46
46
  self, subreddit: str, limit: int = 5, timeframe: str = "day"
47
47
  ) -> str:
48
- """Get the top posts from a specified subreddit over a given timeframe.
48
+ """
49
+ Retrieves and formats top posts from a specified subreddit within a given timeframe using the Reddit API
49
50
 
50
51
  Args:
51
- subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
52
- limit: The maximum number of posts to return (default: 5, max: 100).
53
- timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day').
52
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' prefix
53
+ limit: The maximum number of posts to return (default: 5, max: 100)
54
+ timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day')
54
55
 
55
56
  Returns:
56
- A formatted string listing the top posts or an error message.
57
+ A formatted string containing a numbered list of top posts, including titles, authors, scores, and URLs, or an error message if the request fails
58
+
59
+ Raises:
60
+ RequestException: When the HTTP request to the Reddit API fails
61
+ JSONDecodeError: When the API response contains invalid JSON
62
+
63
+ Tags:
64
+ fetch, reddit, api, list, social-media, important, read-only
57
65
  """
58
66
  valid_timeframes = ["hour", "day", "week", "month", "year", "all"]
59
67
  if timeframe not in valid_timeframes:
60
68
  return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
61
-
62
69
  if not 1 <= limit <= 100:
63
70
  return (
64
71
  f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
65
72
  )
66
-
67
73
  url = f"{self.base_api_url}/r/{subreddit}/top"
68
74
  params = {"limit": limit, "t": timeframe}
69
-
70
75
  logger.info(
71
76
  f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'"
72
77
  )
73
78
  response = self._get(url, params=params)
74
-
75
79
  data = response.json()
76
-
77
80
  if "error" in data:
78
81
  logger.error(
79
82
  f"Reddit API error: {data['error']} - {data.get('message', '')}"
80
83
  )
81
84
  return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
82
-
83
85
  posts = data.get("data", {}).get("children", [])
84
-
85
86
  if not posts:
86
87
  return (
87
88
  f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
88
89
  )
89
-
90
90
  result_lines = [
91
91
  f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"
92
92
  ]
@@ -100,31 +100,36 @@ class RedditApp(APIApplication):
100
100
 
101
101
  result_lines.append(f'{i + 1}. "{title}" by u/{author} (Score: {score})')
102
102
  result_lines.append(f" Link: {full_url}")
103
-
104
103
  return "\n".join(result_lines)
105
104
 
106
105
  def search_subreddits(
107
106
  self, query: str, limit: int = 5, sort: str = "relevance"
108
107
  ) -> str:
109
- """Search for subreddits matching a query string.
108
+ """
109
+ Searches Reddit for subreddits matching a given query string and returns a formatted list of results including subreddit names, subscriber counts, and descriptions.
110
110
 
111
111
  Args:
112
- query: The text to search for in subreddit names and descriptions.
113
- limit: The maximum number of subreddits to return (default: 5, max: 100).
114
- sort: The order of results. Valid options: 'relevance', 'activity' (default: 'relevance').
112
+ query: The text to search for in subreddit names and descriptions
113
+ limit: The maximum number of subreddits to return, between 1 and 100 (default: 5)
114
+ sort: The order of results, either 'relevance' or 'activity' (default: 'relevance')
115
115
 
116
116
  Returns:
117
- A formatted string listing the found subreddits and their descriptions, or an error message.
117
+ A formatted string containing a list of matching subreddits with their names, subscriber counts, and descriptions, or an error message if the search fails or parameters are invalid
118
+
119
+ Raises:
120
+ RequestException: When the HTTP request to Reddit's API fails
121
+ JSONDecodeError: When the API response contains invalid JSON
122
+
123
+ Tags:
124
+ search, important, reddit, api, query, format, list, validation
118
125
  """
119
126
  valid_sorts = ["relevance", "activity"]
120
127
  if sort not in valid_sorts:
121
128
  return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
122
-
123
129
  if not 1 <= limit <= 100:
124
130
  return (
125
131
  f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
126
132
  )
127
-
128
133
  url = f"{self.base_api_url}/subreddits/search"
129
134
  params = {
130
135
  "q": query,
@@ -133,25 +138,19 @@ class RedditApp(APIApplication):
133
138
  # Optionally include NSFW results? Defaulting to false for safety.
134
139
  # "include_over_18": "false"
135
140
  }
136
-
137
141
  logger.info(
138
142
  f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})"
139
143
  )
140
144
  response = self._get(url, params=params)
141
-
142
145
  data = response.json()
143
-
144
146
  if "error" in data:
145
147
  logger.error(
146
148
  f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}"
147
149
  )
148
150
  return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
149
-
150
151
  subreddits = data.get("data", {}).get("children", [])
151
-
152
152
  if not subreddits:
153
153
  return f"No subreddits found matching the query '{query}'."
154
-
155
154
  result_lines = [
156
155
  f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"
157
156
  ]
@@ -173,28 +172,31 @@ class RedditApp(APIApplication):
173
172
  )
174
173
  if description:
175
174
  result_lines.append(f" Description: {description}")
176
-
177
175
  return "\n".join(result_lines)
178
176
 
179
177
  def get_post_flairs(self, subreddit: str):
180
- """Retrieve the list of available post flairs for a specific subreddit.
178
+ """
179
+ Retrieves a list of available post flairs for a specified subreddit using the Reddit API.
181
180
 
182
181
  Args:
183
- subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
182
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' prefix
184
183
 
185
184
  Returns:
186
- A list of dictionaries containing flair details, or an error message.
187
- """
185
+ A list of dictionaries containing flair details if flairs exist, or a string message indicating no flairs are available
188
186
 
189
- url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
187
+ Raises:
188
+ RequestException: When the API request fails or network connectivity issues occur
189
+ JSONDecodeError: When the API response contains invalid JSON data
190
190
 
191
+ Tags:
192
+ fetch, get, reddit, flair, api, read-only
193
+ """
194
+ url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
191
195
  logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
192
196
  response = self._get(url)
193
-
194
197
  flairs = response.json()
195
198
  if not flairs:
196
199
  return f"No post flairs available for r/{subreddit}."
197
-
198
200
  return flairs
199
201
 
200
202
  def create_post(
@@ -206,32 +208,32 @@ class RedditApp(APIApplication):
206
208
  url: str = None,
207
209
  flair_id: str = None,
208
210
  ):
209
- """Create a new post in a specified subreddit.
211
+ """
212
+ Creates a new Reddit post in a specified subreddit with support for text posts, link posts, and image posts
210
213
 
211
214
  Args:
212
- subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
213
- title: The title of the post.
214
- kind: The type of post; either 'self' (text post) or 'link' (link or image post).
215
- text: The text content of the post; required if kind is 'self'.
216
- url: The URL of the link or image; required if kind is 'link'.
217
- For image posts to be displayed correctly, the URL must directly point to an image file
218
- and end with a valid image extension (e.g., .jpg, .png, or .gif).
219
- Note that .gif support can be inconsistent.
220
- flair_id: The ID of the flair to assign to the post.
215
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'
216
+ title: The title of the post
217
+ kind: The type of post; either 'self' (text post) or 'link' (link or image post)
218
+ text: The text content of the post; required if kind is 'self'
219
+ url: The URL of the link or image; required if kind is 'link'. Must end with valid image extension for image posts
220
+ flair_id: The ID of the flair to assign to the post
221
221
 
222
222
  Returns:
223
- The JSON response from the Reddit API, or an error message as a string.
224
- If the reddit api returns an error within the json response, that error will be returned as a string.
225
- """
223
+ The JSON response from the Reddit API, or an error message as a string if the API returns an error
226
224
 
225
+ Raises:
226
+ ValueError: Raised when kind is invalid or when required parameters (text for self posts, url for link posts) are missing
227
+
228
+ Tags:
229
+ create, post, social-media, reddit, api, important
230
+ """
227
231
  if kind not in ["self", "link"]:
228
232
  raise ValueError("Invalid post kind. Must be one of 'self' or 'link'.")
229
-
230
233
  if kind == "self" and not text:
231
234
  raise ValueError("Text content is required for text posts.")
232
235
  if kind == "link" and not url:
233
236
  raise ValueError("URL is required for link posts (including images).")
234
-
235
237
  data = {
236
238
  "sr": subreddit,
237
239
  "title": title,
@@ -241,13 +243,10 @@ class RedditApp(APIApplication):
241
243
  "flair_id": flair_id,
242
244
  }
243
245
  data = {k: v for k, v in data.items() if v is not None}
244
-
245
246
  url_api = f"{self.base_api_url}/api/submit"
246
247
  logger.info(f"Submitting a new post to r/{subreddit}")
247
248
  response = self._post(url_api, data=data)
248
249
  response_json = response.json()
249
-
250
- # Check for Reddit API errors in the response
251
250
  if (
252
251
  response_json
253
252
  and "json" in response_json
@@ -259,27 +258,27 @@ class RedditApp(APIApplication):
259
258
  [f"{code}: {message}" for code, message in errors]
260
259
  )
261
260
  return f"Reddit API error: {error_message}"
262
-
263
261
  return response_json
264
262
 
265
263
  def get_comment_by_id(self, comment_id: str) -> dict:
266
264
  """
267
- Retrieve a specific Reddit comment by its full ID (t1_commentid).
265
+ Retrieves a specific Reddit comment using its unique identifier.
268
266
 
269
267
  Args:
270
- comment_id: The full unique ID of the comment (e.g., 't1_abcdef').
268
+ comment_id: The full unique identifier of the comment (prefixed with 't1_', e.g., 't1_abcdef')
271
269
 
272
270
  Returns:
273
- A dictionary containing the comment data, or an error message if retrieval fails.
274
- """
275
-
276
- # Define the endpoint URL
277
- url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
271
+ A dictionary containing the comment data including attributes like author, body, score, etc. If the comment is not found, returns a dictionary with an error message.
278
272
 
279
- # Make the GET request to the Reddit API
273
+ Raises:
274
+ HTTPError: When the Reddit API request fails due to network issues or invalid authentication
275
+ JSONDecodeError: When the API response cannot be parsed as valid JSON
280
276
 
277
+ Tags:
278
+ retrieve, get, reddit, comment, api, fetch, single-item, important
279
+ """
280
+ url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
281
281
  response = self._get(url)
282
-
283
282
  data = response.json()
284
283
  comments = data.get("data", {}).get("children", [])
285
284
  if comments:
@@ -289,72 +288,82 @@ class RedditApp(APIApplication):
289
288
 
290
289
  def post_comment(self, parent_id: str, text: str) -> dict:
291
290
  """
292
- Post a comment to a Reddit post or another comment.
291
+ Posts a comment to a Reddit post or comment using the Reddit API
293
292
 
294
293
  Args:
295
- parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
296
- text: The text content of the comment.
294
+ parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
295
+ text: The text content of the comment to be posted
297
296
 
298
297
  Returns:
299
- A dictionary containing the response from the Reddit API, or an error message if posting fails.
300
- """
298
+ A dictionary containing the Reddit API response with details about the posted comment
299
+
300
+ Raises:
301
+ RequestException: If the API request fails or returns an error status code
302
+ JSONDecodeError: If the API response cannot be parsed as JSON
301
303
 
304
+ Tags:
305
+ post, comment, social, reddit, api, important
306
+ """
302
307
  url = f"{self.base_api_url}/api/comment"
303
308
  data = {
304
309
  "parent": parent_id,
305
310
  "text": text,
306
311
  }
307
-
308
312
  logger.info(f"Posting comment to {parent_id}")
309
313
  response = self._post(url, data=data)
310
-
311
314
  return response.json()
312
315
 
313
316
  def edit_content(self, content_id: str, text: str) -> dict:
314
317
  """
315
- Edit the text content of a Reddit post or comment.
318
+ Edits the text content of an existing Reddit post or comment using the Reddit API
316
319
 
317
320
  Args:
318
- content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
319
- text: The new text content.
321
+ content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
322
+ text: The new text content to replace the existing content
320
323
 
321
324
  Returns:
322
- A dictionary containing the response from the Reddit API, or an error message if editing fails.
323
- """
325
+ A dictionary containing the API response with details about the edited content
324
326
 
327
+ Raises:
328
+ RequestException: When the API request fails or network connectivity issues occur
329
+ ValueError: When invalid content_id format or empty text is provided
330
+
331
+ Tags:
332
+ edit, update, content, reddit, api, important
333
+ """
325
334
  url = f"{self.base_api_url}/api/editusertext"
326
335
  data = {
327
336
  "thing_id": content_id,
328
337
  "text": text,
329
338
  }
330
-
331
339
  logger.info(f"Editing content {content_id}")
332
340
  response = self._post(url, data=data)
333
-
334
341
  return response.json()
335
342
 
336
343
  def delete_content(self, content_id: str) -> dict:
337
344
  """
338
- Delete a Reddit post or comment.
345
+ Deletes a specified Reddit post or comment using the Reddit API.
339
346
 
340
347
  Args:
341
- content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
348
+ content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
342
349
 
343
350
  Returns:
344
- A dictionary containing the response from the Reddit API, or an error message if deletion fails.
345
- """
351
+ A dictionary containing a success message with the deleted content ID
352
+
353
+ Raises:
354
+ HTTPError: When the API request fails or returns an error status code
355
+ RequestException: When there are network connectivity issues or API communication problems
346
356
 
357
+ Tags:
358
+ delete, content-management, api, reddit, important
359
+ """
347
360
  url = f"{self.base_api_url}/api/del"
348
361
  data = {
349
362
  "id": content_id,
350
363
  }
351
-
352
364
  logger.info(f"Deleting content {content_id}")
353
365
  response = self._post(url, data=data)
354
366
  response.raise_for_status()
355
-
356
- # Reddit's delete endpoint returns an empty response on success.
357
- # We'll just return a success message.
358
367
  return {"message": f"Content {content_id} deleted successfully."}
359
368
 
360
369
  def list_tools(self):
@@ -6,24 +6,23 @@ class ResendApp(APIApplication):
6
6
  def __init__(self, integration: Integration) -> None:
7
7
  super().__init__(name="resend", integration=integration)
8
8
 
9
- def _get_headers(self):
10
- credentials = self.integration.get_credentials()
11
- if not credentials:
12
- raise ValueError("No credentials found")
13
- return {
14
- "Authorization": f"Bearer {credentials['api_key']}",
15
- }
16
-
17
9
  def send_email(self, to: str, subject: str, content: str) -> str:
18
- """Send an email using the Resend API
10
+ """
11
+ Sends an email using the Resend API with specified recipient, subject, and content
19
12
 
20
13
  Args:
21
- to: The email address to send the email to
22
- subject: The subject of the email
23
- content: The content of the email
14
+ to: Email address of the recipient
15
+ subject: Subject line of the email
16
+ content: Main body text content of the email
24
17
 
25
18
  Returns:
26
- A message indicating that the email was sent successfully
19
+ String message confirming successful email delivery ('Sent Successfully')
20
+
21
+ Raises:
22
+ ValueError: Raised when no valid credentials are found for the API
23
+
24
+ Tags:
25
+ send, email, api, communication, important
27
26
  """
28
27
  credentials = self.integration.get_credentials()
29
28
  if not credentials:
@@ -1,49 +1,38 @@
1
1
  import httpx
2
- from loguru import logger
3
2
  from serpapi import SerpApiClient as SerpApiSearch
4
3
 
5
4
  from universal_mcp.applications.application import APIApplication
6
5
 
7
6
 
8
- class SerpApp(APIApplication):
7
+ class SerpapiApp(APIApplication):
9
8
  def __init__(self, **kwargs):
10
9
  super().__init__(name="serpapi", **kwargs)
11
- self.api_key: str | None = None
12
-
13
- def _set_api_key(self):
14
- if self.api_key is not None:
15
- return
16
- if not self.integration:
17
- raise ValueError("Integration is None. Cannot retrieve SERP API Key.")
18
-
19
- credentials = self.integration.get_credentials()
20
- if not credentials:
21
- raise ValueError(
22
- f"Failed to retrieve SERP API Key using integration '{self.integration.name}'. "
23
- f"Check store configuration (e.g., ensure the correct environment variable is set)."
24
- )
25
-
26
- self.api_key = credentials
27
- logger.info("SERP API Key successfully retrieved via integration.")
28
10
 
29
11
  async def search(self, params: dict[str, any] = None) -> str:
30
- """Perform a search on the specified engine using SerpApi.
12
+ """
13
+ Performs an asynchronous search using the SerpApi service and returns formatted search results.
31
14
 
32
15
  Args:
33
- params: Dictionary of engine-specific parameters (e.g., {"q": "Coffee", "engine": "google_light", "location": "Austin, TX"}).
16
+ params: Dictionary of engine-specific parameters (e.g., {'q': 'Coffee', 'engine': 'google_light', 'location': 'Austin, TX'}). Defaults to None.
34
17
 
35
18
  Returns:
36
- A formatted string of search results or an error message.
19
+ A formatted string containing search results with titles, links, and snippets, or an error message if the search fails.
20
+
21
+ Raises:
22
+ httpx.HTTPStatusError: Raised when the API request fails due to HTTP errors (401 for invalid API key, 429 for rate limiting)
23
+ Exception: Raised for general errors such as network issues or invalid parameters
24
+
25
+ Tags:
26
+ search, async, web-scraping, api, serpapi, important
37
27
  """
38
28
  if params is None:
39
29
  params = {}
40
- self._set_api_key()
30
+ api_key = self.integration.get_credentials().get("api_key")
41
31
  params = {
42
- "api_key": self.api_key,
32
+ "api_key": api_key,
43
33
  "engine": "google_light", # Fastest engine by default
44
34
  **params, # Include any additional parameters
45
35
  }
46
-
47
36
  try:
48
37
  search = SerpApiSearch(params)
49
38
  data = search.get_dict()
@@ -8,25 +8,23 @@ class TavilyApp(APIApplication):
8
8
  self.base_url = "https://api.tavily.com"
9
9
  super().__init__(name=name, integration=integration)
10
10
 
11
- def _get_headers(self):
12
- credentials = self.integration.get_credentials()
13
- if not credentials:
14
- raise ValueError("No credentials found")
15
- return {
16
- "Authorization": f"Bearer {credentials['api_key']}",
17
- "Content-Type": "application/json",
18
- }
19
-
20
11
  def search(self, query: str) -> str:
21
- """Search the web using Tavily's search API
12
+ """
13
+ Performs a web search using Tavily's search API and returns either a direct answer or a summary of top results.
22
14
 
23
15
  Args:
24
- query: The search query
16
+ query: The search query string to be processed by Tavily's search engine
25
17
 
26
18
  Returns:
27
- str: A summary of search results
19
+ A string containing either a direct answer from Tavily's AI or a formatted summary of the top 3 search results, with each result containing the title and snippet
20
+
21
+ Raises:
22
+ ValueError: When authentication credentials are invalid or missing (via validate() method)
23
+ HTTPError: When the API request fails or returns an error response
24
+
25
+ Tags:
26
+ search, ai, web, query, important, api-client, text-processing
28
27
  """
29
- self.validate()
30
28
  url = f"{self.base_url}/search"
31
29
  payload = {
32
30
  "query": query,
@@ -40,18 +38,13 @@ class TavilyApp(APIApplication):
40
38
  "include_domains": [],
41
39
  "exclude_domains": [],
42
40
  }
43
-
44
41
  response = self._post(url, payload)
45
42
  result = response.json()
46
-
47
43
  if "answer" in result:
48
44
  return result["answer"]
49
-
50
- # Fallback to combining top results if no direct answer
51
45
  summaries = []
52
46
  for item in result.get("results", [])[:3]:
53
47
  summaries.append(f"• {item['title']}: {item['snippet']}")
54
-
55
48
  return "\n".join(summaries)
56
49
 
57
50
  def list_tools(self):