@alpacakit/channels 0.1.0-beta.36 → 0.1.0-beta.39

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.
Files changed (99) hide show
  1. package/README.md +204 -723
  2. package/dist/binding-compiler.d.ts +12 -2
  3. package/dist/binding-compiler.d.ts.map +1 -1
  4. package/dist/binding-compiler.js +25 -0
  5. package/dist/cli-coercion.d.ts +1 -1
  6. package/dist/cli-coercion.d.ts.map +1 -1
  7. package/dist/cli-coercion.js +14 -29
  8. package/dist/cli-compiler.d.ts +4 -17
  9. package/dist/cli-compiler.d.ts.map +1 -1
  10. package/dist/cli-compiler.js +78 -39
  11. package/dist/cli-help-projection.d.ts.map +1 -1
  12. package/dist/cli-help-projection.js +41 -36
  13. package/dist/cli-model.d.ts +50 -65
  14. package/dist/cli-model.d.ts.map +1 -1
  15. package/dist/cli-model.js +26 -34
  16. package/dist/cli-tree.d.ts +6 -3
  17. package/dist/cli-tree.d.ts.map +1 -1
  18. package/dist/cli-tree.js +161 -387
  19. package/dist/cli.d.ts +1 -5
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +1 -5
  22. package/dist/codecs.d.ts +20 -18
  23. package/dist/codecs.d.ts.map +1 -1
  24. package/dist/codecs.js +21 -31
  25. package/dist/contract.d.ts.map +1 -1
  26. package/dist/contract.js +8 -3
  27. package/dist/definition-error.d.ts +5 -0
  28. package/dist/definition-error.d.ts.map +1 -0
  29. package/dist/definition-error.js +8 -0
  30. package/dist/factory.d.ts +25 -11
  31. package/dist/factory.d.ts.map +1 -1
  32. package/dist/factory.js +28 -18
  33. package/dist/http-compiler.d.ts +15 -0
  34. package/dist/http-compiler.d.ts.map +1 -0
  35. package/dist/http-compiler.js +37 -0
  36. package/dist/http-model.d.ts +42 -0
  37. package/dist/http-model.d.ts.map +1 -0
  38. package/dist/http-model.js +20 -0
  39. package/dist/http-tree.d.ts +7 -0
  40. package/dist/http-tree.d.ts.map +1 -0
  41. package/dist/http-tree.js +181 -0
  42. package/dist/http.d.ts +3 -0
  43. package/dist/http.d.ts.map +1 -0
  44. package/dist/http.js +2 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/input-compiler.d.ts +13 -0
  49. package/dist/input-compiler.d.ts.map +1 -0
  50. package/dist/input-compiler.js +122 -0
  51. package/dist/introspection.d.ts +1 -1
  52. package/dist/introspection.d.ts.map +1 -1
  53. package/dist/introspection.js +30 -16
  54. package/dist/mcp.d.ts +25 -24
  55. package/dist/mcp.d.ts.map +1 -1
  56. package/dist/mcp.js +87 -65
  57. package/dist/model.d.ts +44 -42
  58. package/dist/model.d.ts.map +1 -1
  59. package/dist/model.js +44 -9
  60. package/dist/owned.d.ts +12 -0
  61. package/dist/owned.d.ts.map +1 -0
  62. package/dist/owned.js +61 -0
  63. package/dist/registration.d.ts +41 -0
  64. package/dist/registration.d.ts.map +1 -0
  65. package/dist/registration.js +14 -0
  66. package/dist/runtime.d.ts +2 -35
  67. package/dist/runtime.d.ts.map +1 -1
  68. package/dist/runtime.js +2 -127
  69. package/dist/schema.d.ts +11 -8
  70. package/dist/schema.d.ts.map +1 -1
  71. package/dist/schema.js +110 -47
  72. package/dist/string-coercion.d.ts +4 -0
  73. package/dist/string-coercion.d.ts.map +1 -0
  74. package/dist/string-coercion.js +24 -0
  75. package/dist/tree-internal.d.ts +4 -2
  76. package/dist/tree-internal.d.ts.map +1 -1
  77. package/dist/tree-internal.js +5 -2
  78. package/dist/values.d.ts +1 -6
  79. package/dist/values.d.ts.map +1 -1
  80. package/dist/values.js +1 -6
  81. package/examples/README.md +28 -0
  82. package/examples/book-search.ts +151 -0
  83. package/examples/greeting.ts +193 -0
  84. package/package.json +9 -4
  85. package/dist/cli-application.d.ts +0 -40
  86. package/dist/cli-application.d.ts.map +0 -1
  87. package/dist/cli-application.js +0 -90
  88. package/dist/cli-executor.d.ts +0 -25
  89. package/dist/cli-executor.d.ts.map +0 -1
  90. package/dist/cli-executor.js +0 -44
  91. package/dist/cli-presentation.d.ts +0 -32
  92. package/dist/cli-presentation.d.ts.map +0 -1
  93. package/dist/cli-presentation.js +0 -293
  94. package/dist/cli-result-envelope.d.ts +0 -104
  95. package/dist/cli-result-envelope.d.ts.map +0 -1
  96. package/dist/cli-result-envelope.js +0 -137
  97. package/dist/cli-schema.d.ts +0 -4
  98. package/dist/cli-schema.d.ts.map +0 -1
  99. package/dist/cli-schema.js +0 -7
