SofaScore MCP

SofaScore MCP

An MCP server that answers football questions with interactive widgets, centered on the 2026 FIFA World Cup using SofaScore API data.

Category
访问服务器

README

SofaScore ChatGPT App ⚽🏆

A ChatGPT App (OpenAI Apps SDK / MCP-UI) that answers football questions with rich, interactive widgets — centered on the 2026 FIFA World Cup (48 teams, hosted by the USA, Canada & Mexico) while still handling general football.

It's an MCP server (Streamable HTTP) whose tools each render a React widget inside ChatGPT via the openai/outputTemplate + text/html+skybridge pattern. Data comes from SofaScore's public JSON API.

World Cup hub

<p> <img src="docs/match-detail.png" width="32%" alt="Match detail" /> <img src="docs/knockout-bracket.png" width="32%" alt="Knockout bracket" /> <img src="docs/standings-light.png" width="32%" alt="Standings (light theme)" /> </p>

What it does

Tool What it answers Widget
world_cup Anything about the World Cup — groups, knockout bracket, live/upcoming matches. Views: overview · groups · knockout · matches. Tabbed World Cup hub with a knockout bracket
list_matches "What football is on today?", live scores, a date's fixtures, one competition's matches. Scoreboard grouped by tournament, live pulse
get_match Full detail of one match: score, goal/card timeline, team stats, momentum. Match page with momentum chart & stat bars
get_standings A competition's league table (by name or SofaScore id). Standings table (multi-group aware)
search_football Find a team / player / competition / manager by name. Result list that drills into the other tools

Widgets are theme-aware (light/dark follow window.openai.theme), use SofaScore crest/flag images with an initials fallback, and are interactive — match rows and search results call back into other tools via window.openai.callTool.

Architecture

src/
  server.ts            Express + StreamableHTTPServerTransport (POST /mcp, /health)
  mcp.ts               McpServer factory: server instructions + resources + tools
  resources.ts         registers each built widget as a ui:// text/html+skybridge resource
  sofascore/           data layer: SofaScore client (browser headers, TTL cache,
                       optional proxy, server-side image fetch) + ESPN fallback
                       source, both behind one FootballApi interface (provider.ts,
                       fallback.ts, espn.ts); raw→structuredContent mappers, WC logic
api/index.ts           Vercel serverless entry (re-exports the Express app)
  shared/shapes.ts     structuredContent types shared by server + widgets
  tools/               list_matches · get_match · get_standings · world_cup · search_football
widgets/               React sources (built per-widget to one self-contained HTML)
  shared/              window.openai bridge + hooks, theme, reusable components
scripts/               build-widgets · smoke (wiring) · screenshots (visual)
test/fixtures/         demo SofaScore responses for offline runs

Each widget is built by vite + vite-plugin-singlefile into a single dist/widgets/<name>/index.html (JS + CSS inlined), which is served verbatim as the skybridge resource — no separate asset host needed.

Images (why they route through this server)

Team/tournament/player crests can't be loaded straight from api.sofascore.com inside the ChatGPT Apps sandbox: the iframe's CSP (img-src) blocks the host, and SofaScore itself 403s the referer-less cross-origin request. So the server exposes an image proxy and the widgets point at it instead:

GET /img/team/:id          → https://api.sofascore.com/api/v1/team/:id/image
GET /img/tournament/:id     → …/unique-tournament/:id/image
GET /img/player/:id         → …/player/:id/image
GET /img/flag/:alpha2       → …/img/flags/:alpha2.png

Only these allow-listed shapes are proxied. resources.ts injects window.__SOFA_BASE__ (from PUBLIC_BASE_URL) into each widget's <head>, and widgets/shared/img.ts builds ${__SOFA_BASE__}/img/... URLs from it. If PUBLIC_BASE_URL is unset (e.g. local MCP Inspector) the widgets fall back to loading images directly from SofaScore.

Run it

npm install
npm run dev          # builds widgets, then starts the server on :3000 (POST /mcp)

Point an MCP client at http://localhost:3000/mcp, or run with SOFA_MOCK=1 to serve the bundled fixtures instead of the live API:

SOFA_MOCK=1 npm run dev

Environment variables

