universal-mcp-applications 0.1.33__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 (119) hide show
  1. universal_mcp/applications/BEST_PRACTICES.md +1 -1
  2. universal_mcp/applications/ahrefs/app.py +92 -238
  3. universal_mcp/applications/airtable/app.py +36 -135
  4. universal_mcp/applications/apollo/app.py +124 -477
  5. universal_mcp/applications/asana/app.py +605 -1755
  6. universal_mcp/applications/aws_s3/app.py +63 -119
  7. universal_mcp/applications/bill/app.py +644 -2055
  8. universal_mcp/applications/box/app.py +1246 -4159
  9. universal_mcp/applications/braze/app.py +410 -1476
  10. universal_mcp/applications/browser_use/README.md +15 -1
  11. universal_mcp/applications/browser_use/__init__.py +1 -0
  12. universal_mcp/applications/browser_use/app.py +91 -26
  13. universal_mcp/applications/cal_com_v2/app.py +207 -625
  14. universal_mcp/applications/calendly/app.py +103 -242
  15. universal_mcp/applications/canva/app.py +75 -140
  16. universal_mcp/applications/clickup/app.py +331 -798
  17. universal_mcp/applications/coda/app.py +240 -520
  18. universal_mcp/applications/confluence/app.py +497 -1285
  19. universal_mcp/applications/contentful/app.py +40 -155
  20. universal_mcp/applications/crustdata/app.py +44 -123
  21. universal_mcp/applications/dialpad/app.py +451 -924
  22. universal_mcp/applications/digitalocean/app.py +2071 -6082
  23. universal_mcp/applications/domain_checker/app.py +3 -54
  24. universal_mcp/applications/e2b/app.py +17 -68
  25. universal_mcp/applications/elevenlabs/README.md +27 -3
  26. universal_mcp/applications/elevenlabs/app.py +741 -74
  27. universal_mcp/applications/exa/README.md +8 -4
  28. universal_mcp/applications/exa/app.py +415 -186
  29. universal_mcp/applications/falai/README.md +5 -7
  30. universal_mcp/applications/falai/app.py +156 -232
  31. universal_mcp/applications/figma/app.py +91 -175
  32. universal_mcp/applications/file_system/app.py +2 -13
  33. universal_mcp/applications/firecrawl/app.py +198 -176
  34. universal_mcp/applications/fireflies/app.py +59 -281
  35. universal_mcp/applications/fpl/app.py +92 -529
  36. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  37. universal_mcp/applications/fpl/utils/helper.py +25 -89
  38. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  39. universal_mcp/applications/ghost_content/app.py +70 -179
  40. universal_mcp/applications/github/app.py +30 -67
  41. universal_mcp/applications/gong/app.py +142 -302
  42. universal_mcp/applications/google_calendar/app.py +26 -78
  43. universal_mcp/applications/google_docs/README.md +15 -14
  44. universal_mcp/applications/google_docs/app.py +103 -206
  45. universal_mcp/applications/google_drive/app.py +194 -793
  46. universal_mcp/applications/google_gemini/app.py +68 -59
  47. universal_mcp/applications/google_mail/README.md +1 -0
  48. universal_mcp/applications/google_mail/app.py +93 -214
  49. universal_mcp/applications/google_searchconsole/app.py +25 -58
  50. universal_mcp/applications/google_sheet/README.md +2 -1
  51. universal_mcp/applications/google_sheet/app.py +226 -624
  52. universal_mcp/applications/google_sheet/helper.py +26 -53
  53. universal_mcp/applications/hashnode/app.py +57 -269
  54. universal_mcp/applications/heygen/README.md +10 -32
  55. universal_mcp/applications/heygen/app.py +339 -811
  56. universal_mcp/applications/http_tools/app.py +10 -32
  57. universal_mcp/applications/hubspot/README.md +1 -1
  58. universal_mcp/applications/hubspot/app.py +7508 -99
  59. universal_mcp/applications/jira/app.py +2419 -8334
  60. universal_mcp/applications/klaviyo/app.py +739 -1621
  61. universal_mcp/applications/linkedin/README.md +18 -1
  62. universal_mcp/applications/linkedin/app.py +729 -251
  63. universal_mcp/applications/mailchimp/app.py +696 -1851
  64. universal_mcp/applications/markitdown/app.py +8 -20
  65. universal_mcp/applications/miro/app.py +333 -815
  66. universal_mcp/applications/ms_teams/app.py +420 -1407
  67. universal_mcp/applications/neon/app.py +144 -250
  68. universal_mcp/applications/notion/app.py +38 -53
  69. universal_mcp/applications/onedrive/app.py +26 -48
  70. universal_mcp/applications/openai/app.py +43 -166
  71. universal_mcp/applications/outlook/README.md +22 -9
  72. universal_mcp/applications/outlook/app.py +403 -141
  73. universal_mcp/applications/perplexity/README.md +2 -1
  74. universal_mcp/applications/perplexity/app.py +161 -20
  75. universal_mcp/applications/pipedrive/app.py +1021 -3331
  76. universal_mcp/applications/posthog/app.py +272 -541
  77. universal_mcp/applications/reddit/app.py +65 -164
  78. universal_mcp/applications/resend/app.py +72 -139
  79. universal_mcp/applications/retell/app.py +23 -50
  80. universal_mcp/applications/rocketlane/app.py +252 -965
  81. universal_mcp/applications/scraper/app.py +114 -142
  82. universal_mcp/applications/semanticscholar/app.py +36 -78
  83. universal_mcp/applications/semrush/app.py +44 -78
  84. universal_mcp/applications/sendgrid/app.py +826 -1576
  85. universal_mcp/applications/sentry/app.py +444 -1079
  86. universal_mcp/applications/serpapi/app.py +44 -146
  87. universal_mcp/applications/sharepoint/app.py +27 -49
  88. universal_mcp/applications/shopify/app.py +1748 -4486
  89. universal_mcp/applications/shortcut/app.py +275 -536
  90. universal_mcp/applications/slack/app.py +43 -125
  91. universal_mcp/applications/spotify/app.py +206 -405
  92. universal_mcp/applications/supabase/app.py +174 -283
  93. universal_mcp/applications/tavily/app.py +2 -2
  94. universal_mcp/applications/trello/app.py +853 -2816
  95. universal_mcp/applications/twilio/app.py +27 -62
  96. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  97. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  98. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  99. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  100. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  101. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  102. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  103. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  104. universal_mcp/applications/whatsapp/app.py +35 -186
  105. universal_mcp/applications/whatsapp/audio.py +2 -6
  106. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  107. universal_mcp/applications/whatsapp_business/app.py +86 -299
  108. universal_mcp/applications/wrike/app.py +80 -153
  109. universal_mcp/applications/yahoo_finance/app.py +19 -65
  110. universal_mcp/applications/youtube/app.py +120 -306
  111. universal_mcp/applications/zenquotes/app.py +3 -3
  112. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/METADATA +4 -2
  113. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/RECORD +115 -119
  114. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/WHEEL +1 -1
  115. universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
  116. universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
  117. universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
  118. universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
  119. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.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
 
