universal-mcp 0.1.1__py3-none-any.whl → 0.1.1rc1__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/__init__.py +23 -28
- universal_mcp/applications/application.py +13 -8
- universal_mcp/applications/e2b/app.py +74 -0
- universal_mcp/applications/firecrawl/app.py +381 -0
- universal_mcp/applications/github/README.md +35 -0
- universal_mcp/applications/github/app.py +133 -100
- universal_mcp/applications/google_calendar/app.py +170 -139
- universal_mcp/applications/google_mail/app.py +185 -160
- universal_mcp/applications/markitdown/app.py +32 -0
- universal_mcp/applications/reddit/app.py +112 -71
- universal_mcp/applications/resend/app.py +3 -8
- universal_mcp/applications/serp/app.py +84 -0
- universal_mcp/applications/tavily/app.py +11 -10
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/cli.py +98 -16
- universal_mcp/config.py +20 -3
- universal_mcp/exceptions.py +1 -3
- universal_mcp/integrations/__init__.py +6 -2
- universal_mcp/integrations/agentr.py +26 -24
- universal_mcp/integrations/integration.py +72 -35
- universal_mcp/servers/__init__.py +21 -1
- universal_mcp/servers/server.py +77 -44
- universal_mcp/stores/__init__.py +15 -2
- universal_mcp/stores/store.py +123 -13
- universal_mcp/utils/__init__.py +1 -0
- universal_mcp/utils/api_generator.py +269 -0
- universal_mcp/utils/docgen.py +360 -0
- universal_mcp/utils/installation.py +17 -2
- universal_mcp/utils/openapi.py +202 -104
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.1rc1.dist-info}/METADATA +22 -5
- universal_mcp-0.1.1rc1.dist-info/RECORD +37 -0
- universal_mcp-0.1.1.dist-info/RECORD +0 -29
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.1rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.1rc1.dist-info}/entry_points.txt +0 -0
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            from markitdown import MarkItDown
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from universal_mcp.applications.application import Application
         | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            class MarkitdownApp(Application):
         | 
| 7 | 
            +
                def __init__(self, **kwargs):
         | 
| 8 | 
            +
                    super().__init__(name="markitdown", **kwargs)
         | 
| 9 | 
            +
                    self.markitdown = MarkItDown()
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                async def convert_to_markdown(self, uri: str) -> str:
         | 
| 12 | 
            +
                    """Convert a web page, file, or data URI to markdown format.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    Args:
         | 
| 15 | 
            +
                        uri (str): The URI to convert. Supported URI schemes:
         | 
| 16 | 
            +
                            - http:// or https:// for web pages
         | 
| 17 | 
            +
                            - file:// for local files
         | 
| 18 | 
            +
                            - data: for data URIs
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    Returns:
         | 
| 21 | 
            +
                        str: The markdown representation of the resource content.
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    Example:
         | 
| 24 | 
            +
                        >>> await convert_to_markdown("https://example.com")
         | 
| 25 | 
            +
                        "# Example Domain\n\nThis domain is for use in illustrative examples..."
         | 
| 26 | 
            +
                    """
         | 
| 27 | 
            +
                    return self.markitdown.convert_uri(uri).markdown
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def list_tools(self):
         | 
| 30 | 
            +
                    return [
         | 
| 31 | 
            +
                        self.convert_to_markdown,
         | 
| 32 | 
            +
                    ]
         | 
| @@ -1,8 +1,10 @@ | |
| 1 1 | 
             
            import httpx
         | 
| 2 | 
            +
            from loguru import logger
         | 
| 3 | 
            +
             | 
| 2 4 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 3 | 
            -
            from universal_mcp.integrations import Integration
         | 
| 4 5 | 
             
            from universal_mcp.exceptions import NotAuthorizedError
         | 
| 5 | 
            -
            from  | 
| 6 | 
            +
            from universal_mcp.integrations import Integration
         | 
| 7 | 
            +
             | 
| 6 8 |  | 
| 7 9 | 
             
            class RedditApp(APIApplication):
         | 
| 8 10 | 
             
                def __init__(self, integration: Integration) -> None:
         | 
| @@ -32,126 +34,146 @@ class RedditApp(APIApplication): | |
| 32 34 | 
             
                        raise ValueError("Integration not configured for RedditApp")
         | 
