@cyanheads/mcp-ts-core 0.13.8 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/AGENTS.md +17 -13
  2. package/CLAUDE.md +17 -13
  3. package/README.md +2 -2
  4. package/changelog/0.13.x/0.13.9.md +113 -0
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +20 -9
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/app.d.ts +6 -3
  9. package/dist/core/app.d.ts.map +1 -1
  10. package/dist/core/app.js +6 -4
  11. package/dist/core/app.js.map +1 -1
  12. package/dist/core/context.d.ts +25 -1
  13. package/dist/core/context.d.ts.map +1 -1
  14. package/dist/core/context.js.map +1 -1
  15. package/dist/core/serverManifest.d.ts +6 -0
  16. package/dist/core/serverManifest.d.ts.map +1 -1
  17. package/dist/core/serverManifest.js +6 -0
  18. package/dist/core/serverManifest.js.map +1 -1
  19. package/dist/linter/rules/tool-rules.d.ts +2 -1
  20. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  21. package/dist/linter/rules/tool-rules.js +36 -1
  22. package/dist/linter/rules/tool-rules.js.map +1 -1
  23. package/dist/mcp-server/inputRequired.d.ts +14 -5
  24. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  25. package/dist/mcp-server/inputRequired.js +15 -8
  26. package/dist/mcp-server/inputRequired.js.map +1 -1
  27. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +163 -40
  28. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
  30. package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
  31. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +23 -10
  32. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +296 -80
  34. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  35. package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
  36. package/dist/mcp-server/transports/http/httpTransport.js +65 -9
  37. package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
  38. package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
  39. package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
  40. package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
  41. package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
  42. package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
  43. package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
  44. package/dist/services/canvas/core/CanvasRegistry.js +7 -3
  45. package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
  46. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
  47. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  48. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +620 -328
  49. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  50. package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
  51. package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
  52. package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
  53. package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
  54. package/dist/services/mirror/core/defineMirror.d.ts +1 -0
  55. package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
  56. package/dist/services/mirror/core/defineMirror.js +1 -0
  57. package/dist/services/mirror/core/defineMirror.js.map +1 -1
  58. package/dist/utils/index.d.ts +1 -1
  59. package/dist/utils/index.d.ts.map +1 -1
  60. package/dist/utils/index.js.map +1 -1
  61. package/dist/utils/network/pacer.d.ts +38 -5
  62. package/dist/utils/network/pacer.d.ts.map +1 -1
  63. package/dist/utils/network/pacer.js +87 -25
  64. package/dist/utils/network/pacer.js.map +1 -1
  65. package/dist/utils/telemetry/attributes.d.ts +5 -1
  66. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  67. package/dist/utils/telemetry/attributes.js +5 -1
  68. package/dist/utils/telemetry/attributes.js.map +1 -1
  69. package/framework-skills/add-app-tool/SKILL.md +3 -3
  70. package/framework-skills/add-export/SKILL.md +5 -16
  71. package/framework-skills/add-prompt/SKILL.md +7 -3
  72. package/framework-skills/add-resource/SKILL.md +7 -5
  73. package/framework-skills/add-tool/SKILL.md +12 -10
  74. package/framework-skills/api-auth/SKILL.md +2 -2
  75. package/framework-skills/api-canvas/SKILL.md +17 -8
  76. package/framework-skills/api-config/SKILL.md +4 -4
  77. package/framework-skills/api-context/SKILL.md +14 -3
  78. package/framework-skills/api-errors/SKILL.md +8 -7
  79. package/framework-skills/api-linter/SKILL.md +26 -7
  80. package/framework-skills/api-mirror/SKILL.md +2 -1
  81. package/framework-skills/api-telemetry/SKILL.md +4 -4
  82. package/framework-skills/api-utils/SKILL.md +2 -2
  83. package/framework-skills/design-mcp-server/SKILL.md +2 -2
  84. package/framework-skills/field-test/SKILL.md +4 -4
  85. package/framework-skills/git-wrapup/SKILL.md +8 -6
  86. package/framework-skills/orchestrations/SKILL.md +7 -6
  87. package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
  88. package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
  89. package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
  90. package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
  91. package/framework-skills/polish-docs-meta/SKILL.md +4 -4
  92. package/framework-skills/release-and-publish/SKILL.md +6 -4
  93. package/framework-skills/release-pr-review/SKILL.md +37 -23
  94. package/framework-skills/report-issue-framework/SKILL.md +7 -5
  95. package/framework-skills/report-issue-local/SKILL.md +8 -6
  96. package/framework-skills/security-pass/SKILL.md +8 -8
  97. package/package.json +3 -3
  98. package/scripts/devcheck.ts +7 -6
  99. package/scripts/lint-mcp.ts +87 -27
  100. package/scripts/lint-packaging.ts +61 -0
  101. package/scripts/release-github.ts +117 -5
  102. package/templates/_.mcpbignore +2 -0
