Coverage for src/ecnl/adapters/inbound/mcp_adapter.py: 91%

35 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-28 08:19 +0000

1"""Inbound adapter — exposes the application service as MCP tools. 

2 

3Composition root for the MCP layer: 

4- Creates the FastMCP server instance 

5- Registers liveness/readiness/health endpoints 

6- Delegates per-domain tool registration to the modules in ``tools/`` 

7 

8FastMCP generates tool schemas from Python type hints and docstrings, so the 

9docstrings on each registered tool are the LLM's primary guide. 

10 

11Logging note (STDIO transport): NEVER use print() here. It writes to stdout 

12and corrupts the JSON-RPC stream. 

13""" 

14 

15import logging 

16 

17from mcp.server.fastmcp import FastMCP 

18from starlette.requests import Request 

19from starlette.responses import JSONResponse 

20 

21from ...application.service import ECNLService 

22from ...ports.inbound import Authorizer 

23from .authorization import PassThroughAuthorizer 

24from .tools._base import _safe_call as _safe_call_internal 

25from .tools.analytics import register_analytics_tools 

26from .tools.events import register_event_tools 

27from .tools.matches import register_match_tools 

28from .tools.schedule import register_schedule_tools 

29from .tools.standings import register_standings_tools 

30from .tools.teams import register_team_tools 

31 

32logger = logging.getLogger(__name__) 

33 

34# Re-exported for tests that import `_safe_call` from this module. 

35_safe_call = _safe_call_internal 

36 

37 

38async def _handle_livez(request: Request) -> JSONResponse: 

39 """Liveness probe — returns 200 OK if the HTTP server is up.""" 

40 return JSONResponse({"status": "ok"}) 

41 

42 

43async def _handle_readyz(request: Request) -> JSONResponse: 

44 """Readiness probe — returns 200 when the server is ready to serve traffic.""" 

45 return JSONResponse({"status": "ok"}) 

46 

47 

48async def _handle_health(request: Request) -> JSONResponse: 

49 """Aggregate health endpoint for monitoring systems.""" 

50 return JSONResponse({"status": "ok", "checks": {"liveness": "ok", "readiness": "ok"}}) 

51 

52 

53def create_mcp_server( 

54 service: ECNLService, 

55 host: str = "0.0.0.0", 

56 port: int = 8000, 

57 path: str = "/mcp", 

58 auth_settings=None, 

59 token_verifier=None, 

60 authorizer: Authorizer | None = None, 

61) -> FastMCP: 

62 """Wire the application service into a FastMCP instance and register tools. 

63 

64 Args: 

65 service: The ECNLService to expose as MCP tools. 

66 host: Bind address for HTTP transport (ignored for stdio). Defaults to 0.0.0.0. 

67 port: TCP port for HTTP transport (ignored for stdio). Defaults to 8000. 

68 path: URL path the streamable-http transport is served at (ignored for 

69 stdio). Each MCP server uses a distinct path (e.g. ``/mcp/ecnl``). 

70 auth_settings: Optional ``mcp.server.auth.settings.AuthSettings`` that 

71 enables bearer-token enforcement on the streamable-http transport. 

72 token_verifier: Optional ``TokenVerifier`` consulted on every request. 

73 """ 

74 mcp = FastMCP( 

75 "ecnl", 

76 host=host, 

77 port=port, 

78 stateless_http=True, 

79 streamable_http_path=path, 

80 auth=auth_settings, 

81 token_verifier=token_verifier, 

82 ) 

83 

84 mcp.custom_route("/livez", methods=["GET"])(_handle_livez) 

85 mcp.custom_route("/readyz", methods=["GET"])(_handle_readyz) 

86 mcp.custom_route("/health", methods=["GET"])(_handle_health) 

87 

88 authz = authorizer or PassThroughAuthorizer() 

89 register_event_tools(mcp, service, authz) 

90 register_standings_tools(mcp, service, authz) 

91 register_schedule_tools(mcp, service, authz) 

92 register_team_tools(mcp, service, authz) 

93 register_match_tools(mcp, service, authz) 

94 register_analytics_tools(mcp, service, authz) 

95 

96 return mcp