| 33 35 | 
             
                    credentials = self.integration.get_credentials()
         | 
| 34 36 | 
             
                    if "access_token" not in credentials:
         | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            +
                        logger.error("Reddit credentials found but missing 'access_token'.")
         | 
| 38 | 
            +
                        raise ValueError("Invalid Reddit credentials format.")
         | 
| 37 39 |  | 
| 38 40 | 
             
                    return {
         | 
| 39 41 | 
             
                        "Authorization": f"Bearer {credentials['access_token']}",
         | 
| 40 | 
            -
                        "User-Agent": "agentr-reddit-app/0.1 by AgentR"
         | 
| 42 | 
            +
                        "User-Agent": "agentr-reddit-app/0.1 by AgentR",
         | 
| 41 43 | 
             
                    }
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                def get_subreddit_posts( | 
| 44 | 
            +
             | 
| 45 | 
            +
                def get_subreddit_posts(
         | 
| 46 | 
            +
                    self, subreddit: str, limit: int = 5, timeframe: str = "day"
         | 
| 47 | 
            +
                ) -> str:
         | 
| 44 48 | 
             
                    """Get the top posts from a specified subreddit over a given timeframe.
         | 
| 45 | 
            -
             | 
| 49 | 
            +
             | 
| 46 50 | 
             
                    Args:
         | 
| 47 51 | 
             
                        subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
         | 
| 48 52 | 
             
                        limit: The maximum number of posts to return (default: 5, max: 100).
         | 
| 49 53 | 
             
                        timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day').
         | 
| 50 | 
            -
             | 
| 54 | 
            +
             | 
| 51 55 | 
             
                    Returns:
         | 
| 52 56 | 
             
                        A formatted string listing the top posts or an error message.
         | 
| 53 57 | 
             
                    """
         | 
| 54 | 
            -
                    valid_timeframes = [ | 
| 58 | 
            +
                    valid_timeframes = ["hour", "day", "week", "month", "year", "all"]
         | 
| 55 59 | 
             
                    if timeframe not in valid_timeframes:
         | 
| 56 60 | 
             
                        return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
         | 
| 57 | 
            -
             | 
| 61 | 
            +
             | 
| 58 62 | 
             
                    if not 1 <= limit <= 100:
         | 
| 59 | 
            -
             | 
| 63 | 
            +
                        return (
         | 
| 64 | 
            +
                            f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
         | 
| 65 | 
            +
                        )
         | 
| 60 66 |  | 
| 61 | 
            -
                    
         | 
| 62 67 | 
             
                    url = f"{self.base_api_url}/r/{subreddit}/top"
         | 
| 63 | 
            -
                    params = {
         | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
                    
         | 
| 68 | 
            -
                    logger.info(f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'")
         | 
| 68 | 
            +
                    params = {"limit": limit, "t": timeframe}
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    logger.info(
         | 
| 71 | 
            +
                        f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'"
         | 
| 72 | 
            +
                    )
         | 
| 69 73 | 
             
                    response = self._get(url, params=params)
         | 
| 70 74 |  | 
| 71 75 | 
             
                    data = response.json()
         | 
| 72 | 
            -
             | 
| 76 | 
            +
             | 
| 73 77 | 
             
                    if "error" in data:
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                             | 
| 78 | 
            +
                        logger.error(
         | 
| 79 | 
            +
                            f"Reddit API error: {data['error']} - {data.get('message', '')}"
         | 
| 80 | 
            +
                        )
         | 
| 81 | 
            +
                        return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
         | 
| 76 82 |  | 
| 77 83 | 
             
                    posts = data.get("data", {}).get("children", [])
         | 
| 78 | 
            -
             | 
| 84 | 
            +
             | 
| 79 85 | 
             
                    if not posts:
         | 
| 80 | 
            -
                        return  | 
| 86 | 
            +
                        return (
         | 
| 87 | 
            +
                            f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
         | 
| 88 | 
            +
                        )
         | 
| 81 89 |  | 
| 82 | 
            -
                    result_lines = [ | 
| 90 | 
            +
                    result_lines = [
         | 
| 91 | 
            +
                        f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"
         | 
| 92 | 
            +
                    ]
         | 
| 83 93 | 
             
                    for i, post_container in enumerate(posts):
         | 
| 84 94 | 
             
                        post = post_container.get("data", {})
         | 
| 85 | 
            -
                        title = post.get( | 
| 86 | 
            -
                        score = post.get( | 
| 87 | 
            -
                        author = post.get( | 
| 88 | 
            -
                        permalink = post.get( | 
| 95 | 
            +
                        title = post.get("title", "No Title")
         | 
| 96 | 
            +
                        score = post.get("score", 0)
         | 
| 97 | 
            +
                        author = post.get("author", "Unknown Author")
         | 
| 98 | 
            +
                        permalink = post.get("permalink", "")
         | 
| 89 99 | 
             
                        full_url = f"https://www.reddit.com{permalink}" if permalink else "No Link"
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                        result_lines.append(f | 
| 100 | 
            +
             | 
| 101 | 
            +
                        result_lines.append(f'{i + 1}. "{title}" by u/{author} (Score: {score})')
         | 
| 92 102 | 
             
                        result_lines.append(f"   Link: {full_url}")
         | 
| 93 103 |  | 
| 94 104 | 
             
                    return "\n".join(result_lines)
         | 
| 95 105 |  | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 106 | 
            +
                def search_subreddits(
         | 
| 107 | 
            +
                    self, query: str, limit: int = 5, sort: str = "relevance"
         | 
| 108 | 
            +
                ) -> str:
         | 
| 98 109 | 
             
                    """Search for subreddits matching a query string.
         | 
| 99 | 
            -
             | 
| 110 | 
            +
             | 
| 100 111 | 
             
                    Args:
         | 
| 101 112 | 
             
                        query: The text to search for in subreddit names and descriptions.
         | 
| 102 113 | 
             
                        limit: The maximum number of subreddits to return (default: 5, max: 100).
         | 
| 103 114 | 
             
                        sort: The order of results. Valid options: 'relevance', 'activity' (default: 'relevance').
         | 
| 104 | 
            -
             | 
| 115 | 
            +
             | 
| 105 116 | 
             
                    Returns:
         | 
| 106 117 | 
             
                        A formatted string listing the found subreddits and their descriptions, or an error message.
         | 
| 107 118 | 
             
                    """
         | 
| 108 | 
            -
                    valid_sorts = [ | 
| 119 | 
            +
                    valid_sorts = ["relevance", "activity"]
         | 
| 109 120 | 
             
                    if sort not in valid_sorts:
         | 
| 110 121 | 
             
                        return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
         | 
| 111 | 
            -
             | 
| 122 | 
            +
             | 
| 112 123 | 
             
                    if not 1 <= limit <= 100:
         | 
| 113 | 
            -
             | 
| 124 | 
            +
                        return (
         | 
| 125 | 
            +
                            f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
         | 
| 126 | 
            +
                        )
         | 
| 114 127 |  | 
| 115 | 
            -
                
         | 
| 116 128 | 
             
                    url = f"{self.base_api_url}/subreddits/search"
         | 
| 117 129 | 
             
                    params = {
         | 
| 118 130 | 
             
                        "q": query,
         | 
| 119 131 | 
             
                        "limit": limit,
         | 
| 120 132 | 
             
                        "sort": sort,
         | 
| 121 133 | 
             
                        # Optionally include NSFW results? Defaulting to false for safety.
         | 
| 122 | 
            -
                        # "include_over_18": "false" | 
| 134 | 
            +
                        # "include_over_18": "false"
         | 
| 123 135 | 
             
                    }
         | 
| 124 | 
            -
             | 
| 125 | 
            -
                    logger.info( | 
| 136 | 
            +
             | 
| 137 | 
            +
                    logger.info(
         | 
| 138 | 
            +
                        f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})"
         | 
| 139 | 
            +
                    )
         | 
| 126 140 | 
             
                    response = self._get(url, params=params)
         | 
| 127 | 
            -
             | 
| 141 | 
            +
             | 
| 128 142 | 
             
                    data = response.json()
         | 
| 129 143 |  | 
| 130 144 | 
             
                    if "error" in data:
         | 
| 131 | 
            -
             | 
| 132 | 
            -
                             | 
| 145 | 
            +
                        logger.error(
         | 
| 146 | 
            +
                            f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}"
         | 
| 147 | 
            +
                        )
         | 
| 148 | 
            +
                        return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
         | 
| 133 149 |  | 
| 134 150 | 
             
                    subreddits = data.get("data", {}).get("children", [])
         | 
| 135 | 
            -
             | 
| 151 | 
            +
             | 
| 136 152 | 
             
                    if not subreddits:
         | 
| 137 153 | 
             
                        return f"No subreddits found matching the query '{query}'."
         | 
| 138 154 |  | 
| 139 | 
            -
                    result_lines = [ | 
| 155 | 
            +
                    result_lines = [
         | 
| 156 | 
            +
                        f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"
         | 
| 157 | 
            +
                    ]
         | 
| 140 158 | 
             
                    for i, sub_container in enumerate(subreddits):
         | 
| 141 159 | 
             
                        sub_data = sub_container.get("data", {})
         | 
| 142 | 
            -
                        display_name = sub_data.get( | 
| 143 | 
            -
                        title = sub_data.get( | 
| 144 | 
            -
             | 
| 160 | 
            +
                        display_name = sub_data.get("display_name", "N/A")  # e.g., 'python'
         | 
| 161 | 
            +
                        title = sub_data.get(
         | 
| 162 | 
            +
                            "title", "No Title"
         | 
| 163 | 
            +
                        )  # Often the same as display_name or slightly longer
         | 
| 164 | 
            +
                        subscribers = sub_data.get("subscribers", 0)
         | 
| 145 165 | 
             
                        # Use public_description if available, fallback to title
         | 
| 146 | 
            -
                        description = sub_data.get( | 
| 147 | 
            -
             | 
| 166 | 
            +
                        description = sub_data.get("public_description", "").strip() or title
         | 
| 167 | 
            +
             | 
| 148 168 | 
             
                        # Format subscriber count nicely
         | 
| 149 169 | 
             
                        subscriber_str = f"{subscribers:,}" if subscribers else "Unknown"
         | 
| 150 | 
            -
             | 
| 151 | 
            -
                        result_lines.append( | 
| 170 | 
            +
             | 
| 171 | 
            +
                        result_lines.append(
         | 
| 172 | 
            +
                            f"{i + 1}. r/{display_name} ({subscriber_str} subscribers)"
         | 
| 173 | 
            +
                        )
         | 
| 152 174 | 
             
                        if description:
         | 
| 153 175 | 
             
                            result_lines.append(f"   Description: {description}")
         | 
| 154 | 
            -
             | 
| 176 | 
            +
             | 
| 155 177 | 
             
                    return "\n".join(result_lines)
         | 
| 156 178 |  | 
| 157 179 | 
             
                def get_post_flairs(self, subreddit: str):
         | 
| @@ -163,9 +185,9 @@ class RedditApp(APIApplication): | |
| 163 185 | 
             
                    Returns:
         | 
| 164 186 | 
             
                        A list of dictionaries containing flair details, or an error message.
         | 
| 165 187 | 
             
                    """
         | 
| 166 | 
            -
             | 
| 188 | 
            +
             | 
| 167 189 | 
             
                    url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
         | 
| 168 | 
            -
             | 
| 190 | 
            +
             | 
| 169 191 | 
             
                    logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
         | 
| 170 192 | 
             
                    response = self._get(url)
         | 
| 171 193 |  | 
| @@ -174,8 +196,16 @@ class RedditApp(APIApplication): | |
| 174 196 | 
             
                        return f"No post flairs available for r/{subreddit}."
         | 
| 175 197 |  | 
| 176 198 | 
             
                    return flairs
         | 
| 177 | 
            -
             | 
| 178 | 
            -
                def create_post( | 
| 199 | 
            +
             | 
| 200 | 
            +
                def create_post(
         | 
| 201 | 
            +
                    self,
         | 
| 202 | 
            +
                    subreddit: str,
         | 
| 203 | 
            +
                    title: str,
         | 
| 204 | 
            +
                    kind: str = "self",
         | 
| 205 | 
            +
                    text: str = None,
         | 
| 206 | 
            +
                    url: str = None,
         | 
| 207 | 
            +
                    flair_id: str = None,
         | 
| 208 | 
            +
                ):
         | 
| 179 209 | 
             
                    """Create a new post in a specified subreddit.
         | 
| 180 210 |  | 
| 181 211 | 
             
                    Args:
         | 
| @@ -186,7 +216,7 @@ class RedditApp(APIApplication): | |
| 186 216 | 
             
                        url: The URL of the link or image; required if kind is 'link'.
         | 
| 187 217 | 
             
                            For image posts to be displayed correctly, the URL must directly point to an image file
         | 
| 188 218 | 
             
                            and end with a valid image extension (e.g., .jpg, .png, or .gif).
         | 
| 189 | 
            -
                            Note that .gif support can be inconsistent. | 
| 219 | 
            +
                            Note that .gif support can be inconsistent.
         | 
| 190 220 | 
             
                        flair_id: The ID of the flair to assign to the post.
         | 
| 191 221 |  | 
| 192 222 | 
             
                    Returns:
         | 
| @@ -218,10 +248,16 @@ class RedditApp(APIApplication): | |
| 218 248 | 
             
                    response_json = response.json()
         | 
| 219 249 |  | 
| 220 250 | 
             
                    # Check for Reddit API errors in the response
         | 
| 221 | 
            -
                    if  | 
| 251 | 
            +
                    if (
         | 
| 252 | 
            +
                        response_json
         | 
| 253 | 
            +
                        and "json" in response_json
         | 
| 254 | 
            +
                        and "errors" in response_json["json"]
         | 
| 255 | 
            +
                    ):
         | 
| 222 256 | 
             
                        errors = response_json["json"]["errors"]
         | 
| 223 257 | 
             
                        if errors:
         | 
| 224 | 
            -
                            error_message = ", ".join( | 
| 258 | 
            +
                            error_message = ", ".join(
         | 
| 259 | 
            +
                                [f"{code}: {message}" for code, message in errors]
         | 
| 260 | 
            +
                            )
         | 
| 225 261 | 
             
                            return f"Reddit API error: {error_message}"
         | 
| 226 262 |  | 
| 227 263 | 
             
                    return response_json
         | 
| @@ -241,7 +277,7 @@ class RedditApp(APIApplication): | |
| 241 277 | 
             
                    url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
         | 
| 242 278 |  | 
| 243 279 | 
             
                    # Make the GET request to the Reddit API
         | 
| 244 | 
            -
             | 
| 280 | 
            +
             | 
| 245 281 | 
             
                    response = self._get(url)
         | 
| 246 282 |  | 
| 247 283 | 
             
                    data = response.json()
         | 
| @@ -250,7 +286,7 @@ class RedditApp(APIApplication): | |
| 250 286 | 
             
                        return comments[0]["data"]
         | 
| 251 287 | 
             
                    else:
         | 
| 252 288 | 
             
                        return {"error": "Comment not found."}
         | 
| 253 | 
            -
             | 
| 289 | 
            +
             | 
| 254 290 | 
             
                def post_comment(self, parent_id: str, text: str) -> dict:
         | 
| 255 291 | 
             
                    """
         | 
| 256 292 | 
             
                    Post a comment to a Reddit post or another comment.
         | 
| @@ -262,7 +298,7 @@ class RedditApp(APIApplication): | |
| 262 298 | 
             
                    Returns:
         | 
| 263 299 | 
             
                        A dictionary containing the response from the Reddit API, or an error message if posting fails.
         | 
| 264 300 | 
             
                    """
         | 
| 265 | 
            -
             | 
| 301 | 
            +
             | 
| 266 302 | 
             
                    url = f"{self.base_api_url}/api/comment"
         | 
| 267 303 | 
             
                    data = {
         | 
| 268 304 | 
             
                        "parent": parent_id,
         | 
| @@ -285,7 +321,7 @@ class RedditApp(APIApplication): | |
| 285 321 | 
             
                    Returns:
         | 
| 286 322 | 
             
                        A dictionary containing the response from the Reddit API, or an error message if editing fails.
         | 
| 287 323 | 
             
                    """
         | 
| 288 | 
            -
             | 
| 324 | 
            +
             | 
| 289 325 | 
             
                    url = f"{self.base_api_url}/api/editusertext"
         | 
| 290 326 | 
             
                    data = {
         | 
| 291 327 | 
             
                        "thing_id": content_id,
         | 
| @@ -297,7 +333,6 @@ class RedditApp(APIApplication): | |
| 297 333 |  | 
| 298 334 | 
             
                    return response.json()
         | 
| 299 335 |  | 
| 300 | 
            -
                    
         | 
| 301 336 | 
             
                def delete_content(self, content_id: str) -> dict:
         | 
| 302 337 | 
             
                    """
         | 
| 303 338 | 
             
                    Delete a Reddit post or comment.
         | 
| @@ -308,7 +343,7 @@ class RedditApp(APIApplication): | |
| 308 343 | 
             
                    Returns:
         | 
| 309 344 | 
             
                        A dictionary containing the response from the Reddit API, or an error message if deletion fails.
         | 
| 310 345 | 
             
                    """
         | 
| 311 | 
            -
             | 
| 346 | 
            +
             | 
| 312 347 | 
             
                    url = f"{self.base_api_url}/api/del"
         | 
| 313 348 | 
             
                    data = {
         | 
| 314 349 | 
             
                        "id": content_id,
         | 
| @@ -324,6 +359,12 @@ class RedditApp(APIApplication): | |
| 324 359 |  | 
| 325 360 | 
             
                def list_tools(self):
         | 
| 326 361 | 
             
                    return [
         | 
| 327 | 
            -
                        self.get_subreddit_posts, | 
| 328 | 
            -
                        self. | 
| 329 | 
            -
             | 
| 362 | 
            +
                        self.get_subreddit_posts,
         | 
| 363 | 
            +
                        self.search_subreddits,
         | 
| 364 | 
            +
                        self.get_post_flairs,
         | 
| 365 | 
            +
                        self.create_post,
         | 
| 366 | 
            +
                        self.get_comment_by_id,
         | 
| 367 | 
            +
                        self.post_comment,
         | 
| 368 | 
            +
                        self.edit_content,
         | 
| 369 | 
            +
                        self.delete_content,
         | 
| 370 | 
            +
                    ]
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 2 2 | 
             
            from universal_mcp.integrations import Integration
         | 
| 3 3 |  | 
| 4 | 
            +
             | 
| 4 5 | 
             
            class ResendApp(APIApplication):
         | 
| 5 6 | 
             
                def __init__(self, integration: Integration) -> None:
         | 
| 6 7 | 
             
                    super().__init__(name="resend", integration=integration)
         | 
| @@ -20,7 +21,7 @@ class ResendApp(APIApplication): | |
| 20 21 | 
             
                        to: The email address to send the email to
         | 
| 21 22 | 
             
                        subject: The subject of the email
         | 
| 22 23 | 
             
                        content: The content of the email
         | 
| 23 | 
            -
             | 
| 24 | 
            +
             | 
| 24 25 | 
             
                    Returns:
         | 
| 25 26 | 
             
                        A message indicating that the email was sent successfully
         | 
| 26 27 | 
             
                    """
         | 
| @@ -29,15 +30,9 @@ class ResendApp(APIApplication): | |
| 29 30 | 
             
                        raise ValueError("No credentials found")
         | 
| 30 31 | 
             
                    from_email = credentials.get("from_email", "Manoj <manoj@agentr.dev>")
         | 
| 31 32 | 
             
                    url = "https://api.resend.com/emails"
         | 
| 32 | 
            -
                    body = {
         | 
| 33 | 
            -
                        "from": from_email,
         | 
| 34 | 
            -
                        "to": [to],
         | 
| 35 | 
            -
                        "subject": subject,
         | 
| 36 | 
            -
                        "text": content
         | 
| 37 | 
            -
                    }
         | 
| 33 | 
            +
                    body = {"from": from_email, "to": [to], "subject": subject, "text": content}
         | 
| 38 34 | 
             
                    self._post(url, body)
         | 
| 39 35 | 
             
                    return "Sent Successfully"
         | 
| 40 36 |  | 
| 41 37 | 
             
                def list_tools(self):
         | 
| 42 38 | 
             
                    return [self.send_email]
         | 
| 43 | 
            -
             | 
| @@ -0,0 +1,84 @@ | |
| 1 | 
            +
            import httpx
         | 
| 2 | 
            +
            from loguru import logger
         | 
| 3 | 
            +
            from serpapi import SerpApiClient as SerpApiSearch
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            from universal_mcp.applications.application import APIApplication
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
            class SerpApp(APIApplication):
         | 
| 9 | 
            +
                def __init__(self, **kwargs):
         | 
| 10 | 
            +
                    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 | 
            +
             | 
| 26 | 
            +
                    self.api_key = credentials
         | 
| 27 | 
            +
                    logger.info("SERP API Key successfully retrieved via integration.")
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                async def search(self, params: dict[str, any] = None) -> str:
         | 
| 30 | 
            +
                    """Perform a search on the specified engine using SerpApi.
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    Args:
         | 
| 33 | 
            +
                        params: Dictionary of engine-specific parameters (e.g., {"q": "Coffee", "engine": "google_light", "location": "Austin, TX"}).
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    Returns:
         | 
| 36 | 
            +
                        A formatted string of search results or an error message.
         | 
| 37 | 
            +
                    """
         | 
| 38 | 
            +
                    if params is None:
         | 
| 39 | 
            +
                        params = {}
         | 
| 40 | 
            +
                    self._set_api_key()
         | 
| 41 | 
            +
                    params = {
         | 
| 42 | 
            +
                        "api_key": self.api_key,
         | 
| 43 | 
            +
                        "engine": "google_light",  # Fastest engine by default
         | 
| 44 | 
            +
                        **params,  # Include any additional parameters
         | 
| 45 | 
            +
                    }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    try:
         | 
| 48 | 
            +
                        search = SerpApiSearch(params)
         | 
| 49 | 
            +
                        data = search.get_dict()
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                        # Process organic search results if available
         | 
| 52 | 
            +
                        if "organic_results" in data:
         | 
| 53 | 
            +
                            formatted_results = []
         | 
| 54 | 
            +
                            for result in data.get("organic_results", []):
         | 
| 55 | 
            +
                                title = result.get("title", "No title")
         | 
| 56 | 
            +
                                link = result.get("link", "No link")
         | 
| 57 | 
            +
                                snippet = result.get("snippet", "No snippet")
         | 
| 58 | 
            +
                                formatted_results.append(
         | 
| 59 | 
            +
                                    f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n"
         | 
| 60 | 
            +
                                )
         | 
| 61 | 
            +
                            return (
         | 
| 62 | 
            +
                                "\n".join(formatted_results)
         | 
| 63 | 
            +
                                if formatted_results
         | 
| 64 | 
            +
                                else "No organic results found"
         | 
| 65 | 
            +
                            )
         | 
| 66 | 
            +
                        else:
         | 
| 67 | 
            +
                            return "No organic results found"
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    # Handle HTTP-specific errors
         | 
| 70 | 
            +
                    except httpx.HTTPStatusError as e:
         | 
| 71 | 
            +
                        if e.response.status_code == 429:
         | 
| 72 | 
            +
                            return "Error: Rate limit exceeded. Please try again later."
         | 
| 73 | 
            +
                        elif e.response.status_code == 401:
         | 
| 74 | 
            +
                            return "Error: Invalid API key. Please check your SERPAPI_API_KEY."
         | 
| 75 | 
            +
                        else:
         | 
| 76 | 
            +
                            return f"Error: {e.response.status_code} - {e.response.text}"
         | 
| 77 | 
            +
                    # Handle other exceptions (e.g., network issues)
         | 
| 78 | 
            +
                    except Exception as e:
         | 
| 79 | 
            +
                        return f"Error: {str(e)}"
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def list_tools(self):
         | 
| 82 | 
            +
                    return [
         | 
| 83 | 
            +
                        self.search,
         | 
| 84 | 
            +
                    ]
         | 
| @@ -1,27 +1,28 @@ | |
| 1 1 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 2 2 | 
             
            from universal_mcp.integrations import Integration
         | 
| 3 3 |  | 
| 4 | 
            +
             | 
| 4 5 | 
             
            class TavilyApp(APIApplication):
         | 
| 5 6 | 
             
                def __init__(self, integration: Integration) -> None:
         | 
| 6 7 | 
             
                    name = "tavily"
         | 
| 7 8 | 
             
                    self.base_url = "https://api.tavily.com"
         | 
| 8 9 | 
             
                    super().__init__(name=name, integration=integration)
         | 
| 9 | 
            -
             | 
| 10 | 
            +
             | 
| 10 11 | 
             
                def _get_headers(self):
         | 
| 11 12 | 
             
                    credentials = self.integration.get_credentials()
         | 
| 12 13 | 
             
                    if not credentials:
         | 
| 13 14 | 
             
                        raise ValueError("No credentials found")
         | 
| 14 15 | 
             
                    return {
         | 
| 15 16 | 
             
                        "Authorization": f"Bearer {credentials['api_key']}",
         | 
| 16 | 
            -
                        "Content-Type": "application/json"
         | 
| 17 | 
            +
                        "Content-Type": "application/json",
         | 
| 17 18 | 
             
                    }
         | 
| 18 19 |  | 
| 19 20 | 
             
                def search(self, query: str) -> str:
         | 
| 20 21 | 
             
                    """Search the web using Tavily's search API
         | 
| 21 | 
            -
             | 
| 22 | 
            +
             | 
| 22 23 | 
             
                    Args:
         | 
| 23 24 | 
             
                        query: The search query
         | 
| 24 | 
            -
             | 
| 25 | 
            +
             | 
| 25 26 | 
             
                    Returns:
         | 
| 26 27 | 
             
                        str: A summary of search results
         | 
| 27 28 | 
             
                    """
         | 
| @@ -37,21 +38,21 @@ class TavilyApp(APIApplication): | |
| 37 38 | 
             
                        "include_images": False,
         | 
| 38 39 | 
             
                        "include_image_descriptions": False,
         | 
| 39 40 | 
             
                        "include_domains": [],
         | 
| 40 | 
            -
                        "exclude_domains": []
         | 
| 41 | 
            +
                        "exclude_domains": [],
         | 
| 41 42 | 
             
                    }
         | 
| 42 | 
            -
             | 
| 43 | 
            +
             | 
| 43 44 | 
             
                    response = self._post(url, payload)
         | 
| 44 45 | 
             
                    result = response.json()
         | 
| 45 | 
            -
             | 
| 46 | 
            +
             | 
| 46 47 | 
             
                    if "answer" in result:
         | 
| 47 48 | 
             
                        return result["answer"]
         | 
| 48 | 
            -
             | 
| 49 | 
            +
             | 
| 49 50 | 
             
                    # Fallback to combining top results if no direct answer
         | 
| 50 51 | 
             
                    summaries = []
         | 
| 51 52 | 
             
                    for item in result.get("results", [])[:3]:
         | 
| 52 53 | 
             
                        summaries.append(f"• {item['title']}: {item['snippet']}")
         | 
| 53 | 
            -
             | 
| 54 | 
            +
             | 
| 54 55 | 
             
                    return "\n".join(summaries)
         | 
| 55 | 
            -
             | 
| 56 | 
            +
             | 
| 56 57 | 
             
                def list_tools(self):
         | 
| 57 58 | 
             
                    return [self.search]
         | 
| @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            from universal_mcp.applications.application import APIApplication
         | 
| 2 2 |  | 
| 3 3 |  | 
| 4 | 
            -
            class  | 
| 4 | 
            +
            class ZenquotesApp(APIApplication):
         | 
| 5 5 | 
             
                def __init__(self, **kwargs) -> None:
         | 
| 6 6 | 
             
                    super().__init__(name="zenquote", **kwargs)
         | 
| 7 7 |  | 
| 8 8 | 
             
                def get_quote(self) -> str:
         | 
| 9 9 | 
             
                    """Get an inspirational quote from the Zen Quotes API
         | 
| 10 | 
            -
             | 
| 10 | 
            +
             | 
| 11 11 | 
             
                    Returns:
         | 
| 12 12 | 
             
                        A random inspirational quote
         | 
| 13 13 | 
             
                    """
         | 
| @@ -18,4 +18,4 @@ class ZenQuoteApp(APIApplication): | |
| 18 18 | 
             
                    return f"{quote_data['q']} - {quote_data['a']}"
         | 
| 19 19 |  | 
| 20 20 | 
             
                def list_tools(self):
         | 
| 21 | 
            -
                    return [self.get_quote]
         | 
| 21 | 
            +
                    return [self.get_quote]
         |