Coverage for mcpgateway/cli.py: 97%

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

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

3

4 Copyright 2025

5 SPDX-License-Identifier: Apache-2.0

6 Authors: Mihai Criveti

7

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

9

10 [project.scripts]

11 mcpgateway = "mcpgateway.cli:main"

12

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

14`uvicorn mcpgateway.main:app ...`.

15

16 Features

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

26 Typical 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

37 from __future__ import annotations

38

39# Standard

40 import os

41 import sys

42 from typing import List

43

44# Third-Party

45 import uvicorn

46

47# First-Party

48 from mcpgateway import __version__

49

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

51# Configuration defaults (overridable via environment variables)

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

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

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

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

56

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

58# Helper utilities

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

60

61

62 def _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

80 def _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

111 def 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

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

129 main()