DEV Community

Miheve
Miheve

Posted on

Building a Fantasy Football Data MCP Server with Sportmicro API

When I started this project, I wanted a better answer to a familiar problem: how do you give an AI client grounded football data without letting it drift into invented stats, vague abstractions, or feature creep?

The goal of this repository is deliberately narrow. It is a read-only MCP server for fantasy-football development questions, backed by Sportmicro, and it only exposes documented football data. That means the server is meant to help with research, prototyping, and tool integration—not to pretend it can generate fantasy scoring logic or broader league workflows that are not actually supported.

That constraint shaped almost every design choice in the codebase.

I’m linking the code once here so you can inspect the implementation while reading: View the repository.

The problem I wanted to solve

A lot of sports-data demos are easy to build because they quietly assume the data is static and complete. Real football data is messier than that. Even when you only care about documented resources, you still need to handle empty responses, partial responses, request validation, and a clean boundary between upstream provider data and the tool output your client sees.

This project was built around one practical idea: if an MCP client asks for football data, it should get actual Sportmicro data in a predictable shape, not an ad hoc response layered with assumptions.

So the server focuses on a small set of read-only resources:

  • players
  • teams
  • matches
  • players-statistics
  • player-projections
  • player-projections-week
  • player-projections-season

That scope is important because it keeps the integration honest. If the provider documents a football endpoint, the server can expose it. If not, it stays out of the way.

Architecture: a thin MCP layer over a typed Sportmicro client

The architecture is intentionally simple and easy to reason about.

At the entry point, src/index.ts creates an MCP server with the official @modelcontextprotocol/sdk, uses stdio transport, and wires the tool handlers together. The server is named sportmicro-fantasy-football-data-mcp, and it only advertises tool capability.

The data path looks like this:

MCP client
  -> tool handler
  -> Sportmicro client
  -> documented Sportmicro endpoint
  -> normalized tool result
Enter fullscreen mode Exit fullscreen mode

That separation is the main design choice I wanted to preserve:

  • src/config.ts owns required environment access.
  • src/sportmicro/client.ts owns HTTP transport and authentication.
  • src/sportmicro/schemas.ts owns runtime input validation.
  • src/tools/registry.ts maps MCP tool names to handlers.
  • src/tools/resource-format.ts normalizes results into a consistent shape.

This structure matters because the MCP layer stays small. It doesn’t need to know how Sportmicro builds URLs or how request parameters are validated. It just routes a request to the right handler and returns JSON text.

One small but meaningful detail is that the client is typed and injectable. The SportmicroClient accepts an API key, an optional base URL, and an optional fetchImpl. That makes the client easier to test and keeps the production path separate from test setup.

How Sportmicro is integrated

The integration is built around a dedicated SportmicroClient. It defaults to https://football.sportmicro.com, accepts a bearer token, and attaches Accept: application/json on every request.

Here’s the heart of it:

const response = await this.fetchImpl(buildUrl(this.baseUrl, path, query), {
  headers: {
    Authorization: `Bearer ${this.apiKey}`,
    Accept: 'application/json'
  }
});
Enter fullscreen mode Exit fullscreen mode

That’s a good example of the server’s overall posture. The integration is narrow, explicit, and read-only.

A few implementation choices stand out:

1) URLs are built from a base path and query object

The client takes a path plus a query object, then appends any defined query fields to the URL. That keeps the transport layer generic enough for multiple documented Sportmicro football resources without hardcoding one-off request logic per endpoint.

2) Non-OK responses become errors early

If the response is not successful, the client throws immediately with the status code and path. The tests in src/test/sportmicro-client.test.ts show that behavior directly. That is a good boundary because it prevents invalid upstream responses from leaking into tool formatting code.

3) The client returns an item envelope instead of raw JSON

The client normalizes the response into { items, status }. If the upstream body is an array, it becomes items; otherwise the client falls back to an empty array. That choice keeps the tool layer focused on application behavior instead of response-shape repair.

That said, I’d treat this as a constrained normalization strategy, not a universal claim about every Sportmicro response. The repository is careful to support the documented football endpoints it exposes, not every possible provider format.

