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
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| position | No | ||
| team | No | ||
| limit | No |
Implementation Reference
- src/fpl_mcp/fpl/tools/players.py:322-344 (handler)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 - src/fpl_mcp/fpl/tools/players.py:347-348 (registration)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