@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.
- package/README.md +204 -723
- package/dist/binding-compiler.d.ts +12 -2
- package/dist/binding-compiler.d.ts.map +1 -1
- package/dist/binding-compiler.js +25 -0
- package/dist/cli-coercion.d.ts +1 -1
- package/dist/cli-coercion.d.ts.map +1 -1
- package/dist/cli-coercion.js +14 -29
- package/dist/cli-compiler.d.ts +4 -17
- package/dist/cli-compiler.d.ts.map +1 -1
- package/dist/cli-compiler.js +78 -39
- package/dist/cli-help-projection.d.ts.map +1 -1
- package/dist/cli-help-projection.js +41 -36
- package/dist/cli-model.d.ts +50 -65
- package/dist/cli-model.d.ts.map +1 -1
- package/dist/cli-model.js +26 -34
- package/dist/cli-tree.d.ts +6 -3
- package/dist/cli-tree.d.ts.map +1 -1
- package/dist/cli-tree.js +161 -387
- package/dist/cli.d.ts +1 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -5
- package/dist/codecs.d.ts +20 -18
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +21 -31
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +8 -3
- package/dist/definition-error.d.ts +5 -0
- package/dist/definition-error.d.ts.map +1 -0
- package/dist/definition-error.js +8 -0
- package/dist/factory.d.ts +25 -11
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +28 -18
- package/dist/http-compiler.d.ts +15 -0
- package/dist/http-compiler.d.ts.map +1 -0
- package/dist/http-compiler.js +37 -0
- package/dist/http-model.d.ts +42 -0
- package/dist/http-model.d.ts.map +1 -0
- package/dist/http-model.js +20 -0
- package/dist/http-tree.d.ts +7 -0
- package/dist/http-tree.d.ts.map +1 -0
- package/dist/http-tree.js +181 -0
- package/dist/http.d.ts +3 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/input-compiler.d.ts +13 -0
- package/dist/input-compiler.d.ts.map +1 -0
- package/dist/input-compiler.js +122 -0
- package/dist/introspection.d.ts +1 -1
- package/dist/introspection.d.ts.map +1 -1
- package/dist/introspection.js +30 -16
- package/dist/mcp.d.ts +25 -24
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +87 -65
- package/dist/model.d.ts +44 -42
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +44 -9
- package/dist/owned.d.ts +12 -0
- package/dist/owned.d.ts.map +1 -0
- package/dist/owned.js +61 -0
- package/dist/registration.d.ts +41 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +14 -0
- package/dist/runtime.d.ts +2 -35
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -127
- package/dist/schema.d.ts +11 -8
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +110 -47
- package/dist/string-coercion.d.ts +4 -0
- package/dist/string-coercion.d.ts.map +1 -0
- package/dist/string-coercion.js +24 -0
- package/dist/tree-internal.d.ts +4 -2
- package/dist/tree-internal.d.ts.map +1 -1
- package/dist/tree-internal.js +5 -2
- package/dist/values.d.ts +1 -6
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +1 -6
- package/examples/README.md +28 -0
- package/examples/book-search.ts +151 -0
- package/examples/greeting.ts +193 -0
- package/package.json +9 -4
- package/dist/cli-application.d.ts +0 -40
- package/dist/cli-application.d.ts.map +0 -1
- package/dist/cli-application.js +0 -90
- package/dist/cli-executor.d.ts +0 -25
- package/dist/cli-executor.d.ts.map +0 -1
- package/dist/cli-executor.js +0 -44
- package/dist/cli-presentation.d.ts +0 -32
- package/dist/cli-presentation.d.ts.map +0 -1
- package/dist/cli-presentation.js +0 -293
- package/dist/cli-result-envelope.d.ts +0 -104
- package/dist/cli-result-envelope.d.ts.map +0 -1
- package/dist/cli-result-envelope.js +0 -137
- package/dist/cli-schema.d.ts +0 -4
- package/dist/cli-schema.d.ts.map +0 -1
- package/dist/cli-schema.js +0 -7
package/README.md
CHANGED
|
@@ -1,772 +1,253 @@
|
|
|
1
1
|
# @alpacakit/channels
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
for
|
|
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
|
-
|
|
7
|
-
CLI and MCP.
|
|
8
|
+
## Public entry points
|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
name: "
|
|
116
|
-
|
|
34
|
+
export const GreetEntry = defineEntry({
|
|
35
|
+
name: "greet",
|
|
117
36
|
parameters: [
|
|
118
37
|
defineParameter({
|
|
119
|
-
name: "
|
|
38
|
+
name: "name",
|
|
120
39
|
codec: codecs.string(),
|
|
121
|
-
requirement:
|
|
122
|
-
description: "
|
|
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
|
-
|
|
47
|
+
export type GreetingContext = {
|
|
48
|
+
readonly greeting: string;
|
|
49
|
+
};
|
|
169
50
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
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
|
-
|
|
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: "
|
|
272
|
-
description: "
|
|
273
|
-
|
|
71
|
+
name: "demo",
|
|
72
|
+
description: "Greeting example",
|
|
274
73
|
children: [
|
|
275
74
|
defineEntryNode({
|
|
276
|
-
name: "
|
|
277
|
-
description: "
|
|
278
|
-
endpoints: [
|
|
75
|
+
name: "greet",
|
|
76
|
+
description: "Greet someone",
|
|
77
|
+
endpoints: [GreetEndpoint],
|
|
279
78
|
}),
|
|
280
79
|
],
|
|
281
80
|
}),
|
|
282
81
|
);
|
|
283
82
|
```
|
|
284
83
|
|
|
285
|
-
The
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
passed to the router.
|
|
96
|
+
### HTTP with Hono
|
|
299
97
|
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
120
|
+
### MCP with the MCP SDK
|
|
692
121
|
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
731
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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.
|