ᗣᗣ Milo

May 3, 2026 · View on GitHub

PyPI version Build Status Python 3.14+ License: MIT

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-txt

What is Milo?

Milo 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.command produces 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 + Annotated constraints generate rich JSON Schema. Agents validate inputs before calling.
  • Streaming progress — Commands that yield Progress objects 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 ThreadPoolExecutor with no GIL contention.
  • One runtime dependency — Just kida-templates. No click, no rich, no curses.

Use Milo For

  • 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 reloadmilo dev watches templates and live-reloads on change
  • Session recording and replay — Record user sessions to JSONL, replay for debugging or CI regression tests

Installation

Requires Python 3.14+. If you don't have it: uv python install 3.14.

pip install milo-cli

The PyPI package is milo-cli; import the milo namespace in Python. The milo console command is installed with the package.


Quick Start

Coding agents: jump to docs/agent-quickstart.md for a 5-minute walkthrough from @cli.command to a verified Claude MCP tool call. See also docs/testing.md for the test template.

AI-Native CLI

FunctionDescription
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
--mcpRun as MCP server
--llms-txtGenerate AI discovery doc
--mcp-installRegister in gateway
annotations={...}MCP behavioral hints
Annotated[str, MinLen(1)]Schema constraints

Interactive Apps

FunctionDescription
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_bChain screens into a flow
ctx.run_app(reducer, template, state)Bridge CLI commands to interactive apps
quit_on, with_cursor, with_confirmReducer 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

Features

