Skip to main content
Glama
jjaquezito

Football Intelligence MCP

by jjaquezito

Football Intelligence MCP

A local Model Context Protocol server that gives any MCP host access to a curated historical football database covering 36,982 matches across Europe's seven top competitions from 2010 to 2025.

The protocol is implemented directly over JSON-RPC 2.0, without using any MCP SDK: every message is built and parsed by hand following the 2025-06-18 specification.

What's in the database

Data originally extracted from API-Football, normalized into PostgreSQL.

Competition

League id

Seasons

Premier League

39

2010–2025

La Liga

140

2010–2025

Serie A

135

2010–2025

Bundesliga

78

2010–2025

Ligue 1

61

2010–2025

Primeira Liga

94

2010–2025

UEFA Champions League

2

2011–2025

Table

Rows

fixtures

36,982

fixture_events

476,186

fixture_player_statistics

881,739

lineup_players

1,436,165

fixture_team_statistics

51,822

players

26,996

standings

2,329

Known coverage limits

The server reports these instead of guessing, and so should you:

  • Match statistics start in 2015. Seasons 2010–2014 contain results, goals, events and lineups, but no shots, possession or passing data.

  • Expected goals (xG) only exists from 2023 onward, and never for the Champions League.

  • Formation and coach data starts in 2015.

  • 58 matches (0.16%) have a goal missing from their event list — an upstream gap in API-Football. Scorelines are unaffected; they come from the fixture record, not from summing events.

Call the data_coverage tool to check what exists for any league and season.

Related MCP server: api-football-mcp-server

Installation

Requires Python 3.10+ and PostgreSQL 14+.

git clone https://github.com/jjaquezito/MCP_local.git
cd MCP_local

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

PostgreSQL

You need a running PostgreSQL server before createdb/pg_restore will work — if you don't have one yet:

# macOS
brew install postgresql@14
brew services start postgresql@14        # keeps running across reboots

# Debian/Ubuntu
sudo apt install postgresql
sudo systemctl start postgresql

Then restore the database:

createdb football
pg_restore -d football data/football.dump

cp .env.example .env      # edit DATABASE_URL if your setup differs

Verify the restore:

psql -d football -c "SELECT count(*) FROM fixtures;"
#  count
# -------
#  36982

If createdb/pg_restore/psql fail with something like connection refused or role "postgres" does not exist, PostgreSQL either isn't installed or isn't running — that's almost always the actual problem, not football.dump itself. Go back to the PostgreSQL step above.

Running the server

python server.py

The server speaks JSON-RPC over stdio. It is normally launched by an MCP host rather than by hand, but you can drive it directly:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual","version":"1.0"}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | python server.py

Adding it to your own chatbot

Your host is its own program, so there's no config file shortcut — it has to speak JSON-RPC 2.0 over this process's stdin/stdout directly. The minimum sequence, in any language:

  1. Spawn the server with DATABASE_URL set in its environment, keeping stdin/stdout as pipes:

    proc = subprocess.Popen(
        ["/absolute/path/to/.venv/bin/python", "/absolute/path/to/server.py"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE,
        env={**os.environ, "DATABASE_URL": "postgresql://..."},
        text=True, bufsize=1,
    )
  2. Handshake — one JSON object per line, written to stdin / read from stdout:

    send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
        "protocolVersion": "2025-06-18", "capabilities": {},
        "clientInfo": {"name": "my-host", "version": "1.0"}}})
    recv()  # -> serverInfo + capabilities
    send({"jsonrpc": "2.0", "method": "notifications/initialized"})  # no "id": no reply comes back
  3. Fetch the catalog and translate it to whatever your LLM's tool-call format expects. For Claude's Messages API, inputSchema maps straight to input_schema — nothing else about the shape changes:

    send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
    tools = recv()["result"]["tools"]
  4. Call a tool the same way, matching id to correlate request/response:

    send({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {
        "name": "search_team", "arguments": {"query": "Liverpool"}}})
    result = recv()["result"]  # {"content": [{"type": "text", "text": "..."}], "isError": bool}

That's the entire contract — see Protocol below for the full method and error reference.

Protocol

Transport: stdio, newline-delimited JSON. Protocol version 2025-06-18.

Method

Notes

initialize

Handshake. Returns serverInfo and capabilities.

notifications/initialized

