Coverage for src/usls/adapters/inbound/tools/analytics.py: 70%
20 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-05 10:11 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-05 10:11 +0000
1"""Schedule-strength analytics MCP tools.
3Derived metrics computed on top of the ESPN standings + team-schedule feeds:
4strength of schedule, results-by-opponent-tier, and opponent-quality-adjusted
5points-per-game. These complement the raw league table by exposing
6schedule-strength context the standings alone don't reveal.
7"""
9import logging
11from mcp.server.fastmcp import FastMCP
13from ....application.service import USLSService
14from ....ports.inbound import Authorizer
15from ..formatters import (
16 _fmt_adjusted_ppg,
17 _fmt_results_by_tier,
18 _fmt_strength_of_schedule,
19)
20from ._base import _READ_ANNOTATIONS, _safe_call_authorized
22logger = logging.getLogger(__name__)
25def register_analytics_tools(mcp: FastMCP, service: USLSService, authorizer: Authorizer) -> None:
26 """Register the three schedule-strength analytics tools on `mcp`."""
28 @mcp.tool(annotations=_READ_ANNOTATIONS)
29 async def get_strength_of_schedule(team_id: str) -> str:
30 """Get a team's strength of schedule based on opponents already faced.
32 Returns the average current points-per-game of every opponent the team
33 has played in completed matches, plus a per-opponent breakdown. Useful
34 early in the season for "who has played the tougher schedule so far?"
35 questions.
37 Args:
38 team_id: ESPN numeric team ID (e.g. "18418" for Atlanta United FC).
39 """
40 logger.info("tool=get_strength_of_schedule team_id=%r", team_id)
41 return await _safe_call_authorized(
42 authorizer, "get_strength_of_schedule", service.get_strength_of_schedule(team_id), _fmt_strength_of_schedule
43 )
45 @mcp.tool(annotations=_READ_ANNOTATIONS)
46 async def get_results_by_opponent_tier(team_id: str, tier_size: int = 5) -> str:
47 """Get a team's W-L-T splits against current top-tier, middle, and bottom-tier teams.
49 Tiers are derived from the live league standings: top `tier_size`,
50 bottom `tier_size`, and everyone in between. Lets you ask "how does
51 this team do against the top of the table?" without scanning every
52 result manually.
54 Args:
55 team_id: ESPN numeric team ID.
56 tier_size: Number of teams in each of the top and bottom tiers.
57 Defaults to 5. Must be at least 1, and 2*tier_size must not
58 exceed the league size.
59 """
60 logger.info("tool=get_results_by_opponent_tier team_id=%r tier_size=%r", team_id, tier_size)
61 return await _safe_call_authorized(
62 authorizer,
63 "get_results_by_opponent_tier",
64 service.get_results_by_opponent_tier(team_id, tier_size),
65 _fmt_results_by_tier,
66 )
68 @mcp.tool(annotations=_READ_ANNOTATIONS)
69 async def get_adjusted_points_per_game(team_id: str) -> str:
70 """Get a team's raw points-per-game alongside an opponent-quality-adjusted PPG.
72 Adjusted PPG scales raw PPG by `avg_opponent_ppg / league_average_ppg`,
73 so values above raw PPG indicate the team has earned points against a
74 tougher schedule than league average.
76 Args:
77 team_id: ESPN numeric team ID.
78 """
79 logger.info("tool=get_adjusted_points_per_game team_id=%r", team_id)
80 return await _safe_call_authorized(
81 authorizer, "get_adjusted_points_per_game", service.get_adjusted_points_per_game(team_id), _fmt_adjusted_ppg
82 )