Coverage for mcpgateway/cli.py: 97%

29 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-09 11:03 +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 

36# Future 

37from __future__ import annotations 

38 

39# Standard 

40import os 

41import sys 

42from typing import List 

43 

44# Third-Party 

45import uvicorn 

46 

47# First-Party 

48from mcpgateway import __version__ 

49 

50# --------------------------------------------------------------------------- 

51# Configuration defaults (overridable via environment variables) 

52# --------------------------------------------------------------------------- 

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

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

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

56 

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

58# Helper utilities 

59# --------------------------------------------------------------------------- 

60 

61 

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

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

64 

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

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

67 element of *arg_list* (if any) - if it *starts* with a dash it must be 

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

69 

70 Args: 

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

72 

73 Returns: 

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

75 """ 

76 

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

78 

79 

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

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

82 

83 Args: 

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

85 

86 Returns: 

87 List[str]: List of arguments 

88 """ 

89 

90 args = list(raw_args) # shallow copy - we'll mutate this 

91 

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

93 if _needs_app(args): 

94 args.insert(0, DEFAULT_APP) 

95 

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

97 if "--uds" not in args: 

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

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

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

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

102 

103 return args 

104 

105 

106# --------------------------------------------------------------------------- 

107# Public entry-point 

108# --------------------------------------------------------------------------- 

109 

110 

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

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

113 

114 # Check for version flag 

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

116 print(f"mcpgateway {__version__}") 

117 return 

118 

119 # Discard the program name and inspect the rest. 

120 user_args = sys.argv[1:] 

121 uvicorn_argv = _insert_defaults(user_args) 

122 

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

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

125 uvicorn.main() # pylint: disable=no-value-for-parameter 

126 

127 

128if __name__ == "__main__": # pragma: no cover - executed only when run directly 

129 main()