Skip to main content
Glama
r-huijts

FirstCycling MCP Server

by r-huijts

search_rider

Find professional cyclists by name and retrieve IDs, basic info, and current teams. Use this tool to locate rider details for further operations in the FirstCycling MCP Server.

Instructions

Search for professional cyclists by name. This tool helps find riders by their name, returning a list of matching riders with their IDs and basic information. This is useful when you need a rider's ID for other operations but only know their name.

Example usage: - Search for "Tadej Pogacar" to find Tadej Pogačar's ID - Search for "Van Aert" to find Wout van Aert's ID Returns a formatted string with: - List of matching riders - Each rider's ID, name, nationality, and current team - Number of matches found

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes

Implementation Reference

  • Core handler logic for searching riders by name. Queries the FirstCycling search.php endpoint with the given query, parses the HTML response to extract potential rider matches, computes a custom similarity score using difflib and Soundex for fuzzy matching, filters results above a threshold, handles recursive partial searches if no results, removes duplicates, and returns a sorted list of matching riders with their ID, name, nationality, and team.
    def search(cls, query: str) -> List[Dict[str, Any]]: """ Search for riders by name using fuzzy matching. Parameters: query (str): The name to search for Returns: List[Dict[str, Any]]: List of dictionaries containing rider details (id, name, nationality, team), sorted by best match first """ # Use search.php instead of rider.php for search functionality url = f"{cls.base_url}/search.php?s={query}" try: response = requests.get(url) soup = BeautifulSoup(response.text, 'html.parser') # Find all tables with the rider results tables = soup.find_all('table') results = [] # Look for rider links in all tables for table in tables: rows = table.find_all('tr') for row in rows: cells = row.find_all('td') if not cells: continue try: # Try to find rider link in any cell rider_link = row.find('a', href=lambda href: href and 'rider.php?r=' in href) if rider_link: href = rider_link['href'] match = re.search(r'rider.php\?r=(\d+)', href) if match: rider_id = int(match.group(1)) rider_name = rider_link.text.strip() # Extract nationality and team if available nationality = "" team = "" # Find team info (usually in a span with color:grey) team_span = row.find('span', style=lambda s: s and 'color:grey' in s) if team_span: team = team_span.text.strip() # Look for nationality flag flag_span = row.find('span', class_=lambda c: c and 'flag flag-' in c) if flag_span and 'class' in flag_span.attrs: flag_class = flag_span['class'] if len(flag_class) >= 2 and flag_class[0] == 'flag': nationality = flag_class[1].replace('flag-', '') # Calculate similarity score using our improved method match_ratio = calculate_similarity(query, rider_name) # Only include riders with a minimum match score if match_ratio >= 0.4: # Lower threshold to catch more variations results.append({ 'id': rider_id, 'name': rider_name, 'nationality': nationality, 'team': team, 'match_ratio': match_ratio }) except Exception as e: print(f"Error processing row: {str(e)}") continue # If no direct results, try searching with parts of the query if not results and ' ' in query: # Extract main parts and try searching with them parts = query.strip().split() if len(parts) > 1: # Try with the first part (usually first name) first_part = parts[0] if len(first_part) >= 3: # Only if reasonably long first_part_results = cls.search(first_part) for r in first_part_results: r['match_ratio'] = calculate_similarity(query, r['name']) * 0.9 # Lower confidence results.append(r) # Try with the last part (usually last name) last_part = parts[-1] if len(last_part) >= 3: # Only if reasonably long last_part_results = cls.search(last_part) for r in last_part_results: r['match_ratio'] = calculate_similarity(query, r['name']) * 0.9 # Lower confidence results.append(r) # Sort results by match ratio (best matches first) results.sort(key=lambda x: x['match_ratio'], reverse=True) # Remove duplicates based on rider ID unique_results = [] seen_ids = set() for r in results: if r['id'] not in seen_ids: seen_ids.add(r['id']) unique_results.append(r) # Remove match_ratio from the results for result in unique_results: if 'match_ratio' in result: del result['match_ratio'] return unique_results except Exception as e: print(f"Error searching for rider: {str(e)}") return []
  • Helper function to calculate fuzzy similarity between query and rider name using difflib SequenceMatcher on full names and parts, plus phonetic matching via Soundex boost.
    def calculate_similarity(query, name): """ Calculate similarity between a search query and a rider name, considering different name formats and parts. Parameters: query (str): The search query name (str): The rider name to compare against Returns: float: A similarity score between 0 and 1 """ # Normalize both strings norm_query = normalize(query) norm_name = normalize(name) # Basic similarity using sequence matcher basic_similarity = difflib.SequenceMatcher(None, norm_query, norm_name).ratio() # Split into parts and try different combinations query_parts = norm_query.split() name_parts = norm_name.split() # If either has no parts, return the basic similarity if not query_parts or not name_parts: return basic_similarity # Check for best part matches part_similarities = [] # Compare each query part against the full name for q_part in query_parts: part_sim = difflib.SequenceMatcher(None, q_part, norm_name).ratio() part_similarities.append(part_sim) # Compare full query against each name part for n_part in name_parts: part_sim = difflib.SequenceMatcher(None, norm_query, n_part).ratio() part_similarities.append(part_sim) # Compare all parts combinations (to handle first/last name variations) for q_part in query_parts: for n_part in name_parts: part_sim = difflib.SequenceMatcher(None, q_part, n_part).ratio() part_similarities.append(part_sim) # Get the best part similarity best_part_sim = max(part_similarities) if part_similarities else 0 # Add Soundex comparison for phonetic matching soundex_boost = 0 # Apply Soundex to each part combination to handle phonetic variations for q_part in query_parts: q_soundex = soundex(q_part) for n_part in name_parts: n_soundex = soundex(n_part) if q_soundex == n_soundex and q_soundex: # Exact Soundex match soundex_boost = 0.4 # Significant boost for phonetic matches break if soundex_boost > 0: break # Combine different matching approaches for final score # This weights sequence matching higher, but still allows phonetic matches to influence results combined_sim = (basic_similarity + best_part_sim) / 2 + soundex_boost # Cap at 1.0 for consistency return min(combined_sim, 1.0)

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/r-huijts/firstcycling-mcp'

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