@@ -19,29 +20,30 @@ class LinkedinApp(APIApplication):
19
20
  Args:
20
21
  integration: The integration configuration containing credentials and other settings.
21
22
  It is expected that the integration provides the 'x-api-key'
22
- via headers in `integration.get_credentials()`, e.g.,
23
+ via headers in `integration.get_credentials_async()`, e.g.,
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
- self.account_id = None
28
+ self._account_id = None
29
+
30
+ async def _get_account_id(self) -> str | None:
31
+ if self._account_id:
32
+ return self._account_id
29
33
  if self.integration:
30
- credentials = self.integration.get_credentials()
31
- if credentials:
32
- self.account_id = credentials.get("account_id")
34
+ credentials = await self.integration.get_credentials_async()
35
+ self._account_id = credentials.get("account_id")
36
+ else:
37
+ logger.warning("Integration not found")
38
+ return self._account_id
33
39
 
34
40
  @property
35
41
  def base_url(self) -> str:
36
42
  if not self._base_url:
37
43
  unipile_dsn = os.getenv("UNIPILE_DSN")
38
44
  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
- )
45
+ logger.error("UnipileApp: UNIPILE_DSN environment variable is not set.")
46
+ raise ValueError("UnipileApp: UNIPILE_DSN environment variable is required.")
45
47
  self._base_url = f"https://{unipile_dsn}"
46
48
  return self._base_url
47
49
 
@@ -56,31 +58,25 @@ class LinkedinApp(APIApplication):
56
58
  Overrides the base class method to use X-Api-Key.
57
59
  """
58
60
  if not self.integration:
59
- logger.warning(
60
- "UnipileApp: No integration configured, returning empty headers."
61
- )
61
+ logger.warning("UnipileApp: No integration configured, returning empty headers.")
62
62
  return {}
63
-
64
63
  api_key = os.getenv("UNIPILE_API_KEY")
65
64
  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
-
65
+ logger.error("UnipileApp: API key not found in integration credentials for Unipile.")
66
+ return {"Content-Type": "application/json", "Cache-Control": "no-cache"}
74
67
  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
- }
68
+ return {"x-api-key": api_key, "Content-Type": "application/json", "Cache-Control": "no-cache"}
80
69
 
81
- def _get_search_parameter_id(self, param_type: str, keywords: str) -> str:
70
+ async def _aget_headers(self) -> dict[str, str]:
71
+ """
72
+ Get the headers for Unipile API requests asynchronously.
73
+ Overrides the base class method to use X-Api-Key.
82
74
  """
83
- Retrieves the ID for a given LinkedIn search parameter by its name.
75
+ return self._get_headers()
76
+
77
+ async def _aget_search_parameter_id(self, param_type: str, keywords: str) -> str:
78
+ """
79
+ Retrieves the ID for a given LinkedIn search parameter by its name asynchronously.
84
80
 
85
81
  Args:
86
82
  param_type: The type of parameter to search for (e.g., "LOCATION", "COMPANY").
@@ -94,29 +90,52 @@ class LinkedinApp(APIApplication):
94
90
  httpx.HTTPError: If the API request fails.
95
91
  """
96
92
  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)
93
+ params = {"account_id": await self._get_account_id(), "keywords": keywords, "type": param_type}
94
+ response = await self._aget(url, params=params)
104
95
  results = self._handle_response(response)
105
-
106
96
  items = results.get("items", [])
107
97
  if items:
108
- # Return the ID of the first result, assuming it's the most relevant
109
98
  return items[0]["id"]
110
-
111
99
  raise ValueError(f'Could not find a matching ID for {param_type}: "{keywords}"')
112
100
 
113
- def list_all_chats(
101
+ async def start_new_chat(self, provider_id: str, text: str) -> dict[str, Any]:
102
+ """
103
+ Starts a new chat conversation with a specified user by sending an initial message.
104
+ This function constructs a multipart/form-data request using the `files` parameter
105
+ to ensure correct formatting and headers, working around potential issues in the
106
+ underlying request method.
107
+
108
+ Args:
109
+ provider_id: The LinkedIn provider ID of the user to start the chat with.
110
+ This is available in the response of the `retrieve_user_profile` tool.
111
+ text: The initial message content. For LinkedIn Recruiter accounts, this can include
112
+ HTML tags like <strong>, <em>, <a>, <ul>, <ol>, and <li>.
113
+
114
+ Returns:
115
+ A dictionary containing the details of the newly created chat.
116
+
117
+ Raises:
118
+ httpx.HTTPError: If the API request fails.
119
+
120
+ Tags:
121
+ linkedin, chat, create, start, new, messaging, api, important
122
+ """
123
+ url = f"{self.base_url}/api/v1/chats"
124
+ form_payload = {"account_id": (None, await self._get_account_id()), "text": (None, text), "attendees_ids": (None, provider_id)}
125
+ api_key = os.getenv("UNIPILE_API_KEY")
126
+ if not api_key:
127
+ raise ValueError("UNIPILE_API_KEY environment variable is not set.")
128
+ headers = {"x-api-key": api_key}
129
+ response = requests.post(url, files=form_payload, headers=headers)
130
+ return self._handle_response(response)
131
+
132
+ async def list_all_chats(
114
133
  self,
115
134
  unread: bool | None = None,
116
135
  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
136
+ before: str | None = None,
137
+ after: str | None = None,
138
+ limit: int | None = None,
120
139
  account_type: str | None = None,
121
140
  ) -> dict[str, Any]:
122
141
  """
@@ -141,9 +160,7 @@ class LinkedinApp(APIApplication):
141
160
  """
142
161
  url = f"{self.base_url}/api/v1/chats"
143
162
  params: dict[str, Any] = {}
144
-
145
- params["account_id"] = self.account_id
146
-
163
+ params["account_id"] = await self._get_account_id()
147
164
  if unread is not None:
148
165
  params["unread"] = unread
149
166
  if cursor:
@@ -156,18 +173,16 @@ class LinkedinApp(APIApplication):
156
173
  params["limit"] = limit
157
174
  if account_type:
158
175
  params["account_type"] = account_type
159
-
160
-
161
- response = self._get(url, params=params)
162
- return response.json()
176
+ response = await self._aget(url, params=params)
177
+ return self._handle_response(response)
163
178
 
164
- def list_chat_messages(
179
+ async def list_chat_messages(
165
180
  self,
166
181
  chat_id: str,
167
182
  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
183
+ before: str | None = None,
184
+ after: str | None = None,
185
+ limit: int | None = None,
171
186
  sender_id: str | None = None,
172
187
  ) -> dict[str, Any]:
173
188
  """
@@ -202,15 +217,10 @@ class LinkedinApp(APIApplication):
202
217
  params["limit"] = limit
203
218
  if sender_id:
204
219
  params["sender_id"] = sender_id
220
+ response = await self._aget(url, params=params)
221
+ return self._handle_response(response)
205
222
 
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]:
223
+ async def send_chat_message(self, chat_id: str, text: str) -> dict[str, Any]:
214
224
  """
215
225
  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
226
 
@@ -230,11 +240,10 @@ class LinkedinApp(APIApplication):
230
240
  """
