Skip to main content
Glama
tools.py18.3 kB
"""Movie tools for the Trakt MCP server.""" import json import logging from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Literal from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, Field, PositiveInt, field_validator from client.movies.client import MoviesClient from client.movies.details import MovieDetailsClient from client.movies.popular import PopularMoviesClient from client.movies.stats import MovieStatsClient from client.movies.trending import TrendingMoviesClient from config.api import DEFAULT_LIMIT from config.mcp.tools import TOOL_NAMES from models.formatters.movies import MovieFormatters from models.formatters.videos import VideoFormatters from server.base import BaseToolErrorMixin from utils.api.errors import MCPError, handle_api_errors_func if TYPE_CHECKING: from models.types import MovieResponse, TraktRating logger = logging.getLogger("trakt_mcp") # Type alias for tool handlers ToolHandler = Callable[..., Awaitable[str]] # Pydantic models for parameter validation class LimitOnly(BaseModel): """Parameters for tools that only require a limit.""" limit: PositiveInt = DEFAULT_LIMIT page: int | None = Field( default=None, ge=1, description="Page number for pagination (optional)" ) class PeriodParams(BaseModel): """Parameters for tools that accept limit and time period.""" limit: PositiveInt = DEFAULT_LIMIT period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly" page: int | None = Field( default=None, ge=1, description="Page number for pagination (optional)" ) class MovieIdParam(BaseModel): """Parameters for tools that require a movie ID.""" movie_id: str = Field(..., min_length=1, description="Non-empty Trakt movie ID") @field_validator("movie_id", mode="before") @classmethod def _strip_movie_id(cls, v: object) -> object: return v.strip() if isinstance(v, str) else v class MovieSummaryParams(MovieIdParam): """Parameters for movie summary tools with extended option.""" extended: bool = True class MovieVideoParams(MovieIdParam): """Parameters for movie video tools.""" embed_markdown: bool = True @handle_api_errors_func async def fetch_trending_movies( limit: int = DEFAULT_LIMIT, page: int | None = None ) -> str: """Fetch trending movies from Trakt. Args: limit: Items per page (default: 10) page: Page number (optional). If None, returns all results via auto-pagination. Returns: Information about trending movies. When page is None, returns all movies. When page is specified, returns movies from that page with pagination metadata. """ # Validate parameters with Pydantic for normalization and constraints params = LimitOnly(limit=limit, page=page) limit, page = params.limit, params.page client = TrendingMoviesClient() movies = await client.get_trending_movies(limit=limit, page=page) return MovieFormatters.format_trending_movies(movies) @handle_api_errors_func async def fetch_popular_movies( limit: int = DEFAULT_LIMIT, page: int | None = None ) -> str: """Fetch popular movies from Trakt. Args: limit: Items per page (default: 10) page: Page number (optional). If None, returns all results via auto-pagination. Returns: Information about popular movies. When page is None, returns all movies. When page is specified, returns movies from that page with pagination metadata. """ # Validate parameters with Pydantic for normalization and constraints params = LimitOnly(limit=limit, page=page) limit, page = params.limit, params.page client = PopularMoviesClient() movies = await client.get_popular_movies(limit=limit, page=page) return MovieFormatters.format_popular_movies(movies) @handle_api_errors_func async def fetch_favorited_movies( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: """Fetch most favorited movies from Trakt. Args: limit: Items per page (default: 10) period: Time period for favorite calculation (daily, weekly, monthly, yearly, all) page: Page number (optional). If None, returns all results via auto-pagination. Returns: Information about most favorited movies. When page is None, returns all movies. When page is specified, returns movies from that page with pagination metadata. """ # Validate parameters with Pydantic for normalization and constraints params = PeriodParams(limit=limit, period=period, page=page) limit, period, page = params.limit, params.period, params.page client = MovieStatsClient() movies = await client.get_favorited_movies(limit=limit, period=period, page=page) # Trace structure in debug only (only for list responses to avoid pagination object) if movies and isinstance(movies, list): logger.debug( "Favorited movies API response structure: %s", json.dumps(movies[0], indent=2), ) return MovieFormatters.format_favorited_movies(movies) @handle_api_errors_func async def fetch_played_movies( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: """Fetch most played movies from Trakt. Args: limit: Items per page (default: 10) period: Time period for most played (daily, weekly, monthly, yearly, all) page: Page number (optional). If None, returns all results via auto-pagination. Returns: Information about most played movies. When page is None, returns all movies. When page is specified, returns movies from that page with pagination metadata. """ # Validate parameters with Pydantic for normalization and constraints params = PeriodParams(limit=limit, period=period, page=page) limit, period, page = params.limit, params.period, params.page client = MovieStatsClient() movies = await client.get_played_movies(limit=limit, period=period, page=page) return MovieFormatters.format_played_movies(movies) @handle_api_errors_func async def fetch_watched_movies( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: """Fetch most watched movies from Trakt. Args: limit: Items per page (default: 10) period: Time period for most watched (daily, weekly, monthly, yearly, all) page: Page number (optional). If None, returns all results via auto-pagination. Returns: Information about most watched movies. When page is None, returns all movies. When page is specified, returns movies from that page with pagination metadata. """ # Validate parameters with Pydantic for normalization and constraints params = PeriodParams(limit=limit, period=period, page=page) limit, period, page = params.limit, params.period, params.page client = MovieStatsClient() movies = await client.get_watched_movies(limit=limit, period=period, page=page) return MovieFormatters.format_watched_movies(movies) @handle_api_errors_func async def fetch_movie_ratings(movie_id: str) -> str: """Fetch ratings for a movie from Trakt. Args: movie_id: Trakt ID of the movie Returns: Information about movie ratings including average and distribution Raises: InvalidParamsError: If movie_id is invalid InternalError: If an error occurs fetching movie or ratings data """ # Validate required parameters via Pydantic params = MovieIdParam(movie_id=movie_id) movie_id = params.movie_id try: client = MovieDetailsClient() movie: MovieResponse = await client.get_movie(movie_id) # Handle transitional case where API returns error strings if isinstance(movie, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie", resource_id=movie_id, error_message=movie, operation="fetch_movie_details", ) movie_title = movie.get("title", f"Movie ID: {movie_id}") ratings: TraktRating = await client.get_movie_ratings(movie_id) # Handle transitional case where API returns error strings if isinstance(ratings, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie_ratings", resource_id=movie_id, error_message=ratings, operation="fetch_movie_ratings", movie_title=movie_title, ) return MovieFormatters.format_movie_ratings(ratings, movie_title) except MCPError: raise @handle_api_errors_func async def fetch_movie_summary(movie_id: str, extended: bool = True) -> str: """Fetch movie summary from Trakt. Args: movie_id: Trakt ID of the movie extended: If True, return comprehensive data with production status and metadata. If False, return basic movie information (title, year, IDs). Returns: Movie information formatted as markdown. Extended mode includes production status, ratings, metadata, and detailed information. Basic mode includes title, year, and Trakt ID only. Raises: InvalidParamsError: If movie_id is invalid InternalError: If an error occurs fetching movie data """ # Validate required parameters via Pydantic params = MovieSummaryParams(movie_id=movie_id, extended=extended) movie_id, extended = params.movie_id, params.extended try: client = MovieDetailsClient() if extended: movie: MovieResponse = await client.get_movie_extended(movie_id) # Handle transitional case where API returns error strings if isinstance(movie, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie_extended", resource_id=movie_id, error_message=movie, operation="fetch_movie_extended", ) return MovieFormatters.format_movie_extended(movie) else: movie: MovieResponse = await client.get_movie(movie_id) # Handle transitional case where API returns error strings if isinstance(movie, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie", resource_id=movie_id, error_message=movie, operation="fetch_movie_summary", ) return MovieFormatters.format_movie_summary(movie) except MCPError: raise @handle_api_errors_func async def fetch_movie_videos(movie_id: str, embed_markdown: bool = True) -> str: """Fetch videos for a movie from Trakt. Args: movie_id: Trakt ID, slug, or IMDB ID embed_markdown: Use embedded markdown syntax for video links Returns: Markdown formatted list of videos """ # Validate required parameters via Pydantic params = MovieVideoParams(movie_id=movie_id, embed_markdown=embed_markdown) movie_id, embed_markdown = params.movie_id, params.embed_markdown try: client: MoviesClient = MoviesClient() # Use unified client videos = await client.get_videos(movie_id) # Transitional safeguard if client returns string errors if isinstance(videos, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie_videos", resource_id=movie_id, error_message=videos, operation="fetch_movie_videos", ) # Get movie title for context, fallback to ID if fetch fails try: movie = await client.get_movie(movie_id) # Handle transitional case where API returns error strings if isinstance(movie, str): raise BaseToolErrorMixin.handle_api_string_error( resource_type="movie", resource_id=movie_id, error_message=movie, operation="fetch_movie_for_videos", ) title = movie.get("title", f"Movie ID: {movie_id}") except Exception as e: # Best-effort title lookup - don't fail the whole operation logger.debug( "Non-fatal exception during movie title lookup: %s (movie_id: %s)", str(e), movie_id, exc_info=True, ) # Use movie_id as fallback title if movie fetch fails title = f"Movie ID: {movie_id}" return VideoFormatters.format_videos_list( videos, title, embed_markdown, validate_input=False ) except MCPError: raise def register_movie_tools(mcp: FastMCP) -> tuple[ToolHandler, ...]: """Register movie tools with the MCP server. Returns: Tuple of tool handlers for type checker visibility """ @mcp.tool( name=TOOL_NAMES["fetch_trending_movies"], description="Fetch trending movies from Trakt. Use page parameter for paginated results, or omit for all results.", ) @handle_api_errors_func async def fetch_trending_movies_tool( limit: int = DEFAULT_LIMIT, page: int | None = None ) -> str: # Validate parameters with Pydantic params = LimitOnly(limit=limit, page=page) return await fetch_trending_movies(params.limit, params.page) @mcp.tool( name=TOOL_NAMES["fetch_popular_movies"], description="Fetch popular movies from Trakt. Use page parameter for paginated results, or omit for all results.", ) @handle_api_errors_func async def fetch_popular_movies_tool( limit: int = DEFAULT_LIMIT, page: int | None = None ) -> str: # Validate parameters with Pydantic params = LimitOnly(limit=limit, page=page) return await fetch_popular_movies(params.limit, params.page) @mcp.tool( name=TOOL_NAMES["fetch_favorited_movies"], description="Fetch most favorited movies from Trakt. Use page parameter for paginated results, or omit for all results.", ) @handle_api_errors_func async def fetch_favorited_movies_tool( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: # Validate parameters with Pydantic params = PeriodParams(limit=limit, period=period, page=page) return await fetch_favorited_movies(params.limit, params.period, params.page) @mcp.tool( name=TOOL_NAMES["fetch_played_movies"], description="Fetch most played movies from Trakt. Use page parameter for paginated results, or omit for all results.", ) @handle_api_errors_func async def fetch_played_movies_tool( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: # Validate parameters with Pydantic params = PeriodParams(limit=limit, period=period, page=page) return await fetch_played_movies(params.limit, params.period, params.page) @mcp.tool( name=TOOL_NAMES["fetch_watched_movies"], description="Fetch most watched movies from Trakt. Use page parameter for paginated results, or omit for all results.", ) @handle_api_errors_func async def fetch_watched_movies_tool( limit: int = DEFAULT_LIMIT, period: Literal["daily", "weekly", "monthly", "yearly", "all"] = "weekly", page: int | None = None, ) -> str: # Validate parameters with Pydantic params = PeriodParams(limit=limit, period=period, page=page) return await fetch_watched_movies(params.limit, params.period, params.page) @mcp.tool( name=TOOL_NAMES["fetch_movie_ratings"], description="Fetch ratings and voting statistics for a specific movie", ) @handle_api_errors_func async def fetch_movie_ratings_tool(movie_id: str) -> str: # Validate parameters with Pydantic params = MovieIdParam(movie_id=movie_id) return await fetch_movie_ratings(params.movie_id) @mcp.tool( name=TOOL_NAMES["fetch_movie_summary"], description="Get movie summary from Trakt. Default behavior (extended=true): Returns comprehensive data including production status, ratings, genres, runtime, certification, and metadata. Basic mode (extended=false): Returns only title, year, and Trakt ID.", ) @handle_api_errors_func async def fetch_movie_summary_tool(movie_id: str, extended: bool = True) -> str: # Validate parameters with Pydantic params = MovieSummaryParams(movie_id=movie_id, extended=extended) return await fetch_movie_summary(params.movie_id, params.extended) @mcp.tool( name=TOOL_NAMES["fetch_movie_videos"], description=( "Get videos (trailers, teasers, etc.) for a movie from Trakt. " "Set embed_markdown=False to return simple links instead of YouTube iframes." ), ) @handle_api_errors_func async def fetch_movie_videos_tool( movie_id: str, embed_markdown: bool = True ) -> str: """MCP tool wrapper for fetch_movie_videos.""" # Validate parameters with Pydantic params = MovieVideoParams(movie_id=movie_id, embed_markdown=embed_markdown) return await fetch_movie_videos(params.movie_id, params.embed_markdown) # Return handlers for type checker visibility return ( fetch_trending_movies_tool, fetch_popular_movies_tool, fetch_favorited_movies_tool, fetch_played_movies_tool, fetch_watched_movies_tool, fetch_movie_ratings_tool, fetch_movie_summary_tool, fetch_movie_videos_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