universal-mcp 0.1.7rc2__py3-none-any.whl → 0.1.8__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/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/ahrefs/README.md +76 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +95 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +1195 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/e2b/app.py +14 -35
- universal_mcp/applications/figma/README.md +74 -0
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +1261 -0
- universal_mcp/applications/firecrawl/app.py +29 -32
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -138
- universal_mcp/applications/google_docs/app.py +47 -52
- universal_mcp/applications/google_drive/app.py +119 -113
- universal_mcp/applications/google_mail/app.py +124 -50
- universal_mcp/applications/google_sheet/app.py +89 -91
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +13 -45
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -23
- universal_mcp/applications/{serp → serpapi}/app.py +14 -33
- universal_mcp/applications/tavily/app.py +11 -28
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1372 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1428 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +143 -30
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +201 -146
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
- universal_mcp-0.1.8.dist-info/RECORD +81 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
| @@ -2,7 +2,6 @@ from typing import Any, Literal | |
| 2 2 |  | 
| 3 3 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 4 4 | 
             
            from universal_mcp.integrations import Integration
         | 
| 5 | 
            -
            from loguru import logger
         | 
| 6 5 |  | 
| 7 6 |  | 
| 8 7 | 
             
            class PerplexityApp(APIApplication):
         | 
| @@ -11,38 +10,6 @@ class PerplexityApp(APIApplication): | |
| 11 10 | 
             
                    self.api_key: str | None = None
         | 
| 12 11 | 
             
                    self.base_url = "https://api.perplexity.ai"
         | 
| 13 12 |  | 
| 14 | 
            -
                def _set_api_key(self):
         | 
| 15 | 
            -
                    if self.api_key:
         | 
| 16 | 
            -
                        return
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                    if not self.integration:
         | 
| 19 | 
            -
                        raise ValueError("Integration is None. Cannot retrieve Perplexity API Key.")
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                    credentials = self.integration.get_credentials()
         | 
| 22 | 
            -
                    if not credentials:
         | 
| 23 | 
            -
                        raise ValueError(
         | 
| 24 | 
            -
                            f"Failed to retrieve Perplexity API Key using integration '{self.integration.name}'. "
         | 
| 25 | 
            -
                        )
         | 
| 26 | 
            -
                    api_key = (
         | 
| 27 | 
            -
                        credentials.get("api_key")
         | 
| 28 | 
            -
                        or credentials.get("API_KEY")
         | 
| 29 | 
            -
                        or credentials.get("apiKey")
         | 
| 30 | 
            -
                    )
         | 
| 31 | 
            -
                    if not api_key:
         | 
| 32 | 
            -
                        raise ValueError(
         | 
| 33 | 
            -
                            f"Invalid credential format received for Perplexity API Key via integration '{self.integration.name}'. "
         | 
| 34 | 
            -
                        )
         | 
| 35 | 
            -
                    self.api_key = api_key
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                def _get_headers(self) -> dict[str, str]:
         | 
| 38 | 
            -
                    self._set_api_key()
         | 
| 39 | 
            -
                    logger.info(f"Perplexity API Key: {self.api_key}")
         | 
| 40 | 
            -
                    return {
         | 
| 41 | 
            -
                        "Authorization": f"Bearer {self.api_key}",
         | 
| 42 | 
            -
                        "Content-Type": "application/json",
         | 
| 43 | 
            -
                        "Accept": "application/json",
         | 
| 44 | 
            -
                    }
         | 
| 45 | 
            -
             | 
| 46 13 | 
             
                def chat(
         | 
| 47 14 | 
             
                    self,
         | 
| 48 15 | 
             
                    query: str,
         | 
| @@ -58,34 +25,35 @@ class PerplexityApp(APIApplication): | |
| 58 25 | 
             
                    system_prompt: str = "Be precise and concise.",
         | 
| 59 26 | 
             
                ) -> dict[str, Any] | str:
         | 
| 60 27 | 
             
                    """
         | 
| 61 | 
            -
                     | 
| 62 | 
            -
             | 
| 63 | 
            -
                    This uses the chat completions endpoint, suitable for conversational queries
         | 
| 64 | 
            -
                    and leveraging Perplexity's online capabilities.
         | 
| 28 | 
            +
                    Initiates a chat completion request to generate AI responses using various models with customizable parameters.
         | 
| 65 29 |  | 
| 66 30 | 
             
                    Args:
         | 
| 67 | 
            -
                        query: The  | 
| 68 | 
            -
                        model: The  | 
| 69 | 
            -
                        temperature:  | 
| 70 | 
            -
                        system_prompt:  | 
| 31 | 
            +
                        query: The input text/prompt to send to the chat model
         | 
| 32 | 
            +
                        model: The model to use for chat completion. Options include 'r1-1776', 'sonar', 'sonar-pro', 'sonar-reasoning', 'sonar-reasoning-pro', 'sonar-deep-research'. Defaults to 'sonar'
         | 
| 33 | 
            +
                        temperature: Controls randomness in the model's output. Higher values make output more random, lower values more deterministic. Defaults to 1
         | 
| 34 | 
            +
                        system_prompt: Initial system message to guide the model's behavior. Defaults to 'Be precise and concise.'
         | 
| 71 35 |  | 
| 72 36 | 
             
                    Returns:
         | 
| 73 | 
            -
                        A dictionary containing 'content' (str) and 'citations' (list) | 
| 37 | 
            +
                        A dictionary containing the generated content and citations, with keys 'content' (str) and 'citations' (list), or a string in some cases
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    Raises:
         | 
| 40 | 
            +
                        AuthenticationError: Raised when API authentication fails due to missing or invalid credentials
         | 
| 41 | 
            +
                        HTTPError: Raised when the API request fails or returns an error status
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    Tags:
         | 
| 44 | 
            +
                        chat, generate, ai, completion, important
         | 
| 74 45 | 
             
                    """
         | 
| 75 46 | 
             
                    endpoint = f"{self.base_url}/chat/completions"
         | 
| 76 | 
            -
             | 
| 77 47 | 
             
                    messages = []
         | 
| 78 48 | 
             
                    if system_prompt:
         | 
| 79 49 | 
             
                        messages.append({"role": "system", "content": system_prompt})
         | 
| 80 50 | 
             
                    messages.append({"role": "user", "content": query})
         | 
| 81 | 
            -
             | 
| 82 51 | 
             
                    payload = {
         | 
| 83 52 | 
             
                        "model": model,
         | 
| 84 53 | 
             
                        "messages": messages,
         | 
| 85 54 | 
             
                        "temperature": temperature,
         | 
| 86 55 | 
             
                        # "max_tokens": 512,
         | 
| 87 56 | 
             
                    }
         | 
| 88 | 
            -
             | 
| 89 57 | 
             
                    data = self._post(endpoint, data=payload)
         | 
| 90 58 | 
             
                    response = data.json()
         | 
| 91 59 | 
             
                    content = response["choices"][0]["message"]["content"]
         | 
| @@ -45,48 +45,48 @@ class RedditApp(APIApplication): | |
| 45 45 | 
             
                def get_subreddit_posts(
         | 
| 46 46 | 
             
                    self, subreddit: str, limit: int = 5, timeframe: str = "day"
         | 
| 47 47 | 
             
                ) -> str:
         | 
| 48 | 
            -
                    """ | 
| 48 | 
            +
                    """
         | 
| 49 | 
            +
                    Retrieves and formats top posts from a specified subreddit within a given timeframe using the Reddit API
         | 
| 49 50 |  | 
| 50 51 | 
             
                    Args:
         | 
| 51 | 
            -
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' | 
| 52 | 
            -
                        limit: The maximum number of posts to return (default: 5, max: 100) | 
| 53 | 
            -
                        timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day') | 
| 52 | 
            +
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' prefix
         | 
| 53 | 
            +
                        limit: The maximum number of posts to return (default: 5, max: 100)
         | 
| 54 | 
            +
                        timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day')
         | 
| 54 55 |  | 
| 55 56 | 
             
                    Returns:
         | 
| 56 | 
            -
                        A formatted string  | 
| 57 | 
            +
                        A formatted string containing a numbered list of top posts, including titles, authors, scores, and URLs, or an error message if the request fails
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    Raises:
         | 
| 60 | 
            +
                        RequestException: When the HTTP request to the Reddit API fails
         | 
| 61 | 
            +
                        JSONDecodeError: When the API response contains invalid JSON
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    Tags:
         | 
| 64 | 
            +
                        fetch, reddit, api, list, social-media, important, read-only
         | 
| 57 65 | 
             
                    """
         | 
| 58 66 | 
             
                    valid_timeframes = ["hour", "day", "week", "month", "year", "all"]
         | 
| 59 67 | 
             
                    if timeframe not in valid_timeframes:
         | 
| 60 68 | 
             
                        return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
         | 
| 61 | 
            -
             | 
| 62 69 | 
             
                    if not 1 <= limit <= 100:
         | 
| 63 70 | 
             
                        return (
         | 
| 64 71 | 
             
                            f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
         | 
| 65 72 | 
             
                        )
         | 
| 66 | 
            -
             | 
| 67 73 | 
             
                    url = f"{self.base_api_url}/r/{subreddit}/top"
         | 
| 68 74 | 
             
                    params = {"limit": limit, "t": timeframe}
         | 
| 69 | 
            -
             | 
| 70 75 | 
             
                    logger.info(
         | 
| 71 76 | 
             
                        f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'"
         | 
| 72 77 | 
             
                    )
         | 
| 73 78 | 
             
                    response = self._get(url, params=params)
         | 
| 74 | 
            -
             | 
| 75 79 | 
             
                    data = response.json()
         | 
| 76 | 
            -
             | 
| 77 80 | 
             
                    if "error" in data:
         | 
| 78 81 | 
             
                        logger.error(
         | 
| 79 82 | 
             
                            f"Reddit API error: {data['error']} - {data.get('message', '')}"
         | 
| 80 83 | 
             
                        )
         | 
| 81 84 | 
             
                        return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
         | 
| 82 | 
            -
             | 
| 83 85 | 
             
                    posts = data.get("data", {}).get("children", [])
         | 
| 84 | 
            -
             | 
| 85 86 | 
             
                    if not posts:
         | 
| 86 87 | 
             
                        return (
         | 
| 87 88 | 
             
                            f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
         | 
| 88 89 | 
             
                        )
         | 
| 89 | 
            -
             | 
| 90 90 | 
             
                    result_lines = [
         | 
| 91 91 | 
             
                        f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"
         | 
| 92 92 | 
             
                    ]
         | 
| @@ -100,31 +100,36 @@ class RedditApp(APIApplication): | |
| 100 100 |  | 
| 101 101 | 
             
                        result_lines.append(f'{i + 1}. "{title}" by u/{author} (Score: {score})')
         | 
| 102 102 | 
             
                        result_lines.append(f"   Link: {full_url}")
         | 
| 103 | 
            -
             | 
| 104 103 | 
             
                    return "\n".join(result_lines)
         | 
| 105 104 |  | 
| 106 105 | 
             
                def search_subreddits(
         | 
| 107 106 | 
             
                    self, query: str, limit: int = 5, sort: str = "relevance"
         | 
| 108 107 | 
             
                ) -> str:
         | 
| 109 | 
            -
                    """ | 