Variable Purpose
PORT HTTP port (default 3000).
SOFA_MOCK 1 serves the bundled fixtures instead of the live API — no network needed.
PUBLIC_BASE_URL Public origin this server is reachable at (e.g. https://sofa.example.com). Needed for the image proxy so crests load inside the ChatGPT sandbox. On Vercel it defaults to the deployment URL. Unset elsewhere → widgets load images directly.
SOFA_PROXY Outbound HTTP(S) forward proxy for all SofaScore fetches (data + images). Point at a residential / unblocked proxy to defeat the Varnish/IP block. Falls back to HTTPS_PROXY.
ESPN_ONLY 1 skips SofaScore entirely and serves everything from the ESPN fallback. Handy on hosts where SofaScore is always blocked (avoids the per-request probe + cooldown).
# Live, behind a proxy, with images proxied through this origin:
PUBLIC_BASE_URL=https://sofa.example.com \
SOFA_PROXY=http://user:pass@proxy-host:port \
npm run dev

Add it to ChatGPT (Developer Mode)

  1. Expose the server over HTTPS (e.g. ngrok http 3000).
  2. In ChatGPT → Settings → Connectors → Advanced → Developer mode, add a new connector pointing at https://<your-tunnel>/mcp.
  3. Ask: "Show me the World Cup group standings" or "What football is live right now?" — the matching widget renders inline.

Deploy to Vercel

The repo is Vercel-ready: api/index.ts re-exports the Express app as a single serverless function and vercel.json rewrites every path to it.

vercel deploy        # first run links the project; prod: `vercel --prod`
  • Build — vercel.json's buildCommand runs npm run build, bundling the five widget HTML files into dist/widgets/; includeFiles ships them with the function (the resource loader also resolves them from process.cwd()).
  • Base URL — PUBLIC_BASE_URL defaults to the deployment's VERCEL_URL, so the image proxy works with no extra config. Set it explicitly for a custom domain.
  • Data — ESPN fallback means live data works immediately. To keep SofaScore primary, add SOFA_PROXY in the Vercel project's Environment Variables; to drop it, set ESPN_ONLY=1.
  • Connector URL — point ChatGPT at https://<deployment>/mcp.

The function's maxDuration is 30s to cover ESPN's multi-league fan-out; the landing page at / is served statically from public/.

Verify

All verification runs offline (no live network needed):

npm run typecheck        # server + widgets typecheck
npm run build:widgets    # bundle the 5 self-contained widget HTML files
npm run verify           # build + wiring smoke test + Playwright screenshots
  • scripts/smoke.ts boots the server with fixtures, connects an in-process MCP client, and asserts every tool's openai/outputTemplate resolves to a registered ui:// resource with non-empty structuredContent.
  • scripts/screenshots.ts renders each built widget with the real server output (Chromium, window.openai set exactly like the host) into .artifacts/shots/.

Note on data & this environment

The primary source is the live SofaScore API (api.sofascore.com), used with browser-like headers. Its edge (Varnish) returns 403 to datacenter IPs and flagged TLS fingerprints regardless of headers — so a cloud host, CI runner, or this sandbox often can't reach it. This is an egress-IP block, not a missing header.

So the server falls back to ESPN's public soccer API automatically. Every data source sits behind one FootballApi interface; FallbackApi tries SofaScore first (honouring SOFA_PROXY) and, on any 403 / timeout / empty result, transparently serves the same request from ESPN — mapped into the exact same shapes, so tools and widgets never know the difference. ESPN is reachable from blocked networks, covers the FIFA World Cup + the top-5 European leagues, and needs no API key. Result: live data flows out of the box, no proxy required. Team crests come from a.espncdn.com via the same image proxy.

Ways to control the source:

  1. Default — SofaScore → ESPN fallback. Just works anywhere.
  2. ESPN_ONLY=1 — skip SofaScore entirely (best on always-blocked hosts).
  3. SOFA_PROXY=<url> — give SofaScore a residential / unblocked egress so it stays primary (see below).
  4. SOFA_MOCK=1 — bundled fixtures, fully offline (dev / tests / demos).

The World Cup season resolves dynamically (not hardcoded).

Getting a proxy endpoint

SOFA_PROXY takes a standard http(s)://[user:pass@]host:port URL. What you need is an IP SofaScore hasn't blocked — in practice a residential or mobile proxy (datacenter proxies are usually blocked too). Options:

  • Managed residential proxy providers — Bright Data, Oxylabs, Smartproxy, IPRoyal, Soax, etc. Sign up, create an endpoint, and they give you a http://user:pass@gateway:port URL. Paste it into SOFA_PROXY. Cheapest to start; billed per GB (crest images are tiny, JSON is tiny — usage is low).
  • Your own home/residential IP — run the server on a machine at home, or tunnel through one (e.g. a small box on a home connection running ngrok / tailscale + a forward proxy like tinyproxy or squid). Free if you already have the hardware.
  • A cheap VPS on a residential-friendly ASP — some VPS/edge providers hand out IP ranges SofaScore doesn't block; try a curl from the box first (below).

Test any candidate before wiring it in:

curl -x "http://user:pass@proxy-host:port" \
  -H "User-Agent: Mozilla/5.0" -H "Referer: https://www.sofascore.com/" \
  -o /dev/null -w "%{http_code}\n" \
  https://api.sofascore.com/api/v1/sport/football/events/live

200 → set that URL as SOFA_PROXY. 403 → that IP is blocked too; try another.

Not affiliated with SofaScore; uses their public endpoints for informational use.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选