vsports-mcp
Hosted MCP server · VSports Extended API

Football data your agent can call. No install.

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.

https://vsportsmcp.mxm.pt/mcp Live
Auth header  x-vsports-token: <your token>
Any MCP client Token per user MIT Self-hostable
connected to vsports
> 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/…
Getting connected

Two commands and a question.

Every caller brings their own token, so there is nothing to configure on our side and no server to run on yours.

One limitation, stated plainly: the token travels in a request header, so this works in every client where you control headers — Claude Code, Cursor, Windsurf, your own code. It does not yet work as a claude.ai custom connector, because those authenticate over OAuth and have no field for a header. OAuth support is the next thing on this server’s roadmap.

Get a token

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.

Add it to your client

One line in Claude Code, or the same URL and header in any editor that speaks MCP over Streamable HTTP.

shell
$ claude mcp add --transport http vsports     https://vsportsmcp.mxm.pt/mcp     --header "x-vsports-token: <your token>"
cursor / windsurf — mcp.json
{
  "mcpServers": {
    "vsports": {
      "url": "https://vsportsmcp.mxm.pt/mcp",
      "headers": {
        "x-vsports-token": "<your token>"
      }
    }
  }
}

Ask it something

prompts that work on day one
“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.

What changes

No IDs to look up. No date ranges to guess.

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.

find_team("sporting")
{
  "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
}
Ranked, not guessed

Ambiguity comes back as ambiguity

“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.

Honest about coverage

A search that stopped early says so

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.

Season & video

A whole season in one call. Every clip a club has.

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.

306

Matches in one request

A full Liga Betclic season — played and still to come — without paging, chunking or a date range.

Finished seasons

History stays reachable

Last season resolves by id and its clips are still there. Nine of ten sampled matches from 2025/2026 still carry video.

67 clips

A club's goals, collected

Benfica's league goals across all 34 matches of a season, each with minute, scorer and a playable url.

get_team_videos(6, 192, type_codes=["G","PG","P"], max_matches=34)
{
  "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.

For anyone using both VSports APIs

The server hands you the id for the other one.

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 trap

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

The bridge

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.

Surface

Sixteen tools, grouped by the question they answer.

Find your way in
get_api_guide
The API’s quirks, event type codes, the Media API bridge and worked recipes. Needs no token.
find_team
A team’s id from its name — partial names, codes, accents, typos.
list_tournaments
Every tournament with id, name, season, area and dates.
list_teams_in_tournament
A competition’s full team directory.
Matches
get_live_matches
Everything in progress right now, across all competitions. No arguments.
get_tournament_matches
A whole season in one call, finished ones included — and the Media API ids.
get_next_matches
A team’s upcoming fixtures, soonest first.
get_last_matches
Recent results with scores, newest first.
search_matches
A date range, filterable by tournament or team. Long windows auto-chunk.
get_standings
The league table, with a live variant while matches are on.
Video & timeline
get_team_videos
A club’s clips across a season, newest first, filterable by event type.
get_match_videos
Every clip of one match, normalised to platform, video_id and a canonical url.
get_match_events
The timeline — goals, cards, substitutions, VAR — with minute, player and team.
get_match
Teams, scores, venue and TV channels.
Reference
get_squad
A full squad — players with photos, positions and bios.
get_venue
Stadium name, city, country and photo.

Time-sensitive tools take fresh=true to bypass the cached copy — built for in-play scores and same-day results.

Under the tools

The parts that stop an agent being confidently wrong.

Wrapping a REST API is easy. What takes the time is everything that would otherwise return a plausible answer instead of an error.

Self-documenting

The server explains itself

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.

Tenancy

Your token never touches disk

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.

No silent fallback

A request without a token is refused

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.

Caching

Per-endpoint TTLs, atomic writes

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.

Resilience

Backoff that respects Retry-After

429s and 5xx retry with exponential backoff; a 4xx raises immediately rather than repeating a request that will fail the same way.

Shaped for models

Empty results keep their shape

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.

Known quirks, handled

Three things that would silently give you wrong answers.

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.

Run it yourself

The hosted endpoint is a convenience, not a lock-in.

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.

Locally, over stdio

Dev mode

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.

  • Claude Code, Claude Desktop, Cursor, Windsurf
  • Token from VSPORTS_EXTENDED_TOKEN or a .env
  • Responses cached under your OS cache directory

On your own infrastructure

Container

Serve MCP at /mcp from your own domain, so your users’ traffic never leaves your estate.

  • Dockerfile and a deployment guide are included
  • Set VSPORTS_ALLOWED_HOSTS to your hostname, or every request gets 421
  • The reverse proxy must forward x-vsports-token and must not buffer responses
  • VSPORTS_STATELESS_HTTP=true when you run more than one instance

The source is not public yet. Ask for repository access along with your token at suporte@vsports.pt.