| 108 | 
            +
                    """
         | 
| 109 | 
            +
                    Searches Reddit for subreddits matching a given query string and returns a formatted list of results including subreddit names, subscriber counts, and descriptions.
         | 
| 110 110 |  | 
| 111 111 | 
             
                    Args:
         | 
| 112 | 
            -
                        query: The text to search for in subreddit names and descriptions | 
| 113 | 
            -
                        limit: The maximum number of subreddits to return (default: 5 | 
| 114 | 
            -
                        sort: The order of results | 
| 112 | 
            +
                        query: The text to search for in subreddit names and descriptions
         | 
| 113 | 
            +
                        limit: The maximum number of subreddits to return, between 1 and 100 (default: 5)
         | 
| 114 | 
            +
                        sort: The order of results, either 'relevance' or 'activity' (default: 'relevance')
         | 
| 115 115 |  | 
| 116 116 | 
             
                    Returns:
         | 
| 117 | 
            -
                        A formatted string  | 
| 117 | 
            +
                        A formatted string containing a list of matching subreddits with their names, subscriber counts, and descriptions, or an error message if the search fails or parameters are invalid
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    Raises:
         | 
| 120 | 
            +
                        RequestException: When the HTTP request to Reddit's API fails
         | 
| 121 | 
            +
                        JSONDecodeError: When the API response contains invalid JSON
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    Tags:
         | 
| 124 | 
            +
                        search, important, reddit, api, query, format, list, validation
         | 
| 118 125 | 
             
                    """
         | 
| 119 126 | 
             
                    valid_sorts = ["relevance", "activity"]
         | 
| 120 127 | 
             
                    if sort not in valid_sorts:
         | 
| 121 128 | 
             
                        return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
         | 
| 122 | 
            -
             | 
| 123 129 | 
             
                    if not 1 <= limit <= 100:
         | 
| 124 130 | 
             
                        return (
         | 
| 125 131 | 
             
                            f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
         | 
| 126 132 | 
             
                        )
         | 
| 127 | 
            -
             | 
| 128 133 | 
             
                    url = f"{self.base_api_url}/subreddits/search"
         | 
| 129 134 | 
             
                    params = {
         | 
| 130 135 | 
             
                        "q": query,
         | 
| @@ -133,25 +138,19 @@ class RedditApp(APIApplication): | |
| 133 138 | 
             
                        # Optionally include NSFW results? Defaulting to false for safety.
         | 
| 134 139 | 
             
                        # "include_over_18": "false"
         | 
| 135 140 | 
             
                    }
         | 
| 136 | 
            -
             | 
| 137 141 | 
             
                    logger.info(
         | 
| 138 142 | 
             
                        f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})"
         | 
| 139 143 | 
             
                    )
         | 
| 140 144 | 
             
                    response = self._get(url, params=params)
         | 
| 141 | 
            -
             | 
| 142 145 | 
             
                    data = response.json()
         | 
| 143 | 
            -
             | 
| 144 146 | 
             
                    if "error" in data:
         | 
| 145 147 | 
             
                        logger.error(
         | 
| 146 148 | 
             
                            f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}"
         | 
| 147 149 | 
             
                        )
         | 
| 148 150 | 
             
                        return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
         | 
| 149 | 
            -
             | 
| 150 151 | 
             
                    subreddits = data.get("data", {}).get("children", [])
         | 
| 151 | 
            -
             | 
| 152 152 | 
             
                    if not subreddits:
         | 
| 153 153 | 
             
                        return f"No subreddits found matching the query '{query}'."
         | 
| 154 | 
            -
             | 
| 155 154 | 
             
                    result_lines = [
         | 
| 156 155 | 
             
                        f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"
         | 
| 157 156 | 
             
                    ]
         | 
| @@ -173,28 +172,31 @@ class RedditApp(APIApplication): | |
| 173 172 | 
             
                        )
         | 
| 174 173 | 
             
                        if description:
         | 
| 175 174 | 
             
                            result_lines.append(f"   Description: {description}")
         | 
| 176 | 
            -
             | 
| 177 175 | 
             
                    return "\n".join(result_lines)
         | 
