Skip to main content
Glama
rishijatia

Fantasy Premier League MCP Server

search_fpl_players

Search Fantasy Premier League players by name, with optional filters for position and team to retrieve player details.

Instructions

Search for FPL players by name with optional filtering

    Args:
        query: Player name or partial name to search for
        position: Optional position filter (GKP, DEF, MID, FWD)
        team: Optional team name filter
        limit: Maximum number of results to return

    Returns:
        List of matching players with details
    

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes
positionNo
teamNo
limitNo

Implementation Reference

  • MCP tool handler for search_fpl_players - registered via @mcp.tool() decorator, delegates to search_players async function
    @mcp.tool()
    async def search_fpl_players(
        query: str,
        position: Optional[str] = None,
        team: Optional[str] = None,
        limit: int = 5
    ) -> Dict[str, Any]:
        """Search for FPL players by name with optional filtering
    
        Args:
            query: Player name or partial name to search for
            position: Optional position filter (GKP, DEF, MID, FWD)
            team: Optional team name filter
            limit: Maximum number of results to return
    
        Returns:
            List of matching players with details
        """
        # Handle case when a dictionary is passed instead of string
        if isinstance(query, dict) and 'query' in query:
            query = query['query']
            
        return await search_players(query, position, team, limit)
  • Helper function that performs the actual search logic: calls find_players_by_name, applies optional position/team filters, and returns structured results
    async def search_players(
        query: str,
        position: Optional[str] = None,
        team: Optional[str] = None,
        limit: int = 5
    ) -> Dict[str, Any]:
        """
        Search for players by name with optional filtering by position and team.
    
        Args:
            query: Player name or partial name to search for
            position: Optional position filter (GKP, DEF, MID, FWD)
            team: Optional team name filter
            limit: Maximum number of results to return
    
        Returns:
            List of matching players with details
        """
        logger = logging.getLogger(__name__)
        logger.info(f"Searching players: query={query}, position={position}, team={team}")
    
        # Find players by name
        matches = await find_players_by_name(query, limit=limit * 2)  # Get more than needed for filtering
    
        # Apply position filter if specified
        if position and matches:
            matches = [p for p in matches if p.get("position") == position.upper()]
    
        # Apply team filter if specified
        if team and matches:
            matches = [
                p for p in matches
                if team.lower() in p.get("team", "").lower() or
                team.lower() in p.get("team_short", "").lower()
            ]
    
        # Limit results
        matches = matches[:limit]
    
        return {
            "query": query,
            "filters": {
                "position": position,
                "team": team,
            },
            "total_matches": len(matches),
            "players": matches
        }
  • Core name-matching logic in the resources layer - implements fuzzy matching with nickname support, initial matching, and fallback substring search
    async def find_players_by_name(name: str, limit: int = 5) -> List[Dict[str, Any]]:
        """
        Find players by partial name match with advanced matching.
        
        Args:
            name: Player name to search for (supports partial names, nicknames, and initials)
            limit: Maximum number of results to return
            
        Returns:
            List of matching players sorted by relevance and points
        """
        # Get all players
        logger = logging.getLogger(__name__)
        logger.info(f"Finding players by name: {name}")
        all_players = await get_players_resource()
        logger.info(f"Found {len(all_players)} players")
        
        # Normalize search term
        search_term = name.lower().strip()
        if not search_term:
            return []
        
        # Common nickname and abbreviation mapping
        nicknames = {
            "kdb": "kevin de bruyne",
            "vvd": "virgil van dijk",
            "taa": "trent alexander-arnold",
            "cr7": "cristiano ronaldo",
            "bobby": "roberto firmino",
            "mo salah": "mohamed salah",
            "mane": "sadio mane",
            "auba": "aubameyang",
            "lewa": "lewandowski",
            "kane": "harry kane",
            "rashford": "marcus rashford",
            "son": "heung-min son",
        }
        
        # Check for nickname match
        if search_term in nicknames:
            search_term = nicknames[search_term]
        
        # Split search term into parts for multi-part matching
        search_parts = search_term.split()
    
        
        # Store scored results
        scored_players = []
        
        for player in all_players:
            # Extract player name components
            full_name = player["name"].lower()
            web_name = player.get("web_name", "").lower()
            
            # Try to extract first and last name
            name_parts = full_name.split()
            first_name = name_parts[0] if name_parts else ""
            last_name = name_parts[-1] if len(name_parts) > 1 else ""
            
            # Initialize score and tracking reasons
            score = 0
            
            # 1. Exact full name match
            if search_term == full_name:
                score += 100
            
            # 2. Exact match on web_name (common name)
            elif search_term == web_name:
                score += 90
            
            # 3. Exact match on last name
            elif len(search_parts) == 1 and search_term == last_name:
                score += 80
            
            # 4. Exact match on first name
            elif len(search_parts) == 1 and search_term == first_name:
                score += 70
                
            # 5. Check for initials match (e.g., "KDB")
            if len(search_term) <= 5 and all(c.isalpha() for c in search_term):
                # Try to match initials
                initials = ''.join(part[0] for part in full_name.split() if part)
                if search_term.lower() == initials.lower():
                    score += 85
            
            # 6. Multi-part name matching (e.g., "Mo Salah")
            if len(search_parts) > 1:
                # Check if first part matches first name and last part matches last name
                if (search_parts[0] in first_name and 
                    search_parts[-1] in last_name):
                    score += 75
                
                # Check if parts appear in order in the full name
                search_combined = ''.join(search_parts)
                full_combined = ''.join(full_name.split())
                if search_combined in full_combined:
                    score += 50
            
            # 7. Substring matches
            if search_term in full_name:
                score += 40
            
            # 8. Partial word matches in full name
            for part in search_parts:
                if part in full_name:
                    score += 30
                    
            # 9. Partial word matches in web name
            for part in search_parts:
                if part in web_name:
                    score += 25
            
            # 10. Add a bonus score for high-point players (tiebreaker)
            points_score = min(20, float(player["points"]) / 50)  # Up to 20 extra points
            
            # Total score
            total_score = score + (points_score if score > 0 else 0)
            
            # Add to results if there's any match
            if score > 0:
                scored_players.append((total_score, player))
        
        # Sort by score (highest first)
        sorted_players = [player for _, player in sorted(scored_players, key=lambda x: x[0], reverse=True)]
        # If no matches with good confidence, fall back to simple contains match
        if not sorted_players or (sorted_players and scored_players[0][0] < 30):
            fallback_players = [
                p for p in all_players 
                if search_term in p["name"].lower() or search_term in p.get("web_name", "").lower()
            ]
            # Sort fallback by points
            fallback_players.sort(key=lambda p: float(p["points"]), reverse=True)
            
            # Merge results, prioritizing scored results
            merged = []
            seen_ids = set(p["id"] for p in sorted_players)
            
            merged.extend(sorted_players)
            for p in fallback_players:
                if p["id"] not in seen_ids:
                    merged.append(p)
                    seen_ids.add(p["id"])
            
            sorted_players = merged
        
        # Return limited results
  • Fetches and formats all player data from the FPL API bootstrap endpoint into structured player objects
    async def get_players_resource(name_filter: Optional[str] = None, team_filter: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        Format player data for the MCP resource.
        
        Args:
            name_filter: Optional filter for player name (case-insensitive partial match)
            team_filter: Optional filter for team name (case-insensitive partial match)
            
        Returns:
            Formatted player data
        """
        # Get raw data from API
        data = await api.get_bootstrap_static()
        
        # Create team and position lookup maps
        team_map = {t["id"]: t for t in data["teams"]}
        position_map = {p["id"]: p for p in data["element_types"]}
        logging.info(f"Team map: {team_map}")
        logging.info(f"Position map: {position_map}")
        
        # Format player data
        players = []
        for player in data["elements"]:
            # Extract team and position info
            team = team_map.get(player["team"], {})
            position = position_map.get(player["element_type"], {})
            
            player_name = f"{player['first_name']} {player['second_name']}"
            team_name = team.get("name", "Unknown")
            
            # Apply filters if specified
            if name_filter and name_filter.lower() not in player_name.lower():
                continue
                
            if team_filter and team_filter.lower() not in team_name.lower():
                continue
            
            # Build comprehensive player object with all available stats
            player_data = {
                "id": player["id"],
                "name": player_name,
                "web_name": player["web_name"],
                "team": team_name,
                "team_short": team.get("short_name", "UNK"),
                "position": position.get("singular_name_short", "UNK"),
                "price": player["now_cost"] / 10.0,
                "form": player["form"],
                "points": player["total_points"],
                "points_per_game": player["points_per_game"],
                
                # Playing time
                "minutes": player["minutes"],
                "starts": player["starts"],
                
                # Key stats
                "goals": player["goals_scored"],
                "assists": player["assists"],
                "clean_sheets": player["clean_sheets"],
                "goals_conceded": player["goals_conceded"],
                "own_goals": player["own_goals"],
                "penalties_saved": player["penalties_saved"],
                "penalties_missed": player["penalties_missed"],
                "yellow_cards": player["yellow_cards"],
                "red_cards": player["red_cards"],
                "saves": player["saves"],
                "bonus": player["bonus"],
                "bps": player["bps"],
                
                # Advanced metrics
                "influence": player["influence"],
                "creativity": player["creativity"],
                "threat": player["threat"],
                "ict_index": player["ict_index"],
                
                # Expected stats (if available)
                "expected_goals": player.get("expected_goals", "N/A"),
                "expected_assists": player.get("expected_assists", "N/A"),
                "expected_goal_involvements": player.get("expected_goal_involvements", "N/A"),
                "expected_goals_conceded": player.get("expected_goals_conceded", "N/A"),
                
                # Ownership & transfers
                "selected_by_percent": player["selected_by_percent"],
                "transfers_in_event": player["transfers_in_event"],
                "transfers_out_event": player["transfers_out_event"],
                
                # Price changes
                "cost_change_event": player["cost_change_event"] / 10.0,
                "cost_change_start": player["cost_change_start"] / 10.0,
                
                # Status info
                "status": player["status"],
                "news": player["news"],
                "chance_of_playing_next_round": player["chance_of_playing_next_round"],
            }
            
            players.append(player_data)
        logging.info(f"Formatted {len(players)} players")
        return players
  • Tool registration - the register_tools function (defined at line 282) is exposed via __init__.py to register all player tools including search_fpl_players via the @mcp.tool() decorator
    # Register tools
    register_tools = register_tools
Behavior3/5

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

The description indicates that partial names are supported ('Player name or partial name'), but does not disclose other behavioral traits such as case sensitivity, exact match behavior, pagination, or performance characteristics. Given the absence of annotations, the description adds some context but is not comprehensive.

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 concise, using a structured docstring format with Args and Returns sections. All information is relevant and not extraneous. It could be slightly more streamlined by removing the 'Args' and 'Returns' headers to save space, but overall it is well-organized and front-loaded with the core purpose.

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?

The description covers the input parameters well but lacks details about the output format. 'List of matching players with details' is vague – it does not specify what fields are returned, sorting order, or error conditions. Given that there is no output schema, more detail would be needed for full completeness.

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

Parameters4/5

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

The input schema has 0% description coverage, so the description carries the full burden. It provides clear meanings for all 4 parameters: query is 'Player name or partial name', position lists valid values (GKP, DEF, MID, FWD), team is 'team name filter', and limit is 'Maximum number of results'. This adds significant value beyond the schema's titles.

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

Purpose5/5

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

The description clearly states the tool's function: 'Search for FPL players by name with optional filtering'. It specifies the resource (FPL players) and the action (search), and differentiates from sibling tools like get_player_information by emphasizing name-based search and optional filters.

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 (e.g., compare_players, get_player_information). It does not mention prerequisites, limitations, or scenarios where other tools would be more appropriate. The context is purely functional without decision support.

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