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,76 +1,52 @@
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
- @property
19
- def serpapi_api_key(self) -> str:
16
+ async def get_serpapi_api_key(self) -> str:
20
17
  """
21
18
  A property that lazily retrieves the SerpApi API key from the integration and caches it for future use. It fetches credentials on first access, raising a `NotAuthorizedError` if the key is missing. Subsequent calls efficiently return the cached key.
22
19
  """
23
20
  if self._serpapi_api_key is None:
24
21
  if not self.integration:
25
22
  logger.error("SerpApi App: Integration not configured.")
26
- raise NotAuthorizedError(
27
- "Integration not configured for SerpApi App. Cannot retrieve API key."
28
- )
29
-
23
+ raise NotAuthorizedError("Integration not configured for SerpApi App. Cannot retrieve API key.")
30
24
  try:
31
- credentials = self.integration.get_credentials()
25
+ credentials = await self.integration.get_credentials_async()
32
26
  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
27
+ logger.error(f"SerpApi App: Authorization error when fetching credentials: {e.message}")
28
+ raise
37
29
  except Exception as e:
38
- logger.error(
39
- f"SerpApi App: Unexpected error when fetching credentials: {e}",
40
- exc_info=True,
41
- )
30
+ logger.error(f"SerpApi App: Unexpected error when fetching credentials: {e}", exc_info=True)
42
31
  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
-
32
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
50
33
  if not api_key:
51
34
  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
- ):
35
+ action_message = (
36
+ "API key for SerpApi is missing. Please ensure it's set in the store (e.g., SERPAPI_API_KEY in credentials)."
37
+ )
38
+ if hasattr(self.integration, "authorize") and callable(self.integration.authorize):
56
39
  try:
57
40
  auth_details = self.integration.authorize()
58
41
  if isinstance(auth_details, str):
59
42
  action_message = auth_details
60
43
  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
- ):
44
+ action_message = f"Please authorize via: {auth_details['url']}"
45
+ elif isinstance(auth_details, dict) and "message" in auth_details:
67
46
  action_message = auth_details["message"]
68
47
  except Exception as auth_e:
69
- logger.warning(
70
- f"Could not retrieve specific authorization action for SerpApi: {auth_e}"
71
- )
48
+ logger.warning(f"Could not retrieve specific authorization action for SerpApi: {auth_e}")
72
49
  raise NotAuthorizedError(action_message)
73
-
74
50
  self._serpapi_api_key = api_key
75
51
  logger.info("SerpApi API Key successfully retrieved and cached.")
76
52
  return self._serpapi_api_key
@@ -93,27 +69,15 @@ class SerpapiApp(APIApplication):
93
69
  search, async, web-scraping, api, serpapi, important
94
70
  """
95
71
  request_params = params or {}
96
-
97
72
  try:
98
- current_api_key = self.serpapi_api_key # This can raise NotAuthorizedError
73
+ api_key = await self.get_serpapi_api_key()
99
74
  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.
75
+ serpapi_call_params = {"api_key": api_key, "engine": "google_light", **request_params}
109
76
  search_client = SerpApiSearch(serpapi_call_params)
110
77
  data = search_client.get_dict()
111
-
112
- # Check for errors returned in the API response body
113
78
  if "error" in data:
114
79
  error_message = data["error"]
115
80
  logger.error(f"SerpApi API returned an error: {error_message}")
116
- # Keywords indicating authorization/authentication issues
117
81
  auth_error_keywords = [
118
82
  "invalid api key",
119
83
  "authorization failed",
@@ -122,76 +86,42 @@ class SerpapiApp(APIApplication):
122
86
  "account disabled",
123
87
  "private api key is missing",
124
88
  ]
125
- if any(
126
- keyword in error_message.lower() for keyword in auth_error_keywords
127
- ):
89
+ if any((keyword in error_message.lower() for keyword in auth_error_keywords)):
128
90
  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
91
+ return f"SerpApi API Error: {error_message}"
132
92
  if "organic_results" in data:
133
93
  formatted_results = []
134
94
  for result in data.get("organic_results", []):
135
95
  title = result.get("title", "No title")
136
96
  link = result.get("link", "No link")
137
97
  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
- )
98
+ formatted_results.append(f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n")
99
+ return "\n".join(formatted_results) if formatted_results else "No organic results found."
146
100
  else:
147
101
  return "No organic results found."
148
-
149
- except (
150
- NotAuthorizedError
151
- ): # Catches from self.serpapi_api_key or explicit raise above
102
+ except NotAuthorizedError:
152
103
  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'
104
+ raise
105
+ except httpx.HTTPStatusError as e:
156
106
  logger.warning(
157
107
  f"SerpApi search encountered httpx.HTTPStatusError (unexpected with default SerpApiClient): {e.response.status_code}",
158
108
  exc_info=True,
159
109
  )
160
110
  if e.response.status_code == 429:
161
111
  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
- )
112
+ elif e.response.status_code == 401:
113
+ raise NotAuthorizedError("Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY.")
168
114
  else:
169
115
  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
116
+ except Exception as e:
172
117
  error_message_lower = str(e).lower()
173
118
  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
- )
119
+ auth_error_keywords = ["authentication", "api key", "unauthorized", "401", "forbidden", "invalid key"]
120
+ if any((keyword in error_message_lower for keyword in auth_error_keywords)):
121
+ raise NotAuthorizedError(f"SerpApi authentication/authorization failed: {str(e)}")
187
122
  return f"An unexpected error occurred during search: {str(e)}"
188
123
 
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]:
124
+ async def google_maps_search(self, q: str | None = None, ll: str | None = None, place_id: str | None = None) -> dict[str, Any]:
195
125
  """
