xyz-rust 0.4.2

One definition, three interfaces(CLI / HTTP / MCP)— a command toolkit for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# xyz-rust — One definition, three interfaces

[![Rust](https://img.shields.io/badge/Rust-1.88%2B-orange?style=flat&logo=rust&logoColor=white)](https://www.rust-lang.org/)
[![MCP Protocol](https://img.shields.io/badge/MCP-2024%E2%80%932026--07--28-0764e0?style=flat)](https://modelcontextprotocol.io/specification/2026-07-28)
[![Dependencies](https://img.shields.io/badge/core-std_%2B_serde%2Fchrono-2ea44f?style=flat)](#dependency-policy--binary-size)
[![中文](https://img.shields.io/badge/中文-README.zh--CN.md-red?style=flat)](README.zh-CN.md)

The Rust port of [xyz-go](https://github.com/ejfkdev/xyz-go): **define a command once** (argument struct + validation + per-interface details) and one binary automatically speaks three interfaces — **CLI subcommands**, an **HTTP REST service** (with an OpenAPI document), and an **MCP tool server** (official Rust SDK). The library decides the running mode by itself.

```rust
use xyz_rust::errs;
use xyz_rust::{define, CliHints, HTTPHints, MCPHints, XyzArgs};

#[derive(XyzArgs)]
struct AddArgs {
    #[xyz(desc = "username", required, validate = "min=2,max=32", cli = "positional", http = "path")]
    name: String,
    #[xyz(desc = "age", default = "18")]
    age: i32,
}

fn add(_ctx: &xyz_rust::Ctx, in_: &AddArgs) -> errs::Result<String> {
    Ok(format!("{} is {}", in_.name, in_.age))
}

fn main() {
    define("user.add", add)
        .summary("Add a user")
        .cli(CliHints { usage: "add <name>".into(), ..Default::default() })
        .http(HTTPHints { method: "POST".into(), path: "/users/{name}".into(), ..Default::default() })
        .mcp(MCPHints { annotations: vec!["write".into()], ..Default::default() })
        .run(); // register + dispatch + exit — that's the whole program
}
```

```console
$ cargo run -p xyz-example -- user add bob -a 20    # CLI: subcommands + shorthand flags
id          1
name        bob
age         20
token_set   false

$ cargo run -p xyz-example -- user list             # Vec<struct> renders as an aligned table
id  name   age  token_set
--  -----  ---  ---------
1   alice  18   false
2   bob    25   false

$ curl -s -X POST localhost:8080/users/alice -d '{"age":9}'
{"id":1,"name":"alice","age":9,"token_set":false}

$ cargo run -p xyz-example -- math sum --a 1 --b 2  # bare primitive, one line
3

$ cargo run -p xyz-example -- mcp stdio            # MCP: every command becomes a tool (stdio/streamable HTTP)
```

## Features

- **One definition, zero boilerplate**: the whole `main` is a single `define(...) ... .run()` chain — no `std::process::exit`, no explicit registry, no dispatch switch.
- **One pipeline across all three interfaces**: CLI (strings), HTTP (JSON) and MCP arguments are normalized into the same `serde_json` map and flow through the same decode → defaults → validate → handler path, so behavior never drifts.
- **Per-interface fine-tuning**: shorthands, aliases, env fallbacks, binding locations — and **interface-specific default values** (two-tier layering: global attribute default → per-interface override).
- **Envelope-free responses**: primitives print bare, structs align as `key value` columns, `Vec<struct>` becomes a table, `--json` flips to JSON; HTTP answers bare JSON; MCP returns both `structuredContent` and human `textContent`.
- **One error taxonomy**: a single `errs::new(errs::Kind::NotFound, ...)` drives the CLI exit code, the HTTP status code and the MCP error code simultaneously.
- **Dependency hygiene**: the core modules (spec / registry / errors / cli / logx / root) have zero third-party dependencies apart from the serde family and chrono; the only other third-party tree is the official Rust SDK (rmcp), removable wholesale with `--no-default-features` (smallest trimmed build ≈ 0.81M).
- **Protocol versions under control**: MCP speaks the five spec revisions from 2024-11-05 to 2026-07-28; `--versions` pins the subset. Tools also carry a macro-generated `outputSchema` (OpenAPI response schemas share the same source).
- **Production-friendly**: SIGINT/SIGTERM graceful shutdown (`Ctx` flows into handlers), `/healthz` probe, gzip, CLI help with inline `(default …)`/`(env …)`/`(oneof …)` hints, and `completion bash|zsh|fish`.
- **Built-ins out of the box**: credentials (`--bearer`), default address, log level, timeout, TLS, CORS live in `Config` and the `--xyz.*` command-line namespace.

## Install

```bash
cargo add xyz-rust        # the crate name is xyz-rust — on crates.io `xyz` was already taken by an unrelated project
```

Import as `use xyz_rust::...`. The crate is not published yet; until the first release it lives at the repository root (a path dependency). Requires **Rust 1.88+** (MSRV, dictated by the official Rust SDK rmcp 3.1.4's `rust-version`) and edition 2024. A complete runnable showcase lives in [examples/example](examples/example/src/main.rs) (crate `xyz-example`, 11 commands covering the full API surface); [examples/tour](examples/tour/src/main.rs) (`xyz-tour`) walks through the internals, and [examples/clap](examples/clap/src/main.rs) (`xyz-clap`) shows coexistence with an existing clap app. The compile-time derive macros live in the companion crate `xyz-rust-macros`, pulled in automatically by `xyz-rust`.

## One definition: the argument struct

Attributes on the argument struct form the **shared contract** across every interface (wire names, descriptions, defaults, required-ness, enums, validation, secrecy). They map one-to-one onto the Go port's tags:

| Go tag | Rust attribute | Meaning | Example |
|---|---|---|---|
| `json:"name"` | `#[serde(rename = "name")]` (or `#[xyz(name = "name")]`) | Wire field name; `#[xyz(skip)]` (≡ `json:"-"`) excludes the field from binding & schema (still injectable via env/header by Rust field name) | `#[serde(rename = "user_name")]` |
| `desc:"..."` | `#[xyz(desc = "...")]` | Field description (CLI help and JSON Schema alike) | `#[xyz(desc = "username")]` |
| `default:"..."` | `#[xyz(default = "...")]` | Global default, parsed per field type, overridable per interface | `#[xyz(default = "18")]` |
| `required:"true"` | `#[xyz(required)]` | Must be provided | `#[xyz(required)]` |
| `enum:"a,b"` | `#[xyz(enum = "a,b")]` | Allowed values (enforced at decode; written into schema) | `#[xyz(enum = "fast,slow")]` |
| `validate:"..."` | `#[xyz(validate = "...")]` | Validation rules (built-in validator; see the supported set below) | `#[xyz(validate = "min=2,email")]` |
| `secret:"true"` | `#[xyz(secret)]` | Sensitive: redact in help/logs/echoes | `#[xyz(secret)]` |
| `cli:"..."` | `#[xyz(cli = "...")]` | CLI bindings: `shorthand=a`, `positional`, `hidden`, `env=VAR`, `-` | `#[xyz(cli = "shorthand=a,env=TOKEN")]` |
| `http:"query"` | `#[xyz(http = "query")]` | HTTP binding: `query` (**the default when unset**) / `path` / `header` / `form` / `body` | `#[xyz(http = "header")]` |
| `httpName:"X-Key"` | `#[xyz(http_name = "X-Key")]` | HTTP wire-name override (typically a header name) | `#[xyz(http_name = "X-Api-Key")]` |

A minimal serde subset is also honored by the derive macro: `#[serde(rename = "...")]`, `#[serde(rename_all = "...")]` and `#[serde(skip)]`.

`validate` supports: `required`, `omitempty`, `min`, `max`, `len`, `gt`, `gte`, `lt`, `lte`, `oneof`, `email` — a go-playground-compatible subset. Unsupported rules fail at **registration time**, never silently at runtime.

Type support: `String`, `bool`, all integers (`i8`–`i64`, `u8`–`u64`), `f32`/`f64`, `Vec<T>` (Go `[]T`), `Vec<u8>` (Go `[]byte`), `Option<T>` (Go `*T`), nested structs, `chrono::DateTime<Utc>` (Go `time.Time`), `std::time::Duration` (Go `time.Duration`) and named scalar newtypes via `#[derive(XyzField)]`. All wired formats accept strings (CLI), JSON shapes (HTTP body) and raw JSON (MCP) with lossless conversion checks (`3.7` never silently becomes `int(3)`). There is no runtime reflection: what reflect does on the Go side is generated at compile time by the derive macros.

Named scalar newtypes, and nested argument structs:

```rust
#[derive(Clone, Copy, Debug, PartialEq, XyzField)]
struct Port(i32);

#[derive(XyzArgs)]
struct PortArgs {
    #[xyz(desc = "listen port", default = "8080", cli = "shorthand=p")]
    port: Port,
}
```

## Per-interface configuration & default layering

`.cli()/.http()/.mcp()` on the `define` chain configure command-level details and, via the `fields` map, override attributes per field (both layers merge; a zero-value hint field means "keep the attribute"). Fields are keyed by JSON name or Rust name:

```rust
.cli(CliHints {
    usage:   "add <name>".into(),             // usage line in help
    aliases: vec!["ua".into(), "new".into()], // aliases equal subcommand names
    fields: HashMap::from([
        ("age".to_string(),   CliFieldHint { shorthand: Some("a".into()), ..Default::default() }),
        ("mode".to_string(),  CliFieldHint { default: Some("fast".into()), ..Default::default() }), // CLI-only default
        ("token".to_string(), CliFieldHint { env_var: Some("APP_TOKEN".into()), ..Default::default() }), // env fallback
    ]),
    ..Default::default()
})
```

Hint structs: `CliHints { usage, aliases, hidden, fields }` with `CliFieldHint { shorthand, positional, hidden, skip, env_var, default }`; `HTTPHints { method, path, timeout, fields }` with `HTTPFieldHint { location, name, default }`; `MCPHints { annotations, fields }` with `MCPFieldHint { default }`. A shorthand must be a single character (registration error otherwise).

**Default precedence for one field** (CLI as the example):

```
explicit flag > env fallback > interface default > global attribute default (Invoke fills it) > zero value
```

Mechanism: each frontend injects its own overrides (`Entry.cli_defaults()/http_defaults()/mcp_defaults()`) before calling `Entry.invoke`, which then applies global attribute defaults — one pipeline, drift-free. MCP's overrides also replace `default` in `inputSchema` (the schema is MCP's contract).

## Three modes

```
xyz-example [command] [args]    CLI: subcommand tree, shorthands/aliases/-h/-v/--json/positionals/env
xyz-example serve --addr :8080  HTTP: REST routes + /openapi.json + /mcp on the same port
xyz-example mcp stdio|http      MCP: official Rust SDK, two transports (--versions pins revisions)
xyz-example completion bash|zsh|fish   Built-in shell completion scripts
```

## Command channels, daemons & composable dispatch

- **Per-command channel switches**: `CliHints { skip }`, `HTTPHints { skip }`,
  `MCPHints { skip }` remove the command from the marked channel only
  (no subcommand node / no route / no tool); CLI `skip` also drops aliases
  and completion words.
- **Daemon commands**: `CliHints { daemon: true }` declares a long-running
  lifecycle — the handler blocks until the context is cancelled, the CLI
  exits 0 gracefully, the return value is not rendered and the command is
  implicitly CLI-only.

  ```rust
  define("watch", watch).cli(CliHints { daemon: true, ..Default::default() });
  fn watch(ctx: &Ctx, _: &Args) -> errs::Result<()> { while !ctx.cancelled() { sleep(Duration::from_millis(50)); } Ok(()) }
  ```

- **Channel defaults**: `--default k=v` (repeatable; comma-pairs) —
  injected at serve/mcp startup, fills absent request/tool keys only;
  precedence: explicit > env > interface default > **channel default** >
  global default > zero. Any unrecognised `--key value` in serve/mcp is a
  shorthand for it: `gs serve --index ./wiki` equals
  `--default index=./wiki`. Code side: `Config.channel_defaults`.

- **Composable dispatch**: `let (code, handled) = xyz_rust::try_run(&reg, args)` —
  full dispatch pipeline, but an unknown CLI top word returns `(0, false)`
  silently so the host can route its own commands; `try_run_config` takes a
  custom `Config`.

**Custom help blocks.** Free text blocks, raw multi-line, printed verbatim
(trailing newlines normalized to one); empty = no-op:

```rust
// Overview top/bottom (Config) — app name, description, version, repo…
.run_config(Config {
    help_before: "udf v1.0.0 — inspect disk images\nhttps://github.com/example/udf".into(),
    help_after: "More examples: https://github.com/example/udf#examples".into(),
    ..Default::default()
});
// Per-command -h (CliHints):
define("extract", extract)
    .cli(CliHints { before: "extract — 解包镜像".into(), after: "仓库: https://…".into(), ..Default::default() })
```

## Interface language

The interface language of all built-in text (overview, help labels, usage
errors, diagnostics) is picked in this order: `--xyz.lang=en|zh-CN` flag >
`Config.lang` > `LANG`/`LC_ALL` environment (a lowercase `zh` prefix picks
Chinese) > **English (default)**. Both languages ship in the library.
Configure more multilingual content with `Config.translations`
(language → (message key → text)); key names and English wording are the
xyz-spec §15.5 canonical catalog:

```rust
.run_config(Config {
    lang: "zh-CN".into(), // or: LANG=zh_CN ./app; or: --xyz.lang=zh-CN
    translations: HashMap::from([(
        "en".to_string(),
        HashMap::from([("help.help_flag".to_string(), "show this help".to_string())]),
    )]),
    ..Default::default()
})
```

Error taxonomy messages stay English; user content (summaries,
descriptions, help blocks) is never translated.

**Default subcommand** (CLI only): a command marked `default: true` in its
`CliHints` becomes the default child of its parent node — when the first
argument matches no registered segment (and is not a flag), the whole
argument list is forwarded to it unchanged:

```rust
define("extract", extract)
    .cli(CliHints { usage: "extract <image.tar>", default: true, ..Default::default() })
    .run();
// udf ./image.tar  ⟺  udf extract ./image.tar
// one default per parent node; flags/explicit paths/-h/-v are unaffected
```

**CLI** (std + serde; no clap in the shipped frontend — `examples/clap` shows how to bring your own): registry name `user.add` becomes the two-level subcommand `user add`; `-h/--help` prints per-command help (with inline `(default …)`/`(env …)`/`(oneof …)` hints), -v/--version` prints the *application* version (default `dev`, overridable with `set_version("v1.2.3")` — Rust has no `-ldflags -X` equivalent; the xyz SDK's own version is reported separately as `X-XYZ-Version`/`_meta.xyz.sdk_version`, spec §11.6/§12.8).

**HTTP** (axum): routes come straight from `HTTPHints { method, path }` (`{name}` is a path parameter bound to a field with `http = "path"`); fields without an `http:` attribute bind from the query string by default, a JSON body merges as the argument base; supported methods are GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS (others are a registration error); the error taxonomy maps to status codes (400/401/403/404/409/499/500/503) with `{"error":"..."}` bodies; `GET /openapi.json` serves an OpenAPI 3 document from the same `InputSchema` (response schemas included); `GET /healthz` probes liveness and gzip is answered transparently. Commands without HTTP hints are not routed.

**MCP** (official Rust SDK, rmcp): commands become tools; `tools/list` serves the macro-generated `inputSchema` **and `outputSchema`**; success returns dual content — `structuredContent` (bare JSON) + `textContent` (the CLI-style rendering); failures return `isError: true` with the classified message. Supported spec revisions: `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, `2026-07-28` (the newest is the handshake-free `server/discover` era); `mcp http --versions 2025-06-18,2026-07-28` pins the subset. Built-in constraints: streamable HTTP serves 2026-07-28 only with `--stateless` (SEP-2567); the SDK's streamable-HTTP server allows only loopback `Host` headers by default (its DNS-rebinding protection). `mcp sse` answers a clear error and exits 2 — see [the differences section](#differences-from-the-go-implementation).

A larger real command — attributes, three-layer defaults, error classification, named scalar, `Vec<u8>` and header/env injection all in one definition (taken from [examples/example/src/main.rs](examples/example/src/main.rs)):

```rust
#[derive(XyzArgs)]
struct AddUserArgs {
    #[xyz(desc = "用户名称", required, validate = "min=2,max=32", cli = "positional", http = "path")]
    name: String,
    #[xyz(desc = "邮箱", validate = "omitempty,email")]
    email: String,
    #[xyz(desc = "年龄", default = "18", http = "query")]
    age: i32,
    #[xyz(desc = "部署模式", enum = "fast,slow", http = "query")]
    mode: String,
    #[xyz(desc = "标签", http = "query")]
    tags: Vec<String>,
    #[xyz(skip, secret, desc = "API 令牌(仅 env 注入,不进 schema)")]
    token: String,
    #[xyz(desc = "打印详细信息")]
    verbose: bool,
}

#[derive(Serialize, XyzOutput)]
struct AddUserResp {
    #[serde(rename = "id")]
    id: i64,
    #[serde(rename = "name")]
    name: String,
    #[serde(rename = "age")]
    age: i32,
    #[serde(rename = "token_set")]
    token_set: bool,
}

fn add_user(_ctx: &xyz_rust::Ctx, in_: &AddUserArgs) -> errs::Result<AddUserResp> {
    if in_.name == "missing" {
        return Err(errs::new(errs::Kind::NotFound, "no such user"));
    }
    // ...
}
```

Handlers have the shape `fn(&Ctx, &Args) -> Result<Resp, E>` where `E` is any `std::error::Error` (an existing classification is preserved along the error chain; anything unclassified falls back to `internal`) and `Resp` is `Serialize`. `define("name", handler)` fully infers both types — no explicit generic arguments needed.

### Response rendering (envelope-free)

| Return type | CLI | `--json` / HTTP / MCP structuredContent |
|---|---|---|
| `None` / null (or an empty `Vec<T>`) | nothing | JSON `null` (empty array → `[]`) |
| `string` / `bool` / numbers | bare value, one line | bare JSON value |
| `DateTime<Utc>` | RFC3339 | RFC3339 string |
| `Vec<scalars>` | one per line | JSON array |
| `struct` | aligned `key  value` columns | JSON object |
| `Vec<struct>` | aligned table (header + rule) | JSON array |
| `map` | key/value pairs in (insertion) order | JSON object |
| `xyz_rust::Blocks` (spec §12.7) | text blocks inline; binary blocks written to temp files, paths printed | block envelope JSON (`{"content":[…]}`); MCP carries the blocks verbatim as `Content` |

Structs and maps render through the same `serde_json::Value` path (declaration order preserved via `preserve_order`) — see the [differences section](#differences-from-the-go-implementation).

### Tagged unions (`oneOf`, spec §4.7)

Enum argument fields with `#[serde(tag = "…")]` adjacent tagging become
`oneOf` inputSchema branches — one per variant, discriminator pinned with a
`const`:

```rust
#[derive(xyz_rust::XyzArgs, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
enum Target {
    Element { state_id: i64, index: i64 },
    Coordinate { x: i64, y: i64 },
}
```

Decoding selects exactly one branch (zero or multiple matches are
`invalid_input`). Untagged or internally-tagged enums are rejected at
compile time. The CLI frontend rejects union fields at registration for
now (same stance as nested structs).

## Error taxonomy

Handlers return `errs::Result<T>` (`xyz_rust::errs`) and one classification drives the three interfaces:

| Kind | HTTP | CLI exit | JSON-RPC / MCP |
|---|---|---|---|
| `invalid_input` (auto for decode/validation) | 400 | 2 | -32602 |
| `unauthorized` | 401 | 1 | -32010 |
| `forbidden` | 403 | 1 | -32011 |
| `not_found` | 404 | 1 | -32001 |
| `conflict` | 409 | 1 | -32009 |
| `canceled` | 499 | 1 | -32012 |
| `unavailable` | 503 | 1 | -32603 |
| unclassified (falls back to `internal`) | 500 | 1 | -32603 |

```rust
return Err(errs::new(errs::Kind::NotFound, format!("user {:?} not found", in_.name)));
// or:      errs::errorf!(errs::Kind::NotFound, "user {} not found", in_.name)
```

## Configuration: mode words & capability switches

All dispatch configuration lives in `xyz_rust::Config`; the zero value (derived `Default`) means defaults. Chain style uses `.configure(cfg)`, functional style uses `main_config` / `run_config`:

```rust
xyz_rust::main_config(xyz_rust::Config {
    // serve/mcp/help are reserved words; renaming releases the old words for use as commands
    modes: xyz_rust::ModeWords { serve: "httpd".into(), mcp: "protocol".into(), help: "assist".into() },
    // disabling a channel removes only its runtime path: mcp/serve/help/-v always survive
    capabilities: xyz_rust::Capabilities { no_cli: true, ..Default::default() },
    ..Default::default()
})
```

With `no_cli`, user subcommands disappear but `mcp stdio`, `serve`, `help` and `-v` keep working (the overview annotates "disabled" and hides the command table); the disabled channel's `.cli()/.http()/.mcp()` configuration still compiles and runs — it simply stops being consumed by a frontend that is switched off. A registry with zero registered commands is a silent no-op (exit 0). Capability switches are runtime knobs, independent of the compile-time Cargo features.

## Built-in configuration (`--xyz.*` and `Config` fields)

The library's own settings live in `Config` fields and the `--xyz.*` command-line namespace; precedence: **mode-local flag > global `--xyz.*` / code Config > library defaults**. Inside `serve`/`mcp` the mode word _is_ the namespace, so built-ins use bare names (`--bearer`, `--addr`, `--cors`, …), and the prefixed `--xyz.*` forms work anywhere before the `--` terminator. Renaming the mode words migrates the namespace with them.

| Parameter | Code field | Meaning |
|---|---|---|
| `--bearer=tok1,tok2` (or `--bearer tok`) | `Config.bearer_tokens` | Bearer verification for **serve REST** and **MCP http**: `Authorization: Bearer <tok>` must hit one of the tokens, else 401 + `{"error":"unauthorized"}`; empty = no auth. stdio is a local process and unaffected (a note is logged) |
| `--addr=:8080` | `Config.addr` | Default listen address for serve and mcp http |
| `--log-level=debug` (or `--xyz.log-level`) | `Config.log_level` | Library diagnostics to stderr (`xyz[level]:` prefix): `debug`/`info`/`warn`/`error`, default `info`. Command results and usage errors are unaffected |
| `--timeout=45s` | `Config.timeout` | serve per-request timeout (a `tower-http` TimeoutLayer answering 408); 0 = no timeout layer |
| `--tls-cert/--tls-key` | `Config.cert_file`/`key_file` | serve switches to TLS when both are given |
| `--cors=https://a,b` (or `*`) | `Config.cors_origins` | CORS allowlist for serve and MCP http; OPTIONS preflights answer before auth (browser preflights carry no credentials) |
| `--session-timeout=30m` (mcp only) | `mcp::Options.session_timeout` | Idle-session expiry for streamable HTTP (the SDK's session keep-alive) |

```bash
xyz-example serve --bearer=s3cret                    # REST/openapi/mcp all require credentials
xyz-example mcp http --addr :9000 --bearer a,b       # standalone MCP, same scheme
xyz-example mcp http --xyz.bearer a,b                # prefixed form is equivalent
```

MCP's own flags: `--versions/--name/--server-version/--addr/--json-response/--stateless/--session-timeout/--bearer/--cors`. CLI owns `--json`, `-h`, `-v`.

Waiting list (kept out to stay minimal — each lands in one iteration when asked): log file rotation, basic rate limiting.

## Embedding & multiple registries

Besides the singleton chain, the parameterized pure functions (which return the exit code without exiting) serve embedding, tests and multi-registry setups:

```rust
let reg = xyz_rust::Registry::new();
xyz_rust::spec::command::Command::new("user.add", add_user)
    .summary("...")
    .register(&reg)?;                                   // explicit path
std::process::exit(xyz_rust::run(&reg, args));         // for deferred cleanup in main

let srv = xyz_rust::mcp::server(&reg, xyz_rust::mcp::Options {
    versions: vec![xyz_rust::mcp::PROTOCOL_V2026_07_28.into()],
    ..Default::default()
}, ctx)?;
let router = xyz_rust::httpapi::router(&reg, ctx)?;    // mount all HTTP routes (healthz & openapi included)
let one = xyz_rust::httpapi::handler_for(entry);       // mount one command on any axum Router (entry: Arc<Entry>)
let mut app = xyz_rust::cli::App::new_with_options(&reg, xyz_rust::cli::Options::default())?;
// app.set_output(Some(out_writer), Some(err_writer)); // redirect output streams
app.use_mw(Box::new(mw));                              // Execute middleware: rewrite args, short-circuit, wrap next()

xyz_rust::mcp::run_context(&ctx, &reg, args);
xyz_rust::cli::run_context(&ctx, &reg, args, xyz_rust::cli::Options::default());
```

The dispatch-level entry points are `main_entry(&[&dyn Definable])` / `main_config(cfg)` (which exit) and `run(&reg, args)` / `run_config(&reg, args, cfg)` (which return the exit code).

Already on clap / axum? See the [migration guide](docs/adapters.md) with a runnable example in [`examples/clap`](examples/clap) — three coexistence levels (tier A: replace the frontend and keep the `Entry.invoke` spine; tier B: mount per-command handlers via `httpapi::handler_for` / `cli::App`; tier C: reuse parts like `httpapi::middleware` or `cli::render`), all without touching the core.

## Differences from the Go implementation

1. **No SSE transport.** The official Rust SDK (rmcp 3.1.4) removed the HTTP+SSE transport with the 2026-07-28 revision, so `mcp sse` is parsed but answered with a clear error and exit code 2. Available transports: `stdio` and streamable `http`.
2. **serde enters the core.** Rust's std has no JSON, so the core (spec / registry / errors / cli / logx / root) depends on serde + serde_json — the "missing standard library" — and chrono (time types), and nothing else. `httpapi` sits on axum (the de-facto standard HTTP stack in Rust, the same stack the official rmcp streamable-HTTP examples use), and `mcp` on the official Rust SDK `rmcp` pinned to `=3.1.4` — the only other direct third-party dependency tree. `http` and `mcp` share the tokio+axum cluster behind the internal `http-stack` feature.
3. **No reflection.** What Go derives at runtime from struct tags is generated at compile time by the derive macros. The attribute vocabulary is `#[xyz(desc="...", name="w", required, secret, skip, validate="min=2,email", default="18", enum="a,b", cli="positional"/"shorthand=a,env=X"/"hidden"/"-", http="query|path|header|form|body", http_name="X-Key")]` — one-to-one with the Go tags, plus the serde `rename` fallback and `rename_all` support. Named scalar newtypes derive `XyzField`; result structs derive `Serialize` + `XyzOutput` with wire names following serde conventions, or no derive at all — a `XyzArgs` input struct automatically provides `XyzSchema`.
4. **Handler shape.** `fn(handler(_: &Ctx, _: &Args) -> Result<Resp, E>)` with `E: std::error::Error` (classification on the error chain is preserved) and `R: Serialize`. `define("name", h)` is fully inferred — no Go-style explicit `Define[T, R]` generics.
5. **Result rendering.** Structs and maps are the same shape: both become a `serde_json::Value` after serialization, and `preserve_order` keeps declaration order (Go sorts map keys); a `Vec<u8>` result type gets an array-shaped output schema (its input side is still `string`); `std::time::Duration` has no negative-value support (Rust semantics); `oneof` has no `%v` form for structs.
6. **Version injection.** Call `set_version("v1.2.3")` at release time — Rust has no `-ldflags -X`; the default *application* version is `dev` (xyz-spec §12.6). The SDK's own version (`version::SDK_VERSION`, crate version) is reported apart on HTTP `X-XYZ-Version` and `_meta.xyz.sdk_version`.
7. **HTTP semantics.** Gzip via `tower-http` (compresses any response size and handles `Accept-Encoding` q-values; the Go port only checks the header); per-request timeout via `TimeoutLayer` answering **408** (not 504); request-level cancellation: a client disconnect does not interrupt the running handler; the standard header timeout is not configured (a non-zero `Config.timeout` is the only timeout layer).
8. **MCP differences.** `--versions` accepts the same full set as Go — `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, `2026-07-28` (newest) — but version pinning is handed to the SDK's negotiation via `supported_protocol_versions`; streamable HTTP serves 2026-07-28 only with `--stateless`; and the SDK's streamable-HTTP server allows only loopback `Host` headers by default (rmcp's DNS-rebinding protection).
9. **Capability switches stay available at runtime**: `Capabilities { no_cli, no_mcp, no_http }` are `Config` fields, independent of the Cargo features.

All of the above are filed, with spec-section references, in the spec repository's [deviations register](https://github.com/ejfkdev/xyz-spec/blob/main/deviations.md) (`D-rust-01` … `D-rust-13`) and attested in [CONFORMANCE.md](CONFORMANCE.md).

## Dependency policy & binary size

- **Serde-family-only core**: the core modules have zero third-party dependencies beyond serde, serde_json and chrono; the CLI frontend is std + serde (ctrlc only for signals in non-HTTP builds); `httpapi` needs the axum cluster (`http-stack`: tokio, axum, axum-server, tower-http, rustls); the only third-party tree on top is the official `rmcp` (pinned `=3.1.4`, shared by `mcp`).
- **Trim any channel via Cargo features** (any combination):

| Features | Channels | Size (macOS arm64, `cargo build --release` + strip, `xyz-example`) |
|---|---|---|
| default (`cli,http,mcp`) | CLI + HTTP + MCP | 6.6M |
| `--no-default-features --features cli,http` | CLI + HTTP | 3.4M |
| `--no-default-features --features http,mcp` | HTTP + MCP | 6.5M |
| `--no-default-features --features cli,mcp` | CLI + MCP | 5.7M |
| `--no-default-features --features cli` | CLI only | 0.95M |
| `--no-default-features` | embedding only | 0.81M |

```bash
cargo build --release
cargo build --release --no-default-features --features cli   # CLI only
cargo build --release --no-default-features                  # embedding only
strip target/release/xyz-example
```

For reference, the same showcase in Go measures 8.3M / 6.5M / 8.3M / 7.9M / 4.1M / 3.9M — Rust comes out smaller because there is no Go runtime floor: every Go binary pays ≈1.1M just for the runtime, while a Rust binary starts from the library itself. The trimmed channel disappears as a block; invoking a channel that was not compiled in answers a clear error and exits 1.

## Module layout

| Module | Responsibility | Dependencies |
|---|---|---|
| `src/lib.rs` (root crate `xyz_rust`) | fluent `define` chain, mode dispatch, capability switches, built-in parameters (`--xyz.*`), version | std + serde |
| `src/spec` | generic definition (derive-macro generated), decode pipeline, validation, JSON Schema | std + serde/serde_json + chrono |
| `src/registry` | command table: registration, conflict checks, the process-level singleton | std |
| `src/errors` | error taxonomy and three-interface mappings | std |
| `src/cli` | CLI frontend: command tree, flag parsing, help, rendering, completion | std + serde (+ctrlc) |
| `src/httpapi` | HTTP frontend: routing, binding, middleware, openapi.json | axum cluster |
| `src/logx` | leveled diagnostics to stderr (`xyz[level]:` prefix) | std |
| `src/mcp` | MCP frontend: stdio/streamable HTTP transports, protocol versions | rmcp |
| `src/config`, `src/builtins`, `src/dispatch`, `src/ctx`, `src/overview`, `src/version`, `src/builder` | configuration model, `--xyz.*` parsing, root dispatcher, cancellation context, overview rendering, version string, fluent chain | std |
| `xyz-rust-macros/` | derive macros `XyzArgs` / `XyzField` / `XyzOutput` | syn + quote |
| `examples/example`, `examples/tour`, `examples/clap` | showcase, internal tour, clap coexistence | — |

## Design principles

1. **Zero boilerplate by default, explicit paths always available**: the singleton chain is the main entry; registry-parameterized pure functions are the backdoor for embedding and tests — both share one dispatch and invoke pipeline.
2. **Fail at registration**: bad names/types/attributes/route conflicts/unsupported validation rules surface at startup, never at runtime.
3. **Configuration is data; frontends are consumers**: `.cli()/.http()/.mcp()` only store metadata, so Cargo features and capability switches never break compilation.
4. **Envelope-free, natural forms**: machines read JSON, humans read tables, from one return value.
5. **The shell is uncuttable**: `help`/`-v`/mode words/`completion` work in every combination.
6. **The mode word is the namespace**: built-ins in `serve`/`mcp` use bare names; `--xyz.*` works globally; library messages never hardcode mode words — renaming migrates everything.
7. **Cancellation flows everywhere**: the dispatcher owns a signal `Ctx` that reaches CLI/HTTP/MCP handlers; HTTP and MCP servers drain in-flight requests (graceful window) before exiting.

## Development

```bash
cargo test                                  # unit/integration tests across the library (default features)
cargo test --no-default-features --features cli,http   # every trimmed feature combination also passes (6 combos)
cargo build --workspace                     # all crates incl. macros + examples
cargo run -p xyz-example                    # full showcase
cargo run -p xyz-tour                       # internal-walkthrough tour
cargo run -p xyz-clap -- add bob            # clap coexistence example
```

Output contract: command results go to stdout; errors and diagnostics go to stderr (diagnostics carry the `xyz[level]:` prefix, level via `--log-level`); under the stdio transport stdout is reserved for protocol frames. The `Ctx` passed to handlers is canceled on SIGINT/SIGTERM (the HTTP/MCP servers drain in-flight requests first).

### Release

```bash
cargo package                              # sanity check before publishing
git tag v0.1.0 && git push origin v0.1.0   # consumers: cargo add xyz-rust (first publish: cargo publish)
```

> Also available: [中文文档](README.zh-CN.md)

Release process: see [RELEASING.md](RELEASING.md) — every tag carries a full changelog (the annotated message *is* the release note).