FeatureDescriptionDocs
MCP ServerEvery CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPCMCP →
MCP GatewaySingle gateway aggregates all registered Milo CLIs for unified AI agent accessMCP →
Tool AnnotationsDeclare readOnlyHint, destructiveHint, idempotentHint per MCP specMCP →
Streaming ProgressCommands yield Progress objects; MCP clients receive real-time notificationsMCP →
Schema ConstraintsAnnotated[str, MinLen(1), MaxLen(100)] generates rich JSON SchemaCLI →
llms.txtGenerate AI-readable discovery documents from CLI command definitionsllms.txt →
MiddlewareIntercept MCP calls and CLI commands for logging, auth, and transformationCLI →
ObservabilityBuilt-in request logging with latency stats (milo://stats resource)MCP →
State ManagementRedux-style Store with dispatch, listeners, middleware, and saga schedulingState →
CommandsLightweight Cmd thunks, Batch, Sequence, TickCmd for one-shot effectsCommands →
SagasGenerator-based side effects: Call, Put, Select, Fork, Delay, Retry, Race, All, Take, and moreSagas →
ViewStateDeclarative terminal state (cursor_visible, alt_screen, window_title, mouse_mode)Commands →
FlowsMulti-screen state machines with >> operator and custom transitionsFlows →
FormsText, select, confirm, password fields with validation and TTY fallbackForms →
Input HandlingCross-platform key reader with VT100/xterm escape sequence support (arrows, F-keys, modifiers)Input →
TemplatesKida-powered terminal rendering with built-in form, field, help, and progress templatesTemplates →
Dev Servermilo dev with filesystem polling and @@HOT_RELOAD dispatchDev →
Session RecordingJSONL action log with state hashes for debugging and regression testingTesting →
Snapshot Testingassert_renders, assert_state, assert_saga for deterministic test coverageTesting →
PipelineDeclarative multi-phase workflows with dependency graphs, retry policies, and output capturePipeline →
Help RenderingHelpRenderer — drop-in argparse.HelpFormatter using Kida templatesHelp →
ContextExecution context with verbosity, output format, global options, and run_app() bridgeContext →
ConfigurationConfig with validation, init scaffolding, and profile supportConfig →
Shell CompletionsGenerate bash/zsh/fish completions from CLI definitionsCLI →
Doctor Diagnosticsrun_doctor() validates environment, dependencies, and config healthCLI →

Examples Index

Pick the example closest to your use case, copy its app.py, and adapt. See examples/README.md for run commands, copy paths, and tested starting points.

CLIs (typed function → CLI + MCP + llms.txt)

What you want to buildExampleKey APIs
The simplest possible CLIexamples/greetCLI, @cli.command
Dual-mode CLI ↔ MCP server (flagship)examples/deployAnnotated, MinLen, Context, Progress, --mcp
Context injection, logging, progress, confirmsexamples/ctxdemoContext, ctx.info, ctx.progress, ctx.confirm
Nested command groups (app repo list)examples/groupscli.group(), walk_commands
Fast startup via deferred importsexamples/lazyappcli.lazy_command()
Production CLI with hooks, completions, doctorexamples/devtoolrun_doctor, before_run/after_run, did-you-mean, completions
AI-native CLI surfacing tools + resourcesexamples/taskman@command, @resource, --format, --llms-txt, --mcp
Advanced terminal reports and diagnosticsexamples/outputgalleryContext.render, Kida templates, character maps, JSON output

Configuration, plugins, pipelines

What you want to buildExampleKey APIs
TOML config with profiles + overlaysexamples/configappConfig, ConfigSpec, Config.load, Config.validate
Plugin system with hooks + listenersexamples/pluggableHookRegistry, define, on, invoke
Multi-phase pipeline with deps + retriesexamples/buildpipePipeline, Phase, PhasePolicy, >>

Interactive TUIs (App + reducer)

What you want to buildExampleKey APIs
The simplest TUIexamples/counterApp.from_dir, reducer combinators
Modal input with derived filteringexamples/todotuple state, quit_on, derived views
Tick-driven animationexamples/stopwatchtick_rate, @@TICK, quit_on
Scrollable viewport with saga I/Oexamples/filepickerviewport, sagas, frozen tuples
Multi-screen flow with formsexamples/wizardFlow, FlowScreen, make_form_reducer, FieldSpec

Async work (sagas + Cmd pattern)

What you want to buildExampleKey APIs
Sagas for async side effectsexamples/fetcherCall, Put, Select, Retry
Parallel concurrent workexamples/downloaderFork, Call, Delay, Timeout
Bubbletea-style Cmd thunksexamples/spinnerCmd, Batch, TickCmd, ViewState
Live rendering outside an Appexamples/liverendermilo.live.LiveRenderer, Spinner, terminal_env

Don't see your use case? Run milo new <name> to scaffold a fresh CLI with tests, then milo verify app.py to confirm it works.


Usage

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 --mcp

For 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 --mcp

The 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 state

Saga effects: Call, Put, Select, Fork, Delay, Retry, Timeout, TryCall, Race, All, Take, Debounce, TakeEvery, TakeLatest.

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 state
Testing 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.


Architecture

Elm Architecture — Model-View-Update loop
                    ┌──────────────┐
                    │   Terminal    │
                    │   (View)     │
                    └──────┬───────┘
                           │ Key events

┌──────────┐    ┌──────────────────┐    ┌──────────────┐
│  Kida    │◄───│      Store       │◄───│   Reducer    │
│ Template │    │  (State Tree)    │    │  (Pure fn)   │
└──────────┘    └──────────┬───────┘    └──────────────┘


                    ┌──────────────┐
                    │    Sagas     │
                    │ (Side Effects│
                    │  on ThreadPool)
                    └──────────────┘
  1. Model — Immutable state (plain dicts or frozen dataclasses)
  2. View — Kida templates render state to terminal output
  3. Update — Pure reducer(state, action) -> state functions
  4. EffectsCmd thunks (one-shot) or generator-based sagas (multi-step) on ThreadPoolExecutor
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
ActionTriggerPayload
@@INITStore creation
@@KEYKeyboard inputKey(char, name, ctrl, alt, shift)
@@TICKTimer interval
@@RESIZETerminal resize(cols, rows)
@@NAVIGATEScreen transitionscreen_name
@@HOT_RELOADTemplate file changefile_path
@@EFFECT_RESULTSaga completionresult
@@QUITCtrl+C

Documentation

SectionDescription
AboutPhilosophy, architecture, concepts, and lifecycle
Get StartedInstallation and quickstart
Build CLIsCommands, groups, MCP, llms.txt, context, output, and help
Build AppsState, reducers, templates, input, forms, flows, sagas, and live rendering
QualityTesting, verification, debugging, and pipelines
ReferenceSchema, dispatch, error codes, actions, and types

Development

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 coverage

The Bengal Ecosystem

A structured reactive stack — every layer written in pure Python for 3.14t free-threading.

ᓚᘏᗢBengalStatic site generatorDocs
∿∿PurrContent runtime
⌁⌁ChirpWeb frameworkDocs
=^..^=PounceASGI serverDocs
)彡KidaTemplate engineDocs
ฅᨐฅPatitasMarkdown parserDocs
⌾⌾⌾RosettesSyntax highlighterDocs
ᗣᗣMilo (PyPI: milo-cli)CLI framework ← You are hereDocs

Python-native. Free-threading ready. No npm required.


License

MIT License — see LICENSE for details.