Skip to main content
Glama
Snedz

Ask the match

by Snedz

Mission Winning — explainable match intelligence (synthetic data)

The reason is computed, not written. Every number that reaches the screen is re-derived from the match state before it is shown, and when a claim fails, the rejection and the recovery are on screen rather than in a log.

Entry for the Microsoft × Premier League Inside the Game hackathon, built by Mission Winning. Every club, player and event in the football pipeline is synthetic — win chance here is match intelligence, not a price or a wager.

Live demo: https://snedz.github.io/match-intelligence/ — the static build, no server, no key, no account. It carries the 1:18 demo video and the 14-page deck with it, and it is the same build the video shows.

Run it (no keys, no cloud)

pip install -r requirements.txt
python -m itg.web

Requires Python 3.9 or newer (verified on 3.9.6, 3.12.14 and 3.14.8). Open http://127.0.0.1:8000 . The page replays a seeded synthetic match on a pitch at 30× (play, pause, scrub). Overlay cards and the win-chance ribbon read the pipeline JSON. Fan/Analyst, EN/ES/FR, and the favourite-player list re-render the story with the offline templates.

Run the tests with the same install:

python -m pytest -q     # green, offline, no keys

Ten of the tests are the Microsoft Agent Framework graph and are skipped unless that package is installed; the extra install below turns them on. The exact count is the badge on the repository — it moves every time a test is added, and a number in a document that nobody keeps in step is a number nobody should quote.

The same suite runs on every push, on Python 3.9, 3.12 and 3.14 — .github/workflows/tests.yml. That matrix is exactly the set of interpreters it has been verified on by hand (the same result on each), so a green run is a fact rather than a hope; a version is added only after it has been run locally. tests/test_requirements.py pins the install contract: every third-party import in this repository has to be declared in requirements.txt, so the two commands above cannot silently drift apart.

The Microsoft Agent Framework graph orchestrator is a separate install, because it pulls in ~70 packages and the default install should stay fast:

pip install -r requirements-agent-framework.txt

Everything runs either way. Without the package, itg/workflow.py reports itself unavailable rather than pretending, the dependency-free pipeline is the orchestrator, and the trace records which one ran.

QA recovery demo: http://127.0.0.1:8000/?inject=1 or the "Inject a fabricated claim" switch. The same flag is GET /api/trace?seed=7&inject=1. QA rejects the claim, Story falls back to the offline template, and QA accepts that recap. The trace lists Data → Analysis → WinProb → Story → QA with timings.

python -m itg.pipeline --seed 7 --persona fan --language en
python -m itg.pipeline --seed 7 --persona analyst --language es --favorite Okafor
python -m itg.pipeline fan          # same as persona fan, seed 7, English
python -m pytest -q

--language accepts en, es, or fr (also English / Spanish / French). --persona is fan or analyst. --favorite highlights one player on the team sheet. With no model configured, Story uses offline templates. That path is what the tests run.

The JSON includes overlays. Cards with "type": "win_prob" are the swings, each with a short reason and the claims QA checked against match state.

Related MCP server: Football Atlas

Ask the match — the MCP server (itg/mcp.py)

The same match state, published to any MCP client. This is the integration surface: an agent can ask the match questions and check its own claims.

python -m itg.mcp            # speak MCP on stdio (JSON-RPC 2.0)
python -m itg.mcp --demo     # one scripted exchange, printed
python -m itg.mcp --list-tools

Tool

What it answers

get_match_state

the shared match state: teams, score, events ingested, squad

get_key_moments

the key moments, each with its reason and its recomputable claims

get_win_probability

the win chance at a minute, and the factual cause of the swing

verify_claim

the guardrail, exposed — is this sentence about the match true?

verify_overlay_claims

recompute every number on every on-screen card from match state

get_agent_trace

which agent ran, in which order, who handed off, what was rejected

get_player_profile

one player: team, position, distance, top speed, carries, xT

verify_claim is the one to try first. Given a fabricated sentence it returns accepted: false and the reasons:

{
  "accepted": false,
  "issues": [
    "high-xG count 14 exceeds match state (4)",
    "pressure count 223 exceeds match state (216)"
  ],
  "how": "the QA agent recomputed the claim from match state, it did not read it and judge it"
}

It is hand-rolled JSON-RPC rather than the mcp SDK on purpose: the install path a judge takes is pip install -r requirements.txt, and a new dependency there is a new way for the entry to fail before it is seen. It adds nothing to the requirements, and tests/test_mcp.py asserts that it does not.

In a container

docker build -t mission-winning .
docker run --rm -p 8000:8000 mission-winning          # the web app
docker run --rm -i mission-winning python -m itg.mcp  # the MCP server

docs/AZURE.md has the Azure path for the same image — Container Apps for the app and the MCP server, Foundry with managed identity — with the exact az commands. It is not deployed on Azure: there is no subscription behind this entry, and that document says so in its first paragraph. The Dockerfile has not been built in this environment either, because Docker is not installed here; tests/test_deployment.py checks what it can (every path it copies exists, it copies no .env, it runs as a non-root user, its entrypoint is a real module).

Architecture

