Football Intelligence MCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Football Intelligence MCPPredict the winner of Barcelona vs Real Madrid"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| 36,982 |
| 476,186 |
| 881,739 |
| 1,436,165 |
| 51,822 |
| 26,996 |
| 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.txtPostgreSQL
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 postgresqlThen restore the database:
createdb football
pg_restore -d football data/football.dump
cp .env.example .env # edit DATABASE_URL if your setup differsVerify the restore:
psql -d football -c "SELECT count(*) FROM fixtures;"
# count
# -------
# 36982If 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.pyThe 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.pyAdding 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:
Spawn the server with
DATABASE_URLset 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, )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 backFetch the catalog and translate it to whatever your LLM's tool-call format expects. For Claude's Messages API,
inputSchemamaps straight toinput_schema— nothing else about the shape changes:send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) tools = recv()["result"]["tools"]Call a tool the same way, matching
idto 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 |
| Handshake. Returns |
| Client notification. No |
| Liveness check. Returns |
| Returns the fifteen tool definitions. |
| 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 |
| string | yes | — |
| 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 |
| string | yes | — |
| 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 |
| 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 |
| integer | yes | — |
| integer | no | 5 |
| string ( | 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 |
| 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 |
| integer | yes | — |
| 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 |
| 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 |
| integer | yes | — |
| integer | yes | — |
| 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 |
| integer | yes |
| integer | yes |
| 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 |
| integer | yes |
| integer | yes |
compare_teams
Recent form for two teams plus their head-to-head record, in one call.
Parameter | Type | Required | Default |
| integer | yes | — |
| integer | yes | — |
| integer | no | 10 |
compare_players
Two players' totals for the same season, side by side.
Parameter | Type | Required |
| integer | yes |
| integer | yes |
| 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 |
| integer | no |
| 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 |
| integer | yes |
| 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 |
| integer | yes | — |
| integer | yes | — |
| 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 ──> PostgreSQLserver.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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Historical football results, teams, competitions and draw/streak statistics via 10 read-only tools.
Football fixtures, standings, and odds intelligence for AI agents.
API-Football MCP — comprehensive soccer/football data
Grounded sports predictions plus European soccer and tennis arbitrage data for AI agents.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides 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-
- FlicenseNot gradedqualityDmaintenanceEnables natural language querying of football/soccer data via the API-Football service, providing access to leagues, teams, players, fixtures, standings, and statistics.3-
- AlicenseNot gradedqualityCmaintenanceAccess football (soccer) data including competitions, matches, standings, and team details via the Football-Data.org API.1 npm2MIT
- AlicenseNot gradedqualityCmaintenanceProvides comprehensive soccer/football data including standings, team search, league search, match predictions, and head-to-head records via the API-Football service.9 npmMIT