feat(reddit): add Reddit health checker and update tool functions
This commit is contained in:
@@ -8,6 +8,7 @@ to verify the credential works.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
|
||||
@@ -488,6 +489,97 @@ class ResendHealthChecker:
|
||||
)
|
||||
|
||||
|
||||
class RedditHealthChecker:
|
||||
"""Health checker for Reddit OAuth credentials."""
|
||||
|
||||
TIMEOUT = 10.0
|
||||
|
||||
def check(self, credentials_json: str) -> HealthCheckResult:
|
||||
"""
|
||||
Validate Reddit OAuth credentials using PRAW.
|
||||
|
||||
Expects credentials_json to be a JSON string with:
|
||||
- client_id
|
||||
- client_secret
|
||||
- refresh_token
|
||||
- user_agent
|
||||
"""
|
||||
try:
|
||||
# Parse credentials
|
||||
try:
|
||||
creds = json.loads(credentials_json)
|
||||
except json.JSONDecodeError:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Reddit credentials must be valid JSON",
|
||||
details={"error": "invalid_json"},
|
||||
)
|
||||
|
||||
required_fields = ["client_id", "client_secret", "refresh_token", "user_agent"]
|
||||
missing = [f for f in required_fields if f not in creds]
|
||||
if missing:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Missing required fields: {', '.join(missing)}",
|
||||
details={"missing_fields": missing},
|
||||
)
|
||||
|
||||
# Import praw here to avoid dependency in health_check.py if not needed
|
||||
try:
|
||||
import praw
|
||||
except ImportError:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="PRAW library not installed (pip install praw)",
|
||||
details={"error": "missing_dependency"},
|
||||
)
|
||||
|
||||
# Create Reddit instance and validate
|
||||
try:
|
||||
reddit = praw.Reddit(
|
||||
client_id=creds["client_id"],
|
||||
client_secret=creds["client_secret"],
|
||||
refresh_token=creds["refresh_token"],
|
||||
user_agent=creds["user_agent"],
|
||||
)
|
||||
|
||||
# Make a lightweight API call to validate credentials
|
||||
user = reddit.user.me()
|
||||
username = str(user)
|
||||
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message=f"Reddit credentials valid (authenticated as u/{username})",
|
||||
details={"username": username},
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if "401" in error_msg or "unauthorized" in error_msg:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Reddit credentials are invalid or expired",
|
||||
details={"error": "unauthorized"},
|
||||
)
|
||||
elif "403" in error_msg or "forbidden" in error_msg:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Reddit credentials lack required scopes",
|
||||
details={"error": "insufficient_scope"},
|
||||
)
|
||||
else:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Reddit API error: {str(e)}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
except Exception as e:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Failed to validate Reddit credentials: {str(e)}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
# Registry of health checkers
|
||||
HEALTH_CHECKERS: dict[str, CredentialHealthChecker] = {
|
||||
"hubspot": HubSpotHealthChecker(),
|
||||
@@ -497,6 +589,7 @@ HEALTH_CHECKERS: dict[str, CredentialHealthChecker] = {
|
||||
"anthropic": AnthropicHealthChecker(),
|
||||
"github": GitHubHealthChecker(),
|
||||
"resend": ResendHealthChecker(),
|
||||
"reddit": RedditHealthChecker(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ REDDIT_CREDENTIALS = {
|
||||
tools=[
|
||||
# Search & Monitoring
|
||||
"reddit_search_posts",
|
||||
"reddit_search_comments",
|
||||
"reddit_get_subreddit_new",
|
||||
"reddit_get_subreddit_hot",
|
||||
"reddit_get_post",
|
||||
|
||||
@@ -213,20 +213,19 @@ def register_all_tools(
|
||||
"slack_get_team_stats",
|
||||
# Reddit tools
|
||||
"reddit_search_posts",
|
||||
"reddit_search_comments",
|
||||
"reddit_get_subreddit_posts",
|
||||
"reddit_get_subreddit_new",
|
||||
"reddit_get_subreddit_hot",
|
||||
"reddit_get_post",
|
||||
"reddit_get_post_comments",
|
||||
"reddit_get_user_posts",
|
||||
"reddit_get_comments",
|
||||
"reddit_submit_post",
|
||||
"reddit_reply_to_post",
|
||||
"reddit_reply_to_comment",
|
||||
"reddit_edit_comment",
|
||||
"reddit_delete_comment",
|
||||
"reddit_get_user_profile",
|
||||
"reddit_vote",
|
||||
"reddit_upvote",
|
||||
"reddit_downvote",
|
||||
"reddit_save_post",
|
||||
"reddit_unsave_post",
|
||||
"reddit_remove_post",
|
||||
"reddit_approve_post",
|
||||
"reddit_ban_user",
|
||||
|
||||
@@ -4,13 +4,12 @@ Community management and content monitoring tool for Reddit. Monitor brand menti
|
||||
|
||||
## Features
|
||||
|
||||
### Search & Monitoring (6 functions)
|
||||
### Search & Monitoring (5 functions)
|
||||
- **reddit_search_posts**: Search for posts matching keywords
|
||||
- **reddit_search_comments**: Search for comments (via post search + comment retrieval)
|
||||
- **reddit_get_subreddit_posts**: Get hot/new/top posts from a subreddit
|
||||
- **reddit_get_subreddit_new**: Get new posts from a subreddit
|
||||
- **reddit_get_subreddit_hot**: Get hot posts from a subreddit
|
||||
- **reddit_get_post**: Retrieve specific post details
|
||||
- **reddit_get_post_comments**: Get all comments from a post
|
||||
- **reddit_get_user_posts**: Monitor user posting activity
|
||||
- **reddit_get_comments**: Get all comments from a post
|
||||
|
||||
### Content Creation (5 functions)
|
||||
- **reddit_submit_post**: Create text or link posts
|
||||
@@ -21,9 +20,9 @@ Community management and content monitoring tool for Reddit. Monitor brand menti
|
||||
|
||||
### User Engagement (4 functions)
|
||||
- **reddit_get_user_profile**: View user profiles and karma
|
||||
- **reddit_vote**: Upvote/downvote posts and comments
|
||||
- **reddit_upvote**: Upvote posts and comments
|
||||
- **reddit_downvote**: Downvote posts and comments
|
||||
- **reddit_save_post**: Bookmark posts
|
||||
- **reddit_unsave_post**: Remove bookmarks
|
||||
|
||||
### Moderation (3 functions - requires moderator permissions)
|
||||
- **reddit_remove_post**: Remove posts as a moderator
|
||||
@@ -96,9 +95,8 @@ for post in result["posts"]:
|
||||
|
||||
```python
|
||||
# Get hot posts from a specific subreddit
|
||||
result = reddit_get_subreddit_posts(
|
||||
result = reddit_get_subreddit_hot(
|
||||
subreddit="python",
|
||||
feed_type="hot",
|
||||
limit=25
|
||||
)
|
||||
|
||||
@@ -116,7 +114,7 @@ result = reddit_reply_to_post(
|
||||
)
|
||||
|
||||
# Upvote the post
|
||||
reddit_vote(item_id="abc123", direction="up")
|
||||
reddit_upvote(item_id="abc123")
|
||||
```
|
||||
|
||||
### Create Content
|
||||
@@ -136,7 +134,7 @@ print(f"Post created: {result['permalink']}")
|
||||
|
||||
```python
|
||||
# Get all comments from a post
|
||||
result = reddit_get_post_comments(
|
||||
result = reddit_get_comments(
|
||||
post_id="abc123",
|
||||
sort="best",
|
||||
limit=100
|
||||
@@ -163,19 +161,29 @@ Search for Reddit posts matching a query.
|
||||
|
||||
**Returns:** Dict with `query`, `subreddit`, `count`, and `posts` array
|
||||
|
||||
### reddit_get_subreddit_posts
|
||||
### reddit_get_subreddit_new
|
||||
|
||||
Get posts from a subreddit feed.
|
||||
Get new posts from a subreddit.
|
||||
|
||||
**Arguments:**
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| subreddit | str | Required | Subreddit name (e.g., "python") |
|
||||
| feed_type | str | "hot" | "hot", "new", "top", "rising", "controversial" |
|
||||
| time_filter | str | "all" | Time period for "top"/"controversial" |
|
||||
| limit | int | 25 | Maximum posts to return (1-100) |
|
||||
|
||||
**Returns:** Dict with `subreddit`, `feed_type`, `count`, and `posts` array
|
||||
**Returns:** Dict with `subreddit`, `count`, and `posts` array
|
||||
|
||||
### reddit_get_subreddit_hot
|
||||
|
||||
Get hot posts from a subreddit.
|
||||
|
||||
**Arguments:**
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| subreddit | str | Required | Subreddit name (e.g., "python") |
|
||||
| limit | int | 25 | Maximum posts to return (1-100) |
|
||||
|
||||
**Returns:** Dict with `subreddit`, `count`, and `posts` array
|
||||
|
||||
### reddit_get_post
|
||||
|
||||
@@ -188,7 +196,7 @@ Get a specific Reddit post by ID.
|
||||
|
||||
**Returns:** Dict with `success` and `post` object
|
||||
|
||||
### reddit_get_post_comments
|
||||
### reddit_get_comments
|
||||
|
||||
Get comments from a Reddit post.
|
||||
|
||||
@@ -228,17 +236,27 @@ Reply to a Reddit post.
|
||||
|
||||
**Returns:** Dict with `success`, `comment_id`, and `permalink`
|
||||
|
||||
### reddit_vote
|
||||
### reddit_upvote
|
||||
|
||||
Vote on a post or comment.
|
||||
Upvote a post or comment.
|
||||
|
||||
**Arguments:**
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| item_id | str | Required | Reddit post or comment ID |
|
||||
| direction | str | Required | "up" (upvote), "down" (downvote), "clear" |
|
||||
|
||||
**Returns:** Dict with `success`, `item_id`, `direction`, and `message`
|
||||
**Returns:** Dict with `success`, `item_id`, and `message`
|
||||
|
||||
### reddit_downvote
|
||||
|
||||
Downvote a post or comment.
|
||||
|
||||
**Arguments:**
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| item_id | str | Required | Reddit post or comment ID |
|
||||
|
||||
**Returns:** Dict with `success`, `item_id`, and `message`
|
||||
|
||||
### reddit_get_user_profile
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ Supports:
|
||||
- OAuth 2.0 authentication via REDDIT_CREDENTIALS
|
||||
- Search & Monitoring (5 functions)
|
||||
- Content Creation (5 functions)
|
||||
- User Engagement (3 functions)
|
||||
- User Engagement (4 functions)
|
||||
- Moderation (3 functions)
|
||||
|
||||
Total: 18 tools
|
||||
Total: 17 tools
|
||||
|
||||
API Reference: https://www.reddit.com/dev/api/
|
||||
PRAW Documentation: https://praw.readthedocs.io/
|
||||
@@ -203,50 +203,6 @@ def register_tools(
|
||||
except Exception as e:
|
||||
return {"error": f"Search failed: {str(e)}"}
|
||||
|
||||
@mcp.tool()
|
||||
def reddit_search_comments(
|
||||
query: str,
|
||||
subreddit: str = "all",
|
||||
time_filter: str = "all",
|
||||
sort: str = "relevance",
|
||||
limit: int = 10,
|
||||
) -> dict:
|
||||
"""
|
||||
Search for Reddit comments matching a query.
|
||||
|
||||
Use this to monitor brand mentions or discussions in comments.
|
||||
|
||||
Args:
|
||||
query: Search query (1-512 characters)
|
||||
subreddit: Subreddit name or "all" for site-wide search
|
||||
time_filter: Time period - "hour", "day", "week", "month", "year", "all"
|
||||
sort: Sort method - "relevance", "new", "top"
|
||||
limit: Maximum number of comments to return (1-100)
|
||||
|
||||
Returns:
|
||||
Dict with search results or error dict
|
||||
"""
|
||||
if not query or len(query) > 512:
|
||||
return {"error": "Query must be 1-512 characters"}
|
||||
|
||||
limit = max(1, min(100, limit))
|
||||
|
||||
reddit = _get_reddit_client(credentials)
|
||||
if isinstance(reddit, dict):
|
||||
return reddit
|
||||
|
||||
try:
|
||||
# PRAW's search returns submissions, not comments directly
|
||||
# To get comments, use reddit_search_posts and then reddit_get_comments
|
||||
return {
|
||||
"error": "Comment search not directly supported by PRAW",
|
||||
"help": (
|
||||
"Use reddit_search_posts and then reddit_get_comments for specific posts"
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Search failed: {str(e)}"}
|
||||
|
||||
@mcp.tool()
|
||||
def reddit_get_subreddit_new(
|
||||
subreddit: str,
|
||||
|
||||
Reference in New Issue
Block a user