| 178 176 |  | 
| 179 177 | 
             
                def get_post_flairs(self, subreddit: str):
         | 
| 180 | 
            -
                    """ | 
| 178 | 
            +
                    """
         | 
| 179 | 
            +
                    Retrieves a list of available post flairs for a specified subreddit using the Reddit API.
         | 
| 181 180 |  | 
| 182 181 | 
             
                    Args:
         | 
| 183 | 
            -
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' | 
| 182 | 
            +
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' prefix
         | 
| 184 183 |  | 
| 185 184 | 
             
                    Returns:
         | 
| 186 | 
            -
                        A list of dictionaries containing flair details, or  | 
| 187 | 
            -
                    """
         | 
| 185 | 
            +
                        A list of dictionaries containing flair details if flairs exist, or a string message indicating no flairs are available
         | 
| 188 186 |  | 
| 189 | 
            -
                     | 
| 187 | 
            +
                    Raises:
         | 
| 188 | 
            +
                        RequestException: When the API request fails or network connectivity issues occur
         | 
| 189 | 
            +
                        JSONDecodeError: When the API response contains invalid JSON data
         | 
| 190 190 |  | 
| 191 | 
            +
                    Tags:
         | 
| 192 | 
            +
                        fetch, get, reddit, flair, api, read-only
         | 
| 193 | 
            +
                    """
         | 
| 194 | 
            +
                    url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
         | 
| 191 195 | 
             
                    logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
         | 
| 192 196 | 
             
                    response = self._get(url)
         | 
| 193 | 
            -
             | 
| 194 197 | 
             
                    flairs = response.json()
         | 
| 195 198 | 
             
                    if not flairs:
         | 
| 196 199 | 
             
                        return f"No post flairs available for r/{subreddit}."
         | 
| 197 | 
            -
             | 
| 198 200 | 
             
                    return flairs
         | 
| 199 201 |  | 
| 200 202 | 
             
                def create_post(
         | 
| @@ -206,32 +208,32 @@ class RedditApp(APIApplication): | |
| 206 208 | 
             
                    url: str = None,
         | 
| 207 209 | 
             
                    flair_id: str = None,
         | 
| 208 210 | 
             
                ):
         | 
| 209 | 
            -
                    """ | 
| 211 | 
            +
                    """
         | 
| 212 | 
            +
                    Creates a new Reddit post in a specified subreddit with support for text posts, link posts, and image posts
         | 
| 210 213 |  | 
| 211 214 | 
             
                    Args:
         | 
| 212 | 
            -
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/' | 
| 213 | 
            -
                        title: The title of the post | 
| 214 | 
            -
                        kind: The type of post; either 'self' (text post) or 'link' (link or image post) | 
| 215 | 
            -
                        text: The text content of the post; required if kind is 'self' | 
| 216 | 
            -
                        url: The URL of the link or image; required if kind is 'link'.
         | 
| 217 | 
            -
             | 
| 218 | 
            -
                            and end with a valid image extension (e.g., .jpg, .png, or .gif).
         | 
| 219 | 
            -
                            Note that .gif support can be inconsistent.
         | 
| 220 | 
            -
                        flair_id: The ID of the flair to assign to the post.
         | 
| 215 | 
            +
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'
         | 
| 216 | 
            +
                        title: The title of the post
         | 
| 217 | 
            +
                        kind: The type of post; either 'self' (text post) or 'link' (link or image post)
         | 
| 218 | 
            +
                        text: The text content of the post; required if kind is 'self'
         | 
| 219 | 
            +
                        url: The URL of the link or image; required if kind is 'link'. Must end with valid image extension for image posts
         | 
| 220 | 
            +
                        flair_id: The ID of the flair to assign to the post
         | 
| 221 221 |  | 
| 222 222 | 
             
                    Returns:
         | 
| 223 | 
            -
                        The JSON response from the Reddit API, or an error message as a string | 
| 224 | 
            -
                        If the reddit api returns an error within the json response, that error will be returned as a string.
         | 
| 225 | 
            -
                    """
         | 
| 223 | 
            +
                        The JSON response from the Reddit API, or an error message as a string if the API returns an error
         | 
| 226 224 |  | 
| 225 | 
            +
                    Raises:
         | 
| 226 | 
            +
                        ValueError: Raised when kind is invalid or when required parameters (text for self posts, url for link posts) are missing
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                    Tags:
         | 
| 229 | 
            +
                        create, post, social-media, reddit, api, important
         | 
| 230 | 
            +
                    """
         | 
| 227 231 | 
             
                    if kind not in ["self", "link"]:
         | 
| 228 232 | 
             
                        raise ValueError("Invalid post kind. Must be one of 'self' or 'link'.")
         | 
| 229 | 
            -
             | 
| 230 233 | 
             
                    if kind == "self" and not text:
         | 
| 231 234 | 
             
                        raise ValueError("Text content is required for text posts.")
         | 
| 232 235 | 
             
                    if kind == "link" and not url:
         | 
| 233 236 | 
             
                        raise ValueError("URL is required for link posts (including images).")
         | 
| 234 | 
            -
             | 
