My main motivation for pointing an LLM at NFL data is sports handicapping. When I’m working through a card, I want efficiency numbers, situational splits, and injury reports — in plain English, fast, and correct. That’s what playcall was for, and while I learned a lot building it, it is not the optimal architecture for interfacing your LLM of choice with the nflverse dataset.
Playcall’s approach is natural-language-to-SQL: the model writes queries against the play-by-play parquet, guided by a curated schema prompt and a set of translation rules. Those rules have a significant failure rate even with a frontier model on the other end of the API. My eval suite covers the query shapes I thought to test; the questions I actually ask on a Sunday keep finding new ones, and every new shape is another chance to write wrong SQL.
The better architecture: skip the translation layer you own, and connect a frontier-model harness to an MCP server that returns the data to the harness directly. For NFL stats, use nfl-mcp. pip install nfl-mcp, run nfl-mcp init, and the wizard pulls the nflverse datasets — play-by-play back to 2013, schedules with closing lines, injuries, snap counts, FTN charting, about 35 tables in all — into a local DuckDB file and wires the server into Claude Desktop or Claude Code.
Two things make this setup work:
Run it on a subscription, not an API key. A frontier subscription means token usage isn’t a concern mid-analysis, and you get an actual chat interface for real-time work instead of piping prompts through a script. The frontier models — Opus, GPT-5 — have the best “intuition” for turning an ambiguous question into the right sequence of data fetches, and that intuition is exactly what my hand-written translation rules were a worse version of.
The tool descriptions are the contract. Properly written MCP methods with good natural-language descriptions are what the model reads when it decides which tool answers your question — write them well and you get the highest probability of correct data coming back. nfl-mcp does this well: 21 tools, each scoped in language the model can route on (“PREFERRED for injury report questions…”), with a raw SQL tool explicitly labeled last resort. In playcall, steering the model toward the right query was my prompt-engineering problem. Here it’s baked into the interface.
Opus · GPT-5"] end S["nfl-mcp server
21 tools · NL descriptions"] DB[("DuckDB
nflverse 2013–2025")] NV["nflverse releases
GitHub"] U <-->|"plain English"| LLM LLM <-->|"MCP tool calls"| S S <-->|SQL| DB S -.->|"in-season auto-update poll"| NV classDef user fill:#fef3c7,stroke:#f59e0b,color:#78350f classDef llm fill:#dcfce7,stroke:#10b981,color:#14532d classDef srv fill:#f3e8ff,stroke:#a855f7,color:#581c87 classDef data fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a class U user class LLM llm class Harness,S srv class DB,NV data
A worked example: Super Bowl LX Link to heading
Seattle closed -4.5 against New England, total 45.5. Here’s the question I’d type into the harness the week of the game:
SEA -4.5 vs NE, total 45.5. Pull both teams’ EPA per play and rank them league-wide, both sides of the ball, plus third-down and pressure numbers. Strength on strength — where’s the edge, and is the under live?
Behind that one question the harness makes a handful of tool calls — nfl_schedule for the line, nfl_team_stats for each team, a ranking query over the play-by-play — and composes an answer out of the results:
- New England’s offense was the real thing: 0.158 EPA/play in the 2025 regular season, No. 1 in the league.
- Seattle’s defense was its mirror: -0.117 EPA/play allowed, No. 2 behind only Houston, with 56 sacks, 21 interceptions, and a 69% third-down stop rate on the season.
- The other matchup was nobody’s edge: Seattle’s offense ranked 13th, New England’s defense 11th.
- The read: the league’s best offense against a top-two defense, with average units everywhere else. Lean under, and the side comes down to which elite unit you trust in February.
Final score: Seattle 29, New England 13. The under hit by three and a half; Seattle covered by eleven and a half. And because the database goes back to 2013, one follow-up call pulls the precedent: the last time Seattle’s defense drew the league’s No. 1 offense in a Super Bowl — XLVIII, against Denver — Denver closed -1.5 and lost 43–8.
Yes, it’s easy to handicap a game that already happened. The point is the workflow, not the pick — one plain-English question, a few tool calls, and an answer with real numbers under it. Every figure in this section came out of the MCP server while I was writing the post, through exactly the setup in the diagram.
Keeping the data fresh in-season Link to heading
One gap in the upstream project: the data is static after init, but nflverse rewrites the current season’s files as games finish. I forked the repo to add an update option so new data is polled from the nflverse releases automatically while the nfl-mcp server is running — nfl-mcp update --watch for scheduled refreshes, or serve --auto-update to hot-swap the current season under a live server. A poll is one small request against the release API; data only downloads when nflverse’s timestamp actually moves. It lives at features/in-season-auto-update while it waits to be merged into the official repo.
That’s the architecture I’d recommend now: the data stays nflverse’s, the SQL becomes the model’s problem instead of yours, and the interface is a chat window you already pay for. The data still won’t pick winners. It just makes sure the number you’re arguing with is real.