Coverage for mcpgateway/cli.py: 97%
29 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
« 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
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
8This module is exposed as a **console-script** via:
10 [project.scripts]
11 mcpgateway = "mcpgateway.cli:main"
13so that a user can simply type `mcpgateway ...` instead of the longer
14`uvicorn mcpgateway.main:app ...`.
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.
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"""
36# Future
37from __future__ import annotations
39# Standard
40import os
41import sys
42from typing import List
44# Third-Party
45import uvicorn
47# First-Party
48from mcpgateway import __version__
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"))
57# ---------------------------------------------------------------------------
58# Helper utilities
59# ---------------------------------------------------------------------------
62def _needs_app(arg_list: List[str]) -> bool:
63 """Return *True* when the CLI invocation has *no* positional APP path.
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.
70 Args:
71 arg_list (List[str]): List of arguments
73 Returns:
74 bool: Returns *True* when the CLI invocation has *no* positional APP path
75 """
77 return len(arg_list) == 0 or arg_list[0].startswith("-")
80def _insert_defaults(raw_args: List[str]) -> List[str]:
81 """Return a *new* argv with defaults sprinkled in where needed.
83 Args:
84 raw_args (List[str]): List of input arguments to cli
86 Returns:
87 List[str]: List of arguments
88 """
90 args = list(raw_args) # shallow copy - we'll mutate this
92 # 1️⃣ Ensure an application path is present.
93 if _needs_app(args):
94 args.insert(0, DEFAULT_APP)
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)])
103 return args
106# ---------------------------------------------------------------------------
107# Public entry-point
108# ---------------------------------------------------------------------------
111def main() -> None: # noqa: D401 - imperative mood is fine here
112 """Entry point for the *mcpgateway* console script (delegates to Uvicorn)."""
114 # Check for version flag
115 if "--version" in sys.argv or "-V" in sys.argv:
116 print(f"mcpgateway {__version__}")
117 return
119 # Discard the program name and inspect the rest.
120 user_args = sys.argv[1:]
121 uvicorn_argv = _insert_defaults(user_args)
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
128if __name__ == "__main__": # pragma: no cover - executed only when run directly
129 main()