Client notification. No id, no response.

ping

Liveness check. Returns {}.

tools/list

Returns the fifteen tool definitions.

tools/call

Executes a tool.

Errors follow JSON-RPC 2.0: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error.

Tool-level failures are not JSON-RPC errors — they return a normal result with isError: true, so the model can read the message and recover.

stdout carries only JSON-RPC. All diagnostics go to stderr.

Tools

search_team

Finds teams by partial name, ranked by how many matches they have on record. Call this first — every other team tool needs a team_id.

Parameter

Type

Required

Default

query

string

yes

limit

integer

no

10

{"name": "search_team", "arguments": {"query": "Liverpool", "limit": 1}}
[{"team_id": 40, "name": "Liverpool", "country": "England",
  "founded": 1892, "logo": "https://media.api-sports.io/football/teams/40.png",
  "partidos": 762, "primera_temporada": 2010, "ultima_temporada": 2025}]

search_player

Finds players by name, ranked by minutes played.

Parameter

Type

Required

Default

query

string

yes

limit

integer

no

10

get_match

Full match record: score, venue, referee, per-team statistics and every goal with minute and assist. Matches before 2015 return an explicit note instead of empty statistics.

Parameter

Type

Required

fixture_id

integer

yes

get_team_form

Recent form: streak, points, goals for and against.

before restricts the calculation to matches strictly earlier than that date, which lets you reconstruct a team's state at any past moment. This is the guard against look-ahead bias when building predictive features.

Parameter

Type

Required

Default

team_id

integer

yes

last

integer

no

5

before

string (YYYY-MM-DD)

no

{"name": "get_team_form",
 "arguments": {"team_id": 40, "last": 5, "before": "2020-01-01"}}

get_recent_lineup

Formation, coach (with photo) and starting XI of a team's last match that has lineup data, each player including their grid position (row:col, API-Football's own notation for drawing a formation) and their match rating — enough to render an actual pitch diagram, not just a list. This is a reasonable stand-in for "probable lineup" — it is not a confirmation of who will actually start next; injuries and rotation can change it. Formation/coach data only exists from 2015 onward, same as match statistics.

Parameter

Type

Required

team_id

integer

yes

get_team_squad

Every player who appeared in a team's last N matches, with photo and accumulated goals/assists/average rating, plus jugador_mas_determinante (the player with the most goal involvements in that window) called out separately.

Parameter

Type

Required

Default

team_id

integer

yes

last

integer

no

10

get_league_info

A competition's crest and flag. There are no trophy images in the database — API-Football exposes those through a separate endpoint that was never extracted — this is the closest available visual identity for a league.

Parameter

Type

Required

league_id

integer

yes

get_head_to_head

Win/draw/loss balance between two teams, their most recent meetings, and per-team averages (goals per match, yellow/red cards, fouls, corners) across their full history — useful context for deciding a bet, not just a prediction number. Cards/fouls/corners are only available for matches from 2015 onward; matches without statistics are excluded from those averages, not counted as zero. Also returns equipo_a_id/equipo_a_logo and equipo_b_id/ equipo_b_logo alongside the existing equipo_a/equipo_b name fields.

Parameter

Type

Required

Default

team_a

integer

yes

team_b

integer

yes

limit

integer

no

10

get_team_season

Final league position, points and goals, alongside per-match averages for shots, possession, corners and passing accuracy.

Parameter

Type

Required

team_id

integer

yes

league_id

integer

yes

season

integer

yes

Seasons are named by their starting year: 2024 means the 2024-25 season.

get_player_season

Per-season player totals aggregated from match-level records: goals, assists, minutes, shots, key passes, cards and average rating.

Parameter

Type

Required

player_id

integer

yes

season

integer

yes

compare_teams

Recent form for two teams plus their head-to-head record, in one call.

Parameter

Type

Required

Default

team_a

integer

yes

team_b

integer

yes

last

integer

no

10

compare_players

Two players' totals for the same season, side by side.

Parameter

Type

Required

player_a

integer

yes

player_b

integer

yes

season

integer

yes

data_coverage

What actually exists in the database, per league and season. Use it before claiming a data point is missing.

Parameter

Type

Required

league_id

integer

no

season

integer

no

predict_match

