mii-http
Turn a
.httpspecs file into a real HTTP server — backed by the shell commands you already have.
mii-http reads a small declarative file describing endpoints, parameters, body schemas and the shell command that should run for each route, and brings up a typed, validated, sandboxed HTTP server around it. It's the fastest way to put a clean HTTP face on top of CLI tools, scripts and one-off utilities — without writing a server.
GET /status
Response-Type text/plain
Exec: echo ok
GET /greet
Response-Type text/plain
QUERY name: /[a-zA-Z0-9_]+/
Exec: echo Hello, [%name]
Index
- Why mii-http
- Quick start
- The
.httpformat - CLI
- Editor support
- Architecture
- Security model
- Contributing
- Related
Why mii-http
- Declarative. Endpoints, types, headers, query and body schemas live in a single readable file.
- Typed. Every input is validated against a real type (
int,uuid, ranges, unions, regexes, typed JSON, forms, …) before your command ever runs. - Safe by default. Request values are type-checked and shell-quoted before execution. The
--checkcommand flags risky patterns. - Tiny. One binary. No runtime, no framework to learn — write a spec, point
mii-httpat it.
Quick start
Install the tool using cargo:
or, alternatively...
Build the binary:
Write a spec — hello.http:
VERSION 1
BASE /api
GET /hello
Response-Type text/plain
QUERY name?: /[a-zA-Z0-9_]+/
Exec: echo Hello, [%name]
Validate it, then run it:
Try it out:
A larger, multi-endpoint example lives in examples/sample.http.
The .http format
A spec file has two parts: a setup block with global options, followed by one or more endpoint blocks. Comments start with #.
VERSION 1 # mounts everything under /v1
BASE /named # …prefixed with /named
AUTH Bearer [HEADER API_TOKEN] # bearer-token auth from a header
MAX_BODY_SIZE 1mb
TIMEOUT 30s
GET /users/:user_id:uuid
Response-Type text/plain
Exec: echo user [:user_id]
POST /submit-form
Response-Type text/plain
BODY form {
username: /[a-zA-Z0-9_]+/
age?: int(0..150)
}
Exec: echo username=[$.username] age=[$.age]
Inputs
| Source | Reference in Exec |
|---|---|
| Query param | [%name] / "Hello, {%name}" |
| Path param | [:name] / "user {:name}" |
| Header | [^Name] / "header {^Name}" |
| Body field | [$.field] / "field {$.field}" |
| Whole body | $ (as stdin) |
| User var | [@name] / "var {@name}" |
[ … ]is shell-piece interpolation — use it for any shell word or shell-word group that contains request data, required or optional. Missing optional values drop the whole group.{ … }is string interpolation and is valid only inside quoted strings. Use it when the request data is part of a larger string. Missing optionals become empty strings.$ | …pipes the request body to the command's stdin.- Bare references like
%nameinExecare literal shell text, not interpolation.--checkwarns when they match declared inputs; escape them as\%nameif you really want the literal text.
Types
int, float, boolean, uuid, int(1..10), float(0.0..1.0), unions like red|green|blue, regexes /.../, string, json, typed JSON schemas, form, binary. See specs.md for the authoritative reference and the rules around which types may flow into argv vs. stdin only.
binary is allowed both as a top-level BODY and as a field inside BODY form { ... } (uploaded via multipart/form-data). Outside of stdin, binary values are written to a temp file and the path is interpolated as a quoted shell word.
Streaming responses
Prefix the response type with stream to stream the command's stdout to the client using HTTP chunked transfer instead of buffering the full output:
GET /tail
Response-Type stream text/plain
Exec: tail -F /var/log/syslog
Multi-line Exec
For longer scripts, use the <<< ... >>> form. Each non-empty line becomes its own pipeline statement; indentation is ignored:
POST /complex
Response-Type text/plain
Exec: <<<
echo "step one"
echo "step two for {%name}"
echo "done"
>>>
CLI
mii-http <path> run the server
mii-http --check <path> validate the specs and exit
mii-http --check --json <path> validate and print JSON diagnostics
mii-http --addr 0.0.0.0:8080 <path> bind to a specific address
mii-http -q | --quiet <path> suppress request/error logs
mii-http --dry-run <path> log commands instead of running them
--dry-run is the recommended way to develop a spec: every request prints the exact command line that would have been executed, with all interpolations resolved.
--check --json emits the same validation result as machine-readable diagnostics for editor integrations.
Editor support
A VS Code extension lives in editors/vscode/mii-http. It adds syntax highlighting, mii-http --check --json diagnostics and basic completions for directives, types and Exec references.
Build the binary and the extension before launching the extension host or packaging a VSIX:
The CI workflow publishes downloadable VSIX files through GitHub, not the VS Code Marketplace. When editors/vscode/mii-http/package.json gets a new version on main, .github/workflows/vscode-extension.yml creates a vX.Y.Z-vscode release tag and attaches mii-http-X.Y.Z.vsix to the GitHub Release. Every workflow run also uploads the VSIX as an Actions artifact.
Architecture
┌────────────┐ parse ┌──────────────┐ check ┌──────────────┐
.http ──▶│ parse:: │─────────────▶│ spec AST │────────────▶│ diagnostics │
└────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐ axum ┌──────────────┐
│ server:: │────────────▶│ exec:: │
│ routes & │ │ validate + │
│ validation │ │ shell run │
└──────────────┘ └──────────────┘
Source layout:
- src/parse/ —
chumsky-based parser for the.httpformat. - src/spec.rs, src/value.rs — typed AST and value model.
- src/check.rs, src/diag.rs — semantic checks and
ariadnediagnostics. - src/server.rs —
axumrouting, request validation, header/body/query decoding. - src/exec.rs — shell rendering, interpolation, temp-file materialization and process execution.
Security model
Execruns through/bin/sh; shell syntax written in the spec is trusted shell syntax.- Request values interpolated with
[ … ]or quoted-string{ … }are shell-quoted before execution. - Inputs are validated against their declared types before the command is invoked.
stringand freejsontypes are restricted to stdin only, so unconstrained text cannot become argv.binarybodies andbinaryform fields are written to a temp file and the path is passed as argv (or streamed via stdin).MAX_BODY_SIZE,MAX_QUERY_PARAM_SIZE,MAX_HEADER_SIZEandTIMEOUTare enforced at the request boundary.mii-http --checkhighlights overly-permissive regexes (e.g./.*/) and other risky patterns before they reach production.
Contributing
Issues and PRs are welcome.
- Run
cargo testbefore opening a PR — the suites under tests/ cover the parser, checker, value model and end-to-end execution. - Run
cargo clippy --all-targetsandcargo fmt. - For new spec syntax, update both specs.md and the parser tests; the spec file is the source of truth.
- Keep changes focused — small, reviewable PRs land faster.