231
241
  url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
232
242
  payload: dict[str, Any] = {"text": text}
243
+ response = await self._apost(url, data=payload)
244
+ return self._handle_response(response)
233
245
 
234
- response = self._post(url, data=payload)
235
- return response.json()
236
-
237
- def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
246
+ async def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
238
247
  """
239
248
  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
249
 
@@ -252,18 +261,17 @@ class LinkedinApp(APIApplication):
252
261
  """
253
262
  url = f"{self.base_url}/api/v1/chats/{chat_id}"
254
263
  params: dict[str, Any] = {}
255
- if self.account_id:
256
- params["account_id"] = self.account_id
257
-
258
- response = self._get(url, params=params)
259
- return response.json()
264
+ if await self._get_account_id():
265
+ params["account_id"] = await self._get_account_id()
266
+ response = await self._aget(url, params=params)
267
+ return self._handle_response(response)
260
268
 
261
- def list_all_messages(
269
+ async def list_all_messages(
262
270
  self,
263
271
  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
272
+ before: str | None = None,
273
+ after: str | None = None,
274
+ limit: int | None = None,
267
275
  sender_id: str | None = None,
268
276
  ) -> dict[str, Any]:
269
277
  """
@@ -297,27 +305,22 @@ class LinkedinApp(APIApplication):
297
305
  params["limit"] = limit
298
306
  if sender_id:
299
307
  params["sender_id"] = sender_id
300
- if self.account_id:
301
- params["account_id"] = self.account_id
302
-
303
- response = self._get(url, params=params)
304
- return response.json()
308
+ if await self._get_account_id():
309
+ params["account_id"] = await self._get_account_id()
310
+ response = await self._aget(url, params=params)
311
+ return self._handle_response(response)
305
312
 
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,
313
+ async def list_profile_posts(
314
+ self, provider_id: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
312
315
  ) -> dict[str, Any]:
313
316
  """
314
317
  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.
315
318
 
316
319
  Args:
317
- identifier: The entity's provider internal ID (LinkedIn ID).
320
+ provider_id: The entity's provider internal ID (LinkedIn ID).
318
321
  cursor: Pagination cursor.
319
- limit: Number of items to return (1-100, as per Unipile example, though spec allows up to 250).
320
- is_company: Boolean indicating if the identifier is for a company.
322
+ limit: Number of items to return (1-100).
323
+ is_company: Boolean indicating if the provider_id is for a company.
321
324
 
322
325
  Returns:
323
326
  A dictionary containing a list of post objects and pagination details.
@@ -328,19 +331,45 @@ class LinkedinApp(APIApplication):
328
331
  Tags:
329
332
  linkedin, post, list, user_posts, company_posts, content, api, important
330
333
  """
331
- url = f"{self.base_url}/api/v1/users/{identifier}/posts"
332
- params: dict[str, Any] = {"account_id": self.account_id}
334
+ url = f"{self.base_url}/api/v1/users/{provider_id}/posts"
335
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
333
336
  if cursor:
334
337
  params["cursor"] = cursor
335
338
  if limit:
336
339
  params["limit"] = limit
337
340
  if is_company is not None:
338
341
  params["is_company"] = is_company
342
+ response = await self._aget(url, params=params)
343
+ return self._handle_response(response)
339
344
 
340
- response = self._get(url, params=params)
341
- return response.json()
345
+ async def list_profile_comments(self, provider_id: str, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
346
+ """
347
+ Retrieves a list of comments made by a specific user using their provider ID.
348
+
349
+ Args:
350
+ provider_id: The entity's provider internal ID (LinkedIn ID).
351
+ limit: Number of items to return (1-100).
352
+ cursor: Pagination cursor.
353
+
354
+ Returns:
355
+ A dictionary containing the list of comments.
356
+
357
+ Raises:
358
+ httpx.HTTPError: If the API request fails.
342
359
 
343
- def retrieve_own_profile(self) -> dict[str, Any]:
360
+ Tags:
361
+ linkedin, user, comments, list, content, api
362
+ """
363
+ url = f"{self.base_url}/api/v1/users/{provider_id}/comments"
364
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
365
+ if cursor:
366
+ params["cursor"] = cursor
367
+ if limit:
368
+ params["limit"] = limit
369
+ response = await self._aget(url, params=params)
370
+ return self._handle_response(response)
371
+
372
+ async def retrieve_own_profile(self) -> dict[str, Any]:
344
373
  """
345
374
  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
375
 
@@ -354,11 +383,11 @@ class LinkedinApp(APIApplication):
354
383
  linkedin, user, profile, me, retrieve, get, api
355
384
  """
356
385
  url = f"{self.base_url}/api/v1/users/me"
357
- params: dict[str, Any] = {"account_id": self.account_id}
358
- response = self._get(url, params=params)
359
- return response.json()
386
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
387
+ response = await self._aget(url, params=params)
388
+ return self._handle_response(response)
360
389
 
361
- def retrieve_post(self, post_id: str) -> dict[str, Any]:
390
+ async def retrieve_post(self, post_id: str) -> dict[str, Any]:
362
391
  """
363
392
  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
393
 
@@ -375,22 +404,18 @@ class LinkedinApp(APIApplication):
375
404
  linkedin, post, retrieve, get, content, api, important
376
405
  """
377
406
  url = f"{self.base_url}/api/v1/posts/{post_id}"
378
- params: dict[str, Any] = {"account_id": self.account_id}
379
- response = self._get(url, params=params)
380
- return response.json()
407
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
408
+ response = await self._aget(url, params=params)
409
+ return self._handle_response(response)
381
410
 
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,
411
+ async def list_post_comments(
412
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
388
413
  ) -> dict[str, Any]:
389
414
  """
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'.
415
+ 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
416
 
392
417
  Args:
393
- post_id: The social ID of the post.
418
+ post_id: The social ID of the post which you get from using `retrieve_post` or `list_profile_posts` tools.
394
419
  comment_id: If provided, retrieves replies to this comment ID instead of top-level comments.
395
420
  cursor: Pagination cursor.
396
421
  limit: Number of comments to return. (OpenAPI spec shows type string, passed as string if provided).
@@ -405,22 +430,18 @@ class LinkedinApp(APIApplication):
405
430
  linkedin, post, comment, list, content, api, important
406
431
  """
407
432
  url = f"{self.base_url}/api/v1/posts/{post_id}/comments"
408
- params: dict[str, Any] = {"account_id": self.account_id}
433
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
409
434
  if cursor:
410
435
  params["cursor"] = cursor
411
436
  if limit is not None:
412
437
  params["limit"] = str(limit)
413
438
  if comment_id:
414
439
  params["comment_id"] = comment_id
440
+ response = await self._aget(url, params=params)
441
+ return self._handle_response(response)
415
442
 
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,
443
+ async def create_post(
444
+ self, text: str, mentions: list[dict[str, Any]] | None = None, external_link: str | None = None
424
445
  ) -> dict[str, Any]:
425
446
  """
426
447
  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 +462,16 @@ class LinkedinApp(APIApplication):
441
462
  linkedin, post, create, share, content, api, important
442
463
  """
443
464
  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
-
465
+ params: dict[str, str] = {"account_id": await self._get_account_id(), "text": text}
450
466
  if mentions:
451
467
  params["mentions"] = mentions
452
468
  if external_link:
453
469
  params["external_link"] = external_link
470
+ response = await self._apost(url, data=params)
471
+ return self._handle_response(response)
454
472
 
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,
473
+ async def list_content_reactions(
474
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
464
475
  ) -> dict[str, Any]:
465
476
  """
466
477
  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.
@@ -469,7 +480,7 @@ class LinkedinApp(APIApplication):
469
480
  post_id: The social ID of the post.
470
481
  comment_id: If provided, retrieves reactions for this comment ID.
471
482
  cursor: Pagination cursor.
472
- limit: Number of reactions to return (1-100, spec max 250).
483
+ limit: Number of reactions to return (1-100).
473
484
 
474
485
  Returns:
475
486
  A dictionary containing a list of reaction objects and pagination details.
@@ -481,23 +492,18 @@ class LinkedinApp(APIApplication):
481
492
  linkedin, post, reaction, list, like, content, api
482
493
  """
483
494
  url = f"{self.base_url}/api/v1/posts/{post_id}/reactions"
484
- params: dict[str, Any] = {"account_id": self.account_id}
495
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
485
496
  if cursor:
486
497
  params["cursor"] = cursor
487
498
  if limit:
488
499
  params["limit"] = limit
489
500
  if comment_id:
490
501
  params["comment_id"] = comment_id
502
+ response = await self._aget(url, params=params)
503
+ return self._handle_response(response)
491
504
 
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,
505
+ async def create_post_comment(
506
+ self, post_social_id: str, text: str, comment_id: str | None = None, mentions_body: list[dict[str, Any]] | None = None
501
507
  ) -> dict[str, Any]:
502
508
  """
503
509
  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 +525,18 @@ class LinkedinApp(APIApplication):
519
525
  linkedin, post, comment, create, content, api, important
520
526
  """
521
527
  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
-
528
+ params: dict[str, Any] = {"account_id": await self._get_account_id(), "text": text}
527
529
  if comment_id:
528
530
  params["comment_id"] = comment_id
529
-
530
531
  if mentions_body:
531
532
  params = {"mentions": mentions_body}
533
+ response = await self._apost(url, data=params)
534
+ return self._handle_response(response)
532
535
 
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(
536
+ async def create_reaction(
544
537
  self,
545
538
  post_social_id: str,
546
- reaction_type: Literal[
547
- "like", "celebrate", "love", "insightful", "funny", "support"
548
- ],
539
+ reaction_type: Literal["like", "celebrate", "love", "insightful", "funny", "support"],
549
540
  comment_id: str | None = None,
550
541
  ) -> dict[str, Any]:
551
542
  """
@@ -566,32 +557,18 @@ class LinkedinApp(APIApplication):
566
557
  linkedin, post, reaction, create, like, content, api, important
567
558
  """
568
559
  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
-
560
+ params: dict[str, str] = {"account_id": await self._get_account_id(), "post_id": post_social_id, "reaction_type": reaction_type}
576
561
  if comment_id:
577
562
  params["comment_id"] = comment_id
563
+ response = await self._apost(url, data=params)
564
+ return self._handle_response(response)
578
565
 
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]:
566
+ async def retrieve_user_profile(self, public_identifier: str) -> dict[str, Any]:
590
567
  """
591
568
  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
569
 
593
570
  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".
571
+ 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
572
 
596
573
  Returns:
597
574
  A dictionary containing the user's profile details.
@@ -602,12 +579,12 @@ class LinkedinApp(APIApplication):
602
579
  Tags:
603
580
  linkedin, user, profile, retrieve, get, api, important
604
581
  """
605
- url = f"{self.base_url}/api/v1/users/{identifier}"
606
- params: dict[str, Any] = {"account_id": self.account_id}
607
- response = self._get(url, params=params)
582
+ url = f"{self.base_url}/api/v1/users/{public_identifier}"
583
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
584
+ response = await self._aget(url, params=params)
608
585
  return self._handle_response(response)
609
586
 
610
- def search_people(
587
+ async def search_people(
611
588
  self,
612
589
  cursor: str | None = None,
613
590
  limit: int | None = None,
@@ -618,7 +595,7 @@ class LinkedinApp(APIApplication):
618
595
  ) -> dict[str, Any]:
619
596
  """
620
597
  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
-
598
+
622
599
  Args:
623
600
  cursor: Pagination cursor for the next page of entries.
624
601
  limit: Number of items to return (up to 50 for Classic search).
@@ -626,42 +603,35 @@ class LinkedinApp(APIApplication):
626
603
  location: The geographical location to filter people by (e.g., "United States").
627
604
  industry: The industry to filter people by.(eg., "Information Technology and Services").
628
605
  company: The company to filter people by.(e.g., "Google").
629
-
606
+
630
607
  Returns:
631
608
  A dictionary containing search results and pagination details.
632
-
609
+
633
610
  Raises:
634
611
  httpx.HTTPError: If the API request fails.
635
612
  """
636
613
  url = f"{self.base_url}/api/v1/linkedin/search"
637
-
638
- params: dict[str, Any] = {"account_id": self.account_id}
614
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
639
615
  if cursor:
640
616
  params["cursor"] = cursor
641
617
  if limit is not None:
642
618
  params["limit"] = limit
643
-
644
619
  payload: dict[str, Any] = {"api": "classic", "category": "people"}
645
-
646
620
  if keywords:
647
621
  payload["keywords"] = keywords
648
-
649
622
  if location:
650
- location_id = self._get_search_parameter_id("LOCATION", location)
623
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
651
624
  payload["location"] = [location_id]
652
-
653
625
  if industry:
654
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
626
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
655
627
  payload["industry"] = [industry_id]
656
-
657
628
  if company:
658
- company_id = self._get_search_parameter_id("COMPANY", company)
629
+ company_id = await self._aget_search_parameter_id("COMPANY", company)
659
630
  payload["company"] = [company_id]
660
-
661
- response = self._post(url, params=params, data=payload)
631
+ response = await self._apost(url, params=params, data=payload)
662
632
  return self._handle_response(response)
663
633
 
664
- def search_companies(
634
+ async def search_companies(
665
635
  self,
666
636
  cursor: str | None = None,
667
637
  limit: int | None = None,
@@ -671,45 +641,39 @@ class LinkedinApp(APIApplication):
671
641
  ) -> dict[str, Any]:
672
642
  """
673
643
  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
-
644
+
675
645
  Args:
676
646
  cursor: Pagination cursor for the next page of entries.
677
647
  limit: Number of items to return (up to 50 for Classic search).
678
648
  keywords: Keywords to search for.
679
649
  location: The geographical location to filter companies by (e.g., "United States").
680
650
  industry: The industry to filter companies by.(e.g., "Information Technology and Services").
681
-
651
+
682
652
  Returns:
683
653
  A dictionary containing search results and pagination details.
684
-
654
+
685
655
  Raises:
686
656
  httpx.HTTPError: If the API request fails.
687
657
  """
688
658
  url = f"{self.base_url}/api/v1/linkedin/search"
689
-
690
- params: dict[str, Any] = {"account_id": self.account_id}
659
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
691
660
  if cursor:
692
661
  params["cursor"] = cursor
693
662
  if limit is not None:
694
663
  params["limit"] = limit
695
-
696
664
  payload: dict[str, Any] = {"api": "classic", "category": "companies"}
697
-
698
665
  if keywords:
699
666
  payload["keywords"] = keywords
700
-
701
667
  if location:
702
- location_id = self._get_search_parameter_id("LOCATION", location)
668
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
703
669
  payload["location"] = [location_id]
704
-
705
670
  if industry:
706
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
671
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
707
672
  payload["industry"] = [industry_id]
708
-
709
- response = self._post(url, params=params, data=payload)
673
+ response = await self._apost(url, params=params, data=payload)
710
674
  return self._handle_response(response)
711
675
 
712
- def search_posts(
676
+ async def search_posts(
713
677
  self,
714
678
  cursor: str | None = None,
715
679
  limit: int | None = None,
@@ -719,111 +683,609 @@ class LinkedinApp(APIApplication):
719
683
  ) -> dict[str, Any]:
720
684
  """
721
685
  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
-
686
+
723
687
  Args:
724
688
  cursor: Pagination cursor for the next page of entries.
725
689
  limit: Number of items to return (up to 50 for Classic search).
726
690
  keywords: Keywords to search for.
727
691
  date_posted: Filter by when the post was posted.
728
692
  sort_by: How to sort the results.
729
-
693
+
730
694
  Returns:
731
695
  A dictionary containing search results and pagination details.
732
-
696
+
733
697
  Raises:
734
698
  httpx.HTTPError: If the API request fails.
735
699
  """
736
700
  url = f"{self.base_url}/api/v1/linkedin/search"
737
-
738
- params: dict[str, Any] = {"account_id": self.account_id}
701
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
739
702
  if cursor:
740
703
  params["cursor"] = cursor
741
704
  if limit is not None:
742
705
  params["limit"] = limit
743
-
744
706
  payload: dict[str, Any] = {"api": "classic", "category": "posts"}
745
-
746
707
  if keywords:
747
708
  payload["keywords"] = keywords
748
709
  if date_posted:
749
710
  payload["date_posted"] = date_posted
750
711
  if sort_by:
751
712
  payload["sort_by"] = sort_by
752
-
753
- response = self._post(url, params=params, data=payload)
713
+ response = await self._apost(url, params=params, data=payload)
754
714
  return self._handle_response(response)
755
715
 
756
- def search_jobs(
716
+ async def search_jobs(
757
717
  self,
758
718
  cursor: str | None = None,
759
719
  limit: int | None = None,
760
720
  keywords: str | None = None,
761
721
  region: str | None = None,
762
722
  sort_by: Literal["relevance", "date"] = "relevance",
763
- minimum_salary_value: int = 40,
723
+ minimum_salary_value: Literal[40, 60, 80, 100, 120, 140, 160, 180, 200] = 40,
764
724
  industry: str | None = None,
765
725
  ) -> dict[str, Any]:
766
726
  """
767
727
  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
-
728
+
769
729
  Args:
770
730
  cursor: Pagination cursor for the next page of entries.
771
731
  limit: Number of items to return (up to 50 for Classic search).
772
732
  keywords: Keywords to search for.
773
733
  region: The geographical region to filter jobs by (e.g., "United States").
774
734
  sort_by: How to sort the results.(e.g., "relevance" or "date".)
775
- minimum_salary_value: The minimum salary to filter for.
735
+ minimum_salary_value: The minimum salary to filter for. Allowed values are 40, 60, 80, 100, 120, 140, 160, 180, 200.
776
736
  industry: The industry to filter jobs by.(e.g., "Software Development").
777
-
737
+
778
738
  Returns:
779
739
  A dictionary containing search results and pagination details.
780
-
740
+
781
741
  Raises:
782
742
  httpx.HTTPError: If the API request fails.
783
743
  ValueError: If the specified location is not found.
784
744
  """
785
745
  url = f"{self.base_url}/api/v1/linkedin/search"
786
-
787
- params: dict[str, Any] = {"account_id": self.account_id}
746
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
788
747
  if cursor:
789
748
  params["cursor"] = cursor
790
749
  if limit is not None:
791
750
  params["limit"] = limit
792
-
793
751
  payload: dict[str, Any] = {
794
752
  "api": "classic",
795
753
  "category": "jobs",
796
- "minimum_salary": {
797
- "currency": "USD",
798
- "value": minimum_salary_value,
799
- },
754
+ "minimum_salary": {"currency": "USD", "value": minimum_salary_value},
800
755
  }
801
-
802
756
  if keywords:
803
757
  payload["keywords"] = keywords
804
758
  if sort_by:
805
759
  payload["sort_by"] = sort_by
806
-
807
- # If location is provided, get its ID and add it to the payload
808
760
  if region:
809
- location_id = self._get_search_parameter_id("LOCATION", region)
761
+ location_id = await self._aget_search_parameter_id("LOCATION", region)
810
762
  payload["region"] = location_id
811
-
812
763
  if industry:
813
- industry_id = self._get_search_parameter_id("INDUSTRY", industry)
764
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
814
765
  payload["industry"] = [industry_id]
766
+ response = await self._apost(url, params=params, data=payload)
767
+ return self._handle_response(response)
768
+
769
+ async def send_invitation(self, provider_id: str, user_email: str | None = None, message: str | None = None) -> dict[str, Any]:
770
+ """
771
+ Sends a connection invitation to a LinkedIn user specified by their provider ID. An optional message and the user's email can be included.
772
+
773
+ Args:
774
+ provider_id: The LinkedIn provider ID of the user to invite. This is available in response of `retrieve_user_profile` tool.
775
+ user_email: Optional. The email address of the user, which may be required by LinkedIn.
776
+ message: Optional. A personalized message to include with the invitation (max 300 characters).
777
+
778
+ Returns:
779
+ A dictionary confirming the invitation was sent.
780
+
781
+ Raises:
782
+ httpx.HTTPError: If the API request fails.
783
+ ValueError: If the message exceeds 300 characters.
784
+
785
+ Tags:
786
+ linkedin, user, invite, connect, contact, api, important
787
+ """
788
+ url = f"{self.base_url}/api/v1/users/invite"
789
+ payload: dict[str, Any] = {"account_id": await self._get_account_id(), "provider_id": provider_id}
790
+ if user_email:
791
+ payload["user_email"] = user_email
792
+ if message:
793
+ if len(message) > 300:
794
+ raise ValueError("Message cannot exceed 300 characters.")
795
+ payload["message"] = message
796
+ response = await self._apost(url, data=payload)
797
+ return self._handle_response(response)
798
+
799
+ async def list_sent_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
800
+ """
801
+ 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.
802
+
803
+ Args:
804
+ cursor: A pagination cursor for retrieving the next page of entries.
805
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
806
+
807
+ Returns:
808
+ A dictionary containing a list of sent invitation objects and pagination details.
809
+
810
+ Raises:
811
+ httpx.HTTPError: If the API request fails.
812
+
813
+ Tags:
814
+ linkedin, user, invite, sent, list, contacts, api
815
+ """
816
+ url = f"{self.base_url}/api/v1/users/invite/sent"
817
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
818
+ if cursor:
819
+ params["cursor"] = cursor
820
+ if limit is not None:
821
+ params["limit"] = limit
822
+ response = await self._aget(url, params=params)
823
+ return self._handle_response(response)
824
+
825
+ async def list_received_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
826
+ """
827
+ Retrieves a paginated list of all received connection invitations. This function allows for reviewing and processing incoming connection requests to the specified account.
828
+
829
+ Args:
830
+ cursor: A pagination cursor for retrieving the next page of entries.
831
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
832
+
833
+ Returns:
834
+ A dictionary containing a list of received invitation objects and pagination details.
835
+
836
+ Raises:
837
+ httpx.HTTPError: If the API request fails.
838
+
839
+ Tags:
840
+ linkedin, user, invite, received, list, contacts, api
841
+ """
842
+ url = f"{self.base_url}/api/v1/users/invite/received"
843
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
844
+ if cursor:
845
+ params["cursor"] = cursor
846
+ if limit is not None:
847
+ params["limit"] = limit
848
+ response = await self._aget(url, params=params)
849
+ return self._handle_response(response)
850
+
851
+ async def handle_received_invitation(
852
+ self, invitation_id: str, action: Literal["accept", "decline"], shared_secret: str
853
+ ) -> dict[str, Any]:
854
+ """
855
+ 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`.
856
+
857
+ Args:
858
+ invitation_id: The ID of the invitation to handle.Get this ID from the 'list_received_invitations' tool.
859
+ action: The action to perform, either "accept" or "decline".
860
+ shared_secret: The token provided by LinkedIn, retrieved from the 'list_received_invitations' tool, which is mandatory for this action.
861
+
862
+ Returns:
863
+ A dictionary confirming the action was processed.
864
+
865
+ Raises:
866
+ httpx.HTTPError: If the API request fails.
867
+
868
+ Tags:
869
+ linkedin, user, invite, received, handle, accept, decline, api
870
+ """
871
+ url = f"{self.base_url}/api/v1/users/invite/received/{invitation_id}"
872
+ payload: dict[str, Any] = {"provider": "LINKEDIN", "action": action, "shared_secret": shared_secret, "account_id": await self._get_account_id()}
873
+ response = await self._apost(url, data=payload)
874
+ return self._handle_response(response)
875
+
876
+ async def cancel_sent_invitation(self, invitation_id: str) -> dict[str, Any]:
877
+ """
878
+ Cancels a sent LinkedIn connection invitation that is currently pending. This function performs a DELETE request to remove the invitation, withdrawing the connection request.
879
+
880
+ Args:
881
+ invitation_id: The unique ID of the invitation to cancel. This ID can be obtained from the 'list_sent_invitations' tool.
882
+
883
+ Returns:
884
+ A dictionary confirming the invitation was cancelled.
885
+
886
+ Raises:
887
+ httpx.HTTPError: If the API request fails.
888
+
889
+ Tags:
890
+ linkedin, user, invite, sent, cancel, delete, api
891
+ """
892
+ url = f"{self.base_url}/api/v1/users/invite/sent/{invitation_id}"
893
+ params = {"account_id": await self._get_account_id()}
894
+ response = await self._adelete(url, params=params)
895
+ return self._handle_response(response)
896
+
897
+ async def list_followers(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
898
+ """
899
+ 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.
900
+
901
+ Args:
902
+ cursor: A pagination cursor for retrieving the next page of entries.
903
+ limit: The number of items to return, ranging from 1 to 1000.
904
+
905
+ Returns:
906
+ A dictionary containing a list of follower objects and pagination details.
907
+
908
+ Raises:
909
+ httpx.HTTPError: If the API request fails.
910
+
911
+ Tags:
912
+ linkedin, user, followers, list, contacts, api
913
+ """
914
+ url = f"{self.base_url}/api/v1/users/followers"
915
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
916
+ if cursor:
917
+ params["cursor"] = cursor
918
+ if limit is not None:
919
+ params["limit"] = limit
920
+ response = await self._aget(url, params=params)
921
+ return self._handle_response(response)
922
+
923
+ async def list_following(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
924
+ """
925
+ 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.
926
+
927
+ Args:
928
+ cursor: A pagination cursor for retrieving the next page of entries.
929
+ limit: The number of items to return, ranging from 1 to 1000.
930
+
931
+ Returns:
932
+ A dictionary containing a list of followed account objects and pagination details.
933
+
934
+ Raises:
935
+ httpx.HTTPError: If the API request fails.
936
+
937
+ Tags:
938
+ linkedin, user, following, list, contacts, api
939
+ """
940
+ url = f"{self.base_url}/api/v1/users/following"
941
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
942
+ if cursor:
943
+ params["cursor"] = cursor
944
+ if limit is not None:
945
+ params["limit"] = limit
946
+ response = self._get(url, params=params)
947
+ return self._handle_response(response)
948
+
949
+ async def list_job_postings(
950
+ self,
951
+ category: Literal["active", "draft", "closed"] = "active",
952
+ limit: int | None = None,
953
+ cursor: str | None = None,
954
+ ) -> dict[str, Any]:
955
+ """
956
+ Retrieve the job offers you have posted on LinkedIn whether they are open, closed, or still drafts.
957
+
958
+ Args:
959
+ category: The state of the requested job postings. Default is active.
960
+ limit: A limit for the number of items returned in the response. The value can be set between 1 and 250.
961
+ cursor: A cursor for pagination purposes.
962
+
963
+ Returns:
964
+ A dictionary containing a list of job postings and pagination details.
965
+
966
+ Raises:
967
+ httpx.HTTPError: If the API request fails.
968
+
969
+ Tags:
970
+ linkedin, jobs, list, postings, api
971
+ """
972
+ url = f"{self.base_url}/api/v1/linkedin/jobs"
973
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
974
+ if category:
975
+ params["category"] = category
976
+ if limit:
977
+ params["limit"] = limit
978
+ if cursor:
979
+ params["cursor"] = cursor
980
+ response = await self._aget(url, params=params)
981
+ return self._handle_response(response)
982
+
983
+ async def create_job_posting(
984
+ self,
985
+ job_title: dict[str, str],
986
+ company: dict[str, str],
987
+ workplace: Literal["ON_SITE", "HYBRID", "REMOTE"],
988
+ location: str,
989
+ description: str,
990
+ employment_status: Literal[
991
+ "FULL_TIME", "PART_TIME", "CONTRACT", "TEMPORARY", "OTHER", "VOLUNTEER", "INTERNSHIP"
992
+ ] = "FULL_TIME",
993
+ auto_rejection_template: str | None = None,
994
+ screening_questions: list[dict[str, Any]] | None = None,
995
+ recruiter: dict[str, Any] | None = None,
996
+ ) -> dict[str, Any]:
997
+ """
998
+ Create a new job offer draft.
999
+
1000
+ Args:
1001
+ job_title: Required. A dictionary containing either {"id": "..."} or {"text": "..."}.
1002
+ company: Required. A dictionary containing either {"id": "..."} or {"text": "..."}.
1003
+ workplace: Required. One of "ON_SITE", "HYBRID", "REMOTE".
1004
+ location: Required. The ID of the location parameter. Use type LOCATION on the List search parameters route.
1005
+ description: Required. HTML description of the job.
1006
+ employment_status: Optional. One of "FULL_TIME", "PART_TIME", "CONTRACT", "TEMPORARY", "OTHER", "VOLUNTEER", "INTERNSHIP".
1007
+ auto_rejection_template: Optional. A rejection message template.
1008
+ screening_questions: Optional. A list of screening questions.
1009
+ recruiter: Optional. Recruiter object containing:
1010
+ - project: Required. {"id": "..."} or {"name": "..."}.
1011
+ - functions: Required. List of strings (job function IDs).
1012
+ - industries: Required. List of strings (industry IDs).
1013
+ - seniority: Required. Enum (e.g. "INTERNSHIP", "ENTRY_LEVEL", "ASSOCIATE", "MID_SENIOR_LEVEL", "DIRECTOR", "EXECUTIVE", "NOT_APPLICABLE").
1014
+ - apply_method: Required. {"apply_within_linkedin": ...} or {"apply_through_external_website": ...}.
1015
+ - include_poster_info: Optional boolean.
1016
+ - tracking_pixel_url: Optional string.
1017
+ - company_job_id: Optional string.
1018
+ - auto_archive_applicants: Optional object.
1019
+ - send_rejection_notification: Optional boolean.
1020
+
1021
+ Returns:
1022
+ A dictionary containing the response from the API.
1023
+
1024
+ Raises:
1025
+ httpx.HTTPError: If the API request fails.
1026
+ """
1027
+ url = f"{self.base_url}/api/v1/linkedin/jobs"
1028
+ payload: dict[str, Any] = {
1029
+ "account_id": await self._get_account_id(),
1030
+ "job_title": job_title,
1031
+ "company": company,
1032
+ "workplace": workplace,
1033
+ "location": location,
1034
+ "description": description,
1035
+ "employment_status": employment_status,
1036
+ }
1037
+ if auto_rejection_template:
1038
+ payload["auto_rejection_template"] = auto_rejection_template
1039
+ if screening_questions:
1040
+ payload["screening_questions"] = screening_questions
1041
+ if recruiter:
1042
+ payload["recruiter"] = recruiter
1043
+
1044
+ response = await self._apost(url, data=payload)
1045
+ return self._handle_response(response)
1046
+
1047
+ async def close_job_posting(
1048
+ self,
1049
+ job_id: str,
1050
+ service: Literal["CLASSIC", "RECRUITER"] | None = None,
1051
+ ) -> dict[str, Any]:
1052
+ """
1053
+ Close a job offer you have posted.
1054
+
1055
+ Args:
1056
+ job_id: Required. The ID of the job offer.
1057
+ service: Optional. The Linkedin service the job posting depends on.
1058
+
1059
+ Returns:
1060
+ A dictionary containing the response from the API.
1061
+
1062
+ Raises:
1063
+ httpx.HTTPError: If the API request fails.
1064
+ """
1065
+ url = f"{self.base_url}/api/v1/linkedin/jobs/{job_id}/close"
1066
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
1067
+ if service:
1068
+ params["service"] = service
1069
+ response = await self._apost(url, params=params, data={})
1070
+ return self._handle_response(response)
1071
+
1072
+ async def retrieve_job_posting(
1073
+ self,
1074
+ job_id: str,
1075
+ service: Literal["CLASSIC", "RECRUITER"] = "CLASSIC",
1076
+ ) -> dict[str, Any]:
1077
+ """
1078
+ Retrieve a job offer.
1079
+
1080
+ Args:
1081
+ job_id: Required. The ID of the job offer.
1082
+ service: Required. The Linkedin service the job posting depends on. Default is CLASSIC.
1083
+
1084
+ Returns:
1085
+ A dictionary containing the job offer details.
1086
+
1087
+ Raises:
1088
+ httpx.HTTPError: If the API request fails.
1089
+ """
1090
+ url = f"{self.base_url}/api/v1/linkedin/jobs/{job_id}"
1091
+ params: dict[str, Any] = {"account_id": await self._get_account_id(), "service": service}
1092
+ response = await self._aget(url, params=params)
1093
+ return self._handle_response(response)
1094
+
1095
+ async def publish_job_posting(
1096
+ self,
1097
+ draft_id: str,
1098
+ mode: Literal["FREE"] = "FREE",
1099
+ service: Literal["CLASSIC", "RECRUITER"] = "CLASSIC",
1100
+ hiring_photo_frame: bool | None = None,
1101
+ bypass_email_verification: bool | None = None,
1102
+ ) -> dict[str, Any]:
1103
+ """
1104
+ Publish the job posting draft you have been working on.
1105
+
1106
+ Args:
1107
+ draft_id: Required. The id of the draft to publish.
1108
+ mode: Required. "FREE".
1109
+ service: Optional. The Linkedin service the job posting depends on. Default is CLASSIC.
1110
+ hiring_photo_frame: Optional. Whether or not to add the hiring photo frame to you profile picture.
1111
+ bypass_email_verification: Optional. Whether or not to verify if you're allowed to post a job on behalf on the current company.
1112
+
1113
+ Returns:
1114
+ A dictionary containing the response from the API.
1115
+
1116
+ Raises:
1117
+ httpx.HTTPError: If the API request fails.
1118
+ """
1119
+ url = f"{self.base_url}/api/v1/linkedin/jobs/{draft_id}/publish"
1120
+ payload: dict[str, Any] = {
1121
+ "account_id": await self._get_account_id(),
1122
+ "mode": mode,
1123
+ "service": service,
1124
+ }
1125
+ if hiring_photo_frame is not None:
1126
+ payload["hiring_photo_frame"] = hiring_photo_frame
1127
+ if bypass_email_verification is not None:
1128
+ payload["bypass_email_verification"] = bypass_email_verification
1129
+
1130
+ response = await self._apost(url, data=payload)
1131
+ return self._handle_response(response)
1132
+
1133
+ async def solve_job_publishing_checkpoint(
1134
+ self,
1135
+ draft_id: str,
1136
+ input: str,
1137
+ ) -> dict[str, Any]:
1138
+ """
1139
+ Solve a checkpoint to verify your member privileges.
1140
+
1141
+ Args:
1142
+ draft_id: Required. The id of the draft to solve the checkpoint from.
1143
+ input: Required. The code or input to solve the checkpoint.
1144
+
1145
+ Returns:
1146
+ A dictionary containing the response from the API.
1147
+
1148
+ Raises:
1149
+ httpx.HTTPError: If the API request fails.
1150
+ """
1151
+ url = f"{self.base_url}/api/v1/linkedin/jobs/{draft_id}/checkpoint"
1152
+ payload: dict[str, Any] = {
1153
+ "account_id": await self._get_account_id(),
1154
+ "input": input,
1155
+ }
1156
+ response = await self._apost(url, data=payload)
1157
+ return self._handle_response(response)
1158
+
1159
+ async def list_job_applicants(
1160
+ self,
1161
+ job_id: str,
1162
+ limit: int = 100,
1163
+ cursor: str | None = None,
1164
+ service: Literal["CLASSIC", "RECRUITER"] = "CLASSIC",
1165
+ sort_by: Literal[
1166
+ "relevance", "alphabetical", "newest_first", "screening_requirements"
1167
+ ]
1168
+ | None = None,
1169
+ keywords: str | None = None,
1170
+ ratings: str | None = None,
1171
+ min_years_in_company: float | None = None,
1172
+ max_years_in_company: float | None = None,
1173
+ min_years_in_position: float | None = None,
1174
+ max_years_in_position: float | None = None,
1175
+ min_years_of_experience: float | None = None,
1176
+ max_years_of_experience: float | None = None,
1177
+ ) -> dict[str, Any]:
1178
+ """
1179
+ Retrieve all the users that have applied to a given offer.
1180
+
1181
+ Args:
1182
+ job_id: Required. The ID of the job offer.
1183
+ limit: Optional. The number of results to return. Default 100.
1184
+ cursor: Optional. The cursor to retrieve the next page.
1185
+ service: Optional. The Linkedin service the job posting depends on. Default is CLASSIC.
1186
+ sort_by: Optional. The sorting rule for applicants. Recruiter only.
1187
+ keywords: Optional. Filter results with keywords.
1188
+ ratings: Optional. One or more ratings (UNRATED, GOOD_FIT, MAYBE, NOT_A_FIT) separated by commas.
1189
+ min_years_in_company: Optional. Linkedin Recruiter native filter.
1190
+ max_years_in_company: Optional. Linkedin Recruiter native filter.
1191
+ min_years_in_position: Optional. Linkedin Recruiter native filter.
1192
+ max_years_in_position: Optional. Linkedin Recruiter native filter.
1193
+ min_years_of_experience: Optional. Linkedin Recruiter native filter.
1194
+ max_years_of_experience: Optional. Linkedin Recruiter native filter.
1195
+
1196
+ Returns:
1197
+ A dictionary containing the list of applicants.
815
1198
 
816
- response = self._post(url, params=params, data=payload)
1199
+ Raises:
1200
+ httpx.HTTPError: If the API request fails.
1201
+ """
1202
+ url = f"{self.base_url}/api/v1/linkedin/jobs/{job_id}/applicants"
1203
+ params: dict[str, Any] = {
1204
+ "account_id": await self._get_account_id(),
1205
+ "limit": limit,
1206
+ "service": service,
1207
+ }
1208
+ if cursor:
1209
+ params["cursor"] = cursor
1210
+ if sort_by:
1211
+ params["sort_by"] = sort_by
1212
+ if keywords:
1213
+ params["keywords"] = keywords
1214
+ if ratings:
1215
+ params["ratings"] = ratings
1216
+ if min_years_in_company is not None:
1217
+ params["min_years_in_company"] = min_years_in_company
1218
+ if max_years_in_company is not None:
1219
+ params["max_years_in_company"] = max_years_in_company
1220
+ if min_years_in_position is not None:
1221
+ params["min_years_in_position"] = min_years_in_position
1222
+ if max_years_in_position is not None:
1223
+ params["max_years_in_position"] = max_years_in_position
1224
+ if min_years_of_experience is not None:
1225
+ params["min_years_of_experience"] = min_years_of_experience
1226
+ if max_years_of_experience is not None:
1227
+ params["max_years_of_experience"] = max_years_of_experience
1228
+
1229
+ response = await self._aget(url, params=params)
1230
+ return self._handle_response(response)
1231
+
1232
+ async def retrieve_job_applicant(
1233
+ self,
1234
+ applicant_id: str,
1235
+ ) -> dict[str, Any]:
1236
+ """
1237
+ Retrieve the details of a user that has applied to a given offer. Applies to Classic job posting only.
1238
+
1239
+ Args:
1240
+ applicant_id: Required. The ID of the applicant.
1241
+
1242
+ Returns:
1243
+ A dictionary containing the applicant details.
1244
+
1245
+ Raises:
1246
+ httpx.HTTPError: If the API request fails.
1247
+ """
1248
+ url = f"{self.base_url}/api/v1/linkedin/jobs/applicants/{applicant_id}"
1249
+ params: dict[str, Any] = {"account_id": await self._get_account_id()}
1250
+ response = await self._aget(url, params=params)
1251
+ return self._handle_response(response)
1252
+
1253
+ async def download_job_applicant_resume(
1254
+ self,
1255
+ applicant_id: str,
1256
+ service: Literal["CLASSIC", "RECRUITER"] = "CLASSIC",
1257
+ ) -> dict[str, Any]:
1258
+ """
1259
+ Download the resume of a job applicant.
1260
+
1261
+ Args:
1262
+ applicant_id: Required. The ID of the job applicant.
1263
+ service: Optional. The Linkedin service the applicant depends on. Default is classic.
1264
+
1265
+ Returns:
1266
+ A dictionary containing the resume details (likely a download URL or binary content, depending on API response).
1267
+
1268
+ Raises:
1269
+ httpx.HTTPError: If the API request fails.
1270
+ """
1271
+ url = f"{self.base_url}/api/v1/linkedin/jobs/applicants/{applicant_id}/resume"
1272
+ params: dict[str, Any] = {
1273
+ "account_id": await self._get_account_id(),
1274
+ "service": service,
1275
+ }
1276
+ response = await self._aget(url, params=params)
817
1277
  return self._handle_response(response)
818
1278
 
819
1279
  def list_tools(self) -> list[Callable]:
820
1280
  return [
1281
+ self.start_new_chat,
821
1282
  self.list_all_chats,
822
1283
  self.list_chat_messages,
823
1284
  self.send_chat_message,
824
1285
  self.retrieve_chat,
825
1286
  self.list_all_messages,
826
1287
  self.list_profile_posts,
1288
+ self.list_profile_comments,
827
1289
  self.retrieve_own_profile,
828
1290
  self.retrieve_user_profile,
829
1291
  self.retrieve_post,
@@ -836,4 +1298,20 @@ class LinkedinApp(APIApplication):
836
1298
  self.search_jobs,
837
1299
  self.search_people,
838
1300
  self.search_posts,
1301
+ self.send_invitation,
1302
+ self.list_sent_invitations,
1303
+ self.cancel_sent_invitation,
1304
+ self.list_received_invitations,
1305
+ self.handle_received_invitation,
1306
+ self.list_followers,
1307
+ # self.list_following this endpoint is not yet implemented by unipile
1308
+ self.list_job_postings,
1309
+ self.create_job_posting,
1310
+ self.close_job_posting,
1311
+ self.retrieve_job_posting,
1312
+ self.publish_job_posting,
1313
+ self.solve_job_publishing_checkpoint,
1314
+ self.list_job_applicants,
1315
+ self.retrieve_job_applicant,
1316
+ self.download_job_applicant_resume,
839
1317
  ]