I Built an MCP Server That Helps Claude Write Sports Blogs (Without Making Stuff Up)

May 20, 2026

4 min read

1 views


I Built an MCP Server That Helps Claude Write Sports Blogs (Without Making Stuff Up)

Here's a problem I kept running into: I love sports. I love writing about sports. But after a late playoff game I don't always want to sit down and manually write a full recap from scratch. So I started asking Claude to help — and immediately hit the obvious wall.

Claude would confidently write that "Tatum dropped 34 points on 12-of-22 shooting" when the actual box score told a very different story. The prose was great. The stats were fiction. Not exactly ideal for a sports blog where the whole point is accuracy.

So I built something to fix that: mcp-sports-server, a Model Context Protocol server that gives Claude real, verified ESPN data to work from — and then gets out of the way while Claude does the actual writing.

What It Does

The core idea is simple. Instead of asking Claude to recall stats from training data (which is outdated and unreliable for live games), the server fetches live data from ESPN's public API and hands it to Claude in a structured format. Claude writes the prose. The server handles the data plumbing.

There are five tools:

  • get_todays_games — pulls all games scheduled or in progress for a given league (NBA, NFL, MLB)
  • get_game_summary — full box score, player stats, key plays, and leaders for a specific game
  • generate_blog_draft — the main one: structures ESPN data into a writing brief with a suggested title, frontmatter, and an outline
  • save_draft — writes the finished post to a local drafts/ folder as markdown
  • list_drafts — browse what's already been saved

A typical session looks like this:

You: What NBA games are on tonight?

Claude: [calls get_todays_games]
        Lakers vs Warriors (LIVE Q3), Spurs vs Thunder (Final 118-112)

You: Write a recap of the Spurs game

Claude: [calls generate_blog_draft("401585587", "exciting")]
        [writes full markdown post using returned stats]
        [calls save_draft]

        ✅ Draft saved to drafts/2026-05-20-spurs-defeat-thunder-as-wembanyama-delivers-41.md
        Ready for your review!

The human still reviews before anything goes live. That part is non-negotiable by design.

The Anti-Hallucination Bit

The most important architectural decision was making generate_blog_draft a data-prep tool, not a writing tool. It returns verified stats, a writing outline, and explicit instructions telling Claude to never invent numbers — but it doesn't write a single word of prose. That's Claude's job.

This separation matters because it puts a hard boundary between "facts from ESPN" and "sentences from the model." If a stat isn't in the tool response, it doesn't go in the post.

Under the Hood

The stack is TypeScript on Node.js 20+, using the official @modelcontextprotocol/sdk to handle the MCP protocol over stdio. A few things worth noting:

  • Fastify for the optional HTTP server (health checks, REST endpoints)
  • Zod for schema validation on all ESPN responses — if the API changes shape, it fails loudly rather than silently passing garbage to Claude
  • In-memory TTL cache — scoreboard is cached for 2 minutes, completed game summaries for 10 minutes, live games for 60 seconds
  • Token-bucket rate limiter — stays well within ESPN's unofficial limits (20 req/min with exponential backoff)
  • Vitest for the test suite
  • Docker + docker-compose for containerized deployment, including a nightly scheduler that auto-generates draft placeholders for completed games at 2am

One subtle thing: all logging goes to stderr. The stdout pipe is reserved exclusively for MCP protocol traffic — mixing them would silently corrupt the JSON-RPC stream, which is the kind of bug that's maddening to track down.

What's Next

The architecture is already set up for multi-sport support. Adding NFL or MLB means updating a sport map in the ESPN service, adding sport-specific stat keys to the blog service, and that's about it — the tools themselves don't need to change since they already accept a league parameter.

I'd also like to add a publish_post tool that integrates with a blog platform (probably a static site generator + GitHub Actions pipeline), but that's still on the drawing board. For now, the "always save locally, always review first" model feels right.

--

It's been genuinely fun to use during the playoffs. Ask it for tonight's games, pick one, say "write an exciting recap," and you get a properly-structured markdown draft with real stats in about 15 seconds. Still needs a human edit pass, but it's a solid first draft every time.



mcp
ai
typescript
claude
side-project
Share
Reactions
Comments

Sign in to join the conversation.