Milo'sCLIclass turns decorated Python functions into CLI commands, MCP tools, and llms.txt entries — all from a single definition. Type annotations drive argument parsing, schema generation, and help text.
Creating a CLI
from milo import CLI
cli = CLI(name="myapp", description="My tool", version="1.0.0")
The CLIis the entry point for your application. It manages commands, groups, global options, and dispatches to handlers.
Registering commands
Use the@cli.commanddecorator to register functions as CLI subcommands:
@cli.command("greet", description="Say hello")
def greet(name: str, loud: bool = False) -> str:
msg = f"Hello, {name}!"
return msg.upper() if loud else msg
Type annotations are used to:
- Generate argparse arguments (
--name,--loud) - Generate MCP tool schemas for AI agents
- Determine required vs optional parameters (parameters with defaults are optional)
myapp greet --name Alice
myapp greet --name Alice --loud
Command options
@cli.command(
"deploy",
description="Deploy the application",
aliases=("d",), # Alternative names
tags=("ops",), # Grouping in llms.txt
hidden=True, # Omit from help and llms.txt
)
def deploy(target: str, dry_run: bool = False) -> dict: ...
Supported parameter types
| Python type | argparse | JSON Schema |
|---|---|---|
str |
--flag VALUE |
"string" |
int |
--flag N(type=int) |
"integer" |
float |
--flag N(type=float) |
"number" |
bool |
--flag(store_true) |
"boolean" |
list[str] |
--flag A B C(nargs=*) |
"array" |
X | None |
optional | unwrapped to base type |
Output formatting
Every command gets a--formatflag automatically:
myapp greet --name Alice --format json
myapp greet --name Alice --format table
myapp greet --name Alice --format plain # default
The handler's return value is serialized based on the chosen format. See Output Formatting for details.
Running the CLI
if __name__ == "__main__":
cli.run()
cli.run() parses sys.argv, resolves the command, injects context, calls the handler, and formats the output.
Built-in flags
Every CLI gets these flags automatically:
| Flag | Description |
|---|---|
--version |
Print version and exit |
--llms-txt |
Output an llms.txt AI discovery document |
--mcp |
Run as an MCP server (JSON-RPC on stdin/stdout) |
-v / --verbose |
Increase verbosity (stackable:-vvfor debug) |
-q / --quiet |
Suppress non-error output |
--no-color |
Disable color output |
Programmatic invocation
Call commands directly without going through argparse:
result = cli.call("greet", name="Alice")
result = cli.call("site.build", output="_site") # dotted paths for group commands
This is how the MCP server dispatches tool calls internally.
Fuzzy matching
If a user mistypes a command, the CLI suggests the closest match:
$ myapp gret
Unknown command: 'gret'. Did you mean 'greet'?
Shell completions
Milo can generate shell completion scripts for bash, zsh, and fish. Every CLI gets a--completionsflag:
myapp --completions bash # Print bash completion script
myapp --completions zsh # Print zsh completion script
myapp --completions fish # Print fish completion script
Add the output to your shell config to enable tab-completion for commands and flags:
eval "$(myapp --completions bash)"
Or add to ~/.bashrcfor persistence.
eval "$(myapp --completions zsh)"
Or add to ~/.zshrcfor persistence.
myapp --completions fish | source
Or save to~/.config/fish/completions/myapp.fishfor persistence.
Programmatically, useinstall_completions():
from milo.completions import install_completions
script = install_completions(cli, shell="zsh") # or "bash", "fish"
If shell is omitted, it auto-detects from the $SHELLenvironment variable.
Doctor diagnostics
The doctor system runs health checks against your CLI environment. Define what to verify, andrun_doctorchecks it all at once:
from milo.doctor import run_doctor, format_doctor_report
report = run_doctor(
cli,
config_spec=spec, # Check config files exist
required_env=("API_KEY", "DATABASE_URL"), # Required env vars
required_tools=("git", "node"), # Required binaries on PATH
custom_checks=(my_custom_check,), # Callables returning Check
)
print(format_doctor_report(report))
Output:
✓ python: Python 3.14.0
✓ milo: milo 0.1.1
✓ config:myapp.toml: Found 1 file(s)
✓ env:API_KEY: Set
✗ env:DATABASE_URL: Not set
hint: export DATABASE_URL=<value>
✓ tool:git: /usr/bin/git
5 passed, 0 warnings, 1 failures
Built-in checks include Python version, Milo version, config file discovery, and registered command count. Add custom checks by passing callables that return aCheck:
from milo.doctor import Check
def check_disk_space():
free_gb = get_free_space()
if free_gb < 1:
return Check(name="disk", status="fail", message="Low disk space",
suggestion="Free up disk space")
return Check(name="disk", status="ok", message=f"{free_gb:.1f} GB free")
The DoctorReport dataclass tracks counts: report.ok, report.warnings, report.failures.
Version checking
Milo can check PyPI for newer versions of your package:
from milo.version_check import check_version, format_version_notice
info = check_version("myapp", current_version="1.0.0")
if info and info.update_available:
print(format_version_notice(info, prog="myapp"), file=sys.stderr)
A new version of myapp is available: 1.0.0 -> 1.2.0
pip install --upgrade myapp
Key behaviors:
- Caching — results are cached in
~/.milo/cache/for 24 hours to avoid hitting PyPI on every invocation. - Silent failures — network errors, timeouts, and cache failures are swallowed;
check_version()returnsNone. - Opt-out — set
NO_UPDATE_CHECK=1orCI=1to disable the check entirely. - Installer detection —
format_version_notice()detectsuvvspipand prints the correct upgrade command.
TheVersionInfo dataclass contains current, latest, update_available, and an optional message.
Tip
See Command Groups for organizing commands into nested namespaces, Context for injecting execution context into handlers, and Lazy Loading for deferred imports.