sportsdata-mcp
Free & open source (MIT). Live sports data and cross-book betting odds as
MCP tools — ~500 tools across 28 providers, in Claude Desktop, Cursor, or any
MCP client. Zero config: install, run setup, and the full catalogue serves.
An MCP server that exposes sports-data APIs (bookmakers, league/governing-body feeds, aggregators) as tools, configurable so you only load the tool groups you need. A capability-tag system makes tools from different providers interchangeable wherever they answer the same question — so the model can compare odds across bookies or stats across data sources with one discovery call.
The catalogue spans bookmakers, league/governing-body feeds, and stats
aggregators, and it keeps growing. New providers are added by dropping a YAML
spec into src/sportsdata_mcp/specs/ — the engine needs no code changes — so
the exact provider and tool counts move over time. Run sportsdata-mcp list-groups for the live inventory, and three meta-tools (group discovery,
capability lookup, resource listing) are always on regardless of what you
enable.
Install
One-liner (any MCP client config, via uv):
uvx sportsdata-mcp serve # or: pip install sportsdata-mcp
Prebuilt app (no Python needed): grab the latest
release
(macOS + Windows), unzip, and run sportsdata-mcp setup — it writes the config
for Claude Desktop / Cursor for you. The macOS build is unsigned for now:
right-click → Open the first time.
From source:
git clone https://github.com/DanielTomaro13/sportsdata-mcp.git
cd sportsdata-mcp
pip install -e . # add ".[dev]" for the test + lint toolchain
Quickstart
sportsdata-mcp version # print version info
sportsdata-mcp list-groups # see every available tool group
sportsdata-mcp lint # validate the packaged specs
sportsdata-mcp doctor # probe enabled groups for reachability + auth
sportsdata-mcp serve # start the MCP stdio server (default command)
sportsdata-mcp update-specs # OTA-refresh provider specs (signed bundle); --clear reverts
Provider endpoints drift (e.g. Entain rotates its GraphQL persisted-query hashes).
update-specs fetches a signed spec bundle and applies it into an overlay under
~/.sportsdata/spec-overlay, which the loader prefers over the packaged copy — so a drift
fix doesn't need a whole new app build. The bundle is Ed25519-verified against a baked key
(a product build refuses an unsigned/forged bundle; anti-rollback refuses a stale replay).
Publish one with scripts/publish-spec-bundle.py; point --url / $SPORTSDATA_SPEC_FEED_URL
at the asset. Restart the server after applying.
Enable tool groups with a config file or the SPORTSDATA_MCP_GROUPS env var:
SPORTSDATA_MCP_GROUPS="afl.public.core,sportsbet.racing,entain.graphql" sportsdata-mcp serve
See examples/ for Claude Desktop / Claude Code config snippets,
a worked cross-bookie odds-comparison prompt,
and an NBA shot-chart + box-score walkthrough that
shows the nba_stats_call dispatcher pattern end to end.
Configuration
Config is resolved in this order (first hit wins):
--config <path>flag$SPORTSDATA_MCP_CONFIG./sportsdata-mcp.yaml~/.config/sportsdata-mcp/config.yaml- built-in defaults
# sportsdata-mcp.yaml
enabled_groups:
- afl.public.core
- sportsbet.racing
- entain.graphql
providers: # all optional; sensible defaults apply
sportsbet:
request_timeout_seconds: 30
rate_limit_rps: 10 # sustained requests/sec (token bucket)
max_response_bytes: 0 # 0 = no cap (default); set a positive byte count to guard context
secrets: {} # for authenticated providers; prefer env vars in prod
A provider whose auth reads env: SOME_VAR is satisfied by the real environment
variable first, then by a secrets: { SOME_VAR: "..." } entry of the same name
(a local-dev convenience — keep real secrets in the environment in production).
Environment variables
| Variable | Effect |
|---|---|
SPORTSDATA_MCP_GROUPS | Comma-separated group list; overrides enabled_groups. |
SPORTSDATA_MCP_CONFIG | Path to a config file (see resolution order above). |
SPORTSDATA_MCP_MAX_BYTES | Global response-size cap in bytes for every provider that doesn't set its own max_response_bytes. 0 (the default) means no cap. |
SPORTSDATA_LICENSE | Dormant — the product is free; nothing requires a licence. The signed-entitlement machinery remains for anyone self-hosting gated premium feeds (see below). |
SPORTSDATA_ENTITLEMENT_URL / SPORTSDATA_ENTITLEMENT_PUBKEY | Only relevant with the dormant entitlement gate above. Normally unset. |
The (dormant) entitlement gate
This project used to be a paid product. It's free now — no licence exists or is
needed, and every group serves by default — but the signed-entitlement machinery
(Ed25519-verified feed grants, offline caching, 15-min revalidation) is kept dormant
rather than deleted: it's tested, harmless when unset, and useful to anyone
self-hosting this server who wants to gate premium feeds for their own users. Set
SPORTSDATA_LICENSE + SPORTSDATA_ENTITLEMENT_URL against your own issuing service
to activate it; leave them unset (the default) and nothing changes.
Keyed feeds. A few providers need an upstream credential you supply yourself —
e.g. DATAGOLF_KEY for DataGolf, X_BEARER_TOKEN for Twitter/X. Everything else
needs no key at all.
Meta-tools (list_available_groups, list_tools_by_capability, list_resources)
are always registered regardless of what is enabled, so a fresh install can still
guide the model to turn groups on.
On the response-size cap. There is no cap by default — every tool returns
whatever the upstream API sends. If you want to guard the model's context window you
can opt in to a cap: precedence is providers.<id>.max_response_bytes >
SPORTSDATA_MCP_MAX_BYTES > the default (0, unlimited). Be aware that very large
payloads (e.g. Sportsbet's full *_event_markets firehose, ~2 MB) won't fit in
Claude's ~200 K-token context regardless — for those, prefer a narrower tool such as
sportsbet_sports_card with includeTopMarkets: true.
Tool groups
Run sportsdata-mcp list-groups for live counts and descriptions.
AFL — api.afl.com.au
| Group | Tools | Notes |
|---|---|---|
afl.public.core | 22 | Competitions, seasons, rounds, fixtures, ladders, match stats |
afl.public.broadcasting | 9 | Broadcast regions, guides, providers |
afl.public.content | 8 | News/articles, videos, photos |
afl.premium.cfs | 1 | CFS premium ops — needs the anonymous x-media-mis-token |
afl.premium.statspro | 1 | StatsPro ops — needs the x-media-mis-token |
afl.premium.keyserver | 1 | HLS video URL signing |
Sportsbet — sportsbet.com.au
| Group | Tools | Notes |
|---|---|---|
sportsbet.racing | 15 | Race meetings, racecards, results, futures, SRMs |
sportsbet.sports | 14 | Sport events, markets, prices, SGMs |
sportsbet.cross | 12 | Live status, commentary, ladders, promos, video |
sportsbet.results | 2 | Resulted events by date |
sportsbet.graphql | 1 | Persisted GraphQL gateway (apigw/sportsbook/graph) |
Entain / Ladbrokes — ladbrokes.com.au
| Group | Tools | Notes |
|---|---|---|
entain.rest | 13 | Navigation quick-links and REST surfaces |
entain.graphql | 1 | 127 persisted GraphQL ops (gql/router) |
entain.cdn | 1 | Contentful CMS entries (promotions, major-event nav) |
PointsBet — pointsbet.com.au
| Group | Tools | Notes |
|---|---|---|
pointsbet.sports | 10 | Sports catalogue, competition/event feeds, full event markets, in-play, search |
pointsbet.racing | 11 | Meetings, racecards, results, futures, SRMs, tips, form |
pointsbet.content | 3 | Promotions, promo-code splash, + pointsbet_content_call over the static CMS/nav assets |
TAB — tab.com.au
| Group | Tools | Notes |
|---|---|---|
tab.racing | 9 | Dates, meetings, racecards (fixed + parimutuel), form, next-to-go, jackpots, futures |
tab.sports | 9 | Sports/competitions tree, full match markets + SGM, focused match markets, next-to-go, results, multi-builder |
tab.discovery | 4 | Featured/live recommendations + tab_cms_call over the CMS content feeds |
Unibet — unibet.com.au
| Group | Tools | Notes |
|---|---|---|
unibet.racing | 1 | unibet_racing_call — persisted-GraphQL: meetings, race cards, form, futures, specials |
unibet.sport | 3 | unibet_kambi_call over the Kambi offering API (groups, events, bet offers, in-play, bet-builder) + live stats + odds ladder |
BetR — betr.com.au (BlueBet platform)
| Group | Tools | Notes |
|---|---|---|
betr.racing | 8 | Next-to-jump, today's/grouped racecards, race card, form, fluctuations, movers |
betr.sport | 7 | Event types, competition categories, event markets, match detail, popular SGMs |
betr.content | 4 | Promotions + featured racing + popular market links |
Pinnacle — pinnacle.com (sharp odds)
| Group | Tools | Notes |
|---|---|---|
pinnacle.sports | 13 | Sports/leagues, full + highlighted + live + per-league matchups, carousel, matchup detail, straight + parlay markets (American-odds prices) |
pinnacle.reference | 4 | Enums, market-label dictionary, teaser definitions, API status |
Betfair Exchange — betfair.com.au (exchange odds)
| Group | Tools | Notes |
|---|---|---|
betfair.exchange | 3 | bymarket + byevent back/lay price feeds (the sharpest odds) + cash-out availability |
betfair.navigation | 1 | bynode catalogue graph (sport → meeting → event → market) |
betfair.inplay | 5 | Live scores, event details, timeline (single + batch), scores+broadcast |
Dabble — dabble.com.au (iOS app backend)
| Group | Tools | Notes |
|---|---|---|
dabble.sport | 5 | Discover any competition (active list / name lookup / sports), then its fixtures (embedded markets + decimal odds) + the full per-fixture book (400+ markets + Pick'em props) |
The Australian social-betting app's backend, read directly. Reached by posing
as the iOS app — the spec bakes the app's User-Agent + x-device-id +
x-app-version so the public feeds return JSON anonymously. AU-only and
Cloudflare-fronted (403s from non-AU IPs, like the other AU books). Works for
any competition — dabble_active_competitions lists the ~269 currently-bettable
ones across all sports. Read-only odds — no bet placement.
Composes with the other books via sport.event_markets / sport.prices.
SuperCoach — supercoach.com.au (News Corp / Champion Data fantasy)
| Group | Tools | Notes |
|---|---|---|
supercoach.fantasy | 6 | One uniform surface across all 7 games (afl/nrl/epl/nba/nbl/nfl/bbl) × 2 modes (classic + draft): competition state, the full per-player feed (price + ppts1 projection + ownership + matchup; draft adds predraft_rank), fixtures (with H2H odds), club + single-player catalogues, leagues |
News Corp / Champion Data's salary-cap fantasy game. Every feed lives under
/{year}/api/{sport}/classic/v1/… — pass sport (one of the seven) and year
(the season key: current calendar year for afl/nrl, currently 2025 for the
others, which run across the new year). No auth, not geo-blocked (runs in CI).
The core supercoach_players feed is per-round and large (~1–3 MB); use ppts1
(the real projection), not ppts. Adds the fantasy / projections angle via
stats.fantasy_projections alongside Data Golf. See
documentation/SuperCoach.md.
NBL — nbl.com.au (Australian National Basketball League)
| Group | Tools | Notes |
|---|---|---|
nbl.basketball | 14 | Seasons, teams, ladder, schedule (scores), players + rosters, per-player season stats + game-log box scores, team stats, season stat leaders (sortable), and news |
The league's own site data API — a Redis-cached proxy ("rosetta") over Genius
Sports stats at prod.rosetta.nbl.com.au/get/…. No token, but referer-gated
(403s without an nbl.com.au Origin + Referer — both baked into the spec). Every
response is enveloped {type, count, source, data:[…]}. Season-scoped by year
(the season start year: 2025 = NBL26, current); stat-leaders takes the season UUID
from nbl_seasons. Distinct from the SuperCoach nbl fantasy feed — this is the
official box-score source. See documentation/NBL.md.
WTA — wtatennis.com (Women's Tennis Association, official)
| Group | Tools | Notes |
|---|---|---|
wta.tennis | 8 | Official WTA API: singles/doubles rankings, player catalogue + profiles + match history, tournament calendar + per-edition results + entry lists (seeds) |
The WTA's official data API (api.wtatennis.com/tennis/…) — public Spring REST,
no auth/key, no geo-block, runs in CI. Rankings need type+metric
(rankSingles+singles or rankDoubles+doubles); tournaments are keyed by
tournamentGroup.id + year (Australian Open = group 901). Fills the tennis gap on
the stats side, composing with the bookmakers' live tennis markets. See
documentation/WTA.md. (ATP has no equivalent open API —
atptour.com is Cloudflare bot-protected — so it isn't modelled.)
Racing and Sports — racingandsports.com.au
| Group | Tools | Notes |
|---|---|---|
racingandsports.racing | 3 | Today's race meetings (all codes, verified) + sports match list + per-race odds (token) |
Data Golf — datagolf.com (needs a key)
| Group | Tools | Notes |
|---|---|---|
datagolf.general | 3 | Player list, tour schedule, current event field |
datagolf.predictions | 11 | DG rankings, pre-tournament (+ archive) + in-play model probabilities, skill + approach-skill ratings, player/live SG decompositions, live strokes-gained, live hole stats, DFS projections |
datagolf.betting | 3 | Outright + matchup + all-pairings odds across ~13 books (incl. model line) |
datagolf.historical | 9 | Archived raw round data, event-level results (finishes/earnings/points), historical bookmaker odds (outrights + matchups) and DFS results |
Needs a Data Golf API key in the DATAGOLF_KEY env var (a personal subscription
key — sourced via the static_query auth scheme, never stored in the repo).
FanDuel — fanduel.com (US)
| Group | Tools | Notes |
|---|---|---|
fanduel.racing | 4 | fanduel_racing_call (full-query GraphQL: featured/today races + odds, single-race card, tracks, pools, talent picks) + messages/quick-links/promotions |
fanduel.sportsbook | 2 | fanduel_sb_call (REST: event pages + markets, in-play, promos, configs via the _ak key) + live scores |
NRL — mc.championdata.com
| Group | Tools | Notes |
|---|---|---|
nrl.public.core | 4 | Champion Data match centre: competitions, fixture, per-match player stats, app settings |
Plus the nrl://stats/definitions resource (dictionary of every NRL stat code).
NBA — cdn.nba.com + stats.nba.com
| Group | Tools | Notes |
|---|---|---|
nba.public.cdn | 5 | Open CDN JSON: today's scoreboard, full schedule, live box score + play-by-play, odds |
nba.stats | 2 | nba_daily_lineups + nba_stats_call, the dispatcher over the 138-endpoint /stats/ API |
nba_stats_call fronts the whole stats.nba.com /stats/ analytics surface (player/team
dashboards, box scores v2+v3, shot charts, play-by-play, leaders, standings, draft, hustle,
tracking, …). Browse every operation, its required params and its defaults in the
nba://stats/operations resource.
ESPN — espn.com JSON feeds
| Group | Tools | Notes |
|---|---|---|
espn.scores | 5 | Site API convenience endpoints: scoreboard, teams, standings, game summary, news |
espn.site | 1 | espn_site_call — team detail, rosters, schedules, injuries, depth charts, transactions, history, athlete news, groups, rankings (10 ops) |
espn.core | 1 | espn_core_call — the canonical $ref-linked model: events/competitions, odds, win-probability, plays, venues, drafts, coaches, calendar, transactions (37 ops) |
espn.web | 1 | espn_web_call — site-wide search + common/v3 athlete views (7 ops) |
espn.cdn | 1 | espn_cdn_call — the CDN live core feed: scoreboard/game/boxscore/playbyplay (4 ops) |
All ESPN tools are parametric over sport + league slugs (e.g. football/nfl,
basketball/nba, soccer/eng.1), so the five groups cover every league ESPN
carries. Browse each dispatcher's operations in its espn://{site,core,web,cdn}/operations
resource.
OpenF1 — api.openf1.org (Formula 1, no key)
| Group | Tools | Notes |
|---|---|---|
openf1.reference | 3 | Grand Prix weekends (meetings), sessions (the fixtures feed), driver roster |
openf1.results | 5 | Session classification, starting grid, drivers'/constructors' championship standings, overtakes |
openf1.timing | 5 | Per-lap sector + speed-trap timing, pit stops, tyre stints, live gaps/intervals, track position |
openf1.telemetry | 2 | Car telemetry (speed/throttle/brake/gear/RPM/DRS) + (x,y,z) location at ~3.7 Hz |
openf1.live | 3 | Race-control messages (flags/SC/incidents), team-radio clips, weather |
Free, no-auth public REST surface (auth: none). Scope feeds by session_key /
meeting_key (both accept the literal latest) and driver_number; discover keys
with openf1_sessions / openf1_meetings first.
Cricket Australia — cricket.com.au (no key)
| Group | Tools | Notes |
|---|---|---|
cricketaustralia.core | 7 | Fixtures (the /matches feed), competitions, tours/series, teams, player profiles (batch), venue lookup, competition ladder |
cricketaustralia.match | 3 | Full scorecard (innings batting/bowling/wickets), run-graph series, live video streams |
cricketaustralia.content | 2 | Pulselive CMS: video/text/audio/playlist content list + curated playlists |
Two no-auth hosts (apiv2.cricket.com.au/web + the Pulselive CMS). The apiv2
endpoints carry jsconfig=eccn:true by default so they return the documented
camelCase shape; flow is cricketaustralia_fixtures → cricketaustralia_scorecard?fixtureId= →
cricketaustralia_players?playerIds=.
MLB — statsapi.mlb.com (official Stats API, no key)
| Group | Tools | Notes |
|---|---|---|
mlb.reference | 22 | Sports/leagues/divisions/conferences, teams (+ single, affiliates, history, uniforms), rosters, alumni, coaches, personnel, players (profile, batch, search, season catalogue, changes feed), venues, seasons (current + full history) |
mlb.schedule | 5 | Games by date / range / team, plus postseason (schedule, series, tune-in) and tied games |
mlb.game | 10 | Boxscore, linescore, play-by-play, v1.1 feed/live firehose, win-probability, context metrics, content, per-player game line, changes, uniforms |
mlb.stats | 9 | Standings, season stats, one-player stats, league + team leaders, team-season stats, game pace, high/low records |
mlb.extra | 15 | Draft (+ prospects), awards (catalogue + recipients), attendance, transactions, free agents, jobs (umpires/datacasters/scorers), Home Run Derby, All-Star ballots |
mlb.meta | 1 | mlb_meta — the /{type} lookup for every enum (positions, statTypes, gameTypes, pitchCodes, …) |
The official MLB Stats API the MLB-StatsAPI library wraps, read directly (no key) —
comprehensive coverage of the public surface. sportId=1 is MLB; discover ids
with mlb_teams / mlb_schedule / mlb_player_search, then drill into a game or
player. Most tools accept the API's hydrate string to embed related objects in one
call.
Premier League — premierleague.com (no key)
| Group | Tools | Notes |
|---|---|---|
premierleague.core | 7 | Competitions, season structure, awards, the league table, current gameweek, geo |
premierleague.teams | 10 | Teams (+ batch), squads, form (single + all-teams), team stats, next fixture, club metadata |
premierleague.matches | 8 | Fixtures/results feed + match centre: detail, events, lineups, team stats (~200 Opta metrics), officials, commentary |
premierleague.players | 8 | Player directory, profiles (basic/career/season), batch lookup, season + competition stats, metadata |
premierleague.stats | 2 | Player + team stat leaderboards (sort by any Opta metric) |
premierleague.content | 8 | Editorial content/search, latest+popular news/video, broadcasting schedule |
The private JSON APIs that power premierleague.com, read directly (no key,
no cookies) across three hosts (the SDP stats platform, the editorial/
broadcast api.premierleague.com, and static config on resources.premierleague.com).
Underlying data is Opta. Premier League = competition 8; season id is the
starting year (2025 = 2025/26). Flow: pl_teams → pl_matches → a match id →
pl_match/pl_match_stats; pl_standings for the table. Unofficial/undocumented —
respect the ~5 rps rate limit. The SDP wire params (_limit, _sort,
kickoff>/kickoff<) are exposed under clean tool names (limit, sort,
kickoff_after/kickoff_before).
LaLiga — apim.laliga.com (public key shipped)
| Group | Tools | Notes |
|---|---|---|
laliga.core | 6 | Competitions, season instances (subscriptions), league table, rounds/matchweeks |
laliga.teams | 3 | Season team list, single team, club squad |
laliga.players | 3 | Every-player season stats (≈749, full Opta metrics), player profile + stats |
laliga.matches | 2 | Matches feed + single-match detail |
The private JSON API behind laliga.com (Azure APIM), read directly. Underlying
data is Opta. A public Ocp-Apim-Subscription-Key is shipped as a
working default, so it runs out of the box — but the key rotates; override it
with LALIGA_SUBSCRIPTION_KEY (env or secrets:) when reads start 401-ing
(re-harvest from laliga.com's __NEXT_DATA__). A "subscription" is a season
instance (slug laliga-easports-2025 = 2025/26); detail endpoints are keyed by
slug. Pairs with the Premier League provider for cross-league football
comparison via the shared stats.ladder / sport.fixtures_by_date /
stats.player_season tags.
Serie A — api-sdp.legaseriea.it (no auth)
| Group | Tools | Notes |
|---|---|---|
seriea.core | 3 | All competitions, the 41-season catalogue, single-season detail |
seriea.season | 6 | League table (overall/home/away), the 20 teams, every-player + team Opta stats (paginated), all 380 matches, match lineups |
The public SDP JSON API behind legaseriea.it, read directly (no auth).
Underlying data is Opta. The Serie A competition id is baked in, so you only
ever supply a seasonId (discovered from seriea_seasons; seasonName like
2025/2026). Player stats return identity and ~279 Opta metrics in one call
(no squad endpoint), paginated 30/page with category=General|Goalkeeping.
Completes the big-three football leagues alongside Premier League + La Liga via
the shared stats.ladder / sport.fixtures_by_date / stats.player_season tags.
Kalshi — kalshi.com (prediction markets, no key)
| Group | Tools | Notes |
|---|---|---|
kalshi.markets | 6 | Market catalogue + detail, order book, public trades, OHLC candlesticks (single + batch) |
kalshi.events | 9 | Events, series catalogue (by category), single series, milestones, MVE combo collections, entity registry |
kalshi.exchange | 3 | Exchange status, trading schedule, announcements |
The CFTC-regulated US event-contract exchange. Market data is public — no
key required; optionally set KALSHI_API_KEY_ID + KALSHI_PRIVATE_KEY(_PATH)
and every request is RSA-signed for Kalshi's higher authenticated rate limits
(needs pip install "sportsdata-mcp[kalshi-auth]"). Trading surfaces stay out
of scope (read-only provider). Id chain:
kalshi_series_list(category) → kalshi_events → kalshi_markets →
orderbook/trades/candles by ticker. Prices are dollar-denominated.
Polymarket — polymarket.com (prediction markets, no key, geo-gated)
| Group | Tools | Notes |
|---|---|---|
polymarket.gamma | 9 | Markets/events/series/sports/tags catalogue + site search (the discovery plane) |
polymarket.clob | 6 | Order book, best price, midpoint, spread, price history, CLOB catalogue |
polymarket.data | 2 | Public trade tape + top holders |
The largest crypto prediction market. All read endpoints are anonymous —
the wallet keys Polymarket's SDKs use are for order placement only (out of
scope). ⚠️ Geo-gated: Polymarket drops connections at the network edge
from restricted jurisdictions (verified: AU IPs time out on every host) — run
from an unrestricted region or VPN. Flow: polymarket_events → a market's
clobTokenIds → polymarket_book / polymarket_price_history.
X (Twitter) — api.x.com (needs a Bearer token)
| Group | Tools | Notes |
|---|---|---|
twitter.tweets | 7 | 7-day search, volume counts, post lookup (batch + single), quote/repost/like engagement |
twitter.users | 6 | Profile lookup (handle/id, batch), user timelines, mentions |
twitter.trends | 2 | Trends by location (WOEID) + project usage/cap monitor |
The X API v2 read surface — no anonymous tier, so a Bearer token is
required: env X_BEARER_TOKEN first (an operator can ship a deployment-wide
token for all its users), then the config secrets: block (each user their
own). The env var holds the bare token; the spec adds Bearer . Mind your
tier's monthly read cap (twitter_usage); the spec throttles ~0.5 req/s and
never auto-retries 429s. Write/user-context surfaces (posting, DMs, follows)
are out of scope. Flow: twitter_user_by_username("AFL") → id →
twitter_user_tweets; search with X operators ("Storm" lang:en -is:retweet).
Cross-provider comparison
Every tool is tagged with provider-agnostic capability slugs (e.g.
sport.event_markets, racing.race_card). Tools sharing a slug answer the same
question and are directly comparable across providers. The discovery flow:
list_tools_by_capability("sport.event_markets")→ every enabled tool exposing it- Call each provider's tool concurrently with the resolved event ids
- Compare the raw snapshots (schemas are not normalised — the model reconciles them)
See examples/comparator-prompt.md for a full
"compare Storm v Cowboys odds across bookies" walkthrough.
Per-provider notes
- Sportsbet — anonymous public APIs; no secrets needed. REST events are keyed
by integer
eventId; a persisted-GraphQL gateway is exposed viasportsbet_graphql_call(browsesportsbet://graphql/operations). - Entain / Ladbrokes — a persisted-GraphQL gateway; the model supplies an
operation name + variables (discover them in
entain://graphql/operations). Hashes can drift when the front-end bundle ships; refresh them withsportsdata-mcp refresh-hashes entain. - AFL —
afl.public.*is anonymous.afl.premium.*mints an anonymousx-media-mis-tokenautomatically; some premium endpoints still return 401 for anonymous callers. - NRL — the anonymous Champion Data match-centre CDN (
mc.championdata.com), the same static JSON the official nrl.com match centre reads. No secrets, no cache-buster params needed. Resolve acompetitionIdfromnrl_competitions(e.g. 12999 = 2026 NRL Premiership), amatchIdfromnrl_fixture, then pull per-player match stats fromnrl_match; decode stat codes vianrl://stats/definitions. - NBA — two surfaces, no secrets.
cdn.nba.comis wide open (it even serves JSON astext/plain, which the client accepts).stats.nba.comsits behind Akamai, which black-holes any request missing a full browser header bundle — the spec ships that bundle inprovider.default_headers, so it just works. Akamai also rate-limits hard, so the spec'sdefaultsblock throttles NBA to ~1 req/2.5 s, sets a 45 s timeout, and retries transient429/5xxwith exponential backoff (all overridable viaproviders.nba.*). The/stats/family is one dispatcher (nba_stats_call): pick anoperation(the path segment, e.g.leaguedashplayerstats) and passquery_params— each operation already carries NBA's full default param set, so you override only what matters. Most responses are column-oriented (resultSets:[{name, headers, rowSet}]}); v3 box scores are nested. - ESPN — four public hosts, no auth, no API key:
site.api.espn.com(scores, teams, standings, news, summaries),sports.core.api.espn.com(the canonical$ref-linked model — odds, win-probability, plays, venues, drafts, coaches),site.web.api.espn.com(search + athlete views) andcdn.espn.com(the live core feed, needs?xhr=1). Nearly every URL is.../sports/{sport}/{league}/{resource}, so the tools takesport+leagueas parameters and cover every ESPN league parametrically — NFL, NBA, MLB, NHL, college, soccer (eng.1,esp.1, …), golf, racing, tennis, MMA and more. Discovery:espn_scoreboard(sport, league)→ aneventid →espn_game_summaryor the deepespn_core_call(event_*)ops. The spec throttles to ~5 req/s and retries transient429/5xx(overridable viaproviders.espn.*). Note the core API path usesleagues/{league}(plural); core list responses are lazy{count, items:[{$ref}]}envelopes — follow the refs for detail. - PointsBet — anonymous public APIs, no secrets.
api.au.pointsbet.comserves the sportsbook (sports + racing);pointsbet.com.auserves static CMS/nav assets via thepointsbet_content_calldispatcher. Sports discovery:pointsbet_sport_competitions(sportKey)→ a competition key →pointsbet_event(eventKey)for the full market book. Racing:pointsbet_racing_meetings(startDate, endDate)→ araceId→pointsbet_racing_race. Many feeds return a top-level JSON array. - TAB (Tabcorp) — anonymous public data, no secrets.
api.beta.tab.com.ausits behind Akamai (the spec ships a browser header bundle + ~2.5 rps throttle, like NBA);cmsapi.tab.com.auserves CMS feeds viatab_cms_call. Every endpoint needs ajurisdiction(defaults toNSW). The API is HATEOAS and name-based — paths embed sport/competition/match/venue names with spaces (…/AFL Football/competitions/AFL/matches/Adelaide v Geelong), which the HTTP layer percent-encodes; pass raw names. Racing:tab_racing_meetings(date)→raceType+venueMnemonic→tab_racing_race. Sports:tab_sport→tab_competition→tab_matchfor the full market book. - Unibet — anonymous AU data, no secrets, two surfaces. Racing is
persisted-GraphQL (
unibet_racing_call, thegraphql_persisteddispatcher) atrsa.unibet.com.au— race ids areeventKeys like202606040200.T.AUS.hawkesbury.1; the endpoint enforces Apollo CSRF so aContent-Type: application/jsonheader is sent. Sport is the Kambi offering API (unibet_kambi_callover*.kambicdn.com, market AU): group tree, events, bet offers, in-play, bet-builder. Browse ops inunibet://{racing,sport}/operations. - BetR — anonymous AU data, no secrets. BetR runs on the BlueBet platform,
so the API is
web20-api.bluebet.com.au— a flat REST surface covering racing (next-to-jump, grouped racecards, race cards, form, fluctuations) and sport (event types → categories → markets, SGMs). Thebetr.com.auNext.js_next/data/{buildHash}blobs are skipped (fragile per-deploy hash; the API serves the same data). - Racing and Sports —
www.racingandsports.com.auracing/form data, no auth.racingandsports_todays_racing(/todays-racing-json-v2) is the verified feed — today's meetings across thoroughbred/harness/greyhound, by country. The site is behind Cloudflare, which whitelists that feed but JS-challenges the other paths from datacenter IPs (they work from a residential/browser IP); the form/fields/ results are HTML pages, andGetOddsneeds a per-race token, so only the JSON feeds are modelled. - Betfair Exchange — anonymous, the open read-only web APIs keyed by the public
_akquery param. The crown jewel isbetfair_market_prices(ero …/bymarket) — exchange back/lay prices, the sharpest reference odds. Discover market ids by walkingbetfair_navigation(scan …/bynode, e.g.EVENT_TYPE:7= Horse Racing) down to MARKET nodes; live scores/details come from theipsin-play service.string_csvid params take a list. (Theapiedsracing widgets are Cloudflare-gated from datacenter IPs and theappsyncGraphQL needs a session, so they're out of scope — racing is covered via navigation→bymarket.) - Pinnacle — anonymous, no key. The Arcadia "guest" API
(
guest.api.arcadia.pinnacle.com) — the open feed the web sportsbook reads. Sports only (sharp-odds book, no racing); prices are American odds. Flow:pinnacle_sports→pinnacle_sport_matchups(sportId)→pinnacle_matchup_markets(matchupId). The provider sends Pinnacle's public web-clientX-API-Key, which unlocks the full per-sport + per-league matchup lists and the parlay markets. - FanDuel (US) — anonymous US data, no secrets, two surfaces under one provider.
Racing is the first full-query GraphQL provider:
fanduel_racing_callPOSTs the literal query text (thegraphql_querydispatcher kind, sibling to the persisted-hashgraphql_persisted), with boilerplate variables (brand/product/device/profile) baked as per-opdefault_variables— most calls need none, override only what varies ({results: 12},{trackCode, raceNumber}). Sportsbook is REST (fanduel_sb_call) keyed by the static public_akweb key, region NJ. The two halves need differentOriginheaders, so the sportsbook dispatcher overridesOrigin+x-sportsbook-regionover the racing-origin provider default. Browse ops infanduel://{racing,sportsbook}/operations. (US data — composes with other US sources via capability tags.)
CLI reference
| Command | Purpose |
|---|---|
serve | Start the MCP stdio server (default when no subcommand) |
list-groups | Print every group with tool count + description |
lint | Validate specs against the schema + capability catalogue (nonzero on failure) |
doctor | Per-provider reachability + auth + REST-contract probe (nonzero on failure) |
refresh-hashes <provider> | Refresh persisted-query hashes from the live front-end bundle (--dry-run to preview) |
version | Print version info |
-v / --verbose enables DEBUG logging (and un-silences httpx/httpcore).
Contributing
See documentation/ADDING_A_PROVIDER.md for
the full guide, with separate playbooks for adding a bookmaker vs a sports website /
data API. In short, adding a provider is a spec-only change in the common case:
- Write
src/sportsdata_mcp/specs/<provider>.yaml(copy an existing spec). - Tag each tool with capability slugs from
specs/_capabilities.yaml; add a new slug there if none fits (two providers sharing a slug makes them comparable). sportsdata-mcp lint— must pass.sportsdata-mcp doctor(with the new groups enabled) — probes it live.pytest -m "not live"— offline suite; drop the marker filter to run live tests.- Add a row to
tests/contract/test_api_contracts.pyso the new provider's documented response shape is verified live on every PR (see below).
pip install -e ".[dev]"
pytest -m "not live" # offline suite (the CI gate)
pytest -m contract # live response-contract checks (see below)
ruff check .
CI
Every push/PR runs three jobs (.github/workflows/ci.yml):
- test — ruff,
sportsdata-mcp lint, and the offline suite (pytest -m "not live") across Python 3.11–3.13. The deterministic gate. - contract —
pytest -m contract: live response-contract checks that hit each upstream API and assert it still returns the documented shape (top-level keys, and the documented keys on list items). It is resilient by design — it skips on anything outside our control (network errors,5xx,401/403/429, geo-blocks, a missingDATAGOLF_KEY, or an empty feed) and only fails on a genuine shape regression or a broken spec (wrong path/params →4xx). Bookmaker APIs that geo-block GitHub's runners simply skip there. - package — builds the wheel and proves the CLI loads the packaged specs from a clean install.
License
Proprietary and confidential. Copyright (c) 2026 Daniel Tomaro. All rights
reserved. No use, copying, modification, or distribution is permitted without the
owner's prior written consent — see LICENSE.
mcp-name: io.github.DanielTomaro13/sportsdata-mcp