Tool design: strict inputs, consistent outputs

The tool layer is where the MCP surface becomes concrete.

src/tools/registry.ts defines seven tools and maps each one to a Sportmicro endpoint. Each tool validates input using Zod schemas from src/sportmicro/schemas.ts, calls the client, and then wraps the data with makeToolResult().

That result helper is small but useful:

export function makeToolResult<T>(resource: string, query: Record<string, unknown>, items: T[]) {
  return {
    items,
    meta: {
      resource,
      query,
      count: items.length,
      empty: items.length === 0
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

I like this pattern because every tool response carries the same metadata:

  • the resource name
  • the original query
  • the item count
  • whether the result was empty

That gives MCP consumers a predictable envelope while still preserving the source data itself.

The schemas are also doing real work here. The repository uses Zod to constrain fields like limit, offset, and player_id, and it keeps schemas strict so unknown input keys don’t slip through unnoticed. This is especially helpful in a tool server, where a permissive surface can quickly turn into unclear behavior.

The tests reflect that design. One test checks that a valid player query is passed through correctly and wrapped with metadata. Another confirms that invalid tool input is rejected. That’s exactly the sort of contract I want a server like this to enforce.

Project tree, local setup, and what the repo supports

The project is compact enough to understand at a glance:

src/
  index.ts                MCP server entrypoint
  config.ts               Environment validation
  sportmicro/
    client.ts             Typed Sportmicro football client
    schemas.ts            Runtime validation and request schemas
    types.ts              Shared TypeScript types
  tools/
    registry.ts           MCP tool registration and handlers
    resource-format.ts    Normalized response helpers
  test/
    sportmicro-client.test.ts
    tool-contracts.test.ts
Enter fullscreen mode Exit fullscreen mode

The repository also supports a straightforward local setup. Based on the README and package scripts, the supported workflow is:

  1. Install dependencies with npm install
  2. Set SPORTMICRO_API_KEY
  3. Build with npm run build
  4. Start the MCP server with node dist/index.js

The project expects Node.js 18 or newer and uses stdio transport, so it is meant to be launched by an MCP client rather than served as a traditional web API.

Tests are run with npm test, and the repository indicates they use mocked Sportmicro responses rather than live API calls. That’s a good fit for a contract-focused tool server.

Challenges and trade-offs

I don’t see evidence of a dramatic incident or failure in the repository, so I’d frame the main considerations as design constraints rather than setbacks.

Narrow scope versus broad convenience

The biggest trade-off is obvious: supporting only documented read-only football endpoints keeps the server honest, but it also means the MCP surface is intentionally smaller than what a fantasy app builder might eventually want.

Strict validation versus loose flexibility

The schemas and tool handlers favor explicit input rules over permissive parsing. That can feel strict, but it reduces ambiguity and makes failures easier to reason about.

Normalized wrappers versus raw pass-through

Wrapping results in a consistent meta block improves tool usability, but it also means the server adds a small abstraction layer instead of exposing raw provider responses unchanged. In this project, that trade-off makes sense because the server is meant to be a dependable research interface, not a generic API proxy.

What I’d improve next

If I were continuing the project, the next steps would be practical rather than flashy:

  • Expand documentation around tool examples for each supported endpoint.
  • Add more contract tests for edge cases around empty results and optional filters.
  • Consider whether the tool result format should preserve more upstream status detail where it is useful to clients.
  • Add clearer usage guidance for MCP clients that connect over stdio.
  • Keep watching the boundary between documented Sportmicro fields and any future convenience mapping so the server doesn’t overpromise semantics it cannot prove.

Those are future improvements, not missing features. The current repo already does the main job: it exposes a small, typed, read-only football data surface with strong validation.

Takeaway

The most useful thing I learned from building this was that a good MCP data server is less about being clever and more about being disciplined.

If the goal is to help fantasy-football builders prototype against real football data, the safest path is to keep the surface small, validate inputs strictly, preserve the provider boundary, and make empty or partial responses visible instead of guessing.

That approach makes the server easier to trust, easier to test, and easier to extend later without drifting away from the actual Sportmicro data it is supposed to represent.

Top comments (0)