xyz-rust 0.4.13

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
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# xyz-rust — One definition, three interfaces

[![crates.io](https://img.shields.io/crates/v/xyz-rust.svg?style=flat)](https://crates.io/crates/xyz-rust)
[![docs.rs](https://img.shields.io/docsrs/xyz-rust?style=flat)](https://docs.rs/xyz-rust)
[![Rust](https://img.shields.io/badge/Rust-1.89%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 the human `textContent` plus `structuredContent` for object results (MCP specifies an object there — §12.5).
- **One error taxonomy — with rich context**: a single `errs::new(errs::Kind::NotFound, ...)` drives the CLI exit code, the HTTP status code and the MCP error code simultaneously; `with_code/with_detail/with_status` attach the §8.5 layers and every channel shares one error body (`{error, kind, code, detail}`, spec §8.6).
- **TTY-aware output**: `--format auto` (the default) renders `text` on an interactive terminal and `jsonl` when piped; `--format text|json|jsonl|markdown` and the `--json` alias pick explicitly, with the precedence bare flag > `--xyz.format` > `CliHints.format` > `Config.Format` > auto (spec §10.7).
- **Four modes, namespaced**: `serve` (REST + `/mcp`), `http` (REST only), `mcp`, `help` — each also reachable as the always-available, help-hidden `xyz.<word>`; user commands whose top-level segment equals a mode word shadow the bare form (spec §13.1).
- **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.95M).
- **Protocol versions under control**: MCP speaks the five spec revisions from 2024-11-05 to 2026-07-28; `--versions` pins the subset. Tools carry a macro-generated `outputSchema` for object-typed results — and only for negotiated revisions 2025-06-18 and later (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::...`. Published on crates.io as `xyz-rust` (current 0.4.x line); the bundled examples run from this repository as path dependencies. Requires **Rust 1.89+** (MSRV: the official Rust SDK rmcp 3.6.0 brings `uuid` 1.28, whose floor is 1.89) and edition 2024. A complete runnable showcase lives in [examples/example](examples/example/src/main.rs) (crate `xyz-example`, 14 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).

## Modes

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

`help` takes arguments (dotted or spaced command paths both work: `help user.add`
≡ `help user add`), and `serve -h` / `http -h` / `mcp -h` print the mode's own
help and exit without starting a server (spec §10.4/§13.2). Every mode word is
also reachable through its always-available, help-hidden `xyz.<word>` form; a
user command whose top-level segment equals a mode word *shadows* the bare
form — the bare word routes to the user command and the built-in stays
reachable via `xyz.<word>`, so names like `serve.*` are no longer reserved
(spec §13.1).

## 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).

Output formats: `--format auto|text|json|jsonl|markdown` (with `--json` as the
`json` alias) — `auto` is the default and resolves at render time from the TTY
(interactive → `text`, piped/redirected → `jsonl`; both halves configurable via
`Config.FormatInteractive`/`FormatPiped`, forceable for tests/embedding via
`cli::Options { interactive: Some(..) }`). Precedence: bare `--format`/`--json`
(when not shadowed by a command's own flag) > `--xyz.format` > `CliHints.format`
> `Config.Format` > `auto`. In the machine formats (`json`/`jsonl`) a command
error is written to stderr as the shared §8.6 error object.

**HTTP** (axum): a command with an `HTTPHints { path }` is routed — a `method` value pins one method or a comma-separated list (case-normalized), and **an empty method registers both GET and POST** (GET binds query, POST binds a JSON/form body plus query, same handler; spec §11.1); `{name}` is a path parameter bound to a field with `http = "path"`; fields without an `http:` attribute bind from the query string by default (the §4.6 `http_name` override applies to query and form lookups too), a JSON body merges as the argument base; **unknown query parameters are rejected** with 400 `invalid_input` naming the parameter and the route's accepted set (`--xyz.allow-unknown-query` relaxes this) — silently ignoring them would let a typo or a removed parameter look effective (spec §11.2); 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 the shared §8.6 error body (`{"error":"...","kind":"...","code":"...","detail":{...}}` — flat `error` stays byte-compatible; an explicit `with_status` overrides the Kind-derived code; unmatched routes answer a localized JSON 404); per-request interface language comes from `Accept-Language` (q-ordered, `zh*`→zh-CN/`en*`→en, readable by handlers via `language_from_ctx`, spec §11.7); every response carries server-context headers (`X-App-Name`, `X-App-Version`, `X-XYZ-Version`; per-route `X-XYZ-Command`/`X-XYZ-Duration-Ms`) plus user-defined static headers (`--xyz.header k=v`; suppress the automatic set with `--xyz.no-server-headers`); `GET /openapi.json` serves a rich OpenAPI 3 document from the same `InputSchema`: per operation a summary + description, one `parameters` entry per path/query/header field (wire name, location, correct `required` — path params always —, `desc`, and the rich per-field schema with enum/default/format, the same source the MCP `inputSchema` uses; fields with no explicit `http` location appear as `query` parameters), a requestBody for POST/PUT/PATCH, one operation per registered method (the GET+POST default yields both), and `info.title`/`version` from the application identity (§11.6 values); `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 — for object-typed results, and only when the negotiated revision is 2025-06-18 or later — an `outputSchema` (a non-object result advertises none: a declared schema owes a conforming `structuredContent`, which an array or scalar can never provide); success returns the CLI-style `textContent` plus `structuredContent` when the result value is a JSON object; failures return `isError: true` with the classified message. `initialize` declares the tools capability (`"capabilities":{"tools":{}}`; `listChanged` is not claimed — the tool set is fixed and no change notifications are emitted), so clients that negotiate by capability send `tools/list`. 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 (the pin also bounds the negotiated handshake version, and below 2025-06-18 it drops `outputSchema`/`structuredContent` entirely — the §12.5 revision gate); `--json-response` only takes effect together with `--stateless` (rmcp's legacy session mode always answers `text/event-stream`, and the server logs a warning when the combination is missing). Tool metadata is rich and per-command overridable (spec §12.4): the description merge is the default (`MCPHints.description` overrides), `MCPHints.title` (or a `title:…` annotation) rides `annotations.title`, `MCPHints.meta` merges into the tool's `_meta`, and a field's description can be overridden with `MCPFieldHint.description`. Every result carries `_meta.xyz` server context — `app_name`/`app_version`/`sdk_version`/`command`/`duration_ms` (+ `headers` when configured), and on failures an `error` sub-object with `kind`/`code`/`detail` — while `serverInfo` reports the *application* identity (basename / built version unless `Config.name`/`Config.version` override; spec §12.6/§12.8). 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 body |
|---|---|---|
| `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 |

MCP: the result travels as `textContent` (the CLI-rendered form above); `structuredContent` additionally carries the JSON object for `struct` results — and only for negotiated revisions ≥ 2025-06-18 (§12.5, conformance A.64). Array and scalar values are never placed there.
| `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 env = xyz_rust::env();                             // §14 item 7: { language, interactive, no_color }
let lang = xyz_rust::language_from_ctx(ctx);           // per-request language (§11.7)
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.6.0) 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.6.0` — 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; in `--format markdown` a struct and a map are indistinguishable after serialization, so both use the `| Field | Value |` header (Go uses `| Key | Value |` for maps).
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). MCP structured output is gated the same way as the spec now requires (object-only `outputSchema`, object-only `structuredContent`, revisions ≥ 2025-06-18) whereas Go attaches `outputSchema` for every schematisable result type and always fills `structuredContent` with the bare value — filed as `D-go-04`. Verified upstream limitations in rmcp 3.6.0 (reported from ddc's protocol testing; no xyz-side wrap can fix them without forking the transport): a `tools/call` without `params` answers `-32601` instead of `-32602`; a malformed JSON body answers HTTP 415 instead of 400; deleting a session twice answers 202 both times; a request with `id: null` answers `-32600` rather than echoing it. Fixed by the 3.6.0 upgrade: `server/discover` without `params` now answers `-32602` (was `-32601`), float-form ids are read as integers, and non-JSON stdio lines are consistently ignored. The `initialize` capability object differs in scope by SDK default: Go's go-sdk auto-emits `{"logging":{},"tools":{"listChanged":true}}`, while xyz-rust declares the truthful minimum `{"tools":{}}` (no list-changed notifications are emitted).
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; TLS runs on the `ring` provider — axum-server is built with `tls-rustls-no-provider` and `ring` is installed explicitly at startup, so an embedding graph that also carries `aws-lc-rs` stays deterministic); the only third-party tree on top is the official `rmcp` (pinned `=3.6.0`, 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 | 7.9M |
| `--no-default-features --features cli,http` | CLI + HTTP | 4.3M |
| `--no-default-features --features http,mcp` | HTTP + MCP | 7.8M |
| `--no-default-features --features cli,mcp` | CLI + MCP | 6.4M |
| `--no-default-features --features cli` | CLI only | 1.1M |
| `--no-default-features` | embedding only | 0.95M |

```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 9.4M / 7.6M / 9.4M / 8.8M / 5.1M / 4.4M (`go build -ldflags="-s -w"` + strip) — 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).