package/README.md CHANGED
@@ -1,772 +1,253 @@
1
1
  # @alpacakit/channels
2
2
 
3
- Define an entry once, bind it to CLI and MCP, and use the same typed endpoint
4
- for argument parsing, help, MCP schemas, input decoding, and projection.
3
+ `@alpacakit/channels` defines shared operations and derives registration views
4
+ for application-owned CLI, HTTP, and MCP hosts. It does not own argv parsing,
5
+ network servers, SDK transports, handler registries, response formatting, or
6
+ resource lifetime.
5
7
 
6
- HTTP routing is not currently provided. The supported entry-tree channels are
7
- CLI and MCP.
8
+ ## Public entry points
8
9
 
9
- ## The mental model
10
+ - `@alpacakit/channels`: Entry, Parameter, codec, Endpoint, Node/Tree,
11
+ introspection, input errors, and optional output verification.
12
+ - `@alpacakit/channels/cli`: CLI bindings and `collectCliCommands`.
13
+ - `@alpacakit/channels/http`: HTTP bindings and `collectHttpRoutes`.
14
+ - `@alpacakit/channels/mcp`: MCP bindings and `collectMcpTools`.
10
15
 
11
- You only need four concepts:
16
+ The package has no MCP SDK, Hono, Express, or Commander dependency. Hosts use
17
+ the neutral registration views with the framework version they own.
12
18
 
13
- ```text
14
- Entry = what the command accepts and what it projects
15
- Binding = how one channel supplies that input
16
- Endpoint = the Entry and its Bindings bundled together
17
- Tree = where the Endpoint lives in the command hierarchy
18
- ```
19
-
20
- A useful shorthand is:
21
-
22
- ```text
23
- What? -> Entry
24
- How? -> Binding
25
- Where? -> Tree
26
- Bundle them -> Endpoint
27
- ```
28
-
29
- Suppose an application exposes this CLI command:
30
-
31
- ```console
32
- my-app hint "entry tree" --limit 5
33
- ```
34
-
35
- The same operation is exposed to MCP with this JSON input:
36
-
37
- ```json
38
- {
39
- "query": "entry tree",
40
- "limit": 5
41
- }
42
- ```
19
+ ## Quick start: one handler, three channels
43
20
 
44
- The definitions have this structure:
45
-
46
- ```text
47
- ┌────────────────────────────────────────┐
48
- │ Entry: HintEntry │
49
- │ │
50
- │ query: required string │
51
- │ limit: optional integer, default 20 │
52
- │ │
53
- │ project the values into a HintRequest │
54
- └───────────────────┬────────────────────┘
55
- │
56
- │ attach channel input mappings
57
- ▼
58
- ┌────────────────────────────────────────┐
59
- │ Endpoint: HintEndpoint │
60
- │ │
61
- │ ┌────────────────────────────────────┐ │
62
- │ │ CLI Binding │ │
63
- │ │ query -> positional argument │ │
64
- │ │ limit -> --limit / -n │ │
65
- │ └────────────────────────────────────┘ │
66
- │ │
67
- │ ┌────────────────────────────────────┐ │
68
- │ │ MCP Binding │ │
69
- │ │ query -> JSON property "query" │ │
70
- │ │ limit -> JSON property "limit" │ │
71
- │ └────────────────────────────────────┘ │
72
- └───────────────────┬────────────────────┘
73
- │
74
- │ place the endpoint at an address
75
- ▼
76
- ┌────────────────────────────────────────┐
77
- │ Tree │
78
- │ │
79
- │ my-app │
80
- │ └── hint │
81
- │ └── HintEndpoint │
82
- └────────────────────────────────────────┘
83
- ```
84
-
85
- The important rule is that value types, validation, requiredness, defaults, and
86
- projection are declared only on the Entry. A Binding only says how a channel
87
- supplies a parameter. A Tree only says where an Endpoint lives.
88
-
89
- The following sections build that example from top to bottom.
90
-
91
- ## 1. Define what the command does: Entry
92
-
93
- An Entry answers four questions:
94
-
95
- ```text
96
- What values does this operation accept?
97
- ↓
98
- How are those values validated?
99
- ↓
100
- Which defaults are applied?
101
- ↓
102
- What application value is produced?
103
- ```
21
+ Define the input once, attach the shared operation as the Endpoint's `handler`,
22
+ and collect the Tree in the application that owns each host. The following
23
+ sections use the same `AppTree`; each host is independent.
104
24
 
