universal-mcp-applications 0.1.30rc1__py3-none-any.whl → 0.1.36rc1__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 (106) hide show
  1. universal_mcp/applications/ahrefs/app.py +52 -198
  2. universal_mcp/applications/airtable/app.py +23 -122
  3. universal_mcp/applications/apollo/app.py +111 -464
  4. universal_mcp/applications/asana/app.py +417 -1567
  5. universal_mcp/applications/aws_s3/app.py +33 -100
  6. universal_mcp/applications/bill/app.py +546 -1957
  7. universal_mcp/applications/box/app.py +1068 -3981
  8. universal_mcp/applications/braze/app.py +364 -1430
  9. universal_mcp/applications/browser_use/app.py +2 -8
  10. universal_mcp/applications/cal_com_v2/app.py +207 -625
  11. universal_mcp/applications/calendly/app.py +61 -200
  12. universal_mcp/applications/canva/app.py +45 -110
  13. universal_mcp/applications/clickup/app.py +207 -674
  14. universal_mcp/applications/coda/app.py +146 -426
  15. universal_mcp/applications/confluence/app.py +310 -1098
  16. universal_mcp/applications/contentful/app.py +36 -151
  17. universal_mcp/applications/crustdata/app.py +28 -107
  18. universal_mcp/applications/dialpad/app.py +283 -756
  19. universal_mcp/applications/digitalocean/app.py +1766 -5777
  20. universal_mcp/applications/domain_checker/app.py +3 -54
  21. universal_mcp/applications/e2b/app.py +14 -64
  22. universal_mcp/applications/elevenlabs/app.py +9 -47
  23. universal_mcp/applications/exa/app.py +6 -17
  24. universal_mcp/applications/falai/app.py +23 -100
  25. universal_mcp/applications/figma/app.py +53 -137
  26. universal_mcp/applications/file_system/app.py +2 -13
  27. universal_mcp/applications/firecrawl/app.py +51 -152
  28. universal_mcp/applications/fireflies/app.py +59 -281
  29. universal_mcp/applications/fpl/app.py +91 -528
  30. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  31. universal_mcp/applications/fpl/utils/helper.py +25 -89
  32. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  33. universal_mcp/applications/ghost_content/app.py +52 -161
  34. universal_mcp/applications/github/app.py +19 -56
  35. universal_mcp/applications/gong/app.py +88 -248
  36. universal_mcp/applications/google_calendar/app.py +16 -68
  37. universal_mcp/applications/google_docs/app.py +88 -188
  38. universal_mcp/applications/google_drive/app.py +140 -462
  39. universal_mcp/applications/google_gemini/app.py +12 -64
  40. universal_mcp/applications/google_mail/app.py +28 -157
  41. universal_mcp/applications/google_searchconsole/app.py +15 -48
  42. universal_mcp/applications/google_sheet/app.py +101 -578
  43. universal_mcp/applications/google_sheet/helper.py +10 -37
  44. universal_mcp/applications/hashnode/app.py +57 -269
  45. universal_mcp/applications/heygen/app.py +44 -122
  46. universal_mcp/applications/http_tools/app.py +10 -32
  47. universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
  48. universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
  49. universal_mcp/applications/hubspot/app.py +23 -87
  50. universal_mcp/applications/jira/app.py +2071 -7986
  51. universal_mcp/applications/klaviyo/app.py +494 -1376
  52. universal_mcp/applications/linkedin/README.md +23 -4
  53. universal_mcp/applications/linkedin/app.py +392 -212
  54. universal_mcp/applications/mailchimp/app.py +450 -1605
  55. universal_mcp/applications/markitdown/app.py +8 -20
  56. universal_mcp/applications/miro/app.py +217 -699
  57. universal_mcp/applications/ms_teams/app.py +64 -186
  58. universal_mcp/applications/neon/app.py +86 -192
  59. universal_mcp/applications/notion/app.py +21 -36
  60. universal_mcp/applications/onedrive/app.py +14 -36
  61. universal_mcp/applications/openai/app.py +42 -165
  62. universal_mcp/applications/outlook/app.py +16 -76
  63. universal_mcp/applications/perplexity/app.py +4 -19
  64. universal_mcp/applications/pipedrive/app.py +832 -3142
  65. universal_mcp/applications/posthog/app.py +163 -432
  66. universal_mcp/applications/reddit/app.py +40 -139
  67. universal_mcp/applications/resend/app.py +41 -107
  68. universal_mcp/applications/retell/app.py +14 -41
  69. universal_mcp/applications/rocketlane/app.py +221 -934
  70. universal_mcp/applications/scraper/README.md +7 -4
  71. universal_mcp/applications/scraper/app.py +280 -93
  72. universal_mcp/applications/semanticscholar/app.py +22 -64
  73. universal_mcp/applications/semrush/app.py +43 -77
  74. universal_mcp/applications/sendgrid/app.py +512 -1262
  75. universal_mcp/applications/sentry/app.py +271 -906
  76. universal_mcp/applications/serpapi/app.py +40 -143
  77. universal_mcp/applications/sharepoint/app.py +15 -37
  78. universal_mcp/applications/shopify/app.py +1551 -4287
  79. universal_mcp/applications/shortcut/app.py +155 -417
  80. universal_mcp/applications/slack/app.py +50 -101
  81. universal_mcp/applications/spotify/app.py +126 -325
  82. universal_mcp/applications/supabase/app.py +104 -213
  83. universal_mcp/applications/tavily/app.py +1 -1
  84. universal_mcp/applications/trello/app.py +693 -2656
  85. universal_mcp/applications/twilio/app.py +14 -50
  86. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  87. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  88. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  89. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  90. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  91. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  92. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  93. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  94. universal_mcp/applications/whatsapp/app.py +35 -186
  95. universal_mcp/applications/whatsapp/audio.py +2 -6
  96. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  97. universal_mcp/applications/whatsapp_business/app.py +70 -283
  98. universal_mcp/applications/wrike/app.py +45 -118
  99. universal_mcp/applications/yahoo_finance/app.py +19 -65
  100. universal_mcp/applications/youtube/app.py +75 -261
  101. universal_mcp/applications/zenquotes/app.py +2 -2
  102. {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/METADATA +2 -2
  103. {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/RECORD +105 -106
  104. universal_mcp/applications/scraper/scraper_testers.py +0 -17
  105. {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/WHEEL +0 -0
  106. {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  import os
3
3
  from typing import Any, Callable, Literal
4
-
5
4
  from loguru import logger
6
5
  from universal_mcp.applications.application import APIApplication
7
6
  from universal_mcp.integrations import Integration
@@ -23,7 +22,6 @@ class LinkedinApp(APIApplication):
23
22
  `{"headers": {"x-api-key": "YOUR_API_KEY"}}`.
24
23
  """
25
24
  super().__init__(name="linkedin", integration=integration)
26
-
27
25
  self._base_url = None
28
26
  self.account_id = None
29
27
  if self.integration:
@@ -36,12 +34,8 @@ class LinkedinApp(APIApplication):
36
34
  if not self._base_url:
37
35
  unipile_dsn = os.getenv("UNIPILE_DSN")
38
36
  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
- )
37
+ logger.error("UnipileApp: UNIPILE_DSN environment variable is not set.")
38
+ raise ValueError("UnipileApp: UNIPILE_DSN environment variable is required.")
45
39
  self._base_url = f"https://{unipile_dsn}"
46
40
  return self._base_url
47
41
 
@@ -56,35 +50,46 @@ class LinkedinApp(APIApplication):
56
50
  Overrides the base class method to use X-Api-Key.
57
51
  """
58
52
  if not self.integration:
59
- logger.warning(
60
- "UnipileApp: No integration configured, returning empty headers."
61
- )
53
+ logger.warning("UnipileApp: No integration configured, returning empty headers.")
62
54
  return {}
63
-
64
55
  api_key = os.getenv("UNIPILE_API_KEY")
65
56
  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
-
57
+ logger.error("UnipileApp: API key not found in integration credentials for Unipile.")
58
+ return {"Content-Type": "application/json", "Cache-Control": "no-cache"}
74
59
  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
- }
60
+ return {"x-api-key": api_key, "Content-Type": "application/json", "Cache-Control": "no-cache"}
80
61
 
81
- def list_all_chats(
62
+ def _get_search_parameter_id(self, param_type: str, keywords: str) -> str:
63
+ """
64
+ Retrieves the ID for a given LinkedIn search parameter by its name.
65
+
66
+ Args:
67
+ param_type: The type of parameter to search for (e.g., "LOCATION", "COMPANY").
68
+ keywords: The name of the parameter to find (e.g., "United States").
69
+
70
+ Returns:
71
+ The corresponding ID for the search parameter.
72
+
73
+ Raises:
74
+ ValueError: If no exact match for the keywords is found.
75
+ httpx.HTTPError: If the API request fails.
76
+ """
77
+ url = f"{self.base_url}/api/v1/linkedin/search/parameters"
78
+ params = {"account_id": self.account_id, "keywords": keywords, "type": param_type}
79
+ response = self._get(url, params=params)
80
+ results = self._handle_response(response)
81
+ items = results.get("items", [])
82
+ if items:
83
+ return items[0]["id"]
84
+ raise ValueError(f'Could not find a matching ID for {param_type}: "{keywords}"')
85
+
86
+ async def list_all_chats(
82
87
  self,
83
88
  unread: bool | None = None,
84
89
  cursor: str | None = None,
85
- before: str | None = None, # ISO 8601 UTC datetime
86
- after: str | None = None, # ISO 8601 UTC datetime
87
- limit: int | None = None, # 1-250
90
+ before: str | None = None,
91
+ after: str | None = None,
92
+ limit: int | None = None,
88
93
  account_type: str | None = None,
89
94
  ) -> dict[str, Any]:
90
95
  """
@@ -109,9 +114,7 @@ class LinkedinApp(APIApplication):
109
114
  """
110
115
  url = f"{self.base_url}/api/v1/chats"
111
116
  params: dict[str, Any] = {}
112
-
113
117
  params["account_id"] = self.account_id
114
-
115
118
  if unread is not None:
116
119
  params["unread"] = unread
117
120
  if cursor:
@@ -124,18 +127,16 @@ class LinkedinApp(APIApplication):
124
127
  params["limit"] = limit
125
128
  if account_type:
126
129
  params["account_type"] = account_type
127
-
128
-
129
130
  response = self._get(url, params=params)
130
131
  return response.json()
131
132
 
132
- def list_chat_messages(
133
+ async def list_chat_messages(
133
134
  self,
134
135
  chat_id: str,
135
136
  cursor: str | None = None,
136
- before: str | None = None, # ISO 8601 UTC datetime
137
- after: str | None = None, # ISO 8601 UTC datetime
138
- limit: int | None = None, # 1-250
137
+ before: str | None = None,
138
+ after: str | None = None,
139
+ limit: int | None = None,
139
140
  sender_id: str | None = None,
140
141
  ) -> dict[str, Any]:
141
142
  """
@@ -170,15 +171,10 @@ class LinkedinApp(APIApplication):
170
171
  params["limit"] = limit
171
172
  if sender_id:
172
173
  params["sender_id"] = sender_id
173
-
174
174
  response = self._get(url, params=params)
175
175
  return response.json()
176
176
 
177
- def send_chat_message(
178
- self,
179
- chat_id: str,
180
- text: str,
181
- ) -> dict[str, Any]:
177
+ async def send_chat_message(self, chat_id: str, text: str) -> dict[str, Any]:
182
178
  """
183
179
  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.
184
180
 
@@ -198,11 +194,10 @@ class LinkedinApp(APIApplication):
198
194
  """
199
195
  url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
200
196
  payload: dict[str, Any] = {"text": text}
201
-
202
197
  response = self._post(url, data=payload)
203
198
  return response.json()
204
199
 
205
- def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
200
+ async def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
206
201
  """
207
202
  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.
208
203
 
@@ -222,16 +217,15 @@ class LinkedinApp(APIApplication):
222
217
  params: dict[str, Any] = {}
223
218
  if self.account_id:
224
219
  params["account_id"] = self.account_id
225
-
226
220
  response = self._get(url, params=params)
227
221
  return response.json()
228
222
 
229
- def list_all_messages(
223
+ async def list_all_messages(
230
224
  self,
231
225
  cursor: str | None = None,
232
- before: str | None = None, # ISO 8601 UTC datetime
233
- after: str | None = None, # ISO 8601 UTC datetime
234
- limit: int | None = None, # 1-250
226
+ before: str | None = None,
227
+ after: str | None = None,
228
+ limit: int | None = None,
235
229
  sender_id: str | None = None,
236
230
  ) -> dict[str, Any]:
237
231
  """
@@ -267,64 +261,11 @@ class LinkedinApp(APIApplication):
267
261
  params["sender_id"] = sender_id
268
262
  if self.account_id:
269
263
  params["account_id"] = self.account_id
270
-
271
264
  response = self._get(url, params=params)
272
265
  return response.json()
273
266
 
274
- def list_all_accounts(
275
- self,
276
- cursor: str | None = None,
277
- limit: int | None = None, # 1-259 according to spec
278
- ) -> dict[str, Any]:
279
- """
280
- Retrieves a paginated list of all social media accounts linked to the Unipile service. This is crucial for obtaining the `account_id` required by other methods to specify which user account should perform an action, like sending a message or retrieving user-specific posts.
281
-
282
- Args:
283
- cursor: Pagination cursor.
284
- limit: Number of items to return (1-259).
285
-
286
- Returns:
287
- A dictionary containing a list of account objects and a pagination cursor.
288
-
289
- Raises:
290
- httpx.HTTPError: If the API request fails.
291
-
292
- Tags:
293
- linkedin, account, list, unipile, api, important
294
- """
295
- url = f"{self.base_url}/api/v1/accounts"
296
- params: dict[str, Any] = {}
297
- if cursor:
298
- params["cursor"] = cursor
299
- if limit:
300
- params["limit"] = limit
301
-
302
- response = self._get(url, params=params)
303
- return response.json()
304
-
305
- # def retrieve_linked_account(self) -> dict[str, Any]:
306
- # """
307
- # Retrieves details for the account linked to Unipile. It fetches metadata about the connection itself (e.g., a linked LinkedIn account), differentiating it from `retrieve_user_profile` which fetches a user's profile from the external platform.
308
-
309
- # Returns:
310
- # A dictionary containing the account object details.
311
-
312
- # Raises:
313
- # httpx.HTTPError: If the API request fails.
314
-
315
- # Tags:
316
- # linkedin, account, retrieve, get, unipile, api, important
317
- # """
318
- # url = f"{self.base_url}/api/v1/accounts/{self.account_id}"
319
- # response = self._get(url)
320
- # return response.json()
321
-
322
- def list_profile_posts(
323
- self,
324
- identifier: str, # User or Company provider internal ID
325
- cursor: str | None = None,
326
- limit: int | None = None, # 1-100 (spec says max 250)
327
- is_company: bool | None = None,
267
+ async def list_profile_posts(
268
+ self, identifier: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
328
269
  ) -> dict[str, Any]:
329
270
  """
330
271
  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.
@@ -352,11 +293,10 @@ class LinkedinApp(APIApplication):
352
293
  params["limit"] = limit
353
294
  if is_company is not None:
354
295
  params["is_company"] = is_company
355
-
356
296
  response = self._get(url, params=params)
357
297
  return response.json()
358
298
 
359
- def retrieve_own_profile(self) -> dict[str, Any]:
299
+ async def retrieve_own_profile(self) -> dict[str, Any]:
360
300
  """
361
301
  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.
362
302
 
@@ -374,7 +314,7 @@ class LinkedinApp(APIApplication):
374
314
  response = self._get(url, params=params)
375
315
  return response.json()
376
316
 
377
- def retrieve_post(self, post_id: str) -> dict[str, Any]:
317
+ async def retrieve_post(self, post_id: str) -> dict[str, Any]:
378
318
  """
379
319
  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.
380
320
 
@@ -395,18 +335,14 @@ class LinkedinApp(APIApplication):
395
335
  response = self._get(url, params=params)
396
336
  return response.json()
397
337
 
398
- def list_post_comments(
399
- self,
400
- post_id: str,
401
- comment_id: str | None = None,
402
- cursor: str | None = None,
403
- limit: int | None = None,
338
+ async def list_post_comments(
339
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
404
340
  ) -> dict[str, Any]:
405
341
  """
406
- 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'.
342
+ 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.
407
343
 
408
344
  Args:
409
- post_id: The social ID of the post.
345
+ post_id: The social ID of the post which you get from using `retrieve_post` or `list_profile_posts` tools.
410
346
  comment_id: If provided, retrieves replies to this comment ID instead of top-level comments.
411
347
  cursor: Pagination cursor.
412
348
  limit: Number of comments to return. (OpenAPI spec shows type string, passed as string if provided).
@@ -428,15 +364,11 @@ class LinkedinApp(APIApplication):
428
364
  params["limit"] = str(limit)
429
365
  if comment_id:
430
366
  params["comment_id"] = comment_id
431
-
432
367
  response = self._get(url, params=params)
433
368
  return response.json()
434
369
 
435
- def create_post(
436
- self,
437
- text: str,
438
- mentions: list[dict[str, Any]] | None = None,
439
- external_link: str | None = None,
370
+ async def create_post(
371
+ self, text: str, mentions: list[dict[str, Any]] | None = None, external_link: str | None = None
440
372
  ) -> dict[str, Any]:
441
373
  """
442
374
  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.
@@ -457,26 +389,16 @@ class LinkedinApp(APIApplication):
457
389
  linkedin, post, create, share, content, api, important
458
390
  """
459
391
  url = f"{self.base_url}/api/v1/posts"
460
-
461
- params: dict[str, str] = {
462
- "account_id": self.account_id,
463
- "text": text,
464
- }
465
-
392
+ params: dict[str, str] = {"account_id": self.account_id, "text": text}
466
393
  if mentions:
467
394
  params["mentions"] = mentions
468
395
  if external_link:
469
396
  params["external_link"] = external_link
470
-
471
397
  response = self._post(url, data=params)
472
398
  return response.json()
473
399
 
474
- def list_content_reactions(
475
- self,
476
- post_id: str,
477
- comment_id: str | None = None,
478
- cursor: str | None = None,
479
- limit: int | None = None,
400
+ async def list_content_reactions(
401
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
480
402
  ) -> dict[str, Any]:
481
403
  """
482
404
  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.
@@ -504,16 +426,11 @@ class LinkedinApp(APIApplication):
504
426
  params["limit"] = limit
505
427
  if comment_id:
506
428
  params["comment_id"] = comment_id
507
-
508
429
  response = self._get(url, params=params)
509
430
  return response.json()
510
431
 
511
- def create_post_comment(
512
- self,
513
- post_social_id: str,
514
- text: str,
515
- comment_id: str | None = None, # If provided, replies to a specific comment
516
- mentions_body: list[dict[str, Any]] | None = None,
432
+ async def create_post_comment(
433
+ self, post_social_id: str, text: str, comment_id: str | None = None, mentions_body: list[dict[str, Any]] | None = None
517
434
  ) -> dict[str, Any]:
518
435
  """
519
436
  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.
@@ -535,33 +452,21 @@ class LinkedinApp(APIApplication):
535
452
  linkedin, post, comment, create, content, api, important
536
453
  """
537
454
  url = f"{self.base_url}/api/v1/posts/{post_social_id}/comments"
538
- params: dict[str, Any] = {
539
- "account_id": self.account_id,
540
- "text": text,
541
- }
542
-
455
+ params: dict[str, Any] = {"account_id": self.account_id, "text": text}
543
456
  if comment_id:
544
457
  params["comment_id"] = comment_id
545
-
546
458
  if mentions_body:
547
459
  params = {"mentions": mentions_body}
548
-
549
460
  response = self._post(url, data=params)
550
-
551
461
  try:
552
462
  return response.json()
553
463
  except json.JSONDecodeError:
554
- return {
555
- "status": response.status_code,
556
- "message": "Comment action processed.",
557
- }
464
+ return {"status": response.status_code, "message": "Comment action processed."}
558
465
 
559
- def create_reaction(
466
+ async def create_reaction(
560
467
  self,
561
468
  post_social_id: str,
562
- reaction_type: Literal[
563
- "like", "celebrate", "love", "insightful", "funny", "support"
564
- ],
469
+ reaction_type: Literal["like", "celebrate", "love", "insightful", "funny", "support"],
565
470
  comment_id: str | None = None,
566
471
  ) -> dict[str, Any]:
567
472
  """
@@ -582,113 +487,382 @@ class LinkedinApp(APIApplication):
582
487
  linkedin, post, reaction, create, like, content, api, important
583
488
  """
584
489
  url = f"{self.base_url}/api/v1/posts/reaction"
585
-
586
- params: dict[str, str] = {
587
- "account_id": self.account_id,
588
- "post_id": post_social_id,
589
- "reaction_type": reaction_type,
590
- }
591
-
490
+ params: dict[str, str] = {"account_id": self.account_id, "post_id": post_social_id, "reaction_type": reaction_type}
592
491
  if comment_id:
593
492
  params["comment_id"] = comment_id
594
-
595
493
  response = self._post(url, data=params)
596
-
597
494
  try:
598
495
  return response.json()
599
496
  except json.JSONDecodeError:
600
- return {
601
- "status": response.status_code,
602
- "message": "Reaction action processed.",
603
- }
497
+ return {"status": response.status_code, "message": "Reaction action processed."}
604
498
 
605
- def search(
499
+ async def retrieve_user_profile(self, public_identifier: str) -> dict[str, Any]:
500
+ """
501
+ 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.
502
+
503
+ Args:
504
+ 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".
505
+
506
+ Returns:
507
+ A dictionary containing the user's profile details.
508
+
509
+ Raises:
510
+ httpx.HTTPError: If the API request fails.
511
+
512
+ Tags:
513
+ linkedin, user, profile, retrieve, get, api, important
514
+ """
515
+ url = f"{self.base_url}/api/v1/users/{public_identifier}"
516
+ params: dict[str, Any] = {"account_id": self.account_id}
517
+ response = self._get(url, params=params)
518
+ return self._handle_response(response)
519
+
520
+ async def search_people(
606
521
  self,
607
- category: Literal["people", "companies", "posts", "jobs"],
608
522
  cursor: str | None = None,
609
523
  limit: int | None = None,
610
524
  keywords: str | None = None,
611
- date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
612
- sort_by: Literal["relevance", "date"] = "relevance",
613
- minimum_salary_value: int = 40,
525
+ location: str | None = None,
526
+ industry: str | None = None,
527
+ company: str | None = None,
614
528
  ) -> dict[str, Any]:
615
529
  """
616
- Performs a comprehensive LinkedIn search for people, companies, posts, or jobs using keywords.
617
- Supports pagination and targets either the classic or Sales Navigator API for posts.
618
- For people, companies, and jobs, it uses the classic API.
530
+ 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.
619
531
 
620
532
  Args:
621
- category: Type of search to perform. Valid values are "people", "companies", "posts", or "jobs".
622
533
  cursor: Pagination cursor for the next page of entries.
623
534
  limit: Number of items to return (up to 50 for Classic search).
624
535
  keywords: Keywords to search for.
625
- date_posted: Filter by when the post was posted (posts only). Valid values are "past_day", "past_week", or "past_month".
626
- sort_by: How to sort the results (for posts and jobs). Valid values are "relevance" or "date".
627
- minimum_salary_value: The minimum salary to filter for (jobs only).
536
+ location: The geographical location to filter people by (e.g., "United States").
537
+ industry: The industry to filter people by.(eg., "Information Technology and Services").
538
+ company: The company to filter people by.(e.g., "Google").
628
539
 
629
540
  Returns:
630
541
  A dictionary containing search results and pagination details.
631
542
 
632
543
  Raises:
633
544
  httpx.HTTPError: If the API request fails.
634
- ValueError: If the category is empty.
545
+ """
546
+ url = f"{self.base_url}/api/v1/linkedin/search"
547
+ params: dict[str, Any] = {"account_id": self.account_id}
548
+ if cursor:
549
+ params["cursor"] = cursor
550
+ if limit is not None:
551
+ params["limit"] = limit
552
+ payload: dict[str, Any] = {"api": "classic", "category": "people"}
553
+ if keywords:
554
+ payload["keywords"] = keywords
555
+ if location:
556
+ location_id = self._get_search_parameter_id("LOCATION", location)
557
+ payload["location"] = [location_id]
558
+ if industry:
559
+ industry_id = self._get_search_parameter_id("INDUSTRY", industry)
560
+ payload["industry"] = [industry_id]
561
+ if company:
562
+ company_id = self._get_search_parameter_id("COMPANY", company)
563
+ payload["company"] = [company_id]
564
+ response = self._post(url, params=params, data=payload)
565
+ return self._handle_response(response)
635
566
 
636
- Tags:
637
- linkedin, search, people, companies, posts, jobs, api, important
567
+ async def search_companies(
568
+ self,
569
+ cursor: str | None = None,
570
+ limit: int | None = None,
571
+ keywords: str | None = None,
572
+ location: str | None = None,
573
+ industry: str | None = None,
574
+ ) -> dict[str, Any]:
638
575
  """
639
- if not category:
640
- raise ValueError("Category cannot be empty.")
576
+ 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.
641
577
 
642
- url = f"{self.base_url}/api/v1/linkedin/search"
578
+ Args:
579
+ cursor: Pagination cursor for the next page of entries.
580
+ limit: Number of items to return (up to 50 for Classic search).
581
+ keywords: Keywords to search for.
582
+ location: The geographical location to filter companies by (e.g., "United States").
583
+ industry: The industry to filter companies by.(e.g., "Information Technology and Services").
643
584
 
585
+ Returns:
586
+ A dictionary containing search results and pagination details.
587
+
588
+ Raises:
589
+ httpx.HTTPError: If the API request fails.
590
+ """
591
+ url = f"{self.base_url}/api/v1/linkedin/search"
644
592
  params: dict[str, Any] = {"account_id": self.account_id}
645
593
  if cursor:
646
594
  params["cursor"] = cursor
647
595
  if limit is not None:
648
596
  params["limit"] = limit
597
+ payload: dict[str, Any] = {"api": "classic", "category": "companies"}
598
+ if keywords:
599
+ payload["keywords"] = keywords
600
+ if location:
601
+ location_id = self._get_search_parameter_id("LOCATION", location)
602
+ payload["location"] = [location_id]
603
+ if industry:
604
+ industry_id = self._get_search_parameter_id("INDUSTRY", industry)
605
+ payload["industry"] = [industry_id]
606
+ response = self._post(url, params=params, data=payload)
607
+ return self._handle_response(response)
608
+
609
+ async def search_posts(
610
+ self,
611
+ cursor: str | None = None,
612
+ limit: int | None = None,
613
+ keywords: str | None = None,
614
+ date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
615
+ sort_by: Literal["relevance", "date"] = "relevance",
616
+ ) -> dict[str, Any]:
617
+ """
618
+ 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.
619
+
620
+ Args:
621
+ cursor: Pagination cursor for the next page of entries.
622
+ limit: Number of items to return (up to 50 for Classic search).
623
+ keywords: Keywords to search for.
624
+ date_posted: Filter by when the post was posted.
625
+ sort_by: How to sort the results.
649
626
 
650
- payload: dict[str, Any] = {"api": "classic", "category": category}
627
+ Returns:
628
+ A dictionary containing search results and pagination details.
651
629
 
630
+ Raises:
631
+ httpx.HTTPError: If the API request fails.
632
+ """
633
+ url = f"{self.base_url}/api/v1/linkedin/search"
634
+ params: dict[str, Any] = {"account_id": self.account_id}
635
+ if cursor:
636
+ params["cursor"] = cursor
637
+ if limit is not None:
638
+ params["limit"] = limit
639
+ payload: dict[str, Any] = {"api": "classic", "category": "posts"}
652
640
  if keywords:
653
641
  payload["keywords"] = keywords
642
+ if date_posted:
643
+ payload["date_posted"] = date_posted
644
+ if sort_by:
645
+ payload["sort_by"] = sort_by
646
+ response = self._post(url, params=params, data=payload)
647
+ return self._handle_response(response)
648
+
649
+ async def search_jobs(
650
+ self,
651
+ cursor: str | None = None,
652
+ limit: int | None = None,
653
+ keywords: str | None = None,
654
+ region: str | None = None,
655
+ sort_by: Literal["relevance", "date"] = "relevance",
656
+ minimum_salary_value: int = 40,
657
+ industry: str | None = None,
658
+ ) -> dict[str, Any]:
659
+ """
660
+ 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.
654
661
 
655
- if category == "posts":
656
- if date_posted:
657
- payload["date_posted"] = date_posted
658
- if sort_by:
659
- payload["sort_by"] = sort_by
662
+ Args:
663
+ cursor: Pagination cursor for the next page of entries.
664
+ limit: Number of items to return (up to 50 for Classic search).
665
+ keywords: Keywords to search for.
666
+ region: The geographical region to filter jobs by (e.g., "United States").
667
+ sort_by: How to sort the results.(e.g., "relevance" or "date".)
668
+ minimum_salary_value: The minimum salary to filter for.
669
+ industry: The industry to filter jobs by.(e.g., "Software Development").
660
670
 
661
- elif category == "jobs":
662
- payload["minimum_salary"] = {
663
- "currency": "USD",
664
- "value": minimum_salary_value,
665
- }
666
- if sort_by:
667
- payload["sort_by"] = sort_by
671
+ Returns:
672
+ A dictionary containing search results and pagination details.
668
673
 
674
+ Raises:
675
+ httpx.HTTPError: If the API request fails.
676
+ ValueError: If the specified location is not found.
677
+ """
678
+ url = f"{self.base_url}/api/v1/linkedin/search"
679
+ params: dict[str, Any] = {"account_id": self.account_id}
680
+ if cursor:
681
+ params["cursor"] = cursor
682
+ if limit is not None:
683
+ params["limit"] = limit
684
+ payload: dict[str, Any] = {
685
+ "api": "classic",
686
+ "category": "jobs",
687
+ "minimum_salary": {"currency": "USD", "value": minimum_salary_value},
688
+ }
689
+ if keywords:
690
+ payload["keywords"] = keywords
691
+ if sort_by:
692
+ payload["sort_by"] = sort_by
693
+ if region:
694
+ location_id = self._get_search_parameter_id("LOCATION", region)
695
+ payload["region"] = location_id
696
+ if industry:
697
+ industry_id = self._get_search_parameter_id("INDUSTRY", industry)
698
+ payload["industry"] = [industry_id]
669
699
  response = self._post(url, params=params, data=payload)
670
700
  return self._handle_response(response)
671
701
 
672
- def retrieve_user_profile(self, identifier: str) -> dict[str, Any]:
702
+ async def send_invitation(self, provider_id: str, user_email: str | None = None, message: str | None = None) -> dict[str, Any]:
673
703
  """
674
- 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.
704
+ Sends a connection invitation to a LinkedIn user specified by their provider ID. An optional message and the user's email can be included.
675
705
 
676
706
  Args:
677
- 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".
707
+ provider_id: The LinkedIn provider ID of the user to invite. This is available in response of `retrieve_user_profile` tool.
708
+ user_email: Optional. The email address of the user, which may be required by LinkedIn.
709
+ message: Optional. A personalized message to include with the invitation (max 300 characters).
678
710
 
679
711
  Returns:
680
- A dictionary containing the user's profile details.
712
+ A dictionary confirming the invitation was sent.
681
713
 
682
714
  Raises:
683
715
  httpx.HTTPError: If the API request fails.
716
+ ValueError: If the message exceeds 300 characters.
684
717
 
685
718
  Tags:
686
- linkedin, user, profile, retrieve, get, api, important
719
+ linkedin, user, invite, connect, contact, api, important
720
+ """
721
+ url = f"{self.base_url}/api/v1/users/invite"
722
+ payload: dict[str, Any] = {"account_id": self.account_id, "provider_id": provider_id}
723
+ if user_email:
724
+ payload["user_email"] = user_email
725
+ if message:
726
+ if len(message) > 300:
727
+ raise ValueError("Message cannot exceed 300 characters.")
728
+ payload["message"] = message
729
+ response = self._post(url, data=payload)
730
+ try:
731
+ return response.json()
732
+ except json.JSONDecodeError:
733
+ return {"status": response.status_code, "message": "Invitation action processed."}
734
+
735
+ async def list_sent_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
736
+ """
737
+ 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.
738
+
739
+ Args:
740
+ cursor: A pagination cursor for retrieving the next page of entries.
741
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
742
+
743
+ Returns:
744
+ A dictionary containing a list of sent invitation objects and pagination details.
745
+
746
+ Raises:
747
+ httpx.HTTPError: If the API request fails.
748
+
749
+ Tags:
750
+ linkedin, user, invite, sent, list, contacts, api
687
751
  """
688
- url = f"{self.base_url}/api/v1/users/{identifier}"
752
+ url = f"{self.base_url}/api/v1/users/invite/sent"
689
753
  params: dict[str, Any] = {"account_id": self.account_id}
754
+ if cursor:
755
+ params["cursor"] = cursor
756
+ if limit is not None:
757
+ params["limit"] = limit
690
758
  response = self._get(url, params=params)
691
- return self._handle_response(response)
759
+ return response.json()
760
+
761
+ async def list_received_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
762
+ """
763
+ Retrieves a paginated list of all received connection invitations. This function allows for reviewing and processing incoming connection requests to the specified account.
764
+
765
+ Args:
766
+ cursor: A pagination cursor for retrieving the next page of entries.
767
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
768
+
769
+ Returns:
770
+ A dictionary containing a list of received invitation objects and pagination details.
771
+
772
+ Raises:
773
+ httpx.HTTPError: If the API request fails.
774
+
775
+ Tags:
776
+ linkedin, user, invite, received, list, contacts, api
777
+ """
778
+ url = f"{self.base_url}/api/v1/users/invite/received"
779
+ params: dict[str, Any] = {"account_id": self.account_id}
780
+ if cursor:
781
+ params["cursor"] = cursor
782
+ if limit is not None:
783
+ params["limit"] = limit
784
+ response = self._get(url, params=params)
785
+ return response.json()
786
+
787
+ async def handle_received_invitation(
788
+ self, invitation_id: str, action: Literal["accept", "decline"], shared_secret: str
789
+ ) -> dict[str, Any]:
790
+ """
791
+ 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`.
792
+
793
+ Args:
794
+ invitation_id: The ID of the invitation to handle.Get this ID from the 'list_received_invitations' tool.
795
+ action: The action to perform, either "accept" or "decline".
796
+ shared_secret: The token provided by LinkedIn, retrieved from the 'list_received_invitations' tool, which is mandatory for this action.
797
+
798
+ Returns:
799
+ A dictionary confirming the action was processed.
800
+
801
+ Raises:
802
+ httpx.HTTPError: If the API request fails.
803
+
804
+ Tags:
805
+ linkedin, user, invite, received, handle, accept, decline, api
806
+ """
807
+ url = f"{self.base_url}/api/v1/users/invite/received/{invitation_id}"
808
+ payload: dict[str, Any] = {"provider": "LINKEDIN", "action": action, "shared_secret": shared_secret, "account_id": self.account_id}
809
+ response = self._post(url, data=payload)
810
+ try:
811
+ return response.json()
812
+ except json.JSONDecodeError:
813
+ return {"status": response.status_code, "message": f"Invitation action '{action}' processed."}
814
+
815
+ async def list_followers(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
816
+ """
817
+ 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.
818
+
819
+ Args:
820
+ cursor: A pagination cursor for retrieving the next page of entries.
821
+ limit: The number of items to return, ranging from 1 to 1000.
822
+
823
+ Returns:
824
+ A dictionary containing a list of follower objects and pagination details.
825
+
826
+ Raises:
827
+ httpx.HTTPError: If the API request fails.
828
+
829
+ Tags:
830
+ linkedin, user, followers, list, contacts, api
831
+ """
832
+ url = f"{self.base_url}/api/v1/users/followers"
833
+ params: dict[str, Any] = {"account_id": self.account_id}
834
+ if cursor:
835
+ params["cursor"] = cursor
836
+ if limit is not None:
837
+ params["limit"] = limit
838
+ response = self._get(url, params=params)
839
+ return response.json()
840
+
841
+ def list_following(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
842
+ """
843
+ 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.
844
+
845
+ Args:
846
+ cursor: A pagination cursor for retrieving the next page of entries.
847
+ limit: The number of items to return, ranging from 1 to 1000.
848
+
849
+ Returns:
850
+ A dictionary containing a list of followed account objects and pagination details.
851
+
852
+ Raises:
853
+ httpx.HTTPError: If the API request fails.
854
+
855
+ Tags:
856
+ linkedin, user, following, list, contacts, api
857
+ """
858
+ url = f"{self.base_url}/api/v1/users/following"
859
+ params: dict[str, Any] = {"account_id": self.account_id}
860
+ if cursor:
861
+ params["cursor"] = cursor
862
+ if limit is not None:
863
+ params["limit"] = limit
864
+ response = self._get(url, params=params)
865
+ return response.json()
692
866
 
693
867
  def list_tools(self) -> list[Callable]:
694
868
  return [
@@ -697,8 +871,6 @@ class LinkedinApp(APIApplication):
697
871
  self.send_chat_message,
698
872
  self.retrieve_chat,
699
873
  self.list_all_messages,
700
- self.list_all_accounts,
701
- # self.retrieve_linked_account,
702
874
  self.list_profile_posts,
703
875
  self.retrieve_own_profile,
704
876
  self.retrieve_user_profile,
@@ -708,5 +880,13 @@ class LinkedinApp(APIApplication):
708
880
  self.list_content_reactions,
709
881
  self.create_post_comment,
710
882
  self.create_reaction,
711
- self.search,
883
+ self.search_companies,
884
+ self.search_jobs,
885
+ self.search_people,
886
+ self.search_posts,
887
+ self.send_invitation,
888
+ self.list_sent_invitations,
889
+ self.list_received_invitations,
890
+ self.handle_received_invitation,
891
+ self.list_followers,
712
892
  ]