Data-provider MCP server for football: results, stats, odds, standings, form and head-to-head for 18 European leagues (1993-94 →) and 8 cup competitions, for any MCP client. Also a DeepSeek Harness (dsh) plugin.
Six well-named, read-only tools with clear verb-noun patterns (list_*, get_*). Descriptions are detailed and contextual (avg ~180 chars), exceeding the 10 - 1024 baseline. All parameters have type definitions and descriptions. Input schemas are complete and properly structured. However, output schemas are not formally documented in the tool definitions, LLMs must infer response structure from description text alone. Error handling is minimal; no recovery guidance or actionable error messages visible. No tool annotations (readOnlyHint, idempotentHint) despite all tools being read-only and idempotent. Missing per-tool output schema documentation is the primary gap preventing a higher score.
Head-to-head record between two teams. Args: team_a: first team name; loose matching. team_b: second team name; loose matching. competition: league code, e.g. "E0". season: season label, e.g. "2025-26". last: how many recent meetings to return (1-20, default 10). Note: ties decided on penalties count as wins for the shootout winner; `pen_wins_a`/`pen_wins_b` disclose how many wins came that way.
Matches in a competition season, optionally filtered by date range and/or team. Args: competition: league code, e.g. "E0". season: season label, e.g. "2025-26". date_from: optional "YYYY-MM-DD" start (inclusive). date_to: optional "YYYY-MM-DD" end (inclusive). team: optional team name filter; loose matching. include_stats: include shot/corner/foul/card stats (default true). include_odds: include odds (default true). odds_detail: include opening odds and market-avg open (default false). limit: max rows to return (1-100, default 50). Note: fixtures (not-yet-played) carry team names and sometimes odds but no result. Compact by default: one match = one short dict.
League table replayed from played matches (leakage-safe). Args: competition: league code, e.g. "E0". season: season label, e.g. "2025-26". as_of_date: optional "YYYY-MM-DD" cutoff; the table then reflects only matches played up to that date (no future leakage). Note: administrative points deductions are not in the data; tables for deduction seasons can differ from official ones.
Recent form of one team: last N played matches, newest first, + summary. Args: team: team name; loose matching ("Man United" finds "Manchester Utd"). competition: league code, e.g. "E0". season: season label, e.g. "2025-26". last: how many recent matches to return (1-20, default 10). as_of_date: optional cutoff for leakage-safe historical analysis.
Output schemas not formally documented. Tool descriptions explain what is returned, but LLMs cannot parse structured response schemas from prose. Agents must infer field names and types from description text, increasing hallucination risk.
No tool annotations (readOnlyHint, idempotentHint) despite all six tools being read-only and idempotent. Annotations enable clients to optimize caching, retry logic, and safety checks.
Error handling is implicit. No visible error recovery guidance, categorization (retryable vs. user-fixable), or actionable error messages. E.g., invalid competition code or malformed date will likely return a generic exception rather than 'Invalid competition code. Valid codes: E0, E1, D1, ...'
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 66 | 2026-07-28+ | v2 |
List every supported competition as {code: name}. Codes are used in every other tool (e.g. 'E0' = English Premier League).
List teams recorded in a competition season, with per-team match counts. Use this for team-name discovery before filtering by team in get_matches: names here are exactly the names that source rows carry (e.g. 'Man United').
Pagination not visible in get_matches (limit=1-100, default 50). No next_cursor or total_count returned. Large result sets may exhaust context; no guidance on how to iterate through all matches.
Date format validation relies on Python's fromisoformat(). Invalid dates (e.g., '2025-13-01') will raise ValueError. Error message should be: 'Invalid date: got "2025-13-01", must be YYYY-MM-DD with valid month (01-12) and day.'