Win/draw/loss probability for a match, from a model trained on 23,168 matches (2015–2025, six domestic leagues — Champions League excluded, its knockout format isn't comparable to a round-robin table). Two candidates were compared head to head on a held-out validation season (logistic regression vs. a gradient-boosted trees classifier); the one with the better validation log loss was kept. See model.metricas_prueba_2025 in the tool's own response for the honest, never-touched-during-selection test score.

The match does not need to already exist in the database. Each team's current Elo, recent form and rest days are kept in a team_current_form snapshot, refreshed independently of any single fixture — so this works for a match scheduled for next week just as well as one played five years ago.

Parameter

Type

Required

home_team_id

integer

yes

away_team_id

integer

yes

{"name": "predict_match", "arguments": {"home_team_id": 529, "away_team_id": 531}}
{
  "local": "Barcelona", "local_team_id": 529,
  "local_logo": "https://media.api-sports.io/football/teams/529.png",
  "visitante": "Athletic Club", "visitante_team_id": 531,
  "visitante_logo": "https://media.api-sports.io/football/teams/531.png",
  "probabilidad_local": 0.779, "probabilidad_empate": 0.145, "probabilidad_visitante": 0.076,
  "modelo": "logistic_regression",
  "advertencia": "Probabilidad estadistica basada en historial, no una garantia..."
}

*_logo are direct media.api-sports.io URLs, ready to use as an <img src> with no proxying — a UI client doesn't need a separate lookup to render team crests.

The trained model ships in data/predict_model.joblib (a few KB — a fitted scikit-learn pipeline, not raw weights). Retraining requires the full feature pipeline (conocimiento/ml/), which lives in the private project repository, not here — same relationship as data/football.dump to the extraction pipeline that built it.

predict_scoreline

Complements predict_match with a full scoreline probability grid — the chance of every exact result from 0-0 up to max_goals-max_goals — using a Poisson model over each team's recent goals scored/conceded, plus expected yellow cards and corners for the match and the probability both teams score. This is a different statistical method from predict_match's trained classifier, not the same number recomputed, so a small mismatch between the two (e.g. draw probability) is expected and not a bug.

Parameter

Type

Required

Default

home_team_id

integer

yes

away_team_id

integer

yes

max_goals

integer

no

5

{"name": "predict_scoreline", "arguments": {"home_team_id": 529, "away_team_id": 531}}
{
  "local": "Barcelona", "visitante": "Athletic Club",
  "goles_esperados_local": 1.8, "goles_esperados_visitante": 1.3,
  "marcador_mas_probable": "1-1", "probabilidad_marcador_mas_probable": 10.54,
  "probabilidad_ambos_marcan": 59.8,
  "tarjetas_amarillas_esperadas": 3.5, "corners_esperados": 12.8,
  "matriz_marcadores": [[4.5, 5.9, 3.8, "..."], ["..."]]
}

matriz_marcadores[i][j] is the percentage chance of a i-j final score (home goals first), so matriz_marcadores[1][1] is the chance of exactly 1-1. Cells beyond max_goals in either direction aren't included, so the grid's total is slightly under 100% by design.

Example session

Which team won the Premier League in 2015?

→ search_team {"query": "Leicester"}
→ get_team_season {"team_id": 46, "league_id": 39, "season": 2015}

Leicester City, with 81 points from 23 wins, 12 draws and 3 losses — and only 42.7% average possession, unusual for a champion.

Architecture

MCP host  ──JSON-RPC/stdio──>  server.py  ──>  knowledge/engine.py  ──>  PostgreSQL

server.py owns the protocol and nothing else. All query logic lives in knowledge/engine.py, which returns plain dictionaries and has no knowledge of MCP — so it can be tested, or reused, entirely on its own.

License

MIT. Football data originates from API-Football and is redistributed here for academic use.


Built for CC3067 Redes, Universidad del Valle de Guatemala.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides programmatic access to comprehensive football statistics and live match data via API-Football, enabling applications to retrieve league standings, team fixtures, player statistics, and real-time match events.
    6
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language querying of football/soccer data via the API-Football service, providing access to leagues, teams, players, fixtures, standings, and statistics.
    3
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Access football (soccer) data including competitions, matches, standings, and team details via the Football-Data.org API.
    1 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides comprehensive soccer/football data including standings, team search, league search, match predictions, and head-to-head records via the API-Football service.
    9 npm
    MIT