universal-mcp-applications 0.1.30rc2__py3-none-any.whl → 0.1.36rc2__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/ahrefs/app.py +52 -198
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +111 -464
- universal_mcp/applications/asana/app.py +417 -1567
- universal_mcp/applications/aws_s3/app.py +36 -103
- universal_mcp/applications/bill/app.py +546 -1957
- universal_mcp/applications/box/app.py +1068 -3981
- universal_mcp/applications/braze/app.py +364 -1430
- universal_mcp/applications/browser_use/app.py +2 -8
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +61 -200
- universal_mcp/applications/canva/app.py +45 -110
- universal_mcp/applications/clickup/app.py +207 -674
- universal_mcp/applications/coda/app.py +146 -426
- universal_mcp/applications/confluence/app.py +310 -1098
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +28 -107
- universal_mcp/applications/dialpad/app.py +283 -756
- universal_mcp/applications/digitalocean/app.py +1766 -5777
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/app.py +6 -17
- universal_mcp/applications/falai/app.py +24 -101
- universal_mcp/applications/figma/app.py +53 -137
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +51 -152
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +91 -528
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +52 -161
- universal_mcp/applications/github/app.py +19 -56
- universal_mcp/applications/gong/app.py +88 -248
- universal_mcp/applications/google_calendar/app.py +16 -68
- universal_mcp/applications/google_docs/app.py +88 -188
- universal_mcp/applications/google_drive/app.py +141 -463
- universal_mcp/applications/google_gemini/app.py +12 -64
- universal_mcp/applications/google_mail/app.py +28 -157
- universal_mcp/applications/google_searchconsole/app.py +15 -48
- universal_mcp/applications/google_sheet/app.py +103 -580
- universal_mcp/applications/google_sheet/helper.py +10 -37
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +44 -122
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
- universal_mcp/applications/hubspot/app.py +23 -87
- universal_mcp/applications/jira/app.py +2071 -7986
- universal_mcp/applications/klaviyo/app.py +494 -1376
- universal_mcp/applications/linkedin/README.md +9 -2
- universal_mcp/applications/linkedin/app.py +392 -212
- universal_mcp/applications/mailchimp/app.py +450 -1605
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +217 -699
- universal_mcp/applications/ms_teams/app.py +64 -186
- universal_mcp/applications/neon/app.py +86 -192
- universal_mcp/applications/notion/app.py +21 -36
- universal_mcp/applications/onedrive/app.py +16 -38
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/app.py +24 -84
- universal_mcp/applications/perplexity/app.py +4 -19
- universal_mcp/applications/pipedrive/app.py +832 -3142
- universal_mcp/applications/posthog/app.py +163 -432
- universal_mcp/applications/reddit/app.py +40 -139
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +14 -41
- universal_mcp/applications/rocketlane/app.py +221 -934
- universal_mcp/applications/scraper/README.md +7 -4
- universal_mcp/applications/scraper/app.py +216 -102
- universal_mcp/applications/semanticscholar/app.py +22 -64
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +512 -1262
- universal_mcp/applications/sentry/app.py +271 -906
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +17 -39
- universal_mcp/applications/shopify/app.py +1551 -4287
- universal_mcp/applications/shortcut/app.py +155 -417
- universal_mcp/applications/slack/app.py +50 -101
- universal_mcp/applications/spotify/app.py +126 -325
- universal_mcp/applications/supabase/app.py +104 -213
- universal_mcp/applications/tavily/app.py +1 -1
- universal_mcp/applications/trello/app.py +693 -2656
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +70 -283
- universal_mcp/applications/wrike/app.py +45 -118
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +75 -261
- universal_mcp/applications/zenquotes/app.py +2 -2
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/METADATA +2 -2
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/RECORD +105 -105
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,42 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
from typing import Any
|
|
4
|
-
|
|
5
4
|
import dns.resolver
|
|
6
5
|
import requests
|
|
7
6
|
from universal_mcp.applications.application import APIApplication
|
|
8
7
|
from universal_mcp.integrations import Integration
|
|
9
8
|
|
|
10
|
-
# Configure logging
|
|
11
9
|
logging.basicConfig(
|
|
12
|
-
level=logging.INFO,
|
|
13
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
14
|
-
handlers=[logging.StreamHandler(sys.stderr)],
|
|
10
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler(sys.stderr)]
|
|
15
11
|
)
|
|
16
|
-
|
|
17
12
|
logger = logging.getLogger("domain_checker")
|
|
18
|
-
|
|
19
|
-
# Constants
|
|
20
13
|
RDAP_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json"
|
|
21
14
|
USER_AGENT = "DomainCheckerBot/1.0"
|
|
22
|
-
|
|
23
|
-
# Top TLDs to check
|
|
24
|
-
TOP_TLDS = [
|
|
25
|
-
"com",
|
|
26
|
-
"net",
|
|
27
|
-
"org",
|
|
28
|
-
"io",
|
|
29
|
-
"co",
|
|
30
|
-
"app",
|
|
31
|
-
"dev",
|
|
32
|
-
"ai",
|
|
33
|
-
"me",
|
|
34
|
-
"info",
|
|
35
|
-
"xyz",
|
|
36
|
-
"online",
|
|
37
|
-
"site",
|
|
38
|
-
"tech",
|
|
39
|
-
]
|
|
15
|
+
TOP_TLDS = ["com", "net", "org", "io", "co", "app", "dev", "ai", "me", "info", "xyz", "online", "site", "tech"]
|
|
40
16
|
|
|
41
17
|
|
|
42
18
|
class DomainCheckerApp(APIApplication):
|
|
@@ -52,22 +28,16 @@ class DomainCheckerApp(APIApplication):
|
|
|
52
28
|
Fetches a domain's registration details from Registration Data Access Protocol (RDAP) servers. It dynamically selects the appropriate server URL based on the domain's TLD, with special handling for common ones. Returns the JSON data as a dictionary or None if the request fails or data is unavailable.
|
|
53
29
|
"""
|
|
54
30
|
try:
|
|
55
|
-
# Special case for .ch and .li domains
|
|
56
31
|
tld = domain.split(".")[-1].lower()
|
|
57
32
|
if tld in ["ch", "li"]:
|
|
58
33
|
rdap_url = f"https://rdap.nic.{tld}/domain/{domain}"
|
|
59
|
-
# Use common RDAP servers for known TLDs
|
|
60
34
|
elif tld in ["com", "net"]:
|
|
61
35
|
rdap_url = f"https://rdap.verisign.com/{tld}/v1/domain/{domain}"
|
|
62
36
|
elif tld == "org":
|
|
63
|
-
rdap_url =
|
|
64
|
-
f"https://rdap.publicinterestregistry.org/rdap/domain/{domain}"
|
|
65
|
-
)
|
|
37
|
+
rdap_url = f"https://rdap.publicinterestregistry.org/rdap/domain/{domain}"
|
|
66
38
|
else:
|
|
67
39
|
rdap_url = f"https://rdap.org/domain/{domain}"
|
|
68
|
-
|
|
69
40
|
headers = {"Accept": "application/rdap+json", "User-Agent": USER_AGENT}
|
|
70
|
-
|
|
71
41
|
response = requests.get(rdap_url, headers=headers, timeout=5)
|
|
72
42
|
if response.status_code == 200:
|
|
73
43
|
return response.json()
|
|
@@ -122,21 +92,13 @@ class DomainCheckerApp(APIApplication):
|
|
|
122
92
|
domain, availability, registration, dns, rdap, important
|
|
123
93
|
"""
|
|
124
94
|
logger.info(f"Checking domain: {domain}")
|
|
125
|
-
|
|
126
|
-
# First check DNS
|
|
127
95
|
has_dns = await self._check_dns(domain)
|
|
128
|
-
|
|
129
96
|
if has_dns:
|
|
130
|
-
# Domain exists, get RDAP data if possible
|
|
131
97
|
rdap_data = await self._get_rdap_data(domain)
|
|
132
|
-
|
|
133
98
|
if rdap_data:
|
|
134
|
-
# Extract data from RDAP
|
|
135
99
|
registrar = "Unknown"
|
|
136
100
|
reg_date = "Unknown"
|
|
137
101
|
exp_date = "Unknown"
|
|
138
|
-
|
|
139
|
-
# Extract registrar
|
|
140
102
|
entities = rdap_data.get("entities", [])
|
|
141
103
|
for entity in entities:
|
|
142
104
|
if "registrar" in entity.get("roles", []):
|
|
@@ -146,15 +108,12 @@ class DomainCheckerApp(APIApplication):
|
|
|
146
108
|
if entry[0] in ["fn", "org"] and len(entry) > 3:
|
|
147
109
|
registrar = entry[3]
|
|
148
110
|
break
|
|
149
|
-
|
|
150
|
-
# Extract dates
|
|
151
111
|
events = rdap_data.get("events", [])
|
|
152
112
|
for event in events:
|
|
153
113
|
if event.get("eventAction") == "registration":
|
|
154
114
|
reg_date = event.get("eventDate", "Unknown")
|
|
155
115
|
elif event.get("eventAction") == "expiration":
|
|
156
116
|
exp_date = event.get("eventDate", "Unknown")
|
|
157
|
-
|
|
158
117
|
return {
|
|
159
118
|
"domain": domain,
|
|
160
119
|
"status": "Registered",
|
|
@@ -175,8 +134,6 @@ class DomainCheckerApp(APIApplication):
|
|
|
175
134
|
"rdap_data_available": False,
|
|
176
135
|
"note": "Domain has DNS records but RDAP data couldn't be retrieved",
|
|
177
136
|
}
|
|
178
|
-
|
|
179
|
-
# Try RDAP one more time even if DNS not found
|
|
180
137
|
rdap_data = await self._get_rdap_data(domain)
|
|
181
138
|
if rdap_data:
|
|
182
139
|
return {
|
|
@@ -189,8 +146,6 @@ class DomainCheckerApp(APIApplication):
|
|
|
189
146
|
"rdap_data_available": True,
|
|
190
147
|
"note": "Domain found in RDAP registry",
|
|
191
148
|
}
|
|
192
|
-
|
|
193
|
-
# If we get here, the domain is likely available
|
|
194
149
|
return {
|
|
195
150
|
"domain": domain,
|
|
196
151
|
"status": "Available",
|
|
@@ -233,17 +188,12 @@ class DomainCheckerApp(APIApplication):
|
|
|
233
188
|
tld, keyword, domain-search, availability, bulk-check, important
|
|
234
189
|
"""
|
|
235
190
|
logger.info(f"Checking keyword: {keyword} across TLDs")
|
|
236
|
-
|
|
237
191
|
available = []
|
|
238
192
|
taken = []
|
|
239
|
-
|
|
240
|
-
# Check each TLD in sequence
|
|
241
193
|
for tld in TOP_TLDS:
|
|
242
194
|
domain = f"{keyword}.{tld}"
|
|
243
195
|
has_dns = await self._check_dns(domain)
|
|
244
|
-
|
|
245
196
|
if not has_dns:
|
|
246
|
-
# Double-check with RDAP if no DNS
|
|
247
197
|
rdap_data = await self._get_rdap_data(domain)
|
|
248
198
|
if not rdap_data:
|
|
249
199
|
available.append(domain)
|
|
@@ -251,7 +201,6 @@ class DomainCheckerApp(APIApplication):
|
|
|
251
201
|
taken.append(domain)
|
|
252
202
|
else:
|
|
253
203
|
taken.append(domain)
|
|
254
|
-
|
|
255
204
|
return {
|
|
256
205
|
"keyword": keyword,
|
|
257
206
|
"tlds_checked": len(TOP_TLDS),
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Annotated, Any
|
|
3
|
-
|
|
4
3
|
from loguru import logger
|
|
5
4
|
|
|
6
5
|
try:
|
|
7
6
|
from e2b_code_interpreter import Sandbox
|
|
8
|
-
|
|
9
7
|
except ImportError:
|
|
10
8
|
Sandbox = None
|
|
11
|
-
logger.error(
|
|
12
|
-
"Failed to import E2B Sandbox. Please ensure 'e2b_code_interpreter' is installed."
|
|
13
|
-
)
|
|
14
|
-
|
|
9
|
+
logger.error("Failed to import E2B Sandbox. Please ensure 'e2b_code_interpreter' is installed.")
|
|
15
10
|
from universal_mcp.applications.application import APIApplication
|
|
16
11
|
from universal_mcp.exceptions import NotAuthorizedError, ToolError
|
|
17
12
|
from universal_mcp.integrations import Integration
|
|
@@ -28,9 +23,7 @@ class E2bApp(APIApplication):
|
|
|
28
23
|
super().__init__(name="e2b", integration=integration, **kwargs)
|
|
29
24
|
self._e2b_api_key: str | None = None
|
|
30
25
|
if Sandbox is None:
|
|
31
|
-
logger.warning(
|
|
32
|
-
"E2B Sandbox SDK is not available. E2B tools will not function."
|
|
33
|
-
)
|
|
26
|
+
logger.warning("E2B Sandbox SDK is not available. E2B tools will not function.")
|
|
34
27
|
|
|
35
28
|
@property
|
|
36
29
|
def e2b_api_key(self) -> str:
|
|
@@ -40,54 +33,31 @@ class E2bApp(APIApplication):
|
|
|
40
33
|
if self._e2b_api_key is None:
|
|
41
34
|
if not self.integration:
|
|
42
35
|
logger.error("E2B App: Integration not configured.")
|
|
43
|
-
raise NotAuthorizedError(
|
|
44
|
-
"Integration not configured for E2B App. Cannot retrieve API key."
|
|
45
|
-
)
|
|
46
|
-
|
|
36
|
+
raise NotAuthorizedError("Integration not configured for E2B App. Cannot retrieve API key.")
|
|
47
37
|
try:
|
|
48
38
|
credentials = self.integration.get_credentials()
|
|
49
39
|
except NotAuthorizedError as e:
|
|
50
|
-
logger.error(
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
raise # Re-raise the original NotAuthorizedError
|
|
40
|
+
logger.error(f"E2B App: Authorization error when fetching credentials: {e.message}")
|
|
41
|
+
raise
|
|
54
42
|
except Exception as e:
|
|
55
|
-
logger.error(
|
|
56
|
-
f"E2B App: Unexpected error when fetching credentials: {e}",
|
|
57
|
-
exc_info=True,
|
|
58
|
-
)
|
|
43
|
+
logger.error(f"E2B App: Unexpected error when fetching credentials: {e}", exc_info=True)
|
|
59
44
|
raise NotAuthorizedError(f"Failed to get E2B credentials: {e}")
|
|
60
|
-
|
|
61
|
-
api_key = (
|
|
62
|
-
credentials.get("api_key")
|
|
63
|
-
or credentials.get("API_KEY") # Check common variations
|
|
64
|
-
or credentials.get("apiKey")
|
|
65
|
-
)
|
|
66
|
-
|
|
45
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
67
46
|
if not api_key:
|
|
68
47
|
logger.error("E2B App: API key not found in credentials.")
|
|
69
48
|
action_message = "API key for E2B is missing. Please ensure it's set in the store via MCP frontend or configuration."
|
|
70
|
-
if hasattr(self.integration, "authorize") and callable(
|
|
71
|
-
self.integration.authorize
|
|
72
|
-
):
|
|
49
|
+
if hasattr(self.integration, "authorize") and callable(self.integration.authorize):
|
|
73
50
|
try:
|
|
74
51
|
auth_details = self.integration.authorize()
|
|
75
52
|
if isinstance(auth_details, str):
|
|
76
53
|
action_message = auth_details
|
|
77
54
|
elif isinstance(auth_details, dict) and "url" in auth_details:
|
|
78
|
-
action_message =
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
elif (
|
|
82
|
-
isinstance(auth_details, dict) and "message" in auth_details
|
|
83
|
-
):
|
|
55
|
+
action_message = f"Please authorize via: {auth_details['url']}"
|
|
56
|
+
elif isinstance(auth_details, dict) and "message" in auth_details:
|
|
84
57
|
action_message = auth_details["message"]
|
|
85
58
|
except Exception as auth_e:
|
|
86
|
-
logger.warning(
|
|
87
|
-
f"Could not retrieve specific authorization action for E2B: {auth_e}"
|
|
88
|
-
)
|
|
59
|
+
logger.warning(f"Could not retrieve specific authorization action for E2B: {auth_e}")
|
|
89
60
|
raise NotAuthorizedError(action_message)
|
|
90
|
-
|
|
91
61
|
self._e2b_api_key = api_key
|
|
92
62
|
logger.info("E2B API Key successfully retrieved and cached.")
|
|
93
63
|
return self._e2b_api_key
|
|
@@ -95,40 +65,28 @@ class E2bApp(APIApplication):
|
|
|
95
65
|
def _format_execution_output(self, execution: Any) -> str:
|
|
96
66
|
"""Helper function to format the E2B execution logs nicely."""
|
|
97
67
|
output_parts = []
|
|
98
|
-
|
|
99
68
|
try:
|
|
100
69
|
logs = getattr(execution, "logs", None)
|
|
101
|
-
|
|
102
70
|
if logs is not None:
|
|
103
|
-
# Collect stdout
|
|
104
71
|
if getattr(logs, "stdout", None):
|
|
105
72
|
stdout_content = "".join(logs.stdout).strip()
|
|
106
73
|
if stdout_content:
|
|
107
74
|
output_parts.append(stdout_content)
|
|
108
|
-
|
|
109
|
-
# Collect stderr
|
|
110
75
|
if getattr(logs, "stderr", None):
|
|
111
76
|
stderr_content = "".join(logs.stderr).strip()
|
|
112
77
|
if stderr_content:
|
|
113
78
|
output_parts.append(f"--- ERROR ---\n{stderr_content}")
|
|
114
|
-
|
|
115
|
-
# Fallback: check execution.text (covers expressions returning values)
|
|
116
79
|
if not output_parts and hasattr(execution, "text"):
|
|
117
80
|
text_content = str(execution.text).strip()
|
|
118
81
|
if text_content:
|
|
119
82
|
output_parts.append(text_content)
|
|
120
|
-
|
|
121
83
|
except Exception as e:
|
|
122
84
|
output_parts.append(f"Failed to format execution output: {e}")
|
|
123
|
-
|
|
124
85
|
if not output_parts:
|
|
125
86
|
return "Execution finished with no output (stdout/stderr)."
|
|
126
|
-
|
|
127
87
|
return "\n\n".join(output_parts)
|
|
128
88
|
|
|
129
|
-
def execute_python_code(
|
|
130
|
-
self, code: Annotated[str, "The Python code to execute."]
|
|
131
|
-
) -> str:
|
|
89
|
+
async def execute_python_code(self, code: Annotated[str, "The Python code to execute."]) -> str:
|
|
132
90
|
"""
|
|
133
91
|
Executes a Python code string in a secure E2B sandbox. It authenticates using the configured API key, runs the code, and returns a formatted string containing the execution's output (stdout/stderr). It raises specific exceptions for authorization failures or general execution issues.
|
|
134
92
|
|
|
@@ -151,7 +109,6 @@ class E2bApp(APIApplication):
|
|
|
151
109
|
raise ToolError("E2B Sandbox SDK (e2b_code_interpreter) is not installed.")
|
|
152
110
|
if not code or not isinstance(code, str):
|
|
153
111
|
raise ValueError("Provided code must be a non-empty string.")
|
|
154
|
-
|
|
155
112
|
try:
|
|
156
113
|
logger.info("Attempting to execute Python code in E2B Sandbox.")
|
|
157
114
|
os.environ["E2B_API_KEY"] = self.e2b_api_key
|
|
@@ -164,17 +121,10 @@ class E2bApp(APIApplication):
|
|
|
164
121
|
except Exception as e:
|
|
165
122
|
logger.exception("E2B code execution failed.")
|
|
166
123
|
lower = str(e).lower()
|
|
167
|
-
if (
|
|
168
|
-
"authentication" in lower
|
|
169
|
-
or "api key" in lower
|
|
170
|
-
or "401" in lower
|
|
171
|
-
or "403" in lower
|
|
172
|
-
):
|
|
124
|
+
if "authentication" in lower or "api key" in lower or "401" in lower or ("403" in lower):
|
|
173
125
|
raise NotAuthorizedError(f"E2B authentication/permission failed: {e}")
|
|
174
126
|
raise ToolError(f"E2B code execution failed: {e}")
|
|
175
127
|
|
|
176
128
|
def list_tools(self) -> list[callable]:
|
|
177
129
|
"""Lists the tools available from the E2bApp."""
|
|
178
|
-
return [
|
|
179
|
-
self.execute_python_code,
|
|
180
|
-
]
|
|
130
|
+
return [self.execute_python_code]
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from io import BytesIO
|
|
3
|
-
|
|
4
3
|
import requests
|
|
5
4
|
from universal_mcp.applications.application import APIApplication
|
|
6
5
|
from universal_mcp.exceptions import NotAuthorizedError
|
|
7
6
|
from universal_mcp.integrations import Integration
|
|
8
|
-
|
|
9
7
|
from elevenlabs import ElevenLabs
|
|
10
8
|
from universal_mcp.applications.file_system.app import FileSystemApp
|
|
11
9
|
|
|
@@ -24,24 +22,14 @@ class ElevenlabsApp(APIApplication):
|
|
|
24
22
|
credentials = self.integration.get_credentials()
|
|
25
23
|
if not credentials:
|
|
26
24
|
raise NotAuthorizedError("No credentials found")
|
|
27
|
-
api_key = (
|
|
28
|
-
credentials.get("api_key")
|
|
29
|
-
or credentials.get("API_KEY")
|
|
30
|
-
or credentials.get("apiKey")
|
|
31
|
-
)
|
|
25
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
32
26
|
if not api_key:
|
|
33
27
|
raise NotAuthorizedError("No api key found")
|
|
34
28
|
self._client = ElevenLabs(api_key=api_key)
|
|
35
29
|
return self._client
|
|
36
30
|
|
|
37
|
-
# def get_voices(self):
|
|
38
|
-
# return self.client.voices.list_voices()
|
|
39
|
-
|
|
40
31
|
async def generate_speech_audio_url(
|
|
41
|
-
self,
|
|
42
|
-
text: str,
|
|
43
|
-
voice_id: str = "21m00Tcm4TlvDq8ikWAM",
|
|
44
|
-
model_id: str = "eleven_multilingual_v2",
|
|
32
|
+
self, text: str, voice_id: str = "21m00Tcm4TlvDq8ikWAM", model_id: str = "eleven_multilingual_v2"
|
|
45
33
|
) -> bytes:
|
|
46
34
|
"""
|
|
47
35
|
Converts a text string into speech using the ElevenLabs API. The function then saves the generated audio to a temporary MP3 file and returns a public URL to access it, rather than the raw audio bytes.
|
|
@@ -59,26 +47,14 @@ class ElevenlabsApp(APIApplication):
|
|
|
59
47
|
Tags:
|
|
60
48
|
important
|
|
61
49
|
"""
|
|
62
|
-
audio_generator = self.client.text_to_speech.convert(
|
|
63
|
-
text=text,
|
|
64
|
-
voice_id=voice_id,
|
|
65
|
-
model_id=model_id,
|
|
66
|
-
output_format="mp3_44100_128",
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Collect all audio chunks from the generator
|
|
50
|
+
audio_generator = self.client.text_to_speech.convert(text=text, voice_id=voice_id, model_id=model_id, output_format="mp3_44100_128")
|
|
70
51
|
audio_data = b""
|
|
71
52
|
for chunk in audio_generator:
|
|
72
53
|
audio_data += chunk
|
|
73
|
-
|
|
74
|
-
upload_result = await FileSystemApp.write_file(
|
|
75
|
-
audio_data, f"/tmp/{uuid.uuid4()}.mp3"
|
|
76
|
-
)
|
|
54
|
+
upload_result = await FileSystemApp.write_file(audio_data, f"/tmp/{uuid.uuid4()}.mp3")
|
|
77
55
|
return upload_result["data"]["url"]
|
|
78
56
|
|
|
79
|
-
async def speech_to_text(
|
|
80
|
-
self, audio_file_path: str, language_code: str = "eng", diarize: bool = True
|
|
81
|
-
) -> str:
|
|
57
|
+
async def speech_to_text(self, audio_file_path: str, language_code: str = "eng", diarize: bool = True) -> str:
|
|
82
58
|
"""
|
|
83
59
|
Transcribes an audio file into text using the ElevenLabs API. It supports language specification and speaker diarization, providing the inverse operation to the audio-generating `text_to_speech` method. Note: The docstring indicates this is a placeholder for an undocumented endpoint.
|
|
84
60
|
|
|
@@ -92,19 +68,12 @@ class ElevenlabsApp(APIApplication):
|
|
|
92
68
|
important
|
|
93
69
|
"""
|
|
94
70
|
transcription = self.client.speech_to_text.convert(
|
|
95
|
-
file=audio_file_path,
|
|
96
|
-
model_id="scribe_v1", # Model to use, for now only "scribe_v1" is supported
|
|
97
|
-
tag_audio_events=True, # Tag audio events like laughter, applause, etc.
|
|
98
|
-
language_code=language_code, # Language of the audio file. If set to None, the model will detect the language automatically.
|
|
99
|
-
diarize=diarize, # Whether to annotate who is speaking
|
|
71
|
+
file=audio_file_path, model_id="scribe_v1", tag_audio_events=True, language_code=language_code, diarize=diarize
|
|
100
72
|
)
|
|
101
73
|
return transcription
|
|
102
74
|
|
|
103
75
|
async def speech_to_speech(
|
|
104
|
-
self,
|
|
105
|
-
audio_url: str,
|
|
106
|
-
voice_id: str = "21m00Tcm4TlvDq8ikWAM",
|
|
107
|
-
model_id: str = "eleven_multilingual_sts_v2",
|
|
76
|
+
self, audio_url: str, voice_id: str = "21m00Tcm4TlvDq8ikWAM", model_id: str = "eleven_multilingual_sts_v2"
|
|
108
77
|
) -> bytes:
|
|
109
78
|
"""
|
|
110
79
|
Downloads an audio file from a URL and converts the speech into a specified target voice using the ElevenLabs API. This function transforms the speaker's voice in an existing recording and returns the new audio data as bytes, distinct from creating audio from text.
|
|
@@ -123,19 +92,12 @@ class ElevenlabsApp(APIApplication):
|
|
|
123
92
|
response = requests.get(audio_url)
|
|
124
93
|
audio_data = BytesIO(response.content)
|
|
125
94
|
response = self.client.speech_to_speech.convert(
|
|
126
|
-
voice_id=voice_id,
|
|
127
|
-
audio=audio_data,
|
|
128
|
-
model_id=model_id,
|
|
129
|
-
output_format="mp3_44100_128",
|
|
95
|
+
voice_id=voice_id, audio=audio_data, model_id=model_id, output_format="mp3_44100_128"
|
|
130
96
|
)
|
|
131
97
|
return response.content
|
|
132
98
|
|
|
133
99
|
def list_tools(self):
|
|
134
|
-
return [
|
|
135
|
-
self.generate_speech_audio_url,
|
|
136
|
-
self.speech_to_text,
|
|
137
|
-
self.speech_to_speech,
|
|
138
|
-
]
|
|
100
|
+
return [self.generate_speech_audio_url, self.speech_to_text, self.speech_to_speech]
|
|
139
101
|
|
|
140
102
|
|
|
141
103
|
async def demo_text_to_speech():
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
-
|
|
3
2
|
from universal_mcp.applications.application import APIApplication
|
|
4
3
|
from universal_mcp.integrations import Integration
|
|
5
4
|
|
|
@@ -9,7 +8,7 @@ class ExaApp(APIApplication):
|
|
|
9
8
|
super().__init__(name="exa", integration=integration, **kwargs)
|
|
10
9
|
self.base_url = "https://api.exa.ai"
|
|
11
10
|
|
|
12
|
-
def search_with_filters(
|
|
11
|
+
async def search_with_filters(
|
|
13
12
|
self,
|
|
14
13
|
query,
|
|
15
14
|
useAutoprompt=None,
|
|
@@ -74,7 +73,7 @@ class ExaApp(APIApplication):
|
|
|
74
73
|
response.raise_for_status()
|
|
75
74
|
return response.json()
|
|
76
75
|
|
|
77
|
-
def find_similar_by_url(
|
|
76
|
+
async def find_similar_by_url(
|
|
78
77
|
self,
|
|
79
78
|
url,
|
|
80
79
|
numResults=None,
|
|
@@ -130,7 +129,7 @@ class ExaApp(APIApplication):
|
|
|
130
129
|
response.raise_for_status()
|
|
131
130
|
return response.json()
|
|
132
131
|
|
|
133
|
-
def fetch_page_content(
|
|
132
|
+
async def fetch_page_content(
|
|
134
133
|
self,
|
|
135
134
|
urls,
|
|
136
135
|
ids=None,
|
|
@@ -188,7 +187,7 @@ class ExaApp(APIApplication):
|
|
|
188
187
|
response.raise_for_status()
|
|
189
188
|
return response.json()
|
|
190
189
|
|
|
191
|
-
def answer(self, query, stream=None, text=None, model=None) -> dict[str, Any]:
|
|
190
|
+
async def answer(self, query, stream=None, text=None, model=None) -> dict[str, Any]:
|
|
192
191
|
"""
|
|
193
192
|
Retrieves a direct, synthesized answer for a given query by calling the Exa `/answer` API endpoint. Unlike `search`, which returns web results, this function provides a conclusive response. It supports streaming, including source text, and selecting a search model.
|
|
194
193
|
|
|
@@ -204,12 +203,7 @@ class ExaApp(APIApplication):
|
|
|
204
203
|
Tags:
|
|
205
204
|
important
|
|
206
205
|
"""
|
|
207
|
-
request_body = {
|
|
208
|
-
"query": query,
|
|
209
|
-
"stream": stream,
|
|
210
|
-
"text": text,
|
|
211
|
-
"model": model,
|
|
212
|
-
}
|
|
206
|
+
request_body = {"query": query, "stream": stream, "text": text, "model": model}
|
|
213
207
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
214
208
|
url = f"{self.base_url}/answer"
|
|
215
209
|
query_params = {}
|
|
@@ -218,9 +212,4 @@ class ExaApp(APIApplication):
|
|
|
218
212
|
return response.json()
|
|
219
213
|
|
|
220
214
|
def list_tools(self):
|
|
221
|
-
return [
|
|
222
|
-
self.search_with_filters,
|
|
223
|
-
self.find_similar_by_url,
|
|
224
|
-
self.fetch_page_content,
|
|
225
|
-
self.answer,
|
|
226
|
-
]
|
|
215
|
+
return [self.search_with_filters, self.find_similar_by_url, self.fetch_page_content, self.answer]
|