jk-mcp-mls
# jk-mcp-mls
MCP server that gives Claude live access to Major League Soccer data — teams, matches, standings, rosters, and schedule-strength analytics — via the ESPN public API.
[](https://github.com/jedi-knights/jk-mcp-mls/actions/workflows/ci.yml)
[](https://github.com/jedi-knights/jk-mcp-mls/actions/workflows/badge.yml)
[](https://jedi-knights.github.io/jk-mcp-mls/)
[](https://github.com/jedi-knights/jk-mcp-mls/actions/workflows/evals.yml)
[](https://github.com/jedi-knights/jk-mcp-mls/actions/workflows/release.yml)
[](https://www.python.org/downloads/)
[](LICENSE)
---
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Claude Code](#claude-code)
- [Claude Desktop](#claude-desktop)
- [Docker](#docker)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)
---
## Overview
AI assistants like Claude are knowledgeable, but they have a hard cutoff date — they cannot tell you today's MLS standings, last night's scores, or which teams are currently in a playoff position. This project fixes that.
It is an **MCP server** — a plugin that gives Claude direct access to live MLS data: scores, standings, rosters, and derived schedule-strength analytics. Once installed, you can ask Claude natural-language questions about Major League Soccer and get accurate, up-to-date answers. No subscription, no API key, and no programming required to use it.
This is the **v1 scaffold** — it wraps the ESPN public API only. Richer sources (mlssoccer.com's Opta-powered feed, official CMS award articles, Leagues Cup, U.S. Open Cup, Concacaf Champions Cup) are on the roadmap.
---
## Features
The v1 surface is eleven read-only, idempotent tools split across two tiers.
### ESPN-backed (8)
| Tool | Description |
|---|---|
| `get_teams` | List all 30 MLS clubs with IDs and abbreviations |
| `get_team` | Details for a specific team |
| `get_roster` | Team's active roster — jersey, position, age, citizenship |
| `get_scoreboard` | Match scores for a single day, a date range, or the current matchweek |
| `get_team_schedule` | Every match for a team in the current season — past + upcoming |
| `get_match_details` | One match's full details — score, venue, attendance, goals, cards, subs |
| `get_standings` | Current standings **grouped by Eastern and Western Conferences** |
| `get_news` | Recent MLS news articles |
### Derived analytics (3)
Pure functions over live standings + team schedules, exposing schedule-strength context the raw table does not.
| Tool | Description |
|---|---|
| `get_strength_of_schedule` | Team's average opponent points-per-game across matches already played |
| `get_results_by_opponent_tier` | Team's W-L-T split across current top / middle / bottom standings tiers |
| `get_adjusted_points_per_game` | Team's raw PPG alongside an opponent-quality-adjusted PPG |
### Roadmap
Not in v1; probed and shown to be viable at the ESPN API:
- Leagues Cup (`concacaf.leagues.cup`), U.S. Open Cup (`usa.open`), Concacaf Champions Cup, Campeones Cup
- Player leaderboards and team season aggregates once a stable MLS Opta feed is identified
- Award articles via `mlssoccer.com` CMS
- Playoff bracket for the MLS Cup Playoffs
---
## Requirements
- [Python 3.13+](https://www.python.org/downloads/)
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
---
## Installation
```bash
git clone https://github.com/jedi-knights/jk-mcp-mls.git
cd jk-mcp-mls
uv sync
```
---
## Usage
Run the server in stdio mode (the default — used by Claude Code and Claude Desktop):
```bash
uv run python -m mls.server
```
Run in HTTP mode (for networked or deployed access):
```bash
MCP_TRANSPORT=streamable-http uv run python -m mls.server
```
### Example prompts
**Standings, scores, rosters:**
- Who is leading the MLS Eastern Conference right now?
- Show me every MLS result from this past weekend.
- Who is on Atlanta United's roster?
- When does LAFC play next?
**Schedule strength:**
- Which MLS team has played the toughest schedule so far?
- Show me Atlanta United's record against the current top 5 teams.
- Compare Inter Miami and Seattle Sounders on adjusted points-per-game.
---
## Configuration
All configuration is via environment variables. None are required for local use.
| Variable | Default | Description |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | Transport mode: `stdio` or `streamable-http` |
| `HOST` | `0.0.0.0` | Bind address (HTTP transport only) |
| `PORT` | `8000` | TCP port (HTTP transport only) |
| `MCP_PATH` | `/mcp/mls` | URL path (HTTP transport only) |
| `API_HOST` | `https://site.api.espn.com` | ESPN API base URL |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR` |
| `MCP_TRACING_ENABLED` | unset | Bootstrap the OpenTelemetry SDK |
| `MCP_AUTH_ENABLED` | unset | Require RS256 bearer tokens on streamable-http |
| `MCP_AUTH_ISSUER_URL` | unset | Auth-server origin (required when auth is on) |
| `MCP_AUTH_RESOURCE_URL` | unset | This server's public URL for the `aud` claim |
---
## Claude Code
Install from your local clone globally so the server is available in every project:
```bash
claude mcp add --scope user mls -- uv run --directory /path/to/jk-mcp-mls python -m mls.server
```
Replace `/path/to/jk-mcp-mls` with the absolute path to your clone. Verify with `claude mcp list`.
Drop `--scope user` to register only for the current project, or commit a `.mcp.json` to the repo root for collaborators:
```json
{
"mcpServers": {
"mls": {
"command": "uv",
"args": ["run", "--directory", "/path/to/jk-mcp-mls", "python", "-m", "mls.server"]
}
}
}
```
---
## Claude Desktop
Add the following to your Claude Desktop configuration file.
**Location:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"mls": {
"command": "uv",
"args": [
"run",
"--directory", "/path/to/jk-mcp-mls",
"python", "-m", "mls.server"
]
}
}
}
```
If `uv` is not on Claude Desktop's `PATH`, use the absolute path (`which uv` will show it). Fully quit and relaunch Claude Desktop after saving — a window close is not enough.
---
## Docker
Build the image:
```bash
docker build -t jk-mcp-mls:latest .
```
Run in stdio mode (for MCP clients that spawn a subprocess):
```bash
docker run -i --rm jk-mcp-mls:latest
```
Run in HTTP mode:
```bash
docker run --rm -p 8000:8000 \
-e MCP_TRANSPORT=streamable-http \
jk-mcp-mls:latest
```
---
## Development
### Install
```bash
uv sync
```
### Invoke tasks
All common workflows are `invoke` tasks. Run `uv run inv --list` to see everything.
| Task | Alias | Description |
|---|---|---|
| `uv run inv lint` | `inv l` | Run ruff linter and format check |
| `uv run inv lint --fix` | `inv l --fix` | Auto-fix lint violations and reformat |
| `uv run inv test` | `inv t` | Run the full test suite |
| `uv run inv coverage` | `inv v` | Run tests with coverage report (threshold: 90%) |
| `uv run inv check-complexity` | `inv cc` | Check cyclomatic complexity (max 7) |
| `uv run inv build` | `inv b` | Build wheel and sdist into `dist/` |
| `uv run inv build-image` | `inv bi` | Build the Docker image |
| `uv run inv clean` | `inv c` | Remove build and coverage artifacts |
### Project structure
```
src/mls/
├── server.py # entry point, transport selection, logging setup
├── adapters/
│ ├── inbound/
│ │ ├── mcp_adapter.py # FastMCP server, health endpoints, tool registration
│ │ ├── formatters.py # domain → LLM-readable text
│ │ ├── authorization.py # inbound authz port implementations
│ │ └── tools/
│ │ ├── espn.py # 8 ESPN-backed tools
│ │ └── analytics.py # 3 schedule-strength analytics tools
│ └── outbound/
│ ├── espn_adapter.py # ESPN HTTP client
│ ├── parsers.py # ESPN JSON → domain models
│ ├── retry_adapter.py # transient-failure retry decorator
│ └── caching_adapter.py # in-process TTL cache
├── application/
│ ├── service.py # MLSService — use cases, orchestration
│ ├── _helpers.py # input validation
│ └── _analytics_helpers.py # pure math for schedule-strength tools
├── domain/
│ ├── models.py # Team, Match, Standing (with conference), etc.
│ └── exceptions.py # MLSNotFoundError, UpstreamAPIError
├── ports/
│ ├── inbound.py # Authorizer protocol
│ └── outbound.py # MLSAPIPort protocol
├── observability/ # OpenTelemetry bootstrap (opt-in)
└── security/ # JWKS token verifier
```
The dependency direction flows inward: adapters → ports → domain. Nothing in `domain/` imports from adapters or a framework.
---
## Contributing
1. Fork the repository and clone your fork
2. Create a feature branch: `git checkout -b feature/your-feature`
3. Make your changes following the existing patterns (hexagonal architecture, TDD, conventional commits)
4. Verify the full check suite passes: `uv run inv lint && uv run inv check-complexity && uv run inv coverage`
5. Open a pull request against `main`
All CI checks (lint, complexity, tests, coverage ≥ 90%) must pass before merge.
---
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 11 tools
Most tools are clearly distinct (teams, roster, scoreboard, standings, news), but the three team-performance analytics tools (strength_of_schedule, results_by_opponent_tier, adjusted_points_per_game) share a similar shape and could be confused if an agent doesn't read the descriptions carefully.
All tool names follow a consistent verb_noun pattern: 'get_' followed by a specific resource (teams, team, scoreboard, roster, etc.). There is no mixing of conventions or vague verbs.
11 tools is well within the ideal 3-15 range for a domain-specific server. Each tool covers a distinct need, and the count feels appropriately scoped without being excessive or too thin.
The server provides comprehensive read-only coverage for MLS data: team info, rosters, matches (by date and by team), standings, news, and advanced performance metrics. There are no obvious dead ends or missing core operations for a read-only informational server.