| 235 237 | 
             
                    data = {
         | 
| 236 238 | 
             
                        "sr": subreddit,
         | 
| 237 239 | 
             
                        "title": title,
         | 
| @@ -241,13 +243,10 @@ class RedditApp(APIApplication): | |
| 241 243 | 
             
                        "flair_id": flair_id,
         | 
| 242 244 | 
             
                    }
         | 
| 243 245 | 
             
                    data = {k: v for k, v in data.items() if v is not None}
         | 
| 244 | 
            -
             | 
| 245 246 | 
             
                    url_api = f"{self.base_api_url}/api/submit"
         | 
| 246 247 | 
             
                    logger.info(f"Submitting a new post to r/{subreddit}")
         | 
| 247 248 | 
             
                    response = self._post(url_api, data=data)
         | 
| 248 249 | 
             
                    response_json = response.json()
         | 
| 249 | 
            -
             | 
| 250 | 
            -
                    # Check for Reddit API errors in the response
         | 
| 251 250 | 
             
                    if (
         | 
| 252 251 | 
             
                        response_json
         | 
| 253 252 | 
             
                        and "json" in response_json
         | 
| @@ -259,27 +258,27 @@ class RedditApp(APIApplication): | |
| 259 258 | 
             
                                [f"{code}: {message}" for code, message in errors]
         | 
| 260 259 | 
             
                            )
         | 
| 261 260 | 
             
                            return f"Reddit API error: {error_message}"
         | 
| 262 | 
            -
             | 
| 263 261 | 
             
                    return response_json
         | 
| 264 262 |  | 
| 265 263 | 
             
                def get_comment_by_id(self, comment_id: str) -> dict:
         | 
| 266 264 | 
             
                    """
         | 
| 267 | 
            -
                     | 
| 265 | 
            +
                    Retrieves a specific Reddit comment using its unique identifier.
         | 
| 268 266 |  | 
| 269 267 | 
             
                    Args:
         | 
| 270 | 
            -
                        comment_id: The full unique  | 
| 268 | 
            +
                        comment_id: The full unique identifier of the comment (prefixed with 't1_', e.g., 't1_abcdef')
         | 
| 271 269 |  | 
| 272 270 | 
             
                    Returns:
         | 
| 273 | 
            -
                        A dictionary containing the comment data,  | 
| 274 | 
            -
                    """
         | 
| 275 | 
            -
             | 
| 276 | 
            -
                    # Define the endpoint URL
         | 
| 277 | 
            -
                    url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
         | 
| 271 | 
            +
                        A dictionary containing the comment data including attributes like author, body, score, etc. If the comment is not found, returns a dictionary with an error message.
         | 
| 278 272 |  | 
| 279 | 
            -
                     | 
| 273 | 
            +
                    Raises:
         | 
| 274 | 
            +
                        HTTPError: When the Reddit API request fails due to network issues or invalid authentication
         | 
| 275 | 
            +
                        JSONDecodeError: When the API response cannot be parsed as valid JSON
         | 
| 280 276 |  | 
| 277 | 
            +
                    Tags:
         | 
| 278 | 
            +
                        retrieve, get, reddit, comment, api, fetch, single-item, important
         | 
| 279 | 
            +
                    """
         | 
| 280 | 
            +
                    url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
         | 
| 281 281 | 
             
                    response = self._get(url)
         | 
| 282 | 
            -
             | 
| 283 282 | 
             
                    data = response.json()
         | 
| 284 283 | 
             
                    comments = data.get("data", {}).get("children", [])
         | 
| 285 284 | 
             
                    if comments:
         | 
| @@ -289,72 +288,82 @@ class RedditApp(APIApplication): | |
| 289 288 |  | 
| 290 289 | 
             
                def post_comment(self, parent_id: str, text: str) -> dict:
         | 
| 291 290 | 
             
                    """
         | 
| 292 | 
            -
                     | 
| 291 | 
            +
                    Posts a comment to a Reddit post or comment using the Reddit API
         | 
| 293 292 |  | 
| 294 293 | 
             
                    Args:
         | 
| 295 | 
            -
                        parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment) | 
| 296 | 
            -
                        text: The text content of the comment | 
| 294 | 
            +
                        parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
         | 
| 295 | 
            +
                        text: The text content of the comment to be posted
         | 
| 297 296 |  | 
| 298 297 | 
             
                    Returns:
         | 
| 299 | 
            -
                        A dictionary containing the  | 
| 300 | 
            -
             | 
| 298 | 
            +
                        A dictionary containing the Reddit API response with details about the posted comment
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                    Raises:
         | 
| 301 | 
            +
                        RequestException: If the API request fails or returns an error status code
         | 
| 302 | 
            +
                        JSONDecodeError: If the API response cannot be parsed as JSON
         | 
| 301 303 |  | 
| 304 | 
            +
                    Tags:
         | 
| 305 | 
            +
                        post, comment, social, reddit, api, important
         | 
