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.
- universal_mcp/applications/ahrefs/README.md +51 -0
- universal_mcp/applications/ahrefs/__init__.py +1 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/airtable/README.md +22 -0
- universal_mcp/applications/airtable/__init__.py +1 -0
- universal_mcp/applications/airtable/app.py +479 -0
- universal_mcp/applications/apollo/README.md +44 -0
- universal_mcp/applications/apollo/__init__.py +1 -0
- universal_mcp/applications/apollo/app.py +1847 -0
- universal_mcp/applications/asana/README.md +199 -0
- universal_mcp/applications/asana/__init__.py +1 -0
- universal_mcp/applications/asana/app.py +9509 -0
- universal_mcp/applications/aws-s3/README.md +0 -0
- universal_mcp/applications/aws-s3/__init__.py +1 -0
- universal_mcp/applications/aws-s3/app.py +552 -0
- universal_mcp/applications/bill/README.md +0 -0
- universal_mcp/applications/bill/__init__.py +1 -0
- universal_mcp/applications/bill/app.py +8705 -0
- universal_mcp/applications/box/README.md +307 -0
- universal_mcp/applications/box/__init__.py +1 -0
- universal_mcp/applications/box/app.py +15987 -0
- universal_mcp/applications/braze/README.md +106 -0
- universal_mcp/applications/braze/__init__.py +1 -0
- universal_mcp/applications/braze/app.py +4754 -0
- universal_mcp/applications/cal-com-v2/README.md +150 -0
- universal_mcp/applications/cal-com-v2/__init__.py +1 -0
- universal_mcp/applications/cal-com-v2/app.py +5541 -0
- universal_mcp/applications/calendly/README.md +53 -0
- universal_mcp/applications/calendly/__init__.py +1 -0
- universal_mcp/applications/calendly/app.py +1436 -0
- universal_mcp/applications/canva/README.md +43 -0
- universal_mcp/applications/canva/__init__.py +1 -0
- universal_mcp/applications/canva/app.py +941 -0
- universal_mcp/applications/clickup/README.md +135 -0
- universal_mcp/applications/clickup/__init__.py +1 -0
- universal_mcp/applications/clickup/app.py +5009 -0
- universal_mcp/applications/coda/README.md +108 -0
- universal_mcp/applications/coda/__init__.py +1 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/confluence/README.md +198 -0
- universal_mcp/applications/confluence/__init__.py +1 -0
- universal_mcp/applications/confluence/app.py +6273 -0
- universal_mcp/applications/contentful/README.md +17 -0
- universal_mcp/applications/contentful/__init__.py +1 -0
- universal_mcp/applications/contentful/app.py +364 -0
- universal_mcp/applications/crustdata/README.md +25 -0
- universal_mcp/applications/crustdata/__init__.py +1 -0
- universal_mcp/applications/crustdata/app.py +586 -0
- universal_mcp/applications/dialpad/README.md +202 -0
- universal_mcp/applications/dialpad/__init__.py +1 -0
- universal_mcp/applications/dialpad/app.py +5949 -0
- universal_mcp/applications/digitalocean/README.md +463 -0
- universal_mcp/applications/digitalocean/__init__.py +1 -0
- universal_mcp/applications/digitalocean/app.py +20835 -0
- universal_mcp/applications/domain-checker/README.md +13 -0
- universal_mcp/applications/domain-checker/__init__.py +1 -0
- universal_mcp/applications/domain-checker/app.py +265 -0
- universal_mcp/applications/e2b/README.md +12 -0
- universal_mcp/applications/e2b/__init__.py +1 -0
- universal_mcp/applications/e2b/app.py +187 -0
- universal_mcp/applications/elevenlabs/README.md +88 -0
- universal_mcp/applications/elevenlabs/__init__.py +1 -0
- universal_mcp/applications/elevenlabs/app.py +3235 -0
- universal_mcp/applications/exa/README.md +15 -0
- universal_mcp/applications/exa/__init__.py +1 -0
- universal_mcp/applications/exa/app.py +221 -0
- universal_mcp/applications/falai/README.md +17 -0
- universal_mcp/applications/falai/__init__.py +1 -0
- universal_mcp/applications/falai/app.py +331 -0
- universal_mcp/applications/figma/README.md +49 -0
- universal_mcp/applications/figma/__init__.py +1 -0
- universal_mcp/applications/figma/app.py +1090 -0
- universal_mcp/applications/firecrawl/README.md +20 -0
- universal_mcp/applications/firecrawl/__init__.py +1 -0
- universal_mcp/applications/firecrawl/app.py +514 -0
- universal_mcp/applications/fireflies/README.md +25 -0
- universal_mcp/applications/fireflies/__init__.py +1 -0
- universal_mcp/applications/fireflies/app.py +506 -0
- universal_mcp/applications/fpl/README.md +23 -0
- universal_mcp/applications/fpl/__init__.py +1 -0
- universal_mcp/applications/fpl/app.py +1327 -0
- universal_mcp/applications/fpl/utils/api.py +142 -0
- universal_mcp/applications/fpl/utils/fixtures.py +629 -0
- universal_mcp/applications/fpl/utils/helper.py +982 -0
- universal_mcp/applications/fpl/utils/league_utils.py +546 -0
- universal_mcp/applications/fpl/utils/position_utils.py +68 -0
- universal_mcp/applications/ghost-content/README.md +25 -0
- universal_mcp/applications/ghost-content/__init__.py +1 -0
- universal_mcp/applications/ghost-content/app.py +654 -0
- universal_mcp/applications/github/README.md +1049 -0
- universal_mcp/applications/github/__init__.py +1 -0
- universal_mcp/applications/github/app.py +50600 -0
- universal_mcp/applications/gong/README.md +63 -0
- universal_mcp/applications/gong/__init__.py +1 -0
- universal_mcp/applications/gong/app.py +2297 -0
- universal_mcp/applications/google-ads/README.md +0 -0
- universal_mcp/applications/google-ads/__init__.py +1 -0
- universal_mcp/applications/google-ads/app.py +23 -0
- universal_mcp/applications/google-calendar/README.md +21 -0
- universal_mcp/applications/google-calendar/__init__.py +1 -0
- universal_mcp/applications/google-calendar/app.py +574 -0
- universal_mcp/applications/google-docs/README.md +25 -0
- universal_mcp/applications/google-docs/__init__.py +1 -0
- universal_mcp/applications/google-docs/app.py +760 -0
- universal_mcp/applications/google-drive/README.md +68 -0
- universal_mcp/applications/google-drive/__init__.py +1 -0
- universal_mcp/applications/google-drive/app.py +4936 -0
- universal_mcp/applications/google-gemini/README.md +25 -0
- universal_mcp/applications/google-gemini/__init__.py +1 -0
- universal_mcp/applications/google-gemini/app.py +663 -0
- universal_mcp/applications/google-mail/README.md +31 -0
- universal_mcp/applications/google-mail/__init__.py +1 -0
- universal_mcp/applications/google-mail/app.py +1354 -0
- universal_mcp/applications/google-searchconsole/README.md +21 -0
- universal_mcp/applications/google-searchconsole/__init__.py +1 -0
- universal_mcp/applications/google-searchconsole/app.py +320 -0
- universal_mcp/applications/google-sheet/README.md +36 -0
- universal_mcp/applications/google-sheet/__init__.py +1 -0
- universal_mcp/applications/google-sheet/app.py +1941 -0
- universal_mcp/applications/hashnode/README.md +20 -0
- universal_mcp/applications/hashnode/__init__.py +1 -0
- universal_mcp/applications/hashnode/app.py +455 -0
- universal_mcp/applications/heygen/README.md +44 -0
- universal_mcp/applications/heygen/__init__.py +1 -0
- universal_mcp/applications/heygen/app.py +961 -0
- universal_mcp/applications/http-tools/README.md +16 -0
- universal_mcp/applications/http-tools/__init__.py +1 -0
- universal_mcp/applications/http-tools/app.py +153 -0
- universal_mcp/applications/hubspot/README.md +239 -0
- universal_mcp/applications/hubspot/__init__.py +1 -0
- universal_mcp/applications/hubspot/app.py +416 -0
- universal_mcp/applications/jira/README.md +600 -0
- universal_mcp/applications/jira/__init__.py +1 -0
- universal_mcp/applications/jira/app.py +28804 -0
- universal_mcp/applications/klaviyo/README.md +313 -0
- universal_mcp/applications/klaviyo/__init__.py +1 -0
- universal_mcp/applications/klaviyo/app.py +11236 -0
- universal_mcp/applications/linkedin/README.md +15 -0
- universal_mcp/applications/linkedin/__init__.py +1 -0
- universal_mcp/applications/linkedin/app.py +243 -0
- universal_mcp/applications/mailchimp/README.md +281 -0
- universal_mcp/applications/mailchimp/__init__.py +1 -0
- universal_mcp/applications/mailchimp/app.py +10937 -0
- universal_mcp/applications/markitdown/README.md +12 -0
- universal_mcp/applications/markitdown/__init__.py +1 -0
- universal_mcp/applications/markitdown/app.py +63 -0
- universal_mcp/applications/miro/README.md +151 -0
- universal_mcp/applications/miro/__init__.py +1 -0
- universal_mcp/applications/miro/app.py +5429 -0
- universal_mcp/applications/ms-teams/README.md +42 -0
- universal_mcp/applications/ms-teams/__init__.py +1 -0
- universal_mcp/applications/ms-teams/app.py +1823 -0
- universal_mcp/applications/neon/README.md +74 -0
- universal_mcp/applications/neon/__init__.py +1 -0
- universal_mcp/applications/neon/app.py +2018 -0
- universal_mcp/applications/notion/README.md +30 -0
- universal_mcp/applications/notion/__init__.py +1 -0
- universal_mcp/applications/notion/app.py +527 -0
- universal_mcp/applications/openai/README.md +22 -0
- universal_mcp/applications/openai/__init__.py +1 -0
- universal_mcp/applications/openai/app.py +759 -0
- universal_mcp/applications/outlook/README.md +20 -0
- universal_mcp/applications/outlook/__init__.py +1 -0
- universal_mcp/applications/outlook/app.py +444 -0
- universal_mcp/applications/perplexity/README.md +12 -0
- universal_mcp/applications/perplexity/__init__.py +1 -0
- universal_mcp/applications/perplexity/app.py +65 -0
- universal_mcp/applications/pipedrive/README.md +284 -0
- universal_mcp/applications/pipedrive/__init__.py +1 -0
- universal_mcp/applications/pipedrive/app.py +12924 -0
- universal_mcp/applications/posthog/README.md +132 -0
- universal_mcp/applications/posthog/__init__.py +1 -0
- universal_mcp/applications/posthog/app.py +7125 -0
- universal_mcp/applications/reddit/README.md +135 -0
- universal_mcp/applications/reddit/__init__.py +1 -0
- universal_mcp/applications/reddit/app.py +4652 -0
- universal_mcp/applications/replicate/README.md +18 -0
- universal_mcp/applications/replicate/__init__.py +1 -0
- universal_mcp/applications/replicate/app.py +495 -0
- universal_mcp/applications/resend/README.md +40 -0
- universal_mcp/applications/resend/__init__.py +1 -0
- universal_mcp/applications/resend/app.py +881 -0
- universal_mcp/applications/retell/README.md +21 -0
- universal_mcp/applications/retell/__init__.py +1 -0
- universal_mcp/applications/retell/app.py +333 -0
- universal_mcp/applications/rocketlane/README.md +70 -0
- universal_mcp/applications/rocketlane/__init__.py +1 -0
- universal_mcp/applications/rocketlane/app.py +4346 -0
- universal_mcp/applications/semanticscholar/README.md +25 -0
- universal_mcp/applications/semanticscholar/__init__.py +1 -0
- universal_mcp/applications/semanticscholar/app.py +482 -0
- universal_mcp/applications/semrush/README.md +44 -0
- universal_mcp/applications/semrush/__init__.py +1 -0
- universal_mcp/applications/semrush/app.py +2081 -0
- universal_mcp/applications/sendgrid/README.md +362 -0
- universal_mcp/applications/sendgrid/__init__.py +1 -0
- universal_mcp/applications/sendgrid/app.py +9752 -0
- universal_mcp/applications/sentry/README.md +186 -0
- universal_mcp/applications/sentry/__init__.py +1 -0
- universal_mcp/applications/sentry/app.py +7471 -0
- universal_mcp/applications/serpapi/README.md +14 -0
- universal_mcp/applications/serpapi/__init__.py +1 -0
- universal_mcp/applications/serpapi/app.py +293 -0
- universal_mcp/applications/sharepoint/README.md +0 -0
- universal_mcp/applications/sharepoint/__init__.py +1 -0
- universal_mcp/applications/sharepoint/app.py +215 -0
- universal_mcp/applications/shopify/README.md +321 -0
- universal_mcp/applications/shopify/__init__.py +1 -0
- universal_mcp/applications/shopify/app.py +15392 -0
- universal_mcp/applications/shortcut/README.md +128 -0
- universal_mcp/applications/shortcut/__init__.py +1 -0
- universal_mcp/applications/shortcut/app.py +4478 -0
- universal_mcp/applications/slack/README.md +0 -0
- universal_mcp/applications/slack/__init__.py +1 -0
- universal_mcp/applications/slack/app.py +570 -0
- universal_mcp/applications/spotify/README.md +91 -0
- universal_mcp/applications/spotify/__init__.py +1 -0
- universal_mcp/applications/spotify/app.py +2526 -0
- universal_mcp/applications/supabase/README.md +87 -0
- universal_mcp/applications/supabase/__init__.py +1 -0
- universal_mcp/applications/supabase/app.py +2970 -0
- universal_mcp/applications/tavily/README.md +12 -0
- universal_mcp/applications/tavily/__init__.py +1 -0
- universal_mcp/applications/tavily/app.py +51 -0
- universal_mcp/applications/trello/README.md +266 -0
- universal_mcp/applications/trello/__init__.py +1 -0
- universal_mcp/applications/trello/app.py +10875 -0
- universal_mcp/applications/twillo/README.md +0 -0
- universal_mcp/applications/twillo/__init__.py +1 -0
- universal_mcp/applications/twillo/app.py +269 -0
- universal_mcp/applications/twitter/README.md +100 -0
- universal_mcp/applications/twitter/__init__.py +1 -0
- universal_mcp/applications/twitter/api_segments/__init__.py +0 -0
- universal_mcp/applications/twitter/api_segments/api_segment_base.py +51 -0
- universal_mcp/applications/twitter/api_segments/compliance_api.py +122 -0
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +255 -0
- universal_mcp/applications/twitter/api_segments/dm_events_api.py +140 -0
- universal_mcp/applications/twitter/api_segments/likes_api.py +159 -0
- universal_mcp/applications/twitter/api_segments/lists_api.py +395 -0
- universal_mcp/applications/twitter/api_segments/openapi_json_api.py +34 -0
- universal_mcp/applications/twitter/api_segments/spaces_api.py +309 -0
- universal_mcp/applications/twitter/api_segments/trends_api.py +40 -0
- universal_mcp/applications/twitter/api_segments/tweets_api.py +1403 -0
- universal_mcp/applications/twitter/api_segments/usage_api.py +40 -0
- universal_mcp/applications/twitter/api_segments/users_api.py +1498 -0
- universal_mcp/applications/twitter/app.py +46 -0
- universal_mcp/applications/unipile/README.md +28 -0
- universal_mcp/applications/unipile/__init__.py +1 -0
- universal_mcp/applications/unipile/app.py +829 -0
- universal_mcp/applications/whatsapp/README.md +23 -0
- universal_mcp/applications/whatsapp/__init__.py +1 -0
- universal_mcp/applications/whatsapp/app.py +595 -0
- universal_mcp/applications/whatsapp-business/README.md +34 -0
- universal_mcp/applications/whatsapp-business/__init__.py +1 -0
- universal_mcp/applications/whatsapp-business/app.py +1065 -0
- universal_mcp/applications/wrike/README.md +46 -0
- universal_mcp/applications/wrike/__init__.py +1 -0
- universal_mcp/applications/wrike/app.py +1583 -0
- universal_mcp/applications/youtube/README.md +57 -0
- universal_mcp/applications/youtube/__init__.py +1 -0
- universal_mcp/applications/youtube/app.py +1696 -0
- universal_mcp/applications/zenquotes/README.md +12 -0
- universal_mcp/applications/zenquotes/__init__.py +1 -0
- universal_mcp/applications/zenquotes/app.py +31 -0
- universal_mcp_applications-0.1.1.dist-info/METADATA +172 -0
- universal_mcp_applications-0.1.1.dist-info/RECORD +268 -0
- universal_mcp_applications-0.1.1.dist-info/WHEEL +4 -0
- 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
|
+
]
|