universal-mcp 0.1.7rc2__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.
- universal_mcp/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/ahrefs/README.md +76 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +95 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +1195 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/e2b/app.py +14 -35
- universal_mcp/applications/figma/README.md +74 -0
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +1261 -0
- universal_mcp/applications/firecrawl/app.py +29 -32
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -138
- universal_mcp/applications/google_docs/app.py +47 -52
- universal_mcp/applications/google_drive/app.py +119 -113
- universal_mcp/applications/google_mail/app.py +124 -50
- universal_mcp/applications/google_sheet/app.py +89 -91
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +13 -45
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -23
- universal_mcp/applications/{serp → serpapi}/app.py +14 -33
- universal_mcp/applications/tavily/app.py +11 -28
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1372 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1428 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +143 -30
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +201 -146
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
- universal_mcp-0.1.8.dist-info/RECORD +81 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.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,38 +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:
|
23
|
-
raise ValueError(
|
24
|
-
f"Failed to retrieve Perplexity API Key using integration '{self.integration.name}'. "
|
25
|
-
)
|
26
|
-
api_key = (
|
27
|
-
credentials.get("api_key")
|
28
|
-
or credentials.get("API_KEY")
|
29
|
-
or credentials.get("apiKey")
|
30
|
-
)
|
31
|
-
if not api_key:
|
32
|
-
raise ValueError(
|
33
|
-
f"Invalid credential format received for Perplexity API Key via integration '{self.integration.name}'. "
|
34
|
-
)
|
35
|
-
self.api_key = api_key
|
36
|
-
|
37
|
-
def _get_headers(self) -> dict[str, str]:
|
38
|
-
self._set_api_key()
|
39
|
-
logger.info(f"Perplexity API Key: {self.api_key}")
|
40
|
-
return {
|
41
|
-
"Authorization": f"Bearer {self.api_key}",
|
42
|
-
"Content-Type": "application/json",
|
43
|
-
"Accept": "application/json",
|
44
|
-
}
|
45
|
-
|
46
13
|
def chat(
|
47
14
|
self,
|
48
15
|
query: str,
|
@@ -58,34 +25,35 @@ class PerplexityApp(APIApplication):
|
|
58
25
|
system_prompt: str = "Be precise and concise.",
|
59
26
|
) -> dict[str, Any] | str:
|
60
27
|
"""
|
61
|
-
|
62
|
-
|
63
|
-
This uses the chat completions endpoint, suitable for conversational queries
|
64
|
-
and leveraging Perplexity's online capabilities.
|
28
|
+
Initiates a chat completion request to generate AI responses using various models with customizable parameters.
|
65
29
|
|
66
30
|
Args:
|
67
|
-
query: The
|
68
|
-
model: The
|
69
|
-
temperature:
|
70
|
-
system_prompt:
|
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.'
|
71
35
|
|
72
36
|
Returns:
|
73
|
-
A dictionary containing 'content' (str) and 'citations' (list)
|
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
|
74
45
|
"""
|
75
46
|
endpoint = f"{self.base_url}/chat/completions"
|
76
|
-
|
77
47
|
messages = []
|
78
48
|
if system_prompt:
|
79
49
|
messages.append({"role": "system", "content": system_prompt})
|
80
50
|
messages.append({"role": "user", "content": query})
|
81
|
-
|
82
51
|
payload = {
|
83
52
|
"model": model,
|
84
53
|
"messages": messages,
|
85
54
|
"temperature": temperature,
|
86
55
|
# "max_tokens": 512,
|
87
56
|
}
|
88
|
-
|
89
57
|
data = self._post(endpoint, data=payload)
|
90
58
|
response = data.json()
|
91
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
|
-
"""
|
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
|
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
|
-
"""
|
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
|
114
|
-
sort: The order of results
|
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
|
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
|
-
"""
|
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
|
187
|
-
"""
|
185
|
+
A list of dictionaries containing flair details if flairs exist, or a string message indicating no flairs are available
|
188
186
|
|
189
|
-
|
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
|
-
"""
|
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
|
-
|
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
|
-
|
265
|
+
Retrieves a specific Reddit comment using its unique identifier.
|
268
266
|
|
269
267
|
Args:
|
270
|
-
comment_id: The full unique
|
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,
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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):
|
@@ -5,35 +5,24 @@ from universal_mcp.integrations import Integration
|
|
5
5
|
class ResendApp(APIApplication):
|
6
6
|
def __init__(self, integration: Integration) -> None:
|
7
7
|
super().__init__(name="resend", integration=integration)
|
8
|
-
self.api_key = None
|
9
|
-
|
10
|
-
def _get_headers(self):
|
11
|
-
if not self.api_key:
|
12
|
-
credentials = self.integration.get_credentials()
|
13
|
-
if not credentials:
|
14
|
-
raise ValueError("No credentials found")
|
15
|
-
api_key = (
|
16
|
-
credentials.get("api_key")
|
17
|
-
or credentials.get("API_KEY")
|
18
|
-
or credentials.get("apiKey")
|
19
|
-
)
|
20
|
-
if not api_key:
|
21
|
-
raise ValueError("No API key found")
|
22
|
-
self.api_key = api_key
|
23
|
-
return {
|
24
|
-
"Authorization": f"Bearer {self.api_key}",
|
25
|
-
}
|
26
8
|
|
27
9
|
def send_email(self, to: str, subject: str, content: str) -> str:
|
28
|
-
"""
|
10
|
+
"""
|
11
|
+
Sends an email using the Resend API with specified recipient, subject, and content
|
29
12
|
|
30
13
|
Args:
|
31
|
-
to:
|
32
|
-
subject:
|
33
|
-
content:
|
14
|
+
to: Email address of the recipient
|
15
|
+
subject: Subject line of the email
|
16
|
+
content: Main body text content of the email
|
34
17
|
|
35
18
|
Returns:
|
36
|
-
|
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
|
37
26
|
"""
|
38
27
|
credentials = self.integration.get_credentials()
|
39
28
|
if not credentials:
|
@@ -1,57 +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
|
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
|
-
api_key = (
|
26
|
-
credentials.get("api_key")
|
27
|
-
or credentials.get("API_KEY")
|
28
|
-
or credentials.get("apiKey")
|
29
|
-
)
|
30
|
-
if not api_key:
|
31
|
-
raise ValueError(
|
32
|
-
f"Invalid credential format received for SERP API Key via integration '{self.integration.name}'. "
|
33
|
-
)
|
34
|
-
self.api_key = api_key
|
35
|
-
logger.info("SERP API Key successfully retrieved via integration.")
|
36
10
|
|
37
11
|
async def search(self, params: dict[str, any] = None) -> str:
|
38
|
-
"""
|
12
|
+
"""
|
13
|
+
Performs an asynchronous search using the SerpApi service and returns formatted search results.
|
39
14
|
|
40
15
|
Args:
|
41
|
-
params: Dictionary of engine-specific parameters (e.g., {
|
16
|
+
params: Dictionary of engine-specific parameters (e.g., {'q': 'Coffee', 'engine': 'google_light', 'location': 'Austin, TX'}). Defaults to None.
|
42
17
|
|
43
18
|
Returns:
|
44
|
-
A formatted string
|
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
|
45
27
|
"""
|
46
28
|
if params is None:
|
47
29
|
params = {}
|
48
|
-
self.
|
30
|
+
api_key = self.integration.get_credentials().get("api_key")
|
49
31
|
params = {
|
50
|
-
"api_key":
|
32
|
+
"api_key": api_key,
|
51
33
|
"engine": "google_light", # Fastest engine by default
|
52
34
|
**params, # Include any additional parameters
|
53
35
|
}
|
54
|
-
|
55
36
|
try:
|
56
37
|
search = SerpApiSearch(params)
|
57
38
|
data = search.get_dict()
|
@@ -7,36 +7,24 @@ class TavilyApp(APIApplication):
|
|
7
7
|
name = "tavily"
|
8
8
|
self.base_url = "https://api.tavily.com"
|
9
9
|
super().__init__(name=name, integration=integration)
|
10
|
-
self.api_key = None
|
11
|
-
|
12
|
-
def _get_headers(self):
|
13
|
-
if not self.api_key:
|
14
|
-
credentials = self.integration.get_credentials()
|
15
|
-
if not credentials:
|
16
|
-
raise ValueError("No credentials found")
|
17
|
-
api_key = (
|
18
|
-
credentials.get("api_key")
|
19
|
-
or credentials.get("API_KEY")
|
20
|
-
or credentials.get("apiKey")
|
21
|
-
)
|
22
|
-
if not api_key:
|
23
|
-
raise ValueError("No API key found")
|
24
|
-
self.api_key = api_key
|
25
|
-
return {
|
26
|
-
"Authorization": f"Bearer {self.api_key}",
|
27
|
-
"Content-Type": "application/json",
|
28
|
-
}
|
29
10
|
|
30
11
|
def search(self, query: str) -> str:
|
31
|
-
"""
|
12
|
+
"""
|
13
|
+
Performs a web search using Tavily's search API and returns either a direct answer or a summary of top results.
|
32
14
|
|
33
15
|
Args:
|
34
|
-
query: The search query
|
16
|
+
query: The search query string to be processed by Tavily's search engine
|
35
17
|
|
36
18
|
Returns:
|
37
|
-
|
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
|
38
27
|
"""
|
39
|
-
self.validate()
|
40
28
|
url = f"{self.base_url}/search"
|
41
29
|
payload = {
|
42
30
|
"query": query,
|
@@ -50,18 +38,13 @@ class TavilyApp(APIApplication):
|
|
50
38
|
"include_domains": [],
|
51
39
|
"exclude_domains": [],
|
52
40
|
}
|
53
|
-
|
54
41
|
response = self._post(url, payload)
|
55
42
|
result = response.json()
|
56
|
-
|
57
43
|
if "answer" in result:
|
58
44
|
return result["answer"]
|
59
|
-
|
60
|
-
# Fallback to combining top results if no direct answer
|
61
45
|
summaries = []
|
62
46
|
for item in result.get("results", [])[:3]:
|
63
47
|
summaries.append(f"• {item['title']}: {item['snippet']}")
|
64
|
-
|
65
48
|
return "\n".join(summaries)
|
66
49
|
|
67
50
|
def list_tools(self):
|