105
25
  ```ts
106
26
  import {
107
- codecs,
108
- defineEntry,
109
- defineParameter,
110
- optional,
111
- required,
27
+ codecs, defineEntry, defineEntryEndpoint, defineEntryNode,
28
+ defineEntryTree, defineParameter, optional,
112
29
  } from "@alpacakit/channels";
30
+ import { bindCliEntry, collectCliCommands } from "@alpacakit/channels/cli";
31
+ import { bindHttpEntry, collectHttpRoutes } from "@alpacakit/channels/http";
32
+ import { bindMcpEntry, collectMcpTools } from "@alpacakit/channels/mcp";
113
33
 
114
- const HintEntry = defineEntry({
115
- name: "hint",
116
-
34
+ export const GreetEntry = defineEntry({
35
+ name: "greet",
117
36
  parameters: [
118
37
  defineParameter({
119
- name: "query",
38
+ name: "name",
120
39
  codec: codecs.string(),
121
- requirement: required(),
122
- description: "Text to search for",
123
- }),
124
-
125
- defineParameter({
126
- name: "limit",
127
- codec: codecs.positiveInteger({ max: 100 }),
128
- requirement: optional({ default: 20 }),
129
- description: "Maximum number of hints",
40
+ requirement: optional({ default: "world" }),
41
+ description: "Person to greet",
130
42
  }),
131
43
  ],
132
-
133
- project: ({ values }) => ({
134
- query: values.query,
135
- limit: values.limit,
136
- }),
44
+ project: ({ values }) => ({ name: values.name }),
137
45
  });
138
- ```
139
-
140
- In plain English, this Entry means:
141
-
142
- ```text
143
- hint
144
-
145
- Input:
146
- query
147
- - string
148
- - required
149
-
150
- limit
151
- - positive integer
152
- - at most 100
153
- - defaults to 20
154
-
155
- Projection:
156
- {
157
- query: string,
158
- limit: number
159
- }
160
- ```
161
-
162
- There is no CLI or MCP logic here. The Entry describes the operation itself.
163
-
164
- Because `limit` has a default, `values.limit` is typed as `number`, not
165
- `number | undefined`. Only optional parameters without a default remain
166
- optional in `values`.
167
46
 
168
- ## 2. Define how each channel supplies input: Bindings
47
+ export type GreetingContext = {
48
+ readonly greeting: string;
49
+ };
169
50
 
170
- A CLI Binding maps the Entry parameters to argv syntax:
171
-
172
- ```ts
173
- import { bindCliEntry } from "@alpacakit/channels/cli";
51
+ export function greet(
52
+ input: ReturnType<typeof GreetEntry.project>,
53
+ context: GreetingContext,
54
+ ) {
55
+ return { message: `${context.greeting}, ${input.name}!` };
56
+ }
174
57
 
175
- const HintCliBinding = bindCliEntry({
176
- parameters: [
177
- { kind: "positional", parameterName: "query" },
178
- { kind: "option", parameterName: "limit", flags: ["--limit", "-n"] },
58
+ export const GreetEndpoint = defineEntryEndpoint(GreetEntry, {
59
+ handler: greet,
60
+ channels: [
61
+ bindHttpEntry({ method: "GET" }),
62
+ bindMcpEntry({ name: "greet" }),
63
+ bindCliEntry({
64
+ parameters: [{ kind: "option", parameterName: "name" }],
65
+ }),
179
66
  ],
180
67
  });
181
- ```
182
-
183
- A binding row only needs `kind` and `parameterName`. The rest is derived from
184
- the parameter name and only stated when you want to override it:
185
-
186
- - `query` needs no `valueName` (help shows `QUERY`) and no `variadic` (defaults
187
- to `false`).
188
- - `limit` would default to the single flag `--limit`; here `flags` is given
189
- only to add the `-n` alias.
190
68
 
191
- The mapping is:
192
-
193
- ```text
194
- my-app hint "entry tree" --limit 5
195
- └────┬────┘ └───┬───┘
196
- │ │
197
- ▼ ▼
198
- query limit
199
- ```
200
-
201
- Do not repeat `query`'s string codec or `limit`'s default in the Binding. Those
202
- facts already belong to the Entry.
203
-
204
- An MCP Binding selects the Entry parameters exposed as tool input:
205
-
206
- ```ts
207
- import { bindMcpEntry } from "@alpacakit/channels/mcp";
208
-
209
- const HintMcpBinding = bindMcpEntry("query", "limit");
210
- ```
211
-
212
- The mapping is:
213
-
214
- ```text
215
- {
216
- "query": "entry tree",
217
- "limit": 5
218
- }
219
- │ │
220
- ▼ ▼
221
- query limit
222
- ```
223
-
224
- ## 3. Bundle the Entry and Bindings: Endpoint
225
-
226
- An Endpoint is the public execution identity for the operation:
227
-
228
- ```ts
229
- import { defineEntryEndpoint } from "@alpacakit/channels";
230
-
231
- const HintEndpoint = defineEntryEndpoint(
232
- HintEntry,
233
- HintCliBinding,
234
- HintMcpBinding,
235
- );
236
- ```
237
-
238
- Its structure is:
239
-
240
- ```text
241
- HintEndpoint
242
- │
243
- ├── operation
244
- │ └── HintEntry
245
- │
246
- ├── CLI input mapping
247
- │ ├── query is positional
248
- │ └── limit is --limit / -n
249
- │
250
- └── MCP input mapping
251
- ├── expose query
252
- └── expose limit
253
- ```
254
-
255
- Keep the object returned by `defineEntryEndpoint()`. CLI result narrowing and
256
- all MCP runtime APIs use endpoint identity. Do not reconstruct an equivalent
257
- Endpoint later.
258
-
259
- ## 4. Give the Endpoint an address: Tree
260
-
261
- Place the Endpoint under the `hint` command:
262
-
263
- ```ts
264
- import {
265
- defineEntryNode,
266
- defineEntryTree,
267
- } from "@alpacakit/channels";
268
-
269
- const CommandTree = defineEntryTree(
69
+ export const AppTree = defineEntryTree(
270
70
  defineEntryNode({
271
- name: "my-app",
272
- description: "My application",
273
-
71
+ name: "demo",
72
+ description: "Greeting example",
274
73
  children: [
275
74
  defineEntryNode({
276
- name: "hint",
277
- description: "Find implementation hints",
278
- endpoints: [HintEndpoint],
75
+ name: "greet",
76
+ description: "Greet someone",
77
+ endpoints: [GreetEndpoint],
279
78
  }),
280
79
  ],
281
80
  }),
282
81
  );
283
82
  ```
