universal-mcp-applications 0.1.30__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 (105) 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 +9 -2
  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 +216 -102
  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.30.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/METADATA +2 -2
  103. {universal_mcp_applications-0.1.30.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/RECORD +105 -105
  104. {universal_mcp_applications-0.1.30.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/WHEEL +0 -0
  105. {universal_mcp_applications-0.1.30.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,16 @@
1
- from typing import Any # For type hinting
2
-
1
+ from typing import Any
3
2
  import httpx
4
3
  from loguru import logger
5
4
  from universal_mcp.applications.application import APIApplication
6
- from universal_mcp.exceptions import NotAuthorizedError # For auth errors
7
- from universal_mcp.integrations import Integration # For integration type hint
8
-
9
- from serpapi import SerpApiClient as SerpApiSearch # Added SerpApiError
5
+ from universal_mcp.exceptions import NotAuthorizedError
6
+ from universal_mcp.integrations import Integration
7
+ from serpapi import SerpApiClient as SerpApiSearch
10
8
 
11
9
 
12
10
  class SerpapiApp(APIApplication):
13
11
  def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
14
12
  super().__init__(name="serpapi", integration=integration, **kwargs)
15
- self._serpapi_api_key: str | None = None # Cache for the API key
13
+ self._serpapi_api_key: str | None = None
16
14
  self.base_url = "https://serpapi.com/search"
17
15
 
18
16
  @property
@@ -23,54 +21,33 @@ class SerpapiApp(APIApplication):
23
21
  if self._serpapi_api_key is None:
24
22
  if not self.integration:
25
23
  logger.error("SerpApi App: Integration not configured.")
26
- raise NotAuthorizedError(
27
- "Integration not configured for SerpApi App. Cannot retrieve API key."
28
- )
29
-
24
+ raise NotAuthorizedError("Integration not configured for SerpApi App. Cannot retrieve API key.")
30
25
  try:
31
26
  credentials = self.integration.get_credentials()
32
27
  except NotAuthorizedError as e:
33
- logger.error(
34
- f"SerpApi App: Authorization error when fetching credentials: {e.message}"
35
- )
36
- raise # Re-raise the original NotAuthorizedError
28
+ logger.error(f"SerpApi App: Authorization error when fetching credentials: {e.message}")
29
+ raise
37
30
  except Exception as e:
38
- logger.error(
39
- f"SerpApi App: Unexpected error when fetching credentials: {e}",
40
- exc_info=True,
41
- )
31
+ logger.error(f"SerpApi App: Unexpected error when fetching credentials: {e}", exc_info=True)
42
32
  raise NotAuthorizedError(f"Failed to get SerpApi credentials: {e}")
43
-
44
- api_key = (
45
- credentials.get("api_key")
46
- or credentials.get("API_KEY") # Check common variations
47
- or credentials.get("apiKey")
48
- )
49
-
33
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
50
34
  if not api_key:
51
35
  logger.error("SerpApi App: API key not found in credentials.")
52
- action_message = "API key for SerpApi is missing. Please ensure it's set in the store (e.g., SERPAPI_API_KEY in credentials)."
53
- if hasattr(self.integration, "authorize") and callable(
54
- self.integration.authorize
55
- ):
36
+ action_message = (
37
+ "API key for SerpApi is missing. Please ensure it's set in the store (e.g., SERPAPI_API_KEY in credentials)."
38
+ )
39
+ if hasattr(self.integration, "authorize") and callable(self.integration.authorize):
56
40
  try:
57
41
  auth_details = self.integration.authorize()
58
42
  if isinstance(auth_details, str):
59
43
  action_message = auth_details
60
44
  elif isinstance(auth_details, dict) and "url" in auth_details:
61
- action_message = (
62
- f"Please authorize via: {auth_details['url']}"
63
- )
64
- elif (
65
- isinstance(auth_details, dict) and "message" in auth_details
66
- ):
45
+ action_message = f"Please authorize via: {auth_details['url']}"
46
+ elif isinstance(auth_details, dict) and "message" in auth_details:
67
47
  action_message = auth_details["message"]
68
48
  except Exception as auth_e:
69
- logger.warning(
70
- f"Could not retrieve specific authorization action for SerpApi: {auth_e}"
71
- )
49
+ logger.warning(f"Could not retrieve specific authorization action for SerpApi: {auth_e}")
72
50
  raise NotAuthorizedError(action_message)
73
-
74
51
  self._serpapi_api_key = api_key
75
52
  logger.info("SerpApi API Key successfully retrieved and cached.")
76
53
  return self._serpapi_api_key
@@ -93,27 +70,15 @@ class SerpapiApp(APIApplication):
93
70
  search, async, web-scraping, api, serpapi, important
94
71
  """
95
72
  request_params = params or {}
96
-
97
73
  try:
98
- current_api_key = self.serpapi_api_key # This can raise NotAuthorizedError
74
+ current_api_key = self.serpapi_api_key
99
75
  logger.info("Attempting SerpApi search.")
100
-
101
- serpapi_call_params = {
102
- "api_key": current_api_key,
103
- "engine": "google_light", # Fastest engine by default
104
- **request_params, # Include any additional parameters from the user
105
- }
106
-
107
- # SerpApiSearch (SerpApiClient) uses the 'requests' library and its get_dict() is synchronous.
108
- # If true async behavior is needed, this call should be wrapped with asyncio.to_thread.
76
+ serpapi_call_params = {"api_key": current_api_key, "engine": "google_light", **request_params}
109
77
  search_client = SerpApiSearch(serpapi_call_params)
110
78
  data = search_client.get_dict()
111
-
112
- # Check for errors returned in the API response body
113
79
  if "error" in data:
114
80
  error_message = data["error"]
115
81
  logger.error(f"SerpApi API returned an error: {error_message}")
116
- # Keywords indicating authorization/authentication issues
117
82
  auth_error_keywords = [
118
83
  "invalid api key",
119
84
  "authorization failed",
@@ -122,76 +87,42 @@ class SerpapiApp(APIApplication):
122
87
  "account disabled",
123
88
  "private api key is missing",
124
89
  ]
125
- if any(
126
- keyword in error_message.lower() for keyword in auth_error_keywords
127
- ):
90
+ if any((keyword in error_message.lower() for keyword in auth_error_keywords)):
128
91
  raise NotAuthorizedError(f"SerpApi Error: {error_message}")
129
- return f"SerpApi API Error: {error_message}" # Other API errors (e.g., missing parameters)
130
-
131
- # Process organic search results if available
92
+ return f"SerpApi API Error: {error_message}"
132
93
  if "organic_results" in data:
133
94
  formatted_results = []
134
95
  for result in data.get("organic_results", []):
135
96
  title = result.get("title", "No title")
136
97
  link = result.get("link", "No link")
137
98
  snippet = result.get("snippet", "No snippet")
138
- formatted_results.append(
139
- f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n"
140
- )
141
- return (
142
- "\n".join(formatted_results)
143
- if formatted_results
144
- else "No organic results found."
145
- )
99
+ formatted_results.append(f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n")
100
+ return "\n".join(formatted_results) if formatted_results else "No organic results found."
146
101
  else:
147
102
  return "No organic results found."
148
-
149
- except (
150
- NotAuthorizedError
151
- ): # Catches from self.serpapi_api_key or explicit raise above
103
+ except NotAuthorizedError:
152
104
  logger.error("SerpApi search failed due to an authorization error.")
153
- raise # Re-raise to be handled by the MCP framework
154
-
155
- except httpx.HTTPStatusError as e: # Kept from original for robustness, though SerpApiClient uses 'requests'
105
+ raise
106
+ except httpx.HTTPStatusError as e:
156
107
  logger.warning(
157
108
  f"SerpApi search encountered httpx.HTTPStatusError (unexpected with default SerpApiClient): {e.response.status_code}",
158
109
  exc_info=True,
159
110
  )
160
111
  if e.response.status_code == 429:
161
112
  return "Error: Rate limit exceeded (HTTP 429). Please try again later."
162
- elif (
163
- e.response.status_code == 401
164
- ): # Key was fetched but rejected by API with HTTP 401
165
- raise NotAuthorizedError(
166
- "Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY."
167
- )
113
+ elif e.response.status_code == 401:
114
+ raise NotAuthorizedError("Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY.")
168
115
  else:
169
116
  return f"HTTP Error: {e.response.status_code} - {e.response.text}"
170
-
171
- except Exception as e: # General catch-all, similar to E2B's final catch
117
+ except Exception as e:
172
118
  error_message_lower = str(e).lower()
173
119
  logger.error(f"Unexpected error during SerpApi search: {e}", exc_info=True)
174
- # Infer auth error from generic exception message
175
- auth_error_keywords = [
176
- "authentication",
177
- "api key",
178
- "unauthorized",
179
- "401",
180
- "forbidden",
181
- "invalid key",
182
- ]
183
- if any(keyword in error_message_lower for keyword in auth_error_keywords):
184
- raise NotAuthorizedError(
185
- f"SerpApi authentication/authorization failed: {str(e)}"
186
- )
120
+ auth_error_keywords = ["authentication", "api key", "unauthorized", "401", "forbidden", "invalid key"]
121
+ if any((keyword in error_message_lower for keyword in auth_error_keywords)):
122
+ raise NotAuthorizedError(f"SerpApi authentication/authorization failed: {str(e)}")
187
123
  return f"An unexpected error occurred during search: {str(e)}"
188
124
 
189
- async def google_maps_search(
190
- self,
191
- q: str | None = None,
192
- ll: str | None = None,
193
- place_id: str | None = None,
194
- ) -> dict[str, Any]:
125
+ async def google_maps_search(self, q: str | None = None, ll: str | None = None, place_id: str | None = None) -> dict[str, Any]:
195
126
  """
196
127
  Executes a Google Maps search via SerpApi using a query, coordinates, or place ID. It enhances the results by adding a `google_maps_url` to each location, distinguishing it from `get_google_maps_reviews` which retrieves reviews for a known place.
197
128
 
@@ -210,43 +141,23 @@ class SerpapiApp(APIApplication):
210
141
  Tags:
211
142
  google-maps, search, location, places, important
212
143
  """
213
-
214
144
  query_params = {}
215
- query_params = {
216
- "engine": "google_maps",
217
- "api_key": self.serpapi_api_key,
218
- }
219
-
145
+ query_params = {"engine": "google_maps", "api_key": self.serpapi_api_key}
220
146
  if q is not None:
221
147
  query_params["q"] = q
222
-
223
148
  if ll is not None:
224
149
  query_params["ll"] = ll
225
-
226
150
  if place_id is not None:
227
151
  query_params["place_id"] = place_id
228
-
229
- response = self._get(
230
- self.base_url,
231
- params=query_params,
232
- )
152
+ response = self._get(self.base_url, params=query_params)
233
153
  data = self._handle_response(response)
234
-
235
- # Add Google Maps URLs for each place in the results
236
154
  if "local_results" in data:
237
155
  for place in data["local_results"]:
238
156
  if "place_id" in place:
239
- place["google_maps_url"] = (
240
- f"https://www.google.com/maps/place/?q=place_id:{place['place_id']}"
241
- )
242
-
157
+ place["google_maps_url"] = f"https://www.google.com/maps/place/?q=place_id:{place['place_id']}"
243
158
  return data
244
159
 
245
- async def get_google_maps_reviews(
246
- self,
247
- data_id: str,
248
- hl: str | None = None,
249
- ) -> dict[str, Any]:
160
+ async def get_google_maps_reviews(self, data_id: str, hl: str | None = None) -> dict[str, Any]:
250
161
  """
251
162
  Fetches Google Maps reviews for a specific location via SerpApi using its unique `data_id`. This function uses the `google_maps_reviews` engine, unlike `google_maps_search` which finds locations. Results can be returned in a specified language, defaulting to English.
252
163
 
@@ -264,28 +175,14 @@ class SerpapiApp(APIApplication):
264
175
  Tags:
265
176
  google-maps, reviews, ratings, places, important
266
177
  """
267
-
268
178
  query_params = {}
269
- query_params = {
270
- "engine": "google_maps_reviews",
271
- "data_id": data_id,
272
- "api_key": self.serpapi_api_key,
273
- }
274
-
179
+ query_params = {"engine": "google_maps_reviews", "data_id": data_id, "api_key": self.serpapi_api_key}
275
180
  if hl is not None:
276
181
  query_params["hl"] = hl
277
182
  else:
278
183
  query_params["hl"] = "en"
279
-
280
- response = self._get(
281
- self.base_url,
282
- params=query_params,
283
- )
184
+ response = self._get(self.base_url, params=query_params)
284
185
  return self._handle_response(response)
285
186
 
286
187
  def list_tools(self) -> list[callable]:
287
- return [
288
- self.web_search,
289
- self.google_maps_search,
290
- self.get_google_maps_reviews,
291
- ]
188
+ return [self.web_search, self.google_maps_search, self.get_google_maps_reviews]
@@ -1,7 +1,6 @@
1
1
  import base64
2
2
  import os
3
3
  from typing import Any
4
-
5
4
  from loguru import logger
6
5
  from universal_mcp.applications.application import APIApplication
7
6
  from universal_mcp.integrations import Integration
@@ -17,7 +16,7 @@ class SharepointApp(APIApplication):
17
16
  super().__init__(name="sharepoint", integration=integration, **kwargs)
18
17
  self.base_url = "https://graph.microsoft.com/v1.0"
19
18
 
20
- def get_my_profile(self) -> dict[str, Any]:
19
+ async def get_my_profile(self) -> dict[str, Any]:
21
20
  """
22
21
  Fetches the profile for the currently authenticated user, specifically retrieving their ID and user principal name. This function confirms user identity, distinguishing it from `get_drive_info`, which returns details about the SharePoint storage space (e.g., quota) rather than the user's personal profile.
23
22
 
@@ -35,7 +34,7 @@ class SharepointApp(APIApplication):
35
34
  response = self._get(url, params=query_params)
36
35
  return self._handle_response(response)
37
36
 
38
- def get_drive_info(self) -> dict[str, Any]:
37
+ async def get_drive_info(self) -> dict[str, Any]:
39
38
  """
40
39
  Fetches high-level information about the user's entire SharePoint. It returns drive-wide details like the owner and storage quota, differing from `get_item_metadata` which describes a specific item, and `get_my_profile` which retrieves general user account information.
41
40
 
@@ -63,7 +62,7 @@ class SharepointApp(APIApplication):
63
62
  response = self._get(url)
64
63
  return self._handle_response(response)
65
64
 
66
- def search_files(self, query: str) -> dict[str, Any]:
65
+ async def search_files(self, query: str) -> dict[str, Any]:
67
66
  """
68
67
  Searches the user's entire SharePoint for files and folders matching a specified text query. This function performs a comprehensive search from the drive's root, distinguishing it from `list_files` or `list_folders` which only browse the contents of a single directory.
69
68
 
@@ -78,12 +77,11 @@ class SharepointApp(APIApplication):
78
77
  """
79
78
  if not query:
80
79
  raise ValueError("Search query cannot be empty.")
81
-
82
80
  url = f"{self.base_url}/me/drive/root/search(q='{query}')"
83
81
  response = self._get(url)
84
82
  return self._handle_response(response)
85
83
 
86
- def get_item_metadata(self, item_id: str) -> dict[str, Any]:
84
+ async def get_item_metadata(self, item_id: str) -> dict[str, Any]:
87
85
  """
88
86
  Fetches detailed metadata for a specific file or folder using its unique ID. It returns properties like name, size, and type. Unlike `get_document_content`, it doesn't retrieve the file's actual content, focusing solely on the item's attributes for quick inspection without a full download.
89
87
 
@@ -98,12 +96,11 @@ class SharepointApp(APIApplication):
98
96
  """
99
97
  if not item_id:
100
98
  raise ValueError("Missing required parameter 'item_id'.")
101
-
102
99
  url = f"{self.base_url}/me/drive/items/{item_id}"
103
100
  response = self._get(url)
104
101
  return self._handle_response(response)
105
102
 
106
- def create_folder(self, name: str, parent_id: str = "root") -> dict[str, Any]:
103
+ async def create_folder(self, name: str, parent_id: str = "root") -> dict[str, Any]:
107
104
  """
108
105
  Creates a new folder with a specified name within a parent directory, which defaults to the root. Returns metadata for the new folder. Unlike `create_folder_and_list`, this function only creates the folder and returns its specific metadata, not the parent directory's contents.
109
106
 
@@ -119,13 +116,12 @@ class SharepointApp(APIApplication):
119
116
  """
120
117
  if not name:
121
118
  raise ValueError("Folder name cannot be empty.")
122
-
123
119
  url = f"{self.base_url}/me/drive/items/{parent_id}/children"
124
120
  data = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename"}
125
121
  response = self._post(url, data=data)
126
122
  return self._handle_response(response)
127
123
 
128
- def delete_item(self, item_id: str) -> dict[str, Any]:
124
+ async def delete_item(self, item_id: str) -> dict[str, Any]:
129
125
  """
130
126
  Permanently deletes a specified file or folder from SharePoint using its unique item ID. This versatile function can remove any type of drive item, distinguished from functions that only list or create specific types. A successful deletion returns an empty response, confirming the item's removal.
131
127
 
@@ -140,12 +136,11 @@ class SharepointApp(APIApplication):
140
136
  """
141
137
  if not item_id:
142
138
  raise ValueError("Missing required parameter 'item_id'.")
143
-
144
139
  url = f"{self.base_url}/me/drive/items/{item_id}"
145
140
  response = self._delete(url)
146
141
  return self._handle_response(response)
147
142
 
148
- def download_file(self, item_id: str) -> dict[str, Any]:
143
+ async def download_file(self, item_id: str) -> dict[str, Any]:
149
144
  """
150
145
  Retrieves a temporary, pre-authenticated download URL for a specific file using its item ID. This function provides a link for subsequent download, differing from `get_document_content` which directly fetches the file's raw content. The URL is returned within a dictionary.
151
146
 
@@ -160,7 +155,6 @@ class SharepointApp(APIApplication):
160
155
  """
161
156
  if not item_id:
162
157
  raise ValueError("Missing required parameter 'item_id'.")
163
-
164
158
  url = f"{self.base_url}/me/drive/items/{item_id}"
165
159
  response = self._get(url)
166
160
  metadata = self._handle_response(response)
@@ -169,7 +163,7 @@ class SharepointApp(APIApplication):
169
163
  raise ValueError("Could not retrieve download URL for the item.")
170
164
  return {"download_url": download_url}
171
165
 
172
- def upload_file(self, file_path: str, parent_id: str = "root", file_name: str | None = None) -> dict[str, Any]:
166
+ async def upload_file(self, file_path: str, parent_id: str = "root", file_name: str | None = None) -> dict[str, Any]:
173
167
  """
174
168
  Uploads a local binary file (under 4MB) from a given path to a specified SharePoint folder. Unlike `upload_text_file`, which uploads string content, this function reads from the filesystem. The destination filename can be customized, and it returns the new file's metadata upon completion.
175
169
 
@@ -186,17 +180,15 @@ class SharepointApp(APIApplication):
186
180
  """
187
181
  if not os.path.exists(file_path):
188
182
  raise FileNotFoundError(f"The file was not found at path: {file_path}")
189
-
190
183
  if not file_name:
191
184
  file_name = os.path.basename(file_path)
192
-
193
185
  url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
194
186
  with open(file_path, "rb") as f:
195
187
  data = f.read()
196
188
  response = self._put(url, data=data, content_type="application/octet-stream")
197
189
  return self._handle_response(response)
198
190
 
199
- def list_folders(self, item_id: str = "root") -> dict[str, Any]:
191
+ async def list_folders(self, item_id: str = "root") -> dict[str, Any]:
200
192
  """
201
193
  Retrieves a list of only the folders within a specified parent directory in SharePoint. Unlike `_list_drive_items` which returns all items, this function filters the results to exclude files. Defaults to the root directory if no parent `item_id` is provided.
202
194
 
@@ -213,7 +205,7 @@ class SharepointApp(APIApplication):
213
205
  folders = [item for item in all_items.get("value", []) if "folder" in item]
214
206
  return {"value": folders}
215
207
 
216
- def list_files(self, item_id: str = "root") -> dict[str, Any]:
208
+ async def list_files(self, item_id: str = "root") -> dict[str, Any]:
217
209
  """
218
210
  Retrieves a list of files within a specified SharePoint folder, defaulting to the root. Unlike `_list_drive_items` which fetches all items, this function filters the results to exclusively return items identified as files, excluding any subdirectories.
219
211
 
@@ -230,7 +222,7 @@ class SharepointApp(APIApplication):
230
222
  files = [item for item in all_items.get("value", []) if "file" in item]
231
223
  return {"value": files}
232
224
 
233
- def create_folder_and_list(self, name: str, parent_id: str = "root") -> dict[str, Any]:
225
+ async def create_folder_and_list(self, name: str, parent_id: str = "root") -> dict[str, Any]:
234
226
  """
235
227
  Performs a composite action: creates a new folder, then lists all items (files and folders) within that parent directory. This confirms creation by returning the parent's updated contents, distinct from `create_folder` which only returns the new folder's metadata.
236
228
 
@@ -247,7 +239,7 @@ class SharepointApp(APIApplication):
247
239
  self.create_folder(name=name, parent_id=parent_id)
248
240
  return self._list_drive_items(item_id=parent_id)
249
241
 
250
- def upload_text_file(self, content: str, parent_id: str = "root", file_name: str = "new_file.txt") -> dict[str, Any]:
242
+ async def upload_text_file(self, content: str, parent_id: str = "root", file_name: str = "new_file.txt") -> dict[str, Any]:
251
243
  """
252
244
  Creates and uploads a new file to SharePoint directly from a string of text content. Unlike `upload_file`, which requires a local file path, this function is specifically for creating a text file from in-memory string data, with a customizable name and destination folder.
253
245
 
@@ -264,13 +256,12 @@ class SharepointApp(APIApplication):
264
256
  """
265
257
  if not file_name:
266
258
  raise ValueError("File name cannot be empty.")
267
-
268
259
  url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
269
260
  data = content.encode("utf-8")
270
261
  response = self._put(url, data=data, content_type="text/plain")
271
262
  return self._handle_response(response)
272
263
 
273
- def get_document_content(self, item_id: str) -> dict[str, Any]:
264
+ async def get_document_content(self, item_id: str) -> dict[str, Any]:
274
265
  """
275
266
  Retrieves the content of a specific file by its item ID and returns it directly as base64-encoded data. This function is distinct from `download_file`, which only provides a temporary URL for the content, and from `get_item_metadata`, which returns file attributes without the content itself. The function fetches the content by following the file's pre-authenticated download URL.
276
267
 
@@ -289,36 +280,23 @@ class SharepointApp(APIApplication):
289
280
  """
290
281
  if not item_id:
291
282
  raise ValueError("Missing required parameter 'item_id'.")
292
-
293
283
  metadata = self.get_item_metadata(item_id=item_id)
294
284
  file_metadata = metadata.get("file")
295
285
  if not file_metadata:
296
286
  raise ValueError(f"Item with ID '{item_id}' is not a file.")
297
-
298
287
  file_mime_type = file_metadata.get("mimeType", "application/octet-stream")
299
288
  file_name = metadata.get("name")
300
-
301
289
  download_url = metadata.get("@microsoft.graph.downloadUrl")
302
290
  if not download_url:
303
291
  logger.error(f"Could not find @microsoft.graph.downloadUrl in metadata for item {item_id}")
304
292
  raise ValueError("Could not retrieve download URL for the item.")
305
-
306
293
  response = self._get(download_url)
307
-
308
294
  response.raise_for_status()
309
-
310
295
  content = response.content
311
-
312
296
  attachment_type = file_mime_type.split("/")[0] if "/" in file_mime_type else "file"
313
297
  if attachment_type not in ["image", "audio", "video", "text"]:
314
298
  attachment_type = "file"
315
-
316
- return {
317
- "type": attachment_type,
318
- "data": content,
319
- "mime_type": file_mime_type,
320
- "file_name": file_name,
321
- }
299
+ return {"type": attachment_type, "data": content, "mime_type": file_mime_type, "file_name": file_name}
322
300
 
323
301
  def list_tools(self):
324
302
  return [
@@ -335,4 +313,4 @@ class SharepointApp(APIApplication):
335
313
  self.create_folder_and_list,
336
314
  self.upload_text_file,
337
315
  self.get_document_content,
338
- ]
316
+ ]