| 306 | 
            +
                    """
         | 
| 302 307 | 
             
                    url = f"{self.base_api_url}/api/comment"
         | 
| 303 308 | 
             
                    data = {
         | 
| 304 309 | 
             
                        "parent": parent_id,
         | 
| 305 310 | 
             
                        "text": text,
         | 
| 306 311 | 
             
                    }
         | 
| 307 | 
            -
             | 
| 308 312 | 
             
                    logger.info(f"Posting comment to {parent_id}")
         | 
| 309 313 | 
             
                    response = self._post(url, data=data)
         | 
| 310 | 
            -
             | 
| 311 314 | 
             
                    return response.json()
         | 
| 312 315 |  | 
| 313 316 | 
             
                def edit_content(self, content_id: str, text: str) -> dict:
         | 
| 314 317 | 
             
                    """
         | 
| 315 | 
            -
                     | 
| 318 | 
            +
                    Edits the text content of an existing Reddit post or comment using the Reddit API
         | 
| 316 319 |  | 
| 317 320 | 
             
                    Args:
         | 
| 318 | 
            -
                        content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment) | 
| 319 | 
            -
                        text: The new text content | 
| 321 | 
            +
                        content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
         | 
| 322 | 
            +
                        text: The new text content to replace the existing content
         | 
| 320 323 |  | 
| 321 324 | 
             
                    Returns:
         | 
| 322 | 
            -
                        A dictionary containing the  | 
| 323 | 
            -
                    """
         | 
| 325 | 
            +
                        A dictionary containing the API response with details about the edited content
         | 
| 324 326 |  | 
| 327 | 
            +
                    Raises:
         | 
| 328 | 
            +
                        RequestException: When the API request fails or network connectivity issues occur
         | 
| 329 | 
            +
                        ValueError: When invalid content_id format or empty text is provided
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                    Tags:
         | 
| 332 | 
            +
                        edit, update, content, reddit, api, important
         | 
| 333 | 
            +
                    """
         | 
| 325 334 | 
             
                    url = f"{self.base_api_url}/api/editusertext"
         | 
| 326 335 | 
             
                    data = {
         | 
| 327 336 | 
             
                        "thing_id": content_id,
         | 
| 328 337 | 
             
                        "text": text,
         | 
| 329 338 | 
             
                    }
         | 
| 330 | 
            -
             | 
| 331 339 | 
             
                    logger.info(f"Editing content {content_id}")
         | 
| 332 340 | 
             
                    response = self._post(url, data=data)
         | 
| 333 | 
            -
             | 
| 334 341 | 
             
                    return response.json()
         | 
| 335 342 |  | 
| 336 343 | 
             
                def delete_content(self, content_id: str) -> dict:
         | 
| 337 344 | 
             
                    """
         | 
| 338 | 
            -
                     | 
| 345 | 
            +
                    Deletes a specified Reddit post or comment using the Reddit API.
         | 
| 339 346 |  | 
| 340 347 | 
             
                    Args:
         | 
| 341 | 
            -
                        content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment) | 
| 348 | 
            +
                        content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment)
         | 
| 342 349 |  | 
| 343 350 | 
             
                    Returns:
         | 
| 344 | 
            -
                        A dictionary containing  | 
| 345 | 
            -
             | 
| 351 | 
            +
                        A dictionary containing a success message with the deleted content ID
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                    Raises:
         | 
| 354 | 
            +
                        HTTPError: When the API request fails or returns an error status code
         | 
| 355 | 
            +
                        RequestException: When there are network connectivity issues or API communication problems
         | 
| 346 356 |  | 
| 357 | 
            +
                    Tags:
         | 
| 358 | 
            +
                        delete, content-management, api, reddit, important
         | 
| 359 | 
            +
                    """
         | 
| 347 360 | 
             
                    url = f"{self.base_api_url}/api/del"
         | 
| 348 361 | 
             
                    data = {
         | 
| 349 362 | 
             
                        "id": content_id,
         | 
| 350 363 | 
             
                    }
         | 
| 351 | 
            -
             | 
| 352 364 | 
             
                    logger.info(f"Deleting content {content_id}")
         | 
| 353 365 | 
             
                    response = self._post(url, data=data)
         | 
| 354 366 | 
             
                    response.raise_for_status()
         | 
| 355 | 
            -
             | 
| 356 | 
            -
                    # Reddit's delete endpoint returns an empty response on success.
         | 
| 357 | 
            -
                    # We'll just return a success message.
         | 
| 358 367 | 
             
                    return {"message": f"Content {content_id} deleted successfully."}
         | 
| 359 368 |  | 
| 360 369 | 
             
                def list_tools(self):
         | 
| @@ -5,35 +5,24 @@ from universal_mcp.integrations import Integration | |
| 5 5 | 
             
            class ResendApp(APIApplication):
         | 
| 6 6 | 
             
                def __init__(self, integration: Integration) -> None:
         | 
| 7 7 | 
             
                    super().__init__(name="resend", integration=integration)
         | 
| 8 | 
            -
                    self.api_key = None
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                def _get_headers(self):
         | 
| 11 | 
            -
                    if not self.api_key:
         | 
| 12 | 
            -
                        credentials = self.integration.get_credentials()
         | 
| 13 | 
            -
                        if not credentials:
         | 
| 14 | 
            -
                            raise ValueError("No credentials found")
         | 
| 15 | 
            -
                        api_key = (
         | 
| 16 | 
            -
                            credentials.get("api_key")
         | 
| 17 | 
            -
                            or credentials.get("API_KEY")
         | 
| 18 | 
            -
                            or credentials.get("apiKey")
         | 
| 19 | 
            -
                        )
         | 
