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

1"""Schedule-strength analytics MCP tools. 

2 

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""" 

8 

9import logging 

10 

11from mcp.server.fastmcp import FastMCP 

12 

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 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25def register_analytics_tools(mcp: FastMCP, service: USLSService, authorizer: Authorizer) -> None: 

26 """Register the three schedule-strength analytics tools on `mcp`.""" 

27 

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. 

31 

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. 

36 

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 ) 

44 

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. 

48 

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. 

53 

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 ) 

67 

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. 

71 

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. 

75 

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 )