Skip to main content
Glama
rishijatia

Fantasy Premier League MCP Server

analyze_players

Filter and analyze Fantasy Premier League players by position, team, price, points, ownership, and form to identify optimal selections for your squad.

Instructions

Filter and analyze FPL players based on multiple criteria

Args:
    position: Player position (e.g., "midfielders", "defenders")
    team: Team name filter
    min_price: Minimum player price in millions
    max_price: Maximum player price in millions
    min_points: Minimum total points
    min_ownership: Minimum ownership percentage
    max_ownership: Maximum ownership percentage
    form_threshold: Minimum form rating
    include_gameweeks: Whether to include gameweek-by-gameweek data
    num_gameweeks: Number of recent gameweeks to include
    sort_by: Metric to sort results by (default: total_points)
    sort_order: Sort direction ("asc" or "desc")
    limit: Maximum number of players to return
    
Returns:
    Filtered player data with summary statistics

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
positionNo
teamNo
min_priceNo
max_priceNo
min_pointsNo
min_ownershipNo
max_ownershipNo
form_thresholdNo
include_gameweeksNo
num_gameweeksNo
sort_byNototal_points
sort_orderNodesc
limitNo

Implementation Reference

  • The core handler function for the 'analyze_players' MCP tool. Filters FPL players by position, team, price range, points, ownership, form, etc. Computes summaries, distributions, sorts results, and optionally includes recent gameweek history and form stats.
    async def analyze_players(
        position: Optional[str] = None,
        team: Optional[str] = None,
        min_price: Optional[float] = None,
        max_price: Optional[float] = None,
        min_points: Optional[int] = None,
        min_ownership: Optional[float] = None,
        max_ownership: Optional[float] = None,
        form_threshold: Optional[float] = None,
        include_gameweeks: bool = False,
        num_gameweeks: int = 5,
        sort_by: str = "total_points",
        sort_order: str = "desc",
        limit: int = 20
    ) -> Dict[str, Any]:
        """Filter and analyze FPL players based on multiple criteria
        
        Args:
            position: Player position (e.g., "midfielders", "defenders")
            team: Team name filter
            min_price: Minimum player price in millions
            max_price: Maximum player price in millions
            min_points: Minimum total points
            min_ownership: Minimum ownership percentage
            max_ownership: Maximum ownership percentage
            form_threshold: Minimum form rating
            include_gameweeks: Whether to include gameweek-by-gameweek data
            num_gameweeks: Number of recent gameweeks to include
            sort_by: Metric to sort results by (default: total_points)
            sort_order: Sort direction ("asc" or "desc")
            limit: Maximum number of players to return
            
        Returns:
            Filtered player data with summary statistics
        """
        logger.info(f"Tool called: analyze_players({position}, {team}, ...)")
        
        # Handle dictionary parameters
        if isinstance(position, dict):
            if 'position' in position:
                position = position['position']
            else:
                position = None
                
        if isinstance(team, dict):
            if 'team' in team:
                team = team['team']
            else:
                team = None
        
        if isinstance(min_price, dict):
            if 'min_price' in min_price:
                min_price = min_price['min_price']
            else:
                min_price = None
                
        if isinstance(max_price, dict):
            if 'max_price' in max_price:
                max_price = max_price['max_price']
            else:
                max_price = None
                
        if isinstance(min_points, dict):
            if 'min_points' in min_points:
                min_points = min_points['min_points']
            else:
                min_points = None
                
        if isinstance(min_ownership, dict):
            if 'min_ownership' in min_ownership:
                min_ownership = min_ownership['min_ownership']
            else:
                min_ownership = None
                
        if isinstance(max_ownership, dict):
            if 'max_ownership' in max_ownership:
                max_ownership = max_ownership['max_ownership']
            else:
                max_ownership = None
                
        if isinstance(form_threshold, dict):
            if 'form_threshold' in form_threshold:
                form_threshold = form_threshold['form_threshold']
            else:
                form_threshold = None
                
        if isinstance(include_gameweeks, dict):
            if 'include_gameweeks' in include_gameweeks:
                include_gameweeks = include_gameweeks['include_gameweeks']
            else:
                include_gameweeks = False
                
        if isinstance(num_gameweeks, dict):
            if 'num_gameweeks' in num_gameweeks:
                num_gameweeks = num_gameweeks['num_gameweeks']
            else:
                num_gameweeks = 5
                
        if isinstance(sort_by, dict):
            if 'sort_by' in sort_by:
                sort_by = sort_by['sort_by']
            else:
                sort_by = "total_points"
                
        if isinstance(sort_order, dict):
            if 'sort_order' in sort_order:
                sort_order = sort_order['sort_order']
            else:
                sort_order = "desc"
                
        if isinstance(limit, dict):
            if 'limit' in limit:
                limit = limit['limit']
            else:
                limit = 20
        
        # Get cached complete player dataset
        all_players = await get_cached_player_data()
        
        # Normalize position if provided
        normalized_position = normalize_position(position) if position else None
        position_changed = normalized_position != position if position else False
        
        # Apply all filters
        filtered_players = []
        for player in all_players:
            # Check position filter
            if normalized_position and player.get("position") != normalized_position:
                continue
                
            # Check team filter
            if team and not (
                team.lower() in player.get("team", "").lower() or 
                team.lower() in player.get("team_short", "").lower()
            ):
                continue
                
            # Check price range
            if min_price is not None and player.get("price", 0) < min_price:
                continue
            if max_price is not None and player.get("price", 0) > max_price:
                continue
                
            # Check points threshold
            if min_points is not None and player.get("points", 0) < min_points:
                continue
                
            # Check ownership range
            try:
                ownership = float(player.get("selected_by_percent", 0).replace("%", ""))
                if min_ownership is not None and ownership < min_ownership:
                    continue
                if max_ownership is not None and ownership > max_ownership:
                    continue
            except (ValueError, TypeError):
                # Skip ownership check if value can't be converted
                pass
                
            # Check form threshold
            try:
                form = float(player.get("form", 0))
                if form_threshold is not None and form < form_threshold:
                    continue
            except (ValueError, TypeError):
                # Skip form check if value can't be converted
                pass
    
            player['status'] = "available" if player.get("status") == "a" else "unavailable"
                
            # Player passed all filters
            filtered_players.append(player)
        
        # Sort results
        reverse = sort_order.lower() != "asc"
        try:
            # Handle numeric sorting properly
            numeric_fields = ["points", "price", "form", "selected_by_percent", "value"]
            if sort_by in numeric_fields:
                filtered_players.sort(
                    key=lambda p: float(p.get(sort_by, 0)) 
                    if p.get(sort_by) is not None else 0,
                    reverse=reverse
                )
            else:
                filtered_players.sort(
                    key=lambda p: p.get(sort_by, ""), 
                    reverse=reverse
                )
        except (KeyError, ValueError):
            # Fall back to points sorting
            filtered_players.sort(
                key=lambda p: float(p.get("points", 0)), 
                reverse=True
            )
        
        # Calculate summary statistics
        total_players = len(filtered_players)
        average_points = sum(float(p.get("points", 0)) for p in filtered_players) / max(1, total_players)
        average_price = sum(float(p.get("price", 0)) for p in filtered_players) / max(1, total_players)
        
        # Count position and team distributions
        position_counts = Counter(p.get("position") for p in filtered_players)
        team_counts = Counter(p.get("team") for p in filtered_players)
        
        # Build filter description
        applied_filters = []
        if normalized_position:
            applied_filters.append(f"Position: {normalized_position}")
        if team:
            applied_filters.append(f"Team: {team}")
        if min_price is not None:
            applied_filters.append(f"Min price: £{min_price}m")
        if max_price is not None:
            applied_filters.append(f"Max price: £{max_price}m")
        if min_points is not None:
            applied_filters.append(f"Min points: {min_points}")
        if min_ownership is not None:
            applied_filters.append(f"Min ownership: {min_ownership}%")
        if max_ownership is not None:
            applied_filters.append(f"Max ownership: {max_ownership}%")
        if form_threshold is not None:
            applied_filters.append(f"Min form: {form_threshold}")
        
        # Build results with summary and detail sections
        result = {
            "summary": {
                "total_matches": total_players,
                "filters_applied": applied_filters,
                "average_points": round(average_points, 1),
                "average_price": round(average_price, 2),
                "position_distribution": dict(position_counts),
                "team_distribution": dict(sorted(
                    team_counts.items(), 
                    key=lambda x: x[1], 
                    reverse=True
                )[:10]),  # Top 10 teams
            },
            "players": filtered_players[:limit]  # Apply limit to detailed results
        }
        
        # Add position normalization note if relevant
        if position_changed:
            result["summary"]["position_note"] = f"'{position}' was interpreted as '{normalized_position}'"
        
        # Include gameweek history if requested
        if include_gameweeks and filtered_players:
            try:
                # Get history for top players (limit)
                player_ids = [p.get("id") for p in filtered_players[:limit]]
                gameweek_data = await fixtures.get_player_gameweek_history(player_ids, num_gameweeks)
                
                # Add gameweek data to the result
                result["gameweek_data"] = gameweek_data
                
                # Calculate and add recent form stats based on gameweek history
                recent_form_stats = {}
                
                if "players" in gameweek_data:
                    for player_id, history in gameweek_data["players"].items():
                        player_id = int(player_id)
                        
                        # Find matching player in our filtered list
                        player_info = next((p for p in filtered_players if p.get("id") == player_id), None)
                        if not player_info:
                            continue
                        
                        # Initialize stats
                        recent_stats = {
                            "player_name": player_info.get("name", "Unknown"),
                            "matches": len(history),
                            "minutes": 0,
                            "points": 0,
                            "goals": 0,
                            "assists": 0,
                            "clean_sheets": 0,
                            "bonus": 0,
                            "expected_goals": 0,
                            "expected_assists": 0,
                            "expected_goal_involvements": 0,
                            "points_per_game": 0,
                            "gameweeks_analyzed": gameweek_data.get("gameweeks", [])
                        }
                        
                        # Sum up stats from gameweek history
                        for gw in history:
                            recent_stats["minutes"] += gw.get("minutes", 0)
                            recent_stats["points"] += gw.get("points", 0)
                            recent_stats["goals"] += gw.get("goals", 0)
                            recent_stats["assists"] += gw.get("assists", 0)
                            recent_stats["clean_sheets"] += gw.get("clean_sheets", 0)
                            recent_stats["bonus"] += gw.get("bonus", 0)
                            recent_stats["expected_goals"] += float(gw.get("expected_goals", 0))
                            recent_stats["expected_assists"] += float(gw.get("expected_assists", 0))
                            recent_stats["expected_goal_involvements"] += float(gw.get("expected_goal_involvements", 0))
                        
                        # Calculate averages
                        if recent_stats["matches"] > 0:
                            recent_stats["points_per_game"] = round(recent_stats["points"] / recent_stats["matches"], 1)
                            
                        # Round floating point values
                        recent_stats["expected_goals"] = round(recent_stats["expected_goals"], 2)
                        recent_stats["expected_assists"] = round(recent_stats["expected_assists"], 2)
                        recent_stats["expected_goal_involvements"] = round(recent_stats["expected_goal_involvements"], 2)
                        
                        recent_form_stats[str(player_id)] = recent_stats
                
                # Add recent form stats to result
                result["recent_form"] = {
                    "description": f"Stats for the last {num_gameweeks} gameweeks only",
                    "player_stats": recent_form_stats
                }
                
                # Add labels to clarify which stats are season-long vs. recent
                for player in result["players"]:
                    player["stats_type"] = "season_totals"
                    
            except Exception as e:
                logger.error(f"Error fetching gameweek data: {e}")
                result["gameweek_data_error"] = str(e)
        
        return result
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. While it mentions what the tool returns ('Filtered player data with summary statistics'), it doesn't address important behavioral aspects like whether this is a read-only operation, performance characteristics, data freshness, error conditions, or any limitations on the filtering capabilities.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections for purpose, arguments, and returns. While somewhat lengthy due to the 13 parameters, every sentence earns its place by providing essential information. The front-loaded purpose statement is clear, and the parameter explanations are efficiently organized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 13 parameters and no output schema, the description provides good parameter documentation but lacks important contextual information. Without annotations or output schema, it should ideally explain more about the return format, data limitations, or typical use cases. The parameter coverage is excellent, but other aspects are underdeveloped.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description provides excellent parameter semantics with detailed explanations for all 13 parameters, including examples ('e.g., "midfielders", "defenders"'), clarifications ('in millions', 'percentage'), and default values. This fully compensates for the 0% schema description coverage and adds substantial value beyond the basic schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose as 'Filter and analyze FPL players based on multiple criteria', which is a specific verb+resource combination. However, it doesn't explicitly distinguish this tool from sibling tools like 'search_fpl_players' or 'compare_players', which likely have overlapping functionality in the FPL domain.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. With multiple sibling tools available (like search_fpl_players, compare_players, get_player_information), there's no indication of when this comprehensive filtering/analysis tool is preferred over more specialized alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/rishijatia/fantasy-pl-mcp'

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