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)
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes the tool's behavior: it performs a search operation (implying read-only, non-destructive), returns a list of matches with specific fields (IDs, names, nationality, team), and indicates the output format. However, it lacks details on error handling, rate limits, or authentication needs.

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

Conciseness5/5

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

The description is well-structured and front-loaded with the core purpose, followed by usage guidance, examples, and output details. Each sentence adds value without redundancy, making it efficient and easy to parse for an AI agent.

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

Completeness4/5

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

Given the tool's low complexity (1 parameter, no output schema, no annotations), the description is largely complete. It covers purpose, usage, parameters, and output format adequately. However, it could improve by mentioning limitations (e.g., partial name matching) or error cases, which would enhance completeness for a search tool.

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 schema has 0% description coverage for its single parameter (query), but the description compensates by explaining that the query is used to 'search for professional cyclists by name' and provides examples (e.g., 'Tadej Pogacar', 'Van Aert'). This adds meaningful context beyond the bare schema, though it does not specify format constraints like case sensitivity.

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 purpose with a specific verb ('search') and resource ('professional cyclists by name'), distinguishing it from sibling tools that focus on retrieving specific rider data (e.g., get_rider_info) or race-related information. It explicitly mentions returning IDs and basic information, which sets it apart as a lookup tool.

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

Usage Guidelines5/5

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

The description provides explicit guidance on when to use this tool ('when you need a rider's ID for other operations but only know their name'), including example use cases. It implicitly distinguishes it from sibling tools by focusing on name-based searching rather than retrieving pre-defined rider data, though it does not explicitly name 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

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

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