@procedures/client 0.8.0 → 0.8.1

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 (188) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +95 -8
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +4 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +11 -1
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/client/types.d.ts +19 -0
  9. package/dist/adapters/http/client/types.d.ts.map +1 -1
  10. package/dist/adapters/http/server/cors.d.ts +64 -0
  11. package/dist/adapters/http/server/cors.d.ts.map +1 -0
  12. package/dist/adapters/http/server/cors.js +113 -0
  13. package/dist/adapters/http/server/cors.js.map +1 -0
  14. package/dist/adapters/http/server/handler.d.ts +136 -0
  15. package/dist/adapters/http/server/handler.d.ts.map +1 -0
  16. package/dist/adapters/http/server/handler.js +495 -0
  17. package/dist/adapters/http/server/handler.js.map +1 -0
  18. package/dist/adapters/http/server/strategies.d.ts +14 -5
  19. package/dist/adapters/http/server/strategies.d.ts.map +1 -1
  20. package/dist/adapters/http/server/strategies.js.map +1 -1
  21. package/dist/adapters/http/server/transport.d.ts +30 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +48 -432
  24. package/dist/adapters/http/server/transport.js.map +1 -1
  25. package/dist/adapters/http/server/types.d.ts +13 -0
  26. package/dist/adapters/http/server/types.d.ts.map +1 -1
  27. package/dist/adapters/websocket/server/transport.d.ts +46 -1
  28. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/server/transport.js +152 -15
  30. package/dist/adapters/websocket/server/transport.js.map +1 -1
  31. package/dist/adapters/websocket/server/types.d.ts +10 -3
  32. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  33. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  34. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  35. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  36. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  37. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  38. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  39. package/dist/adapters/websocket/server/upgrade.js +49 -0
  40. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  41. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  42. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  43. package/dist/adapters/websocket/shared/rpc.js +8 -2
  44. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  45. package/dist/browser.d.ts +12 -4
  46. package/dist/browser.d.ts.map +1 -1
  47. package/dist/browser.js +11 -2
  48. package/dist/browser.js.map +1 -1
  49. package/dist/client/client.d.ts +43 -0
  50. package/dist/client/client.d.ts.map +1 -1
  51. package/dist/client/client.js +69 -1
  52. package/dist/client/client.js.map +1 -1
  53. package/dist/events/publisher.d.ts +14 -0
  54. package/dist/events/publisher.d.ts.map +1 -1
  55. package/dist/events/publisher.js +42 -4
  56. package/dist/events/publisher.js.map +1 -1
  57. package/dist/events/rows.d.ts +31 -0
  58. package/dist/events/rows.d.ts.map +1 -0
  59. package/dist/events/rows.js +39 -0
  60. package/dist/events/rows.js.map +1 -0
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/node.d.ts +14 -0
  66. package/dist/node.d.ts.map +1 -1
  67. package/dist/node.js +9 -0
  68. package/dist/node.js.map +1 -1
  69. package/dist/procedures/capability.d.ts +6 -0
  70. package/dist/procedures/capability.d.ts.map +1 -1
  71. package/dist/procedures/capability.js +25 -0
  72. package/dist/procedures/capability.js.map +1 -1
  73. package/dist/procedures/collection/changes.d.ts +94 -0
  74. package/dist/procedures/collection/changes.d.ts.map +1 -0
  75. package/dist/procedures/collection/changes.js +182 -0
  76. package/dist/procedures/collection/changes.js.map +1 -0
  77. package/dist/procedures/collection/index.d.ts +8 -3
  78. package/dist/procedures/collection/index.d.ts.map +1 -1
  79. package/dist/procedures/collection/index.js +6 -1
  80. package/dist/procedures/collection/index.js.map +1 -1
  81. package/dist/procedures/collection/procedures.d.ts +39 -2
  82. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  83. package/dist/procedures/collection/procedures.js +111 -8
  84. package/dist/procedures/collection/procedures.js.map +1 -1
  85. package/dist/procedures/collection/query.d.ts +246 -0
  86. package/dist/procedures/collection/query.d.ts.map +1 -0
  87. package/dist/procedures/collection/query.js +854 -0
  88. package/dist/procedures/collection/query.js.map +1 -0
  89. package/dist/procedures/collection/schemas.d.ts +50 -0
  90. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  91. package/dist/procedures/collection/schemas.js +144 -6
  92. package/dist/procedures/collection/schemas.js.map +1 -1
  93. package/dist/procedures/collection/storage.d.ts +58 -6
  94. package/dist/procedures/collection/storage.d.ts.map +1 -1
  95. package/dist/procedures/collection/storage.js +23 -0
  96. package/dist/procedures/collection/storage.js.map +1 -1
  97. package/dist/procedures/elicit.d.ts +190 -0
  98. package/dist/procedures/elicit.d.ts.map +1 -0
  99. package/dist/procedures/elicit.js +127 -0
  100. package/dist/procedures/elicit.js.map +1 -0
  101. package/dist/procedures/hints.d.ts +43 -0
  102. package/dist/procedures/hints.d.ts.map +1 -0
  103. package/dist/procedures/hints.js +74 -0
  104. package/dist/procedures/hints.js.map +1 -0
  105. package/dist/procedures/index.d.ts +9 -2
  106. package/dist/procedures/index.d.ts.map +1 -1
  107. package/dist/procedures/index.js +5 -1
  108. package/dist/procedures/index.js.map +1 -1
  109. package/dist/procedures/invoke.d.ts +20 -0
  110. package/dist/procedures/invoke.d.ts.map +1 -1
  111. package/dist/procedures/invoke.js +13 -0
  112. package/dist/procedures/invoke.js.map +1 -1
  113. package/dist/procedures/progress.d.ts +24 -0
  114. package/dist/procedures/progress.d.ts.map +1 -0
  115. package/dist/procedures/progress.js +11 -0
  116. package/dist/procedures/progress.js.map +1 -0
  117. package/dist/procedures/registry.d.ts.map +1 -1
  118. package/dist/procedures/registry.js +5 -0
  119. package/dist/procedures/registry.js.map +1 -1
  120. package/dist/procedures/types.d.ts +42 -0
  121. package/dist/procedures/types.d.ts.map +1 -1
  122. package/dist/procedures/types.js.map +1 -1
  123. package/dist/procedures/untrusted.d.ts +28 -0
  124. package/dist/procedures/untrusted.d.ts.map +1 -0
  125. package/dist/procedures/untrusted.js +32 -0
  126. package/dist/procedures/untrusted.js.map +1 -0
  127. package/dist/server/browser-guard.d.ts +12 -2
  128. package/dist/server/browser-guard.d.ts.map +1 -1
  129. package/dist/server/browser-guard.js +43 -6
  130. package/dist/server/browser-guard.js.map +1 -1
  131. package/dist/server/host-policy.d.ts +10 -1
  132. package/dist/server/host-policy.d.ts.map +1 -1
  133. package/dist/server/host-policy.js +5 -0
  134. package/dist/server/host-policy.js.map +1 -1
  135. package/dist/server/index.d.ts +1 -1
  136. package/dist/server/index.d.ts.map +1 -1
  137. package/dist/server/index.js.map +1 -1
  138. package/dist/server/procedure-server.d.ts +38 -6
  139. package/dist/server/procedure-server.d.ts.map +1 -1
  140. package/dist/server/procedure-server.js +65 -13
  141. package/dist/server/procedure-server.js.map +1 -1
  142. package/dist/server/request-auth.d.ts +51 -4
  143. package/dist/server/request-auth.d.ts.map +1 -1
  144. package/dist/server/request-auth.js +116 -5
  145. package/dist/server/request-auth.js.map +1 -1
  146. package/dist/server/types.d.ts +73 -5
  147. package/dist/server/types.d.ts.map +1 -1
  148. package/dist/server/types.js.map +1 -1
  149. package/package.json +5 -1
  150. package/src/adapters/http/client/transport.ts +11 -1
  151. package/src/adapters/http/client/types.ts +22 -0
  152. package/src/adapters/http/server/cors.ts +144 -0
  153. package/src/adapters/http/server/handler.ts +624 -0
  154. package/src/adapters/http/server/strategies.ts +16 -6
  155. package/src/adapters/http/server/transport.ts +55 -478
  156. package/src/adapters/http/server/types.ts +15 -0
  157. package/src/adapters/websocket/server/transport.ts +166 -16
  158. package/src/adapters/websocket/server/types.ts +11 -3
  159. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  160. package/src/adapters/websocket/server/upgrade.ts +88 -0
  161. package/src/adapters/websocket/shared/rpc.ts +9 -2
  162. package/src/browser.ts +79 -2
  163. package/src/client/client.ts +83 -1
  164. package/src/events/publisher.ts +48 -4
  165. package/src/events/rows.ts +48 -0
  166. package/src/index.ts +1 -1
  167. package/src/node.ts +21 -0
  168. package/src/procedures/capability.ts +21 -0
  169. package/src/procedures/collection/changes.ts +245 -0
  170. package/src/procedures/collection/index.ts +53 -1
  171. package/src/procedures/collection/procedures.ts +163 -7
  172. package/src/procedures/collection/query.ts +1009 -0
  173. package/src/procedures/collection/schemas.ts +196 -7
  174. package/src/procedures/collection/storage.ts +78 -6
  175. package/src/procedures/elicit.ts +320 -0
  176. package/src/procedures/hints.ts +91 -0
  177. package/src/procedures/index.ts +78 -2
  178. package/src/procedures/invoke.ts +29 -0
  179. package/src/procedures/progress.ts +25 -0
  180. package/src/procedures/registry.ts +4 -0
  181. package/src/procedures/types.ts +49 -0
  182. package/src/procedures/untrusted.ts +50 -0
  183. package/src/server/browser-guard.ts +48 -6
  184. package/src/server/host-policy.ts +13 -1
  185. package/src/server/index.ts +3 -0
  186. package/src/server/procedure-server.ts +82 -14
  187. package/src/server/request-auth.ts +128 -6
  188. package/src/server/types.ts +82 -5
