@candor-ui/react 1.5.0 → 1.8.0

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 (72) hide show
  1. package/dist/adapters/ag-ui.d.ts +24 -24
  2. package/dist/adapters/ai-sdk.d.ts +6 -6
  3. package/dist/adapters/claude-agent-sdk.d.ts +51 -34
  4. package/dist/adapters/claude-agent-sdk.js +46 -2
  5. package/dist/adapters/claude-agent-server.d.ts +34 -34
  6. package/dist/adapters/use-ag-ui.d.ts +6 -6
  7. package/dist/adapters/use-ag-ui.js +14 -2
  8. package/dist/adapters/use-candor-chat.d.ts +1 -1
  9. package/dist/adapters/use-candor-chat.js +1 -1
  10. package/dist/adapters/use-claude-agent.d.ts +10 -10
  11. package/dist/adapters/use-claude-agent.js +1 -1
  12. package/dist/blocks/copy-field.d.ts +1 -1
  13. package/dist/blocks/copy-field.js +1 -1
  14. package/dist/blocks/degraded-panel.d.ts +5 -5
  15. package/dist/blocks/locale.d.ts +6 -6
  16. package/dist/blocks/locked.d.ts +8 -8
  17. package/dist/blocks/relative-time.d.ts +6 -6
  18. package/dist/blocks/status-dot.d.ts +3 -3
  19. package/dist/blocks/step-list.d.ts +3 -3
  20. package/dist/blocks/streaming-text.d.ts +3 -3
  21. package/dist/chat/agent-avatar.d.ts +4 -4
  22. package/dist/chat/ai-disclosure.d.ts +5 -5
  23. package/dist/chat/ai-disclosure.js +1 -1
  24. package/dist/chat/attachment-preview.d.ts +6 -6
  25. package/dist/chat/attachment-preview.js +1 -1
  26. package/dist/chat/card-registry.js +1 -1
  27. package/dist/chat/citation.d.ts +3 -3
  28. package/dist/chat/citation.js +3 -3
  29. package/dist/chat/composer.d.ts +37 -20
  30. package/dist/chat/composer.js +13 -6
  31. package/dist/chat/context-meter.d.ts +1 -1
  32. package/dist/chat/context-meter.js +1 -1
  33. package/dist/chat/entity-chip.d.ts +3 -3
  34. package/dist/chat/entity-chip.js +1 -1
  35. package/dist/chat/folded-turns.d.ts +11 -6
  36. package/dist/chat/folded-turns.js +2 -2
  37. package/dist/chat/message-actions.d.ts +6 -6
  38. package/dist/chat/message-actions.js +1 -1
  39. package/dist/chat/message-content.d.ts +13 -7
  40. package/dist/chat/message-content.js +10 -5
  41. package/dist/chat/message.d.ts +64 -37
  42. package/dist/chat/message.js +17 -11
  43. package/dist/chat/payload.d.ts +4 -4
  44. package/dist/chat/payload.js +2 -2
  45. package/dist/chat/permission-prompt.d.ts +23 -23
  46. package/dist/chat/prompt-suggestions.d.ts +4 -4
  47. package/dist/chat/prompt-suggestions.js +1 -1
  48. package/dist/chat/reasoning.d.ts +3 -3
  49. package/dist/chat/reasoning.js +1 -1
  50. package/dist/chat/thread-divider.d.ts +3 -3
  51. package/dist/chat/thread-event.d.ts +10 -5
  52. package/dist/chat/thread-event.js +3 -2
  53. package/dist/chat/thread.d.ts +23 -7
  54. package/dist/chat/thread.js +28 -5
  55. package/dist/chat/tool-call.d.ts +2 -2
  56. package/dist/chat/tool-call.js +2 -2
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +1 -0
  59. package/dist/lib/chat.d.ts +42 -42
  60. package/dist/lib/know.d.ts +14 -12
  61. package/dist/lib/mcp-input.d.ts +316 -0
  62. package/dist/lib/mcp-input.js +369 -0
  63. package/dist/lib/render-prop.d.ts +4 -4
  64. package/dist/lib/safe-markdown.d.ts +1 -1
  65. package/dist/lib/status.d.ts +21 -0
  66. package/dist/lib/status.js +17 -0
  67. package/dist/lib/use-shortcut.d.ts +1 -1
  68. package/dist/lib/use-virtual-list.d.ts +6 -6
  69. package/dist/styles.css +4 -1
  70. package/dist/ui/button.d.ts +1 -1
  71. package/dist/ui/button.js +1 -1
  72. package/package.json +1 -1
