universal-mcp-applications 0.1.33__py3-none-any.whl → 0.1.39rc8__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 (113) hide show
  1. universal_mcp/applications/ahrefs/app.py +92 -238
  2. universal_mcp/applications/airtable/app.py +23 -122
  3. universal_mcp/applications/apollo/app.py +122 -475
  4. universal_mcp/applications/asana/app.py +605 -1755
  5. universal_mcp/applications/aws_s3/app.py +36 -103
  6. universal_mcp/applications/bill/app.py +644 -2055
  7. universal_mcp/applications/box/app.py +1246 -4159
  8. universal_mcp/applications/braze/app.py +410 -1476
  9. universal_mcp/applications/browser_use/README.md +15 -1
  10. universal_mcp/applications/browser_use/__init__.py +1 -0
  11. universal_mcp/applications/browser_use/app.py +86 -24
  12. universal_mcp/applications/cal_com_v2/app.py +207 -625
  13. universal_mcp/applications/calendly/app.py +103 -242
  14. universal_mcp/applications/canva/app.py +75 -140
  15. universal_mcp/applications/clickup/app.py +331 -798
  16. universal_mcp/applications/coda/app.py +240 -520
  17. universal_mcp/applications/confluence/app.py +497 -1285
  18. universal_mcp/applications/contentful/app.py +36 -151
  19. universal_mcp/applications/crustdata/app.py +42 -121
  20. universal_mcp/applications/dialpad/app.py +451 -924
  21. universal_mcp/applications/digitalocean/app.py +2071 -6082
  22. universal_mcp/applications/domain_checker/app.py +3 -54
  23. universal_mcp/applications/e2b/app.py +14 -64
  24. universal_mcp/applications/elevenlabs/app.py +9 -47
  25. universal_mcp/applications/exa/README.md +8 -4
  26. universal_mcp/applications/exa/app.py +408 -186
  27. universal_mcp/applications/falai/app.py +24 -101
  28. universal_mcp/applications/figma/app.py +91 -175
  29. universal_mcp/applications/file_system/app.py +2 -13
  30. universal_mcp/applications/firecrawl/app.py +186 -163
  31. universal_mcp/applications/fireflies/app.py +59 -281
  32. universal_mcp/applications/fpl/app.py +92 -529
  33. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  34. universal_mcp/applications/fpl/utils/helper.py +25 -89
  35. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  36. universal_mcp/applications/ghost_content/app.py +66 -175
  37. universal_mcp/applications/github/app.py +28 -65
  38. universal_mcp/applications/gong/app.py +140 -300
  39. universal_mcp/applications/google_calendar/app.py +26 -78
  40. universal_mcp/applications/google_docs/app.py +98 -202
  41. universal_mcp/applications/google_drive/app.py +194 -793
  42. universal_mcp/applications/google_gemini/app.py +27 -62
  43. universal_mcp/applications/google_mail/README.md +1 -0
  44. universal_mcp/applications/google_mail/app.py +93 -214
  45. universal_mcp/applications/google_searchconsole/app.py +25 -58
  46. universal_mcp/applications/google_sheet/app.py +171 -624
  47. universal_mcp/applications/google_sheet/helper.py +26 -53
  48. universal_mcp/applications/hashnode/app.py +57 -269
  49. universal_mcp/applications/heygen/app.py +77 -155
  50. universal_mcp/applications/http_tools/app.py +10 -32
  51. universal_mcp/applications/hubspot/README.md +1 -1
  52. universal_mcp/applications/hubspot/app.py +7508 -99
  53. universal_mcp/applications/jira/app.py +2419 -8334
  54. universal_mcp/applications/klaviyo/app.py +737 -1619
  55. universal_mcp/applications/linkedin/README.md +5 -0
  56. universal_mcp/applications/linkedin/app.py +332 -227
  57. universal_mcp/applications/mailchimp/app.py +696 -1851
  58. universal_mcp/applications/markitdown/app.py +8 -20
  59. universal_mcp/applications/miro/app.py +333 -815
  60. universal_mcp/applications/ms_teams/app.py +85 -207
  61. universal_mcp/applications/neon/app.py +144 -250
  62. universal_mcp/applications/notion/app.py +36 -51
  63. universal_mcp/applications/onedrive/app.py +26 -48
  64. universal_mcp/applications/openai/app.py +42 -165
  65. universal_mcp/applications/outlook/README.md +22 -9
  66. universal_mcp/applications/outlook/app.py +403 -141
  67. universal_mcp/applications/perplexity/README.md +2 -1
  68. universal_mcp/applications/perplexity/app.py +162 -20
  69. universal_mcp/applications/pipedrive/app.py +1021 -3331
  70. universal_mcp/applications/posthog/app.py +272 -541
  71. universal_mcp/applications/reddit/app.py +61 -160
  72. universal_mcp/applications/resend/app.py +41 -107
  73. universal_mcp/applications/retell/app.py +23 -50
  74. universal_mcp/applications/rocketlane/app.py +250 -963
  75. universal_mcp/applications/scraper/app.py +67 -125
  76. universal_mcp/applications/semanticscholar/app.py +36 -78
  77. universal_mcp/applications/semrush/app.py +43 -77
  78. universal_mcp/applications/sendgrid/app.py +826 -1576
  79. universal_mcp/applications/sentry/app.py +444 -1079
  80. universal_mcp/applications/serpapi/app.py +40 -143
  81. universal_mcp/applications/sharepoint/app.py +27 -49
  82. universal_mcp/applications/shopify/app.py +1743 -4479
  83. universal_mcp/applications/shortcut/app.py +272 -534
  84. universal_mcp/applications/slack/app.py +41 -123
  85. universal_mcp/applications/spotify/app.py +206 -405
  86. universal_mcp/applications/supabase/app.py +174 -283
  87. universal_mcp/applications/tavily/app.py +2 -2
  88. universal_mcp/applications/trello/app.py +853 -2816
  89. universal_mcp/applications/twilio/app.py +14 -50
  90. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  91. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  92. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  93. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  94. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  95. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  96. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  97. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  98. universal_mcp/applications/whatsapp/app.py +35 -186
  99. universal_mcp/applications/whatsapp/audio.py +2 -6
  100. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  101. universal_mcp/applications/whatsapp_business/app.py +86 -299
  102. universal_mcp/applications/wrike/app.py +80 -153
  103. universal_mcp/applications/yahoo_finance/app.py +19 -65
  104. universal_mcp/applications/youtube/app.py +120 -306
  105. universal_mcp/applications/zenquotes/app.py +3 -3
  106. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/METADATA +4 -2
  107. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/RECORD +109 -113
  108. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/WHEEL +1 -1
  109. universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
  110. universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
  111. universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
  112. universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
  113. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,10 @@
