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.

Files changed (45) hide show
  1. universal_mcp/applications/BEST_PRACTICES.md +1 -1
  2. universal_mcp/applications/airtable/app.py +13 -13
  3. universal_mcp/applications/apollo/app.py +2 -2
  4. universal_mcp/applications/aws_s3/app.py +30 -19
  5. universal_mcp/applications/browser_use/app.py +10 -7
  6. universal_mcp/applications/contentful/app.py +4 -4
  7. universal_mcp/applications/crustdata/app.py +2 -2
  8. universal_mcp/applications/e2b/app.py +3 -4
  9. universal_mcp/applications/elevenlabs/README.md +27 -3
  10. universal_mcp/applications/elevenlabs/app.py +753 -48
  11. universal_mcp/applications/exa/app.py +18 -11
  12. universal_mcp/applications/falai/README.md +5 -7
  13. universal_mcp/applications/falai/app.py +160 -159
  14. universal_mcp/applications/firecrawl/app.py +14 -15
  15. universal_mcp/applications/ghost_content/app.py +4 -4
  16. universal_mcp/applications/github/app.py +2 -2
  17. universal_mcp/applications/gong/app.py +2 -2
  18. universal_mcp/applications/google_docs/README.md +15 -14
  19. universal_mcp/applications/google_docs/app.py +5 -4
  20. universal_mcp/applications/google_gemini/app.py +61 -17
  21. universal_mcp/applications/google_sheet/README.md +2 -1
  22. universal_mcp/applications/google_sheet/app.py +55 -0
  23. universal_mcp/applications/heygen/README.md +10 -32
  24. universal_mcp/applications/heygen/app.py +350 -744
  25. universal_mcp/applications/klaviyo/app.py +2 -2
  26. universal_mcp/applications/linkedin/README.md +14 -2
  27. universal_mcp/applications/linkedin/app.py +411 -38
  28. universal_mcp/applications/ms_teams/app.py +420 -1285
  29. universal_mcp/applications/notion/app.py +2 -2
  30. universal_mcp/applications/openai/app.py +1 -1
  31. universal_mcp/applications/perplexity/app.py +6 -7
  32. universal_mcp/applications/reddit/app.py +4 -4
  33. universal_mcp/applications/resend/app.py +31 -32
  34. universal_mcp/applications/rocketlane/app.py +2 -2
  35. universal_mcp/applications/scraper/app.py +51 -21
  36. universal_mcp/applications/semrush/app.py +1 -1
  37. universal_mcp/applications/serpapi/app.py +8 -7
  38. universal_mcp/applications/shopify/app.py +5 -7
  39. universal_mcp/applications/shortcut/app.py +3 -2
  40. universal_mcp/applications/slack/app.py +2 -2
  41. universal_mcp/applications/twilio/app.py +14 -13
  42. {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/METADATA +1 -1
  43. {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/RECORD +45 -45
  44. {universal_mcp_applications-0.1.39rc8.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/WHEEL +0 -0
  45. {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 _get_headers(self):
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.get_credentials()
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.get_credentials()
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
- @property
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.get_credentials()
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.perplexity_api_key
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 _post(self, url, data):
15
+ async def _apost(self, url, data):
16
16
  try:
17
- headers = self._get_headers()
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 _get_headers(self):
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.get_credentials()
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
- @property
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.get_credentials()
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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.api_key
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 _get_headers(self) -> dict[str, str]:
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.get_credentials()
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.get_credentials()
21
- self.account_id = credentials.get("account_id")
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
- self.account_id = None
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.account_id, "keywords": keywords, "type": param_type}
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, identifier: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
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
- identifier: The entity's provider internal ID (LinkedIn ID).
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 identifier is for a company.
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/{identifier}/posts"
109
- params: dict[str, Any] = {"account_id": self.account_id}
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 linkedin_retrieve_profile(self, identifier: str) -> dict[str, Any]:
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
- identifier: 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".
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/{identifier}"
136
- params: dict[str, Any] = {"account_id": self.account_id}
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.account_id}
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.account_id}
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.account_id}
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.account_id}
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.account_id}
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.get_credentials()
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
- @property
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.get_credentials()
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
- current_api_key = self.serpapi_api_key
73
+ api_key = await self.get_serpapi_api_key()
75
74
  logger.info("Attempting SerpApi search.")
76
- serpapi_call_params = {"api_key": current_api_key, "engine": "google_light", **request_params}
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
- query_params = {"engine": "google_maps", "api_key": self.serpapi_api_key}
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
- query_params = {"engine": "google_maps_reviews", "data_id": data_id, "api_key": self.serpapi_api_key}
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.base_url = None
10
+ self._base_url = None
11
11
 
12
- @property
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.get_credentials()
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
- @base_url.setter
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.base_url}/admin/oauth/access_scopes.json"
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()