Skip to main content

ᗣᗣ Milo

PyPI version Build Status Python 3.14+ License: MIT

One typed Python function becomes a human CLI command, an MCP tool, and an agent-readable discovery entry. (evidence: one-definition-three-surfaces)

Prove It in 60 Seconds

With uv installed, paste this into a clean directory. uv downloads Python 3.14 and Milo when they are not already available:

uvx --python 3.14 --from milo-cli milo new hello_milo
uv run --python 3.14 --with milo-cli python hello_milo/app.py greet --name World
uv run --python 3.14 --with milo-cli milo verify hello_milo/app.py

The second command prints Hello, World!. The ten-check verifier (evidence: verify-ten-check-conformance) then exercises import, schema generation, MCP discovery, MCP Apps tool/resource/gateway conformance, and a real subprocess JSON-RPC handshake with resource reads. Do not register a new tool until it reports zero failures.

Now give Claude Code the same file as a local stdio MCP server:

claude mcp add --transport stdio hello_milo -- \
  uv run --python 3.14 --with milo-cli python "$PWD/hello_milo/app.py" --mcp

Ask Claude to “use the greet tool to greet Ada,” or call the same function by hand:

uv run --python 3.14 --with milo-cli python hello_milo/app.py greet --name Ada

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, and Milo enforces it before handlers run.
  • 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. (evidence: free-threaded-runtime)
  • One runtime dependency — Just kida-templates. No click, no rich, no curses. (evidence: one-runtime-dependency)

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.

Migrating an established framework or developer CLI? Use the mature-CLI adoption guide to inventory compatibility, phase the cutover, and add an exact-version downstream canary before switching entry points.

AI-Native CLI

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
ui=MCPAppToolMeta("ui://...") Link a tool to an MCP Apps UI resource
Annotated[str, MinLen(1)] Schema constraints
Annotated[str, Positional("NAME")] Positional CLI presentation
Option(aliases=("-n",)) Compatible option aliases
surfaces=("cli",) Keep long-running commands out of agent discovery
terminal_renderer=... Human output over structured command results

Interactive Apps

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

Features

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 and enforces 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, Race, All, Take, and more 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 VT100/xterm 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 →
Pipeline Declarative multi-phase workflows with dependency graphs, retry policies, and output capture Pipeline →
Help Rendering HelpRenderer — drop-in argparse.HelpFormatter using Kida templates Help →
Context Injectable output, interaction, approvals, 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 →

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.

For a recording-ready integration demo instead of a copy path, run the Waypoint showcase: three parallel agents journal a race through hooks and CLI, a shell-less agent reads and picks through MCP, and a human gets the same history as a TUI and MCP Apps DAG.

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

What you want to build Example Key APIs
The simplest possible CLI examples/greet CLI, @cli.command
Dual-mode CLI ↔ MCP server (flagship) examples/deploy Annotated, MinLen, Context, Progress, --mcp
MCP tool with a negotiated interactive UI resource examples/mcp_app Dependency-free HTML, ui_resource, MCPAppToolMeta, structured fallback
Context injection, host-owned output, progress, confirms examples/ctxdemo Context, OutputSink, ctx.progress, ctx.confirm
Nested command groups (app repo list) examples/groups cli.group(), walk_commands
Fast startup via deferred imports examples/lazyapp cli.lazy_command()
Production CLI with hooks, completions, doctor examples/devtool run_doctor, before_command/after_command, did-you-mean, completions
AI-native CLI surfacing tools + resources examples/taskman @cli.command, @cli.resource, --format, --llms-txt, --mcp
Advanced terminal reports and diagnostics examples/outputgallery Context.render, Kida templates, character maps, JSON output

Configuration, plugins, pipelines

What you want to build Example Key APIs
TOML config with profiles + overlays examples/configapp Config, ConfigSpec, Config.load, Config.validate
Plugin system with hooks + listeners examples/pluggable HookRegistry, define, on, invoke
Multi-phase pipeline with deps + retries examples/buildpipe Pipeline, Phase, PhasePolicy, >>

Interactive TUIs (App + reducer)

What you want to build Example Key APIs
The simplest TUI examples/counter App.from_dir, reducer combinators
Modal input with derived filtering examples/todo tuple state, quit_on, derived views
Tick-driven animation examples/stopwatch tick_rate, @@TICK, quit_on
Scrollable viewport with saga I/O examples/filepicker viewport, sagas, frozen tuples
Multi-screen flow with forms examples/wizard Flow, FlowScreen, make_form_reducer, FieldSpec

Async work (sagas + Cmd pattern)

What you want to build Example Key APIs
Sagas for async side effects examples/fetcher Call, Put, Select, Retry
Parallel concurrent work examples/downloader Fork, Call, Delay, Timeout
Bubbletea-style Cmd thunks examples/spinner Cmd, Batch, TickCmd, ViewState
Live rendering outside an App examples/liverender milo.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 --transport stdio myapp -- \
  uv run python "$PWD/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 --transport stdio milo -- uv run python -m milo.gateway --mcp

The gateway namespaces tools automatically (taskman.add, deployer.deploy) and rewrites negotiated MCP Apps ui:// links without collisions. It preserves outputSchema, structuredContent, tool annotations, resource metadata, and streaming Progress notifications across child CLIs.

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

Documentation

Section Description
About Philosophy, architecture, concepts, and lifecycle
Get Started Installation and quickstart
Build CLIs Commands, groups, MCP, llms.txt, context, output, and help
Build Apps State, reducers, templates, input, forms, flows, sagas, and live rendering
Quality Testing, verification, debugging, and pipelines
Reference Schema, 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

See CONTRIBUTING.md for repository setup, proof, changelog, and pull-request expectations. Report suspected vulnerabilities through the private contact in SECURITY.md, not a public issue.


The Bengal Ecosystem

A 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.


License

MIT License — see LICENSE for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

milo_cli-0.4.2.tar.gz (302.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

milo_cli-0.4.2-py3-none-any.whl (173.6 kB view details)

Uploaded Python 3

File details

Details for the file milo_cli-0.4.2.tar.gz.

File metadata

  • Download URL: milo_cli-0.4.2.tar.gz
  • Upload date:
  • Size: 302.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for milo_cli-0.4.2.tar.gz
Algorithm Hash digest
SHA256 982b5e5e4b42cbbc2211edda6b6e9d37543beb4e31a34b64415e1bc0c3887b93
MD5 6f434b3c7febdf95b1aec6eb4d25882d
BLAKE2b-256 9d71a4ec57b28f8e04cce7c75485b02a31cf4b4dee3bbb4bb5d47f6a2fae78c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for milo_cli-0.4.2.tar.gz:

Publisher: python-publish.yml on lbliii/milo-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file milo_cli-0.4.2-py3-none-any.whl.

File metadata

  • Download URL: milo_cli-0.4.2-py3-none-any.whl
  • Upload date:
  • Size: 173.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for milo_cli-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1b07bd9385fd72fb25a8f53b6896dc456b5b93b3f61a25d69ea6a41f0b633b90
MD5 b8f190cd163c5816457e8fd8cbd9d037
BLAKE2b-256 0ac87b926bad943283d88a5613b1cdc2d100701b9988f6465a367a8b8982b97d

See more details on using hashes here.

Provenance

The following attestation bundles were made for milo_cli-0.4.2-py3-none-any.whl:

Publisher: python-publish.yml on lbliii/milo-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.3

2 files

This release

0.4.2 This release

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page