1
- import json
2
1
  import os
3
- from typing import Any, Callable, Literal
2
+ from collections.abc import Callable
3
+ from typing import Any, Literal
4
+ import requests
4
5
 
5
6
  from loguru import logger
6
- from universal_mcp.applications.application import APIApplication
7
+ from universal_mcp.applications.application import APIApplication, BaseApplication
7
8
  from universal_mcp.integrations import Integration
8
9
 
9
10
 
@@ -23,7 +24,6 @@ class LinkedinApp(APIApplication):
23
24
  `{"headers": {"x-api-key": "YOUR_API_KEY"}}`.
24
25
  """
25
26
  super().__init__(name="linkedin", integration=integration)
26
-
27
27
  self._base_url = None
28
28
  self.account_id = None
29
29
  if self.integration:
@@ -36,12 +36,8 @@ class LinkedinApp(APIApplication):
36
36
  if not self._base_url:
37
37
  unipile_dsn = os.getenv("UNIPILE_DSN")
38
38
  if not unipile_dsn:
39
- logger.error(
40
- "UnipileApp: UNIPILE_DSN environment variable is not set."
41
- )
42
- raise ValueError(
43
- "UnipileApp: UNIPILE_DSN environment variable is required."
44
- )
39
+ logger.error("UnipileApp: UNIPILE_DSN environment variable is not set.")
40
+ raise ValueError("UnipileApp: UNIPILE_DSN environment variable is required.")
45
41
  self._base_url = f"https://{unipile_dsn}"
46
42
  return self._base_url
47
43
 
@@ -56,31 +52,25 @@ class LinkedinApp(APIApplication):
56
52
  Overrides the base class method to use X-Api-Key.
57
53
  """
58
54
  if not self.integration:
59
- logger.warning(
60
- "UnipileApp: No integration configured, returning empty headers."
61
- )
55
+ logger.warning("UnipileApp: No integration configured, returning empty headers.")
62
56
  return {}
63
-
64
57
  api_key = os.getenv("UNIPILE_API_KEY")
65
58
  if not api_key:
66
- logger.error(
67
- "UnipileApp: API key not found in integration credentials for Unipile."
68
- )
69
- return { # Or return minimal headers if some calls might not need auth (unlikely for Unipile)
70
- "Content-Type": "application/json",
71
- "Cache-Control": "no-cache",
72
- }
73
-
59
+ logger.error("UnipileApp: API key not found in integration credentials for Unipile.")
60
+ return {"Content-Type": "application/json", "Cache-Control": "no-cache"}
74
61
  logger.debug("UnipileApp: Using X-Api-Key for authentication.")
75
- return {
76
- "x-api-key": api_key,
77
- "Content-Type": "application/json",
78
- "Cache-Control": "no-cache", # Often good practice for APIs
79
- }
62
+ return {"x-api-key": api_key, "Content-Type": "application/json", "Cache-Control": "no-cache"}
63
+
64
+ async def _aget_headers(self) -> dict[str, str]:
65
+ """
66
+ Get the headers for Unipile API requests asynchronously.
67
+ Overrides the base class method to use X-Api-Key.
68
+ """
69
+ return self._get_headers()
80
70
 
81
- def _get_search_parameter_id(self, param_type: str, keywords: str) -> str:
71
+ async def _aget_search_parameter_id(self, param_type: str, keywords: str) -> str:
82
72
  """
83
- Retrieves the ID for a given LinkedIn search parameter by its name.
73
+ Retrieves the ID for a given LinkedIn search parameter by its name asynchronously.
84
74
 
85
75
  Args:
86
76
  param_type: The type of parameter to search for (e.g., "LOCATION", "COMPANY").
@@ -94,29 +84,52 @@ class LinkedinApp(APIApplication):
94
84
  httpx.HTTPError: If the API request fails.
95
85
  """
96
86
  url = f"{self.base_url}/api/v1/linkedin/search/parameters"
97
- params = {
98
- "account_id": self.account_id,
99
- "keywords": keywords,
100
- "type": param_type,
101
- }
102
-
103
- response = self._get(url, params=params)
87
+ params = {"account_id": self.account_id, "keywords": keywords, "type": param_type}
88
+ response = await self._aget(url, params=params)
104
89
  results = self._handle_response(response)
105
-
106
90
  items = results.get("items", [])
107
91
  if items:
108
- # Return the ID of the first result, assuming it's the most relevant
109
92
  return items[0]["id"]
110
-
111
93
  raise ValueError(f'Could not find a matching ID for {param_type}: "{keywords}"')
112
94
 