196
126
  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
127
 
@@ -210,43 +140,24 @@ class SerpapiApp(APIApplication):
210
140
  Tags:
211
141
  google-maps, search, location, places, important
212
142
  """
213
-
214
143
  query_params = {}
215
- query_params = {
216
- "engine": "google_maps",
217
- "api_key": self.serpapi_api_key,
218
- }
219
-
144
+ api_key = await self.get_serpapi_api_key()
145
+ query_params = {"engine": "google_maps", "api_key": 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 = await self._aget(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,15 @@ 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
+ api_key = await self.get_serpapi_api_key()
180
+ query_params = {"engine": "google_maps_reviews", "data_id": data_id, "api_key": api_key}
275
181
  if hl is not None:
276
182
  query_params["hl"] = hl
277
183
  else:
278
184
  query_params["hl"] = "en"
279
-
280
- response = self._get(
281
- self.base_url,
282
- params=query_params,
283
- )
185
+ response = await self._aget(self.base_url, params=query_params)
284
186
  return self._handle_response(response)
285
187
 
286
188
  def list_tools(self) -> list[callable]:
287
- return [
288
- self.web_search,
289
- self.google_maps_search,
290
- self.get_google_maps_reviews,
291
- ]
189
+ 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
 
@@ -32,10 +31,10 @@ class SharepointApp(APIApplication):
32
31
  """
33
32
  url = f"{self.base_url}/me"
34
33
  query_params = {"$select": "id,userPrincipalName"}
35
- response = self._get(url, params=query_params)
34
+ response = await self._aget(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
 
@@ -46,7 +45,7 @@ class SharepointApp(APIApplication):
46
45
  drive, storage, quota, info
47
46
  """
48
47
  url = f"{self.base_url}/me/drive"
49
- response = self._get(url)
48
+ response = await self._aget(url)
50
49
  return self._handle_response(response)
51
50
 
52
51
  def _list_drive_items(self, item_id: str = "root") -> dict[str, Any]:
@@ -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
- response = self._get(url)
81
+ response = await self._aget(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
- response = self._get(url)
100
+ response = await self._aget(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
- response = self._post(url, data=data)
121
+ response = await self._apost(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
- response = self._delete(url)
140
+ response = await self._adelete(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,16 +155,15 @@ 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
- response = self._get(url)
159
+ response = await self._aget(url)
166
160
  metadata = self._handle_response(response)
167
161
  download_url = metadata.get("@microsoft.graph.downloadUrl")
168
162
  if not download_url:
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
- response = self._put(url, data=data, content_type="application/octet-stream")
188
+ response = await self._aput(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
 
@@ -244,10 +236,10 @@ class SharepointApp(APIApplication):
244
236
  Tags:
245
237
  create, folder, list, important
246
238
  """
247
- self.create_folder(name=name, parent_id=parent_id)
239
+ await 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
- response = self._put(url, data=data, content_type="text/plain")
261
+ response = await self._aput(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
- metadata = self.get_item_metadata(item_id=item_id)
283
+ metadata = await 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
- response = self._get(download_url)
307
-
293
+ response = await self._aget(download_url)
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
+ ]