@@ -0,0 +1,316 @@
1
+ /**
2
+ * MCP's "input required" results (spec 2026-07-28, multi round-trip requests) as Candor data: a server's form becomes
3
+ * questions for ClarifyingQuestions, a plain confirmation or a link to open becomes a two-answer Decision for
4
+ * ApprovalDecision, and the person's answers become the `inputResponses` the client sends when it retries the call.
5
+ * Requests it can't show (sampling, roots, anything newer) stay listed as not supported, never dropped.
6
+ *
7
+ * Pure functions on plain data, so they run in the browser or on the server, with any MCP client.
8
+ *
9
+ * Checked against https://modelcontextprotocol.io/specification/2026-07-28 (basic/patterns/mrtr, client/elicitation
10
+ * and the schema): `resultType: "input_required"`, `inputRequests` as a map keyed by the server's ids, `requestState`
11
+ * echoed back unchanged, and the retry's `inputResponses` keyed the same way. Elicitation results are
12
+ * `{ action: "accept" | "decline" | "cancel", content? }`.
13
+ */
14
+ /** A form field's schema, as MCP restricts it: a string, number, boolean or enum. Unknown keys pass through. */
15
+ export type McpFieldSchema = {
16
+ /** "string", "number", "integer", "boolean" or "array" (a multi-select enum). */
17
+ type?: string | undefined;
18
+ /** A short name for the field. */
19
+ title?: string | undefined;
20
+ /** What the field is for, in a sentence. */
21
+ description?: string | undefined;
22
+ /** The server's default. Shown as the server's suggestion; Candor never fills it in for the person. */
23
+ default?: string | number | boolean | string[] | undefined;
24
+ /** A single-select enum without titles. */
25
+ enum?: string[] | undefined;
26
+ /** Display names for `enum`, in the same order (the legacy form, still sent by older servers). */
27
+ enumNames?: string[] | undefined;
28
+ /** A single-select enum with titles. */
29
+ oneOf?: {
30
+ const: string;
31
+ title?: string | undefined;
32
+ }[] | undefined;
33
+ /** A multi-select enum's items: `enum` without titles or `anyOf` with them. */
34
+ items?: {
35
+ type?: string | undefined;
36
+ enum?: string[] | undefined;
37
+ anyOf?: {
38
+ const: string;
39
+ title?: string | undefined;
40
+ }[] | undefined;
41
+ } | undefined;
42
+ /** At least this many choices, for a multi-select. */
43
+ minItems?: number | undefined;
44
+ /** At most this many choices, for a multi-select. */
45
+ maxItems?: number | undefined;
46
+ /** At least this many characters. */
47
+ minLength?: number | undefined;
48
+ /** At most this many characters. */
49
+ maxLength?: number | undefined;
50
+ /** "email", "uri", "date" or "date-time". */
51
+ format?: string | undefined;
52
+ /** The smallest number allowed. */
53
+ minimum?: number | undefined;
54
+ /** The largest number allowed. */
55
+ maximum?: number | undefined;
56
+ };
57
+ /** An `elicitation/create` request in form mode: a message and a flat schema of primitive fields. */
58
+ export type McpElicitFormRequest = {
59
+ method: "elicitation/create";
60
+ params: {
61
+ /** "form", or left out (which means form). */
62
+ mode?: "form" | undefined;
63
+ /** Why the server asks, in its own words. */
64
+ message: string;
65
+ requestedSchema: {
66
+ type: "object";
67
+ properties: Record<string, McpFieldSchema>;
68
+ required?: string[] | undefined;
69
+ };
70
+ };
71
+ };
72
+ /** An `elicitation/create` request in URL mode: a page the person opens outside the client (sign-in, payment). */
73
+ export type McpElicitUrlRequest = {
74
+ method: "elicitation/create";
75
+ params: {
76
+ mode: "url";
77
+ /** Why the server asks, in its own words. */
78
+ message: string;
79
+ /** The page to open, only once the person agrees. */
80
+ url: string;
81
+ };
82
+ };
83
+ /**
84
+ * A result that asks for more input before the call can finish. At least one of `inputRequests` and `requestState`
85
+ * is present. Values in `inputRequests` are left `unknown`: a server may send kinds this file doesn't know.
86
+ */
87
+ export type McpInputRequiredResult = {
88
+ resultType: "input_required";
89
+ /** The server's requests, keyed by its own ids. */
90
+ inputRequests?: Record<string, unknown> | undefined;
91
+ /** Opaque to the client: sent back exactly as received, never read or changed. */
92
+ requestState?: string | undefined;
93
+ _meta?: Record<string, unknown> | undefined;
94
+ };
95
+ /** The answer to one elicitation, as MCP takes it back. `content` only when accepted in form mode. */
96
+ export type McpElicitResult = {
97
+ action: "accept" | "decline" | "cancel";
98
+ content?: Record<string, string | number | boolean | string[]> | undefined;
99
+ };
100
+ /**
101
+ * A question in ClarifyingQuestions' shape (`ClarifyingQuestion`), defined here so this free file needs nothing from
102
+ * a paid one. The question's id is the form field's name.
103
+ */
104
+ export type McpQuestion = {
105
+ id: string;
106
+ kind: "choice";
107
+ prompt: string;
108
+ options: {
109
+ id: string;
110
+ label: string;
111
+ detail?: string | undefined;
112
+ }[];
113
+ multiple?: boolean | undefined;
114
+ /** The server's default, marked as a suggestion and never pre-selected. */
115
+ recommended?: string | undefined;
116
+ /** A short label shown above the question: the field's title when it also has a description. */
117
+ tag?: string | undefined;
118
+ /** Always false: an MCP enum takes only its own values. */
119
+ other?: boolean | undefined;
120
+ } | {
121
+ id: string;
122
+ kind: "text";
123
+ prompt: string;
124
+ /** A hint for the expected format, or the server's default, never filled in. */
125
+ placeholder?: string | undefined;
126
+ /** The field isn't required: left empty, it isn't sent. */
127
+ optional?: boolean | undefined;
128
+ /** A short label shown above the question. */
129
+ tag?: string | undefined;
130
+ };
131
+ /** A person's answer, in ClarifyingQuestions' shape (`ClarifyingAnswer`). */
132
+ export type McpQuestionAnswer = {
133
+ questionId: string;
134
+ choice?: string[] | undefined;
135
+ text?: string | undefined;
136
+ };
137
+ /**
138
+ * A two-answer confirmation in ApprovalDecision's shape (`Decision`), defined here so this free file needs nothing
139
+ * from a paid one. The choices' ids are "accept" and "decline", which is what the retry sends.
140
+ */
141
+ export type McpDecision = {
142
+ id: string;
143
+ question: string;
144
+ /** What to decide on, as label and value: the page's host and its full address. */
145
+ facts?: {
146
+ label: string;
147
+ value: string;
148
+ }[] | undefined;
149
+ /** Accept first, decline second. */
150
+ choices: {
151
+ id: "accept" | "decline";
152
+ label: string;
153
+ detail?: string | undefined;
154
+ }[];
155
+ state: "open";
156
+ };
157
+ /** One of the server's requests, as Candor shows it. `key` is the server's id for it. */
158
+ export type McpInputItem =
159
+ /**
160
+ * A form: answer it with ClarifyingQuestions. `fields` and `required` are the server's schema, which
161
+ * `mcpInputResponses` checks the answers against.
162
+ */
163
+ {
164
+ key: string;
165
+ kind: "questions";
166
+ message: string;
167
+ questions: McpQuestion[];
168
+ fields: Record<string, McpFieldSchema>;
169
+ required: string[];
170
+ }
171
+ /** A form with no fields: the server asks to go ahead. Answer it with ApprovalDecision. */
172
+ | {
173
+ key: string;
174
+ kind: "confirm";
175
+ message: string;
176
+ decision: McpDecision;
177
+ }
178
+ /**
179
+ * A page to open outside the client. Show the full `url` and its `host` before the person agrees, and open it only
180
+ * once they accept (`decision`). `warnings` say why the address deserves a second look.
181
+ */
182
+ | {
183
+ key: string;
184
+ kind: "url";
185
+ message: string;
186
+ url: string;
187
+ host: string;
188
+ warnings: string[];
189
+ decision: McpDecision;
190
+ }
191
+ /** A request Candor can't show: its method (when it has one) and why. Show it; don't drop it. */
192
+ | {
193
+ key: string;
194
+ kind: "unsupported";
195
+ method?: string | undefined;
196
+ reason: string;
197
+ request: unknown;
198
+ };
199
+ /** An input-required result, ready to show. */
200
+ export type McpInputView = {
201
+ /** One per request, in the server's order. */
202
+ items: McpInputItem[];
203
+ /** Sent back exactly as received; absent when the server sent none. */
204
+ requestState?: string | undefined;
205
+ /** The server's name as your app knows it (passed in), so the person can tell which server asks. */
206
+ server?: string | undefined;
207
+ /** The name the server gives itself in the result's `_meta`. Its own claim: show it as such, if at all. */
208
+ serverClaims?: string | undefined;
209
+ };
210
+ /** A person's reply to one item, by its key. */
211
+ export type McpInputReply =
212
+ /** A form, answered: the answers from ClarifyingQuestions. */
213
+ {
214
+ key: string;
215
+ action: "accept";
216
+ answers?: McpQuestionAnswer[] | undefined;
217
+ }
218
+ /** Declined outright, or dismissed without a choice. */
219
+ | {
220
+ key: string;
221
+ action: "decline" | "cancel";
222
+ };
223
+ /** Why an answer can't be sent yet. */
224
+ export type McpInputProblem = {
225
+ /** The item's key. */
226
+ key: string;
227
+ /** The field, for a form. */
228
+ questionId?: string | undefined;
229
+ /** Which rule it broke. */
230
+ code: "unanswered" | "required" | "notAnOption" | "notANumber" | "notAnInteger" | "tooSmall" | "tooLarge" | "tooShort" | "tooLong" | "format" | "tooFew" | "tooMany";
231
+ /** The same in words, from `mcpInputWords`. */
232
+ message: string;
233
+ };
234
+ /** What a retry sends, or why it can't be sent yet. */
235
+ export type McpInputResponses = {
236
+ ok: true;
237
+ /** The retry's `inputResponses`, keyed by the server's ids. */
238
+ inputResponses: Record<string, unknown>;
239
+ /** Keys of requests nothing was sent for (not supported and not answered by your app). The server may ask again. */
240
+ notSent: string[];
241
+ } | {
242
+ ok: false;
243
+ problems: McpInputProblem[];
244
+ };
245
+ /** The words this file writes into questions, decisions and problems. Pass your own to `mcpInputView` or `mcpInputResponses`. */
246
+ export declare const mcpInputWords: {
247
+ yes: string;
248
+ no: string;
249
+ /** The extra option on a choice the server doesn't require. */
250
+ skip: string;
251
+ defaultHint: string;
252
+ formatEmail: string;
253
+ formatUri: string;
254
+ formatDate: string;
255
+ formatDateTime: string;
256
+ confirm: string;
257
+ openPage: string;
258
+ decline: string;
259
+ host: string;
260
+ address: string;
261
+ punycode: string;
262
+ notHttps: string;
263
+ badUrl: string;
264
+ unsupportedSampling: string;
265
+ unsupportedRoots: string;
266
+ unsupportedMethod: string;
267
+ unsupportedShape: string;
268
+ unsupportedField: string;
269
+ unanswered: string;
270
+ required: string;
271
+ notAnOption: string;
272
+ notANumber: string;
273
+ notAnInteger: string;
274
+ tooSmall: string;
275
+ tooLarge: string;
276
+ tooShort: string;
277
+ tooLong: string;
278
+ format: string;
279
+ tooFew: string;
280
+ tooMany: string;
281
+ };
282
+ /** The words, with any you pass in place of the defaults. */
283
+ export type McpInputWords = Partial<typeof mcpInputWords>;
284
+ /**
285
+ * Whether a tools/call (or prompts/get, resources/read) result asks for more input. A result without `resultType`
286
+ * comes from an older server and counts as complete, as the spec says.
287
+ */
288
+ export declare function isMcpInputRequired(result: unknown): result is McpInputRequiredResult;
289
+ /**
290
+ * An input-required result as items to show: forms as questions, confirmations and links as decisions, and anything
291
+ * else as a visible "not supported". Undefined when the result doesn't ask for input. `server` is your app's own name
292
+ * for the server, which the person should see beside every request.
293
+ */
294
+ export declare function mcpInputView(result: unknown, options?: {
295
+ server?: string | undefined;
296
+ words?: McpInputWords | undefined;
297
+ }): McpInputView | undefined;
298
+ /**
299
+ * The retry's `inputResponses` for the person's replies, checked against each form's schema first (the spec asks
300
+ * clients to validate). Every form, confirmation and link needs a reply; a form is sent only once every required
301
+ * field has a valid answer. `own` holds responses your app made itself, for requests Candor doesn't show (a sampling
302
+ * or roots answer), keyed by the server's ids; anything left is listed in `notSent`.
303
+ */
304
+ export declare function mcpInputResponses(view: McpInputView, replies: McpInputReply[], options?: {
305
+ own?: Record<string, unknown> | undefined;
306
+ words?: McpInputWords | undefined;
307
+ }): McpInputResponses;
308
+ /**
309
+ * The params for retrying the original request: its own params (name and arguments for tools/call) with
310
+ * `inputResponses` and the result's `requestState` exactly as received. A `requestState` from an earlier round is
311
+ * removed when this result has none, as the spec requires. Send it as a new request with a new JSON-RPC id.
312
+ */
313
+ export declare function mcpRetryParams<P extends Record<string, unknown>>(original: P, view: Pick<McpInputView, "requestState">, inputResponses: Record<string, unknown>): Omit<P, "inputResponses" | "requestState"> & {
314
+ inputResponses: Record<string, unknown>;
315
+ requestState?: string | undefined;
316
+ };
@@ -0,0 +1,369 @@
1
+ /**
2
+ * MCP's "input required" results (spec 2026-07-28, multi round-trip requests) as Candor data: a server's form becomes
3
+ * questions for ClarifyingQuestions, a plain confirmation or a link to open becomes a two-answer Decision for
4
+ * ApprovalDecision, and the person's answers become the `inputResponses` the client sends when it retries the call.
5
+ * Requests it can't show (sampling, roots, anything newer) stay listed as not supported, never dropped.
6
+ *
7
+ * Pure functions on plain data, so they run in the browser or on the server, with any MCP client.
8
+ *
9
+ * Checked against https://modelcontextprotocol.io/specification/2026-07-28 (basic/patterns/mrtr, client/elicitation
10
+ * and the schema): `resultType: "input_required"`, `inputRequests` as a map keyed by the server's ids, `requestState`
11
+ * echoed back unchanged, and the retry's `inputResponses` keyed the same way. Elicitation results are
12
+ * `{ action: "accept" | "decline" | "cancel", content? }`.
13
+ */
14
+ /** The words this file writes into questions, decisions and problems. Pass your own to `mcpInputView` or `mcpInputResponses`. */
15
+ export const mcpInputWords = {
16
+ yes: "Yes",
17
+ no: "No",
18
+ /** The extra option on a choice the server doesn't require. */
19
+ skip: "Leave unanswered",
20
+ defaultHint: "Server’s default: {value}",
21
+ formatEmail: "name@example.com",
22
+ formatUri: "https://…",
23
+ formatDate: "YYYY-MM-DD",
24
+ formatDateTime: "YYYY-MM-DDTHH:MM",
25
+ confirm: "Go ahead",
26
+ openPage: "Open the page",
27
+ decline: "Decline",
28
+ host: "Opens",
29
+ address: "Full address",
30
+ punycode: "The address uses an encoded name (xn--), which can imitate another site.",
31
+ notHttps: "The page isn’t on https.",
32
+ badUrl: "The server sent an address that isn’t a valid URL, so it can’t be opened.",
33
+ unsupportedSampling: "The server asks the client to run a model prompt (sampling), which this app doesn’t do here.",
34
+ unsupportedRoots: "The server asks for the client’s folders (roots), which this app doesn’t answer here.",
35
+ unsupportedMethod: "The server asks for something this app can’t show yet: {method}.",
36
+ unsupportedShape: "The server’s request isn’t in a shape this app recognises.",
37
+ unsupportedField: "The form has a field this app can’t show: {field}.",
38
+ unanswered: "Not answered yet.",
39
+ required: "An answer is needed.",
40
+ notAnOption: "That isn’t one of the choices.",
41
+ notANumber: "That isn’t a number.",
42
+ notAnInteger: "That needs to be a whole number.",
43
+ tooSmall: "That needs to be at least {min}.",
44
+ tooLarge: "That needs to be at most {max}.",
45
+ tooShort: "That needs at least {min} characters.",
46
+ tooLong: "That allows at most {max} characters.",
47
+ format: "That doesn’t look like {format}.",
48
+ tooFew: "Choose at least {min}.",
49
+ tooMany: "Choose at most {max}.",
50
+ };
51
+ const fill = (s, vars) => s.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
52
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
53
+ /** The id of "Leave unanswered" on a choice the server doesn't require; no enum value or "true"/"false" can clash with it. */
54
+ const SKIP = "\u0000skip";
55
+ /* ---------------------------------------------------------------- result → view */
56
+ /**
57
+ * Whether a tools/call (or prompts/get, resources/read) result asks for more input. A result without `resultType`
58
+ * comes from an older server and counts as complete, as the spec says.
59
+ */
60
+ export function isMcpInputRequired(result) {
61
+ return isObject(result) && result.resultType === "input_required";
62
+ }
63
+ /**
64
+ * An input-required result as items to show: forms as questions, confirmations and links as decisions, and anything
65
+ * else as a visible "not supported". Undefined when the result doesn't ask for input. `server` is your app's own name
66
+ * for the server, which the person should see beside every request.
67
+ */
68
+ export function mcpInputView(result, options = {}) {
69
+ if (!isMcpInputRequired(result))
70
+ return undefined;
71
+ const w = { ...mcpInputWords, ...options.words };
72
+ const requests = isObject(result.inputRequests) ? result.inputRequests : {};
73
+ const items = Object.entries(requests).map(([key, request]) => itemOf(key, request, w));
74
+ const info = isObject(result._meta) ? result._meta["io.modelcontextprotocol/serverInfo"] : undefined;
75
+ const claims = isObject(info) ? (typeof info.title === "string" && info.title) || (typeof info.name === "string" && info.name) || undefined : undefined;
76
+ return {
77
+ items,
78
+ ...(typeof result.requestState === "string" && { requestState: result.requestState }),
79
+ ...(options.server && { server: options.server }),
80
+ ...(claims && { serverClaims: claims }),
81
+ };
82
+ }
83
+ function itemOf(key, request, w) {
84
+ const unsupported = (reason, method) => ({ key, kind: "unsupported", ...(method && { method }), reason, request });
85
+ if (!isObject(request) || typeof request.method !== "string")
86
+ return unsupported(w.unsupportedShape);
87
+ const method = request.method;
88
+ if (method === "sampling/createMessage")
89
+ return unsupported(w.unsupportedSampling, method);
90
+ if (method === "roots/list")
91
+ return unsupported(w.unsupportedRoots, method);
92
+ if (method !== "elicitation/create")
93
+ return unsupported(fill(w.unsupportedMethod, { method }), method);
94
+ const params = request.params;
95
+ if (!isObject(params) || typeof params.message !== "string")
96
+ return unsupported(w.unsupportedShape, method);
97
+ const message = params.message;
98
+ const mode = params.mode ?? "form";
99
+ if (mode === "url") {
100
+ if (typeof params.url !== "string")
101
+ return unsupported(w.unsupportedShape, method);
102
+ let parsed;
103
+ try {
104
+ parsed = new URL(params.url);
105
+ }
106
+ catch {
107
+ return unsupported(w.badUrl, method);
108
+ }
109
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
110
+ return unsupported(w.badUrl, method);
111
+ const warnings = [];
112
+ if (parsed.hostname.split(".").some((part) => part.startsWith("xn--")))
113
+ warnings.push(w.punycode);
114
+ if (parsed.protocol !== "https:")
115
+ warnings.push(w.notHttps);
116
+ return {
117
+ key,
118
+ kind: "url",
119
+ message,
120
+ url: params.url,
121
+ host: parsed.host,
122
+ warnings,
123
+ decision: {
124
+ id: key,
125
+ question: message,
126
+ facts: [
127
+ { label: w.host, value: parsed.host },
128
+ { label: w.address, value: params.url },
129
+ ],
130
+ choices: [
131
+ { id: "accept", label: w.openPage },
132
+ { id: "decline", label: w.decline },
133
+ ],
134
+ state: "open",
135
+ },
136
+ };
137
+ }
138
+ if (mode !== "form")
139
+ return unsupported(fill(w.unsupportedMethod, { method: `${method} (${String(mode)})` }), method);
140
+ const schema = params.requestedSchema;
141
+ if (!isObject(schema) || (schema.properties !== undefined && !isObject(schema.properties)))
142
+ return unsupported(w.unsupportedShape, method);
143
+ const properties = (schema.properties ?? {});
144
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []);
145
+ const names = Object.keys(properties);
146
+ if (!names.length) {
147
+ return {
148
+ key,
149
+ kind: "confirm",
150
+ message,
151
+ decision: {
152
+ id: key,
153
+ question: message,
154
+ choices: [
155
+ { id: "accept", label: w.confirm },
156
+ { id: "decline", label: w.decline },
157
+ ],
158
+ state: "open",
159
+ },
160
+ };
161
+ }
162
+ const questions = [];
163
+ for (const name of names) {
164
+ const q = questionOf(name, properties[name], required.has(name), w);
165
+ // One field it can't show makes the whole form unsupported: answering only part of it would send a half-truth.
166
+ if (!q)
167
+ return unsupported(fill(w.unsupportedField, { field: name }), method);
168
+ questions.push(q);
169
+ }
170
+ return { key, kind: "questions", message, questions, fields: properties, required: [...required] };
171
+ }
172
+ /** A field's options, when it is an enum (single or multiple); undefined otherwise. */
173
+ function optionsOf(s) {
174
+ const titled = (list) => Array.isArray(list) && list.every((o) => isObject(o) && typeof o.const === "string")
175
+ ? list.map((o) => ({ id: o.const, label: typeof o.title === "string" && o.title ? o.title : o.const }))
176
+ : undefined;
177
+ const plain = (list, names) => Array.isArray(list) && list.every((v) => typeof v === "string")
178
+ ? list.map((v, i) => ({ id: v, label: Array.isArray(names) && typeof names[i] === "string" && names[i] ? names[i] : v }))
179
+ : undefined;
180
+ if (s.type === "array") {
181
+ const items = isObject(s.items) ? s.items : undefined;
182
+ const options = items ? (items.anyOf !== undefined ? titled(items.anyOf) : plain(items.enum)) : undefined;
183
+ return options ? { options, multiple: true } : undefined;
184
+ }
185
+ if (s.type === "string" || s.type === undefined) {
186
+ if (s.oneOf !== undefined) {
187
+ const options = titled(s.oneOf);
188
+ return options ? { options, multiple: false } : undefined;
189
+ }
190
+ if (s.enum !== undefined) {
191
+ const options = plain(s.enum, s.enumNames);
192
+ return options ? { options, multiple: false } : undefined;
193
+ }
194
+ }
195
+ return undefined;
196
+ }
197
+ const formatHint = (format, w) => format === "email" ? w.formatEmail : format === "uri" ? w.formatUri : format === "date" ? w.formatDate : format === "date-time" ? w.formatDateTime : undefined;
198
+ function questionOf(name, raw, required, w) {
199
+ if (!isObject(raw))
200
+ return undefined;
201
+ const s = raw;
202
+ const title = typeof s.title === "string" && s.title ? s.title : undefined;
203
+ const description = typeof s.description === "string" && s.description ? s.description : undefined;
204
+ const prompt = description ?? title ?? name;
205
+ const tag = description && title ? title : undefined;
206
+ const head = { id: name, prompt, ...(tag && { tag }) };
207
+ const enumOf = optionsOf(s);
208
+ if (enumOf) {
209
+ const options = !required && !enumOf.multiple ? [...enumOf.options, { id: SKIP, label: w.skip }] : enumOf.options;
210
+ const recommended = typeof s.default === "string" && enumOf.options.some((o) => o.id === s.default) ? s.default : undefined;
211
+ return { ...head, kind: "choice", options, multiple: enumOf.multiple, other: false, ...(recommended && { recommended }) };
212
+ }
213
+ if (s.type === "boolean") {
214
+ const options = [
215
+ { id: "true", label: w.yes },
216
+ { id: "false", label: w.no },
217
+ ...(required ? [] : [{ id: SKIP, label: w.skip }]),
218
+ ];
219
+ const recommended = typeof s.default === "boolean" ? String(s.default) : undefined;
220
+ return { ...head, kind: "choice", options, multiple: false, other: false, ...(recommended && { recommended }) };
221
+ }
222
+ if (s.type === "string" || s.type === "number" || s.type === "integer") {
223
+ const hint = s.default !== undefined && s.default !== null && typeof s.default !== "object" ? fill(w.defaultHint, { value: String(s.default) }) : formatHint(s.format, w);
224
+ return { ...head, kind: "text", ...(hint && { placeholder: hint }), ...(!required && { optional: true }) };
225
+ }
226
+ return undefined;
227
+ }
228
+ /* ---------------------------------------------------------------- answers → retry */
229
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
230
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
231
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/;
232
+ function formatOk(format, text) {
233
+ if (format === "email")
234
+ return EMAIL.test(text);
235
+ if (format === "date")
236
+ return DATE.test(text) && !Number.isNaN(Date.parse(text));
237
+ if (format === "date-time")
238
+ return DATE_TIME.test(text) && !Number.isNaN(Date.parse(text));
239
+ if (format === "uri") {
240
+ try {
241
+ return Boolean(new URL(text));
242
+ }
243
+ catch {
244
+ return false;
245
+ }
246
+ }
247
+ return true;
248
+ }
249
+ /**
250
+ * The retry's `inputResponses` for the person's replies, checked against each form's schema first (the spec asks
251
+ * clients to validate). Every form, confirmation and link needs a reply; a form is sent only once every required
252
+ * field has a valid answer. `own` holds responses your app made itself, for requests Candor doesn't show (a sampling
253
+ * or roots answer), keyed by the server's ids; anything left is listed in `notSent`.
254
+ */
255
+ export function mcpInputResponses(view, replies, options = {}) {
256
+ const w = { ...mcpInputWords, ...options.words };
257
+ const problems = [];
258
+ // Keys are the server's ids, so the maps have no prototype: "__proto__" or "toString" is just another key.
259
+ const inputResponses = Object.create(null);
260
+ const notSent = [];
261
+ const problem = (key, code, vars = {}, questionId) => problems.push({ key, ...(questionId !== undefined && { questionId }), code, message: fill(w[code], vars) });
262
+ for (const item of view.items) {
263
+ if (item.kind === "unsupported") {
264
+ if (options.own && Object.hasOwn(options.own, item.key))
265
+ inputResponses[item.key] = options.own[item.key];
266
+ else
267
+ notSent.push(item.key);
268
+ continue;
269
+ }
270
+ const reply = replies.find((r) => r.key === item.key);
271
+ if (!reply) {
272
+ problem(item.key, "unanswered");
273
+ continue;
274
+ }
275
+ if (reply.action !== "accept") {
276
+ inputResponses[item.key] = { action: reply.action };
277
+ continue;
278
+ }
279
+ if (item.kind !== "questions") {
280
+ // A confirmation or a link: accepting sends no content. For a link, open it now; the server finds out on retry.
281
+ inputResponses[item.key] = { action: "accept" };
282
+ continue;
283
+ }
284
+ const content = Object.create(null);
285
+ const before = problems.length;
286
+ for (const q of item.questions) {
287
+ const s = item.fields[q.id] ?? {};
288
+ const required = item.required.includes(q.id);
289
+ const a = reply.answers?.find((x) => x.questionId === q.id);
290
+ if (q.kind === "choice") {
291
+ const picked = (a?.choice ?? []).filter((c) => c !== SKIP);
292
+ if (picked.some((c) => !q.options.some((o) => o.id === c))) {
293
+ problem(item.key, "notAnOption", {}, q.id);
294
+ continue;
295
+ }
296
+ if (q.multiple) {
297
+ if (!picked.length && (!required || s.minItems === 0)) {
298
+ if (required)
299
+ content[q.id] = [];
300
+ continue;
301
+ }
302
+ if (!picked.length && s.minItems === undefined)
303
+ problem(item.key, "required", {}, q.id);
304
+ else if (typeof s.minItems === "number" && picked.length < s.minItems)
305
+ problem(item.key, "tooFew", { min: s.minItems }, q.id);
306
+ else if (typeof s.maxItems === "number" && picked.length > s.maxItems)
307
+ problem(item.key, "tooMany", { max: s.maxItems }, q.id);
308
+ else
309
+ content[q.id] = picked;
310
+ continue;
311
+ }
312
+ const value = picked[0];
313
+ if (value === undefined) {
314
+ if (required)
315
+ problem(item.key, "required", {}, q.id);
316
+ continue;
317
+ }
318
+ content[q.id] = s.type === "boolean" ? value === "true" : value;
319
+ continue;
320
+ }
321
+ const text = a?.text?.trim() ?? "";
322
+ if (!text) {
323
+ if (!q.optional)
324
+ problem(item.key, "required", {}, q.id);
325
+ continue;
326
+ }
327
+ if (s.type === "number" || s.type === "integer") {
328
+ const n = Number(text);
329
+ if (!Number.isFinite(n))
330
+ problem(item.key, "notANumber", {}, q.id);
331
+ else if (s.type === "integer" && !Number.isInteger(n))
332
+ problem(item.key, "notAnInteger", {}, q.id);
333
+ else if (typeof s.minimum === "number" && n < s.minimum)
334
+ problem(item.key, "tooSmall", { min: s.minimum }, q.id);
335
+ else if (typeof s.maximum === "number" && n > s.maximum)
336
+ problem(item.key, "tooLarge", { max: s.maximum }, q.id);
337
+ else
338
+ content[q.id] = n;
339
+ continue;
340
+ }
341
+ if (typeof s.minLength === "number" && text.length < s.minLength)
342
+ problem(item.key, "tooShort", { min: s.minLength }, q.id);
343
+ else if (typeof s.maxLength === "number" && text.length > s.maxLength)
344
+ problem(item.key, "tooLong", { max: s.maxLength }, q.id);
345
+ else if (!formatOk(s.format, text))
346
+ problem(item.key, "format", { format: formatHint(s.format, w) ?? String(s.format) }, q.id);
347
+ else
348
+ content[q.id] = text;
349
+ }
350
+ if (problems.length === before)
351
+ inputResponses[item.key] = { action: "accept", content };
352
+ }
353
+ return problems.length ? { ok: false, problems } : { ok: true, inputResponses, notSent };
354
+ }
355
+ /**
356
+ * The params for retrying the original request: its own params (name and arguments for tools/call) with
357
+ * `inputResponses` and the result's `requestState` exactly as received. A `requestState` from an earlier round is
358
+ * removed when this result has none, as the spec requires. Send it as a new request with a new JSON-RPC id.
359
+ */
360
+ export function mcpRetryParams(original, view, inputResponses) {
361
+ const rest = { ...original };
362
+ delete rest.inputResponses;
363
+ delete rest.requestState;
364
+ return {
365
+ ...rest,
366
+ inputResponses,
367
+ ...(view.requestState !== undefined && { requestState: view.requestState }),
368
+ };
369
+ }