Skip to main content
Glama
r-huijts

FirstCycling MCP Server

by r-huijts

search_race

Find cycling races by name to retrieve their IDs and countries. Use this tool to search for races like Tour de France or Giro d'Italia and get structured information for further operations.

Instructions

Search for cycling races by name. This tool helps find races by their name, returning a list of matching races with their IDs and countries. This is useful when you know a race's name but need its ID for other operations.

Example usage:
- Search for "tour" to find Tour de France and other tours
- Search for "giro" to find Giro d'Italia

Returns a formatted string with:
- List of matching races
- Each race's ID, name, and country
- Number of matches found

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYes

Implementation Reference

  • Core implementation of search_race tool: sends HTTP GET request to FirstCycling's race.php search endpoint with query parameters and returns raw HTML response.
    def search_race(self, query="", year=None, category="1"):
        return self._get_resource_response(self['race.php'], q=query, y=year, c=category)
  • Supporting helper function that parses the HTML from search_race to extract the best matching race ID using fuzzy string matching on race titles.
    def search_race_id(query, html, threshold=0.7):
        """
        Zoekt naar de race id binnen de HTML-respons door de titels fuzzy te matchen met de query.
        
        Parameters:
            query (str): De zoekopdracht, bv. "Milan Sanremo".
            html (str): De HTML-respons van fc.search_race.
            threshold (float): De minimale overeenkomst (0.0 tot 1.0) om als match te beschouwen.
            
        Returns:
            int of None: Het race-ID als er een match is, anders None.
        """
        soup = BeautifulSoup(html, "html.parser")
        norm_query = normalize(query)
        matches = []
        
        # Zoek naar alle <a>-tags die een href bevatten met race.php?r=
        for a in soup.find_all("a", href=re.compile(r"race\.php\?r=")):
            title = a.get("title")
            if title:
                norm_title = normalize(title)
                ratio = difflib.SequenceMatcher(None, norm_query, norm_title).ratio()
                if ratio >= threshold:
                    # Extraheer de race id uit de URL, bv. race.php?r=4&y=2025
                    m = re.search(r"r=(\d+)", a["href"])
                    if m:
                        race_id = int(m.group(1))
                        matches.append((race_id, title, ratio))
        
        if matches:
            # Geef de beste match (met de hoogste overeenkomst) terug
            best_match = max(matches, key=lambda x: x[2])
            return best_match[0]
        return None
  • High-level class method that implements race search by calling search_race to get HTML, then search_race_id to find matching race ID, returning structured result.
    @classmethod
    def search(cls, query, year=None, category="1"):
        """
        Search for races by name using fuzzy matching.
    
        Parameters
        ----------
        query : str
            The search query string.
        year : int
            The year to search for races (e.g., 2025). If None, uses current year.
        category : str
            Category code - e.g., "1" for WorldTour, "2" for ProSeries.
    
        Returns
        -------
        list
            A list containing one dictionary with the race id and query as name if a match is found,
            or an empty list otherwise.
        """
        try:
            # Get HTML content using search_race API call
            html = fc.search_race(query, year, category)
            # Find best matching race ID using fuzzy matching
            race_id = search_race_id(query, html)
            if race_id is not None:
                # Return a dictionary with all expected keys
                return [{
                    'id': race_id,
                    'name': query,
                    'country': '',
                    'date': '',
                    'category': category
                }]
            else:
                return []
        except Exception as e:
            print(f"Error in Race.search: {str(e)}")
            return []
Behavior3/5

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

With no annotations provided, the description carries the full burden. It discloses that the tool returns a list of matching races with IDs and countries, and mentions the output format. However, it doesn't cover important behavioral aspects like pagination, rate limits, error conditions, or whether the search is case-sensitive/fuzzy. The example usage adds some context but leaves gaps.

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: purpose first, then utility, examples, and return format. Every sentence earns its place - no redundant information. The example usage is helpful without being verbose.

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 moderate complexity (single parameter search), no annotations, and no output schema, the description does a good job covering purpose, usage, parameter meaning, and return format. However, it lacks details about search behavior (exact vs. partial matching, sorting) and error handling that would make it fully complete.

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 (no parameter descriptions in schema), but the description compensates well. It explains that the 'query' parameter is for searching by race name, provides two concrete examples ('tour' and 'giro'), and clarifies this is for name-based search. This adds significant meaning beyond the bare schema.

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: 'Search for cycling races by name' (specific verb+resource). It distinguishes from siblings by focusing on name-based search rather than retrieving details, results, or rider information like get_race_details or search_rider. The description explicitly mentions what it returns and why it's useful.

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

Usage Guidelines4/5

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

The description provides clear context: 'useful when you know a race's name but need its ID for other operations,' which implicitly suggests using this tool for ID lookup rather than get_race_details for comprehensive race information. However, it doesn't explicitly state when NOT to use this tool or name specific alternatives among the siblings.

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