Coverage for mcpgateway/cli.py: 97%

29 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-22 12:51 +0100

1# -*- coding: utf-8 -*- 

2"""mcpgateway CLI ─ a thin wrapper around Uvicorn 

3 

4Copyright 2025 

5SPDX-License-Identifier: Apache-2.0 

6Authors: Mihai Criveti 

7 

8This module is exposed as a **console-script** via: 

9 

10 [project.scripts] 

11 mcpgateway = "mcpgateway.cli:main" 

12 

13so that a user can simply type `mcpgateway …` instead of the longer 

14`uvicorn mcpgateway.main:app …`. 

15 

16Features 

17───────── 

18* Injects the default FastAPI application path (``mcpgateway.main:app``) 

19 when the user doesn't supply one explicitly. 

20* Adds sensible default host/port (127.0.0.1:4444) unless the user passes 

21 ``--host``/``--port`` or overrides them via the environment variables 

22 ``MCG_HOST`` and ``MCG_PORT``. 

23* Forwards *all* remaining arguments verbatim to Uvicorn's own CLI, so 

24 `--reload`, `--workers`, etc. work exactly the same. 

25 

26Typical usage 

27───────────── 

28```console 

29$ mcpgateway --reload # dev server on 127.0.0.1:4444 

30$ mcpgateway --workers 4 # production-style multiprocess 

31$ mcpgateway 127.0.0.1:8000 --reload # explicit host/port keeps defaults out 

32$ mcpgateway mypkg.other:app # run a different ASGI callable 

33``` 

34""" 

35 

36from __future__ import annotations 

37 

38import os 

39import sys 

40from typing import List 

41 

42import uvicorn 

43 

44from mcpgateway import __version__ 

45 

46# --------------------------------------------------------------------------- 

47# Configuration defaults (overridable via environment variables) 

48# --------------------------------------------------------------------------- 

49DEFAULT_APP = "mcpgateway.main:app" # dotted path to FastAPI instance 

50DEFAULT_HOST = os.getenv("MCG_HOST", "127.0.0.1") 

51DEFAULT_PORT = int(os.getenv("MCG_PORT", "4444")) 

52 

53# --------------------------------------------------------------------------- 

54# Helper utilities 

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

56 

57 

58def _needs_app(arg_list: List[str]) -> bool: 

59 """Return *True* when the CLI invocation has *no* positional APP path. 

60 

61 According to Uvicorn's argument grammar, the **first** non-flag token 

62 is taken as the application path. We therefore look at the first 

63 element of *arg_list* (if any) – if it *starts* with a dash it must be 

64 an option, hence the app path is missing and we should inject ours. 

65 

66 Args: 

67 arg_list (List[str]): List of arguments 

68 

69 Returns: 

70 bool: Returns *True* when the CLI invocation has *no* positional APP path 

71 """ 

72 

73 return len(arg_list) == 0 or arg_list[0].startswith("-") 

74 

75 

76def _insert_defaults(raw_args: List[str]) -> List[str]: 

77 """Return a *new* argv with defaults sprinkled in where needed. 

78 

79 Args: 

80 raw_args (List[str]): List of input arguments to cli 

81 

82 Returns: 

83 List[str]: List of arguments 

84 """ 

85 

86 args = list(raw_args) # shallow copy – we'll mutate this 

87 

88 # 1️⃣ Ensure an application path is present. 

89 if _needs_app(args): 

90 args.insert(0, DEFAULT_APP) 

91 

92 # 2️⃣ Supply host/port if neither supplied nor UNIX domain socket. 

93 if "--uds" not in args: 

94 if "--host" not in args and "--http" not in args: 

95 args.extend(["--host", DEFAULT_HOST]) 

96 if "--port" not in args: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true

97 args.extend(["--port", str(DEFAULT_PORT)]) 

98 

99 return args 

100 

101 

102# --------------------------------------------------------------------------- 

103# Public entry-point 

104# --------------------------------------------------------------------------- 

105 

106 

107def main() -> None: # noqa: D401 – imperative mood is fine here 

108 """Entry point for the *mcpgateway* console script (delegates to Uvicorn).""" 

109 

110 # Check for version flag 

111 if "--version" in sys.argv or "-V" in sys.argv: 

112 print(f"mcpgateway {__version__}") 

113 return 

114 

115 # Discard the program name and inspect the rest. 

116 user_args = sys.argv[1:] 

117 uvicorn_argv = _insert_defaults(user_args) 

118 

119 # Uvicorn's `main()` uses sys.argv – patch it in and run. 

120 sys.argv = ["mcpgateway", *uvicorn_argv] 

121 uvicorn.main() 

122 

123 

124if __name__ == "__main__": # pragma: no cover – executed only when run directly 

125 main()