universal-mcp-applications 0.1.1__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.
Files changed (268) hide show
  1. universal_mcp/applications/ahrefs/README.md +51 -0
  2. universal_mcp/applications/ahrefs/__init__.py +1 -0
  3. universal_mcp/applications/ahrefs/app.py +2291 -0
  4. universal_mcp/applications/airtable/README.md +22 -0
  5. universal_mcp/applications/airtable/__init__.py +1 -0
  6. universal_mcp/applications/airtable/app.py +479 -0
  7. universal_mcp/applications/apollo/README.md +44 -0
  8. universal_mcp/applications/apollo/__init__.py +1 -0
  9. universal_mcp/applications/apollo/app.py +1847 -0
  10. universal_mcp/applications/asana/README.md +199 -0
  11. universal_mcp/applications/asana/__init__.py +1 -0
  12. universal_mcp/applications/asana/app.py +9509 -0
  13. universal_mcp/applications/aws-s3/README.md +0 -0
  14. universal_mcp/applications/aws-s3/__init__.py +1 -0
  15. universal_mcp/applications/aws-s3/app.py +552 -0
  16. universal_mcp/applications/bill/README.md +0 -0
  17. universal_mcp/applications/bill/__init__.py +1 -0
  18. universal_mcp/applications/bill/app.py +8705 -0
  19. universal_mcp/applications/box/README.md +307 -0
  20. universal_mcp/applications/box/__init__.py +1 -0
  21. universal_mcp/applications/box/app.py +15987 -0
  22. universal_mcp/applications/braze/README.md +106 -0
  23. universal_mcp/applications/braze/__init__.py +1 -0
  24. universal_mcp/applications/braze/app.py +4754 -0
  25. universal_mcp/applications/cal-com-v2/README.md +150 -0
  26. universal_mcp/applications/cal-com-v2/__init__.py +1 -0
  27. universal_mcp/applications/cal-com-v2/app.py +5541 -0
  28. universal_mcp/applications/calendly/README.md +53 -0
  29. universal_mcp/applications/calendly/__init__.py +1 -0
  30. universal_mcp/applications/calendly/app.py +1436 -0
  31. universal_mcp/applications/canva/README.md +43 -0
  32. universal_mcp/applications/canva/__init__.py +1 -0
  33. universal_mcp/applications/canva/app.py +941 -0
  34. universal_mcp/applications/clickup/README.md +135 -0
  35. universal_mcp/applications/clickup/__init__.py +1 -0
  36. universal_mcp/applications/clickup/app.py +5009 -0
  37. universal_mcp/applications/coda/README.md +108 -0
  38. universal_mcp/applications/coda/__init__.py +1 -0
  39. universal_mcp/applications/coda/app.py +3671 -0
  40. universal_mcp/applications/confluence/README.md +198 -0
  41. universal_mcp/applications/confluence/__init__.py +1 -0
  42. universal_mcp/applications/confluence/app.py +6273 -0
  43. universal_mcp/applications/contentful/README.md +17 -0
  44. universal_mcp/applications/contentful/__init__.py +1 -0
  45. universal_mcp/applications/contentful/app.py +364 -0
  46. universal_mcp/applications/crustdata/README.md +25 -0
  47. universal_mcp/applications/crustdata/__init__.py +1 -0
  48. universal_mcp/applications/crustdata/app.py +586 -0
  49. universal_mcp/applications/dialpad/README.md +202 -0
  50. universal_mcp/applications/dialpad/__init__.py +1 -0
  51. universal_mcp/applications/dialpad/app.py +5949 -0
  52. universal_mcp/applications/digitalocean/README.md +463 -0
  53. universal_mcp/applications/digitalocean/__init__.py +1 -0
  54. universal_mcp/applications/digitalocean/app.py +20835 -0
  55. universal_mcp/applications/domain-checker/README.md +13 -0
  56. universal_mcp/applications/domain-checker/__init__.py +1 -0
  57. universal_mcp/applications/domain-checker/app.py +265 -0
  58. universal_mcp/applications/e2b/README.md +12 -0
  59. universal_mcp/applications/e2b/__init__.py +1 -0
  60. universal_mcp/applications/e2b/app.py +187 -0
  61. universal_mcp/applications/elevenlabs/README.md +88 -0
  62. universal_mcp/applications/elevenlabs/__init__.py +1 -0
  63. universal_mcp/applications/elevenlabs/app.py +3235 -0
  64. universal_mcp/applications/exa/README.md +15 -0
  65. universal_mcp/applications/exa/__init__.py +1 -0
  66. universal_mcp/applications/exa/app.py +221 -0
  67. universal_mcp/applications/falai/README.md +17 -0
  68. universal_mcp/applications/falai/__init__.py +1 -0
  69. universal_mcp/applications/falai/app.py +331 -0
  70. universal_mcp/applications/figma/README.md +49 -0
  71. universal_mcp/applications/figma/__init__.py +1 -0
  72. universal_mcp/applications/figma/app.py +1090 -0
  73. universal_mcp/applications/firecrawl/README.md +20 -0
  74. universal_mcp/applications/firecrawl/__init__.py +1 -0
  75. universal_mcp/applications/firecrawl/app.py +514 -0
  76. universal_mcp/applications/fireflies/README.md +25 -0
  77. universal_mcp/applications/fireflies/__init__.py +1 -0
  78. universal_mcp/applications/fireflies/app.py +506 -0
  79. universal_mcp/applications/fpl/README.md +23 -0
  80. universal_mcp/applications/fpl/__init__.py +1 -0
  81. universal_mcp/applications/fpl/app.py +1327 -0
  82. universal_mcp/applications/fpl/utils/api.py +142 -0
  83. universal_mcp/applications/fpl/utils/fixtures.py +629 -0
  84. universal_mcp/applications/fpl/utils/helper.py +982 -0
  85. universal_mcp/applications/fpl/utils/league_utils.py +546 -0
  86. universal_mcp/applications/fpl/utils/position_utils.py +68 -0
  87. universal_mcp/applications/ghost-content/README.md +25 -0
  88. universal_mcp/applications/ghost-content/__init__.py +1 -0
  89. universal_mcp/applications/ghost-content/app.py +654 -0
  90. universal_mcp/applications/github/README.md +1049 -0
  91. universal_mcp/applications/github/__init__.py +1 -0
  92. universal_mcp/applications/github/app.py +50600 -0
  93. universal_mcp/applications/gong/README.md +63 -0
  94. universal_mcp/applications/gong/__init__.py +1 -0
  95. universal_mcp/applications/gong/app.py +2297 -0
  96. universal_mcp/applications/google-ads/README.md +0 -0
  97. universal_mcp/applications/google-ads/__init__.py +1 -0
  98. universal_mcp/applications/google-ads/app.py +23 -0
  99. universal_mcp/applications/google-calendar/README.md +21 -0
  100. universal_mcp/applications/google-calendar/__init__.py +1 -0
  101. universal_mcp/applications/google-calendar/app.py +574 -0
  102. universal_mcp/applications/google-docs/README.md +25 -0
  103. universal_mcp/applications/google-docs/__init__.py +1 -0
  104. universal_mcp/applications/google-docs/app.py +760 -0
  105. universal_mcp/applications/google-drive/README.md +68 -0
  106. universal_mcp/applications/google-drive/__init__.py +1 -0
  107. universal_mcp/applications/google-drive/app.py +4936 -0
  108. universal_mcp/applications/google-gemini/README.md +25 -0
  109. universal_mcp/applications/google-gemini/__init__.py +1 -0
  110. universal_mcp/applications/google-gemini/app.py +663 -0
  111. universal_mcp/applications/google-mail/README.md +31 -0
  112. universal_mcp/applications/google-mail/__init__.py +1 -0
  113. universal_mcp/applications/google-mail/app.py +1354 -0
  114. universal_mcp/applications/google-searchconsole/README.md +21 -0
  115. universal_mcp/applications/google-searchconsole/__init__.py +1 -0
  116. universal_mcp/applications/google-searchconsole/app.py +320 -0
  117. universal_mcp/applications/google-sheet/README.md +36 -0
  118. universal_mcp/applications/google-sheet/__init__.py +1 -0
  119. universal_mcp/applications/google-sheet/app.py +1941 -0
  120. universal_mcp/applications/hashnode/README.md +20 -0
  121. universal_mcp/applications/hashnode/__init__.py +1 -0
  122. universal_mcp/applications/hashnode/app.py +455 -0
  123. universal_mcp/applications/heygen/README.md +44 -0
  124. universal_mcp/applications/heygen/__init__.py +1 -0
  125. universal_mcp/applications/heygen/app.py +961 -0
  126. universal_mcp/applications/http-tools/README.md +16 -0
  127. universal_mcp/applications/http-tools/__init__.py +1 -0
  128. universal_mcp/applications/http-tools/app.py +153 -0
  129. universal_mcp/applications/hubspot/README.md +239 -0
  130. universal_mcp/applications/hubspot/__init__.py +1 -0
  131. universal_mcp/applications/hubspot/app.py +416 -0
  132. universal_mcp/applications/jira/README.md +600 -0
  133. universal_mcp/applications/jira/__init__.py +1 -0
  134. universal_mcp/applications/jira/app.py +28804 -0
  135. universal_mcp/applications/klaviyo/README.md +313 -0
  136. universal_mcp/applications/klaviyo/__init__.py +1 -0
  137. universal_mcp/applications/klaviyo/app.py +11236 -0
  138. universal_mcp/applications/linkedin/README.md +15 -0
  139. universal_mcp/applications/linkedin/__init__.py +1 -0
  140. universal_mcp/applications/linkedin/app.py +243 -0
  141. universal_mcp/applications/mailchimp/README.md +281 -0
  142. universal_mcp/applications/mailchimp/__init__.py +1 -0
  143. universal_mcp/applications/mailchimp/app.py +10937 -0
  144. universal_mcp/applications/markitdown/README.md +12 -0
  145. universal_mcp/applications/markitdown/__init__.py +1 -0
  146. universal_mcp/applications/markitdown/app.py +63 -0
  147. universal_mcp/applications/miro/README.md +151 -0
  148. universal_mcp/applications/miro/__init__.py +1 -0
  149. universal_mcp/applications/miro/app.py +5429 -0
  150. universal_mcp/applications/ms-teams/README.md +42 -0
  151. universal_mcp/applications/ms-teams/__init__.py +1 -0
  152. universal_mcp/applications/ms-teams/app.py +1823 -0
  153. universal_mcp/applications/neon/README.md +74 -0
  154. universal_mcp/applications/neon/__init__.py +1 -0
  155. universal_mcp/applications/neon/app.py +2018 -0
  156. universal_mcp/applications/notion/README.md +30 -0
  157. universal_mcp/applications/notion/__init__.py +1 -0
  158. universal_mcp/applications/notion/app.py +527 -0
  159. universal_mcp/applications/openai/README.md +22 -0
  160. universal_mcp/applications/openai/__init__.py +1 -0
  161. universal_mcp/applications/openai/app.py +759 -0
  162. universal_mcp/applications/outlook/README.md +20 -0
  163. universal_mcp/applications/outlook/__init__.py +1 -0
  164. universal_mcp/applications/outlook/app.py +444 -0
  165. universal_mcp/applications/perplexity/README.md +12 -0
  166. universal_mcp/applications/perplexity/__init__.py +1 -0
  167. universal_mcp/applications/perplexity/app.py +65 -0
  168. universal_mcp/applications/pipedrive/README.md +284 -0
  169. universal_mcp/applications/pipedrive/__init__.py +1 -0
  170. universal_mcp/applications/pipedrive/app.py +12924 -0
  171. universal_mcp/applications/posthog/README.md +132 -0
  172. universal_mcp/applications/posthog/__init__.py +1 -0
  173. universal_mcp/applications/posthog/app.py +7125 -0
  174. universal_mcp/applications/reddit/README.md +135 -0
  175. universal_mcp/applications/reddit/__init__.py +1 -0
  176. universal_mcp/applications/reddit/app.py +4652 -0
  177. universal_mcp/applications/replicate/README.md +18 -0
  178. universal_mcp/applications/replicate/__init__.py +1 -0
  179. universal_mcp/applications/replicate/app.py +495 -0
  180. universal_mcp/applications/resend/README.md +40 -0
  181. universal_mcp/applications/resend/__init__.py +1 -0
  182. universal_mcp/applications/resend/app.py +881 -0
  183. universal_mcp/applications/retell/README.md +21 -0
  184. universal_mcp/applications/retell/__init__.py +1 -0
  185. universal_mcp/applications/retell/app.py +333 -0
  186. universal_mcp/applications/rocketlane/README.md +70 -0
  187. universal_mcp/applications/rocketlane/__init__.py +1 -0
  188. universal_mcp/applications/rocketlane/app.py +4346 -0
  189. universal_mcp/applications/semanticscholar/README.md +25 -0
  190. universal_mcp/applications/semanticscholar/__init__.py +1 -0
  191. universal_mcp/applications/semanticscholar/app.py +482 -0
  192. universal_mcp/applications/semrush/README.md +44 -0
  193. universal_mcp/applications/semrush/__init__.py +1 -0
  194. universal_mcp/applications/semrush/app.py +2081 -0
  195. universal_mcp/applications/sendgrid/README.md +362 -0
  196. universal_mcp/applications/sendgrid/__init__.py +1 -0
  197. universal_mcp/applications/sendgrid/app.py +9752 -0
  198. universal_mcp/applications/sentry/README.md +186 -0
  199. universal_mcp/applications/sentry/__init__.py +1 -0
  200. universal_mcp/applications/sentry/app.py +7471 -0
  201. universal_mcp/applications/serpapi/README.md +14 -0
  202. universal_mcp/applications/serpapi/__init__.py +1 -0
  203. universal_mcp/applications/serpapi/app.py +293 -0
  204. universal_mcp/applications/sharepoint/README.md +0 -0
  205. universal_mcp/applications/sharepoint/__init__.py +1 -0
  206. universal_mcp/applications/sharepoint/app.py +215 -0
  207. universal_mcp/applications/shopify/README.md +321 -0
  208. universal_mcp/applications/shopify/__init__.py +1 -0
  209. universal_mcp/applications/shopify/app.py +15392 -0
  210. universal_mcp/applications/shortcut/README.md +128 -0
  211. universal_mcp/applications/shortcut/__init__.py +1 -0
  212. universal_mcp/applications/shortcut/app.py +4478 -0
  213. universal_mcp/applications/slack/README.md +0 -0
  214. universal_mcp/applications/slack/__init__.py +1 -0
  215. universal_mcp/applications/slack/app.py +570 -0
  216. universal_mcp/applications/spotify/README.md +91 -0
  217. universal_mcp/applications/spotify/__init__.py +1 -0
  218. universal_mcp/applications/spotify/app.py +2526 -0
  219. universal_mcp/applications/supabase/README.md +87 -0
  220. universal_mcp/applications/supabase/__init__.py +1 -0
  221. universal_mcp/applications/supabase/app.py +2970 -0
  222. universal_mcp/applications/tavily/README.md +12 -0
  223. universal_mcp/applications/tavily/__init__.py +1 -0
  224. universal_mcp/applications/tavily/app.py +51 -0
  225. universal_mcp/applications/trello/README.md +266 -0
  226. universal_mcp/applications/trello/__init__.py +1 -0
  227. universal_mcp/applications/trello/app.py +10875 -0
  228. universal_mcp/applications/twillo/README.md +0 -0
  229. universal_mcp/applications/twillo/__init__.py +1 -0
  230. universal_mcp/applications/twillo/app.py +269 -0
  231. universal_mcp/applications/twitter/README.md +100 -0
  232. universal_mcp/applications/twitter/__init__.py +1 -0
  233. universal_mcp/applications/twitter/api_segments/__init__.py +0 -0
  234. universal_mcp/applications/twitter/api_segments/api_segment_base.py +51 -0
  235. universal_mcp/applications/twitter/api_segments/compliance_api.py +122 -0
  236. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +255 -0
  237. universal_mcp/applications/twitter/api_segments/dm_events_api.py +140 -0
  238. universal_mcp/applications/twitter/api_segments/likes_api.py +159 -0
  239. universal_mcp/applications/twitter/api_segments/lists_api.py +395 -0
  240. universal_mcp/applications/twitter/api_segments/openapi_json_api.py +34 -0
  241. universal_mcp/applications/twitter/api_segments/spaces_api.py +309 -0
  242. universal_mcp/applications/twitter/api_segments/trends_api.py +40 -0
  243. universal_mcp/applications/twitter/api_segments/tweets_api.py +1403 -0
  244. universal_mcp/applications/twitter/api_segments/usage_api.py +40 -0
  245. universal_mcp/applications/twitter/api_segments/users_api.py +1498 -0
  246. universal_mcp/applications/twitter/app.py +46 -0
  247. universal_mcp/applications/unipile/README.md +28 -0
  248. universal_mcp/applications/unipile/__init__.py +1 -0
  249. universal_mcp/applications/unipile/app.py +829 -0
  250. universal_mcp/applications/whatsapp/README.md +23 -0
  251. universal_mcp/applications/whatsapp/__init__.py +1 -0
  252. universal_mcp/applications/whatsapp/app.py +595 -0
  253. universal_mcp/applications/whatsapp-business/README.md +34 -0
  254. universal_mcp/applications/whatsapp-business/__init__.py +1 -0
  255. universal_mcp/applications/whatsapp-business/app.py +1065 -0
  256. universal_mcp/applications/wrike/README.md +46 -0
  257. universal_mcp/applications/wrike/__init__.py +1 -0
  258. universal_mcp/applications/wrike/app.py +1583 -0
  259. universal_mcp/applications/youtube/README.md +57 -0
  260. universal_mcp/applications/youtube/__init__.py +1 -0
  261. universal_mcp/applications/youtube/app.py +1696 -0
  262. universal_mcp/applications/zenquotes/README.md +12 -0
  263. universal_mcp/applications/zenquotes/__init__.py +1 -0
  264. universal_mcp/applications/zenquotes/app.py +31 -0
  265. universal_mcp_applications-0.1.1.dist-info/METADATA +172 -0
  266. universal_mcp_applications-0.1.1.dist-info/RECORD +268 -0
  267. universal_mcp_applications-0.1.1.dist-info/WHEEL +4 -0
  268. universal_mcp_applications-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,14 @@
