Skip to main content
Glama
tools.py22 kB
"""Comment tools for the Trakt MCP server.""" from collections.abc import Awaitable, Callable from typing import Literal, NoReturn, TypedDict from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, Field, PositiveInt, ValidationError, field_validator from client.comments.details import CommentDetailsClient from client.comments.episode import EpisodeCommentsClient from client.comments.movie import MovieCommentsClient from client.comments.season import SeasonCommentsClient from client.comments.show import ShowCommentsClient from config.api import DEFAULT_LIMIT, DEFAULT_MAX_PAGES from config.mcp.tools import TOOL_NAMES from models.formatters.comments import CommentsFormatters from models.types import CommentResponse from models.types.pagination import PaginatedResponse from server.base import BaseToolErrorMixin from server.movies.tools import MovieIdParam from server.shows.tools import ShowIdParam from utils.api.errors import handle_api_errors_func # Comment sort options supported by Trakt API CommentSort = Literal["newest", "oldest", "likes", "replies"] class ValidationErrorDetail(TypedDict): """Typed structure for validation error details.""" field: str message: str type: str input: object | None class CommentIdParam(BaseModel): """Parameters for tools that require a comment ID.""" comment_id: str = Field(..., min_length=1, description="Non-empty Trakt comment ID") @field_validator("comment_id", mode="before") @classmethod def _strip_comment_id(cls, v: object) -> object: return v.strip() if isinstance(v, str) else v class SeasonParam(BaseModel): """Parameters for tools that require show ID and season.""" show_id: str = Field(..., min_length=1, description="Non-empty Trakt show ID") season: PositiveInt = Field(..., description="Season number (positive integer)") @field_validator("show_id", mode="before") @classmethod def _strip_show_id(cls, v: object) -> object: return v.strip() if isinstance(v, str) else v class EpisodeParam(BaseModel): """Parameters for tools that require show ID, season, and episode.""" show_id: str = Field(..., min_length=1, description="Non-empty Trakt show ID") season: PositiveInt = Field(..., description="Season number (positive integer)") episode: PositiveInt = Field(..., description="Episode number (positive integer)") @field_validator("show_id", mode="before") @classmethod def _strip_show_id(cls, v: object) -> object: return v.strip() if isinstance(v, str) else v class CommentsListOptionsParam(BaseModel): """Parameters for comment listing tools with validation constraints.""" limit: PositiveInt = Field( DEFAULT_LIMIT, le=200, description="Maximum number of comments to return (1-200)", ) sort: CommentSort = Field("newest", description="Sort order for comments") show_spoilers: bool = Field(False, description="Whether to show spoiler content") class PageParam(BaseModel): """Parameters for tools that support pagination.""" page: int | None = Field( default=None, ge=1, description="Optional page number for pagination (1-based, positive integer)", ) @field_validator("page", mode="before") @classmethod def _validate_page(cls, v: object) -> object: """Handle empty strings and None values; trim whitespace.""" if v is None: return None if isinstance(v, str): v = v.strip() if v == "": return None return v def _handle_validation_error(e: ValidationError, context: str) -> NoReturn: """Handle validation errors with consistent formatting via BaseToolErrorMixin. Args: e: The ValidationError to handle context: Context string for the error message Raises: BaseToolErrorMixin error: Formatted validation error via mixin """ validation_errors: list[ValidationErrorDetail] = [ ValidationErrorDetail( field=str(error.get("loc", [context])[-1]), message=str(error.get("msg", "Invalid value")), type=str(error.get("type", "validation_error")), input=error.get("input"), ) for error in e.errors() ] raise BaseToolErrorMixin.handle_validation_error( f"Invalid parameters for {context}", validation_errors=validation_errors, operation=f"{context.replace(' ', '_')}_validation", ) from e def _ensure_not_error_string( value: object, *, resource_type: str, resource_id: str, operation: str ) -> None: """Helper to check if API response is an error string and raise appropriate error. Args: value: The API response value to check resource_type: Type of resource for error context resource_id: ID of the resource for error context operation: Operation being performed for error context Raises: BaseToolErrorMixin error: If value is an error string """ if isinstance(value, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type=resource_type, resource_id=resource_id, error_message=value, operation=operation, ) async def _fetch_and_format_comments( *, resource_type: str, resource_id: str, fetch_fn: Callable[ [], Awaitable[list[CommentResponse] | PaginatedResponse[CommentResponse]] ], title: str, show_spoilers: bool, ) -> str: """Helper to reduce duplication in comment fetching functions. Args: resource_type: Type of resource for error context (e.g., "movie_comments") resource_id: ID of the resource for error context fetch_fn: Zero-argument callable that performs the client API call title: Title to use in formatted output show_spoilers: Whether to show spoiler content Returns: Formatted comments as markdown string Raises: BaseToolErrorMixin error: If API response is an error string """ data = await fetch_fn() _ensure_not_error_string( data, resource_type=resource_type, resource_id=resource_id, operation=f"fetch_{resource_type}", ) return CommentsFormatters.format_comments(data, title, show_spoilers=show_spoilers) async def _fetch_and_format_comment( *, resource_type: str, resource_id: str, fetch_fn: Callable[[], Awaitable[CommentResponse]], show_spoilers: bool, ) -> str: """Helper to reduce duplication in single comment fetching functions. Args: resource_type: Type of resource for error context (e.g., "comment") resource_id: ID of the resource for error context fetch_fn: Zero-argument callable that performs the client API call show_spoilers: Whether to show spoiler content Returns: Formatted comment as markdown string Raises: BaseToolErrorMixin error: If API response is an error string """ data = await fetch_fn() _ensure_not_error_string( data, resource_type=resource_type, resource_id=resource_id, operation=f"fetch_{resource_type}", ) return CommentsFormatters.format_comment(data, show_spoilers=show_spoilers) # Type aliases for tool functions MovieCommentsToolType = Callable[ [str, int, bool, CommentSort, int | None, int], Awaitable[str] ] ShowCommentsToolType = Callable[ [str, int, bool, CommentSort, int | None, int], Awaitable[str] ] SeasonCommentsToolType = Callable[ [str, int, int, bool, CommentSort, int | None, int], Awaitable[str] ] EpisodeCommentsToolType = Callable[ [str, int, int, int, bool, CommentSort, int | None, int], Awaitable[str] ] CommentToolType = Callable[[str, bool], Awaitable[str]] CommentRepliesToolType = Callable[[str, int, bool, int | None, int], Awaitable[str]] @handle_api_errors_func async def fetch_movie_comments( movie_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: """Fetch comments for a movie from Trakt. Args: movie_id: Trakt ID of the movie limit: Maximum number of comments to return show_spoilers: Whether to show spoilers by default sort: How to sort comments (newest, oldest, likes, replies) page: Page number (optional). If None, returns all results via auto-pagination. max_pages: Maximum number of pages to fetch during auto-pagination Returns: Information about movie comments Raises: InvalidParamsError: If movie_id is invalid InternalError: If an error occurs fetching comments """ try: id_params = MovieIdParam(movie_id=movie_id) options = CommentsListOptionsParam( limit=limit, sort=sort, show_spoilers=show_spoilers ) page_params = PageParam(page=page) movie_id = id_params.movie_id page = page_params.page except ValidationError as e: _handle_validation_error(e, "movie comments") client = MovieCommentsClient() return await _fetch_and_format_comments( resource_type="movie_comments", resource_id=movie_id, fetch_fn=lambda: client.get_movie_comments( movie_id, limit=options.limit, sort=options.sort, page=page, max_pages=max_pages, ), title=f"Movie ID: {movie_id}", show_spoilers=options.show_spoilers, ) @handle_api_errors_func async def fetch_show_comments( show_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: """Fetch comments for a show from Trakt. Args: show_id: Trakt ID of the show limit: Maximum number of comments to return show_spoilers: Whether to show spoilers by default sort: How to sort comments (newest, oldest, likes, replies) page: Page number (optional). If None, returns all results via auto-pagination. max_pages: Maximum number of pages to fetch during auto-pagination Returns: Information about show comments Raises: InvalidParamsError: If show_id is invalid InternalError: If an error occurs fetching comments """ try: id_params = ShowIdParam(show_id=show_id) options = CommentsListOptionsParam( limit=limit, sort=sort, show_spoilers=show_spoilers ) page_params = PageParam(page=page) show_id = id_params.show_id page = page_params.page except ValidationError as e: _handle_validation_error(e, "show comments") client = ShowCommentsClient() return await _fetch_and_format_comments( resource_type="show_comments", resource_id=show_id, fetch_fn=lambda: client.get_show_comments( show_id, limit=options.limit, sort=options.sort, page=page, max_pages=max_pages, ), title=f"Show ID: {show_id}", show_spoilers=options.show_spoilers, ) @handle_api_errors_func async def fetch_season_comments( show_id: str, season: int, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: """Fetch comments for a season from Trakt. Args: show_id: Trakt ID of the show season: Season number limit: Maximum number of comments to return show_spoilers: Whether to show spoilers by default sort: How to sort comments (newest, oldest, likes, replies) page: Page number (optional). If None, returns all results via auto-pagination. max_pages: Maximum number of pages to fetch during auto-pagination Returns: Information about season comments Raises: InvalidParamsError: If show_id or season is invalid InternalError: If an error occurs fetching comments """ try: id_params = SeasonParam(show_id=show_id, season=season) options = CommentsListOptionsParam( limit=limit, sort=sort, show_spoilers=show_spoilers ) page_params = PageParam(page=page) show_id, season = id_params.show_id, id_params.season page = page_params.page except ValidationError as e: _handle_validation_error(e, "season comments") client = SeasonCommentsClient() return await _fetch_and_format_comments( resource_type="season_comments", resource_id=f"{show_id}-{season}", fetch_fn=lambda: client.get_season_comments( show_id, season, limit=options.limit, sort=options.sort, page=page, max_pages=max_pages, ), title=f"Show ID: {show_id} - Season {season}", show_spoilers=options.show_spoilers, ) @handle_api_errors_func async def fetch_episode_comments( show_id: str, season: int, episode: int, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: """Fetch comments for an episode from Trakt. Args: show_id: Trakt ID of the show season: Season number episode: Episode number limit: Maximum number of comments to return show_spoilers: Whether to show spoilers by default sort: How to sort comments (newest, oldest, likes, replies) page: Page number (optional). If None, returns all results via auto-pagination. max_pages: Maximum number of pages to fetch during auto-pagination Returns: Information about episode comments Raises: InvalidParamsError: If show_id, season, or episode is invalid InternalError: If an error occurs fetching comments """ try: id_params = EpisodeParam(show_id=show_id, season=season, episode=episode) options = CommentsListOptionsParam( limit=limit, sort=sort, show_spoilers=show_spoilers ) page_params = PageParam(page=page) show_id, season, episode = ( id_params.show_id, id_params.season, id_params.episode, ) page = page_params.page except ValidationError as e: _handle_validation_error(e, "episode comments") client = EpisodeCommentsClient() return await _fetch_and_format_comments( resource_type="episode_comments", resource_id=f"{show_id}-{season}-{episode}", fetch_fn=lambda: client.get_episode_comments( show_id, season, episode, limit=options.limit, sort=options.sort, page=page, max_pages=max_pages, ), title=f"Show ID: {show_id} - S{season:02d}E{episode:02d}", show_spoilers=options.show_spoilers, ) @handle_api_errors_func async def fetch_comment(comment_id: str, show_spoilers: bool = False) -> str: """Fetch a specific comment from Trakt. Args: comment_id: Trakt ID of the comment show_spoilers: Whether to show spoilers by default Returns: Information about the comment Raises: InvalidParamsError: If comment_id is invalid InternalError: If an error occurs fetching comment """ try: params = CommentIdParam(comment_id=comment_id) comment_id = params.comment_id except ValidationError as e: _handle_validation_error(e, "comment") client = CommentDetailsClient() return await _fetch_and_format_comment( resource_type="comment", resource_id=comment_id, fetch_fn=lambda: client.get_comment(comment_id), show_spoilers=show_spoilers, ) @handle_api_errors_func async def fetch_comment_replies( comment_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: """Fetch replies for a comment from Trakt. Args: comment_id: Trakt ID of the comment limit: Maximum number of replies to return show_spoilers: Whether to show spoilers by default page: Page number (optional). If None, returns all results via auto-pagination. max_pages: Maximum number of pages to fetch during auto-pagination Returns: Information about the comment and its replies Raises: InvalidParamsError: If comment_id is invalid InternalError: If an error occurs fetching comment replies """ try: id_params = CommentIdParam(comment_id=comment_id) page_params = PageParam(page=page) comment_id = id_params.comment_id page = page_params.page except ValidationError as e: _handle_validation_error(e, "comment replies") client = CommentDetailsClient() # Fetch comment data comment = await client.get_comment(comment_id) _ensure_not_error_string( comment, resource_type="comment", resource_id=comment_id, operation="fetch_comment_replies", ) # Fetch replies data replies = await client.get_comment_replies( comment_id, limit=limit, page=page, max_pages=max_pages ) _ensure_not_error_string( replies, resource_type="comment_replies", resource_id=comment_id, operation="fetch_comment_replies", ) return CommentsFormatters.format_comment( comment, with_replies=True, replies=replies, show_spoilers=show_spoilers, ) def register_comment_tools( mcp: FastMCP, ) -> tuple[ MovieCommentsToolType, ShowCommentsToolType, SeasonCommentsToolType, EpisodeCommentsToolType, CommentToolType, CommentRepliesToolType, ]: """Register comment tools with the MCP server. Args: mcp: FastMCP instance used to register tool handlers. Returns: Tuple of tool handlers for type-checker visibility. """ @mcp.tool( name=TOOL_NAMES["fetch_movie_comments"], description="Fetch comments for a specific movie from Trakt. Supports optional pagination with 'page' parameter and safety cap 'max_pages'.", ) async def fetch_movie_comments_tool( movie_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: return await fetch_movie_comments( movie_id, limit, show_spoilers, sort, page, max_pages ) @mcp.tool( name=TOOL_NAMES["fetch_show_comments"], description="Fetch comments for a specific TV show from Trakt. Supports optional pagination with 'page' parameter and safety cap 'max_pages'.", ) async def fetch_show_comments_tool( show_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: return await fetch_show_comments( show_id, limit, show_spoilers, sort, page, max_pages ) @mcp.tool( name=TOOL_NAMES["fetch_season_comments"], description="Fetch comments for a specific TV show season from Trakt. Supports optional pagination with 'page' parameter and safety cap 'max_pages'.", ) async def fetch_season_comments_tool( show_id: str, season: int, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: return await fetch_season_comments( show_id, season, limit, show_spoilers, sort, page, max_pages ) @mcp.tool( name=TOOL_NAMES["fetch_episode_comments"], description="Fetch comments for a specific TV show episode from Trakt. Supports optional pagination with 'page' parameter and safety cap 'max_pages'.", ) async def fetch_episode_comments_tool( show_id: str, season: int, episode: int, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, sort: CommentSort = "newest", page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: return await fetch_episode_comments( show_id, season, episode, limit, show_spoilers, sort, page, max_pages ) @mcp.tool( name=TOOL_NAMES["fetch_comment"], description="Fetch a specific comment from Trakt", ) async def fetch_comment_tool(comment_id: str, show_spoilers: bool = False) -> str: return await fetch_comment(comment_id, show_spoilers) @mcp.tool( name=TOOL_NAMES["fetch_comment_replies"], description="Fetch replies for a specific comment from Trakt. Supports optional pagination with 'page' parameter and safety cap 'max_pages'.", ) async def fetch_comment_replies_tool( comment_id: str, limit: int = DEFAULT_LIMIT, show_spoilers: bool = False, page: int | None = None, max_pages: int = DEFAULT_MAX_PAGES, ) -> str: return await fetch_comment_replies( comment_id, limit, show_spoilers, page, max_pages ) # Return handlers for type checker visibility return ( fetch_movie_comments_tool, fetch_show_comments_tool, fetch_season_comments_tool, fetch_episode_comments_tool, fetch_comment_tool, fetch_comment_replies_tool, )

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/wwiens/trakt_mcpserver'

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