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