universal-mcp 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/applications/__init__.py +23 -28
- universal_mcp/applications/application.py +13 -8
- universal_mcp/applications/e2b/app.py +74 -0
- universal_mcp/applications/firecrawl/app.py +381 -0
- universal_mcp/applications/github/README.md +35 -0
- universal_mcp/applications/github/app.py +133 -100
- universal_mcp/applications/google_calendar/app.py +170 -139
- universal_mcp/applications/google_mail/app.py +185 -160
- universal_mcp/applications/markitdown/app.py +32 -0
- universal_mcp/applications/notion/README.md +32 -0
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +415 -0
- universal_mcp/applications/reddit/app.py +112 -71
- universal_mcp/applications/resend/app.py +3 -8
- universal_mcp/applications/serp/app.py +84 -0
- universal_mcp/applications/tavily/app.py +11 -10
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/cli.py +98 -16
- universal_mcp/config.py +20 -3
- universal_mcp/exceptions.py +1 -3
- universal_mcp/integrations/__init__.py +6 -2
- universal_mcp/integrations/agentr.py +26 -24
- universal_mcp/integrations/integration.py +72 -35
- universal_mcp/servers/__init__.py +21 -1
- universal_mcp/servers/server.py +77 -44
- universal_mcp/stores/__init__.py +15 -2
- universal_mcp/stores/store.py +123 -13
- universal_mcp/utils/__init__.py +1 -0
- universal_mcp/utils/api_generator.py +269 -0
- universal_mcp/utils/docgen.py +360 -0
- universal_mcp/utils/installation.py +17 -2
- universal_mcp/utils/openapi.py +216 -111
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/METADATA +23 -5
- universal_mcp-0.1.2.dist-info/RECORD +40 -0
- universal_mcp-0.1.1.dist-info/RECORD +0 -29
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,10 @@
|
|
1
1
|
import httpx
|
2
|
+
from loguru import logger
|
3
|
+
|
2
4
|
from universal_mcp.applications.application import APIApplication
|
3
|
-
from universal_mcp.integrations import Integration
|
4
5
|
from universal_mcp.exceptions import NotAuthorizedError
|
5
|
-
from
|
6
|
+
from universal_mcp.integrations import Integration
|
7
|
+
|
6
8
|
|
7
9
|
class RedditApp(APIApplication):
|
8
10
|
def __init__(self, integration: Integration) -> None:
|
@@ -32,126 +34,146 @@ class RedditApp(APIApplication):
|
|
32
34
|
raise ValueError("Integration not configured for RedditApp")
|
33
35
|
credentials = self.integration.get_credentials()
|
34
36
|
if "access_token" not in credentials:
|
35
|
-
|
36
|
-
|
37
|
+
logger.error("Reddit credentials found but missing 'access_token'.")
|
38
|
+
raise ValueError("Invalid Reddit credentials format.")
|
37
39
|
|
38
40
|
return {
|
39
41
|
"Authorization": f"Bearer {credentials['access_token']}",
|
40
|
-
"User-Agent": "agentr-reddit-app/0.1 by AgentR"
|
42
|
+
"User-Agent": "agentr-reddit-app/0.1 by AgentR",
|
41
43
|
}
|
42
|
-
|
43
|
-
def get_subreddit_posts(
|
44
|
+
|
45
|
+
def get_subreddit_posts(
|
46
|
+
self, subreddit: str, limit: int = 5, timeframe: str = "day"
|
47
|
+
) -> str:
|
44
48
|
"""Get the top posts from a specified subreddit over a given timeframe.
|
45
|
-
|
49
|
+
|
46
50
|
Args:
|
47
51
|
subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
|
48
52
|
limit: The maximum number of posts to return (default: 5, max: 100).
|
49
53
|
timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day').
|
50
|
-
|
54
|
+
|
51
55
|
Returns:
|
52
56
|
A formatted string listing the top posts or an error message.
|
53
57
|
"""
|
54
|
-
valid_timeframes = [
|
58
|
+
valid_timeframes = ["hour", "day", "week", "month", "year", "all"]
|
55
59
|
if timeframe not in valid_timeframes:
|
56
60
|
return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
|
57
|
-
|
61
|
+
|
58
62
|
if not 1 <= limit <= 100:
|
59
|
-
|
63
|
+
return (
|
64
|
+
f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
|
65
|
+
)
|
60
66
|
|
61
|
-
|
62
67
|
url = f"{self.base_api_url}/r/{subreddit}/top"
|
63
|
-
params = {
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
logger.info(f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'")
|
68
|
+
params = {"limit": limit, "t": timeframe}
|
69
|
+
|
70
|
+
logger.info(
|
71
|
+
f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'"
|
72
|
+
)
|
69
73
|
response = self._get(url, params=params)
|
70
74
|
|
71
75
|
data = response.json()
|
72
|
-
|
76
|
+
|
73
77
|
if "error" in data:
|
74
|
-
|
75
|
-
|
78
|
+
logger.error(
|
79
|
+
f"Reddit API error: {data['error']} - {data.get('message', '')}"
|
80
|
+
)
|
81
|
+
return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
|
76
82
|
|
77
83
|
posts = data.get("data", {}).get("children", [])
|
78
|
-
|
84
|
+
|
79
85
|
if not posts:
|
80
|
-
return
|
86
|
+
return (
|
87
|
+
f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
|
88
|
+
)
|
81
89
|
|
82
|
-
result_lines = [
|
90
|
+
result_lines = [
|
91
|
+
f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"
|
92
|
+
]
|
83
93
|
for i, post_container in enumerate(posts):
|
84
94
|
post = post_container.get("data", {})
|
85
|
-
title = post.get(
|
86
|
-
score = post.get(
|
87
|
-
author = post.get(
|
88
|
-
permalink = post.get(
|
95
|
+
title = post.get("title", "No Title")
|
96
|
+
score = post.get("score", 0)
|
97
|
+
author = post.get("author", "Unknown Author")
|
98
|
+
permalink = post.get("permalink", "")
|
89
99
|
full_url = f"https://www.reddit.com{permalink}" if permalink else "No Link"
|
90
|
-
|
91
|
-
result_lines.append(f
|
100
|
+
|
101
|
+
result_lines.append(f'{i + 1}. "{title}" by u/{author} (Score: {score})')
|
92
102
|
result_lines.append(f" Link: {full_url}")
|
93
103
|
|
94
104
|
return "\n".join(result_lines)
|
95
105
|
|
96
|
-
|
97
|
-
|
106
|
+
def search_subreddits(
|
107
|
+
self, query: str, limit: int = 5, sort: str = "relevance"
|
108
|
+
) -> str:
|
98
109
|
"""Search for subreddits matching a query string.
|
99
|
-
|
110
|
+
|
100
111
|
Args:
|
101
112
|
query: The text to search for in subreddit names and descriptions.
|
102
113
|
limit: The maximum number of subreddits to return (default: 5, max: 100).
|
103
114
|
sort: The order of results. Valid options: 'relevance', 'activity' (default: 'relevance').
|
104
|
-
|
115
|
+
|
105
116
|
Returns:
|
106
117
|
A formatted string listing the found subreddits and their descriptions, or an error message.
|
107
118
|
"""
|
108
|
-
valid_sorts = [
|
119
|
+
valid_sorts = ["relevance", "activity"]
|
109
120
|
if sort not in valid_sorts:
|
110
121
|
return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
|
111
|
-
|
122
|
+
|
112
123
|
if not 1 <= limit <= 100:
|
113
|
-
|
124
|
+
return (
|
125
|
+
f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
|
126
|
+
)
|
114
127
|
|
115
|
-
|
116
128
|
url = f"{self.base_api_url}/subreddits/search"
|
117
129
|
params = {
|
118
130
|
"q": query,
|
119
131
|
"limit": limit,
|
120
132
|
"sort": sort,
|
121
133
|
# Optionally include NSFW results? Defaulting to false for safety.
|
122
|
-
# "include_over_18": "false"
|
134
|
+
# "include_over_18": "false"
|
123
135
|
}
|
124
|
-
|
125
|
-
logger.info(
|
136
|
+
|
137
|
+
logger.info(
|
138
|
+
f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})"
|
139
|
+
)
|
126
140
|
response = self._get(url, params=params)
|
127
|
-
|
141
|
+
|
128
142
|
data = response.json()
|
129
143
|
|
130
144
|
if "error" in data:
|
131
|
-
|
132
|
-
|
145
|
+
logger.error(
|
146
|
+
f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}"
|
147
|
+
)
|
148
|
+
return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
|
133
149
|
|
134
150
|
subreddits = data.get("data", {}).get("children", [])
|
135
|
-
|
151
|
+
|
136
152
|
if not subreddits:
|
137
153
|
return f"No subreddits found matching the query '{query}'."
|
138
154
|
|
139
|
-
result_lines = [
|
155
|
+
result_lines = [
|
156
|
+
f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"
|
157
|
+
]
|
140
158
|
for i, sub_container in enumerate(subreddits):
|
141
159
|
sub_data = sub_container.get("data", {})
|
142
|
-
display_name = sub_data.get(
|
143
|
-
title = sub_data.get(
|
144
|
-
|
160
|
+
display_name = sub_data.get("display_name", "N/A") # e.g., 'python'
|
161
|
+
title = sub_data.get(
|
162
|
+
"title", "No Title"
|
163
|
+
) # Often the same as display_name or slightly longer
|
164
|
+
subscribers = sub_data.get("subscribers", 0)
|
145
165
|
# Use public_description if available, fallback to title
|
146
|
-
description = sub_data.get(
|
147
|
-
|
166
|
+
description = sub_data.get("public_description", "").strip() or title
|
167
|
+
|
148
168
|
# Format subscriber count nicely
|
149
169
|
subscriber_str = f"{subscribers:,}" if subscribers else "Unknown"
|
150
|
-
|
151
|
-
result_lines.append(
|
170
|
+
|
171
|
+
result_lines.append(
|
172
|
+
f"{i + 1}. r/{display_name} ({subscriber_str} subscribers)"
|
173
|
+
)
|
152
174
|
if description:
|
153
175
|
result_lines.append(f" Description: {description}")
|
154
|
-
|
176
|
+
|
155
177
|
return "\n".join(result_lines)
|
156
178
|
|
157
179
|
def get_post_flairs(self, subreddit: str):
|
@@ -163,9 +185,9 @@ class RedditApp(APIApplication):
|
|
163
185
|
Returns:
|
164
186
|
A list of dictionaries containing flair details, or an error message.
|
165
187
|
"""
|
166
|
-
|
188
|
+
|
167
189
|
url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
|
168
|
-
|
190
|
+
|
169
191
|
logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
|
170
192
|
response = self._get(url)
|
171
193
|
|
@@ -174,8 +196,16 @@ class RedditApp(APIApplication):
|
|
174
196
|
return f"No post flairs available for r/{subreddit}."
|
175
197
|
|
176
198
|
return flairs
|
177
|
-
|
178
|
-
def create_post(
|
199
|
+
|
200
|
+
def create_post(
|
201
|
+
self,
|
202
|
+
subreddit: str,
|
203
|
+
title: str,
|
204
|
+
kind: str = "self",
|
205
|
+
text: str = None,
|
206
|
+
url: str = None,
|
207
|
+
flair_id: str = None,
|
208
|
+
):
|
179
209
|
"""Create a new post in a specified subreddit.
|
180
210
|
|
181
211
|
Args:
|
@@ -186,7 +216,7 @@ class RedditApp(APIApplication):
|
|
186
216
|
url: The URL of the link or image; required if kind is 'link'.
|
187
217
|
For image posts to be displayed correctly, the URL must directly point to an image file
|
188
218
|
and end with a valid image extension (e.g., .jpg, .png, or .gif).
|
189
|
-
Note that .gif support can be inconsistent.
|
219
|
+
Note that .gif support can be inconsistent.
|
190
220
|
flair_id: The ID of the flair to assign to the post.
|
191
221
|
|
192
222
|
Returns:
|
@@ -218,10 +248,16 @@ class RedditApp(APIApplication):
|
|
218
248
|
response_json = response.json()
|
219
249
|
|
220
250
|
# Check for Reddit API errors in the response
|
221
|
-
if
|
251
|
+
if (
|
252
|
+
response_json
|
253
|
+
and "json" in response_json
|
254
|
+
and "errors" in response_json["json"]
|
255
|
+
):
|
222
256
|
errors = response_json["json"]["errors"]
|
223
257
|
if errors:
|
224
|
-
error_message = ", ".join(
|
258
|
+
error_message = ", ".join(
|
259
|
+
[f"{code}: {message}" for code, message in errors]
|
260
|
+
)
|
225
261
|
return f"Reddit API error: {error_message}"
|
226
262
|
|
227
263
|
return response_json
|
@@ -241,7 +277,7 @@ class RedditApp(APIApplication):
|
|
241
277
|
url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
|
242
278
|
|
243
279
|
# Make the GET request to the Reddit API
|
244
|
-
|
280
|
+
|
245
281
|
response = self._get(url)
|
246
282
|
|
247
283
|
data = response.json()
|
@@ -250,7 +286,7 @@ class RedditApp(APIApplication):
|
|
250
286
|
return comments[0]["data"]
|
251
287
|
else:
|
252
288
|
return {"error": "Comment not found."}
|
253
|
-
|
289
|
+
|
254
290
|
def post_comment(self, parent_id: str, text: str) -> dict:
|
255
291
|
"""
|
256
292
|
Post a comment to a Reddit post or another comment.
|
@@ -262,7 +298,7 @@ class RedditApp(APIApplication):
|
|
262
298
|
Returns:
|
263
299
|
A dictionary containing the response from the Reddit API, or an error message if posting fails.
|
264
300
|
"""
|
265
|
-
|
301
|
+
|
266
302
|
url = f"{self.base_api_url}/api/comment"
|
267
303
|
data = {
|
268
304
|
"parent": parent_id,
|
@@ -285,7 +321,7 @@ class RedditApp(APIApplication):
|
|
285
321
|
Returns:
|
286
322
|
A dictionary containing the response from the Reddit API, or an error message if editing fails.
|
287
323
|
"""
|
288
|
-
|
324
|
+
|
289
325
|
url = f"{self.base_api_url}/api/editusertext"
|
290
326
|
data = {
|
291
327
|
"thing_id": content_id,
|
@@ -297,7 +333,6 @@ class RedditApp(APIApplication):
|
|
297
333
|
|
298
334
|
return response.json()
|
299
335
|
|
300
|
-
|
301
336
|
def delete_content(self, content_id: str) -> dict:
|
302
337
|
"""
|
303
338
|
Delete a Reddit post or comment.
|
@@ -308,7 +343,7 @@ class RedditApp(APIApplication):
|
|
308
343
|
Returns:
|
309
344
|
A dictionary containing the response from the Reddit API, or an error message if deletion fails.
|
310
345
|
"""
|
311
|
-
|
346
|
+
|
312
347
|
url = f"{self.base_api_url}/api/del"
|
313
348
|
data = {
|
314
349
|
"id": content_id,
|
@@ -324,6 +359,12 @@ class RedditApp(APIApplication):
|
|
324
359
|
|
325
360
|
def list_tools(self):
|
326
361
|
return [
|
327
|
-
self.get_subreddit_posts,
|
328
|
-
self.
|
329
|
-
|
362
|
+
self.get_subreddit_posts,
|
363
|
+
self.search_subreddits,
|
364
|
+
self.get_post_flairs,
|
365
|
+
self.create_post,
|
366
|
+
self.get_comment_by_id,
|
367
|
+
self.post_comment,
|
368
|
+
self.edit_content,
|
369
|
+
self.delete_content,
|
370
|
+
]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from universal_mcp.applications.application import APIApplication
|
2
2
|
from universal_mcp.integrations import Integration
|
3
3
|
|
4
|
+
|
4
5
|
class ResendApp(APIApplication):
|
5
6
|
def __init__(self, integration: Integration) -> None:
|
6
7
|
super().__init__(name="resend", integration=integration)
|
@@ -20,7 +21,7 @@ class ResendApp(APIApplication):
|
|
20
21
|
to: The email address to send the email to
|
21
22
|
subject: The subject of the email
|
22
23
|
content: The content of the email
|
23
|
-
|
24
|
+
|
24
25
|
Returns:
|
25
26
|
A message indicating that the email was sent successfully
|
26
27
|
"""
|
@@ -29,15 +30,9 @@ class ResendApp(APIApplication):
|
|
29
30
|
raise ValueError("No credentials found")
|
30
31
|
from_email = credentials.get("from_email", "Manoj <manoj@agentr.dev>")
|
31
32
|
url = "https://api.resend.com/emails"
|
32
|
-
body = {
|
33
|
-
"from": from_email,
|
34
|
-
"to": [to],
|
35
|
-
"subject": subject,
|
36
|
-
"text": content
|
37
|
-
}
|
33
|
+
body = {"from": from_email, "to": [to], "subject": subject, "text": content}
|
38
34
|
self._post(url, body)
|
39
35
|
return "Sent Successfully"
|
40
36
|
|
41
37
|
def list_tools(self):
|
42
38
|
return [self.send_email]
|
43
|
-
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import httpx
|
2
|
+
from loguru import logger
|
3
|
+
from serpapi import SerpApiClient as SerpApiSearch
|
4
|
+
|
5
|
+
from universal_mcp.applications.application import APIApplication
|
6
|
+
|
7
|
+
|
8
|
+
class SerpApp(APIApplication):
|
9
|
+
def __init__(self, **kwargs):
|
10
|
+
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
|
+
|
29
|
+
async def search(self, params: dict[str, any] = None) -> str:
|
30
|
+
"""Perform a search on the specified engine using SerpApi.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
params: Dictionary of engine-specific parameters (e.g., {"q": "Coffee", "engine": "google_light", "location": "Austin, TX"}).
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
A formatted string of search results or an error message.
|
37
|
+
"""
|
38
|
+
if params is None:
|
39
|
+
params = {}
|
40
|
+
self._set_api_key()
|
41
|
+
params = {
|
42
|
+
"api_key": self.api_key,
|
43
|
+
"engine": "google_light", # Fastest engine by default
|
44
|
+
**params, # Include any additional parameters
|
45
|
+
}
|
46
|
+
|
47
|
+
try:
|
48
|
+
search = SerpApiSearch(params)
|
49
|
+
data = search.get_dict()
|
50
|
+
|
51
|
+
# Process organic search results if available
|
52
|
+
if "organic_results" in data:
|
53
|
+
formatted_results = []
|
54
|
+
for result in data.get("organic_results", []):
|
55
|
+
title = result.get("title", "No title")
|
56
|
+
link = result.get("link", "No link")
|
57
|
+
snippet = result.get("snippet", "No snippet")
|
58
|
+
formatted_results.append(
|
59
|
+
f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n"
|
60
|
+
)
|
61
|
+
return (
|
62
|
+
"\n".join(formatted_results)
|
63
|
+
if formatted_results
|
64
|
+
else "No organic results found"
|
65
|
+
)
|
66
|
+
else:
|
67
|
+
return "No organic results found"
|
68
|
+
|
69
|
+
# Handle HTTP-specific errors
|
70
|
+
except httpx.HTTPStatusError as e:
|
71
|
+
if e.response.status_code == 429:
|
72
|
+
return "Error: Rate limit exceeded. Please try again later."
|
73
|
+
elif e.response.status_code == 401:
|
74
|
+
return "Error: Invalid API key. Please check your SERPAPI_API_KEY."
|
75
|
+
else:
|
76
|
+
return f"Error: {e.response.status_code} - {e.response.text}"
|
77
|
+
# Handle other exceptions (e.g., network issues)
|
78
|
+
except Exception as e:
|
79
|
+
return f"Error: {str(e)}"
|
80
|
+
|
81
|
+
def list_tools(self):
|
82
|
+
return [
|
83
|
+
self.search,
|
84
|
+
]
|
@@ -1,27 +1,28 @@
|
|
1
1
|
from universal_mcp.applications.application import APIApplication
|
2
2
|
from universal_mcp.integrations import Integration
|
3
3
|
|
4
|
+
|
4
5
|
class TavilyApp(APIApplication):
|
5
6
|
def __init__(self, integration: Integration) -> None:
|
6
7
|
name = "tavily"
|
7
8
|
self.base_url = "https://api.tavily.com"
|
8
9
|
super().__init__(name=name, integration=integration)
|
9
|
-
|
10
|
+
|
10
11
|
def _get_headers(self):
|
11
12
|
credentials = self.integration.get_credentials()
|
12
13
|
if not credentials:
|
13
14
|
raise ValueError("No credentials found")
|
14
15
|
return {
|
15
16
|
"Authorization": f"Bearer {credentials['api_key']}",
|
16
|
-
"Content-Type": "application/json"
|
17
|
+
"Content-Type": "application/json",
|
17
18
|
}
|
18
19
|
|
19
20
|
def search(self, query: str) -> str:
|
20
21
|
"""Search the web using Tavily's search API
|
21
|
-
|
22
|
+
|
22
23
|
Args:
|
23
24
|
query: The search query
|
24
|
-
|
25
|
+
|
25
26
|
Returns:
|
26
27
|
str: A summary of search results
|
27
28
|
"""
|
@@ -37,21 +38,21 @@ class TavilyApp(APIApplication):
|
|
37
38
|
"include_images": False,
|
38
39
|
"include_image_descriptions": False,
|
39
40
|
"include_domains": [],
|
40
|
-
"exclude_domains": []
|
41
|
+
"exclude_domains": [],
|
41
42
|
}
|
42
|
-
|
43
|
+
|
43
44
|
response = self._post(url, payload)
|
44
45
|
result = response.json()
|
45
|
-
|
46
|
+
|
46
47
|
if "answer" in result:
|
47
48
|
return result["answer"]
|
48
|
-
|
49
|
+
|
49
50
|
# Fallback to combining top results if no direct answer
|
50
51
|
summaries = []
|
51
52
|
for item in result.get("results", [])[:3]:
|
52
53
|
summaries.append(f"• {item['title']}: {item['snippet']}")
|
53
|
-
|
54
|
+
|
54
55
|
return "\n".join(summaries)
|
55
|
-
|
56
|
+
|
56
57
|
def list_tools(self):
|
57
58
|
return [self.search]
|
@@ -1,13 +1,13 @@
|
|
1
1
|
from universal_mcp.applications.application import APIApplication
|
2
2
|
|
3
3
|
|
4
|
-
class
|
4
|
+
class ZenquotesApp(APIApplication):
|
5
5
|
def __init__(self, **kwargs) -> None:
|
6
6
|
super().__init__(name="zenquote", **kwargs)
|
7
7
|
|
8
8
|
def get_quote(self) -> str:
|
9
9
|
"""Get an inspirational quote from the Zen Quotes API
|
10
|
-
|
10
|
+
|
11
11
|
Returns:
|
12
12
|
A random inspirational quote
|
13
13
|
"""
|
@@ -18,4 +18,4 @@ class ZenQuoteApp(APIApplication):
|
|
18
18
|
return f"{quote_data['q']} - {quote_data['a']}"
|
19
19
|
|
20
20
|
def list_tools(self):
|
21
|
-
return [self.get_quote]
|
21
|
+
return [self.get_quote]
|