Skip to main content
Glama
rhettlong

USCardForum MCP Server

by rhettlong
client.py17.1 kB
"""Main Discourse client for USCardForum. This module provides the primary interface for interacting with the forum, composing all API modules into a unified client. """ from __future__ import annotations from typing import Any, Iterator import requests from uscardforum.api.auth import AuthAPI from uscardforum.api.categories import CategoriesAPI from uscardforum.api.search import SearchAPI from uscardforum.api.topics import TopicsAPI from uscardforum.api.users import UsersAPI from uscardforum.utils.cloudflare import ( create_cloudflare_session_with_fallback, extended_warm_up, ) from uscardforum.models.auth import ( Bookmark, LoginResult, Notification, NotificationLevel, Session, SubscriptionResult, ) from uscardforum.models.categories import CategoryMap from uscardforum.models.search import SearchResult from uscardforum.models.topics import Post, TopicInfo, TopicSummary from uscardforum.models.users import ( FollowList, UserAction, UserBadges, UserReactions, UserSummary, ) DEFAULT_BASE_URL: str = "https://www.uscardforum.com" class DiscourseClient: """Client for interacting with USCardForum Discourse API. This client provides a unified interface for: - Browsing topics and posts - Searching the forum - Viewing user profiles and activity - Authentication and session management - Bookmarking and subscribing (when authenticated) The client handles Cloudflare protection via cloudscraper and implements rate limiting to respect server resources. Example: ```python client = DiscourseClient() # Browse hot topics topics = client.get_hot_topics() for topic in topics: print(f"{topic.title} ({topic.posts_count} posts)") # Read a topic info = client.get_topic_info(12345) posts = client.get_topic_posts(12345) # Search results = client.search("Chase Sapphire") ``` """ def __init__( self, base_url: str = DEFAULT_BASE_URL, timeout_seconds: float = 15.0, session: requests.Session | None = None, ) -> None: """Initialize the Discourse client. Args: base_url: Forum base URL (default: https://www.uscardforum.com) timeout_seconds: Default request timeout (default: 15.0) session: Optional custom requests Session """ normalized = base_url.rstrip("/") self._base_url = normalized self._timeout_seconds = timeout_seconds # Create session with Cloudflare bypass if session is not None: self._session = session else: self._session = create_cloudflare_session_with_fallback( normalized, timeout_seconds ) # Initialize API modules self._topics = TopicsAPI(self._session, normalized, timeout_seconds) self._users = UsersAPI(self._session, normalized, timeout_seconds) self._search = SearchAPI(self._session, normalized, timeout_seconds) self._categories = CategoriesAPI(self._session, normalized, timeout_seconds) self._auth = AuthAPI(self._session, normalized, timeout_seconds) # Warm up session with extended strategy extended_warm_up(self._session, normalized, timeout_seconds) def _enrich_with_categories(self, objects: list[Any]) -> list[Any]: """Enrich objects with category names using cached map. Supports objects with category_id/category_name attributes (like TopicSummary) and dictionaries with category_id key. Args: objects: List of objects to enrich Returns: Enriched objects """ try: category_map = self.get_category_map().categories for obj in objects: # Handle Pydantic models (TopicSummary, SearchTopic) if hasattr(obj, "category_id") and hasattr(obj, "category_name"): if obj.category_id and obj.category_id in category_map: obj.category_name = category_map[obj.category_id] # Handle dictionaries elif isinstance(obj, dict): cat_id = obj.get("category_id") if cat_id and cat_id in category_map: obj["category_name"] = category_map[cat_id] except Exception: # Fail gracefully if category map cannot be fetched pass return objects # ------------------------------------------------------------------------- # Properties # ------------------------------------------------------------------------- @property def base_url(self) -> str: """Forum base URL.""" return self._base_url @property def is_authenticated(self) -> bool: """Whether currently logged in.""" return self._auth.is_authenticated @property def logged_in_username(self) -> str | None: """Currently logged-in username.""" return self._auth.logged_in_username # ------------------------------------------------------------------------- # Topic Methods # ------------------------------------------------------------------------- def get_hot_topics(self, *, page: int | None = None) -> list[TopicSummary]: """Fetch currently hot/trending topics. Args: page: Page number for pagination (0-indexed, default: 0) Returns: List of hot topic summaries """ topics = self._topics.get_hot_topics(page=page) return self._enrich_with_categories(topics) def get_new_topics(self, *, page: int | None = None) -> list[TopicSummary]: """Fetch latest new topics. Args: page: Page number for pagination (0-indexed, default: 0) Returns: List of new topic summaries """ topics = self._topics.get_new_topics(page=page) return self._enrich_with_categories(topics) def get_top_topics( self, period: str = "monthly", *, page: int | None = None ) -> list[TopicSummary]: """Fetch top topics for a time period. Args: period: One of 'daily', 'weekly', 'monthly', 'quarterly', 'yearly' page: Page number for pagination (0-indexed, default: 0) Returns: List of top topic summaries """ topics = self._topics.get_top_topics(period=period, page=page) return self._enrich_with_categories(topics) def get_topic_info(self, topic_id: int) -> TopicInfo: """Fetch topic metadata. Args: topic_id: Topic ID Returns: Topic info with post count, title, timestamps """ return self._topics.get_topic_info(topic_id) def get_topic_posts( self, topic_id: int, *, post_number: int = 1, include_raw: bool = False, ) -> list[Post]: """Fetch a batch of posts starting at a specific post number. Args: topic_id: Topic ID post_number: Starting post number (default: 1) include_raw: Include raw markdown (default: False) Returns: List of posts sorted by post_number """ return self._topics.get_topic_posts( topic_id, post_number=post_number, include_raw=include_raw ) def get_all_topic_posts( self, topic_id: int, *, include_raw: bool = False, start_post_number: int = 1, end_post_number: int | None = None, max_posts: int | None = None, ) -> list[Post]: """Fetch all posts in a topic with automatic pagination. Args: topic_id: Topic ID include_raw: Include raw markdown (default: False) start_post_number: Starting post number (default: 1) end_post_number: Optional ending post number max_posts: Optional maximum posts to fetch Returns: List of all matching posts """ return self._topics.get_all_topic_posts( topic_id, include_raw=include_raw, start_post_number=start_post_number, end_post_number=end_post_number, max_posts=max_posts, ) # ------------------------------------------------------------------------- # Search Methods # ------------------------------------------------------------------------- def search( self, query: str, *, page: int | None = None, order: str | None = None, ) -> SearchResult: """Search the forum. Args: query: Search query (supports Discourse operators) page: Optional page number order: Optional sort order Returns: Search results with posts, topics, and users """ result = self._search.search(query, page=page, order=order) self._enrich_with_categories(result.topics) return result # ------------------------------------------------------------------------- # Category Methods # ------------------------------------------------------------------------- def get_categories(self) -> list: """Fetch all forum categories. Returns: List of Category objects (including subcategories) """ return self._categories.get_categories() def get_category_map(self) -> CategoryMap: """Get mapping of category IDs to names. Returns: CategoryMap with ID to name mapping """ return self._categories.get_category_map() # ------------------------------------------------------------------------- # User Methods # ------------------------------------------------------------------------- def get_user_summary(self, username: str) -> UserSummary: """Fetch user profile summary. Args: username: User handle Returns: Comprehensive user summary """ summary = self._users.get_user_summary(username) if summary.top_topics: self._enrich_with_categories(summary.top_topics) return summary def get_user_actions( self, username: str, *, filter: int | None = None, offset: int | None = None, ) -> list[UserAction]: """Fetch user actions/activity. Args: username: User handle filter: Optional action filter (e.g., 5 for replies) offset: Optional pagination offset Returns: List of user action objects """ return self._users.get_user_actions(username, filter=filter, offset=offset) def get_user_replies( self, username: str, offset: int | None = None, ) -> list[UserAction]: """Fetch user's replies. Args: username: User handle offset: Optional pagination offset Returns: List of reply action objects """ return self._users.get_user_replies(username, offset=offset) def get_user_topics( self, username: str, page: int | None = None, ) -> list[dict[str, Any]]: """Fetch topics created by user. Args: username: User handle page: Optional page number Returns: List of topic objects """ topics = self._users.get_user_topics(username, page=page) return self._enrich_with_categories(topics) def get_user_badges( self, username: str, grouped: bool = True, ) -> UserBadges: """Fetch user's badges. Args: username: User handle grouped: Group badges (default: True) Returns: User badges data """ return self._users.get_user_badges(username, grouped=grouped) def list_user_badges( self, badge_id: int, offset: int | None = None, ) -> dict[str, Any]: """List users with a specific badge. Args: badge_id: Badge ID offset: Optional pagination offset Returns: Users with the badge """ return self._users.list_users_with_badge(badge_id, offset=offset) def get_user_following( self, username: str, page: int | None = None, ) -> FollowList: """Fetch users that a user follows. Args: username: User handle page: Optional page number Returns: List of followed users """ return self._users.get_user_following(username, page=page) def get_user_followers( self, username: str, page: int | None = None, ) -> FollowList: """Fetch users following a user. Args: username: User handle page: Optional page number Returns: List of follower users """ return self._users.get_user_followers(username, page=page) def get_user_reactions( self, username: str, offset: int | None = None, ) -> UserReactions: """Fetch user's post reactions. Args: username: User handle offset: Optional pagination offset Returns: User reactions data """ return self._users.get_user_reactions(username, offset=offset) # ------------------------------------------------------------------------- # Authentication Methods # ------------------------------------------------------------------------- def login( self, username: str, password: str, second_factor_token: str | None = None, remember_me: bool = True, ) -> LoginResult: """Login to the forum. Args: username: Forum username password: Forum password second_factor_token: Optional 2FA token remember_me: Remember session (default: True) Returns: Login result with success status """ return self._auth.login( username, password, second_factor_token=second_factor_token, remember_me=remember_me ) def get_current_session(self) -> Session: """Get current session info. Returns: Session data including user info """ return self._auth.get_current_session() def get_notifications( self, since_id: int | None = None, only_unread: bool = False, limit: int | None = None, ) -> list[Notification]: """Fetch notifications (requires auth). Args: since_id: Only notifications after this ID only_unread: Only unread notifications limit: Maximum notifications to return Returns: List of notification objects """ return self._auth.get_notifications( since_id=since_id, only_unread=only_unread, limit=limit ) def iter_notifications( self, poll_interval_seconds: float = 10.0, since_id: int | None = None, ) -> Iterator[Notification]: """Yield new notifications by polling. Args: poll_interval_seconds: Poll interval (default: 10.0) since_id: Start from this notification ID Yields: New notification objects """ yield from self._auth.iter_notifications( poll_interval_seconds=poll_interval_seconds, since_id=since_id ) def bookmark_post( self, post_id: int, name: str | None = None, reminder_type: int | None = None, reminder_at: str | None = None, auto_delete_preference: int | None = 3, ) -> Bookmark: """Bookmark a post (requires auth). Args: post_id: Post ID to bookmark name: Optional bookmark name reminder_type: Optional reminder type reminder_at: Optional reminder datetime auto_delete_preference: Auto-delete setting (default: 3) Returns: Created bookmark """ return self._auth.bookmark_post( post_id, name=name, reminder_type=reminder_type, reminder_at=reminder_at, auto_delete_preference=auto_delete_preference, ) def subscribe_topic( self, topic_id: int, level: int = 2, ) -> SubscriptionResult: """Set topic notification level (requires auth). Args: topic_id: Topic ID level: 0=muted, 1=normal, 2=tracking, 3=watching Returns: Subscription result """ return self._auth.subscribe_topic(topic_id, level=NotificationLevel(level))

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rhettlong/uscardforum-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server