284
83
 
285
- The resulting hierarchy is:
286
-
287
- ```text
288
- CommandTree
84
+ `GreetEndpoint.handler === greet`. The first argument to `defineEntryEndpoint`
85
+ is the Entry definition; `handler` is the function the host callback calls.
86
+ Bindings describe how to expose the operation and do not contain another
87
+ handler. Definition and collection never invoke it or create its context.
289
88
 
290
- my-app <- root surface name
291
- │
292
- └── hint <- token that appears in argv
293
- │
294
- └── HintEndpoint <- operation executed here
295
- ```
89
+ The second handler argument is your own context: here it contains `greeting`,
90
+ but it can contain a service, authenticated user, or cancellation signal. Create
91
+ it inside the host callback when its dependencies are available. A handler that
92
+ needs no context can accept only the projected input. Handlers may take zero,
93
+ one, or two parameters; rest and three-or-more-parameter signatures are rejected
94
+ by TypeScript.
296
95
 
297
- The root name is the program or surface name. It is not included in the argv
298
- passed to the router.
96
+ ### HTTP with Hono
299
97
 
300
- Independent modules can contribute canonical nodes and merge them without an
301
- intermediate route model:
98
+ Install Hono in the consuming application. Register each route on an existing
99
+ Hono app, use its request API, and choose its response format yourself:
302
100
 
303
101
  ```ts
304
- const children = mergeEntryTreeNodes([
305
- ...configCommandNodes,
306
- ...integrationCommandNodes,
307
- ]);
308
- ```
309
-
310
- Nodes with the same name are merged recursively. Their descriptions and
311
- channel segment overrides must agree; identical endpoint objects are
312
- deduplicated by identity.
102
+ import { Hono } from "hono";
313
103
 
314
- An endpoint placed directly on the root is the default command:
315
-
316
- ```text
317
- my-app
318
- ├── DefaultEndpoint <- runs for []
319
- └── hint
320
- └── HintEndpoint <- runs for ["hint", ...]
321
- ```
322
-
323
- ## 5. Execute it from CLI
324
-
325
- ```ts
326
- import {
327
- createCliRouter,
328
- isCliResolvedEndpoint,
329
- } from "@alpacakit/channels/cli";
330
-
331
- const router = createCliRouter(CommandTree);
332
-
333
- const result = router.resolve([
334
- "hint",
335
- "entry tree",
336
- "--limit",
337
- "5",
338
- ]);
339
-
340
- if (isCliResolvedEndpoint(result, HintEndpoint)) {
341
- console.log(result.projection);
342
- }
343
- ```
344
-
345
- The typed projection is:
346
-
347
- ```ts
348
- {
349
- query: "entry tree",
350
- limit: 5,
351
- }
352
- ```
353
-
354
- At runtime the data flows through these stages:
355
-
356
- ```text
357
- CLI argv
358
- │
359
- │ ["hint", "entry tree", "--limit", "5"]
360
- ▼
361
- CLI Router
362
- │
363
- │ find HintEndpoint from the Tree
364
- ▼
365
- CLI Binding
366
- │
367
- │ query = "entry tree"
368
- │ limit = "5"
369
- ▼
370
- Shared Decoder
371
- │
372
- │ validate query as a string
373
- │ coerce and validate limit as an integer
374
- ▼
375
- HintEntry.project()
376
- │
377
- ▼
378
- {
379
- query: "entry tree",
380
- limit: 5
381
- }
382
- ```
383
-
384
- A stored Tree may contain many differently typed entries, so its endpoints are
385
- deliberately type-erased. `isCliResolvedEndpoint()` checks endpoint identity and
386
- restores the exact projection type for the selected Endpoint.
387
-
388
- The same router also exposes a channel-derived help surface:
389
-
390
- ```ts
391
- const help = router.help;
392
- ```
393
-
394
- For applications with many endpoints, an optional typed registry replaces a
395
- manual identity-check chain:
396
-
397
- ```ts
398
- const executor = createCliExecutor(router, [
399
- handleCliEndpoint(HintEndpoint, ({ projection }) => runHint(projection)),
400
- ]);
401
- ```
402
-
403
- The registry rejects duplicate, missing, and out-of-router endpoints when it is
404
- created. Handlers, output policy, and process exit behavior remain outside the
405
- Entry definition.
406
-
407
- ## 6. Execute the same Endpoint from MCP
408
-
409
- Compile the Endpoint once, then reuse the handle for both registration and
410
- per-invocation decoding:
411
-
412
- ```ts
413
- import {
414
- createMcpEndpoint,
415
- isMcpResolved,
416
- } from "@alpacakit/channels/mcp";
417
-
418
- const HintMcpEndpoint = createMcpEndpoint(HintEndpoint);
419
- ```
420
-
421
- `createMcpEndpoint()` validates the MCP binding once and returns a handle:
422
-
423
- ```text
424
- HintMcpEndpoint
425
- │
426
- ├── inputSchema <- raw Zod object shape for tool registration
427
- │
428
- └── parse(raw) <- decode + project one invocation
429
- ```
430
-
431
- `inputSchema` is a raw Zod object shape suitable for MCP tool registration. It
432
- contains descriptions, validation, and requiredness derived from `HintEntry`.
433
-
434
- Decode and project an MCP invocation through the same handle:
435
-
436
- ```ts
437
- const result = HintMcpEndpoint.parse({
438
- query: "entry tree",
439
- limit: 5,
440
- });
441
-
442
- if (isMcpResolved(result)) {
443
- console.log(result.projection);
444
- }
445
- ```
446
-
447
- Create the handle once (typically at startup) and reuse it for every request.
448
- The binding is compiled a single time, so `parse()` does no per-invocation
449
- recompilation, and an invalid binding throws `EntryTreeDefinitionError`
450
- eagerly — exactly like `createCliRouter()`.
451
-
452
- The MCP runtime path is:
453
-
454
- ```text
455
- MCP JSON
456
- │
457
- │ { query: "entry tree", limit: 5 }
458
- ▼
459
- HintEndpoint
460
- │
461
- ▼
462
- MCP Binding
463
- │
464
- │ select query and limit
465
- ▼
466
- Shared Decoder
467
- │
468
- │ validate both values
469
- ▼
470
- HintEntry.project()
471
- │
472
- ▼
473
- {
474
- query: "entry tree",
475
- limit: 5
476
- }
477
- ```
478
-
479
- CLI and MCP have different inputs, but they converge on the same decoder,
480
- defaults, and projection:
481
-
482
- ```text
483
- CLI argv ──> CLI Binding ──┐
484
- │
485
- ▼
486
- HintEndpoint
487
- │
488
- ▼
489
- Shared Decoder
490
- │
491
- ▼
492
- HintEntry.project()
493
- │
494
- ▼
495
- same projection
496
- ▲
497
- │
498
- MCP JSON ──> MCP Binding ──┘
499
- ```
500
-
501
- If `limit` is omitted from either channel, both produce the Entry default:
502
-
503
- ```ts
504
- {
505
- query: "entry tree",
506
- limit: 20,
507
- }
508
- ```
509
-
510
- ## Where each decision belongs
511
-
512
- When adding a command, ask these questions in order:
513
-
514
- | Question | Definition |
515
- | --- | --- |
516
- | What values does the operation need? | Entry parameters |
517
- | How are they validated and defaulted? | Entry codecs and requirements |
518
- | What application value should be produced? | Entry `project()` |
519
- | How does CLI supply each value? | CLI Binding |
520
- | Which values does MCP expose? | MCP Binding |
521
- | What object ties the operation and channels together? | Endpoint |
522
- | Under which command path does it live? | Tree |
523
-
524
- Do not put CLI flags, MCP property selection, or command-tree placement on the
525
- Entry. Do not repeat codecs, requiredness, or defaults in Bindings.
526
-
527
- ## Suggested application file layout
528
-
529
- For a larger application, separating these responsibilities keeps the model
530
- easy to navigate:
531
-
532
- ```text
533
- src/
534
- ├── hint-entry.ts
535
- │ └── HintEntry
536
- │ parameters, codecs, defaults, projection
537
- │
538
- ├── hint-endpoint.ts
539
- │ └── HintEndpoint
540
- │ CLI and MCP Bindings
541
- │
542
- ├── command-tree.ts
543
- │ └── CommandTree
544
- │ command hierarchy
545
- │
546
- ├── cli.ts
547
- │ └── createCliRouter(CommandTree)
548
- │
549
- └── mcp.ts
550
- └── createMcpEndpoint(HintEndpoint)
551
- ```
552
-
553
- ## Package entries
554
-
555
- The neutral model and channel adapters are intentionally separate imports:
556
-
557
- | Import | Purpose |
558
- | --- | --- |
559
- | `@alpacakit/channels` | Parameters, codecs, entries, output contracts, and entry-tree builders |
560
- | `@alpacakit/channels/cli` | CLI bindings/coercion, router, typed executor, help traversal, and text renderers |
561
- | `@alpacakit/channels/mcp` | MCP binding, the compiled endpoint handle, output schema projection, and the resolved-result guard |
562
-
563
- `zod` is a peer dependency. In this workspace, depend on the package with
564
- `workspace:*` and use the workspace Zod version.
565
-
566
- ## Parameter and codec reference
567
-
568
- Built-in codecs are:
569
-
570
- - `codecs.string()`
571
- - `codecs.stringList()`
572
- - `codecs.boolean()`
573
- - `codecs.positiveInteger({ max })`
574
- - `codecs.stringEnum(values)`
575
- - `codecs.stringEnumList(values)`
576
-
577
- Use `required()` for required input, `optional()` for an optional value without
578
- a default, and `optional({ default })` for a materialized default.
579
-
580
- Defaults belong to the Entry, not a channel. They are applied even when an
581
- optional parameter is not exposed by the selected channel. A required parameter
582
- must be reachable through every channel binding on that Endpoint.
583
-
584
- `codecs.define({ schema, shape })` is available for advanced codecs. Its shape
585
- is the CLI representation contract and must match the schema's value type.
586
- Arrays are limited to supported scalar element shapes.
587
-
588
- ## CLI reference
589
-
590
- Use `overrideCliSegment()` when the CLI spelling differs from the logical node
591
- name:
592
-
593
- ```ts
594
- import { overrideCliSegment } from "@alpacakit/channels/cli";
595
-
596
- defineEntryNode({
597
- name: "hint",
598
- description: "Find implementation hints",
599
- segments: [overrideCliSegment({ token: "h", aliases: ["hi"] })],
600
- endpoints: [HintEndpoint],
104
+ const app = new Hono();
105
+ collectHttpRoutes(AppTree).forEach((route) => {
106
+ app.on(route.method, route.path, async (context) => {
107
+ const input = route.input.parseQuery(context.req.queries());
108
+ const result = await route.handler(input, { greeting: "Hello" });
109
+ return context.json(result);
110
+ });
601
111
  });
602
112
  ```
603
113
 
604
- CLI binding and routing rules:
605
-
606
- - An option with no `flags` defaults to a single `--<kebab-case parameter
607
- name>` long flag. Provide `flags` only to add aliases or a short flag.
608
- - A value-taking option's `valueName` defaults to the upper snake-cased
609
- parameter name. Boolean flags reject `valueName` because they take no value.
610
- - A positional's `valueName` defaults to the upper snake-cased parameter name,
611
- and `variadic` defaults to `false`. State either only to override it.
612
- - Options use reachable `-x` or `--long` flags. Requiredness comes from the
613
- Entry parameter, not the Binding.
614
- - A defaulted boolean option is a flag by default. `negated: true` adds its
615
- generated `--no-...` long flags.
616
- - A boolean without a default accepts an explicit value unless `booleanMode`
617
- selects another supported behavior.
618
- - Array options may be repeated. `repeatMode: "comma_or_repeat"` also accepts
619
- comma-separated values.
620
- - A variadic positional requires an array codec and must be the final
621
- positional.
622
- - An executable node cannot have both positional arguments and child commands.
623
- - `--` forces all remaining tokens to be parsed as positionals.
624
- - A matching child command wins over an executable parent.
625
- - `coercion: "json"` parses strict JSON before Entry validation.
626
- `coercion: "json_or_string"` preserves malformed JSON as text. Final value
627
- validation always belongs to the Entry codec.
628
-
629
- `createCliRouter()` compiles and validates the complete CLI surface. Invalid
630
- definitions throw `EntryTreeDefinitionError` at construction time.
631
-
632
- The router's `help` property contains the compiled hierarchy, aliases,
633
- render-ready option groups, positionals, requiredness, defaults, repeat modes,
634
- and generated negative flags. Authored option-section names are resolved during
635
- projection, so renderers consume `optionGroups` without performing a join.
636
- `findCliHelpSurface()`, `listCliHelpSurfaces()`, `renderCliHelp()`, and
637
- `formatCliRouterError()` provide standard lookup, traversal, one-surface text
638
- rendering, and diagnostics without writing output or choosing an exit code.
639
- `renderCliHelp()` implements `CliHelpRender`; its context carries the full
640
- selected `programName`, root `executableName`, path, and description preference.
641
- The renderer path uses canonical CLI segment tokens; a directly invoked alias
642
- may still be preserved in `programName` for user-facing usage text.
643
- `--help`, complete-reference framing, and `--version` remain application policy.
644
-
645
- The router returns a discriminated union:
646
-
647
- - `resolved`
648
- - `unknown_command`
649
- - `incomplete_command`
650
- - `invalid_input`
651
- - `internal_error`
652
-
653
- `path` is preserved for diagnostics. Consumers may use the standard formatter
654
- or supply their own wording. Process exit codes remain consumer policy.
655
-
656
- ## MCP reference
657
-
658
- `createMcpEndpoint()` compiles the Endpoint once and returns the handle used for
659
- both schema generation and runtime parsing. Do not keep a second runtime copy of
660
- the MCP Binding.
661
-
662
- ```ts
663
- declare const toolArguments: Record<string, unknown>;
664
-
665
- const mcpEndpoint = createMcpEndpoint(HintEndpoint);
666
- const inputShape = mcpEndpoint.inputSchema;
667
- const result = mcpEndpoint.parse(toolArguments);
668
- ```
669
-
670
- `inputSchema` is a raw Zod object shape. It includes only MCP-bound parameters
671
- and projects their schemas, descriptions, and requiredness.
672
-
673
- `parse()` applies Entry defaults to all parameters, validates bound input, and
674
- runs the Entry projection. It returns:
675
-
676
- - `resolved` with the typed projection
677
- - `invalid_input` for missing input, codec failures, or an `EntryInputError`
678
- deliberately thrown by `project()`
679
- - `internal_error` with the original error for an unexpected `project()` throw
680
-
681
- `isMcpResolved(result)` narrows a parse result to its `resolved` variant, the
682
- MCP counterpart of `isCliResolvedEndpoint()`.
683
-
684
- An invalid Endpoint binding throws `EntryTreeDefinitionError` from
685
- `createMcpEndpoint()` itself, so definition errors surface at construction rather
686
- than on the first invocation.
687
-
688
- MCP tool naming and registration stay with the consumer. Hierarchical MCP tool
689
- names are not currently derived from the Tree.
114
+ `GET /greet?name=Ada` produces `{ "message": "Hello, Ada!" }`.
115
+ For JSON bodies, pass the object from your framework to `route.input.parse`
116
+ instead. Authentication, middleware, server startup, and response handling stay
117
+ in the application; the same registration views also work with Express or
118
+ another router.
690
119
 
691
- ## Output contracts
120
+ ### MCP with the MCP SDK
692
121
 
693
- An output contract is optional. Add one when the same successful result schema
694
- must drive runtime verification and MCP structured output:
122
+ Install the MCP SDK in the consuming application and use your own server and
123
+ transport:
695
124
 
696
125
  ```ts
697
- import {
698
- createOutputContract,
699
- defineEntry,
700
- defineEntryEndpoint,
701
- verifyEntryOutput,
702
- } from "@alpacakit/channels";
703
- import {
704
- bindMcpEntry,
705
- createMcpOutputSchema,
706
- } from "@alpacakit/channels/mcp";
707
- import { z } from "zod";
708
-
709
- const HintOutputSchema = z
710
- .object({
711
- hints: z.array(z.string()).readonly(),
712
- })
713
- .strict()
714
- .readonly();
715
-
716
- const EntryWithOutput = defineEntry({
717
- name: "hint",
718
- parameters: [],
719
- project: () => null,
720
- output: createOutputContract(HintOutputSchema),
721
- });
722
-
723
- const EndpointWithOutput = defineEntryEndpoint(
724
- EntryWithOutput,
725
- bindMcpEntry(),
726
- );
727
-
728
- const outputSchema = createMcpOutputSchema(EndpointWithOutput);
126
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
729
127
 
730
- const verified = verifyEntryOutput(EntryWithOutput, {
731
- hints: ["A validated result"],
128
+ const server = new McpServer({ name: "demo", version: "1.0.0" });
129
+ collectMcpTools(AppTree).forEach((tool) => {
130
+ server.registerTool(
131
+ tool.name,
132
+ { inputSchema: tool.input.schema },
133
+ async (values) => {
134
+ // The SDK has already validated and transformed the public values.
135
+ const input = tool.input.project(values);
136
+ const result = await tool.handler(input, { greeting: "Hello" });
137
+ return { content: [{ type: "text", text: result.message }] };
138
+ },
139
+ );
732
140
  });
141
+ // Connect `server` to the transport chosen by your application.
142
+ ```
143
+
144
+ Calling tool `greet` with `{ "name": "Ada" }` returns the greeting as MCP text.
145
+ Use `project`, not `parse`, after SDK validation so defaults and transforms are
146
+ not applied twice. The SDK's callback also lets the application use MCP request
147
+ metadata or cancellation when constructing the shared handler's context.
148
+
149
+ ### CLI with your own parser
150
+
151
+ The collected view is a tree of nodes, so a CLI host walks it once to learn
152
+ which paths are executable and what each command accepts. Argv parsing stays in
153
+ the application. Use the input plan to convert and validate supplied values,
154
+ then call the shared handler in your host callback:
155
+
156
+ ```ts
157
+ const view = collectCliCommands(AppTree);
158
+ const commands = new Map<
159
+ string,
160
+ (supplied: Record<string, unknown>) => Promise<string>
161
+ >();
162
+
163
+ function register(node: typeof view.root): void {
164
+ node.commands.forEach((spec) => {
165
+ // `spec.parameters` describes the flags and positionals your parser must
166
+ // accept for this command.
167
+ commands.set(spec.path.join(" "), async (supplied) => {
168
+ const input = spec.input.parseOptions(supplied);
169
+ // Build the handler context here, where its dependencies are available.
170
+ const result = await spec.handler(input, { greeting: "Hello" });
171
+ return result.message;
172
+ });
173
+ });
174
+ node.children.forEach(register);
175
+ }
733
176
 
734
- void outputSchema;
735
- void verified;
736
- ```
737
-
738
- `createMcpOutputSchema()` requires an object output contract. Invalid output
739
- passed to `verifyEntryOutput()` throws `EntryOutputContractError`.
740
-
741
- ## Error boundaries
742
-
743
- - `EntryTreeDefinitionError` means the Tree or a channel Binding is invalid.
744
- Treat it as a startup or programmer error.
745
- - `EntryInputError` may be thrown inside `project()` for an expected semantic
746
- input rejection. It becomes `invalid_input`. Pass
747
- `{ parameterName: "name" }` to associate all issues with one Entry parameter;
748
- an unknown parameter name is an `internal_error` programmer failure.
749
- - Codec and required-input failures become `invalid_input`.
750
- - Any other projection exception becomes `internal_error` and retains the
751
- original error. It is never reported as caller input failure.
752
- - `EntryOutputContractError` means a handler returned a value outside its
753
- declared success contract.
754
-
755
- ## Scope
756
-
757
- The package currently provides the neutral entry tree, CLI router, optional CLI
758
- executor and text renderers, and MCP endpoint adapter. It does not provide an
759
- HTTP router, process exit policy, automatic `--help`/`--version` handling, MCP
760
- tool registration, authentication, or a network listener.
761
-
762
- Every command surface uses `defineEntryEndpoint()` with `bindCliEntry()` and
763
- `bindMcpEntry()`. There is no separate legacy option-descriptor API.
764
-
765
- ## Zod peer and source linking
766
-
767
- Packed consumers are type-checked against the declared `zod: ^4.3.6` peer. A
768
- packed channels build was verified with both Zod 4.3.6 and 4.4.3. Source-linking
769
- the package is different: if channels resolves its development Zod 4.3.6 while
770
- the consumer resolves 4.4.3, TypeScript can expand the two distinct Zod type
771
- graphs until it reports excessive depth or exhausts memory. When linking source,
772
- use exactly one shared Zod installation/version, or consume a packed tarball.
177
+ register(view.root);
178
+
179
+ // Your parser selects the path and supplies only the options the user typed.
180
+ const greetCommand = commands.get("greet");
181
+ if (!greetCommand) throw new Error("Missing greet command");
182
+ console.log(await greetCommand({ name: "Ada" })); // Hello, Ada!
183
+ ```
184
+
185
+ This demonstrates the handoff after parsing `demo greet --name Ada`; it does
186
+ not parse argv. Supplying no `name` key applies the authored default `world`.
187
+ Pass `parseOptions` only the canonical parameter names the user explicitly
188
+ supplied — defaults inserted by your parser would override the authored ones.
189
+ The walk visits commands in nested groups and executable parents with children;
190
+ the tree also retains group-only nodes for your host's help and registration.
191
+
192
+ `collectCliCommands` accepts a Node or a Tree. A collection requires a context
193
+ satisfying all its handlers. Collect modules with incompatible contexts
194
+ separately, and create each module's context inside its invocation callback.
195
+ The host owns output formatting and resource cleanup.
196
+
197
+ The complete [greeting example](./examples/greeting.ts) turns the same view into
198
+ a Commander program, including flag registration, `getOptionValueSource`
199
+ filtering so only user-supplied options reach `parseOptions`, and process error
200
+ handling. Its adapter handles value-taking options only, which is all the
201
+ greeting tree uses; register boolean flags and positionals from
202
+ `spec.parameters` with your parser's corresponding APIs. That file is shipped
203
+ with the package and is compiled and invoked from its packed artifact. The
204
+ [book-search example](./examples/book-search.ts) also demonstrates a typed
205
+ output contract and domain outcome union.
206
+
207
+ ## Input and failure handling
208
+
209
+ | Host input | Method | Work performed |
210
+ | --- | --- | --- |
211
+ | CLI values explicitly supplied by the user | `input.parseOptions(raw)` | String coercion, validation/defaults, then Entry projection |
212
+ | HTTP query strings or string arrays | `input.parseQuery(raw)` | Query coercion, validation/defaults, then Entry projection |
213
+ | Unvalidated JSON object | `input.parse(raw)` | Validation/defaults, then Entry projection |
214
+ | Values already validated by `input.schema` | `input.project(values)` | Entry projection and hidden defaults, without revalidating public values |
215
+
216
+ The host decides how to report invalid input and operation failures. Catch
217
+ `EntryInputError` around the input-processing call when mapping it to HTTP 400,
218
+ a CLI usage error, or an MCP error result. Keep the handler call outside that
219
+ catch: an exception from the operation or its dependencies is a separate
220
+ failure, even if it has the same error class. Response envelopes, logging,
221
+ resource cleanup, and process exit remain application responsibilities.
222
+
223
+ `parseOptions` must receive only canonical parameter names explicitly supplied
224
+ by the user. Do not pass defaults inserted by the external parser.
225
+ `parseQuery` accepts a scalar string or one-element string array, rejects
226
+ duplicate scalar values, and treats a scalar value for an array parameter as a
227
+ one-element array. `input.parse` is the JSON-value path and performs no CLI or
228
+ query string coercion.
229
+
230
+ `input.schema` performs public parameter validation, authored raw defaults, and
231
+ codec transforms. It never performs Entry projection. After an SDK validates
232
+ with that schema, call `input.project` once; otherwise call `input.parse` once.
233
+ Authored defaults are represented by the public schema (including input-mode
234
+ JSON Schema conversion) before codec transforms, and are copied for each
235
+ invocation, including object and array defaults. Non-object JSON containers are
236
+ rejected as `EntryInputError` by every parse path.
237
+
238
+ ## Tree placement
239
+
240
+ A Tree root is a container: its CLI path is `[]`, its HTTP path is `/`, and its
241
+ CLI token is the program display name. Collecting a Node directly creates a
242
+ virtual CLI root and includes the Node's own segment (`[token]` and
243
+ `/segment`). Empty and group-only nodes remain visible in the CLI view.
244
+
245
+ Use `mergeEntryTreeNodes` to explicitly merge compatible same-name nodes.
246
+ Collectors deduplicate the same Endpoint at the same placement, retain the same
247
+ Endpoint at different CLI/HTTP paths, and emit one MCP tool per Endpoint.
248
+ Cycles, conflicting paths, invalid segments, duplicate channel bindings,
249
+ unreachable required parameters, and HTTP/MCP/CLI ownership collisions throw
250
+ `EntryTreeDefinitionError` before a host registers anything.
251
+
252
+ See the shipped [examples guide](./examples/README.md) for the two examples and
253
+ the division of responsibility between channels and the host.