Skip to main content
Glama
r-huijts

FirstCycling MCP Server

by r-huijts

get_rider_victories

Retrieve a detailed list of a cyclist's UCI-registered race victories, including race categories, dates, and years. Optionally filter results to display only WorldTour wins. Use rider ID to fetch data.

Instructions

Get a comprehensive list of a rider's UCI victories. This tool retrieves detailed information about all UCI-registered race victories achieved by the cyclist throughout their career. Victories can be filtered to show only WorldTour wins if desired.

Note: If you don't know the rider's ID, use the search_rider tool first to find it by name.

Example usage:
- Get all UCI victories for Tadej Pogačar (ID: 16973)
- Get WorldTour victories for Jonas Vingegaard (ID: 16974)

Returns a formatted string with:
- Complete list of victories
- Race details including category
- Date and year of each victory
- Option to filter by WorldTour races only

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
rider_idYes
world_tour_onlyNo

Implementation Reference

  • The handler function named get_rider_victories that implements the tool logic by fetching rider victories using the API and displaying summary statistics.
    def get_rider_victories(rider_id):
        """Get a rider's victories from FirstCycling"""
        rider = Rider(rider_id)
        
        # Get basic rider info
        print(f"Rider ID: {rider.ID}")
        
        # Get all victories
        victories = rider.victories()
        
        # Display information about victories
        if hasattr(victories, 'results_df') and not victories.results_df.empty:
            print(f"\nFound {len(victories.results_df)} career victories:")
            
            # Group by year to count victories per year
            victories_by_year = victories.results_df.groupby('Year').size()
            print("\nVictories by year:")
            for year, count in victories_by_year.items():
                print(f"{year}: {count} wins")
            
            # Check for specific race categories
            if 'CAT' in victories.results_df.columns:
                categories = victories.results_df['CAT'].value_counts()
                print("\nVictories by category:")
                for category, count in categories.items():
                    print(f"{category}: {count}")
                    
            # Display the first 10 victories
            print("\nMost recent 10 victories:")
            # Sort by date (newest first) and show the first 10
            if 'Date_Formatted' in victories.results_df.columns:
                recent_victories = victories.results_df.sort_values('Date_Formatted', ascending=False).head(10)
                # Display in a readable format
                for _, row in recent_victories.iterrows():
                    print(f"{row['Date_Formatted']}: {row['Race']} ({row['CAT']})")
        else:
            print("No victories found for this rider.")
  • Supporting method in Rider class to fetch the victories data by calling the RiderVictories endpoint.
    def victories(self, world_tour=None, uci=None):
    	"""
    	Get the rider's victories.
    
    	Parameters
    	----------
    	world_tour : bool
    		True if only World Tour wins wanted
    	uci : bool
    		True if only UCI wins wanted
    
    	Returns
    	-------
    	RiderVictories
    	"""
    	return self._get_endpoint(endpoint=RiderVictories, high=1, k=1, uci=1 if uci else None, wt=1 if world_tour else None)
  • The core parsing class for rider victories endpoint that extracts and formats the victories table into a pandas DataFrame.
    class RiderVictories(RiderEndpoint):
    	"""
    	Rider's victories. Extends RiderEndpoint.
    
    	Attributes
    	----------
    	results_df : pd.DataFrame
    		Table of rider's victories.
    	"""
    
    	def _parse_soup(self):
    		super()._parse_soup()
    		self._get_victories()
    
    	def _get_victories(self):
    		# Find table with victories
    		table = self.soup.find('table', {'class': "sortTabell tablesorter"})
    		if table:
    			# Check if the table has "No data" content
    			no_data_text = table.get_text().strip()
    			if "No data" in no_data_text:
    				# Table exists but has no data
    				self.results_df = pd.DataFrame()
    				return
    				
    			try:
    				# Try to parse using the parse_table function
    				self.results_df = parse_table(table)
    				if self.results_df is None:
    					self.results_df = pd.DataFrame()  # Empty DataFrame if no victories found
    			except Exception as e:
    				# If there's an error in parsing, handle it by creating a basic DataFrame manually
    				print(f"Warning: Error parsing victories table: {str(e)}")
    				# Fallback: Try to create a DataFrame directly from the HTML
    				try:
    					import io
    					from dateutil.parser import parse
    					
    					# Parse the basic table
    					html = str(table)
    					self.results_df = pd.read_html(io.StringIO(html), decimal=',')[0]
    					
    					# Check if the table contains "No data"
    					if (self.results_df.shape[0] == 1 and 
    						"No data" in self.results_df.iloc[0, 0]):
    						self.results_df = pd.DataFrame()
    						return
    					
    					# Clean up column names
    					# The typical format is: Year | Date | Race | Category
    					if 'Date.1' in self.results_df.columns:
    						self.results_df.rename(columns={
    							'Date': 'Year', 
    							'Date.1': 'Date',
    							'Unnamed: 2': 'Month_Day'  # This can be blank or contain additional date info
    						}, inplace=True)
    						
    						# Convert Year to string
    						self.results_df['Year'] = self.results_df['Year'].astype(str)
    						
    						# Handle date formatting - combine Year and Date if available
    						if 'Month_Day' in self.results_df.columns:
    							# Clean Month_Day column (keep only non-NaN values)
    							self.results_df = self.results_df.drop('Month_Day', axis=1)
    							
    						# If Date column has decimal format (e.g., 22.04), treat as MM.DD format
    						def format_date(row):
    							try:
    								if pd.notnull(row['Date']):
    									date_str = row['Date']
    									if isinstance(date_str, float):
    										# Convert float (22.04) to string and handle decimal
    										date_parts = str(date_str).split('.')
    										if len(date_parts) == 2:
    											day = date_parts[0].zfill(2)
    											month = date_parts[1].zfill(2)
    											return f"{row['Year']}-{month}-{day}"
    									return f"{row['Year']}-01-01"  # Default date if format not recognized
    								return f"{row['Year']}-01-01"  # Default date if no Date value
    							except:
    								return f"{row['Year']}-01-01"  # Default for any errors
    						
    						# Create formatted date column
    						self.results_df['Date_Formatted'] = self.results_df.apply(format_date, axis=1)
    				except Exception as e:
    					# If all else fails, just return an empty DataFrame
    					print(f"Warning: Error creating DataFrame from table HTML: {str(e)}")
    					self.results_df = pd.DataFrame()
    		else:
    			# No table found
    			self.results_df = pd.DataFrame()
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 what the tool does (retrieves detailed victory information), mentions filtering options (WorldTour only), and outlines the return format (formatted string with specific details). However, it lacks information on potential limitations like rate limits, error handling, or data freshness.

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: it starts with the core purpose, adds filtering details, provides usage notes, includes practical examples, and ends with return value information. Every sentence adds value without redundancy, making it efficient and easy to parse.

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 complexity (2 parameters, no output schema, no annotations), the description does an excellent job covering purpose, usage, parameters, and return format. It lacks only minor details like error cases or pagination, but for a read-only query tool, it is nearly complete. The absence of an output schema is mitigated by the detailed return description.

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

Parameters5/5

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

The schema description coverage is 0%, so the description must compensate. It fully explains both parameters: rider_id is described in the context of retrieving victories for a specific cyclist, and world_tour_only is explicitly mentioned as a filter option. The example usage further clarifies how these parameters are applied.

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 specific action ('retrieves detailed information about all UCI-registered race victories') and distinguishes it from siblings by focusing on comprehensive victory data rather than specific race types (e.g., grand_tour_results, monument_results) or basic rider info. It explicitly mentions filtering by WorldTour wins, which adds specificity.

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 vs. alternatives: it instructs to use search_rider first if the rider ID is unknown, and it distinguishes from siblings by specifying that this tool is for 'comprehensive list of UCI victories' rather than subsets like best_results or race_history. The example usage reinforces this context.

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