universal-mcp-applications 0.1.39rc8__py3-none-any.whl → 0.1.39rc16__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.
Potentially problematic release.
This version of universal-mcp-applications might be problematic. Click here for more details.
- universal_mcp/applications/BEST_PRACTICES.md +1 -1
- universal_mcp/applications/airtable/app.py +13 -13
- universal_mcp/applications/apollo/app.py +2 -2
- universal_mcp/applications/aws_s3/app.py +30 -19
- universal_mcp/applications/browser_use/app.py +10 -7
- universal_mcp/applications/contentful/app.py +4 -4
- universal_mcp/applications/crustdata/app.py +2 -2
- universal_mcp/applications/e2b/app.py +3 -4
- universal_mcp/applications/elevenlabs/README.md +27 -3
- universal_mcp/applications/elevenlabs/app.py +753 -48
- universal_mcp/applications/exa/app.py +18 -11
- universal_mcp/applications/falai/README.md +5 -7
- universal_mcp/applications/falai/app.py +160 -159
- universal_mcp/applications/firecrawl/app.py +14 -15
- universal_mcp/applications/ghost_content/app.py +4 -4
- universal_mcp/applications/github/app.py +2 -2
- universal_mcp/applications/gong/app.py +2 -2
- universal_mcp/applications/google_docs/README.md +15 -14
- universal_mcp/applications/google_docs/app.py +5 -4
- universal_mcp/applications/google_gemini/app.py +61 -17
- universal_mcp/applications/google_sheet/README.md +2 -1
- universal_mcp/applications/google_sheet/app.py +55 -0
- universal_mcp/applications/heygen/README.md +10 -32
- universal_mcp/applications/heygen/app.py +350 -744
- universal_mcp/applications/klaviyo/app.py +2 -2
- universal_mcp/applications/linkedin/README.md +14 -2
- universal_mcp/applications/linkedin/app.py +411 -38
- universal_mcp/applications/ms_teams/app.py +420 -1285
- universal_mcp/applications/notion/app.py +2 -2
- universal_mcp/applications/openai/app.py +1 -1
- universal_mcp/applications/perplexity/app.py +6 -7
- universal_mcp/applications/reddit/app.py +4 -4
- universal_mcp/applications/resend/app.py +31 -32
- universal_mcp/applications/rocketlane/app.py +2 -2
- universal_mcp/applications/scraper/app.py +51 -21
- universal_mcp/applications/semrush/app.py +1 -1
- universal_mcp/applications/serpapi/app.py +8 -7
- universal_mcp/applications/shopify/app.py +5 -7
- universal_mcp/applications/shortcut/app.py +3 -2
- universal_mcp/applications/slack/app.py +2 -2
- universal_mcp/applications/twilio/app.py +14 -13
- {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/METADATA +1 -1
- {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/RECORD +45 -45
- {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,10 +8,10 @@ class NotionApp(APIApplication):
|
|
|
8
8
|
super().__init__(name="notion", integration=integration, **kwargs)
|
|
9
9
|
self.base_url = "https://api.notion.com"
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
async def _aget_headers(self):
|
|
12
12
|
if not self.integration:
|
|
13
13
|
raise ValueError("Integration not configured for NotionApp")
|
|
14
|
-
credentials = self.integration.
|
|
14
|
+
credentials = await self.integration.get_credentials_async()
|
|
15
15
|
if "headers" in credentials:
|
|
16
16
|
return credentials["headers"]
|
|
17
17
|
return {"Authorization": f"Bearer {credentials['access_token']}", "Accept": "application/json", "Notion-Version": "2022-06-28"}
|
|
@@ -28,7 +28,7 @@ class OpenaiApp(APIApplication):
|
|
|
28
28
|
"""Initializes and returns the AsyncOpenAI client."""
|
|
29
29
|
if not self.integration:
|
|
30
30
|
raise ValueError("Integration not provided for OpenaiApp.")
|
|
31
|
-
creds = self.integration.
|
|
31
|
+
creds = await self.integration.get_credentials_async()
|
|
32
32
|
api_key = creds.get("api_key")
|
|
33
33
|
organization = creds.get("organization")
|
|
34
34
|
project = creds.get("project")
|
|
@@ -18,8 +18,7 @@ class PerplexityApp(APIApplication):
|
|
|
18
18
|
if AsyncPerplexity is None:
|
|
19
19
|
logger.warning("Perplexity SDK is not available. Perplexity tools will not function.")
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
def perplexity_api_key(self) -> str:
|
|
21
|
+
async def get_perplexity_api_key(self) -> str:
|
|
23
22
|
"""
|
|
24
23
|
A property that lazily retrieves and caches the Perplexity API key from the configured integration.
|
|
25
24
|
"""
|
|
@@ -28,7 +27,7 @@ class PerplexityApp(APIApplication):
|
|
|
28
27
|
logger.error(f"{self.name.capitalize()} App: Integration not configured.")
|
|
29
28
|
raise NotAuthorizedError(f"Integration not configured for {self.name.capitalize()} App. Cannot retrieve API key.")
|
|
30
29
|
try:
|
|
31
|
-
credentials = self.integration.
|
|
30
|
+
credentials = await self.integration.get_credentials_async()
|
|
32
31
|
except NotAuthorizedError as e:
|
|
33
32
|
logger.error(f"{self.name.capitalize()} App: Authorization error when fetching credentials: {e.message}")
|
|
34
33
|
raise
|
|
@@ -59,14 +58,14 @@ class PerplexityApp(APIApplication):
|
|
|
59
58
|
assert self._perplexity_api_key is not None
|
|
60
59
|
return self._perplexity_api_key
|
|
61
60
|
|
|
62
|
-
def _get_client(self) -> AsyncPerplexity:
|
|
61
|
+
async def _get_client(self) -> AsyncPerplexity:
|
|
63
62
|
"""
|
|
64
63
|
Initializes and returns the Perplexity client after ensuring API key is set.
|
|
65
64
|
"""
|
|
66
65
|
if AsyncPerplexity is None:
|
|
67
66
|
logger.error("Perplexity SDK is not available.")
|
|
68
67
|
raise ToolError("Perplexity SDK is not installed or failed to import.")
|
|
69
|
-
current_api_key = self.
|
|
68
|
+
current_api_key = await self.get_perplexity_api_key()
|
|
70
69
|
return AsyncPerplexity(api_key=current_api_key)
|
|
71
70
|
|
|
72
71
|
async def answer_with_search(
|
|
@@ -149,7 +148,7 @@ class PerplexityApp(APIApplication):
|
|
|
149
148
|
messages.append({"role": "system", "content": system_prompt})
|
|
150
149
|
messages.append({"role": "user", "content": query})
|
|
151
150
|
|
|
152
|
-
client = self._get_client()
|
|
151
|
+
client = await self._get_client()
|
|
153
152
|
# client.chat.completions.create supports response_format
|
|
154
153
|
kwargs: dict[str, Any] = {
|
|
155
154
|
"model": model,
|
|
@@ -187,7 +186,7 @@ class PerplexityApp(APIApplication):
|
|
|
187
186
|
Tags:
|
|
188
187
|
search, web, research, citations, current events, important
|
|
189
188
|
"""
|
|
190
|
-
client = self._get_client()
|
|
189
|
+
client = await self._get_client()
|
|
191
190
|
response = await client.search.create(
|
|
192
191
|
query=query,
|
|
193
192
|
max_results=max_results,
|
|
@@ -12,9 +12,9 @@ class RedditApp(APIApplication):
|
|
|
12
12
|
self.base_api_url = "https://oauth.reddit.com"
|
|
13
13
|
self.base_url = "https://oauth.reddit.com"
|
|
14
14
|
|
|
15
|
-
def
|
|
15
|
+
async def _apost(self, url, data):
|
|
16
16
|
try:
|
|
17
|
-
headers = self.
|
|
17
|
+
headers = await self._aget_headers()
|
|
18
18
|
response = httpx.post(url, headers=headers, data=data)
|
|
19
19
|
response.raise_for_status()
|
|
20
20
|
return response
|
|
@@ -30,10 +30,10 @@ class RedditApp(APIApplication):
|
|
|
30
30
|
logger.error(f"Error posting {url}: {e}")
|
|
31
31
|
raise e
|
|
32
32
|
|
|
33
|
-
def
|
|
33
|
+
async def _aget_headers(self):
|
|
34
34
|
if not self.integration:
|
|
35
35
|
raise ValueError("Integration not configured for RedditApp")
|
|
36
|
-
credentials = self.integration.
|
|
36
|
+
credentials = await self.integration.get_credentials_async()
|
|
37
37
|
if "access_token" not in credentials:
|
|
38
38
|
logger.error("Reddit credentials found but missing 'access_token'.")
|
|
39
39
|
raise ValueError("Invalid Reddit credentials format.")
|
|
@@ -10,15 +10,14 @@ class ResendApp(APIApplication):
|
|
|
10
10
|
super().__init__(name="resend", integration=integration, **kwargs)
|
|
11
11
|
self._api_key = None
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
def api_key(self) -> str:
|
|
13
|
+
async def get_api_key(self) -> str:
|
|
15
14
|
"""
|
|
16
15
|
A property that lazily retrieves, validates, and caches the Resend API key from integration credentials. On first access, it configures the `resend` library, raising an error if authentication fails. This ensures the application is authenticated for all subsequent API calls within the class.
|
|
17
16
|
"""
|
|
18
17
|
if self._api_key is None:
|
|
19
18
|
if not self.integration:
|
|
20
19
|
raise NotAuthorizedError("Resend integration not configured.")
|
|
21
|
-
credentials = self.integration.
|
|
20
|
+
credentials = await self.integration.get_credentials_async()
|
|
22
21
|
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
23
22
|
if not api_key:
|
|
24
23
|
raise NotAuthorizedError("Resend API key not found in credentials.")
|
|
@@ -45,7 +44,7 @@ class ResendApp(APIApplication):
|
|
|
45
44
|
Tags:
|
|
46
45
|
send, email, api, communication, important
|
|
47
46
|
"""
|
|
48
|
-
self.
|
|
47
|
+
api_key = await self.get_api_key()
|
|
49
48
|
params: resend.Emails.SendParams = {"from": from_email, "to": to_emails, "subject": subject, "text": text}
|
|
50
49
|
try:
|
|
51
50
|
email = resend.Emails.send(params)
|
|
@@ -69,7 +68,7 @@ class ResendApp(APIApplication):
|
|
|
69
68
|
Tags:
|
|
70
69
|
batch, send, emails, resend-api
|
|
71
70
|
"""
|
|
72
|
-
self.
|
|
71
|
+
api_key = await self.get_api_key()
|
|
73
72
|
if not 1 <= len(emails) <= 100:
|
|
74
73
|
raise ToolError("The number of emails in a batch must be between 1 and 100.")
|
|
75
74
|
params: list[resend.Emails.SendParams] = emails
|
|
@@ -95,7 +94,7 @@ class ResendApp(APIApplication):
|
|
|
95
94
|
Tags:
|
|
96
95
|
retrieve, email, management
|
|
97
96
|
"""
|
|
98
|
-
self.
|
|
97
|
+
api_key = await self.get_api_key()
|
|
99
98
|
try:
|
|
100
99
|
email = resend.Emails.get(email_id=email_id)
|
|
101
100
|
return email
|
|
@@ -119,7 +118,7 @@ class ResendApp(APIApplication):
|
|
|
119
118
|
Tags:
|
|
120
119
|
update, email, async_job, management
|
|
121
120
|
"""
|
|
122
|
-
self.
|
|
121
|
+
api_key = await self.get_api_key()
|
|
123
122
|
params: resend.Emails.UpdateParams = {"id": email_id, "scheduled_at": scheduled_at}
|
|
124
123
|
try:
|
|
125
124
|
response = resend.Emails.update(params=params)
|
|
@@ -143,7 +142,7 @@ class ResendApp(APIApplication):
|
|
|
143
142
|
Tags:
|
|
144
143
|
cancel, email, management
|
|
145
144
|
"""
|
|
146
|
-
self.
|
|
145
|
+
api_key = await self.get_api_key()
|
|
147
146
|
try:
|
|
148
147
|
response = resend.Emails.cancel(email_id=email_id)
|
|
149
148
|
return response
|
|
@@ -166,7 +165,7 @@ class ResendApp(APIApplication):
|
|
|
166
165
|
Tags:
|
|
167
166
|
create, domain, management, api, batch, important
|
|
168
167
|
"""
|
|
169
|
-
self.
|
|
168
|
+
api_key = await self.get_api_key()
|
|
170
169
|
params: resend.Domains.CreateParams = {"name": name}
|
|
171
170
|
try:
|
|
172
171
|
domain = resend.Domains.create(params)
|
|
@@ -190,7 +189,7 @@ class ResendApp(APIApplication):
|
|
|
190
189
|
Tags:
|
|
191
190
|
retrieve, domain, management
|
|
192
191
|
"""
|
|
193
|
-
self.
|
|
192
|
+
api_key = await self.get_api_key()
|
|
194
193
|
try:
|
|
195
194
|
domain = resend.Domains.get(domain_id=domain_id)
|
|
196
195
|
return domain
|
|
@@ -213,7 +212,7 @@ class ResendApp(APIApplication):
|
|
|
213
212
|
Tags:
|
|
214
213
|
verify, domain
|
|
215
214
|
"""
|
|
216
|
-
self.
|
|
215
|
+
api_key = await self.get_api_key()
|
|
217
216
|
try:
|
|
218
217
|
response = resend.Domains.verify(domain_id=domain_id)
|
|
219
218
|
return response
|
|
@@ -241,7 +240,7 @@ class ResendApp(APIApplication):
|
|
|
241
240
|
Tags:
|
|
242
241
|
update, domain, management
|
|
243
242
|
"""
|
|
244
|
-
self.
|
|
243
|
+
api_key = await self.get_api_key()
|
|
245
244
|
params: resend.Domains.UpdateParams = {"id": domain_id}
|
|
246
245
|
if open_tracking is not None:
|
|
247
246
|
params["open_tracking"] = open_tracking
|
|
@@ -268,7 +267,7 @@ class ResendApp(APIApplication):
|
|
|
268
267
|
Tags:
|
|
269
268
|
list, domains, important, management
|
|
270
269
|
"""
|
|
271
|
-
self.
|
|
270
|
+
api_key = await self.get_api_key()
|
|
272
271
|
try:
|
|
273
272
|
domains = resend.Domains.list()
|
|
274
273
|
return domains
|
|
@@ -291,7 +290,7 @@ class ResendApp(APIApplication):
|
|
|
291
290
|
Tags:
|
|
292
291
|
remove, management, api, domain
|
|
293
292
|
"""
|
|
294
|
-
self.
|
|
293
|
+
api_key = await self.get_api_key()
|
|
295
294
|
try:
|
|
296
295
|
response = resend.Domains.remove(domain_id=domain_id)
|
|
297
296
|
return response
|
|
@@ -314,7 +313,7 @@ class ResendApp(APIApplication):
|
|
|
314
313
|
Tags:
|
|
315
314
|
create, api-key, authentication
|
|
316
315
|
"""
|
|
317
|
-
self.
|
|
316
|
+
api_key = await self.get_api_key()
|
|
318
317
|
params: resend.ApiKeys.CreateParams = {"name": name}
|
|
319
318
|
try:
|
|
320
319
|
api_key_obj = resend.ApiKeys.create(params)
|
|
@@ -338,7 +337,7 @@ class ResendApp(APIApplication):
|
|
|
338
337
|
Tags:
|
|
339
338
|
list, api, important
|
|
340
339
|
"""
|
|
341
|
-
self.
|
|
340
|
+
api_key = await self.get_api_key()
|
|
342
341
|
try:
|
|
343
342
|
keys = resend.ApiKeys.list()
|
|
344
343
|
return keys
|
|
@@ -361,7 +360,7 @@ class ResendApp(APIApplication):
|
|
|
361
360
|
Tags:
|
|
362
361
|
remove, api-key, management
|
|
363
362
|
"""
|
|
364
|
-
self.
|
|
363
|
+
api_key = await self.get_api_key()
|
|
365
364
|
try:
|
|
366
365
|
response = resend.ApiKeys.remove(api_key_id=api_key_id)
|
|
367
366
|
return response
|
|
@@ -387,7 +386,7 @@ class ResendApp(APIApplication):
|
|
|
387
386
|
Tags:
|
|
388
387
|
broadcast, email, important
|
|
389
388
|
"""
|
|
390
|
-
self.
|
|
389
|
+
api_key = await self.get_api_key()
|
|
391
390
|
params: resend.Broadcasts.CreateParams = {"audience_id": audience_id, "from": from_email, "subject": subject, "html": html}
|
|
392
391
|
try:
|
|
393
392
|
broadcast = resend.Broadcasts.create(params)
|
|
@@ -411,7 +410,7 @@ class ResendApp(APIApplication):
|
|
|
411
410
|
Tags:
|
|
412
411
|
retrieve, broadcast
|
|
413
412
|
"""
|
|
414
|
-
self.
|
|
413
|
+
api_key = await self.get_api_key()
|
|
415
414
|
try:
|
|
416
415
|
broadcast = resend.Broadcasts.get(id=broadcast_id)
|
|
417
416
|
return broadcast
|
|
@@ -436,7 +435,7 @@ class ResendApp(APIApplication):
|
|
|
436
435
|
Tags:
|
|
437
436
|
update, management, broadcast, api
|
|
438
437
|
"""
|
|
439
|
-
self.
|
|
438
|
+
api_key = await self.get_api_key()
|
|
440
439
|
params: resend.Broadcasts.UpdateParams = {"id": broadcast_id}
|
|
441
440
|
if html is not None:
|
|
442
441
|
params["html"] = html
|
|
@@ -467,7 +466,7 @@ class ResendApp(APIApplication):
|
|
|
467
466
|
Tags:
|
|
468
467
|
broadcast, send, api, management
|
|
469
468
|
"""
|
|
470
|
-
self.
|
|
469
|
+
api_key = await self.get_api_key()
|
|
471
470
|
params: resend.Broadcasts.SendParams = {"broadcast_id": broadcast_id}
|
|
472
471
|
if scheduled_at:
|
|
473
472
|
params["scheduled_at"] = scheduled_at
|
|
@@ -493,7 +492,7 @@ class ResendApp(APIApplication):
|
|
|
493
492
|
Tags:
|
|
494
493
|
remove, broadcast, api-management, draft-status
|
|
495
494
|
"""
|
|
496
|
-
self.
|
|
495
|
+
api_key = await self.get_api_key()
|
|
497
496
|
try:
|
|
498
497
|
response = resend.Broadcasts.remove(id=broadcast_id)
|
|
499
498
|
return response
|
|
@@ -513,7 +512,7 @@ class ResendApp(APIApplication):
|
|
|
513
512
|
Tags:
|
|
514
513
|
list, broadcast, api, management, important
|
|
515
514
|
"""
|
|
516
|
-
self.
|
|
515
|
+
api_key = await self.get_api_key()
|
|
517
516
|
try:
|
|
518
517
|
broadcasts = resend.Broadcasts.list()
|
|
519
518
|
return broadcasts
|
|
@@ -536,7 +535,7 @@ class ResendApp(APIApplication):
|
|
|
536
535
|
Tags:
|
|
537
536
|
create, audience, management, important
|
|
538
537
|
"""
|
|
539
|
-
self.
|
|
538
|
+
api_key = await self.get_api_key()
|
|
540
539
|
params: resend.Audiences.CreateParams = {"name": name}
|
|
541
540
|
try:
|
|
542
541
|
audience = resend.Audiences.create(params)
|
|
@@ -560,7 +559,7 @@ class ResendApp(APIApplication):
|
|
|
560
559
|
Tags:
|
|
561
560
|
fetch, audience, management, api
|
|
562
561
|
"""
|
|
563
|
-
self.
|
|
562
|
+
api_key = await self.get_api_key()
|
|
564
563
|
try:
|
|
565
564
|
audience = resend.Audiences.get(id=audience_id)
|
|
566
565
|
return audience
|
|
@@ -583,7 +582,7 @@ class ResendApp(APIApplication):
|
|
|
583
582
|
Tags:
|
|
584
583
|
remove, audience, management, api
|
|
585
584
|
"""
|
|
586
|
-
self.
|
|
585
|
+
api_key = await self.get_api_key()
|
|
587
586
|
try:
|
|
588
587
|
response = resend.Audiences.remove(id=audience_id)
|
|
589
588
|
return response
|
|
@@ -603,7 +602,7 @@ class ResendApp(APIApplication):
|
|
|
603
602
|
Tags:
|
|
604
603
|
list, audiences, management, important
|
|
605
604
|
"""
|
|
606
|
-
self.
|
|
605
|
+
api_key = await self.get_api_key()
|
|
607
606
|
try:
|
|
608
607
|
audiences = resend.Audiences.list()
|
|
609
608
|
return audiences
|
|
@@ -632,7 +631,7 @@ class ResendApp(APIApplication):
|
|
|
632
631
|
Tags:
|
|
633
632
|
create, contact, management, important
|
|
634
633
|
"""
|
|
635
|
-
self.
|
|
634
|
+
api_key = await self.get_api_key()
|
|
636
635
|
params: resend.Contacts.CreateParams = {"audience_id": audience_id, "email": email, "unsubscribed": unsubscribed}
|
|
637
636
|
if first_name:
|
|
638
637
|
params["first_name"] = first_name
|
|
@@ -662,7 +661,7 @@ class ResendApp(APIApplication):
|
|
|
662
661
|
Tags:
|
|
663
662
|
retrieve, contact, audience, management, api
|
|
664
663
|
"""
|
|
665
|
-
self.
|
|
664
|
+
api_key = await self.get_api_key()
|
|
666
665
|
if not (contact_id or email) or (contact_id and email):
|
|
667
666
|
raise ToolError("You must provide exactly one of 'contact_id' or 'email'.")
|
|
668
667
|
params = {"audience_id": audience_id}
|
|
@@ -705,7 +704,7 @@ class ResendApp(APIApplication):
|
|
|
705
704
|
Tags:
|
|
706
705
|
update, contact, management
|
|
707
706
|
"""
|
|
708
|
-
self.
|
|
707
|
+
api_key = await self.get_api_key()
|
|
709
708
|
if not (contact_id or email) or (contact_id and email):
|
|
710
709
|
raise ToolError("You must provide exactly one of 'contact_id' or 'email' to identify the contact.")
|
|
711
710
|
params: resend.Contacts.UpdateParams = {"audience_id": audience_id}
|
|
@@ -745,7 +744,7 @@ class ResendApp(APIApplication):
|
|
|
745
744
|
Tags:
|
|
746
745
|
remove, contact-management, api-call
|
|
747
746
|
"""
|
|
748
|
-
self.
|
|
747
|
+
api_key = await self.get_api_key()
|
|
749
748
|
if not (contact_id or email) or (contact_id and email):
|
|
750
749
|
raise ToolError("You must provide exactly one of 'contact_id' or 'email'.")
|
|
751
750
|
params = {"audience_id": audience_id}
|
|
@@ -775,7 +774,7 @@ class ResendApp(APIApplication):
|
|
|
775
774
|
Tags:
|
|
776
775
|
list, contacts, management, important
|
|
777
776
|
"""
|
|
778
|
-
self.
|
|
777
|
+
api_key = await self.get_api_key()
|
|
779
778
|
try:
|
|
780
779
|
contacts = resend.Contacts.list(audience_id=audience_id)
|
|
781
780
|
return contacts
|
|
@@ -9,7 +9,7 @@ class RocketlaneApp(APIApplication):
|
|
|
9
9
|
super().__init__(name="rocketlane", integration=integration, **kwargs)
|
|
10
10
|
self.base_url = "https://api.rocketlane.com/api"
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
async def _aget_headers(self) -> dict[str, str]:
|
|
13
13
|
"""
|
|
14
14
|
Get the headers for Rocketlane API requests.
|
|
15
15
|
Overrides the base class method to use 'api-key'.
|
|
@@ -17,7 +17,7 @@ class RocketlaneApp(APIApplication):
|
|
|
17
17
|
if not self.integration:
|
|
18
18
|
logger.warning("RocketlaneApp: No integration configured, returning empty headers.")
|
|
19
19
|
return {}
|
|
20
|
-
credentials = self.integration.
|
|
20
|
+
credentials = await self.integration.get_credentials_async()
|
|
21
21
|
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
22
22
|
if not api_key:
|
|
23
23
|
logger.error("RocketlaneApp: API key not found in integration credentials.")
|
|
@@ -16,12 +16,17 @@ class ScraperApp(APIApplication):
|
|
|
16
16
|
|
|
17
17
|
def __init__(self, integration: Integration, **kwargs: Any) -> None:
|
|
18
18
|
super().__init__(name="scraper", integration=integration, **kwargs)
|
|
19
|
+
self._account_id = None
|
|
20
|
+
|
|
21
|
+
async def _get_account_id(self) -> str | None:
|
|
22
|
+
if self._account_id:
|
|
23
|
+
return self._account_id
|
|
19
24
|
if self.integration:
|
|
20
|
-
credentials = self.integration.
|
|
21
|
-
self.
|
|
25
|
+
credentials = await self.integration.get_credentials_async()
|
|
26
|
+
self._account_id = credentials.get("account_id")
|
|
22
27
|
else:
|
|
23
28
|
logger.warning("Integration not found")
|
|
24
|
-
|
|
29
|
+
return self._account_id
|
|
25
30
|
|
|
26
31
|
@property
|
|
27
32
|
def base_url(self) -> str:
|
|
@@ -43,9 +48,6 @@ class ScraperApp(APIApplication):
|
|
|
43
48
|
Get the headers for Unipile API requests.
|
|
44
49
|
Overrides the base class method to use X-Api-Key.
|
|
45
50
|
"""
|
|
46
|
-
if not self.integration:
|
|
47
|
-
logger.warning("UnipileApp: No integration configured, returning empty headers.")
|
|
48
|
-
return {}
|
|
49
51
|
api_key = os.getenv("UNIPILE_API_KEY")
|
|
50
52
|
if not api_key:
|
|
51
53
|
logger.error("UnipileApp: API key not found in integration credentials for Unipile.")
|
|
@@ -76,7 +78,7 @@ class ScraperApp(APIApplication):
|
|
|
76
78
|
httpx.HTTPError: If the API request fails.
|
|
77
79
|
"""
|
|
78
80
|
url = f"{self.base_url}/api/v1/linkedin/search/parameters"
|
|
79
|
-
params = {"account_id": self.
|
|
81
|
+
params = {"account_id": await self._get_account_id(), "keywords": keywords, "type": param_type}
|
|
80
82
|
response = await self._aget(url, params=params)
|
|
81
83
|
results = self._handle_response(response)
|
|
82
84
|
items = results.get("items", [])
|
|
@@ -85,16 +87,16 @@ class ScraperApp(APIApplication):
|
|
|
85
87
|
raise ValueError(f'Could not find a matching ID for {param_type}: "{keywords}"')
|
|
86
88
|
|
|
87
89
|
async def linkedin_list_profile_posts(
|
|
88
|
-
self,
|
|
90
|
+
self, provider_id: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
|
|
89
91
|
) -> dict[str, Any]:
|
|
90
92
|
"""
|
|
91
93
|
Fetches a paginated list of posts from a specific user or company profile using its provider ID. The `is_company` flag must specify the entity type. Unlike `linkedin_search_posts`, this function directly retrieves content from a known profile's feed instead of performing a global keyword search.
|
|
92
94
|
|
|
93
95
|
Args:
|
|
94
|
-
|
|
96
|
+
provider_id: The entity's provider internal ID (LinkedIn ID).
|
|
95
97
|
cursor: Pagination cursor.
|
|
96
98
|
limit: Number of items to return (1-100, as per Unipile example, though spec allows up to 250).
|
|
97
|
-
is_company: Boolean indicating if the
|
|
99
|
+
is_company: Boolean indicating if the provider_id is for a company.
|
|
98
100
|
|
|
99
101
|
Returns:
|
|
100
102
|
A dictionary containing a list of post objects and pagination details.
|
|
@@ -105,8 +107,8 @@ class ScraperApp(APIApplication):
|
|
|
105
107
|
Tags:
|
|
106
108
|
linkedin, post, list, user_posts, company_posts, content, api, important
|
|
107
109
|
"""
|
|
108
|
-
url = f"{self.base_url}/api/v1/users/{
|
|
109
|
-
params: dict[str, Any] = {"account_id": self.
|
|
110
|
+
url = f"{self.base_url}/api/v1/users/{provider_id}/posts"
|
|
111
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
110
112
|
if cursor:
|
|
111
113
|
params["cursor"] = cursor
|
|
112
114
|
if limit:
|
|
@@ -116,12 +118,39 @@ class ScraperApp(APIApplication):
|
|
|
116
118
|
response = await self._aget(url, params=params)
|
|
117
119
|
return response.json()
|
|
118
120
|
|
|
119
|
-
async def
|
|
121
|
+
async def linkedin_list_profile_comments(self, provider_id: str, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Retrieves a list of comments made by a specific user using their provider ID.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
provider_id: The entity's provider internal ID (LinkedIn ID).
|
|
127
|
+
limit: Number of items to return (1-100).
|
|
128
|
+
cursor: Pagination cursor.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A dictionary containing the list of comments.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
httpx.HTTPError: If the API request fails.
|
|
135
|
+
|
|
136
|
+
Tags:
|
|
137
|
+
linkedin, user, comments, list, content, api
|
|
138
|
+
"""
|
|
139
|
+
url = f"{self.base_url}/api/v1/users/{provider_id}/comments"
|
|
140
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
141
|
+
if cursor:
|
|
142
|
+
params["cursor"] = cursor
|
|
143
|
+
if limit:
|
|
144
|
+
params["limit"] = limit
|
|
145
|
+
response = await self._aget(url, params=params)
|
|
146
|
+
return self._handle_response(response)
|
|
147
|
+
|
|
148
|
+
async def linkedin_retrieve_profile(self, provider_id: str) -> dict[str, Any]:
|
|
120
149
|
"""
|
|
121
150
|
Fetches a specific LinkedIn user's profile using their public or internal ID. Unlike `linkedin_search_people`, which discovers multiple users via keywords, this function targets and retrieves detailed data for a single, known individual based on a direct identifier.
|
|
122
151
|
|
|
123
152
|
Args:
|
|
124
|
-
|
|
153
|
+
provider_id: Can be the provider's internal id OR the provider's public id of the requested user.For example, for https://www.linkedin.com/in/manojbajaj95/, the identifier is "manojbajaj95".
|
|
125
154
|
|
|
126
155
|
Returns:
|
|
127
156
|
A dictionary containing the user's profile details.
|
|
@@ -132,8 +161,8 @@ class ScraperApp(APIApplication):
|
|
|
132
161
|
Tags:
|
|
133
162
|
linkedin, user, profile, retrieve, get, api, important
|
|
134
163
|
"""
|
|
135
|
-
url = f"{self.base_url}/api/v1/users/{
|
|
136
|
-
params: dict[str, Any] = {"account_id": self.
|
|
164
|
+
url = f"{self.base_url}/api/v1/users/{provider_id}"
|
|
165
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
137
166
|
response = await self._aget(url, params=params)
|
|
138
167
|
return self._handle_response(response)
|
|
139
168
|
|
|
@@ -159,7 +188,7 @@ class ScraperApp(APIApplication):
|
|
|
159
188
|
linkedin, post, comment, list, content, api, important
|
|
160
189
|
"""
|
|
161
190
|
url = f"{self.base_url}/api/v1/posts/{post_id}/comments"
|
|
162
|
-
params: dict[str, Any] = {"account_id": self.
|
|
191
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
163
192
|
if cursor:
|
|
164
193
|
params["cursor"] = cursor
|
|
165
194
|
if limit is not None:
|
|
@@ -196,7 +225,7 @@ class ScraperApp(APIApplication):
|
|
|
196
225
|
httpx.HTTPError: If the API request fails.
|
|
197
226
|
"""
|
|
198
227
|
url = f"{self.base_url}/api/v1/linkedin/search"
|
|
199
|
-
params: dict[str, Any] = {"account_id": self.
|
|
228
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
200
229
|
if cursor:
|
|
201
230
|
params["cursor"] = cursor
|
|
202
231
|
if limit is not None:
|
|
@@ -241,7 +270,7 @@ class ScraperApp(APIApplication):
|
|
|
241
270
|
httpx.HTTPError: If the API request fails.
|
|
242
271
|
"""
|
|
243
272
|
url = f"{self.base_url}/api/v1/linkedin/search"
|
|
244
|
-
params: dict[str, Any] = {"account_id": self.
|
|
273
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
245
274
|
if cursor:
|
|
246
275
|
params["cursor"] = cursor
|
|
247
276
|
if limit is not None:
|
|
@@ -283,7 +312,7 @@ class ScraperApp(APIApplication):
|
|
|
283
312
|
httpx.HTTPError: If the API request fails.
|
|
284
313
|
"""
|
|
285
314
|
url = f"{self.base_url}/api/v1/linkedin/search"
|
|
286
|
-
params: dict[str, Any] = {"account_id": self.
|
|
315
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
287
316
|
if cursor:
|
|
288
317
|
params["cursor"] = cursor
|
|
289
318
|
if limit is not None:
|
|
@@ -328,7 +357,7 @@ class ScraperApp(APIApplication):
|
|
|
328
357
|
ValueError: If the specified location is not found.
|
|
329
358
|
"""
|
|
330
359
|
url = f"{self.base_url}/api/v1/linkedin/search"
|
|
331
|
-
params: dict[str, Any] = {"account_id": self.
|
|
360
|
+
params: dict[str, Any] = {"account_id": await self._get_account_id()}
|
|
332
361
|
if cursor:
|
|
333
362
|
params["cursor"] = cursor
|
|
334
363
|
if limit is not None:
|
|
@@ -360,6 +389,7 @@ class ScraperApp(APIApplication):
|
|
|
360
389
|
"""
|
|
361
390
|
return [
|
|
362
391
|
self.linkedin_list_profile_posts,
|
|
392
|
+
self.linkedin_list_profile_comments,
|
|
363
393
|
self.linkedin_retrieve_profile,
|
|
364
394
|
self.linkedin_list_post_comments,
|
|
365
395
|
self.linkedin_search_people,
|
|
@@ -16,7 +16,7 @@ class SemrushApp(APIApplication):
|
|
|
16
16
|
def _get_headers(self):
|
|
17
17
|
if not self.integration:
|
|
18
18
|
raise ValueError("Integration not found")
|
|
19
|
-
credentials = self.integration.
|
|
19
|
+
credentials = await self.integration.get_credentials_async()
|
|
20
20
|
if "api_key" in credentials:
|
|
21
21
|
self.api_key = credentials["api_key"]
|
|
22
22
|
return {}
|
|
@@ -13,8 +13,7 @@ class SerpapiApp(APIApplication):
|
|
|
13
13
|
self._serpapi_api_key: str | None = None
|
|
14
14
|
self.base_url = "https://serpapi.com/search"
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
def serpapi_api_key(self) -> str:
|
|
16
|
+
async def get_serpapi_api_key(self) -> str:
|
|
18
17
|
"""
|
|
19
18
|
A property that lazily retrieves the SerpApi API key from the integration and caches it for future use. It fetches credentials on first access, raising a `NotAuthorizedError` if the key is missing. Subsequent calls efficiently return the cached key.
|
|
20
19
|
"""
|
|
@@ -23,7 +22,7 @@ class SerpapiApp(APIApplication):
|
|
|
23
22
|
logger.error("SerpApi App: Integration not configured.")
|
|
24
23
|
raise NotAuthorizedError("Integration not configured for SerpApi App. Cannot retrieve API key.")
|
|
25
24
|
try:
|
|
26
|
-
credentials = self.integration.
|
|
25
|
+
credentials = await self.integration.get_credentials_async()
|
|
27
26
|
except NotAuthorizedError as e:
|
|
28
27
|
logger.error(f"SerpApi App: Authorization error when fetching credentials: {e.message}")
|
|
29
28
|
raise
|
|
@@ -71,9 +70,9 @@ class SerpapiApp(APIApplication):
|
|
|
71
70
|
"""
|
|
72
71
|
request_params = params or {}
|
|
73
72
|
try:
|
|
74
|
-
|
|
73
|
+
api_key = await self.get_serpapi_api_key()
|
|
75
74
|
logger.info("Attempting SerpApi search.")
|
|
76
|
-
serpapi_call_params = {"api_key":
|
|
75
|
+
serpapi_call_params = {"api_key": api_key, "engine": "google_light", **request_params}
|
|
77
76
|
search_client = SerpApiSearch(serpapi_call_params)
|
|
78
77
|
data = search_client.get_dict()
|
|
79
78
|
if "error" in data:
|
|
@@ -142,7 +141,8 @@ class SerpapiApp(APIApplication):
|
|
|
142
141
|
google-maps, search, location, places, important
|
|
143
142
|
"""
|
|
144
143
|
query_params = {}
|
|
145
|
-
|
|
144
|
+
api_key = await self.get_serpapi_api_key()
|
|
145
|
+
query_params = {"engine": "google_maps", "api_key": api_key}
|
|
146
146
|
if q is not None:
|
|
147
147
|
query_params["q"] = q
|
|
148
148
|
if ll is not None:
|
|
@@ -176,7 +176,8 @@ class SerpapiApp(APIApplication):
|
|
|
176
176
|
google-maps, reviews, ratings, places, important
|
|
177
177
|
"""
|
|
178
178
|
query_params = {}
|
|
179
|
-
|
|
179
|
+
api_key = await self.get_serpapi_api_key()
|
|
180
|
+
query_params = {"engine": "google_maps_reviews", "data_id": data_id, "api_key": api_key}
|
|
180
181
|
if hl is not None:
|
|
181
182
|
query_params["hl"] = hl
|
|
182
183
|
else:
|
|
@@ -7,16 +7,15 @@ from universal_mcp.integrations import Integration
|
|
|
7
7
|
class ShopifyApp(APIApplication):
|
|
8
8
|
def __init__(self, integration: Integration = None, **kwargs) -> None:
|
|
9
9
|
super().__init__(name="shopify", integration=integration, **kwargs)
|
|
10
|
-
self.
|
|
10
|
+
self._base_url = None
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
def base_url(self) -> str:
|
|
12
|
+
async def get_base_url(self) -> str:
|
|
14
13
|
"""
|
|
15
14
|
Get the base URL for the Shopify API.
|
|
16
15
|
This is constructed from the integration's credentials.
|
|
17
16
|
"""
|
|
18
17
|
if not self._base_url:
|
|
19
|
-
credentials = self.integration.
|
|
18
|
+
credentials = await self.integration.get_credentials_async()
|
|
20
19
|
subdomain = credentials.get("subdomain")
|
|
21
20
|
if not subdomain:
|
|
22
21
|
logger.error("Integration credentials must include 'subdomain'.")
|
|
@@ -24,8 +23,7 @@ class ShopifyApp(APIApplication):
|
|
|
24
23
|
self._base_url = f"https://{subdomain}.myshopify.com"
|
|
25
24
|
return self._base_url
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
def base_url(self, base_url: str) -> None:
|
|
26
|
+
def set_base_url(self, base_url: str) -> None:
|
|
29
27
|
"""
|
|
30
28
|
Set the base URL for the Shopify API.
|
|
31
29
|
This is useful for testing or if the base URL changes.
|
|
@@ -50,7 +48,7 @@ class ShopifyApp(APIApplication):
|
|
|
50
48
|
Tags:
|
|
51
49
|
Access, AccessScope
|
|
52
50
|
"""
|
|
53
|
-
url = f"{self.
|
|
51
|
+
url = f"{await self.get_base_url()}/admin/oauth/access_scopes.json"
|
|
54
52
|
query_params = {}
|
|
55
53
|
response = await self._aget(url, params=query_params)
|
|
56
54
|
response.raise_for_status()
|