Skip to content

lbliii/milo-cli

Repository files navigation

ᗣᗣ Milo

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

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.

Requires Python 3.14+


Quick Start

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
Annotated[str, MinLen(1)] Schema constraints

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

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

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.

ᓚᘏᗢ 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.

About

ᗣᗣ Milo

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages