argsbarg 7.0.8 → 7.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [7.0.8] - 2026-09-15
10
+ ## [7.0.9] - 2026-09-16
11
+
12
+ ### Added
13
+
14
+ - **`kind: "document"` leaf commands with YAML and JSON support** — introduced `kind: "document"` as the primary naming for structured payload leaves, while retaining `kind: "json"` and `isJsonLeaf` as fully backward-compatible aliases. Both `"document"` and `"json"` leaves now accept YAML input in addition to JSON via command positional arguments and piped stdin.
15
+ - **YAML request body support in HTTP server** — the HTTP API server now accepts YAML request bodies in addition to JSON for structured document endpoints.
16
+ - **`isDocumentLeaf` and `parseDocumentText` exports** — exported `isDocumentLeaf` type guard and `parseDocumentText` utility from framework root and CLI exports.
17
+
18
+ ### Changed
19
+
20
+ - **Help rendering for document leaves** — usage lines for `kind: "document"` leaves render `[DOCUMENT]` (retaining `[JSON]` for legacy `kind: "json"` leaves) and describe inputs as `"Pass a JSON or YAML document as an argument or pipe to stdin."`
11
21
 
12
22
  ### Added
13
23
 
@@ -992,8 +1002,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
992
1002
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
993
1003
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
994
1004
 
995
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.8...HEAD
996
- [7.0.8]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.8
1005
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.9...HEAD
1006
+ [7.0.9]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.9
997
1007
  [7.0.7]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.7
998
1008
  [7.0.6]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.6