@@ -0,0 +1,320 @@
1
+ /**
2
+ * The questions of a procedure to the person at the caller (CLI-12).
3
+ *
4
+ * A face of a host (MCP, a terminal) can ask the person at the caller for input or for a
5
+ * confirmation during a call. The face puts an `Elicit` function on the request, and the
6
+ * procedure gets it as `ctx.elicit`. A face that cannot ask gives no function. Then the helpers
7
+ * `elicitInput` and `elicitConfirmation` give the action `unavailable` with a reason, so a
8
+ * procedure always gets a clear answer.
9
+ *
10
+ * The operator can answer some questions before the calls (`ElicitationPolicy.operatorConsent`):
11
+ * a consent that the operator gives when the server starts. The person comes first: the operator
12
+ * answers only when the face cannot ask the person. When the face can ask, and the question
13
+ * fails (a timeout, a lost connection), the answer is `unavailable`: the silence of a person is
14
+ * not a consent.
15
+ */
16
+
17
+ import type { Duration } from "../duration.js";
18
+ import type { ProcedureContext } from "./types.js";
19
+
20
+ // =============================================================================
21
+ // The fields of a form
22
+ // =============================================================================
23
+
24
+ /** The texts of a field that the face shows to the person. */
25
+ export interface ElicitFieldBase {
26
+ /** The short name of the field. */
27
+ readonly title?: string | undefined;
28
+ /** The text that tells what the field is for. */
29
+ readonly description?: string | undefined;
30
+ }
31
+
32
+ /** A field of free text. */
33
+ export interface ElicitTextField extends ElicitFieldBase {
34
+ readonly type: "string";
35
+ readonly enum?: undefined;
36
+ readonly minLength?: number | undefined;
37
+ readonly maxLength?: number | undefined;
38
+ readonly format?: "email" | "uri" | "date" | "date-time" | undefined;
39
+ readonly default?: string | undefined;
40
+ }
41
+
42
+ /** A field of a number. */
43
+ export interface ElicitNumberField extends ElicitFieldBase {
44
+ readonly type: "number" | "integer";
45
+ readonly minimum?: number | undefined;
46
+ readonly maximum?: number | undefined;
47
+ readonly default?: number | undefined;
48
+ }
49
+
50
+ /** A field of yes or no. */
51
+ export interface ElicitBooleanField extends ElicitFieldBase {
52
+ readonly type: "boolean";
53
+ readonly default?: boolean | undefined;
54
+ }
55
+
56
+ /** A field of one choice from a list. `enumNames` gives the text of each choice. */
57
+ export interface ElicitChoiceField extends ElicitFieldBase {
58
+ readonly type: "string";
59
+ readonly enum: readonly string[];
60
+ readonly enumNames?: readonly string[] | undefined;
61
+ readonly default?: string | undefined;
62
+ }
63
+
64
+ /** A field of some choices from a list. */
65
+ export interface ElicitChoicesField extends ElicitFieldBase {
66
+ readonly type: "array";
67
+ readonly items: { readonly type: "string"; readonly enum: readonly string[] };
68
+ readonly minItems?: number | undefined;
69
+ readonly maxItems?: number | undefined;
70
+ readonly default?: readonly string[] | undefined;
71
+ }
72
+
73
+ /** A field of a form: the primitive fields of MCP elicitation. A form has no nested fields. */
74
+ export type ElicitField = ElicitTextField | ElicitNumberField | ElicitBooleanField | ElicitChoiceField | ElicitChoicesField;
75
+
76
+ /** The value of a field in an answer. */
77
+ export type ElicitValue = string | number | boolean | readonly string[];
78
+
79
+ // =============================================================================
80
+ // Questions and answers
81
+ // =============================================================================
82
+
83
+ /** A question with a form: the person fills in the fields. */
84
+ export interface ElicitFormRequest {
85
+ readonly mode?: "form" | undefined;
86
+ /** The question, as the face shows it to the person. */
87
+ readonly message: string;
88
+ /** The fields of the answer, by name. */
89
+ readonly fields: Readonly<Record<string, ElicitField>>;
90
+ /** The names of the fields that an accepted answer must have. */
91
+ readonly required?: readonly string[] | undefined;
92
+ /**
93
+ * The key of the consent that the question asks for, for example `"user-provided"`. With a
94
+ * key, the operator can give the consent before the calls (`ElicitationPolicy.operatorConsent`).
95
+ * Without a key, only the person can answer.
96
+ */
97
+ readonly consent?: string | undefined;
98
+ }
99
+
100
+ /**
101
+ * A question with a URL: the person opens the URL and completes a step outside the face (a
102
+ * sign-in, a payment). The answer tells only if the person agreed to open it.
103
+ */
104
+ export interface ElicitUrlRequest {
105
+ readonly mode: "url";
106
+ readonly message: string;
107
+ readonly url: string;
108
+ /** The id of the question. The default is a random id. */
109
+ readonly id?: string | undefined;
110
+ }
111
+
112
+ /** A question to the person at the caller. */
113
+ export type ElicitRequest = ElicitFormRequest | ElicitUrlRequest;
114
+
115
+ /** The kind of a question: a form, or a URL. */
116
+ export type ElicitMode = "form" | "url";
117
+
118
+ /**
119
+ * What the answer of a question is:
120
+ * - `accept`: the person, or the operator, gave the answer.
121
+ * - `decline`: the person said no.
122
+ * - `cancel`: the person closed the question without a choice.
123
+ * - `unavailable`: no one can answer. The face cannot ask the person, and no consent of the
124
+ * operator applies. `reason` tells why.
125
+ */
126
+ export type ElicitAction = "accept" | "decline" | "cancel" | "unavailable";
127
+
128
+ /** The answer of a question. */
129
+ export interface ElicitResult {
130
+ readonly action: ElicitAction;
131
+ /** The values of the fields, for `accept`. */
132
+ readonly content?: Readonly<Record<string, ElicitValue>> | undefined;
133
+ /** Who answered: the person, or the operator (a consent of the start). Absent for `unavailable`. */
134
+ readonly by?: "person" | "operator" | undefined;
135
+ /** Why no one answered, for `unavailable`. */
136
+ readonly reason?: string | undefined;
137
+ }
138
+
139
+ /** The options of one question. */
140
+ export interface ElicitOptions {
141
+ /** When the signal aborts, the question stops. The default is the signal of the call. */
142
+ readonly signal?: AbortSignal | undefined;
143
+ /** The time that the face waits for the answer. The face has a default (MCP: 5 minutes). */
144
+ readonly timeout?: Duration | undefined;
145
+ }
146
+
147
+ /**
148
+ * The function that asks the person at the caller (`ctx.elicit`). `modes` tells which kinds of
149
+ * question the face can ask the person. An empty list: only the operator answers.
150
+ */
151
+ export interface Elicit {
152
+ (request: ElicitRequest, options?: ElicitOptions): Promise<ElicitResult>;
153
+ readonly modes: readonly ElicitMode[];
154
+ }
155
+
156
+ /** This function makes an `Elicit` function from the modes of a face and its ask function. */
157
+ export function createElicit(
158
+ modes: readonly ElicitMode[],
159
+ ask: (request: ElicitRequest, options: ElicitOptions) => Promise<ElicitResult>,
160
+ ): Elicit {
161
+ const elicit = (request: ElicitRequest, options?: ElicitOptions): Promise<ElicitResult> => ask(request, options ?? {});
162
+ return Object.assign(elicit, { modes: Object.freeze([...modes]) });
163
+ }
164
+
165
+ // =============================================================================
166
+ // The policy of the host
167
+ // =============================================================================
168
+
169
+ /** The rules of a host for the questions of its procedures. */
170
+ export interface ElicitationPolicy {
171
+ /** False: the face does not ask the person, also when it can. The default is true. */
172
+ readonly ask?: boolean | undefined;
173
+ /**
174
+ * The consents that the operator gives when the server starts: `true` for each question with a
175
+ * consent key, or a list of consent keys. The operator answers only when the face cannot ask
176
+ * the person, not when a question of the face fails. The default is no consent. Example:
177
+ * `["user-provided"]` for the flag `--allow-user-provided` of polite-crawler.
178
+ */
179
+ readonly operatorConsent?: boolean | readonly string[] | undefined;
180
+ /** The time that the face waits for an answer, when the question gives no time. */
181
+ readonly timeout?: Duration | undefined;
182
+ }
183
+
184
+ function messageOf(error: unknown): string {
185
+ return error instanceof Error ? error.message : String(error);
186
+ }
187
+
188
+ /** True when an error is the abort of a signal, or the signal aborted. */
189
+ function isAbort(error: unknown, signal: AbortSignal | undefined): boolean {
190
+ return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
191
+ }
192
+
193
+ /**
194
+ * The answer when a question of the face fails: `cancel` for an abort, else `unavailable`. The
195
+ * operator does not answer here: the person can see the question.
196
+ */
197
+ function failedAnswer(error: unknown, signal: AbortSignal | undefined): ElicitResult {
198
+ const reason = `The face could not get the answer of the person: ${messageOf(error)}`;
199
+ return { action: isAbort(error, signal) ? "cancel" : "unavailable", reason };
200
+ }
201
+
202
+ /**
203
+ * The answer of the operator to a question, or undefined. The operator answers a form question
204
+ * with a consent key that the operator gave. Each field of yes or no gets `true`, and another
205
+ * field gets its default. The operator gives no answer when a necessary field has no default.
206
+ */
207
+ export function operatorAnswer(
208
+ request: ElicitRequest,
209
+ consent: boolean | readonly string[] | undefined,
210
+ ): ElicitResult | undefined {
211
+ if (consent === undefined || consent === false || request.mode === "url" || request.consent === undefined) return undefined;
212
+ if (consent !== true && !consent.includes(request.consent)) return undefined;
213
+ const content: Record<string, ElicitValue> = {};
214
+ for (const [name, field] of Object.entries(request.fields)) {
215
+ if (field.type === "boolean") content[name] = true;
216
+ else if (field.default !== undefined) content[name] = field.default;
217
+ else if (request.required?.includes(name)) return undefined;
218
+ }
219
+ return { action: "accept", content, by: "operator" };
220
+ }
221
+
222
+ /**
223
+ * This function applies the policy of a host to the `Elicit` function of a face. The result
224
+ * asks the person when the face can. A failed question gives `unavailable` (`cancel` for an
225
+ * abort). When the face cannot ask, the consent of the operator answers, or the answer is
226
+ * `unavailable`. The function gives undefined when no one can answer: no face and no consent.
227
+ */
228
+ export function elicitWithPolicy(face: Elicit | undefined, policy: ElicitationPolicy = {}): Elicit | undefined {
229
+ const asker = policy.ask === false ? undefined : face;
230
+ const consent = policy.operatorConsent;
231
+ const operator = consent === true || (Array.isArray(consent) && consent.length > 0);
232
+ if (!asker && !operator) return undefined;
233
+ return createElicit(asker?.modes ?? [], async (request, options) => {
234
+ const timed: ElicitOptions =
235
+ options.timeout === undefined && policy.timeout !== undefined ? { ...options, timeout: policy.timeout } : options;
236
+ const mode: ElicitMode = request.mode ?? "form";
237
+ let reason: string;
238
+ if (asker && asker.modes.includes(mode)) {
239
+ // The person can see the question: a timeout or a lost connection is not a consent
240
+ try {
241
+ return await asker(request, timed);
242
+ } catch (error) {
243
+ return failedAnswer(error, options.signal);
244
+ }
245
+ } else if (asker) {
246
+ reason = `The face cannot ask a question of the mode "${mode}"`;
247
+ } else {
248
+ reason = policy.ask === false ? "The host does not ask the person" : "The face of the call cannot ask the person";
249
+ }
250
+ return operatorAnswer(request, consent) ?? { action: "unavailable", reason };
251
+ });
252
+ }
253
+
254
+ // =============================================================================
255
+ // The helpers of a procedure
256
+ // =============================================================================
257
+
258
+ /** The reason of an answer when the context has no `elicit` function. */
259
+ export const NO_ELICITATION_REASON =
260
+ "The face of the call cannot ask the person, and the operator gave no consent for this question";
261
+
262
+ /**
263
+ * This function asks the person at the caller through `ctx.elicit`. It always gives an answer,
264
+ * and it does not throw. The answer is `unavailable` with a reason when the face cannot ask, or
265
+ * when the question fails. An abort of the call gives `cancel`.
266
+ */
267
+ export async function elicitInput(
268
+ ctx: Pick<ProcedureContext, "elicit" | "signal">,
269
+ request: ElicitRequest,
270
+ options: ElicitOptions = {},
271
+ ): Promise<ElicitResult> {
272
+ if (!ctx.elicit) return { action: "unavailable", reason: NO_ELICITATION_REASON };
273
+ const signal = options.signal ?? ctx.signal;
274
+ try {
275
+ return await ctx.elicit(request, signal ? { ...options, signal } : options);
276
+ } catch (error) {
277
+ return failedAnswer(error, signal);
278
+ }
279
+ }
280
+
281
+ /** The options of `elicitConfirmation`. */
282
+ export interface ConfirmationOptions extends ElicitOptions {
283
+ /** The key of the consent, so the operator can give it before the calls. */
284
+ readonly consent?: string | undefined;
285
+ /** The name of the field of yes or no. The default is `"confirm"`. */
286
+ readonly field?: string | undefined;
287
+ /** The text of the field. The default is `"Yes"`. */
288
+ readonly title?: string | undefined;
289
+ /** The text that tells what a yes does. */
290
+ readonly description?: string | undefined;
291
+ }
292
+
293
+ /** The answer of a confirmation. `confirmed` is true only for an accepted answer of yes. */
294
+ export interface ConfirmationResult extends ElicitResult {
295
+ readonly confirmed: boolean;
296
+ }
297
+
298
+ /**
299
+ * This function asks the person at the caller a question of yes or no. `confirmed` is true only
300
+ * when the person, or the consent of the operator, says yes.
301
+ */
302
+ export async function elicitConfirmation(
303
+ ctx: Pick<ProcedureContext, "elicit" | "signal">,
304
+ question: string,
305
+ options: ConfirmationOptions = {},
306
+ ): Promise<ConfirmationResult> {
307
+ const { consent, field: fieldOption, title, description, ...elicitOptions } = options;
308
+ const field = fieldOption ?? "confirm";
309
+ const result = await elicitInput(
310
+ ctx,
311
+ {
312
+ message: question,
313
+ fields: { [field]: { type: "boolean", title: title ?? "Yes", ...(description !== undefined ? { description } : {}) } },
314
+ required: [field],
315
+ ...(consent !== undefined ? { consent } : {}),
316
+ },
317
+ elicitOptions,
318
+ );
319
+ return { ...result, confirmed: result.action === "accept" && result.content?.[field] === true };
320
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The tool hints of a procedure with its override (CLI-10).
3
+ *
4
+ * The declared effects give the hints of a tool (`toolHints()`). A procedure can make its tool
5
+ * look more dangerous with `metadata.toolHints`, for example `openWorldHint: true` for a store
6
+ * that other people can read. An override cannot make a tool look safer than its effects:
7
+ * the effects stay the truth, and the host enforces the effects, not the hints.
8
+ *
9
+ * The cautious value of each hint:
10
+ * - `readOnlyHint: false`: the tool can change something.
11
+ * - `destructiveHint: true`: the tool can delete or overwrite data.
12
+ * - `idempotentHint: false`: a second call can change more.
13
+ * - `openWorldHint: true`: the tool can reach systems outside the host.
14
+ *
15
+ * An override cannot give the other value, also when the effects give it. The registry refuses
16
+ * such an override when the procedure registers.
17
+ */
18
+
19
+ import type { AnyProcedure } from "./types.js";
20
+ import { toolHints, type ToolHints } from "./capability.js";
21
+
22
+ /**
23
+ * The override of the hints of a tool. Each hint has only its cautious value, so the type refuses
24
+ * the other value before the registry does.
25
+ */
26
+ export interface ToolHintOverride {
27
+ readonly readOnlyHint?: false | undefined;
28
+ readonly destructiveHint?: true | undefined;
29
+ readonly idempotentHint?: false | undefined;
30
+ readonly openWorldHint?: true | undefined;
31
+ }
32
+
33
+ type HintName = keyof ToolHints;
34
+
35
+ /** The cautious value of each hint: the only value that an override can give. */
36
+ const CAUTIOUS: Readonly<Record<HintName, boolean>> = {
37
+ readOnlyHint: false,
38
+ destructiveHint: true,
39
+ idempotentHint: false,
40
+ openWorldHint: true,
41
+ };
42
+
43
+ const HINT_NAMES = Object.keys(CAUTIOUS) as HintName[];
44
+
45
+ /** The override of a procedure (`metadata.toolHints`), or undefined. */
46
+ export function toolHintOverrideOf(procedure: AnyProcedure): ToolHintOverride | undefined {
47
+ const value = (procedure.metadata as { toolHints?: unknown } | undefined)?.toolHints;
48
+ return value !== null && typeof value === "object" ? (value as ToolHintOverride) : undefined;
49
+ }
50
+
51
+ /**
52
+ * The refusal of the override of a procedure, or undefined when the override is correct. An
53
+ * override can give only the cautious value of a hint.
54
+ */
55
+ export function toolHintOverrideRefusal(procedure: AnyProcedure): string | undefined {
56
+ const value = (procedure.metadata as { toolHints?: unknown } | undefined)?.toolHints;
57
+ if (value === undefined) return undefined;
58
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
59
+ return "metadata.toolHints must be an object of hints";
60
+ }
61
+ for (const [name, hint] of Object.entries(value as Record<string, unknown>)) {
62
+ if (hint === undefined) continue;
63
+ if (!HINT_NAMES.includes(name as HintName)) {
64
+ return `metadata.toolHints has an unknown hint "${name}". The hints are ${HINT_NAMES.join(", ")}`;
65
+ }
66
+ const cautious = CAUTIOUS[name as HintName];
67
+ if (hint !== cautious) {
68
+ return (
69
+ `metadata.toolHints.${name} can only be ${String(cautious)}: an override can only make a tool look more ` +
70
+ `dangerous than its effects. The declared effects give the other hints`
71
+ );
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * The hints of the tool of a procedure: the hints of its effects, with the cautious values of
79
+ * its override. A value of the override that is not cautious has no effect (the registry refuses
80
+ * it at registration).
81
+ */
82
+ export function toolHintsOf(procedure: AnyProcedure): ToolHints {
83
+ const derived = toolHints(procedure);
84
+ const override = toolHintOverrideOf(procedure);
85
+ if (!override) return derived;
86
+ const hints: { -readonly [name in HintName]?: boolean } = { ...derived };
87
+ for (const name of HINT_NAMES) {
88
+ if (override[name] === CAUTIOUS[name]) hints[name] = CAUTIOUS[name];
89
+ }
90
+ return hints;
91
+ }
@@ -71,6 +71,41 @@ export {
71
71
  newIdempotencyKey,
72
72
  } from "./idempotency.js";
73
73
  export type { IdempotencyOptions } from "./idempotency.js";
74
+ // What the face of a call offers the procedure (CLI-10, CLI-12)
75
+ export {
76
+ createElicit,
77
+ elicitWithPolicy,
78
+ operatorAnswer,
79
+ elicitInput,
80
+ elicitConfirmation,
81
+ NO_ELICITATION_REASON,
82
+ } from "./elicit.js";
83
+ export type {
84
+ Elicit,
85
+ ElicitRequest,
86
+ ElicitFormRequest,
87
+ ElicitUrlRequest,
88
+ ElicitMode,
89
+ ElicitAction,
90
+ ElicitResult,
91
+ ElicitOptions,
92
+ ElicitField,
93
+ ElicitFieldBase,
94
+ ElicitTextField,
95
+ ElicitNumberField,
96
+ ElicitBooleanField,
97
+ ElicitChoiceField,
98
+ ElicitChoicesField,
99
+ ElicitValue,
100
+ ElicitationPolicy,
101
+ ConfirmationOptions,
102
+ ConfirmationResult,
103
+ } from "./elicit.js";
104
+ export type { ProgressUpdate, ReportProgress } from "./progress.js";
105
+ export { toolHintsOf, toolHintOverrideOf, toolHintOverrideRefusal } from "./hints.js";
106
+ export type { ToolHintOverride } from "./hints.js";
107
+ export { untrustedText, untrustedFieldsOf, untrustedTextsOf } from "./untrusted.js";
108
+ export type { UntrustedText, UntrustedSource, UntrustedValue } from "./untrusted.js";
74
109
  export type { ToolHints, CapabilityRefusal, CapabilityRefusalReason, InvocationExplanation, EffectsViolation } from "./capability.js";
75
110
 
76
111
  // Registry
@@ -88,8 +123,49 @@ export {
88
123
  export type { ProcedureDefinition, ProcedureStub } from "./define.js";
89
124
 
90
125
  // Collection procedures
91
- export { createCollectionProcedures } from "./collection/index.js";
92
- export type { CollectionStorage, StorageChanges, StorageMetadata } from "./collection/index.js";
126
+ export {
127
+ createCollectionProcedures,
128
+ collectionChangeTopic,
129
+ collectionTopicReads,
130
+ DEFAULT_FIND_LIMIT,
131
+ DEFAULT_QUERY_OPERATORS,
132
+ DEFAULT_QUERY_POLICY,
133
+ QUERY_OPERATORS,
134
+ applyPatch,
135
+ compareStorageValues,
136
+ compileSelector,
137
+ isPatchOperations,
138
+ matchesSelector,
139
+ mergePatch,
140
+ patchProblems,
141
+ queryEntries,
142
+ queryOptionsProblems,
143
+ selectorProblems,
144
+ } from "./collection/index.js";
145
+ export type {
146
+ CollectionProceduresOptions,
147
+ CollectionChangeEventsOptions,
148
+ CollectionChangeEvent,
149
+ StoragePatchOperations,
150
+ StorageQueryOperator,
151
+ StorageQueryPolicy,
152
+ CollectionStorage,
153
+ StorageChanges,
154
+ StorageMetadata,
155
+ StorageWriteOptions,
156
+ StorageSetChange,
157
+ StorageDeleteChange,
158
+ StorageClearChange,
159
+ StorageChangeEvent,
160
+ StorageChangeBatch,
161
+ StorageObserver,
162
+ StorageFieldOperators,
163
+ StoragePatch,
164
+ StorageQueryOptions,
165
+ StorageSelector,
166
+ StorageSort,
167
+ StorageSortDirection,
168
+ } from "./collection/index.js";
93
169
 
94
170
  // Manual registration helpers
95
171
  export {
@@ -40,7 +40,10 @@ import { nestedCallRefusal } from "./policy.js";
40
40
  import { scopeOf, withScope, type HydrationEvent } from "./ref.js";
41
41
  import { UNLIMITED_CAPABILITY, capabilityRefusal, examinesNestedCall, narrowCapability, type CapabilityRefusalReason } from "./capability.js";
42
42
  import { timeoutAfterMs } from "./interceptors.js";
43
+ import type { Elicit } from "./elicit.js";
44
+ import type { ReportProgress } from "./progress.js";
43
45
  import type { EventBus } from "../events/types.js";
46
+ import type { Principal } from "../server/types.js";
44
47
  import { getGlobalEventBus, withEventBusSignal } from "../events/bus.js";
45
48
 
46
49
  // =============================================================================
@@ -120,6 +123,16 @@ export interface InvokeOptions {
120
123
  repository?: RepositoryProvider | undefined;
121
124
  /** The bus of `ctx.bus`. The default is the global bus of `getGlobalEventBus()`. */
122
125
  bus?: EventBus | undefined;
126
+ /**
127
+ * The function that asks the person at the caller (`ctx.elicit`, CLI-12). A face of the host
128
+ * gives it. The nested calls get the same function.
129
+ */
130
+ elicit?: Elicit | undefined;
131
+ /**
132
+ * The function that tells the caller the progress of the call (`ctx.progress`). Only the call
133
+ * of the host gets it in its context, not its nested calls.
134
+ */
135
+ progress?: ReportProgress | undefined;
123
136
  /** The input is valid already (for example, the route resolver validated it). */
124
137
  inputValidated?: boolean | undefined;
125
138
  /**
@@ -154,6 +167,11 @@ export interface InvokeOptions {
154
167
  * It drops a refused stream item. A refused value gives `FORBIDDEN` (reason `clearance`).
155
168
  */
156
169
  admitItem?: AdmitItem | undefined;
170
+ /**
171
+ * The principal of the call, from the authentication of the host. It goes to `ctx.principal`,
172
+ * to the invocation and to the nested calls.
173
+ */
174
+ principal?: Principal | undefined;
157
175
  }
158
176
 
159
177
  /**
@@ -184,6 +202,8 @@ export interface Invocation {
184
202
  readonly depth: number;
185
203
  /** The capability of the invocation (frozen). */
186
204
  readonly capability: Capability;
205
+ /** The principal of the call: who sends it. A call from this process has none. */
206
+ readonly principal?: Principal | undefined;
187
207
  }
188
208
 
189
209
  /** The changes that an interceptor gives to the next step: the handler and the nested calls get them. */
@@ -313,6 +333,7 @@ export async function invokeProcedure<TOutput = unknown>(
313
333
  parent: options.parent,
314
334
  depth: options.parent ? options.parent.depth + 1 : 0,
315
335
  capability: options.capability ?? UNLIMITED_CAPABILITY,
336
+ ...(options.principal !== undefined ? { principal: options.principal } : {}),
316
337
  };
317
338
  // A deadline that the parent does not have (from a host, a request or a narrowed nested call)
318
339
  // ends the invocation at that time, as the timeout interceptor does
@@ -486,13 +507,21 @@ export function createProcedureContext(
486
507
  if (options.expose) context.expose = options.expose;
487
508
  if (options.repository) context.repository = options.repository;
488
509
  if (options.onRef) context.onRef = options.onRef;
510
+ if (options.principal !== undefined) context.principal = options.principal;
489
511
  // The procedure of the parent invocation made this call (roadmap R33: the root of the files)
490
512
  if (invocation?.parent) context.caller = invocation.parent.procedure;
513
+ // The output filter of the host, for the data that a handler sends later (roadmap CLI-15)
514
+ const admitItem = options.admitItem;
515
+ if (invocation && invocation.depth === 0 && admitItem) context.admit = async (item) => admitItem(item, invocation);
491
516
  // Every context has a bus. With a signal, the waits of the handler on the bus end when the
492
517
  // invocation aborts, so a stream handler that waits on ctx.bus.stream() can finish. (Before,
493
518
  // no host set ctx.bus: deep dive CORE-15.)
494
519
  const bus = options.bus ?? getGlobalEventBus();
495
520
  context.bus = options.signal ? withEventBusSignal(bus, options.signal) : bus;
521
+ // What the face of the call offers (CLI-12): the nested calls ask the same person, and only
522
+ // the call of the host reports its progress, so the values of one call stay in order
523
+ if (options.elicit) context.elicit = options.elicit;
524
+ if (options.progress && !invocation?.parent && !options.parent) context.progress = options.progress;
496
525
  return context;
497
526
  }
498
527
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The progress of a call, from the procedure to the caller (CLI-10).
3
+ *
4
+ * A face of a host (MCP) can show the progress of a long call while it operates. The face puts a
5
+ * `ReportProgress` function on the request when the caller asked for progress, and the procedure
6
+ * gets it as `ctx.progress`. A stream does not need it: a face that collects a stream shows each
7
+ * item as progress. Only the call of the host gets the function, not its nested calls, so the
8
+ * values of one call stay in order.
9
+ */
10
+
11
+ /** One step of the progress of a call. */
12
+ export interface ProgressUpdate {
13
+ /** The work that is done. It must increase with each update of a call. */
14
+ readonly progress: number;
15
+ /** The work of the whole call, when the procedure knows it. */
16
+ readonly total?: number | undefined;
17
+ /** A short text about the step, for example the URL that a crawler fetched. */
18
+ readonly message?: string | undefined;
19
+ }
20
+
21
+ /**
22
+ * The function that tells the caller the progress of the call (`ctx.progress`). It does not
23
+ * reject: a caller that is gone does not stop the call.
24
+ */
25
+ export type ReportProgress = (update: ProgressUpdate) => Promise<void>;
@@ -15,6 +15,7 @@ import type {
15
15
  } from "./types.js";
16
16
  import { pathToKey } from "./types.js";
17
17
  import { validateCalls } from "./policy.js";
18
+ import { toolHintOverrideRefusal } from "./hints.js";
18
19
 
19
20
  // =============================================================================
20
21
  // Registry Errors
@@ -135,6 +136,9 @@ export class ProcedureRegistry {
135
136
  } catch (error) {
136
137
  throw new RegistryError((error as Error).message, path);
137
138
  }
139
+ // An override of the tool hints can only make the tool look more dangerous than its effects
140
+ const hintRefusal = toolHintOverrideRefusal(procedure);
141
+ if (hintRefusal) throw new RegistryError(`${hintRefusal} (${key})`, path);
138
142
 
139
143
  const replaced = this.procedures.get(key);
140
144
  // The key joins the segments with dots: ["a.b", "c"] and ["a", "b.c"] have one key