One endpoint, your token, sixteen tools — fixtures, live scores, standings, match timelines, squads and video clips for Portuguese and international leagues. Point your agent at it and ask in plain language.
> todos os golos do Benfica na época passada · find_team(name="benfica") → team_id=6 Benfica (SLB) · get_team_videos(6, 192, type_codes=["G","PG"], max_matches=34) → 67 clips · 34/34 matches · complete=true 2026-05-16 7' R. Rios vsports.pt/b24/… 2026-05-16 15' Alexander Bah vsports.pt/b24/… 2026-05-11 46' Rafa Silva vsports.pt/b24/…
Every caller brings their own token, so there is nothing to configure on our side and no server to run on yours.
The Extended API is credentialed per publisher. Request one at suporte@vsports.pt. Each user gets their own, so one can be revoked without touching the rest.
One line in Claude Code, or the same URL and header in any editor that speaks MCP over Streamable HTTP.
$ claude mcp add --transport http vsports https://vsportsmcp.mxm.pt/mcp --header "x-vsports-token: <your token>"
{
"mcpServers": {
"vsports": {
"url": "https://vsportsmcp.mxm.pt/mcp",
"headers": {
"x-vsports-token": "<your token>"
}
}
}
}
“What’s the Liga Betclic table right now?” “Any games live at the moment?” “Who scored in Benfica’s last game, and when?” “Show me every Benfica goal this season.”
Sixteen tools show up under vsports. No SDK, no client library.
A REST client makes you resolve a tournament, then a team, then a window, before you get to the question. The tools collapse that: names go in, ranked matches come out, and the id feeds straight into the next call.
{
"query": "sporting",
"matches": [
{
"team_id": 1,
"name": "Sporting",
"official_name": "Sporting Clube de Portugal",
"code": "SCP",
"score": 1.0
},
{
"team_id": 8,
"name": "Braga",
"official_name": "Sporting Braga",
"score": 0.9
}
],
"complete": true
}
“Sporting” is genuinely two clubs — Braga’s official name is
Sporting Braga, which is why matching reads that field and not just
the display name. You get both, scored, so the agent can pick or
ask. Accents, club codes and typos resolve too:
vitoria finds Vitória SC,
SLB finds Benfica,
chelsae finds Chelsea.
There is no team-search endpoint upstream, so the index is built by
sweeping competitions. Every response carries
complete — the failure worth preventing
is an agent reporting that a club does not exist when it was simply
never searched for.
Date-range search only reaches a recent window, which is why a finished
season looks empty through it. get_tournament_matches
does not have that limit: it returns the entire tournament-season, and a
past tournament id keeps resolving long after it stops appearing at the
top of the directory.
A full Liga Betclic season — played and still to come — without paging, chunking or a date range.
Last season resolves by id and its clips are still there. Nine of ten sampled matches from 2025/2026 still carry video.
Benfica's league goals across all 34 matches of a season, each with minute, scorer and a playable url.
{
"total_clips": 67,
"matches_scanned": 34,
"matches_available": 34,
"complete": true,
"videos": [
{
"date_utc": "2026-05-16", "minute": 7,
"player": "R. Rios", "team": "Benfica",
"type_code": "G", "platform": "vsports",
"url": "https://vsports.pt/b24/embd/golo-benfica-r-rios-7-…"
}
]
}
Returns the club’s own clips by default — “Benfica’s goals” is 67, not the
91 scored in Benfica’s matches. Pass
by_team_only=false for the opposition’s too,
which is what a match report wants.
VSports runs two APIs. This one serves fixtures, timelines and embeds; the Media API serves the same matches as direct mp4 files. If you need a playable file rather than an iframe, you need both — and then you need to know which match over here is which match over there.
The two id spaces overlap numerically and disagree on meaning. A mismatched id does not error — it returns a different club, with HTTP 200.
id Extended Media 6 Benfica Sporting 4 Estoril Braga 7 Marítimo FC Porto
Every match from get_tournament_matches
carries the Media API’s ids for itself and both teams. Published by
the API, not inferred — and absent from the REST reference entirely.
{
"id": 45266,
"provider_id": 30359,
"team_A": "Benfica",
"team_A_id": 6,
"team_A_provider_id": 15
}
Coverage is complete wherever it has been checked: 306/306 for two Liga
seasons, 380/380 Premier League, 234/234 Champions League.
get_api_guide carries the whole thing, so an
agent with nothing but the URL still learns it.
Time-sensitive tools take fresh=true to bypass
the cached copy — built for in-play scores and same-day results.
Wrapping a REST API is easy. What takes the time is everything that would otherwise return a plausible answer instead of an error.
Season-rotating ids, the recent-only date window, event type codes, the Media bridge and worked recipes all come back from get_api_guide — before a token is even configured.
Cache keys carry a SHA-256 digest of the caller’s token, never the token. One client pool and one cache segment per tenant, because tokens carry different licensing entitlements.
The hosted server deliberately ignores its own credentials. A call that arrives without a token fails rather than being quietly billed to somebody else’s quota.
Sixty seconds for live scores, a week for stadiums. Entries are written to a temp file and swapped in, so a killed process cannot leave truncated JSON behind.
429s and 5xx retry with exponential backoff; a 4xx raises immediately rather than repeating a request that will fail the same way.
A team with no upcoming fixture returns an empty list with the same keys as a hit, not a 404 — so a caller never branches on the shape of the response.
182 tests, ruff and mypy clean, on Python 3.10 through 3.13. Ships py.typed.
Tournament ids are reissued every season.
Liga Betclic is 192 in 2025/2026 and
270 in 2026/2027. An id is a
tournament-season, not a competition — hardcode one and you read last
season’s table with no error to tell you.
A corner is recorded against the team that conceded it.
On a CN occurrence the team attached is the
one that gave the corner away, verified against the match statistics.
Counting corners naively inverts the result, so the team filter
corrects for it — and the guide says exactly where the raw field is
left untouched.
A clip’s platform field is not trustworthy.
Portuguese competitions embed VSports’ own player, the English ones
YouTube and Twitter, and a goal can be on one while the highlight reel
is on another. get_match_videos parses the
embed itself and returns the platform, the video id and a canonical
url.
Same server, MIT licensed. The tool bodies are identical whichever way you run it — only how a token is resolved and how clients are pooled differs.
One user per process. The client runs the server as a subprocess and the token comes from the environment — the shortest path if you are building against the data.
VSPORTS_EXTENDED_TOKEN or a .env
Serve MCP at /mcp from your own domain, so
your users’ traffic never leaves your estate.
VSPORTS_ALLOWED_HOSTS to your hostname, or every request gets 421x-vsports-token and must not buffer responsesVSPORTS_STATELESS_HTTP=true when you run more than one instanceThe source is not public yet. Ask for repository access along with your token at suporte@vsports.pt.