flowchart LR
  G["Generator<br/>seeded possession-chain<br/>live event stream"] --> D["Data agent<br/>events → shared MatchState"]
  D --> A["Analysis agent<br/>xG · pass difficulty · pressure surges<br/>momentum · synthetic xT"]
  A --> W["WinProb agent<br/>Poisson win chance<br/>+ a factual 'why it moved'"]
  W --> S["Story agent<br/>persona × language × favourite player"]
  S --> Q["QA agent<br/>fact-check score, reasons,<br/>distances vs MatchState"]
  Q -- "reject → grounded template" --> S
  Q --> O["Overlay JSON (timestamped)<br/>+ web UI"]
  D --> X["MCP server<br/>the same state,<br/>for any agent or client"]
  Q --> X
  M["Model chain<br/>Foundry (not wired — awaits Azure)<br/>→ OpenAI-compatible (Ollama)<br/>→ offline templates (default)"] -.-> S

Every agent reads and writes the same MatchState, so the QA agent checks a claim against the state the Story agent wrote from. One shared state, not five private copies.

The model chain always terminates at the offline templates, which is why the demo runs with no key, no account and no network. A model call that fails falls back to the grounded template; it never invents a value to cover the failure.

The pipeline

Stage

Agent

File

What it adds

Ingest

Data agent

itg/agents/data_agent.py

Streaming events into shared match state

Interpret + Explain

Analysis agent

itg/agents/analysis_agent.py

xG, pressure windows, momentum, pass difficulty

Interpret

Win-chance agent

itg/winprob.py

Live home/draw/away chance and why it moved

Render + Personalize

Story agent

itg/agents/story_agent.py

Fan vs analyst, EN/ES/FR, favourite player

Guardrail

QA agent

itg/agents/qa_agent.py

Score, reasons, distances, and wagering language

itg/pipeline.py is the orchestrator. If QA rejects a recap, Story is called again on the offline template only. python -m itg.web serves that JSON to the replay page (itg/web/).

itg/generator.py is a seeded possession-chain simulator. Shots get a toy xG from distance to goal. Carries, a synthetic expected-threat value, per-player speed and distance, and match metadata are added without changing that shot model, so the calibration below still applies. On-ball events also carry a SPADL-shaped descriptor; see NOTICE. Clubs play in the fictional Harbor Invitational.

Model backends (itg/llm.py)

  1. Microsoft Foundry via Microsoft Agent Framework — set FOUNDRY_PROJECT_ENDPOINT, run az login. Not wired to a project yet; that waits on an Azure account.

  2. Any OpenAI-compatible endpoint (for example local Ollama) — LLM_BASE_URL.

  3. Offline templates — the default. Always works with no keys. Tests use this mode.

If a model call fails, the agent falls back to the grounded template. Copy .env.example → .env. .env is git-ignored. Never commit keys.

License: MIT.

Win-chance agent (itg/winprob.py)

Live home/draw/away chances that explain their own swings ("Harbor City FC win chance 39%→8%, tilted to Northgate United by a goal by Vidal; 1 high-xG chance under sustained press (10 pressures)"). Poisson model on remaining minutes; rate = blend of pre-match strength and observed xG. Fitted on 300 synthetic matches, tested on 300 held-out synthetic matches — each scored at seven time points, so 2,100 scored predictions (python3 -m itg.winprob backtest):

Model

Brier (3-way)

Log-loss

Win-chance agent

0.350

0.599

Score-only baseline

0.390

0.673

Reliability table in predictions/winprob-backtest.json (predicted vs actual within 7 points per bin; the largest gap is 6.8 points, in the 0.7–0.8 bin. The per-bin counts are scored predictions, not matches.) A 40-match smoke check in the tests must stay under a Brier of 0.40.

The full method, the reliability table bin by bin, and what this measurement does and does not establish are in docs/CALIBRATION.md. tests/test_deployment.py asserts that the numbers quoted there still match predictions/winprob-backtest.json, so the document cannot drift away from the artifact it was measured from.

What is where

itg/pipeline.py

the orchestrator: ingest → interpret → explain → render → personalize, with the QA retry

itg/mcp.py

the MCP server over the same match state

itg/agents/

Data, Analysis, Story and QA agents

itg/winprob.py

the win-chance model and its backtest

itg/web/

the replay page and the JSON API

docs/CALIBRATION.md

how the win-chance number was measured

docs/AZURE.md

the Azure path, and what is not deployed

SUBMISSION.md, DECK.md

the pitch and the deck

tools/md2pdf.py

the deck generator, so the PDF is reproducible rather than a hand-written file

.github/copilot-instructions.md

how to work in this repository — for a human contributor, and for an agentic editor

inside-the-game-deck.manifest.json

the hash of the deck the PDF was built from; a test fails if the PDF goes stale

inside-the-game-demo.mp4

the 1:14 demo video (silent, 1600×900)

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables MCP clients to access live football data from FotMob, including match stats, team form, injuries, and player workload, without making predictions.
    10
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides access to a cited knowledge graph on football's Laws of the Game and major competitions, with tools for search, question answering, and entity exploration, returning claims with sources and confidence.
    CC BY-4.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides live football data from API-Football over HTTP, enabling queries for upcoming matches, recent results, current standings, injuries, live matches, and team search through MCP tools.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to fetch live football odds and team form, and flag matches where the market price diverges from a simple form-based estimate.
    Eclipse Public 2.0