| 20 | 
            -
                        if not api_key:
         | 
| 21 | 
            -
                            raise ValueError("No API key found")
         | 
| 22 | 
            -
                        self.api_key = api_key
         | 
| 23 | 
            -
                    return {
         | 
| 24 | 
            -
                        "Authorization": f"Bearer {self.api_key}",
         | 
| 25 | 
            -
                    }
         | 
| 26 8 |  | 
| 27 9 | 
             
                def send_email(self, to: str, subject: str, content: str) -> str:
         | 
| 28 | 
            -
                    """ | 
| 10 | 
            +
                    """
         | 
| 11 | 
            +
                    Sends an email using the Resend API with specified recipient, subject, and content
         | 
| 29 12 |  | 
| 30 13 | 
             
                    Args:
         | 
| 31 | 
            -
                        to:  | 
| 32 | 
            -
                        subject:  | 
| 33 | 
            -
                        content:  | 
| 14 | 
            +
                        to: Email address of the recipient
         | 
| 15 | 
            +
                        subject: Subject line of the email
         | 
| 16 | 
            +
                        content: Main body text content of the email
         | 
| 34 17 |  | 
| 35 18 | 
             
                    Returns:
         | 
| 36 | 
            -
                         | 
| 19 | 
            +
                        String message confirming successful email delivery ('Sent Successfully')
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    Raises:
         | 
| 22 | 
            +
                        ValueError: Raised when no valid credentials are found for the API
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    Tags:
         | 
| 25 | 
            +
                        send, email, api, communication, important
         | 
| 37 26 | 
             
                    """
         | 
| 38 27 | 
             
                    credentials = self.integration.get_credentials()
         | 
| 39 28 | 
             
                    if not credentials:
         | 
| @@ -1,57 +1,38 @@ | |
| 1 1 | 
             
            import httpx
         | 
| 2 | 
            -
            from loguru import logger
         | 
| 3 2 | 
             
            from serpapi import SerpApiClient as SerpApiSearch
         | 
| 4 3 |  | 
| 5 4 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 6 5 |  | 
| 7 6 |  | 
| 8 | 
            -
            class  | 
| 7 | 
            +
            class SerpapiApp(APIApplication):
         | 
| 9 8 | 
             
                def __init__(self, **kwargs):
         | 
| 10 9 | 
             
                    super().__init__(name="serpapi", **kwargs)
         | 
| 11 | 
            -
                    self.api_key: str | None = None
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                def _set_api_key(self):
         | 
| 14 | 
            -
                    if self.api_key is not None:
         | 
| 15 | 
            -
                        return
         | 
| 16 | 
            -
                    if not self.integration:
         | 
| 17 | 
            -
                        raise ValueError("Integration is None. Cannot retrieve SERP API Key.")
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                    credentials = self.integration.get_credentials()
         | 
| 20 | 
            -
                    if not credentials:
         | 
| 21 | 
            -
                        raise ValueError(
         | 
| 22 | 
            -
                            f"Failed to retrieve SERP API Key using integration '{self.integration.name}'. "
         | 
| 23 | 
            -
                            f"Check store configuration (e.g., ensure the correct environment variable is set)."
         | 
| 24 | 
            -
                        )
         | 
| 25 | 
            -
                    api_key = (
         | 
| 26 | 
            -
                        credentials.get("api_key")
         | 
| 27 | 
            -
                        or credentials.get("API_KEY")
         | 
| 28 | 
            -
                        or credentials.get("apiKey")
         | 
| 29 | 
            -
                    )
         | 
| 30 | 
            -
                    if not api_key:
         | 
| 31 | 
            -
                        raise ValueError(
         | 
| 32 | 
            -
                            f"Invalid credential format received for SERP API Key via integration '{self.integration.name}'. "
         | 
| 33 | 
            -
                        )
         | 
| 34 | 
            -
                    self.api_key = api_key
         | 
| 35 | 
            -
                    logger.info("SERP API Key successfully retrieved via integration.")
         | 
| 36 10 |  | 
| 37 11 | 
             
                async def search(self, params: dict[str, any] = None) -> str:
         | 
| 38 | 
            -
                    """ | 
| 12 | 
            +
                    """
         | 
| 13 | 
            +
                    Performs an asynchronous search using the SerpApi service and returns formatted search results.
         | 
| 39 14 |  | 
| 40 15 | 
             
                    Args:
         | 
| 41 | 
            -
                        params: Dictionary of engine-specific parameters (e.g., { | 
| 16 | 
            +
                        params: Dictionary of engine-specific parameters (e.g., {'q': 'Coffee', 'engine': 'google_light', 'location': 'Austin, TX'}). Defaults to None.
         | 
| 42 17 |  | 
| 43 18 | 
             
                    Returns:
         | 
| 44 | 
            -
                        A formatted string  | 
| 19 | 
            +
                        A formatted string containing search results with titles, links, and snippets, or an error message if the search fails.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    Raises:
         | 
| 22 | 
            +
                        httpx.HTTPStatusError: Raised when the API request fails due to HTTP errors (401 for invalid API key, 429 for rate limiting)
         | 
| 23 | 
            +
                        Exception: Raised for general errors such as network issues or invalid parameters
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    Tags:
         | 
| 26 | 
            +
                        search, async, web-scraping, api, serpapi, important
         | 
| 45 27 | 
             
                    """
         | 