@@ -40,12 +40,13 @@ export declare function renderToolContent(def: AnyToolDefinition, validatedOutpu
40
40
  * stay JSON-only.
41
41
  *
42
42
  * The `Recovery:` line is dropped when the message already contains the hint
43
- * verbatim (#459) — `buildArgumentRecoveryHint` falls back to an issue's own
44
- * message for a constraint or refinement, and repeating that sentence costs
45
- * the reader without adding a next step. Containment, not equality: the
46
- * argument-rejection preamble and a field-path prefix both leave the hint's
47
- * whole text on screen. `structuredContent.error.data.recovery.hint` stays
48
- * populated either way, so #445's guarantee holds on the JSON surface.
43
+ * verbatim (#459) — `buildArgumentRecoveryHint` restates a constraint or
44
+ * refinement issue as its own message line, and a hint made only of those is
45
+ * the message's issue text, so repeating it costs the reader without adding a
46
+ * next step. Containment, not equality: the argument-rejection preamble leaves
47
+ * that text whole on screen, and so does a handler hint the message embeds.
48
+ * `structuredContent.error.data.recovery.hint` stays populated either way, so
49
+ * #445's guarantee holds on the JSON surface.
49
50
  *
50
51
  * Note: `_meta.error` is intentionally NOT emitted — the error code, message,
51
52
  * and data live on `structuredContent.error` instead, mirroring the success
@@ -82,10 +83,22 @@ export interface ParseToolArgumentsOptions {
82
83
  * An ordered pre-validation step wraps the parse. Before it,
83
84
  * {@link prevalidateToolArguments} drops client-added keys (#453) and rewrites
84
85
  * key aliases (#452); after a failure — and only then —
85
- * {@link repairRepresentations} undoes a stringified array and the arguments
86
- * are parsed once more (#234), the repair kept only if the author's own schema
87
- * now accepts it. When nothing validates, the *original* rejection is thrown
88
- * verbatim, built from the arguments that produced it.
86
+ * {@link repairRepresentations} undoes a stringified array or object or an
87
+ * integer sent for a string, and the arguments are parsed once more (#234,
88
+ * #479, #487), the repair kept only if the author's own schema now accepts it.
89
+ * When that attempt still fails and its drop discarded a key,
90
+ * {@link prevalidateAliasFirst} reruns the stages alias-first and the same
91
+ * parse-then-repair runs on the result, kept only if it validates (#563).
92
+ * {@link recordPrevalidation} then counts and logs the attempt the handler
93
+ * receives, so a call the first attempt validates is untouched by the retry.
94
+ * When nothing validates, the *original* rejection of the last attempt is
95
+ * thrown — the retry's when it ran, since there every key the drop discarded
96
+ * reached its target and the issues name what is wrong with the value it
97
+ * carried — built from the arguments that produced it, identical to the one
98
+ * the same call gets under `input: { coerce: false }`. It carries the rewrites
99
+ * and underscore-rule drops that attempt made as `data.input`, and as
100
+ * sentences closing the hint (#468); a call with neither gains no
101
+ * `data.input`.
89
102
  *
90
103
  * The single argument-rejection path. {@link createToolHandler} and the
91
104
  * `runToolContract` test helper both route through it, so a test written to
@@ -1 +1 @@
1
- {"version":3,"file":"toolHandlerFactory.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolHandlerFactory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"toolHandlerFactory.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolHandlerFactory.ts"],"names":[],"mappings":"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"}
@@ -17,8 +17,8 @@ import { measureToolExecution, recordToolRejection } from '../../../utils/intern
17
17
  import { requestContextService, withExtra, } from '../../../utils/internal/requestContext.js';
18
18
  import { sanitization } from '../../../utils/security/sanitization.js';
19
19
  import { ATTR_MCP_TOOL_ENRICHED } from '../../../utils/telemetry/attributes.js';
20
- import { countCoerced, prevalidateToolArguments, repairRepresentations, } from './inputPrevalidation.js';
21
- import { isZodObjectSchema } from './schemaShape.js';
20
+ import { countCoerced, prevalidateAliasFirst, prevalidateToolArguments, recordPrevalidation, repairRepresentations, } from './inputPrevalidation.js';
21
+ import { isZodObjectSchema, zodDef } from './schemaShape.js';
22
22
  // ---------------------------------------------------------------------------
23
23
  // Default formatter
24
24
  // ---------------------------------------------------------------------------
@@ -94,12 +94,13 @@ function renderBranchableTerms(data) {
94
94
  * stay JSON-only.
95
95
  *
96
96
  * The `Recovery:` line is dropped when the message already contains the hint
97
- * verbatim (#459) — `buildArgumentRecoveryHint` falls back to an issue's own
98
- * message for a constraint or refinement, and repeating that sentence costs
99
- * the reader without adding a next step. Containment, not equality: the
100
- * argument-rejection preamble and a field-path prefix both leave the hint's
101
- * whole text on screen. `structuredContent.error.data.recovery.hint` stays
102
- * populated either way, so #445's guarantee holds on the JSON surface.
97
+ * verbatim (#459) — `buildArgumentRecoveryHint` restates a constraint or
98
+ * refinement issue as its own message line, and a hint made only of those is
99
+ * the message's issue text, so repeating it costs the reader without adding a
100
+ * next step. Containment, not equality: the argument-rejection preamble leaves
101
+ * that text whole on screen, and so does a handler hint the message embeds.
102
+ * `structuredContent.error.data.recovery.hint` stays populated either way, so
103
+ * #445's guarantee holds on the JSON surface.
103
104
  *
104
105
  * Note: `_meta.error` is intentionally NOT emitted — the error code, message,
105
106
  * and data live on `structuredContent.error` instead, mirroring the success
@@ -146,36 +147,91 @@ const ABSENT = Symbol('absent');
146
147
  * is how Zod itself reads it.
147
148
  */
148
149
  function readArgumentAt(args, path) {
149
- let cursor = args;
150
- for (const segment of path) {
151
- if (cursor === null || typeof cursor !== 'object')
152
- return ABSENT;
153
- if (!Object.hasOwn(cursor, segment))
154
- return ABSENT;
155
- cursor = cursor[segment];
156
- }
157
- return cursor === undefined ? ABSENT : cursor;
150
+ const value = path.reduce(stepInto, args);
151
+ return value === undefined ? ABSENT : value;
152
+ }
153
+ /** The caller's value one step down, or `undefined` when nothing owns one there. */
154
+ function stepInto(value, step) {
155
+ return value !== null && typeof value === 'object' && Object.hasOwn(value, step)
156
+ ? value[step]
157
+ : undefined;
158
158
  }
159
159
  /** The accepted-value half of an `invalid_value` sentence, from the issue's own values. */
160
160
  function expectedValuesText(values) {
161
161
  const rendered = values.map((value) => JSON.stringify(value)).join('|');
162
162
  return values.length === 1 ? `Expected ${rendered}` : `Expected one of ${rendered}`;
163
163
  }
164
+ /** The branch's only issue, when it has exactly one. */
165
+ function onlyIssue(branch) {
166
+ return branch.length === 1 ? branch[0] : undefined;
167
+ }
168
+ /** Whether any of a branch's issues names a path below the branch's root. */
169
+ function failsBelowRoot(branch) {
170
+ return branch.some((issue) => issue.path.length > 0);
171
+ }
164
172
  /**
165
- * The union branches worth rendering: every branch except one whose only issue
166
- * is a single-valued `invalid_value`.
173
+ * The union branches worth rendering. Two filters, both reading issue shape
174
+ * only, never message text:
167
175
  *
168
- * That shape is the `z.literal('')` blank-field sentinel of the form-client
169
- * convention — never the branch that says what would have been accepted. The
170
- * filter reads issue shape only, never message text, so a one-entry
171
- * `z.enum([...])` (which Zod reports identically) is filtered too and the
172
- * caller falls back to the union's own message.
176
+ * - **#417** — drop a branch whose only issue is a single-valued
177
+ * `invalid_value`. That shape is the `z.literal('')` blank-field sentinel of
178
+ * the form-client convention — never the branch that says what would have
179
+ * been accepted. A one-entry `z.enum([...])`, which Zod reports identically,
180
+ * is filtered too, and the caller falls back to the union's own message.
181
+ * - **#492** — once some branch fails below its root, drop every branch whose
182
+ * only issue is a root `invalid_type`. In a one-or-many field
183
+ * (`z.union([z.array(Item), Item])`) that branch merely says the value is
184
+ * the other shape; the branch that failed inside the value is the one that
185
+ * says what to change. When every branch fails at its root, none is dropped.
173
186
  */
174
187
  function selectUnionBranches(branches) {
175
- return branches.filter((branch) => {
176
- const only = branch.length === 1 ? branch[0] : undefined;
188
+ const selected = branches.filter((branch) => {
189
+ const only = onlyIssue(branch);
177
190
  return !(only?.code === 'invalid_value' && only.values.length === 1);
178
191
  });
192
+ if (!selected.some(failsBelowRoot))
193
+ return selected;
194
+ return selected.filter((branch) => {
195
+ const only = onlyIssue(branch);
196
+ return !(only?.code === 'invalid_type' && only.path.length === 0);
197
+ });
198
+ }
199
+ /**
200
+ * The issues a rejection renders, in order: Zod's list, except that a union
201
+ * left with one selected branch that fails below its root is replaced by that
202
+ * branch's issues under the union's path (#492) — so a one-or-many field
203
+ * reports a list element's field error exactly as a list-only field does
204
+ * (`items.1.name: …`). Recursive, so a one-or-many union nested in another, or
205
+ * inside a list element, resolves the same way at every level.
206
+ *
207
+ * The message ({@link formatInputValidationMessage}) and the hint
208
+ * ({@link buildArgumentRecoveryHint}) both render from this list, which keeps
209
+ * the hint's restatements identical to the message's lines. `data.issues` is
210
+ * never rebuilt from it: it ships Zod's own list.
211
+ */
212
+ function renderedIssues(issues, prefix = []) {
213
+ return issues.flatMap((issue) => {
214
+ const path = [...prefix, ...issue.path];
215
+ if (issue.code === 'invalid_union') {
216
+ const [branch, ...others] = selectUnionBranches(issue.errors);
217
+ if (branch && others.length === 0 && failsBelowRoot(branch)) {
218
+ return renderedIssues(branch, path);
219
+ }
220
+ }
221
+ return [{ issue, path }];
222
+ });
223
+ }
224
+ /** `items.1.name` — the dotted form a path takes in the message and the hint. */
225
+ function dottedPath(path) {
226
+ return path.map(String).join('.');
227
+ }
228
+ /**
229
+ * One line of the rendered detail: `path: message`, or the bare message at the
230
+ * root. `args` decides the absent/present bit {@link renderIssueMessage} reads.
231
+ */
232
+ function renderIssueLine({ issue, path }, args) {
233
+ const message = renderIssueMessage(issue, readArgumentAt(args, path) === ABSENT);
234
+ return path.length > 0 ? `${dottedPath(path)}: ${message}` : message;
179
235
  }
180
236
  /**
181
237
  * The readable half of one issue's rendered line.
@@ -228,7 +284,7 @@ function renderIssueMessage(issue, absent) {
228
284
  */
229
285
  function renderBranchIssue(issue, absent) {
230
286
  const message = renderIssueMessage(issue, absent);
231
- return issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ${message}` : message;
287
+ return issue.path.length > 0 ? `${dottedPath(issue.path)}: ${message}` : message;
232
288
  }
233
289
  /**
234
290
  * Renders an argument-validation failure the way the MCP SDK renders its own,
@@ -241,11 +297,8 @@ function renderBranchIssue(issue, absent) {
241
297
  * {@link readArgumentAt}; see {@link renderIssueMessage} for what that decides.
242
298
  */
243
299
  export function formatInputValidationMessage(toolName, error, args) {
244
- const detail = error.issues
245
- .map((issue) => {
246
- const message = renderIssueMessage(issue, readArgumentAt(args, issue.path) === ABSENT);
247
- return issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ${message}` : message;
248
- })
300
+ const detail = renderedIssues(error.issues)
301
+ .map((entry) => renderIssueLine(entry, args))
249
302
  .join(', ');
250
303
  return `Input validation error: Invalid arguments for tool ${toolName}: ${detail}`;
251
304
  }
@@ -277,30 +330,145 @@ function joinNames(names) {
277
330
  function rootPropertyNames(input) {
278
331
  return isZodObjectSchema(input) ? Object.keys(input.shape) : [];
279
332
  }
333
+ /**
334
+ * The `z.object()` a nested unknown-key issue sits in, found by walking its
335
+ * rendered path down from `schema` beside the caller's own `value` — or
336
+ * `undefined` when the path does not land on exactly one object (#566).
337
+ *
338
+ * Wrappers (`optional`, `nullable`, `default`, …), `pipe`, and `z.lazy()` are
339
+ * looked through. A numeric step enters an array element or tuple item; a
340
+ * string step, a property or a record value. A discriminated union follows the
341
+ * variant the argument's own discriminator selects, as Zod did. A plain union
342
+ * follows the one option under which the rest of the path still lands on an
343
+ * object — the branch {@link renderedIssues} lifted under #492. Anything else,
344
+ * an intersection or a union two options satisfy, resolves to nothing.
345
+ */
346
+ function objectSchemaAt(schema, path, value) {
347
+ const def = zodDef(schema);
348
+ if (!def)
349
+ return undefined;
350
+ if (def.type === 'lazy' && def.getter)
351
+ return objectSchemaAt(def.getter(), path, value);
352
+ if (def.type === 'pipe')
353
+ return objectSchemaAt(def.in, path, value);
354
+ if (def.innerType !== undefined)
355
+ return objectSchemaAt(def.innerType, path, value);
356
+ if (def.type === 'union')
357
+ return unionOptionAt(def, path, value);
358
+ const [step, ...rest] = path;
359
+ if (step === undefined)
360
+ return isZodObjectSchema(schema) ? schema : undefined;
361
+ const next = stepInto(value, step);
362
+ switch (def.type) {
363
+ case 'object':
364
+ return typeof step === 'string' && def.shape && Object.hasOwn(def.shape, step)
365
+ ? objectSchemaAt(def.shape[step], rest, next)
366
+ : undefined;
367
+ case 'record':
368
+ return objectSchemaAt(def.valueType, rest, next);
369
+ case 'array':
370
+ return typeof step === 'number' ? objectSchemaAt(def.element, rest, next) : undefined;
371
+ case 'tuple':
372
+ return typeof step === 'number'
373
+ ? objectSchemaAt(def.items?.[step] ?? def.rest, rest, next)
374
+ : undefined;
375
+ default:
376
+ return undefined;
377
+ }
378
+ }
379
+ /** {@link objectSchemaAt} at a union: the one option the path resolves through. */
380
+ function unionOptionAt(def, path, value) {
381
+ const options = def.options ?? [];
382
+ const { discriminator } = def;
383
+ if (typeof discriminator === 'string') {
384
+ const tag = stepInto(value, discriminator);
385
+ const selected = options.find((option) => {
386
+ const field = isZodObjectSchema(option) ? option.shape[discriminator] : undefined;
387
+ return field?.safeParse(tag).success === true;
388
+ });
389
+ return selected === undefined ? undefined : objectSchemaAt(selected, path, value);
390
+ }
391
+ const resolved = options.flatMap((option) => objectSchemaAt(option, path, value) ?? []);
392
+ return resolved.length === 1 ? resolved[0] : undefined;
393
+ }
394
+ /**
395
+ * The unknown-key sentence for one `unrecognized_keys` issue.
396
+ *
397
+ * A root key names the root properties the tool advertises (#445). A key inside
398
+ * a nested strict object is named by its full path, beside the keys that object
399
+ * accepts (#566) — the root list there would send the caller to move the key to
400
+ * the root or rename it after a root field. Neither list is given when there is
401
+ * none to give: a discriminated-union root, a nested object declaring no keys,
402
+ * or a path {@link objectSchemaAt} cannot resolve.
403
+ */
404
+ function unknownKeySentence(input, keys, path, args) {
405
+ const label = keys.length === 1 ? 'Unknown key' : 'Unknown keys';
406
+ if (path.length === 0) {
407
+ const accepted = rootPropertyNames(input);
408
+ return accepted.length > 0
409
+ ? `${label} ${keys.join(', ')}. This tool accepts: ${accepted.join(', ')}.`
410
+ : `${label} ${keys.join(', ')}.`;
411
+ }
412
+ const where = dottedPath(path);
413
+ const named = keys.map((key) => `${where}.${key}`).join(', ');
414
+ const accepted = Object.keys(objectSchemaAt(input, path, args)?.shape ?? {});
415
+ return accepted.length > 0
416
+ ? `${label} ${named}. ${where} accepts: ${accepted.join(', ')}.`
417
+ : `${label} ${named}.`;
418
+ }
419
+ /**
420
+ * The wrong-type sentence for one `invalid_type` issue.
421
+ *
422
+ * `int` is the one expectation a JSON number fails by type — `.int()`,
423
+ * `z.int()`, `z.int32()`, and `z.uint32()` all report it, while range and
424
+ * safe-integer violations arrive as `too_big` / `too_small` — so a number
425
+ * arriving there has a fractional part, and the sentence names that fix
426
+ * rather than the type names `int` and `number`, which the value already
427
+ * satisfies (#499).
428
+ */
429
+ function wrongTypeSentence(subject, expected, arrived) {
430
+ if (arrived === ABSENT)
431
+ return `Send ${subject} as ${withArticle(expected)}.`;
432
+ if (expected === 'int' && typeof arrived === 'number') {
433
+ return `Send ${subject} as an integer, not a fractional number.`;
434
+ }
435
+ return `Send ${subject} as ${withArticle(expected)}, not ${arrivedTypeText(arrived)}.`;
436
+ }
280
437
  /**
281
438
  * Synthesizes `data.recovery.hint` from the Zod issues, the raw arguments, and
282
439
  * the root schema (#445) — so the one failure a weaker model hits most often
283
440
  * carries the same next step every handler-thrown error does, instead of
284
441
  * costing a round trip for the schema.
285
442
  *
286
- * One sentence per issue, joined into a single hint, except that every missing
287
- * required field collapses into one `Provide …` sentence at the first of their
288
- * positions. An issue no bucket claims contributes its rendered message
289
- * unchanged, which keeps the hint nonempty for any rejection Zod can produce.
443
+ * One sentence per {@link renderedIssues} entry, joined into a single hint,
444
+ * except that every missing required field collapses into one `Provide …`
445
+ * sentence at the first of their positions. An issue no bucket claims is
446
+ * restated as its message line — `start: Must be …`, path included, so
447
+ * identical constraints on different fields stay distinguishable (#493).
448
+ *
449
+ * When every sentence is a restatement, the hint is the message's issue text
450
+ * verbatim, which is what lets {@link buildToolErrorResult} drop the
451
+ * `Recovery:` line (#459). When restatements share the hint with the
452
+ * framework's own sentences, each is terminated so it cannot run into the next
453
+ * one, and a sentence already stated is not repeated.
454
+ *
455
+ * `report` closes the hint with what the pre-validation step changed before
456
+ * the parse (#468) — `Validated query as targetQuery.` for a rewritten key,
457
+ * `Dropped undeclared key _max.` for an underscore-rule drop — since the issues
458
+ * name only the keys that were validated. Both are framework sentences, never
459
+ * restatements, so a hint carrying one keeps its `Recovery:` line.
290
460
  */
291
- function buildArgumentRecoveryHint(def, error, args) {
461
+ function buildArgumentRecoveryHint(def, error, args, report) {
292
462
  const sentences = [];
463
+ const restatements = [];
293
464
  const missing = [];
294
465
  let missingSlot = -1;
295
- for (const issue of error.issues) {
296
- const path = issue.path.map(String).join('.');
297
- const arrived = readArgumentAt(args, issue.path);
466
+ for (const entry of renderedIssues(error.issues)) {
467
+ const { issue } = entry;
468
+ const path = dottedPath(entry.path);
469
+ const arrived = readArgumentAt(args, entry.path);
298
470
  if (issue.code === 'unrecognized_keys') {
299
- const label = issue.keys.length === 1 ? 'Unknown key' : 'Unknown keys';
300
- const accepted = rootPropertyNames(def.input);
301
- sentences.push(accepted.length > 0
302
- ? `${label} ${issue.keys.join(', ')}. This tool accepts: ${accepted.join(', ')}.`
303
- : `${label} ${issue.keys.join(', ')}.`);
471
+ sentences.push(unknownKeySentence(def.input, issue.keys, entry.path, args));
304
472
  continue;
305
473
  }
306
474
  if (path.length > 0 && arrived === ABSENT) {
@@ -312,18 +480,26 @@ function buildArgumentRecoveryHint(def, error, args) {
312
480
  continue;
313
481
  }
314
482
  if (issue.code === 'invalid_type') {
315
- const subject = path.length > 0 ? path : 'the arguments';
316
- const expected = withArticle(issue.expected);
317
- sentences.push(arrived === ABSENT
318
- ? `Send ${subject} as ${expected}.`
319
- : `Send ${subject} as ${expected}, not ${arrivedTypeText(arrived)}.`);
483
+ sentences.push(wrongTypeSentence(path.length > 0 ? path : 'the arguments', issue.expected, arrived));
320
484
  continue;
321
485
  }
322
- sentences.push(renderIssueMessage(issue, false));
486
+ const line = renderIssueLine(entry, args);
487
+ restatements.push(line);
488
+ sentences.push(terminateSentence(line));
489
+ }
490
+ if (report && report.aliased.length > 0) {
491
+ const rewrites = report.aliased.map(({ alias, target }) => `${alias} as ${target}`);
492
+ sentences.push(`Validated ${joinNames(rewrites)}.`);
493
+ }
494
+ if (report && report.ignored.length > 0) {
495
+ const label = report.ignored.length === 1 ? 'key' : 'keys';
496
+ sentences.push(`Dropped undeclared ${label} ${joinNames(report.ignored)}.`);
323
497
  }
498
+ if (restatements.length === sentences.length)
499
+ return restatements.join(', ');
324
500
  if (missingSlot >= 0)
325
501
  sentences[missingSlot] = `Provide ${joinNames(missing)}.`;
326
- return sentences.join(' ');
502
+ return [...new Set(sentences)].join(' ');
327
503
  }
328
504
  /**
329
505
  * Validates raw tool arguments against the definition's `input` schema, or
@@ -337,10 +513,22 @@ function buildArgumentRecoveryHint(def, error, args) {
337
513
  * An ordered pre-validation step wraps the parse. Before it,
338
514
  * {@link prevalidateToolArguments} drops client-added keys (#453) and rewrites
339
515
  * key aliases (#452); after a failure — and only then —
340
- * {@link repairRepresentations} undoes a stringified array and the arguments
341
- * are parsed once more (#234), the repair kept only if the author's own schema
342
- * now accepts it. When nothing validates, the *original* rejection is thrown
343
- * verbatim, built from the arguments that produced it.
516
+ * {@link repairRepresentations} undoes a stringified array or object or an
517
+ * integer sent for a string, and the arguments are parsed once more (#234,
518
+ * #479, #487), the repair kept only if the author's own schema now accepts it.
519
+ * When that attempt still fails and its drop discarded a key,
520
+ * {@link prevalidateAliasFirst} reruns the stages alias-first and the same
521
+ * parse-then-repair runs on the result, kept only if it validates (#563).
522
+ * {@link recordPrevalidation} then counts and logs the attempt the handler
523
+ * receives, so a call the first attempt validates is untouched by the retry.
524
+ * When nothing validates, the *original* rejection of the last attempt is
525
+ * thrown — the retry's when it ran, since there every key the drop discarded
526
+ * reached its target and the issues name what is wrong with the value it
527
+ * carried — built from the arguments that produced it, identical to the one
528
+ * the same call gets under `input: { coerce: false }`. It carries the rewrites
529
+ * and underscore-rule drops that attempt made as `data.input`, and as
530
+ * sentences closing the hint (#468); a call with neither gains no
531
+ * `data.input`.
344
532
  *
345
533
  * The single argument-rejection path. {@link createToolHandler} and the
346
534
  * `runToolContract` test helper both route through it, so a test written to
@@ -351,26 +539,54 @@ function buildArgumentRecoveryHint(def, error, args) {
351
539
  * ({@link parseToolOutput}).
352
540
  */
353
541
  export function parseToolArguments(def, input, options = {}) {
354
- const prepared = prevalidateToolArguments(def, input, options.input, options.context);
355
- const parsed = def.input.safeParse(prepared);
542
+ const first = prevalidateToolArguments(def, input, options.input);
543
+ const parsed = parseAttempt(def, first.args, options.input);
356
544
  if (parsed.success)
357
- return parsed.data;
358
- if (options.input?.coerce !== false) {
359
- const repaired = repairRepresentations(prepared, parsed.error.issues);
360
- if (repaired !== prepared) {
361
- const retried = def.input.safeParse(repaired);
362
- if (retried.success) {
363
- countCoerced(def.name, options.context);
364
- return retried.data;
365
- }
366
- }
545
+ return accept(def, first, parsed, options.context);
546
+ let rejected = { attempt: first, error: parsed.error };
547
+ const retry = prevalidateAliasFirst(def, input, first, options.input);
548
+ if (retry) {
549
+ const retried = parseAttempt(def, retry.args, options.input);
550
+ if (retried.success)
551
+ return accept(def, retry, retried, options.context);
552
+ rejected = { attempt: retry, error: retried.error };
367
553
  }
368
- throw new McpError(JsonRpcErrorCode.InvalidParams, formatInputValidationMessage(def.name, parsed.error, prepared), {
369
- issues: parsed.error.issues,
554
+ const { attempt, error } = rejected;
555
+ recordPrevalidation(def, attempt, options.context);
556
+ const { report } = attempt;
557
+ throw new McpError(JsonRpcErrorCode.InvalidParams, formatInputValidationMessage(def.name, error, attempt.args), {
558
+ issues: error.issues,
370
559
  reason: INVALID_ARGUMENTS_REASON,
371
- recovery: { hint: buildArgumentRecoveryHint(def, parsed.error, prepared) },
560
+ ...(report && { input: report }),
561
+ recovery: { hint: buildArgumentRecoveryHint(def, error, attempt.args, report) },
372
562
  });
373
563
  }
564
+ /**
565
+ * Parses one attempt's arguments, and on failure repairs them once and
566
+ * re-parses, keeping the repair only if the schema then accepts it. A failure
567
+ * carries the first parse's error: a discarded repair leaves no trace.
568
+ */
569
+ function parseAttempt(def, args, options) {
570
+ const parsed = def.input.safeParse(args);
571
+ if (parsed.success)
572
+ return { success: true, data: parsed.data, coerced: [] };
573
+ if (options?.coerce !== false) {
574
+ const repair = repairRepresentations(args, parsed.error.issues);
575
+ if (repair.args !== args) {
576
+ const retried = def.input.safeParse(repair.args);
577
+ if (retried.success)
578
+ return { success: true, data: retried.data, coerced: repair.kinds };
579
+ }
580
+ }
581
+ return { success: false, error: parsed.error };
582
+ }
583
+ /** Emits the winning attempt's telemetry and hands its arguments to the handler. */
584
+ function accept(def, attempt, parsed, context) {
585
+ recordPrevalidation(def, attempt, context);
586
+ if (parsed.coerced.length > 0)
587
+ countCoerced(def.name, parsed.coerced, context);
588
+ return parsed.data;
589
+ }
374
590
  /**
375
591
  * Builds an error `CallToolResult` from a raw thrown value. Classifies via
376
592
  * {@link ErrorHandler.classifyOnly} when the value isn't already an
@@ -422,15 +638,15 @@ export function parseToolOutput(def, value) {
422
638
  });
423
639
  }
424
640
  /**
425
- * A contract entry's `when` text, terminated so the entry that follows it —
426
- * and the description's own trailing sentence — starts a new one (#389).
427
- * Nothing validates a punctuation convention on `when`, and an entry authored
428
- * as a fragment otherwise runs into whatever comes next. Punctuating at the
429
- * join leaves the authored text alone, terminator or not.
641
+ * `text`, trimmed and ending in terminal punctuation, so whatever is joined
642
+ * after it starts a new sentence. Punctuating at the join leaves authored text
643
+ * alone, terminator or not: a contract entry's `when` (#389), which nothing
644
+ * validates a punctuation convention on, and an issue message restated in an
645
+ * argument hint beside the framework's own sentences (#493).
430
646
  */
431
- function terminateWhen(when) {
432
- const text = when.trim();
433
- return /[.?!]$/.test(text) ? text : `${text}.`;
647
+ function terminateSentence(text) {
648
+ const trimmed = text.trim();
649
+ return /[.?!]$/.test(trimmed) ? trimmed : `${trimmed}.`;
434
650
  }
435
651
  /**
436
652
  * The error envelope a tool can put on `structuredContent` when it fails,
@@ -457,7 +673,7 @@ function toolErrorEnvelopeSchema(def) {
457
673
  ? z
458
674
  .string()
459
675
  .describe(`Machine-readable failure mode. Declared by this tool: ${contract
460
- .map((entry) => `\`${entry.reason}\`: ${terminateWhen(entry.when)}`)
676
+ .map((entry) => `\`${entry.reason}\`: ${terminateSentence(entry.when)}`)
461
677
  .join(' ')} Other values are possible when a failure originates below the handler.`)
462
678
  .meta({ examples: reasons })
463
679
  : z.string().describe('Machine-readable failure mode.');