Typer and Milo both make Python type hints central to CLI authoring. The main
migration question is not syntax; it is contract surface. Milo adds MCP
tools/list, MCP tools/call, llms.txt discovery, structured error data, and
milo verifyto the same typed function.
Official references: Typer first steps and Typer command arguments.
Before
import typer
app = typer.Typer()
@app.command()
def greet(name: str, loud: bool = False):
message = f"Hello, {name}!"
print(message.upper() if loud else message)
if __name__ == "__main__":
app()
After
from milo import CLI
cli = CLI(name="greeter", description="Greeting commands")
@cli.command("greet", description="Return a greeting")
def greet(name: str, loud: bool = False) -> str:
"""Greet someone.
Args:
name: Person to greet.
loud: If true, SHOUT.
"""
message = f"Hello, {name}!"
return message.upper() if loud else message
if __name__ == "__main__":
cli.run()
Mapping
| Typer concept | Milo equivalent |
|---|---|
typer.run(main) |
cli = CLI(...); @cli.command(...); cli.run() |
app = typer.Typer() |
cli = CLI(...) |
@app.command() |
@cli.command("name", description="...") |
Required argument fromname: str |
Required schema field fromname: str |
Option fromflag: bool = False |
Optional flag fromflag: bool = False |
| Function docstring help | Function docstring plusArgs:parameter descriptions |
Add The Agent Contract
After migrating a Typer command, add these checks:
uv run python app.py --llms-txt
uv run milo verify app.py
uv run pytest tests/ -q
And test MCP dispatch directly:
from milo.mcp import _call_tool
def test_mcp_dispatch():
result = _call_tool(cli, {"name": "greet", "arguments": {"name": "Agent"}})
assert result["content"][0]["text"] == "Hello, Agent!"
What To Watch
- Replace output-first handlers with return values when agents need structured data.
- Document every public parameter.
milo verifywarns when schema fields lack descriptions. - Keep interactive behavior behind
ctx.is_interactiveso MCP calls remain deterministic.