| 46 28 | 
             
                    if params is None:
         | 
| 47 29 | 
             
                        params = {}
         | 
| 48 | 
            -
                    self. | 
| 30 | 
            +
                    api_key = self.integration.get_credentials().get("api_key")
         | 
| 49 31 | 
             
                    params = {
         | 
| 50 | 
            -
                        "api_key":  | 
| 32 | 
            +
                        "api_key": api_key,
         | 
| 51 33 | 
             
                        "engine": "google_light",  # Fastest engine by default
         | 
| 52 34 | 
             
                        **params,  # Include any additional parameters
         | 
| 53 35 | 
             
                    }
         | 
| 54 | 
            -
             | 
| 55 36 | 
             
                    try:
         | 
| 56 37 | 
             
                        search = SerpApiSearch(params)
         | 
| 57 38 | 
             
                        data = search.get_dict()
         | 
| @@ -7,36 +7,24 @@ class TavilyApp(APIApplication): | |
| 7 7 | 
             
                    name = "tavily"
         | 
| 8 8 | 
             
                    self.base_url = "https://api.tavily.com"
         | 
| 9 9 | 
             
                    super().__init__(name=name, integration=integration)
         | 
| 10 | 
            -
                    self.api_key = None
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                def _get_headers(self):
         | 
| 13 | 
            -
                    if not self.api_key:
         | 
| 14 | 
            -
                        credentials = self.integration.get_credentials()
         | 
| 15 | 
            -
                        if not credentials:
         | 
| 16 | 
            -
                            raise ValueError("No credentials found")
         | 
| 17 | 
            -
                        api_key = (
         | 
| 18 | 
            -
                            credentials.get("api_key")
         | 
| 19 | 
            -
                            or credentials.get("API_KEY")
         | 
| 20 | 
            -
                            or credentials.get("apiKey")
         | 
| 21 | 
            -
                        )
         | 
| 22 | 
            -
                        if not api_key:
         | 
| 23 | 
            -
                            raise ValueError("No API key found")
         | 
| 24 | 
            -
                        self.api_key = api_key
         | 
| 25 | 
            -
                    return {
         | 
| 26 | 
            -
                        "Authorization": f"Bearer {self.api_key}",
         | 
| 27 | 
            -
                        "Content-Type": "application/json",
         | 
| 28 | 
            -
                    }
         | 
| 29 10 |  | 
| 30 11 | 
             
                def search(self, query: str) -> str:
         | 
| 31 | 
            -
                    """ | 
| 12 | 
            +
                    """
         | 
| 13 | 
            +
                    Performs a web search using Tavily's search API and returns either a direct answer or a summary of top results.
         | 
| 32 14 |  | 
| 33 15 | 
             
                    Args:
         | 
| 34 | 
            -
                        query: The search query
         | 
| 16 | 
            +
                        query: The search query string to be processed by Tavily's search engine
         | 
| 35 17 |  | 
| 36 18 | 
             
                    Returns:
         | 
| 37 | 
            -
                         | 
| 19 | 
            +
                        A string containing either a direct answer from Tavily's AI or a formatted summary of the top 3 search results, with each result containing the title and snippet
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    Raises:
         | 
| 22 | 
            +
                        ValueError: When authentication credentials are invalid or missing (via validate() method)
         | 
| 23 | 
            +
                        HTTPError: When the API request fails or returns an error response
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    Tags:
         | 
| 26 | 
            +
                        search, ai, web, query, important, api-client, text-processing
         | 
| 38 27 | 
             
                    """
         | 
| 39 | 
            -
                    self.validate()
         | 
| 40 28 | 
             
                    url = f"{self.base_url}/search"
         | 
| 41 29 | 
             
                    payload = {
         | 
| 42 30 | 
             
                        "query": query,
         | 
| @@ -50,18 +38,13 @@ class TavilyApp(APIApplication): | |
| 50 38 | 
             
                        "include_domains": [],
         | 
| 51 39 | 
             
                        "exclude_domains": [],
         | 
| 52 40 | 
             
                    }
         | 
| 53 | 
            -
             | 
| 54 41 | 
             
                    response = self._post(url, payload)
         | 
| 55 42 | 
             
                    result = response.json()
         | 
| 56 | 
            -
             | 
| 57 43 | 
             
                    if "answer" in result:
         | 
| 58 44 | 
             
                        return result["answer"]
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                    # Fallback to combining top results if no direct answer
         | 
| 61 45 | 
             
                    summaries = []
         | 
| 62 46 | 
             
                    for item in result.get("results", [])[:3]:
         | 
| 63 47 | 
             
                        summaries.append(f"• {item['title']}: {item['snippet']}")
         | 
| 64 | 
            -
             | 
| 65 48 | 
             
                    return "\n".join(summaries)
         | 
| 66 49 |  | 
| 67 50 | 
             
                def list_tools(self):
         |