113
- def list_all_chats(
95
+ async def start_new_chat(self, provider_id: str, text: str) -> dict[str, Any]:
96
+ """
97
+ Starts a new chat conversation with a specified user by sending an initial message.
98
+ This function constructs a multipart/form-data request using the `files` parameter
99
+ to ensure correct formatting and headers, working around potential issues in the
100
+ underlying request method.
101
+
102
+ Args:
103
+ provider_id: The LinkedIn provider ID of the user to start the chat with.
104
+ This is available in the response of the `retrieve_user_profile` tool.
105
+ text: The initial message content. For LinkedIn Recruiter accounts, this can include
106
+ HTML tags like <strong>, <em>, <a>, <ul>, <ol>, and <li>.
107
+
108
+ Returns:
109
+ A dictionary containing the details of the newly created chat.
110
+
111
+ Raises:
112
+ httpx.HTTPError: If the API request fails.
113
+
114
+ Tags:
115
+ linkedin, chat, create, start, new, messaging, api, important
116
+ """
117
+ url = f"{self.base_url}/api/v1/chats"
118
+ form_payload = {"account_id": (None, self.account_id), "text": (None, text), "attendees_ids": (None, provider_id)}
119
+ api_key = os.getenv("UNIPILE_API_KEY")
120
+ if not api_key:
121
+ raise ValueError("UNIPILE_API_KEY environment variable is not set.")
122
+ headers = {"x-api-key": api_key}
123
+ response = requests.post(url, files=form_payload, headers=headers)
124
+ return self._handle_response(response)
125
+
126
+ async def list_all_chats(
114
127
  self,
115
128
  unread: bool | None = None,
116
129
  cursor: str | None = None,
117
- before: str | None = None, # ISO 8601 UTC datetime
118
- after: str | None = None, # ISO 8601 UTC datetime
119
- limit: int | None = None, # 1-250
130
+ before: str | None = None,
131
+ after: str | None = None,
132
+ limit: int | None = None,
120
133
  account_type: str | None = None,
121
134
  ) -> dict[str, Any]:
122
135
  """
@@ -141,9 +154,7 @@ class LinkedinApp(APIApplication):
141
154
  """
142
155
  url = f"{self.base_url}/api/v1/chats"
143
156
  params: dict[str, Any] = {}
144
-
145
157
  params["account_id"] = self.account_id
146
-
147
158
  if unread is not None:
148
159
  params["unread"] = unread
149
160
  if cursor:
@@ -156,18 +167,16 @@ class LinkedinApp(APIApplication):
156
167
  params["limit"] = limit
157
168
  if account_type:
158
169
  params["account_type"] = account_type
159
-
160
-
161
- response = self._get(url, params=params)
162
- return response.json()
170
+ response = await self._aget(url, params=params)
171
+ return self._handle_response(response)
163
172
 
164
- def list_chat_messages(
173
+ async def list_chat_messages(
165
174
  self,
166
175
  chat_id: str,
167
176
  cursor: str | None = None,
168
- before: str | None = None, # ISO 8601 UTC datetime
169
- after: str | None = None, # ISO 8601 UTC datetime
170
- limit: int | None = None, # 1-250
177
+ before: str | None = None,
178
+ after: str | None = None,
179
+ limit: int | None = None,
171
180
  sender_id: str | None = None,
172
181
  ) -> dict[str, Any]:
173
182
  """
@@ -202,15 +211,10 @@ class LinkedinApp(APIApplication):
202
211
  params["limit"] = limit
203
212
  if sender_id:
204
213
  params["sender_id"] = sender_id
214
+ response = await self._aget(url, params=params)
215
+ return self._handle_response(response)
205
216
 
206
- response = self._get(url, params=params)
207
- return response.json()
208
-
209
- def send_chat_message(
210
- self,
211
- chat_id: str,
212
- text: str,
213
- ) -> dict[str, Any]:
217
+ async def send_chat_message(self, chat_id: str, text: str) -> dict[str, Any]:
214
218
  """
215
219
  Sends a text message to a specific chat conversation using its `chat_id`. This function creates a new message via a POST request, distinguishing it from read-only functions like `list_chat_messages`. It returns the API's response, which typically confirms the successful creation of the message.
216
220
 
@@ -230,11 +234,10 @@ class LinkedinApp(APIApplication):
230
234
  """
231
235
  url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
232
236
  payload: dict[str, Any] = {"text": text}
237
+ response = await self._apost(url, data=payload)
238
+ return self._handle_response(response)
233
239
 
234
- response = self._post(url, data=payload)
235
- return response.json()
236
-
237
- def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
240
+ async def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
238
241
  """
239
242
  Retrieves a single chat's details using its Unipile or provider-specific ID. This function is distinct from `list_all_chats`, which returns a collection, by targeting one specific conversation.
240
243
 
@@ -254,16 +257,15 @@ class LinkedinApp(APIApplication):
254
257
  params: dict[str, Any] = {}
255
258
  if self.account_id:
256
259
  params["account_id"] = self.account_id
260
+ response = await self._aget(url, params=params)
261
+ return self._handle_response(response)
257
262
 
258
- response = self._get(url, params=params)
259
- return response.json()
260
-
261
- def list_all_messages(
263
+ async def list_all_messages(
262
264
  self,
263
265
  cursor: str | None = None,
264
- before: str | None = None, # ISO 8601 UTC datetime
265
- after: str | None = None, # ISO 8601 UTC datetime
266
- limit: int | None = None, # 1-250
266
+ before: str | None = None,
267
+ after: str | None = None,
268
+ limit: int | None = None,
267
269
  sender_id: str | None = None,
268
270
  ) -> dict[str, Any]:
269
271
  """
@@ -299,16 +301,11 @@ class LinkedinApp(APIApplication):
299
301
  params["sender_id"] = sender_id
300
302
  if self.account_id:
301
303
  params["account_id"] = self.account_id
304
+ response = await self._aget(url, params=params)
305
+ return self._handle_response(response)
302
306
 
303
- response = self._get(url, params=params)
304
- return response.json()
305
-
306
- def list_profile_posts(
307
- self,
308
- identifier: str, # User or Company provider internal ID
309
- cursor: str | None = None,
310
- limit: int | None = None, # 1-100 (spec says max 250)
311
- is_company: bool | None = None,
307
+ async def list_profile_posts(
308
+ self, identifier: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
312
309
  ) -> dict[str, Any]:
313
310
  """
314
311
  Retrieves a paginated list of posts from a specific user or company profile using their provider ID. An authorizing `account_id` is required, and the `is_company` flag must specify the entity type, distinguishing this from `retrieve_post` which fetches a single post by its own ID.
@@ -336,11 +333,10 @@ class LinkedinApp(APIApplication):
336
333
  params["limit"] = limit
337
334
  if is_company is not None:
338
335
  params["is_company"] = is_company
336
+ response = await self._aget(url, params=params)
337
+ return self._handle_response(response)
339
338
 
340
- response = self._get(url, params=params)
341
- return response.json()
342
-
343
- def retrieve_own_profile(self) -> dict[str, Any]:
339
+ async def retrieve_own_profile(self) -> dict[str, Any]:
344
340
  """
345
341
  Retrieves the profile details for the user associated with the Unipile account. This function targets the API's 'me' endpoint to fetch the authenticated user's profile, distinct from `retrieve_user_profile` which fetches profiles of other users by their public identifier.
346
342
 
@@ -355,10 +351,10 @@ class LinkedinApp(APIApplication):
355
351
  """
356
352
  url = f"{self.base_url}/api/v1/users/me"
357
353
  params: dict[str, Any] = {"account_id": self.account_id}
358
- response = self._get(url, params=params)
359
- return response.json()
354
+ response = await self._aget(url, params=params)
355
+ return self._handle_response(response)
360
356
 
361
- def retrieve_post(self, post_id: str) -> dict[str, Any]:
357
+ async def retrieve_post(self, post_id: str) -> dict[str, Any]:
362
358
  """
363
359
  Fetches a specific post's details by its unique ID. Unlike `list_profile_posts`, which retrieves a collection of posts from a user or company profile, this function targets one specific post and returns its full object.
364
360
 
@@ -376,21 +372,17 @@ class LinkedinApp(APIApplication):
376
372
  """
377
373
  url = f"{self.base_url}/api/v1/posts/{post_id}"
378
374
  params: dict[str, Any] = {"account_id": self.account_id}
379
- response = self._get(url, params=params)
380
- return response.json()
375
+ response = await self._aget(url, params=params)
376
+ return self._handle_response(response)
381
377
 
382
- def list_post_comments(
383
- self,
384
- post_id: str,
385
- comment_id: str | None = None,
386
- cursor: str | None = None,
387
- limit: int | None = None,
378
+ async def list_post_comments(
379
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
388
380
  ) -> dict[str, Any]:
389
381
  """
390
- Fetches comments for a specific post. Providing an optional `comment_id` retrieves threaded replies instead of top-level comments. This read-only operation contrasts with `create_post_comment`, which publishes new comments, and `list_content_reactions`, which retrieves 'likes'.
382
+ Fetches comments for a specific post. Providing an optional `comment_id` retrieves threaded replies instead of top-level comments. `retrieve_post` or `list_profile_posts` can be used to obtain the `post_id` which is the social_id in their response.
391
383
 
392
384
  Args:
393
- post_id: The social ID of the post.
385
+ post_id: The social ID of the post which you get from using `retrieve_post` or `list_profile_posts` tools.
394
386
  comment_id: If provided, retrieves replies to this comment ID instead of top-level comments.
395
387
  cursor: Pagination cursor.
396
388
  limit: Number of comments to return. (OpenAPI spec shows type string, passed as string if provided).
@@ -412,15 +404,11 @@ class LinkedinApp(APIApplication):
412
404
  params["limit"] = str(limit)
413
405
  if comment_id:
414
406
  params["comment_id"] = comment_id
407
+ response = await self._aget(url, params=params)
408
+ return self._handle_response(response)
415
409
 
416
- response = self._get(url, params=params)
417
- return response.json()
418
-
419
- def create_post(
420
- self,
421
- text: str,
422
- mentions: list[dict[str, Any]] | None = None,
423
- external_link: str | None = None,
410
+ async def create_post(
411
+ self, text: str, mentions: list[dict[str, Any]] | None = None, external_link: str | None = None
424
412
  ) -> dict[str, Any]:
425
413
  """
426
414
  Publishes a new top-level post from the account, including text, user mentions, and an external link. This function creates original content, distinguishing it from `create_post_comment` which adds replies to existing posts.
@@ -441,26 +429,16 @@ class LinkedinApp(APIApplication):
441
429
  linkedin, post, create, share, content, api, important
442
430
  """
443
431
  url = f"{self.base_url}/api/v1/posts"
444
-
445
- params: dict[str, str] = {
446
- "account_id": self.account_id,
447
- "text": text,
448
- }
449
-
432
+ params: dict[str, str] = {"account_id": self.account_id, "text": text}
450
433
  if mentions:
451
434
  params["mentions"] = mentions
452
435
  if external_link:
453
436
  params["external_link"] = external_link
437
+ response = await self._apost(url, data=params)
438
+ return self._handle_response(response)
454
439
 
455
- response = self._post(url, data=params)
456
- return response.json()
457
-
458
- def list_content_reactions(
459
- self,
460
- post_id: str,
461
- comment_id: str | None = None,
462
- cursor: str | None = None,
463
- limit: int | None = None,
440
+ async def list_content_reactions(
441
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
464
442
  ) -> dict[str, Any]:
465
443
  """
466
444
  Retrieves a paginated list of reactions for a given post or, optionally, a specific comment. This read-only operation uses the account for the request, distinguishing it from the `create_reaction` function which adds new reactions.
@@ -488,16 +466,11 @@ class LinkedinApp(APIApplication):
488
466
  params["limit"] = limit
489
467
  if comment_id:
490
468
  params["comment_id"] = comment_id
469
+ response = await self._aget(url, params=params)
470
+ return self._handle_response(response)
491
471
 
492
- response = self._get(url, params=params)
493
- return response.json()
494
-
495
- def create_post_comment(
496
- self,
497
- post_social_id: str,
498
- text: str,
499
- comment_id: str | None = None, # If provided, replies to a specific comment
500
- mentions_body: list[dict[str, Any]] | None = None,
472
+ async def create_post_comment(
473
+ self, post_social_id: str, text: str, comment_id: str | None = None, mentions_body: list[dict[str, Any]] | None = None
501
474
  ) -> dict[str, Any]:
502
475
  """
503
476
  Publishes a comment on a specified post. By providing an optional `comment_id`, it creates a threaded reply to an existing comment instead of a new top-level one. This function's dual capability distinguishes it from `list_post_comments`, which only retrieves comments and their replies.
@@ -519,33 +492,18 @@ class LinkedinApp(APIApplication):
519
492
  linkedin, post, comment, create, content, api, important
520
493
  """
521
494
  url = f"{self.base_url}/api/v1/posts/{post_social_id}/comments"
522
- params: dict[str, Any] = {
523
- "account_id": self.account_id,
524
- "text": text,
525
- }
526
-
495
+ params: dict[str, Any] = {"account_id": self.account_id, "text": text}
527
496
  if comment_id:
528
497
  params["comment_id"] = comment_id
529
-
530
498
  if mentions_body:
531
499
  params = {"mentions": mentions_body}
500
+ response = await self._apost(url, data=params)
501
+ return self._handle_response(response)
532
502
 
533
- response = self._post(url, data=params)
534
-
535
- try:
536
- return response.json()
537
- except json.JSONDecodeError:
538
- return {
539
- "status": response.status_code,
540
- "message": "Comment action processed.",
541
- }
542
-
543
- def create_reaction(
503
+ async def create_reaction(
544
504
  self,
545
505
  post_social_id: str,
546
- reaction_type: Literal[
547
- "like", "celebrate", "love", "insightful", "funny", "support"
548
- ],
506
+ reaction_type: Literal["like", "celebrate", "love", "insightful", "funny", "support"],
549
507
  comment_id: str | None = None,
550
508
  ) -> dict[str, Any]:
551
509
  """
@@ -566,32 +524,18 @@ class LinkedinApp(APIApplication):
566
524
  linkedin, post, reaction, create, like, content, api, important
567
525
  """
568
526
  url = f"{self.base_url}/api/v1/posts/reaction"
569
-
570
- params: dict[str, str] = {
571
- "account_id": self.account_id,
572
- "post_id": post_social_id,
573
- "reaction_type": reaction_type,
574
- }
575
-
527
+ params: dict[str, str] = {"account_id": self.account_id, "post_id": post_social_id, "reaction_type": reaction_type}
576
528
  if comment_id:
577
529
  params["comment_id"] = comment_id
530
+ response = await self._apost(url, data=params)
531
+ return self._handle_response(response)
578
532
 
579
- response = self._post(url, data=params)
580
-
581
- try:
582
- return response.json()
583
- except json.JSONDecodeError:
584
- return {
585
- "status": response.status_code,
586
- "message": "Reaction action processed.",
587
- }
588
-
589
- def retrieve_user_profile(self, identifier: str) -> dict[str, Any]:
533
+ async def retrieve_user_profile(self, public_identifier: str) -> dict[str, Any]:
590
534
  """
591
535
  Retrieves a specific LinkedIn user's profile using their public or internal ID. Unlike `retrieve_own_profile`, which fetches the authenticated user's details, this function targets and returns data for any specified third-party user profile on the platform.
592
536
 
593
537
  Args:
594
- 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".
538
+ public_identifier: Extract this value from the response of `search_people` tool. The response contains a public_identifier field.For example, for https://www.linkedin.com/in/manojbajaj95/, the identifier is "manojbajaj95".
595
539
 
596
540
  Returns:
597
541
  A dictionary containing the user's profile details.
@@ -602,12 +546,12 @@ class LinkedinApp(APIApplication):
602
546
  Tags:
603
547
  linkedin, user, profile, retrieve, get, api, important
604
548
  """
605
- url = f"{self.base_url}/api/v1/users/{identifier}"
549
+ url = f"{self.base_url}/api/v1/users/{public_identifier}"
606
550
  params: dict[str, Any] = {"account_id": self.account_id}
607
- response = self._get(url, params=params)
551
+ response = await self._aget(url, params=params)
608
552
  return self._handle_response(response)
609
553
 
610
- def search_people(
554
+ async def search_people(
611
555
  self,
612
556
  cursor: str | None = None,
613
557
  limit: int | None = None,
@@ -618,7 +562,7 @@ class LinkedinApp(APIApplication):
618
562
  ) -> dict[str, Any]:
619
563
  """
620
564
  Searches for LinkedIn user profiles using keywords, with optional filters for location, industry, and company. This function specifically targets the 'people' category, distinguishing it from other search methods like `search_companies` or `search_jobs` that query different entity types through the same API endpoint.
621
-
565
+
622
566
  Args:
623
567
  cursor: Pagination cursor for the next page of entries.
624
568
  limit: Number of items to return (up to 50 for Classic search).
@@ -626,42 +570,35 @@ class LinkedinApp(APIApplication):
626
570
  location: The geographical location to filter people by (e.g., "United States").
627
571
  industry: The industry to filter people by.(eg., "Information Technology and Services").
628
572
  company: The company to filter people by.(e.g., "Google").
629
-
573
+
630
574
  Returns:
631
575
  A dictionary containing search results and pagination details.
632
-
576
+
633
577
  Raises:
634
578
  httpx.HTTPError: If the API request fails.
635
579
  """
636
580
  url = f"{self.base_url}/api/v1/linkedin/search"
637
-
638
581
  params: dict[str, Any] = {"account_id": self.account_id}
639
582
  if cursor:
640
583
  params["cursor"] = cursor
641
584
  if limit is not None:
642
585
  params["limit"] = limit
643
-
644
586
  payload: dict[str, Any] = {"api": "classic", "category": "people"}
645
-
646
587
  if keywords:
647
588
  payload["keywords"] = keywords
648
-
649
589
  if location:
650
- location_id = self._get_search_parameter_id("LOCATION", location)
590
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
651
591
  payload["location"] = [location_id]
652
-
653
592
  if industry:
654
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
593
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
655
594
  payload["industry"] = [industry_id]
656
-
657
595
  if company:
658
- company_id = self._get_search_parameter_id("COMPANY", company)
596
+ company_id = await self._aget_search_parameter_id("COMPANY", company)
659
597
  payload["company"] = [company_id]
660
-
661
- response = self._post(url, params=params, data=payload)
598
+ response = await self._apost(url, params=params, data=payload)
662
599
  return self._handle_response(response)
663
600
 
664
- def search_companies(
601
+ async def search_companies(
665
602
  self,
666
603
  cursor: str | None = None,
667
604
  limit: int | None = None,
@@ -671,45 +608,39 @@ class LinkedinApp(APIApplication):
671
608
  ) -> dict[str, Any]:
672
609
  """
673
610
  Performs a paginated search for companies on LinkedIn using keywords, with optional location and industry filters. Its specific 'companies' search category distinguishes it from other methods like `search_people` or `search_posts`, ensuring that only company profiles are returned.
674
-
611
+
675
612
  Args:
676
613
  cursor: Pagination cursor for the next page of entries.
677
614
  limit: Number of items to return (up to 50 for Classic search).
678
615
  keywords: Keywords to search for.
679
616
  location: The geographical location to filter companies by (e.g., "United States").
680
617
  industry: The industry to filter companies by.(e.g., "Information Technology and Services").
681
-
618
+
682
619
  Returns:
683
620
  A dictionary containing search results and pagination details.
684
-
621
+
685
622
  Raises:
686
623
  httpx.HTTPError: If the API request fails.
687
624
  """
688
625
  url = f"{self.base_url}/api/v1/linkedin/search"
689
-
690
626
  params: dict[str, Any] = {"account_id": self.account_id}
691
627
  if cursor:
692
628
  params["cursor"] = cursor
693
629
  if limit is not None:
694
630
  params["limit"] = limit
695
-
696
631
  payload: dict[str, Any] = {"api": "classic", "category": "companies"}
697
-
698
632
  if keywords:
699
633
  payload["keywords"] = keywords
700
-
701
634
  if location:
702
- location_id = self._get_search_parameter_id("LOCATION", location)
635
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
703
636
  payload["location"] = [location_id]
704
-
705
637
  if industry:
706
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
638
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
707
639
  payload["industry"] = [industry_id]
708
-
709
- response = self._post(url, params=params, data=payload)
640
+ response = await self._apost(url, params=params, data=payload)
710
641
  return self._handle_response(response)
711
642
 
712
- def search_posts(
643
+ async def search_posts(
713
644
  self,
714
645
  cursor: str | None = None,
715
646
  limit: int | None = None,
@@ -719,105 +650,272 @@ class LinkedinApp(APIApplication):
719
650
  ) -> dict[str, Any]:
720
651
  """
721
652
  Performs a keyword-based search for LinkedIn posts, allowing filters for date and sorting by relevance. This function executes a general, platform-wide content search, distinguishing it from other search functions that target people, companies, or jobs, and from `list_profile_posts` which retrieves from a specific profile.
722
-
653
+
723
654
  Args:
724
655
  cursor: Pagination cursor for the next page of entries.
725
656
  limit: Number of items to return (up to 50 for Classic search).
726
657
  keywords: Keywords to search for.
727
658
  date_posted: Filter by when the post was posted.
728
659
  sort_by: How to sort the results.
729
-
660
+
730
661
  Returns:
731
662
  A dictionary containing search results and pagination details.
732
-
663
+
733
664
  Raises:
734
665
  httpx.HTTPError: If the API request fails.
735
666
  """
736
667
  url = f"{self.base_url}/api/v1/linkedin/search"
737
-
738
668
  params: dict[str, Any] = {"account_id": self.account_id}
739
669
  if cursor:
740
670
  params["cursor"] = cursor
741
671
  if limit is not None:
742
672
  params["limit"] = limit
743
-
744
673
  payload: dict[str, Any] = {"api": "classic", "category": "posts"}
745
-
746
674
  if keywords:
747
675
  payload["keywords"] = keywords
748
676
  if date_posted:
749
677
  payload["date_posted"] = date_posted
750
678
  if sort_by:
751
679
  payload["sort_by"] = sort_by
752
-
753
- response = self._post(url, params=params, data=payload)
680
+ response = await self._apost(url, params=params, data=payload)
754
681
  return self._handle_response(response)
755
682
 
756
- def search_jobs(
683
+ async def search_jobs(
757
684
  self,
758
685
  cursor: str | None = None,
759
686
  limit: int | None = None,
760
687
  keywords: str | None = None,
761
688
  region: str | None = None,
762
689
  sort_by: Literal["relevance", "date"] = "relevance",
763
- minimum_salary_value: int = 40,
690
+ minimum_salary_value: Literal[40, 60, 80, 100, 120, 140, 160, 180, 200] = 40,
764
691
  industry: str | None = None,
765
692
  ) -> dict[str, Any]:
766
693
  """
767
694
  Performs a LinkedIn search for jobs, filtering results by keywords, region, industry, and minimum salary. Unlike other search functions (`search_people`, `search_companies`), this method is specifically configured to query the 'jobs' category, providing a paginated list of relevant employment opportunities.
768
-
695
+
769
696
  Args:
770
697
  cursor: Pagination cursor for the next page of entries.
771
698
  limit: Number of items to return (up to 50 for Classic search).
772
699
  keywords: Keywords to search for.
773
700
  region: The geographical region to filter jobs by (e.g., "United States").
774
701
  sort_by: How to sort the results.(e.g., "relevance" or "date".)
775
- minimum_salary_value: The minimum salary to filter for.
702
+ minimum_salary_value: The minimum salary to filter for. Allowed values are 40, 60, 80, 100, 120, 140, 160, 180, 200.
776
703
  industry: The industry to filter jobs by.(e.g., "Software Development").
777
-
704
+
778
705
  Returns:
779
706
  A dictionary containing search results and pagination details.
780
-
707
+
781
708
  Raises:
782
709
  httpx.HTTPError: If the API request fails.
783
710
  ValueError: If the specified location is not found.
784
711
  """
785
712
  url = f"{self.base_url}/api/v1/linkedin/search"
786
-
787
713
  params: dict[str, Any] = {"account_id": self.account_id}
788
714
  if cursor:
789
715
  params["cursor"] = cursor
790
716
  if limit is not None:
791
717
  params["limit"] = limit
792
-
793
718
  payload: dict[str, Any] = {
794
719
  "api": "classic",
795
720
  "category": "jobs",
796
- "minimum_salary": {
797
- "currency": "USD",
798
- "value": minimum_salary_value,
799
- },
721
+ "minimum_salary": {"currency": "USD", "value": minimum_salary_value},
800
722
  }
801
-
802
723
  if keywords:
803
724
  payload["keywords"] = keywords
804
725
  if sort_by:
805
726
  payload["sort_by"] = sort_by
806
-
807
- # If location is provided, get its ID and add it to the payload
808
727
  if region:
809
- location_id = self._get_search_parameter_id("LOCATION", region)
728
+ location_id = await self._aget_search_parameter_id("LOCATION", region)
810
729
  payload["region"] = location_id
811
-
812
730
  if industry:
813
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
731
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
814
732
  payload["industry"] = [industry_id]
733
+ response = await self._apost(url, params=params, data=payload)
734
+ return self._handle_response(response)
735
+
736
+ async def send_invitation(self, provider_id: str, user_email: str | None = None, message: str | None = None) -> dict[str, Any]:
737
+ """
738
+ Sends a connection invitation to a LinkedIn user specified by their provider ID. An optional message and the user's email can be included.
739
+
740
+ Args:
741
+ provider_id: The LinkedIn provider ID of the user to invite. This is available in response of `retrieve_user_profile` tool.
742
+ user_email: Optional. The email address of the user, which may be required by LinkedIn.
743
+ message: Optional. A personalized message to include with the invitation (max 300 characters).
744
+
745
+ Returns:
746
+ A dictionary confirming the invitation was sent.
747
+
748
+ Raises:
749
+ httpx.HTTPError: If the API request fails.
750
+ ValueError: If the message exceeds 300 characters.
751
+
752
+ Tags:
753
+ linkedin, user, invite, connect, contact, api, important
754
+ """
755
+ url = f"{self.base_url}/api/v1/users/invite"
756
+ payload: dict[str, Any] = {"account_id": self.account_id, "provider_id": provider_id}
757
+ if user_email:
758
+ payload["user_email"] = user_email
759
+ if message:
760
+ if len(message) > 300:
761
+ raise ValueError("Message cannot exceed 300 characters.")
762
+ payload["message"] = message
763
+ response = await self._apost(url, data=payload)
764
+ return self._handle_response(response)
765
+
766
+ async def list_sent_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
767
+ """
768
+ Retrieves a paginated list of all sent connection invitations that are currently pending. This function allows for iterating through the history of outstanding connection requests made from the specified account.
769
+
770
+ Args:
771
+ cursor: A pagination cursor for retrieving the next page of entries.
772
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
815
773
 
816
- response = self._post(url, params=params, data=payload)
774
+ Returns:
775
+ A dictionary containing a list of sent invitation objects and pagination details.
776
+
777
+ Raises:
778
+ httpx.HTTPError: If the API request fails.
779
+
780
+ Tags:
781
+ linkedin, user, invite, sent, list, contacts, api
782
+ """
783
+ url = f"{self.base_url}/api/v1/users/invite/sent"
784
+ params: dict[str, Any] = {"account_id": self.account_id}
785
+ if cursor:
786
+ params["cursor"] = cursor
787
+ if limit is not None:
788
+ params["limit"] = limit
789
+ response = await self._aget(url, params=params)
790
+ return self._handle_response(response)
791
+
792
+ async def list_received_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
793
+ """
794
+ Retrieves a paginated list of all received connection invitations. This function allows for reviewing and processing incoming connection requests to the specified account.
795
+
796
+ Args:
797
+ cursor: A pagination cursor for retrieving the next page of entries.
798
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
799
+
800
+ Returns:
801
+ A dictionary containing a list of received invitation objects and pagination details.
802
+
803
+ Raises:
804
+ httpx.HTTPError: If the API request fails.
805
+
806
+ Tags:
807
+ linkedin, user, invite, received, list, contacts, api
808
+ """
809
+ url = f"{self.base_url}/api/v1/users/invite/received"
810
+ params: dict[str, Any] = {"account_id": self.account_id}
811
+ if cursor:
812
+ params["cursor"] = cursor
813
+ if limit is not None:
814
+ params["limit"] = limit
815
+ response = await self._aget(url, params=params)
816
+ return self._handle_response(response)
817
+
818
+ async def handle_received_invitation(
819
+ self, invitation_id: str, action: Literal["accept", "decline"], shared_secret: str
820
+ ) -> dict[str, Any]:
821
+ """
822
+ Accepts or declines a received LinkedIn connection invitation using its ID and a required shared secret. This function performs a POST request to update the invitation's status, distinguishing it from read-only functions like `list_received_invitations`.
823
+
824
+ Args:
825
+ invitation_id: The ID of the invitation to handle.Get this ID from the 'list_received_invitations' tool.
826
+ action: The action to perform, either "accept" or "decline".
827
+ shared_secret: The token provided by LinkedIn, retrieved from the 'list_received_invitations' tool, which is mandatory for this action.
828
+
829
+ Returns:
830
+ A dictionary confirming the action was processed.
831
+
832
+ Raises:
833
+ httpx.HTTPError: If the API request fails.
834
+
835
+ Tags:
836
+ linkedin, user, invite, received, handle, accept, decline, api
837
+ """
838
+ url = f"{self.base_url}/api/v1/users/invite/received/{invitation_id}"
839
+ payload: dict[str, Any] = {"provider": "LINKEDIN", "action": action, "shared_secret": shared_secret, "account_id": self.account_id}
840
+ response = await self._apost(url, data=payload)
841
+ return self._handle_response(response)
842
+
843
+ async def cancel_sent_invitation(self, invitation_id: str) -> dict[str, Any]:
844
+ """
845
+ Cancels a sent LinkedIn connection invitation that is currently pending. This function performs a DELETE request to remove the invitation, withdrawing the connection request.
846
+
847
+ Args:
848
+ invitation_id: The unique ID of the invitation to cancel. This ID can be obtained from the 'list_sent_invitations' tool.
849
+
850
+ Returns:
851
+ A dictionary confirming the invitation was cancelled.
852
+
853
+ Raises:
854
+ httpx.HTTPError: If the API request fails.
855
+
856
+ Tags:
857
+ linkedin, user, invite, sent, cancel, delete, api
858
+ """
859
+ url = f"{self.base_url}/api/v1/users/invite/sent/{invitation_id}"
860
+ params = {"account_id": self.account_id}
861
+ response = await self._adelete(url, params=params)
862
+ return self._handle_response(response)
863
+
864
+ async def list_followers(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
865
+ """
866
+ Retrieves a paginated list of all followers for the current user's account. This function is distinct from `list_following` as it shows who follows the user, not who the user follows.
867
+
868
+ Args:
869
+ cursor: A pagination cursor for retrieving the next page of entries.
870
+ limit: The number of items to return, ranging from 1 to 1000.
871
+
872
+ Returns:
873
+ A dictionary containing a list of follower objects and pagination details.
874
+
875
+ Raises:
876
+ httpx.HTTPError: If the API request fails.
877
+
878
+ Tags:
879
+ linkedin, user, followers, list, contacts, api
880
+ """
881
+ url = f"{self.base_url}/api/v1/users/followers"
882
+ params: dict[str, Any] = {"account_id": self.account_id}
883
+ if cursor:
884
+ params["cursor"] = cursor
885
+ if limit is not None:
886
+ params["limit"] = limit
887
+ response = await self._aget(url, params=params)
888
+ return self._handle_response(response)
889
+
890
+ async def list_following(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
891
+ """
892
+ Retrieves a paginated list of all accounts that the current user is following. This function is the counterpart to `list_followers`, focusing on the user's outgoing connections rather than incoming ones.
893
+
894
+ Args:
895
+ cursor: A pagination cursor for retrieving the next page of entries.
896
+ limit: The number of items to return, ranging from 1 to 1000.
897
+
898
+ Returns:
899
+ A dictionary containing a list of followed account objects and pagination details.
900
+
901
+ Raises:
902
+ httpx.HTTPError: If the API request fails.
903
+
904
+ Tags:
905
+ linkedin, user, following, list, contacts, api
906
+ """
907
+ url = f"{self.base_url}/api/v1/users/following"
908
+ params: dict[str, Any] = {"account_id": self.account_id}
909
+ if cursor:
910
+ params["cursor"] = cursor
911
+ if limit is not None:
912
+ params["limit"] = limit
913
+ response = self._get(url, params=params)
817
914
  return self._handle_response(response)
818
915
 
819
916
  def list_tools(self) -> list[Callable]:
820
917
  return [
918
+ self.start_new_chat,
821
919
  self.list_all_chats,
822
920
  self.list_chat_messages,
823
921
  self.send_chat_message,
@@ -836,4 +934,11 @@ class LinkedinApp(APIApplication):
836
934
  self.search_jobs,
837
935
  self.search_people,
838
936
  self.search_posts,
937
+ self.send_invitation,
938
+ self.list_sent_invitations,
939
+ self.cancel_sent_invitation,
940
+ self.list_received_invitations,
941
+ self.handle_received_invitation,
942
+ self.list_followers,
943
+ # self.list_following this endpoint is not yet implemented by unipile
839
944
  ]