Coverage for src/usls/adapters/inbound/mcp_adapter.py: 89%

27 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-05 10:11 +0000

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

2 

3This file is the composition root for the MCP layer: 

4- Creates the FastMCP server instance 

5- Registers liveness/readiness/health endpoints 

6- Delegates per-data-source 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 USLSService 

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.espn import register_espn_tools 

27 

28logger = logging.getLogger(__name__) 

29 

30# Re-exported so existing test imports (``from usls.adapters.inbound.mcp_adapter 

31# import _safe_call``) keep working. 

32_safe_call = _safe_call_internal 

33 

34 

35# --------------------------------------------------------------------------- 

36# Health probe handlers 

37# --------------------------------------------------------------------------- 

38 

39 

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

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

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

43 

44 

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

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

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

48 

49 

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

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

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

53 

54 

55# --------------------------------------------------------------------------- 

56# MCP server factory 

57# --------------------------------------------------------------------------- 

58 

59 

60def create_mcp_server( 

61 service: USLSService, 

62 host: str = "0.0.0.0", 

63 port: int = 8000, 

64 path: str = "/mcp", 

65 auth_settings=None, 

66 token_verifier=None, 

67 authorizer: Authorizer | None = None, 

68) -> FastMCP: 

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

70 

71 Args: 

72 service: The USLSService to expose as MCP tools. 

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

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

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

76 stdio). Each MCP server uses a distinct path (e.g. ``/mcp/usls``) so a 

77 single gateway can namespace many servers under ``/mcp/<name>``. 

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

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

80 Pair with ``token_verifier``. When both are None the transport 

81 stays open (local-dev / stdio behaviour preserved). 

82 token_verifier: Optional ``TokenVerifier`` consulted on every inbound 

83 request when ``auth_settings`` is set. The FastMCP runtime turns a 

84 ``None`` return into a 401 response. 

85 """ 

86 mcp = FastMCP( 

87 "usls", 

88 host=host, 

89 port=port, 

90 stateless_http=True, 

91 streamable_http_path=path, 

92 auth=auth_settings, 

93 token_verifier=token_verifier, 

94 ) 

95 

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

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

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

99 

100 authz = authorizer or PassThroughAuthorizer() 

101 register_espn_tools(mcp, service, authz) 

102 register_analytics_tools(mcp, service, authz) 

103 

104 return mcp