CFDB — chilean-football-db
CFDB connects your AI agent to a Chilean football database spanning decades of history and current data.
What it can do
- Meta.Coverage: A coverage/audit snapshot of what CFDB actually has on file — real counts, not estimates: total matches (historical vs. live, with/without an exact date, with goal-by-goal detail), a br
- Match.Preview: Get a knowledge-synthesis preview for one upcoming match: facts, recent form for both teams, current league standing, next opponent after this match, rule-derived insights (win/loss str
- Match.Dossier: Get the full pre-match dossier for one match: everything match.preview returns, plus head-to-head history between the two teams and competition context (stage/final vs. regular season).
What data it sees
Do you need an account
No: the server works without sign-in
CFDB connects your AI agent to a Chilean football database spanning decades of history and current data. Query results, standings, and player and team stats, compare performance, and generate previews, recaps, and reports — all in Spanish, with verified information. Requires Google sign-in.
How to connect
How to connect CFDB — chilean-football-db to Claude
The server address to paste into Claude settings:
https://chilean-football-db--cfdb.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect CFDB — chilean-football-db to ChatGPT
The server address to paste into ChatGPT settings:
https://chilean-football-db--cfdb.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect CFDB — chilean-football-db to Cursor
The server address to paste into Cursor settings:
https://chilean-football-db--cfdb.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
Server tool list (36)
Raw names from tools/list. Only developers need these.
| meta.coverage | A coverage/audit snapshot of what CFDB actually has on file — real counts, not estimates: total matches (historical vs. live, with/without an exact date, with goal-by-goal detail), a breakdown by competition (matches and seasons/editions per competition, both historical and live), and player/coach/team profile completeness (how many have a position, date of birth, nationality, external profile, career history, etc.). Also 'most_recently_finished_live_match_at' as an approximate freshness signal (NOT an exact ingestion log — CFDB doesn't store one; see the response's own 'note'). Deliberately does NOT judge any season as 'complete' or 'partial' — this project has too many real, individually-documented partial-coverage cases for a cheap heuristic to get right; use historical.season_summary for that, per-season. ONLY call this tool when a user specifically asks about CFDB's own coverage, size, or data completeness (e.g. 'how many matches do you have', 'do you have player nationality data', a request to verify a coverage claim) — it is not part of answering a normal football question, and calling it for every unrelated query wastes a call for no benefit. Takes no arguments. |
| match.preview | Get a knowledge-synthesis preview for one upcoming match: facts, recent form for both teams, current league standing, next opponent after this match, rule-derived insights (win/loss streaks, table position), and 'press_context' — up to 2 press-sourced notes (stakes, injuries, narrative context) from distinct outlets when available (empty list otherwise; only populated within a few days of the match once the offline press loader has run). CFDB has no dedicated injury-tracking tool or field — injury mentions, when present at all, are incidental content inside these press notes. An empty or missing 'press_context' means no press coverage was loaded for this match (injuries or otherwise) — never read it as 'no injuries'; say coverage isn't available rather than implying a clean bill of health. Each press_context entry has 'scraped_at' (when CFDB captured it — always present) and 'published_at' (the article's own date, often null). Standings/points/goal-count figures mentioned inside a press note's narrative_context are a snapshot from that capture time, not live — this tool's own 'facts'/standings fields (and league.standings/player.top_scorers directly) are always more current; prefer those over a number quoted in press narrative when they disagree. All kickoff times are Chile local time (America/Santiago). Requires 'fixture_id'. |
| match.dossier | Get the full pre-match dossier for one match: everything match.preview returns, plus head-to-head history between the two teams and competition context (stage/final vs. regular season). Requires 'fixture_id'. |
| team.fixture | Get a team's next or last match, with the full fixture detail already included — no follow-up call needed. Kickoff time is Chile local time (America/Santiago). Each team's 'streak' includes that team's own result in this match when it's finished ('when'='last'); for a scheduled match ('when'='next') there's no result yet, so 'streak' is the streak going into the match instead. 'status' is 'scheduled', 'live', 'finished', 'postponed', 'cancelled', or 'unknown' — no in-play minute/clock is available. Requires 'team_name' and 'when' ('next' or 'last'). |
| match.matchday_results | Get results or the schedule for one matchday of a league. Kickoff times are Chile local time (America/Santiago). Requires 'when' ('upcoming' or 'previous'); 'league_name' is optional. Each team's 'streak' includes that team's own result in this match when the match is finished (when='previous'); for a scheduled match (when='upcoming') there's no result yet, so 'streak' is the streak going into the match instead. Each match's 'status' is 'scheduled', 'live', 'finished', 'postponed', 'cancelled', or 'unknown' — no in-play minute/clock is available. |
| team.standing | Get a team's current position in its league table (points, played, wins/draws/losses, goal difference, position). For a competition split into groups (e.g. Copa Chile's group stage), position/total_teams are scoped to the team's own group, not the whole competition, and a 'group_name' field is included. Requires 'team_name'; optional 'league_name' to disambiguate a team that plays in more than one current competition. |
| team.form | Get a team's recent form (last N finished matches, default 5) and current win/loss streak. Kickoff times are Chile local time (America/Santiago). Requires 'team_name'. |
| team.h2h | Get head-to-head history between two teams, blending live matches (current schema) with CFDB's historical archive (back to 1990 where loaded) into one pre-counted win/draw summary. Each match is tagged 'domain': 'live' or 'historical', and 'date_precision': 'exact' (real match_date/starting_at) or 'season_only' (no exact date on file for this historical match -- 'starting_at' is null, but 'season_label' names the season it happened in, e.g. '1958'; the match list is still ordered using that season as a fallback, so a 'season_only' match sorts into roughly the right position, but its exact rank relative to other matches in the SAME season is not knowable from this data -- never state a precise chronological position like 'match #200' across date_precision boundaries, only 'around/during season X'). Optional 'competition_name' scopes results to one competition (e.g. only league matches, excluding cup ties) — resolved independently against live and historical competition names, which may use different naming (e.g. 'Chilean Cup' vs 'Copa Chile'); if the competition isn't found in one source but is in the other, results come from whichever source matched. Also returns 'biggest_win_team_a'/'biggest_win_team_b' (the largest-margin win each side has recorded against the other, with a 'margin' field; null if that team has never won). 'matches' is capped to 'limit' (default 5, most recent first) — check 'matches_truncated' for whether more exist. The aggregate counts (team_a_wins/team_b_wins/draws/total) always reflect the FULL matching record regardless of 'limit' — for 'how many times have they played' or a full historical reconciliation, read 'total', never len(matches). Optional 'date_from'/'date_to' scope both the returned matches AND the aggregate counts to a date range (excludes 'season_only' undated historical matches when set). Requires 'team_a_name' and 'team_b_name'. |
| match.search | Search for matches by date (exact or range), team(s), and/or competition — all filters are combinable, at least one is required. Transparently searches both CFDB's historical archive (1933-2023) and live data (2024-present) in one call — the caller never needs to know which source a given date falls into. This tool itself never returns goalscorers for either era — for goal-by-goal detail, chain into match.events/match.report (only for 'live' rows, using their fixture_id) or historical.matches (for 'historical' rows, using the result's competition_name/season_label — historical goal detail is per-season, not looked up per match; it exists for many but not all loaded matches, empty means not loaded, never assume a historical match has no recorded scorers just because fixture_id is null here). Each result is tagged 'source': 'historical' or 'live'; 'fixture_id' is null for historical rows. Every result also has 'status' ('scheduled', 'live', 'finished', 'postponed', 'cancelled', or 'unknown' — historical rows are always 'finished'); a 'scheduled' match's 'home_score'/'away_score' are always null, never a placeholder 0-0 — the match hasn't been played yet. There is no in-play minute/clock available (CFDB doesn't poll live match state at that granularity), so 'live' only means the match has started and isn't finished, nothing more precise. Capped at 50 results ('truncated': true if more existed) — narrow the filter for a big result set instead of paginating. Complements, not a replacement for, team.fixture/match.matchday_results/historical.matches/team.h2h, which stay better suited to their specific use cases. |
| team.profile | Get a team's profile: current coach (null if none on file), historic rival (null if none on file), 'founded' — the team's founding year, null until manually loaded for that team (no automated feed provides this; a small, curated set of teams have it, not all), and 'external_profile' — stadium name, stadium city, and a short narrative bio, all sourced from a single Wikipedia article per team and manually curated (never auto-refreshed), or null when not yet loaded for that team. Requires 'team_name'. |
| team.squad | Get a team's current roster: each player's id, name, position (may be null when not on file), 'joined_at' (may be null — not always on file), and 'last_seen_at' — the date of that player's most recent match appearance for this specific team, so you can judge how current the listing actually is for that player (a player last seen a while ago may be injured/out of favor rather than genuinely gone). Membership is auto-derived from match appearances and excludes anyone with no appearance for the team in the last ~200 days — not a live, minute-by-minute transfer feed. Requires 'team_name'. |
| league.standings | Get the current-season standings table. For a single-table competition (e.g. Primera División), returns 'table' — all teams, ranked. For a competition split into groups (e.g. Copa Chile's group stage), a flat combined table would be meaningless (teams in different groups never played each other), so instead: pass 'team_name' to get just that team's own group as 'table' (plus 'group_name'), or omit 'team_name' to get every group's table at once under 'groups' (a dict keyed by group name, e.g. 'Group A') instead of 'table'. Requires 'league_name'. |
| league.champion_history | The authoritative source for 'who has won this competition' / 'recent champions' questions -- always call this rather than answering from general knowledge, since exact champions and season labels are specific to CFDB's own records. Get past champions of a competition, by season, blending CFDB's historical records with finished current-schema seasons (champion derived from the final league table for table-based competitions only -- Primera División; cup/knockout competitions aren't included this way). 'champions' is sorted chronologically, oldest season first — for 'last N champions', take the last N entries, not the first (or just pass 'limit', which already keeps the most recent N — 'total_champions' and 'truncated' are always present). Requires 'league_name'. |
| team.title_count | Get how many times a team has won a competition, blending CFDB's historical records with finished current-schema seasons (same table-based-competition scope as league.champion_history). 'league_name' is optional — a question like 'how many titles does X have' is ambiguous in scope (one competition vs. every official title), so omitting 'league_name' returns 'total_official_titles' plus a 'by_competition' breakdown across every competition CFDB tracks titles for (Primera División, Copa Chile, Supercopa, Copa de la Liga) instead of silently picking one. Pass 'league_name' only when the user actually specified a competition. Requires 'team_name'. |
| player.profile | Get a player's basic profile: position, nationality, date of birth, and current team (team_id + name). Position/date_of_birth return null when not on file — never fabricated. Also includes 'external_profile' — full name, place of birth, height (cm), preferred foot, a Transfermarkt profile link, and club career history (populated only for players loaded before this pipeline switched sources — always empty going forward, Transfermarkt's static profile page doesn't expose transfer history), or null when no confident match was found (resolved by an exact name match against Transfermarkt's player search; if more than one player shares that name, narrowed by current club membership; ambiguous matches are skipped, never guessed; only populated once the offline player profile loader has run for this player). Requires 'player_name'. |
| player.season_stats | Get a player's current-season stats (matches played, minutes, goals, assists, cards). Assists are inferred from goal-event data and may undercount. Also includes 'advanced_stats' — a curated set of season stats (rating, accurate_passes_percentage, key_passes, shots_total, shots_on_target, big_chances_created, tackles, interceptions, duels_won, successful_dribbles, fouls, saves, goals_conceded), or null when not yet migrated for this player+competition, or null when the player has current-season advanced stats in more than one competition (e.g. league + cup) and no 'league_name' was given — in that case, 'advanced_stats_available_leagues' lists which competitions have data, so you can retry with 'league_name'. Any individual advanced_stats field can itself be null if the underlying data feed never recorded it — never assume 0. Requires 'player_name'; optional 'league_name' to narrow scope. |
| player.status | Get a player's suspension risk from accumulated yellow cards this season (4 = risk, 5 = automatic suspension per Chilean football rules; no reset-after-serving logic), scoped to a single competition — suspension accumulation is per-competition, never blended. If the player has current-season appearances in more than one competition (league, cup, etc.) and no 'league_name' is given, returns a 'competition_required' error listing 'available_competitions' instead of guessing which one; retry with 'league_name' set to one of those. Requires 'player_name'. |
| player.top_scorers | Get a league's current-season top scorer ranking: rank, player (id + name), team (id + name), and goal count, sorted descending. Requires 'league_name'; optional 'top_n' (default 10). |
| player.stat_leaders | Get a season player ranking by a single stat: 'goals', 'assists', 'rating', 'accurate_passes_percentage', 'duels_won', or 'tackles'. Requires 'stat'. Optional 'league_name' (defaults to the server's default league); two independent, combinable position filters -- 'position_group' (coarse: 'GK'/'DEF'/'MID'/'FWD') and 'position' (exact: e.g. 'Left Back', 'Central Midfield' -- use this one for a specific role like 'left-backs' or 'wingers', not just position_group's broad DEF/MID/FWD split); use either alone or both together (AND). Both have real, partial coverage -- a filtered ranking may come back shorter than top_n or empty, which reflects real data coverage, not a missing feature, never fill the gap with a guess. Also 'seasons_back' (0=current season, up to 2 seasons back), 'top_n' (default 10, max 10). 'rating' and 'accurate_passes_percentage' exclude players under 450 minutes played that season and return an 'insufficient_sample' error if nobody clears that floor; the other stats have no floor. Every row includes 'matches_played' and both 'position_group'/'position' regardless of which stat or filter was requested. |
| team.stat_leaders | Get a season team ranking by a single stat: 'possession_percentage', 'successful_passes_percentage', 'shots_total', 'duels_won', 'tackles', or 'big_chances_created'. Requires 'stat'. Optional 'league_name' (defaults to the server's default league), 'seasons_back' (0=current season, up to 2 seasons back), 'top_n' (default 10, max 10). Only counts finished matches. 'possession_percentage' and 'successful_passes_percentage' are per-match averages and exclude teams under 3 finished matches that season (returns 'insufficient_sample' if nobody clears that floor); the other stats are season totals with no floor. |
| player.compare | Compare two players' current-season stats (matches played, minutes, goals, assists, cards — same fields as player.season_stats, one call per player) plus 'insights' — multiple comparative lines (goals, assists, minutes played, and rating when both players have it), including an explicit warning when the two players' minutes played differ by 300+ (their numbers aren't directly comparable at that point). Optional 'league_name' scopes both players' stats to one competition, same as player.season_stats. Requires 'player_a_name' and 'player_b_name'. |
| coach.profile | Get a coach's profile: current team (null if not currently coaching any team on file), tenure start date, previous teams, date_of_birth and nationality (both null until the offline coach profile loader has resolved this coach), 'press_context' — up to 2 press-sourced notes on career trajectory/tactical style from distinct outlets when available (empty list otherwise; populated for coaches who were active when the offline coach press loader last ran — a one-shot, non-refreshing load), and 'external_profile' — full name, place of birth, height (cm), preferred foot, a Transfermarkt profile link, and club career history (always empty for now, Transfermarkt's static profile page doesn't expose transfer history), or null when no confident match was found (resolved by an exact name match against Transfermarkt's coach search; if more than one coach shares that name, narrowed by current club membership; ambiguous matches are skipped, never guessed; only populated once the offline coach profile loader has run for this coach). Requires 'coach_name'. |
| coach.compare | Compare two coaches' win rates over their most recent tenure. Requires 'coach_a_name' and 'coach_b_name'. |
| match.events | Get the full events timeline for one match, ordered by minute: goals (including penalties/own goals), cards, and substitutions, each with the player, minute, event type, and which side (home/away) it belongs to. Requires 'fixture_id'. |
| match.lineups | Get one match's lineups: for each team, 'starting_xi' and 'substitutes', each player with id, name, general position (their usual position, not necessarily what they played specifically in this match), is_captain, rating, and minutes_played. Two real limits, never guessed around: there is no tactical formation (e.g. '4-3-3') anywhere in CFDB — Sportmonks isn't queried for it — and no per-match jersey number (only each player's current squad number exists, via team.squad, which may differ from what they wore in an older match); neither field is in this response at all. Both lists empty means lineup data hasn't been loaded for this match (only recently-finished live-schema matches get lineups fetched), not that the match had no lineup. Requires 'fixture_id'. |
| player.match_stats | Get one player's performance in one specific match (not a season average — use player.season_stats for that): is_starter, minutes_played, is_captain, and a curated set of match stats (rating, passes, accurate_passes, key_passes, shots_total, shots_on_target, tackles, interceptions, duels_won, successful_dribbles, fouls, saves, goals_conceded). Does NOT include goals/assists/yellow_cards for this match — use match.events for those. Any stat field can be null if the underlying data feed didn't record it for that match — never assume 0. Fails with reason 'not_found' if the player has no recorded appearance in that fixture. Requires 'fixture_id' and 'player_id' (get player ids from match.events or team.squad). |
| team.match_stats | Get one team's stat profile in one specific match: a curated set of match stats (possession_percentage, successful_passes_percentage, shots_total, shots_on_target, corners, fouls, tackles, duels_won, interceptions, big_chances_created). This is the team-level counterpart to player.match_stats. Does NOT include goals (use match.matchday_results/team.fixture for the score) or cards (use match.events). Any stat field can be null if the underlying data feed didn't record it for that match — never assume 0. Fails with reason 'not_found' if no stats are recorded for that team in that fixture. Requires 'fixture_id' and 'team_id'. |
| team.compare | Compare two teams' current-season standing, with an interpretive insight (not just numbers side by side). Requires 'team_a_name' and 'team_b_name'. |
| match.report | Get a post-match recap for one finished match: result headline, everything match.dossier returns, plus 'events' — the full goals/cards/substitutions timeline (same shape as match.events, no separate call needed), 'player_ratings' — every player from BOTH teams who has a real match rating on file, already sorted highest-to-lowest, each entry {player_id, name, team_id, team_name, rating, goals, assists} (goals/assists counted from this same match's real events); empty list if no player in the match has a rating loaded yet. Use this one field for every 'destacado'/'figura'/rating-related question about this match instead of calling player.match_stats per-player: for the overall standout, take player_ratings[0]; for one team's standout ('la figura de la U'), filter to that team_id first, then take its first entry — if that filtered list is empty, say that team has no rating data for this match rather than guessing one from the goal timeline, even when the other team's entries exist; for a top-N ranking, take the first N entries (per team or overall, matching what was asked); for 'quién rindió peor', take the last entry (again per team if asked that way). And 'press_context' — up to 2 press-sourced notes (reactions, incidents, why the result happened) from distinct outlets when available (empty list otherwise; only populated within a day of the match once the offline post-match press loader has run), each with 'scraped_at' (when CFDB captured it). Fails with reason 'not_finished' if the match hasn't been played yet. Requires 'fixture_id'. |
| match.storylines | Scan one matchday for narrative angles: win/loss streaks (3+ games), winless streaks (5+ games without a win, includes draws), relegation-zone teams (bottom 2 of the current table — a simple proxy based on this season's table only, NOT Chile's official 3-season rolling-average relegation calculation, which CFDB does not compute), teams ≥10 points off the league leader, and goal-scoring milestones (a player reaching a multiple of 5 season goals — only surfaced for 'previous' matchdays, since future matches have no goals yet). Requires 'when' ('upcoming' or 'previous'); 'league_name' is optional. |
| league.power_rankings | Get CFDB's own team power ranking for a league (CFDB's original methodology: 40% points/game + 35% goal difference/game + 25% recent form). For a competition split into groups (e.g. Copa Chile's group stage), a single cross-group score would be meaningless — teams in different groups haven't played comparable schedules — so the response is a 'groups' dict (each group's own independently-normalized ranking) instead of one flat 'ranking' list; check which key is present. An empty 'ranking' includes an explicit 'reason' ('no_standings_data') and 'message' — never read a bare empty list as evidence the competition doesn't exist. Requires 'league_name'. |
| historical.season_summary | List which competitions/seasons have data loaded, and what kind (standings/matches/top scorers) — blends CFDB's historical records (through 2023) with finished-or-in-progress current-schema seasons (2024+), transparently, as one list. Coverage is partial and growing over time — ALWAYS call this first before historical.matches/historical.standings/historical.top_scorers, and use the exact 'competition_name'/'season_label' strings it returns. Optional 'competition_name' to filter (matches broadly — a substring like 'Copa' lists every matching competition from both schemas rather than erroring). Each season's 'notes' field, when present (historical seasons only), explains the tournament's structure for seasons with multiple stages/zones (e.g. separate regional zones feeding into a final round) — read it before interpreting that season's 'stage' values from historical.matches, since 'stage' is a free-text label with no fixed vocabulary across seasons. |
| historical.matches | Get match results for one competition+season — transparently resolves against CFDB's historical records or, if not found there, the current schema (2024+), so the same call works regardless of era. Use historical.season_summary first to find valid 'competition_name'/'season_label' values. Requires both. Response includes 'season_notes' (structure description for multi-stage historical seasons, always null for current-schema seasons — see historical.season_summary) and each match's 'stage' (tournament phase, null if none), 'matchday' (round number within that stage — always null for current-schema seasons, which don't carry a round number), 'venue' (stadium name, null where not loaded), and 'notes' (free-text per-match commentary, historical only — carries no structural meaning), and 'goals' (list of {player_name, team_name, minute, is_penalty, is_own_goal} for historical matches with goal-by-goal detail loaded — empty list if none was loaded for that match, never guessed; always null for current-schema (2024+) matches — use match.events for those instead). Optional 'limit' caps 'matches' to the first N (default: unlimited — some cup-format seasons have a few hundred matches, e.g. Copa Chile 1974's 368); 'total_matches' and 'truncated' are always present so you can tell the real size either way. |
| historical.standings | Get the final (or current, if the season is still in progress) table for one competition+season — same historical-or-current-schema resolution as historical.matches. Use historical.season_summary first. Requires 'competition_name' and 'season_label'. Current-schema competitions with multiple groups (e.g. a cup's group stage) return an empty table here — top-of-group isn't the same as a single ranked table, so it's left empty rather than shown misleadingly. Historical-schema seasons instead include a 'group_name' field per row (null for a normal single-table season) — some formats (e.g. Chile's 2002-2007 'torneo a la mexicana') split one season into several real, independently-ranked qualification groups; rows are grouped together and never blended into one fake combined ranking, so always check 'group_name' before treating position 1 as the season leader. |
| historical.top_scorers | Get the top-scorer ranking for one competition+season — same historical-or-current-schema resolution as historical.matches. Use historical.season_summary first. Requires 'competition_name' and 'season_label'. Historical rankings are often partial (sometimes just the season's official top scorer, not a full list) depending on what was loaded; current-schema rankings are computed from every recorded goal event, uncapped. |
| team.season_roster | Get a team's squad for one specific season/year — 'team_name' is matched case/accent-insensitively; 'season_label' must still match exactly (e.g. '2000', 'Apertura 2002' — check historical.season_summary for the exact label). Coverage is partial: only the current canonical season is auto-populated (from live squad membership); every other season, historical or canonical, returns an empty roster until it has been loaded by hand. An empty roster means 'not loaded yet', not 'no players'. |