999
1009
  [7.0.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.5
@@ -108,7 +108,7 @@ Use root **`notes`** for cross-cutting hints shown in help (install commands, do
108
108
  Descriptions and schemas are copied into MCP tools and HTTP OpenAPI — optimize for smaller, clearer agent payloads:
109
109
 
110
110
  - **Declare options on the leaf command** that uses them — routing groups cannot declare options (program root may). Wire schemas (MCP, OpenAPI) expose leaf-local options only.
111
- - Prefer **`kind: "json"`** leaves with schemagen `inputSchema` for complex tool bodies (one nested object beats many flat flags).
111
+ - Prefer **`kind: "document"`** (or legacy `kind: "json"`) leaves with schemagen `inputSchema` for complex tool bodies (one nested object beats many flat flags).
112
112
  - Keep **`description`** strings short and action-oriented; put examples in **`notes`**, not duplicated in every option.
113
113
  - Use **`hidden: true`** or **`mcpTool.enabled: false`** for debug/internal commands.
114
114
  - For shape discovery: HTTP agents load **`docs openapi`** or `GET /openapi.json`; MCP agents use **`docs cli-schema`**; load full **`docs cli`** only when prose is needed.
@@ -300,15 +300,15 @@ Use **`ctx.jsonOpt("invoice")`**, **`ctx.inputs`**, or **`ctx.inputsAs<MyInput>(
300
300
 
301
301
  At most one `pipable` Json option per leaf. Json option names must appear in `inputSchema.properties` when a custom `inputSchema` is set.
302
302
 
303
- ### Pure JSON leaves (`kind: "json"`)
303
+ ### Structured document leaves (`kind: "document"`)
304
304
 
305
- When the entire tool body is JSON (no CLI flags), set **`kind: "json"`** on the leaf with **`inputSchema`** and **no `options` or `positionals`**:
305
+ When the entire tool body is a structured document (JSON or YAML, no CLI flags), set **`kind: "document"`** (or legacy `"json"`) on the leaf with **`inputSchema`** and **no `options` or `positionals`**:
306
306
 
307
307
  ```typescript
308
308
  {
309
309
  key: "render-invoice",
310
310
  description: "Render an invoice from template data",
311
- kind: "json",
311
+ kind: "document",
312
312
  inputSchema,
313
313
  handler: (ctx) => {
314
314
  const { format, invoice } = ctx.inputsAs<RenderInvoiceInput>();
@@ -319,10 +319,25 @@ When the entire tool body is JSON (no CLI flags), set **`kind: "json"`** on the
319
319
 
320
320
  | Surface | How input is supplied |
321
321
  | --- | --- |
322
- | CLI | One JSON positional **or** pipe a JSON document to stdin |
323
- | MCP / HTTP | Full tool args object (`ctx.toolArgs` / POST body) |
322
+ | CLI | One JSON or YAML positional **or** pipe a JSON/YAML document to stdin |
323
+ | MCP / HTTP | Full tool args object (`ctx.toolArgs` / JSON or YAML request body) |
324
324
 
325
- Example CLI: `jq '{format:"pdf", invoice:.}' data.json | myapp render-invoice`
325
+ Example CLI:
326
+ ```bash
327
+ # JSON positional or pipe
328
+ jq '{format:"pdf", invoice:.}' data.json | myapp render-invoice
329
+ myapp render-invoice '{"format":"pdf","invoice":{"id":"INV-1"}}'
330
+
331
+ # YAML positional or pipe
332
+ myapp render-invoice 'format: pdf
333
+ invoice:
334
+ id: INV-1'
335
+ cat << 'EOF' | myapp render-invoice
336
+ format: pdf
337
+ invoice:
338
+ id: INV-1
339
+ EOF
340
+ ```
326
341
 
327
342
  See [output-schema.md](output-schema.md) for schemagen `inputType`, [http-server.md](http-server.md) for HTTP tool bodies, and [json-schema-subset.md](json-schema-subset.md) for validation drafts, Zod interop, and keyword notes.
328
343
 
package/index.d.ts CHANGED
@@ -618,15 +618,16 @@ export interface CliNodeBase {
618
618
  /** Global or command-level flags/options. */
619
619
  options?: CliOption[];
620
620
  }
621
- /** Leaf input mode: `json` = pure JSON body (no CLI flags). */
622
- export type CliLeafKind = "json";
621
+ /** Leaf input mode: `document` (or legacy `json`) = structured JSON or YAML document body (no CLI flags). */
622
+ export type CliLeafKind = "document" | "json";
623
623
  /**
624
624
  * A leaf command node with a handler and optional positionals.
625
625
  */
626
626
  export type CliLeaf = CliNodeBase & {
627
627
  /**
628
- * When `"json"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
629
- * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
628
+ * When `"document"` (or legacy `"json"`), the leaf accepts a single JSON or YAML document
629
+ * (CLI positional or piped stdin; MCP/HTTP tool args = body). Requires `inputSchema`;
630
+ * forbids `options` and `positionals`.
630
631
  */
631
632
  kind?: CliLeafKind;
632
633
  /** Handler function for leaf commands. */
@@ -814,8 +815,14 @@ export type CliProgram = CliNode & {
814
815
  /** Program version (printed by the `version` built-in and MCP serverInfo). */
815
816
  version: string;
816
817
  };
817
- /** True when the leaf accepts a pure JSON body (no CLI flags). */
818
- export declare function isJsonLeaf(leaf: CliLeaf): boolean;
818
+ /** True when the leaf accepts a structured JSON or YAML document body (no CLI flags). */
819
+ export declare function isDocumentLeaf(
820
+ /** Leaf command node to inspect. */
821
+ leaf: CliLeaf): boolean;
822
+ /** True when the leaf accepts a structured document body (backward-compatible alias for `isDocumentLeaf`). */
823
+ export declare function isJsonLeaf(
824
+ /** Leaf command node to inspect. */
825
+ leaf: CliLeaf): boolean;
819
826
  /**
820
827
  * Handler closure type for leaf commands.
821
828
  * Supports sync and async handlers; non-undefined return values become implicit JSON responses for headless invocations.
@@ -844,6 +851,12 @@ export declare function parseDateTime(s: string): string;
844
851
  export declare class LeafInputError extends Error {
845
852
  constructor(message: string);
846
853
  }
854
+ /** Parses a JSON or YAML string from a command argument or document body. */
855
+ export declare function parseDocumentText(
856
+ /** Raw text containing a JSON or YAML document. */
857
+ raw: string,
858
+ /** Field or argument label for error reporting. */
859
+ label: string): unknown;
847
860
  /** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
848
861
  export declare function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined;
849
862
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "7.0.8",
3
+ "version": "7.0.9",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -0,0 +1,208 @@
1
+ /*
2
+ Tests for structured document leaves (kind: "document") supporting JSON and YAML document input.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { Cli } from "../index.ts";
7
+ import { LeafInputError, parseDocumentText } from "./leaf-inputs.ts";
8
+ import { ParseKind, parse } from "./parse.ts";
9
+ import { CliOptionKind, type CliProgram, CliSchemaValidationError } from "./types.ts";
10
+ import { cliValidateProgram } from "./validate.ts";
11
+
12
+ /** JSON Schema for the deployment test document body. */
13
+ const deploySchema = {
14
+ type: "object",
15
+ properties: {
16
+ target: { type: "string", enum: ["staging", "production"] },
17
+ config: {
18
+ type: "object",
19
+ properties: { replicas: { type: "integer" } },
20
+ required: ["replicas"],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ required: ["target", "config"],
25
+ additionalProperties: false,
26
+ } as const;
27
+
28
+ /** Creates a test program with a single `kind: "document"` leaf command. */
29
+ function documentLeafProgram() {
30
+ return {
31
+ key: "document-leaf-test",
32
+ version: "1.0.0",
33
+ description: "document leaf tests",
34
+ commands: [
35
+ {
36
+ key: "deploy",
37
+ description: "Deploy from document body",
38
+ kind: "document",
39
+ inputSchema: deploySchema,
40
+ handler: (ctx) => ctx.inputsAs<{ target: string; config: { replicas: number } }>(),
41
+ },
42
+ ],
43
+ } satisfies CliProgram;
44
+ }
45
+
46
+ /** Tests for `kind: "document"` leaf commands. */
47
+ describe("kind: document leaf", () => {
48
+ /** Tests validation rules for document leaves. */
49
+ test("validate requires inputSchema and forbids options/positionals", () => {
50
+ expect(() =>
51
+ cliValidateProgram({
52
+ key: "bad",
53
+ version: "1",
54
+ description: "bad",
55
+ commands: [{ key: "x", description: "x", kind: "document", handler: () => {} }],
56
+ }),
57
+ ).toThrow(CliSchemaValidationError);
58
+
59
+ expect(() =>
60
+ cliValidateProgram({
61
+ key: "bad",
62
+ version: "1",
63
+ description: "bad",
64
+ commands: [
65
+ {
66
+ key: "x",
67
+ description: "x",
68
+ kind: "document",
69
+ inputSchema: deploySchema,
70
+ options: [{ name: "f", description: "f", kind: CliOptionKind.String }],
71
+ handler: () => {},
72
+ },
73
+ ],
74
+ }),
75
+ ).toThrow(CliSchemaValidationError);
76
+
77
+ expect(() =>
78
+ cliValidateProgram({
79
+ key: "bad",
80
+ version: "1",
81
+ description: "bad",
82
+ commands: [
83
+ {
84
+ key: "x",
85
+ description: "x",
86
+ kind: "document",
87
+ inputSchema: deploySchema,
88
+ positionals: [{ name: "file", description: "file", kind: CliOptionKind.String }],
89
+ handler: () => {},
90
+ },
91
+ ],
92
+ }),
93
+ ).toThrow(CliSchemaValidationError);
94
+ });
95
+
96
+ /** Tests that flags on document leaves are rejected. */
97
+ test("parse rejects CLI flags on document leaf", () => {
98
+ const root = documentLeafProgram();
99
+ const pr = parse(root, ["deploy", "--target", "staging"]);
100
+ expect(pr.kind).toBe(ParseKind.Error);
101
+ expect(pr.errorMsg).toContain("Document commands do not accept options");
102
+ });
103
+
104
+ /** Tests that document leaf accepts positional argument tokens. */
105
+ test("parse accepts document positional token", () => {
106
+ const root = documentLeafProgram();
107
+ const pr = parse(root, ["deploy", "target: staging"]);
108
+ expect(pr.kind).toBe(ParseKind.Ok);
109
+ expect(pr.args).toEqual(["target: staging"]);
110
+ });
111
+
112
+ /** Tests reading body from YAML positional argv. */
113
+ test("invoke reads body from YAML positional argv", async () => {
114
+ const cli = new Cli(documentLeafProgram());
115
+ const yaml = "target: staging\nconfig:\n replicas: 3";
116
+ const result = await cli.invoke(["deploy", yaml], { invocation: "mcp" });
117
+ expect(result.kind).toBe("ok");
118
+ expect(result.response?.body).toEqual({ target: "staging", config: { replicas: 3 } });
119
+
120
+ const cliResult = await cli.invoke(["deploy", yaml], { invocation: "cli" });
121
+ expect(cliResult.kind).toBe("ok");
122
+ expect(JSON.parse(cliResult.stdout)).toEqual({ target: "staging", config: { replicas: 3 } });
123
+ });
124
+
125
+ /** Tests reading body from JSON positional argv. */
126
+ test("invoke reads body from JSON positional argv", async () => {
127
+ const cli = new Cli(documentLeafProgram());
128
+ const json = '{"target":"production","config":{"replicas":5}}';
129
+ const result = await cli.invoke(["deploy", json], { invocation: "mcp" });
130
+ expect(result.kind).toBe("ok");
131
+ expect(result.response?.body).toEqual({ target: "production", config: { replicas: 5 } });
132
+ });
133
+
134
+ /** Tests reading body from toolArgs. */
135
+ test("invoke reads body from toolArgs", async () => {
136
+ const cli = new Cli(documentLeafProgram());
137
+ const result = await cli.invoke(["deploy"], {
138
+ invocation: "http",
139
+ toolArgs: { target: "production", config: { replicas: 10 } },
140
+ });
141
+ expect(result.kind).toBe("ok");
142
+ expect(result.response?.body).toEqual({ target: "production", config: { replicas: 10 } });
143
+ });
144
+
145
+ /** Tests error message when document body is missing. */
146
+ test("invoke errors when body is missing", async () => {
147
+ const cli = new Cli(documentLeafProgram());
148
+ const result = await cli.invoke(["deploy"], { invocation: "cli" });
149
+ expect(result.kind).toBe("error");
150
+ expect(result.errorMsg).toContain("Missing document input");
151
+ });
152
+
153
+ /** Tests inputSchema validation error handling. */
154
+ test("invoke validates inputSchema before handler runs", async () => {
155
+ let handlerRan = false;
156
+ const base = documentLeafProgram();
157
+ const program = {
158
+ ...base,
159
+ commands: [
160
+ {
161
+ ...base.commands[0],
162
+ handler: () => {
163
+ handlerRan = true;
164
+ },
165
+ },
166
+ ],
167
+ } satisfies CliProgram;
168
+ const cli = new Cli(program);
169
+ const result = await cli.invoke(["deploy", "target: invalid-target\nconfig:\n replicas: 1"], {
170
+ invocation: "cli",
171
+ });
172
+ expect(result.kind).toBe("error");
173
+ expect(handlerRan).toBe(false);
174
+ });
175
+
176
+ /** Tests non-object document body returns error. */
177
+ test("non-object document body returns error", async () => {
178
+ const cli = new Cli(documentLeafProgram());
179
+ const result = await cli.invoke(["deploy", '"just-a-string"'], { invocation: "cli" });
180
+ expect(result.kind).toBe("error");
181
+ expect(result.errorMsg).toContain("Document input must be a JSON or YAML object");
182
+ });
183
+ });
184
+
185
+ /** Tests for parseDocumentText helper function. */
186
+ describe("parseDocumentText", () => {
187
+ /** Tests parsing valid JSON. */
188
+ test("parses valid JSON object", () => {
189
+ const parsed = parseDocumentText('{"key":"value"}', "test");
190
+ expect(parsed).toEqual({ key: "value" });
191
+ });
192
+
193
+ /** Tests parsing valid YAML. */
194
+ test("parses valid YAML object", () => {
195
+ const parsed = parseDocumentText("key: value\nnested:\n count: 2", "test");
196
+ expect(parsed).toEqual({ key: "value", nested: { count: 2 } });
197
+ });
198
+
199
+ /** Tests empty string throws error. */
200
+ test("throws on empty string", () => {
201
+ expect(() => parseDocumentText(" ", "test")).toThrow(LeafInputError);
202
+ });
203
+
204
+ /** Tests invalid syntax throws error. */
205
+ test("throws on invalid syntax", () => {
206
+ expect(() => parseDocumentText("{bad json", "test")).toThrow(LeafInputError);
207
+ });
208
+ });
@@ -1,9 +1,14 @@
1
+ /*
2
+ Tests for pure JSON leaves (kind: "json") including backward-compatibility and YAML document input.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { Cli } from "../index.ts";
3
7
  import { ParseKind, parse } from "./parse.ts";
4
8
  import { CliOptionKind, type CliProgram, CliSchemaValidationError } from "./types.ts";
5
9
  import { cliValidateProgram } from "./validate.ts";
6
10
 
11
+ /** JSON Schema for the invoice render test body. */
7
12
  const bodySchema = {
8
13
  type: "object",
9
14
  properties: {
@@ -19,6 +24,7 @@ const bodySchema = {
19
24
  additionalProperties: false,
20
25
  } as const;
21
26
 
27
+ /** Creates a test program with a single `kind: "json"` leaf command. */
22
28
  function jsonLeafProgram() {
23
29
  return {
24
30
  key: "json-leaf-test",
@@ -36,7 +42,9 @@ function jsonLeafProgram() {
36
42
  } satisfies CliProgram;
37
43
  }
38
44
 
45
+ /** Tests for `kind: "json"` leaf commands. */
39
46
  describe("kind: json leaf", () => {
47
+ /** Tests validation rules for json leaves. */
40
48
  test("validate requires inputSchema and forbids options/positionals", () => {
41
49
  expect(() =>
42
50
  cliValidateProgram({
@@ -84,6 +92,7 @@ describe("kind: json leaf", () => {
84
92
  ).toThrow(CliSchemaValidationError);
85
93
  });
86
94
 
95
+ /** Tests that flags on json leaves are rejected. */
87
96
  test("parse rejects CLI flags on json leaf", () => {
88
97
  const root = jsonLeafProgram();
89
98
  const pr = parse(root, ["render", "--format", "pdf"]);
@@ -91,6 +100,7 @@ describe("kind: json leaf", () => {
91
100
  expect(pr.errorMsg).toContain("JSON commands do not accept options");
92
101
  });
93
102
 
103
+ /** Tests that json leaf accepts JSON positional arguments. */
94
104
  test("parse accepts JSON positional", () => {
95
105
  const root = jsonLeafProgram();
96
106
  const pr = parse(root, ["render", '{"format":"pdf","invoice":{"id":"1"}}']);
@@ -98,6 +108,7 @@ describe("kind: json leaf", () => {
98
108
  expect(pr.args).toEqual(['{"format":"pdf","invoice":{"id":"1"}}']);
99
109
  });
100
110
 
111
+ /** Tests reading body from toolArgs. */
101
112
  test("invoke reads body from toolArgs", async () => {
102
113
  const cli = new Cli(jsonLeafProgram());
103
114
  const result = await cli.invoke(["render"], {
@@ -108,6 +119,7 @@ describe("kind: json leaf", () => {
108
119
  expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
109
120
  });
110
121
 
122
+ /** Tests reading body from JSON positional argv. */
111
123
  test("invoke reads body from JSON positional argv", async () => {
112
124
  const cli = new Cli(jsonLeafProgram());
113
125
  const result = await cli.invoke(["render", '{"format":"html","invoice":{"id":"2"}}'], {
@@ -117,6 +129,18 @@ describe("kind: json leaf", () => {
117
129
  expect(result.response?.body).toEqual({ format: "html", invoice: { id: "2" } });
118
130
  });
119
131
 
132
+ /** Tests reading body from YAML positional argv for kind: "json" leaf. */
133
+ test("invoke reads body from YAML positional argv", async () => {
134
+ const cli = new Cli(jsonLeafProgram());
135
+ const yamlBody = "format: pdf\ninvoice:\n id: 'INV-YAML-1'";
136
+ const result = await cli.invoke(["render", yamlBody], {
137
+ invocation: "mcp",
138
+ });
139
+ expect(result.kind).toBe("ok");
140
+ expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-YAML-1" } });
141
+ });
142
+
143
+ /** Tests error when body is missing. */
120
144
  test("invoke errors when body missing", async () => {
121
145
  const cli = new Cli(jsonLeafProgram());
122
146
  const result = await cli.invoke(["render"], { invocation: "http" });
@@ -124,6 +148,7 @@ describe("kind: json leaf", () => {
124
148
  expect(result.errorMsg).toContain("Missing JSON input");
125
149
  });
126
150
 
151
+ /** Tests inputSchema validation before handler runs. */
127
152
  test("invoke validates inputSchema before handler", async () => {
128
153
  let called = false;
129
154
  const base = jsonLeafProgram();
@@ -147,6 +172,7 @@ describe("kind: json leaf", () => {
147
172
  expect(called).toBe(false);
148
173
  });
149
174
 
175
+ /** Tests error on non-object JSON body. */
150
176
  test("non-object JSON body returns error", async () => {
151
177
  const cli = new Cli(jsonLeafProgram());
152
178
  const result = await cli.invoke(["render", '"not-an-object"'], { invocation: "cli" });
@@ -7,7 +7,7 @@ import { isInteractiveTty } from "../utils.ts";
7
7
  import type { CliContext, CliLeafInputs } from "./context.ts";
8
8
  import { collectOptionDefs } from "./parse.ts";
9
9
  import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
10
- import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter, isJsonLeaf } from "./types.ts";
10
+ import { type CliLeafKind, CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter, isDocumentLeaf } from "./types.ts";
11
11
 
12
12
  /** Thrown when leaf input resolution or validation fails. */
13
13
  export class LeafInputError extends Error {
@@ -17,8 +17,11 @@ export class LeafInputError extends Error {
17
17
  }
18
18
  }
19
19
 
20
- /** Internal key for piped stdin on `kind: "json"` leaves. */
21
- export const JSON_LEAF_BODY_KEY = "__jsonLeafBody";
20
+ /** Internal key for piped stdin on `kind: "document"` or `kind: "json"` leaves. */
21
+ export const DOCUMENT_LEAF_BODY_KEY = "__documentLeafBody";
22
+
23
+ /** Internal key for piped stdin on `kind: "json"` leaves (backward-compatible alias). */
24
+ export const JSON_LEAF_BODY_KEY = DOCUMENT_LEAF_BODY_KEY;
22
25
 
23
26
  function resolveLeaf(program: CliProgram, commandPath: string[]): CliLeaf | undefined {
24
27
  let node: CliNode = program;
@@ -36,7 +39,12 @@ function leafNode(ctx: CliContext): CliLeaf | undefined {
36
39
  }
37
40
 
38
41
  /** Parses a JSON string from a `--name` flag value. */
39
- export function parseJsonText(raw: string, label: string): unknown {
42
+ export function parseJsonText(
43
+ /** Raw text to parse as JSON. */
44
+ raw: string,
45
+ /** Field or argument label for error reporting. */
46
+ label: string,
47
+ ): unknown {
40
48
  const trimmed = raw.trim();
41
49
  if (trimmed.length === 0) {
42
50
  throw new LeafInputError(`${label}: JSON value is empty`);
@@ -48,6 +56,31 @@ export function parseJsonText(raw: string, label: string): unknown {
48
56
  }
49
57
  }
50
58
 
59
+ /** Parses a JSON or YAML string from a command argument or document body. */
60
+ export function parseDocumentText(
61
+ /** Raw text containing a JSON or YAML document. */
62
+ raw: string,
63
+ /** Field or argument label for error reporting. */
64
+ label: string,
65
+ ): unknown {
66
+ const trimmed = raw.trim();
67
+ if (trimmed.length === 0) {
68
+ throw new LeafInputError(`${label}: value is empty`);
69
+ }
70
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
71
+ try {
72
+ return JSON.parse(trimmed);
73
+ } catch {
74
+ // Fall through to YAML if JSON parse fails
75
+ }
76
+ }
77
+ try {
78
+ return Bun.YAML.parse(trimmed);
79
+ } catch {
80
+ throw new LeafInputError(`${label}: invalid JSON or YAML`);
81
+ }
82
+ }
83
+
51
84
  async function readPipedJsonStdin(): Promise<unknown> {
52
85
  const raw = await new Response(Bun.stdin).text();
53
86
  const trimmed = raw.trim();
@@ -61,20 +94,38 @@ async function readPipedJsonStdin(): Promise<unknown> {
61
94
  }
62
95
  }
63
96
 
64
- function jsonLeafBodyHelp(): string {
97
+ /** Returns the error message when document input is missing. */
98
+ function jsonLeafBodyHelp(
99
+ /** Leaf kind: document or json. */
100
+ kind: CliLeafKind = "json",
101
+ ): string {
102
+ if (kind === "document") {
103
+ return "Missing document input: pass a JSON or YAML document as an argument or pipe to stdin";
104
+ }
65
105
  return "Missing JSON input: pass a JSON document as an argument or pipe to stdin";
66
106
  }
67
107
 
68
- async function readPipedJsonStdinForJsonLeaf(): Promise<unknown> {
108
+ /** Reads piped stdin for a document or json leaf command. */
109
+ async function readPipedJsonStdinForJsonLeaf(
110
+ /** Leaf kind: document or json. */
111
+ kind: CliLeafKind = "json",
112
+ ): Promise<unknown> {
69
113
  const raw = await new Response(Bun.stdin).text();
70
114
  const trimmed = raw.trim();
71
115
  if (trimmed.length === 0) {
72
- throw new LeafInputError(jsonLeafBodyHelp());
116
+ throw new LeafInputError(jsonLeafBodyHelp(kind));
117
+ }
118
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
119
+ try {
120
+ return JSON.parse(trimmed);
121
+ } catch {
122
+ // Fall through to YAML
123
+ }
73
124
  }
74
125
  try {
75
- return JSON.parse(trimmed);
126
+ return Bun.YAML.parse(trimmed);
76
127
  } catch {
77
- throw new LeafInputError("stdin is not valid JSON");
128
+ throw new LeafInputError("stdin is not valid JSON or YAML");
78
129
  }
79
130
  }
80
131
 
@@ -130,8 +181,8 @@ export async function preloadPipableJson(
130
181
  }
131
182
 
132
183
  const leaf = resolveLeaf(program, commandPath);
133
- if (leaf && isJsonLeaf(leaf) && args.length === 0) {
134
- return { [JSON_LEAF_BODY_KEY]: await readPipedJsonStdinForJsonLeaf() };
184
+ if (leaf && isDocumentLeaf(leaf) && args.length === 0) {
185
+ return { [JSON_LEAF_BODY_KEY]: await readPipedJsonStdinForJsonLeaf(leaf.kind) };
135
186
  }
136
187
 
137
188
  for (const opt of collectOptionDefs(program, commandPath)) {
@@ -181,22 +232,26 @@ export function loadLeafInputs(ctx: CliContext): CliLeafInputs {
181
232
  const leaf = leafNode(ctx);
182
233
  if (!leaf) return {};
183
234
 
184
- if (isJsonLeaf(leaf)) {
235
+ if (isDocumentLeaf(leaf)) {
185
236
  let body: unknown;
186
237
  if (ctx.toolArgs !== undefined) {
187
238
  body = ctx.toolArgs;
188
239
  } else if (ctx.args.length > 0) {
189
240
  const [arg0] = ctx.args;
190
241
  if (arg0 === undefined) {
191
- throw new LeafInputError(jsonLeafBodyHelp());
242
+ throw new LeafInputError(jsonLeafBodyHelp(leaf.kind));
192
243
  }
193
- body = parseJsonText(arg0, "JSON argument");
244
+ const label = leaf.kind === "document" ? "Document argument" : "JSON argument";
245
+ body = parseDocumentText(arg0, label);
194
246
  } else if (JSON_LEAF_BODY_KEY in ctx.preloadedJson) {
195
247
  body = ctx.preloadedJson[JSON_LEAF_BODY_KEY];
196
248
  } else {
197
- throw new LeafInputError(jsonLeafBodyHelp());
249
+ throw new LeafInputError(jsonLeafBodyHelp(leaf.kind));
198
250
  }
199
251
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
252
+ if (leaf.kind === "document") {
253
+ throw new LeafInputError("Document input must be a JSON or YAML object");
254
+ }
200
255
  throw new LeafInputError("JSON input must be a JSON object");
201
256
  }
202
257
  const out = body as CliLeafInputs;
package/src/core/parse.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  type CliRouter,
20
20
  isCliLeaf,
21
21
  isCliRouter,
22
- isJsonLeaf,
22
+ isDocumentLeaf,
23
23
  } from "./types.ts";
24
24
 
25
25
  // ── Parse Result ──────────────────────────────────────────────────────────────
@@ -292,9 +292,9 @@ export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
292
292
  return [...(node.options ?? [])];
293
293
  }
294
294
 
295
- /** Fills `args` for a json leaf from `startIdx` (0 or 1 JSON string positional). */
295
+ /** Fills `args` for a document / json leaf from `startIdx` (0 or 1 JSON or YAML string positional). */
296
296
  function finishJsonLeaf(
297
- _node: CliLeaf,
297
+ node: CliLeaf,
298
298
  startIdx: number,
299
299
  argv: string[],
300
300
  path: string[],
@@ -313,7 +313,8 @@ function finishJsonLeaf(
313
313
  return errorResult("Unexpected extra arguments", path, [], pathParams);
314
314
  }
315
315
  if (tok.startsWith("-")) {
316
- return errorResult(`JSON commands do not accept options: ${tok}`, path, [], pathParams);
316
+ const kindLabel = node.kind === "document" ? "Document" : "JSON";
317
+ return errorResult(`${kindLabel} commands do not accept options: ${tok}`, path, [], pathParams);
317
318
  }
318
319
  args.push(tok);
319
320
  idx += 1;
@@ -552,7 +553,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
552
553
  let node: CliNode | undefined;
553
554
 
554
555
  if (isCliLeaf(root)) {
555
- if (isJsonLeaf(root)) {
556
+ if (isDocumentLeaf(root)) {
556
557
  return finishJsonLeaf(root, i, argv, path, opts, pathParams);
557
558
  }
558
559
  return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals, pathParams);
@@ -645,7 +646,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
645
646
 
646
647
  // Walk the command tree
647
648
  while (true) {
648
- if (isCliLeaf(current) && isJsonLeaf(current)) {
649
+ if (isCliLeaf(current) && isDocumentLeaf(current)) {
649
650
  return finishJsonLeaf(current, i, argv, path, opts, pathParams);
650
651
  }
651
652
 
package/src/core/types.ts CHANGED
@@ -500,16 +500,17 @@ export interface CliNodeBase {
500
500
  options?: CliOption[];
501
501
  }
502
502
 
503
- /** Leaf input mode: `json` = pure JSON body (no CLI flags). */
504
- export type CliLeafKind = "json";
503
+ /** Leaf input mode: `document` (or legacy `json`) = structured JSON or YAML document body (no CLI flags). */
504
+ export type CliLeafKind = "document" | "json";
505
505
 
506
506
  /**
507
507
  * A leaf command node with a handler and optional positionals.
508
508
  */
509
509
  export type CliLeaf = CliNodeBase & {
510
510
  /**
511
- * When `"json"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
512
- * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
511
+ * When `"document"` (or legacy `"json"`), the leaf accepts a single JSON or YAML document
512
+ * (CLI positional or piped stdin; MCP/HTTP tool args = body). Requires `inputSchema`;
513
+ * forbids `options` and `positionals`.
513
514
  */
514
515
  kind?: CliLeafKind;
515
516
  /** Handler function for leaf commands. */
@@ -691,9 +692,20 @@ export function isCliLeaf(node: CliNode): node is CliLeaf {
691
692
  return "handler" in node && typeof node.handler === "function";
692
693
  }
693
694
 
694
- /** True when the leaf accepts a pure JSON body (no CLI flags). */
695
- export function isJsonLeaf(leaf: CliLeaf): boolean {
696
- return leaf.kind === "json";
695
+ /** True when the leaf accepts a structured JSON or YAML document body (no CLI flags). */
696
+ export function isDocumentLeaf(
697
+ /** Leaf command node to inspect. */
698
+ leaf: CliLeaf,
699
+ ): boolean {
700
+ return leaf.kind === "document" || leaf.kind === "json";
701
+ }
702
+
703
+ /** True when the leaf accepts a structured document body (backward-compatible alias for `isDocumentLeaf`). */
704
+ export function isJsonLeaf(
705
+ /** Leaf command node to inspect. */
706
+ leaf: CliLeaf,
707
+ ): boolean {
708
+ return isDocumentLeaf(leaf);
697
709
  }
698
710
 
699
711
  /** True when the node is a router (has subcommands). */
@@ -17,7 +17,7 @@ import {
17
17
  CliValueFormat,
18
18
  isCliLeaf,
19
19
  isCliRouter,
20
- isJsonLeaf,
20
+ isDocumentLeaf,
21
21
  } from "./types.ts";
22
22
 
23
23
  /** Validates `docs` configuration on the program root. */
@@ -264,15 +264,16 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
264
264
  if (isRoot && node.mcpTool !== undefined) {
265
265
  throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
266
266
  }
267
- if (isJsonLeaf(node)) {
267
+ if (isDocumentLeaf(node)) {
268
+ const kindStr = `kind: "${node.kind ?? "document"}"`;
268
269
  if (node.inputSchema === undefined) {
269
- throw new CliSchemaValidationError(`kind: "json" requires inputSchema on ${node.key}`);
270
+ throw new CliSchemaValidationError(`${kindStr} requires inputSchema on ${node.key}`);
270
271
  }
271
272
  if ((node.options ?? []).length > 0) {
272
- throw new CliSchemaValidationError(`kind: "json" forbids options on ${node.key}`);
273
+ throw new CliSchemaValidationError(`${kindStr} forbids options on ${node.key}`);
273
274
  }
274
275
  if ((node.positionals ?? []).length > 0) {
275
- throw new CliSchemaValidationError(`kind: "json" forbids positionals on ${node.key}`);
276
+ throw new CliSchemaValidationError(`${kindStr} forbids positionals on ${node.key}`);
276
277
  }
277
278
  }
278
279
  const outputSchema = node.outputSchema;
@@ -40,6 +40,7 @@ export {
40
40
  CliOptionKind,
41
41
  CliSchemaValidationError,
42
42
  CliValueFormat,
43
+ isDocumentLeaf,
43
44
  isJsonLeaf,
44
45
  } from "../core/types.ts";
45
46
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "../runtime/cli.ts";
package/src/help.test.ts CHANGED
@@ -278,6 +278,46 @@ describe("cliHelpRender", () => {
278
278
  expect(help).toContain("# Generated ID.");
279
279
  expect(help).toContain("id: string");
280
280
  });
281
+
282
+ /** Tests that document leaf commands render [DOCUMENT] usage and schema sections. */
283
+ test("document leaf renders [DOCUMENT] usage and schemas in non-TTY mode", () => {
284
+ const root = testProgram({
285
+ key: "myapp",
286
+ version: "1.0.0",
287
+ description: "Test application.",
288
+ commands: [
289
+ {
290
+ key: "deploy",
291
+ kind: "document",
292
+ description: "Deploy from document.",
293
+ inputSchema: {
294
+ type: "object",
295
+ properties: {
296
+ target: { type: "string", description: "Deployment target." },
297
+ },
298
+ required: ["target"],
299
+ },
300
+ outputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ url: { type: "string", description: "Deployment URL." },
304
+ },
305
+ required: ["url"],
306
+ },
307
+ handler: () => {},
308
+ },
309
+ ],
310
+ });
311
+ const help = cliHelpRender(cliPresentationRoot(root), ["deploy"], false, { isTTY: false });
312
+ expect(help).toContain("myapp deploy [DOCUMENT]");
313
+ expect(help).toContain("Pass a JSON or YAML document as an argument or pipe to stdin.");
314
+ expect(help).toContain("Input Schema:");
315
+ expect(help).toContain("# Deployment target.");
316
+ expect(help).toContain("target: string");
317
+ expect(help).toContain("Output Schema (JSON):");
318
+ expect(help).toContain("# Deployment URL.");
319
+ expect(help).toContain("url: string");
320
+ });
281
321
  });
282
322
 
283
323
  /** Tests for converting JSON Schema to human- and agent-friendly YAML lines. */
package/src/help.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  type CliRouter,
17
17
  isCliLeaf,
18
18
  isCliRouter,
19
- isJsonLeaf,
19
+ isDocumentLeaf,
20
20
  } from "./core/types.ts";
21
21
  import { visibleOptions, visibleSubcommands } from "./runtime/exposure.ts";
22
22
 
@@ -376,8 +376,9 @@ function usageLines(
376
376
  helpPath: string[],
377
377
  hasCommands: boolean,
378
378
  hasArgs: boolean,
379
- jsonLeaf: boolean,
379
+ documentLeaf: boolean,
380
380
  color: boolean,
381
+ leafKind?: string,
381
382
  ): string[] {
382
383
  let fullPath = appName;
383
384
  for (const seg of helpPath) {
@@ -386,7 +387,8 @@ function usageLines(
386
387
  const usageOpts = color ? style.aquaBold("[OPTIONS]") : "[OPTIONS]";
387
388
  const usageCmd = color ? style.aquaBold("COMMAND") : "COMMAND";
388
389
  const usageArgs = color ? style.aquaBold("[ARGS]...") : "[ARGS]...";
389
- const usageJson = color ? style.aquaBold("[JSON]") : "[JSON]";
390
+ const docTag = leafKind === "document" ? "[DOCUMENT]" : "[JSON]";
391
+ const usageDoc = color ? style.aquaBold(docTag) : docTag;
390
392
 
391
393
  const out: string[] = [];
392
394
  if (helpPath.length === 0) {
@@ -397,8 +399,8 @@ function usageLines(
397
399
  }
398
400
  return out;
399
401
  }
400
- if (jsonLeaf) {
401
- out.push(`${fullPath} ${usageJson}`);
402
+ if (documentLeaf) {
403
+ out.push(`${fullPath} ${usageDoc}`);
402
404
  return out;
403
405
  }
404
406
  out.push(`${fullPath} ${usageOpts}${hasArgs ? ` ${usageArgs}` : ""}`);
@@ -408,10 +410,11 @@ function usageLines(
408
410
  return out;
409
411
  }
410
412
 
411
- /** Table rows for `kind: "json"` leaf input (schema properties + stdin hint). */
412
- function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined): HelpRow[] {
413
- const hint = "Pass a JSON document as an argument or pipe to stdin.";
414
- const rows: HelpRow[] = [{ label: "JSON", description: hint }];
413
+ /** Table rows for `kind: "document"` / `kind: "json"` leaf input (schema properties + stdin hint). */
414
+ function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined, kind?: string): HelpRow[] {
415
+ const hint = "Pass a JSON or YAML document as an argument or pipe to stdin.";
416
+ const label = kind === "document" ? "DOCUMENT" : "JSON";
417
+ const rows: HelpRow[] = [{ label, description: hint }];
415
418
  const props = inputSchema?.properties;
416
419
  if (!props || typeof props !== "object" || Array.isArray(props)) {
417
420
  return rows;
@@ -763,7 +766,7 @@ export function cliHelpRender(
763
766
  if (isCliLeaf(schema as unknown as CliNode) && showSchema) {
764
767
  const leaf = schema as unknown as CliLeaf;
765
768
  if (leaf.outputSchema !== undefined) {
766
- const title = isJsonLeaf(leaf) ? "Output Schema (JSON)" : "Output Schema (with --json)";
769
+ const title = isDocumentLeaf(leaf) ? "Output Schema (JSON)" : "Output Schema (with --json)";
767
770
  const yamlLines = schemaToYamlLines(leaf.outputSchema, 0);
768
771
  if (yamlLines.length > 0) {
769
772
  lines.push("");
@@ -800,14 +803,15 @@ export function cliHelpRender(
800
803
  lines.push(color ? style.white(node.description) : node.description);
801
804
  lines.push("");
802
805
  }
803
- const nodeIsJsonLeaf = isCliLeaf(node) && isJsonLeaf(node);
806
+ const nodeIsDocumentLeaf = isCliLeaf(node) && isDocumentLeaf(node);
804
807
  const usage = usageLines(
805
808
  schema.key,
806
809
  helpPath,
807
810
  isCliRouter(node) && node.commands.length > 0,
808
811
  isCliLeaf(node) && (node.positionals ?? []).length > 0,
809
- nodeIsJsonLeaf,
812
+ nodeIsDocumentLeaf,
810
813
  color,
814
+ isCliLeaf(node) ? node.kind : undefined,
811
815
  );
812
816
  if (isTTY) {
813
817
  lines.push(renderTextBox("Usage", usage, hw, color).join("\n"));
@@ -815,8 +819,8 @@ export function cliHelpRender(
815
819
  lines.push(renderPlainSection("Usage", usage).join("\n"));
816
820
  }
817
821
 
818
- if (nodeIsJsonLeaf && isCliLeaf(node)) {
819
- const inputRows = rowsForJsonInput(node.inputSchema);
822
+ if (nodeIsDocumentLeaf && isCliLeaf(node)) {
823
+ const inputRows = rowsForJsonInput(node.inputSchema, node.kind);
820
824
  const inputBox = isTTY ? renderTableBox("Input", inputRows, hw, color) : renderPlainTable("Input", inputRows, hw);
821
825
  if (inputBox.length > 0) {
822
826
  lines.push("");
@@ -860,7 +864,7 @@ export function cliHelpRender(
860
864
  }
861
865
 
862
866
  if (isCliLeaf(node) && node.outputSchema !== undefined && showSchema) {
863
- const title = nodeIsJsonLeaf ? "Output Schema (JSON)" : "Output Schema (with --json)";
867
+ const title = nodeIsDocumentLeaf ? "Output Schema (JSON)" : "Output Schema (with --json)";
864
868
  const yamlLines = schemaToYamlLines(node.outputSchema, 0);
865
869
  if (yamlLines.length > 0) {
866
870
  lines.push("");
@@ -3,7 +3,7 @@ Hand-built OpenAPI 3.1 document from exposed HTTP REST routes.
3
3
  */
4
4
 
5
5
  import type { CliHttpMethod, CliNode, CliProgram } from "../core/types.ts";
6
- import { CliOptionKind, isCliLeaf, isJsonLeaf } from "../core/types.ts";
6
+ import { CliOptionKind, isCliLeaf, isDocumentLeaf } from "../core/types.ts";
7
7
  import { leafWireOptions } from "../mcp/tools.ts";
8
8
  import { collectHttpRoutes, defaultSuccessStatus } from "./routes.ts";
9
9
  import { dereferenceJsonSchema } from "./schema-deref.ts";
@@ -253,7 +253,7 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
253
253
  ];
254
254
  } else {
255
255
  op.requestBody = {
256
- required: isJsonLeaf(route.leaf),
256
+ required: isDocumentLeaf(route.leaf),
257
257
  content: {
258
258
  [JSON_CONTENT_TYPE]: {
259
259
  schema: dereferenceJsonSchema(buildInputSchema(program, route)),
@@ -8,7 +8,7 @@ import {
8
8
  type CliNode,
9
9
  type CliProgram,
10
10
  isCliLeaf,
11
- isJsonLeaf,
11
+ isDocumentLeaf,
12
12
  CliOptionKind as OptKind,
13
13
  } from "../core/types.ts";
14
14
  import { formatMcpOptionValue, leafHasYesOption, leafWireOptions } from "../mcp/tools.ts";
@@ -252,7 +252,7 @@ export function httpRequestToArgv(
252
252
  }
253
253
 
254
254
  const leaf = route.leaf;
255
- if (isJsonLeaf(leaf)) {
255
+ if (isDocumentLeaf(leaf)) {
256
256
  return argv;
257
257
  }
258
258
 
@@ -175,7 +175,15 @@ export async function handleApiRequest(
175
175
  }
176
176
  body = parsed as Record<string, unknown>;
177
177
  } catch {
178
- return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
178
+ try {
179
+ const parsed = Bun.YAML.parse(rawBody);
180
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
181
+ return finish(apiErrorResponse(400, { error: "Request body must be a JSON object" }));
182
+ }
183
+ body = parsed as Record<string, unknown>;
184
+ } catch {
185
+ return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
186
+ }
179
187
  }
180
188
  }
181
189
  }
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  } from "./core/formats.ts";
19
19
  export {
20
20
  LeafInputError,
21
+ parseDocumentText,
21
22
  preloadPipableJson,
22
23
  readJsonOptionValue,
23
24
  } from "./core/leaf-inputs.ts";
@@ -68,6 +69,7 @@ export {
68
69
  CliOptionKind,
69
70
  CliSchemaValidationError,
70
71
  CliValueFormat,
72
+ isDocumentLeaf,
71
73
  isJsonLeaf,
72
74
  } from "./core/types.ts";
73
75
  export type { HeadlessContext } from "./headless/routing.ts";
package/src/mcp/tools.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  type CliProgram,
14
14
  CliValueFormat,
15
15
  isCliLeaf,
16
- isJsonLeaf,
16
+ isDocumentLeaf,
17
17
  leafOutputSchema,
18
18
  } from "../core/types.ts";
19
19
  import { docsMcpResources } from "../docs/mcp-resources.ts";
@@ -155,7 +155,7 @@ function positionalProperty(p: CliPositional): Record<string, unknown> {
155
155
 
156
156
  /** Builds inputSchema for a leaf command. */
157
157
  function buildInputSchema(leaf: CliLeaf): Record<string, unknown> {
158
- if (isJsonLeaf(leaf) && leaf.inputSchema !== undefined) {
158
+ if (isDocumentLeaf(leaf) && leaf.inputSchema !== undefined) {
159
159
  return leaf.inputSchema;
160
160
  }
161
161
 
@@ -294,7 +294,7 @@ export function mcpToolCallToArgv(
294
294
  tool: McpToolDef,
295
295
  args: Record<string, unknown>,
296
296
  ): string[] | { error: string } {
297
- if (isJsonLeaf(tool.leaf)) {
297
+ if (isDocumentLeaf(tool.leaf)) {
298
298
  return [...tool.path];
299
299
  }
300
300
 
@@ -448,6 +448,42 @@ describe("HTTP API routes", () => {
448
448
  expect(body).toEqual({ error: "bad input" });
449
449
  });
450
450
 
451
+ /** Tests that POST endpoints accept YAML request bodies. */
452
+ test("POST accepts YAML request body", async () => {
453
+ const yamlProgram = testProgram({
454
+ key: "app",
455
+ description: "Test app",
456
+ httpServer: { enabled: true },
457
+ commands: [
458
+ {
459
+ key: "create",
460
+ kind: "document",
461
+ description: "Create resource",
462
+ inputSchema: {
463
+ type: "object",
464
+ properties: { name: { type: "string" } },
465
+ required: ["name"],
466
+ },
467
+ handler: (ctx: CliContextType) => {
468
+ return { created: ctx.inputsAs<{ name: string }>().name };
469
+ },
470
+ },
471
+ ],
472
+ });
473
+ cliValidateProgram(yamlProgram);
474
+ const res = await apiRequest(
475
+ yamlProgram,
476
+ new Request("http://127.0.0.1/create", {
477
+ method: "POST",
478
+ headers: { "content-type": "application/yaml" },
479
+ body: "name: test-resource",
480
+ }),
481
+ );
482
+ expect(res.status).toBe(201);
483
+ const body = (await res.json()) as Record<string, unknown>;
484
+ expect(body).toEqual({ created: "test-resource" });
485
+ });
486
+
451
487
  test("GET /openapi.json lists REST paths", async () => {
452
488
  const res = await apiRequest(program, new Request("http://127.0.0.1/openapi.json"));
453
489
  expect(res.status).toBe(200);