1
+ # SerpapiApp MCP Server
2
+
3
+ An MCP Server for the SerpapiApp API.
4
+
5
+ ## 🛠️ Tool List
6
+
7
+ This is automatically generated from OpenAPI schema for the SerpapiApp API.
8
+
9
+
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `search` | Performs a search using the SerpApi service and returns formatted search results. |
13
+ | `google_maps_search` | Performs a Google Maps search using the SerpApi service and returns formatted search results. |
14
+ | `get_google_maps_reviews` | Retrieves Google Maps reviews for a specific place using the SerpApi service. |
@@ -0,0 +1 @@
1
+ from .app import SerpapiApp
@@ -0,0 +1,293 @@
1
+ from typing import Any # For type hinting
2
+
3
+ import httpx
4
+ from loguru import logger
5
+ 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
10
+
11
+
12
+ class SerpapiApp(APIApplication):
13
+ def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
14
+ super().__init__(name="serpapi", integration=integration, **kwargs)
15
+ self._serpapi_api_key: str | None = None # Cache for the API key
16
+ self.base_url = "https://serpapi.com/search"
17
+
18
+ @property
19
+ def serpapi_api_key(self) -> str:
20
+ """
21
+ Retrieves and caches the SerpApi API key from the integration.
22
+ Raises NotAuthorizedError if the key cannot be obtained.
23
+ """
24
+ if self._serpapi_api_key is None:
25
+ if not self.integration:
26
+ logger.error("SerpApi App: Integration not configured.")
27
+ raise NotAuthorizedError(
28
+ "Integration not configured for SerpApi App. Cannot retrieve API key."
29
+ )
30
+
31
+ try:
32
+ credentials = self.integration.get_credentials()
33
+ except NotAuthorizedError as e:
34
+ logger.error(
35
+ f"SerpApi App: Authorization error when fetching credentials: {e.message}"
36
+ )
37
+ raise # Re-raise the original NotAuthorizedError
38
+ except Exception as e:
39
+ logger.error(
40
+ f"SerpApi App: Unexpected error when fetching credentials: {e}",
41
+ exc_info=True,
42
+ )
43
+ raise NotAuthorizedError(f"Failed to get SerpApi credentials: {e}")
44
+
45
+ api_key = (
46
+ credentials.get("api_key")
47
+ or credentials.get("API_KEY") # Check common variations
48
+ or credentials.get("apiKey")
49
+ )
50
+
51
+ if not api_key:
52
+ logger.error("SerpApi App: API key not found in credentials.")
53
+ action_message = "API key for SerpApi is missing. Please ensure it's set in the store (e.g., SERPAPI_API_KEY in credentials)."
54
+ if hasattr(self.integration, "authorize") and callable(
55
+ self.integration.authorize
56
+ ):
57
+ try:
58
+ auth_details = self.integration.authorize()
59
+ if isinstance(auth_details, str):
60
+ action_message = auth_details
61
+ elif isinstance(auth_details, dict) and "url" in auth_details:
62
+ action_message = (
63
+ f"Please authorize via: {auth_details['url']}"
64
+ )
65
+ elif (
66
+ isinstance(auth_details, dict) and "message" in auth_details
67
+ ):
68
+ action_message = auth_details["message"]
69
+ except Exception as auth_e:
70
+ logger.warning(
71
+ f"Could not retrieve specific authorization action for SerpApi: {auth_e}"
72
+ )
73
+ raise NotAuthorizedError(action_message)
74
+
75
+ self._serpapi_api_key = api_key
76
+ logger.info("SerpApi API Key successfully retrieved and cached.")
77
+ return self._serpapi_api_key
78
+
79
+ async def search(self, params: dict[str, Any] | None = None) -> str:
80
+ """
81
+ Performs a search using the SerpApi service and returns formatted search results.
82
+ Note: The underlying SerpApiSearch().get_dict() call is synchronous.
83
+
84
+ Args:
85
+ params: Dictionary of engine-specific parameters (e.g., {'q': 'Coffee', 'engine': 'google_light', 'location': 'Austin, TX'}). Defaults to None.
86
+
87
+ Returns:
88
+ A formatted string containing search results with titles, links, and snippets, or an error message if the search fails.
89
+
90
+ Raises:
91
+ NotAuthorizedError: If the API key cannot be retrieved or is invalid/rejected by SerpApi.
92
+ Exception: For other unexpected errors during the search process. (Specific HTTP errors or SerpApiErrors are caught and returned as strings or raise NotAuthorizedError).
93
+
94
+ Tags:
95
+ search, async, web-scraping, api, serpapi, important
96
+ """
97
+ request_params = params or {}
98
+
99
+ try:
100
+ current_api_key = self.serpapi_api_key # This can raise NotAuthorizedError
101
+ logger.info("Attempting SerpApi search.")
102
+
103
+ serpapi_call_params = {
104
+ "api_key": current_api_key,
105
+ "engine": "google_light", # Fastest engine by default
106
+ **request_params, # Include any additional parameters from the user
107
+ }
108
+
109
+ # SerpApiSearch (SerpApiClient) uses the 'requests' library and its get_dict() is synchronous.
110
+ # If true async behavior is needed, this call should be wrapped with asyncio.to_thread.
111
+ search_client = SerpApiSearch(serpapi_call_params)
112
+ data = search_client.get_dict()
113
+
114
+ # Check for errors returned in the API response body
115
+ if "error" in data:
116
+ error_message = data["error"]
117
+ logger.error(f"SerpApi API returned an error: {error_message}")
118
+ # Keywords indicating authorization/authentication issues
119
+ auth_error_keywords = [
120
+ "invalid api key",
121
+ "authorization failed",
122
+ "api key needed",
123
+ "forbidden",
124
+ "account disabled",
125
+ "private api key is missing",
126
+ ]
127
+ if any(
128
+ keyword in error_message.lower() for keyword in auth_error_keywords
129
+ ):
130
+ raise NotAuthorizedError(f"SerpApi Error: {error_message}")
131
+ return f"SerpApi API Error: {error_message}" # Other API errors (e.g., missing parameters)
132
+
133
+ # Process organic search results if available
134
+ if "organic_results" in data:
135
+ formatted_results = []
136
+ for result in data.get("organic_results", []):
137
+ title = result.get("title", "No title")
138
+ link = result.get("link", "No link")
139
+ snippet = result.get("snippet", "No snippet")
140
+ formatted_results.append(
141
+ f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n"
142
+ )
143
+ return (
144
+ "\n".join(formatted_results)
145
+ if formatted_results
146
+ else "No organic results found."
147
+ )
148
+ else:
149
+ return "No organic results found."
150
+
151
+ except (
152
+ NotAuthorizedError
153
+ ): # Catches from self.serpapi_api_key or explicit raise above
154
+ logger.error("SerpApi search failed due to an authorization error.")
155
+ raise # Re-raise to be handled by the MCP framework
156
+
157
+ except httpx.HTTPStatusError as e: # Kept from original for robustness, though SerpApiClient uses 'requests'
158
+ logger.warning(
159
+ f"SerpApi search encountered httpx.HTTPStatusError (unexpected with default SerpApiClient): {e.response.status_code}",
160
+ exc_info=True,
161
+ )
162
+ if e.response.status_code == 429:
163
+ return "Error: Rate limit exceeded (HTTP 429). Please try again later."
164
+ elif (
165
+ e.response.status_code == 401
166
+ ): # Key was fetched but rejected by API with HTTP 401
167
+ raise NotAuthorizedError(
168
+ "Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY."
169
+ )
170
+ else:
171
+ return f"HTTP Error: {e.response.status_code} - {e.response.text}"
172
+
173
+ except Exception as e: # General catch-all, similar to E2B's final catch
174
+ error_message_lower = str(e).lower()
175
+ logger.error(f"Unexpected error during SerpApi search: {e}", exc_info=True)
176
+ # Infer auth error from generic exception message
177
+ auth_error_keywords = [
178
+ "authentication",
179
+ "api key",
180
+ "unauthorized",
181
+ "401",
182
+ "forbidden",
183
+ "invalid key",
184
+ ]
185
+ if any(keyword in error_message_lower for keyword in auth_error_keywords):
186
+ raise NotAuthorizedError(
187
+ f"SerpApi authentication/authorization failed: {str(e)}"
188
+ )
189
+ return f"An unexpected error occurred during search: {str(e)}"
190
+
191
+ async def google_maps_search(
192
+ self,
193
+ q: str | None = None,
194
+ ll: str | None = None,
195
+ place_id: str | None = None,
196
+ ) -> dict[str, Any]:
197
+ """
198
+ Performs a Google Maps search using the SerpApi service and returns formatted search results.
199
+
200
+ Args:
201
+ q (string, optional): The search query for Google Maps (e.g., "Coffee", "Restaurants", "Gas stations").
202
+ ll (string, optional): Latitude and longitude with zoom level in format "@lat,lng,zoom" (e.g., "@40.7455096,-74.0083012,14z"). The zoom attribute ranges from 3z (map completely zoomed out) to 21z (map completely zoomed in). Results are not guaranteed to be within the requested geographic location.
203
+ place_id (string, optional): The unique reference to a place in Google Maps. Place IDs are available for most locations, including businesses, landmarks, parks, and intersections. You can find the place_id using our Google Maps API. place_id can be used without any other optional parameter. place_id and data_cid can't be used together.
204
+
205
+ Returns:
206
+ dict[str, Any]: Formatted Google Maps search results with place names, addresses, ratings, and other details.
207
+
208
+ Raises:
209
+ ValueError: Raised when required parameters are missing.
210
+ HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
211
+
212
+ Tags:
213
+ google-maps, search, location, places, important
214
+ """
215
+
216
+ query_params = {}
217
+ query_params = {
218
+ "engine": "google_maps",
219
+ "api_key": self.serpapi_api_key,
220
+ }
221
+
222
+ if q is not None:
223
+ query_params["q"] = q
224
+
225
+ if ll is not None:
226
+ query_params["ll"] = ll
227
+
228
+ if place_id is not None:
229
+ query_params["place_id"] = place_id
230
+
231
+ response = self._get(
232
+ self.base_url,
233
+ params=query_params,
234
+ )
235
+ data = self._handle_response(response)
236
+
237
+ # Add Google Maps URLs for each place in the results
238
+ if "local_results" in data:
239
+ for place in data["local_results"]:
240
+ if "place_id" in place:
241
+ place["google_maps_url"] = (
242
+ f"https://www.google.com/maps/place/?q=place_id:{place['place_id']}"
243
+ )
244
+
245
+ return data
246
+
247
+ async def get_google_maps_reviews(
248
+ self,
249
+ data_id: str,
250
+ hl: str | None = None,
251
+ ) -> dict[str, Any]:
252
+ """
253
+ Retrieves Google Maps reviews for a specific place using the SerpApi service.
254
+
255
+ Args:
256
+ data_id (string): The data ID of the place to get reviews for (e.g., "0x89c259af336b3341:0xa4969e07ce3108de").
257
+ hl (string, optional): Language parameter for the search results. Defaults to "en".
258
+
259
+ Returns:
260
+ dict[str, Any]: Google Maps reviews data with ratings, comments, and other review details.
261
+
262
+ Raises:
263
+ ValueError: Raised when required parameters are missing.
264
+ HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
265
+
266
+ Tags:
267
+ google-maps, reviews, ratings, places, important
268
+ """
269
+
270
+ query_params = {}
271
+ query_params = {
272
+ "engine": "google_maps_reviews",
273
+ "data_id": data_id,
274
+ "api_key": self.serpapi_api_key,
275
+ }
276
+
277
+ if hl is not None:
278
+ query_params["hl"] = hl
279
+ else:
280
+ query_params["hl"] = "en"
281
+
282
+ response = self._get(
283
+ self.base_url,
284
+ params=query_params,
285
+ )
286
+ return self._handle_response(response)
287
+
288
+ def list_tools(self) -> list[callable]:
289
+ return [
290
+ self.search,
291
+ self.google_maps_search,
292
+ self.get_google_maps_reviews,
293
+ ]
File without changes
@@ -0,0 +1 @@
1
+ from .app import SharepointApp
@@ -0,0 +1,215 @@
1
+ import base64
2
+ import io
3
+ from datetime import datetime
4
+ from io import BytesIO
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+ from office365.graph_client import GraphClient
9
+ from universal_mcp.applications import BaseApplication
10
+ from universal_mcp.integrations import Integration
11
+
12
+
13
+ def _to_iso_optional(dt_obj: datetime | None) -> str | None:
14
+ """Converts a datetime object to ISO format string, or returns None if the object is None."""
15
+ if dt_obj is not None:
16
+ return dt_obj.isoformat()
17
+ return None
18
+
19
+
20
+ class SharepointApp(BaseApplication):
21
+ """
22
+ Base class for Universal MCP Applications.
23
+ """
24
+
25
+ def __init__(self, integration: Integration = None, client=None, **kwargs) -> None:
26
+ """Initializes the SharepointApp.
27
+ Args:
28
+ client (GraphClient | None, optional): An existing GraphClient instance. If None, a new client will be created on first use.
29
+ """
30
+ super().__init__(name="sharepoint", integration=integration, **kwargs)
31
+ self._client = client
32
+ self.integration = integration
33
+ self._site_url = None
34
+
35
+ @property
36
+ def client(self):
37
+ """Gets the GraphClient instance, initializing it if necessary.
38
+
39
+ Returns:
40
+ GraphClient: The authenticated GraphClient instance.
41
+ """
42
+ if not self.integration:
43
+ raise ValueError("Integration is required")
44
+ credentials = self.integration.get_credentials()
45
+ if not credentials:
46
+ raise ValueError("No credentials found")
47
+
48
+ if not credentials.get("access_token"):
49
+ raise ValueError("No access token found")
50
+
51
+ def acquire_token():
52
+ access_token = credentials.get("access_token")
53
+ refresh_token = credentials.get("refresh_token")
54
+ return {
55
+ "access_token": access_token,
56
+ "refresh_token": refresh_token,
57
+ "token_type": "Bearer",
58
+ }
59
+
60
+ if self._client is None:
61
+ self._client = GraphClient(token_callback=acquire_token)
62
+ # Get me
63
+ me = self._client.me.get().execute_query()
64
+ logger.debug(me.properties)
65
+ # Get sites
66
+ sites = self._client.sites.root.get().execute_query()
67
+ self._site_url = sites.properties.get("id")
68
+ return self._client
69
+
70
+ def list_folders(self, folder_path: str | None = None) -> list[dict[str, Any]]:
71
+ """Lists folders in the specified directory or root if not specified.
72
+
73
+ Args:
74
+ folder_path (Optional[str], optional): The path to the parent folder. If None, lists folders in the root.
75
+
76
+ Returns:
77
+ List[Dict[str, Any]]: A list of folder names in the specified directory.
78
+
79
+ Tags:
80
+ important
81
+ """
82
+ if folder_path:
83
+ folder = self.client.me.drive.root.get_by_path(folder_path)
84
+ folders = folder.get_folders(False).execute_query()
85
+ else:
86
+ folders = self.client.me.drive.root.get_folders(False).execute_query()
87
+
88
+ return [folder.properties.get("name") for folder in folders]
89
+
90
+ def create_folder(
91
+ self, folder_name: str, folder_path: str | None = None
92
+ ) -> dict[str, Any]:
93
+ """Creates a folder in the specified directory or root if not specified.
94
+
95
+ Args:
96
+ folder_name (str): The name of the folder to create.
97
+ folder_path (str | None, optional): The path to the parent folder. If None, creates in the root.
98
+
99
+ Returns:
100
+ Dict[str, Any]: The updated list of folders in the target directory.
101
+
102
+ Tags:
103
+ important
104
+ """
105
+ if folder_path:
106
+ folder = self.client.me.drive.root.get_by_path(folder_path)
107
+ else:
108
+ folder = self.client.me.drive.root
109
+ folder.create_folder(folder_name).execute_query()
110
+ return self.list_folders(folder_path)
111
+
112
+ def list_documents(self, folder_path: str) -> list[dict[str, Any]]:
113
+ """Lists all documents in a specified folder.
114
+
115
+ Args:
116
+ folder_path (str): The path to the folder whose documents are to be listed.
117
+
118
+ Returns:
119
+ List[Dict[str, Any]]: A list of dictionaries containing document metadata.
120
+
121
+ Tags:
122
+ important
123
+ """
124
+ folder = self.client.me.drive.root.get_by_path(folder_path)
125
+ files = folder.get_files(False).execute_query()
126
+
127
+ return [
128
+ {
129
+ "name": f.name,
130
+ "url": f.properties.get("ServerRelativeUrl"),
131
+ "size": f.properties.get("Length"),
132
+ "created": _to_iso_optional(f.properties.get("TimeCreated")),
133
+ "modified": _to_iso_optional(f.properties.get("TimeLastModified")),
134
+ }
135
+ for f in files
136
+ ]
137
+
138
+ def create_document(
139
+ self, file_path: str, file_name: str, content: str
140
+ ) -> dict[str, Any]:
141
+ """Creates a document in the specified folder.
142
+
143
+ Args:
144
+ file_path (str): The path to the folder where the document will be created.
145
+ file_name (str): The name of the document to create.
146
+ content (str): The content to write into the document.
147
+
148
+ Returns:
149
+ Dict[str, Any]: The updated list of documents in the folder.
150
+
151
+ Tags: important
152
+ """
153
+ file = self.client.me.drive.root.get_by_path(file_path)
154
+ file_io = io.StringIO(content)
155
+ file_io.name = file_name
156
+ file.upload_file(file_io).execute_query()
157
+ return self.list_documents(file_path)
158
+
159
+ def get_document_content(self, file_path: str) -> dict[str, Any]:
160
+ """Gets the content of a specified document.
161
+
162
+ Args:
163
+ file_path (str): The path to the document.
164
+
165
+ Returns:
166
+ Dict[str, Any]: A dictionary containing the document's name, content type, content (as text or base64), and size.
167
+
168
+ Tags: important
169
+ """
170
+ file = self.client.me.drive.root.get_by_path(file_path).get().execute_query()
171
+ content_stream = BytesIO()
172
+ file.download(content_stream).execute_query()
173
+ content_stream.seek(0)
174
+ content = content_stream.read()
175
+
176
+ is_text_file = file_path.lower().endswith(
177
+ (".txt", ".csv", ".json", ".xml", ".html", ".md", ".js", ".css", ".py")
178
+ )
179
+ content_dict = (
180
+ {"content": content.decode("utf-8")}
181
+ if is_text_file
182
+ else {"content_base64": base64.b64encode(content).decode("ascii")}
183
+ )
184
+ return {
185
+ "name": file_path.split("/")[-1],
186
+ "content_type": "text" if is_text_file else "binary",
187
+ **content_dict,
188
+ "size": len(content),
189
+ }
190
+
191
+ def delete_file(self, file_path: str):
192
+ """Deletes a file from OneDrive.
193
+
194
+ Args:
195
+ file_path (str): The path to the file to delete.
196
+
197
+ Returns:
198
+ bool: True if the file was deleted successfully.
199
+
200
+ Tags:
201
+ important
202
+ """
203
+ file = self.client.me.drive.root.get_by_path(file_path)
204
+ file.delete_object().execute_query()
205
+ return True
206
+
207
+ def list_tools(self):
208
+ return [
209
+ self.list_folders,
210
+ self.create_folder,
211
+ self.list_documents,
212
+ self.create_document,
213
+ self.get_document_content,
214
+ self.delete_file,
215
+ ]