Build CLIs that humans and AI agents both use natively
from milo import CLI
cli = CLI(name="deployer", description="Deploy services to environments")
@cli.command("deploy", description="Deploy a service", annotations={"destructiveHint": True})
def deploy(environment: str, service: str, version: str = "latest") -> dict:
"""Deploy a service to the specified environment."""
return {"status": "deployed", "environment": environment, "service": service, "version": version}
cli.run()Three protocols from one decorator:
# Human CLI
deployer deploy --environment production --service api
# MCP tool (AI agent calls this via JSON-RPC)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"deploy","arguments":{"environment":"staging","service":"api"}}}' \
| deployer --mcp
# AI-readable discovery document
deployer --llms-txtMilo is a Python framework where every CLI is simultaneously a terminal app, a command-line tool, and an MCP server. Write one function with type annotations and a docstring — Milo generates the argparse subcommand, the MCP tool schema, and the llms.txt entry automatically.
Why people pick it:
- Every CLI is an MCP server —
@cli.commandproduces an argparse subcommand, MCP tool, and llms.txt entry from one function. AI agents discover and call your tools with zero extra code. - Dual-mode commands — The same command shows an interactive UI when a human runs it, and returns structured JSON when an AI calls it via MCP.
- Annotated schemas — Type hints +
Annotatedconstraints generate rich JSON Schema. Agents validate inputs before calling. - Streaming progress — Commands that yield
Progressobjects stream notifications to MCP clients in real time. - Elm Architecture — Immutable state, pure reducers, declarative views. Every state transition is explicit and testable.
- Free-threading ready — Built for Python 3.14t (PEP 703). Sagas run on
ThreadPoolExecutorwith no GIL contention. - One runtime dependency — Just
kida-templates. No click, no rich, no curses.
- AI agent toolchains — Every CLI doubles as an MCP server; register multiple CLIs behind a single gateway
- Interactive CLI tools — Wizards, installers, configuration prompts, and guided workflows
- Dual-mode commands — Interactive when a human runs them, structured when an AI calls them
- Multi-screen terminal apps — Declarative flows with
>>operator for screen-to-screen navigation - Forms and data collection — Text, select, confirm, and password fields with validation
- Dev tools with hot reload —
milo devwatches templates and live-reloads on change - Session recording and replay — Record user sessions to JSONL, replay for debugging or CI regression tests
pip install milo-cliThe PyPI package is milo-cli; import the milo namespace in Python. The milo console command is installed with the package.
Requires Python 3.14+
| Function | Description |
|---|---|
CLI(name, description, version) |
Create a CLI application |
@cli.command(name, description) |
Register a typed command |
cli.group(name, description) |
Create a command group |
cli.run() |
Parse args and dispatch |
cli.call("cmd", **kwargs) |
Programmatic invocation |
--mcp |
Run as MCP server |
--llms-txt |
Generate AI discovery doc |
--mcp-install |
Register in gateway |
annotations={...} |
MCP behavioral hints |
Annotated[str, MinLen(1)] |
Schema constraints |
| Function | Description |
|---|---|
App(template, reducer, initial_state) |
Create a single-screen app |
App.from_flow(flow) |
Create a multi-screen app from a Flow |
form(*specs) |
Run an interactive form, return {field: value} |
FlowScreen(name, template, reducer) |
Define a named screen |
flow = screen_a >> screen_b |
Chain screens into a flow |
ctx.run_app(reducer, template, state) |
Bridge CLI commands to interactive apps |
quit_on, with_cursor, with_confirm |
Reducer combinator decorators |
Cmd(fn), Batch(cmds), Sequence(cmds) |
Side effects on thread pool |
ViewState(cursor_visible=True, ...) |
Declarative terminal state |
DevServer(app, watch_dirs) |
Hot-reload dev server |
| Feature | Description | Docs |
|---|---|---|
| MCP Server | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | MCP → |
| MCP Gateway | Single gateway aggregates all registered Milo CLIs for unified AI agent access | MCP → |
| Tool Annotations | Declare readOnlyHint, destructiveHint, idempotentHint per MCP spec |
MCP → |
| Streaming Progress | Commands yield Progress objects; MCP clients receive real-time notifications |
MCP → |
| Schema Constraints | Annotated[str, MinLen(1), MaxLen(100)] generates rich JSON Schema |
CLI → |
| llms.txt | Generate AI-readable discovery documents from CLI command definitions | llms.txt → |
| Middleware | Intercept MCP calls and CLI commands for logging, auth, and transformation | CLI → |
| Observability | Built-in request logging with latency stats (milo://stats resource) |
MCP → |
| State Management | Redux-style Store with dispatch, listeners, middleware, and saga scheduling |
State → |
| Commands | Lightweight Cmd thunks, Batch, Sequence, TickCmd for one-shot effects |
Commands → |
| Sagas | Generator-based side effects: Call, Put, Select, Fork, Delay, Retry |
Sagas → |
| ViewState | Declarative terminal state (cursor_visible, alt_screen, window_title, mouse_mode) |
Commands → |
| Flows | Multi-screen state machines with >> operator and custom transitions |
Flows → |
| Forms | Text, select, confirm, password fields with validation and TTY fallback | Forms → |
| Input Handling | Cross-platform key reader with full escape sequence support (arrows, F-keys, modifiers) | Input → |
| Templates | Kida-powered terminal rendering with built-in form, field, help, and progress templates | Templates → |
| Dev Server | milo dev with filesystem polling and @@HOT_RELOAD dispatch |
Dev → |
| Session Recording | JSONL action log with state hashes for debugging and regression testing | Testing → |
| Snapshot Testing | assert_renders, assert_state, assert_saga for deterministic test coverage |
Testing → |
| Help Rendering | HelpRenderer — drop-in argparse.HelpFormatter using Kida templates |
Help → |
| Context | Execution context with verbosity, output format, global options, and run_app() bridge |
Context → |
| Configuration | Config with validation, init scaffolding, and profile support |
Config → |
| Shell Completions | Generate bash/zsh/fish completions from CLI definitions | CLI → |
| Doctor Diagnostics | run_doctor() validates environment, dependencies, and config health |
CLI → |
Dual-Mode Commands — Interactive for humans, structured for AI
from milo import CLI, Context, Action, Quit, SpecialKey
from milo.streaming import Progress
from typing import Annotated
from milo import MinLen
cli = CLI(name="deployer", description="Deploy services")
@cli.command("deploy", description="Deploy a service", annotations={"destructiveHint": True})
def deploy(
environment: Annotated[str, MinLen(1)],
service: Annotated[str, MinLen(1)],
ctx: Context = None,
) -> dict:
"""Deploy a service to an environment."""
# Interactive mode: show confirmation UI
if ctx and ctx.is_interactive:
if not ctx.confirm(f"Deploy {service} to {environment}?"):
return {"status": "cancelled"}
# Stream progress (MCP clients see real-time notifications)
yield Progress(status=f"Deploying {service}", step=0, total=2)
yield Progress(status="Verifying health", step=1, total=2)
return {"status": "deployed", "environment": environment, "service": service}Run by a human: interactive confirmation, then progress output. Called via MCP: progress notifications stream, then structured JSON result.
MCP Server & Gateway — AI agent integration
Every Milo CLI is automatically an MCP server:
# Run as MCP server (stdin/stdout JSON-RPC)
myapp --mcp
# Register with an AI host directly
claude mcp add myapp -- uv run python examples/deploy/app.py --mcpFor multiple CLIs, register them and run a single gateway:
# Register CLIs
taskman --mcp-install
deployer --mcp-install
# Run the unified gateway
uv run python -m milo.gateway --mcp
# Or register the gateway with your AI host
claude mcp add milo -- uv run python -m milo.gateway --mcpThe gateway namespaces tools automatically: taskman.add, deployer.deploy, etc. Implements MCP 2025-11-25 with outputSchema, structuredContent, tool annotations, and streaming Progress notifications.
Built-in milo://stats resource exposes request latency, error counts, and throughput.
Schema Constraints — Rich validation from type hints
from typing import Annotated
from milo import CLI, MinLen, MaxLen, Gt, Lt, Pattern, Description
cli = CLI(name="app")
@cli.command("create-user", description="Create a user account")
def create_user(
name: Annotated[str, MinLen(1), MaxLen(100), Description("Full name")],
age: Annotated[int, Gt(0), Lt(200)],
email: Annotated[str, Pattern(r"^[^@]+@[^@]+$")],
) -> dict:
return {"name": name, "age": age, "email": email}Generates JSON Schema with minLength, maxLength, exclusiveMinimum, exclusiveMaximum, pattern, and description — AI agents validate inputs before calling.
Single-Screen App — Counter with keyboard input
from milo import App, Action
def reducer(state, action):
if state is None:
return {"count": 0}
if action.type == "@@KEY" and action.payload.char == " ":
return {**state, "count": state["count"] + 1}
return state
app = App(template="counter.kida", reducer=reducer, initial_state=None)
final_state = app.run()counter.kida:
Count: {{ count }}
Press SPACE to increment, Ctrl+C to quit.
Multi-Screen Flow — Chain screens with >>
from milo import App
from milo.flow import FlowScreen
welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
config = FlowScreen("config", "config.kida", config_reducer)
confirm = FlowScreen("confirm", "confirm.kida", confirm_reducer)
flow = welcome >> config >> confirm
app = App.from_flow(flow)
app.run()Navigate between screens by dispatching @@NAVIGATE actions from your reducers. Add custom transitions with flow.with_transition("welcome", "confirm", on="@@SKIP").
Interactive Forms — Collect structured input
from milo import form, FieldSpec, FieldType
result = form(
FieldSpec("name", "Your name"),
FieldSpec("env", "Environment", field_type=FieldType.SELECT,
choices=("dev", "staging", "prod")),
FieldSpec("confirm", "Deploy?", field_type=FieldType.CONFIRM),
)
# result = {"name": "Alice", "env": "prod", "confirm": True}Tab/Shift+Tab navigates fields. Arrow keys cycle select options. Falls back to plain input() prompts when stdin is not a TTY.
Sagas — Generator-based side effects
from milo import Call, Put, Select, ReducerResult
def fetch_saga():
url = yield Select(lambda s: s["url"])
data = yield Call(fetch_json, (url,))
yield Put(Action("FETCH_DONE", payload=data))
def reducer(state, action):
if action.type == "@@KEY" and action.payload.char == "f":
return ReducerResult({**state, "loading": True}, sagas=(fetch_saga,))
if action.type == "FETCH_DONE":
return {**state, "loading": False, "data": action.payload}
return stateSaga effects: Call(fn, args), Put(action), Select(selector), Fork(saga), Delay(seconds), Retry(fn, ...).
For one-shot effects, use Cmd instead — no generator needed:
from milo import Cmd, ReducerResult
def fetch_status():
return Action("STATUS", payload=urllib.request.urlopen(url).status)
def reducer(state, action):
if action.type == "CHECK":
return ReducerResult(state, cmds=(Cmd(fetch_status),))
return stateTesting Utilities — Snapshot, state, and saga assertions
from milo.testing import assert_renders, assert_state, assert_saga
from milo import Action, Call
# Snapshot test: render state through template, compare to file
assert_renders({"count": 5}, "counter.kida", snapshot="tests/snapshots/count_5.txt")
# Reducer test: feed actions, assert final state
assert_state(reducer, None, [Action("@@INIT"), Action("INCREMENT")], {"count": 1})
# Saga test: step through generator, assert each yielded effect
assert_saga(my_saga(), [(Call(fetch, ("url",), {}), {"data": 42})])Set MILO_UPDATE_SNAPSHOTS=1 to regenerate snapshot files.
Elm Architecture — Model-View-Update loop
┌──────────────┐
│ Terminal │
│ (View) │
└──────┬───────┘
│ Key events
▼
┌──────────┐ ┌──────────────────┐ ┌──────────────┐
│ Kida │◄───│ Store │◄───│ Reducer │
│ Template │ │ (State Tree) │ │ (Pure fn) │
└──────────┘ └──────────┬───────┘ └──────────────┘
│
▼
┌──────────────┐
│ Sagas │
│ (Side Effects│
│ on ThreadPool)
└──────────────┘
- Model — Immutable state (plain dicts or frozen dataclasses)
- View — Kida templates render state to terminal output
- Update — Pure
reducer(state, action) -> statefunctions - Effects —
Cmdthunks (one-shot) or generator-based sagas (multi-step) onThreadPoolExecutor
Event Loop — App lifecycle
App.run()
├── Store(reducer, initial_state)
├── KeyReader (raw mode, escape sequences → Key objects)
├── TerminalRenderer (alternate screen buffer, flicker-free updates)
├── Optional: tick thread (@@TICK at interval)
├── Optional: SIGWINCH handler (@@RESIZE)
└── Loop:
read key → dispatch @@KEY → reducer → re-render
until state.submitted or @@QUIT
Builtin Actions — Event vocabulary
| Action | Trigger | Payload |
|---|---|---|
@@INIT |
Store creation | — |
@@KEY |
Keyboard input | Key(char, name, ctrl, alt, shift) |
@@TICK |
Timer interval | — |
@@RESIZE |
Terminal resize | (cols, rows) |
@@NAVIGATE |
Screen transition | screen_name |
@@HOT_RELOAD |
Template file change | file_path |
@@EFFECT_RESULT |
Saga completion | result |
@@QUIT |
Ctrl+C | — |
| Section | Description |
|---|---|
| Get Started | Installation and quickstart |
| MCP & AI | MCP server, gateway, annotations, streaming, and llms.txt |
| Usage | State, sagas, flows, forms, templates |
| Testing | Snapshots, recording, replay |
| Reference | Complete API documentation |
git clone https://github.com/lbliii/milo-cli.git
cd milo-cli
# Uses Python 3.14t by default (.python-version)
uv sync --group dev --python 3.14t
PYTHON_GIL=0 uv run --python 3.14t pytest tests/
make ci # optional: ruff + ty + tests with coverageA structured reactive stack — every layer written in pure Python for 3.14t free-threading.
| ᓚᘏᗢ | Bengal | Static site generator | Docs |
| ∿∿ | Purr | Content runtime | — |
| ⌁⌁ | Chirp | Web framework | Docs |
| =^..^= | Pounce | ASGI server | Docs |
| )彡 | Kida | Template engine | Docs |
| ฅᨐฅ | Patitas | Markdown parser | Docs |
| ⌾⌾⌾ | Rosettes | Syntax highlighter | Docs |
| ᗣᗣ | Milo (PyPI: milo-cli) |
CLI framework ← You are here | Docs |
Python-native. Free-threading ready. No npm required.
MIT License — see LICENSE for details.