@cyanheads/mcp-ts-core 0.13.7 → 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.
- package/AGENTS.md +19 -15
- package/CLAUDE.md +19 -15
- package/README.md +4 -2
- package/changelog/0.13.x/0.13.8.md +101 -0
- package/changelog/0.13.x/0.13.9.md +113 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +39 -9
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +6 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +20 -6
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +25 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +13 -3
- package/dist/core/context.js.map +1 -1
- package/dist/core/serverManifest.d.ts +6 -0
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +6 -0
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/core/worker.d.ts +2 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +2 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts +3 -2
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +9 -2
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/handler-body-rules.d.ts.map +1 -1
- package/dist/linter/rules/handler-body-rules.js +10 -4
- package/dist/linter/rules/handler-body-rules.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +5 -0
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +44 -17
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts +2 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +36 -1
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +14 -5
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +15 -8
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/outputContract.d.ts +33 -0
- package/dist/mcp-server/outputContract.d.ts.map +1 -0
- package/dist/mcp-server/outputContract.js +43 -0
- package/dist/mcp-server/outputContract.js.map +1 -0
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +10 -2
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +163 -40
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +39 -15
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +361 -93
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +4 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +2 -5
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +65 -9
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.js +2 -2
- package/dist/mcp-server/transports/http/sessionStore.js.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +8 -4
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/core/DataCanvas.d.ts.map +1 -1
- package/dist/services/canvas/core/DataCanvas.js +7 -5
- package/dist/services/canvas/core/DataCanvas.js.map +1 -1
- package/dist/services/canvas/core/canvasFactory.d.ts.map +1 -1
- package/dist/services/canvas/core/canvasFactory.js +2 -2
- package/dist/services/canvas/core/canvasFactory.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +645 -344
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
- package/dist/services/llm/providers/openrouter.provider.js +1 -1
- package/dist/services/llm/providers/openrouter.provider.js.map +1 -1
- package/dist/services/mirror/core/defineMirror.d.ts +1 -0
- package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
- package/dist/services/mirror/core/defineMirror.js +1 -0
- package/dist/services/mirror/core/defineMirror.js.map +1 -1
- package/dist/services/speech/providers/elevenlabs.provider.js +3 -3
- package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
- package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/whisper.provider.js +5 -5
- package/dist/services/speech/providers/whisper.provider.js.map +1 -1
- package/dist/storage/core/StorageService.d.ts.map +1 -1
- package/dist/storage/core/StorageService.js +3 -6
- package/dist/storage/core/StorageService.js.map +1 -1
- package/dist/storage/core/storageFactory.d.ts.map +1 -1
- package/dist/storage/core/storageFactory.js +12 -15
- package/dist/storage/core/storageFactory.js.map +1 -1
- package/dist/storage/core/storageValidation.d.ts +13 -13
- package/dist/storage/core/storageValidation.d.ts.map +1 -1
- package/dist/storage/core/storageValidation.js +49 -125
- package/dist/storage/core/storageValidation.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +5 -3
- package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +3 -3
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +4 -4
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +6 -5
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +7 -1
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/testing/index.d.ts +15 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +51 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +7 -4
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/formatting/codeSpan.d.ts +27 -0
- package/dist/utils/formatting/codeSpan.d.ts.map +1 -0
- package/dist/utils/formatting/codeSpan.js +42 -0
- package/dist/utils/formatting/codeSpan.js.map +1 -0
- package/dist/utils/formatting/diffFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/diffFormatter.js +7 -15
- package/dist/utils/formatting/diffFormatter.js.map +1 -1
- package/dist/utils/formatting/markdownBuilder.d.ts +12 -5
- package/dist/utils/formatting/markdownBuilder.d.ts.map +1 -1
- package/dist/utils/formatting/markdownBuilder.js +14 -2
- package/dist/utils/formatting/markdownBuilder.js.map +1 -1
- package/dist/utils/formatting/tableFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/tableFormatter.js +5 -9
- package/dist/utils/formatting/tableFormatter.js.map +1 -1
- package/dist/utils/formatting/treeFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/treeFormatter.js +5 -9
- package/dist/utils/formatting/treeFormatter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +17 -10
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +47 -26
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/mappings.d.ts +17 -1
- package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/mappings.js +22 -1
- package/dist/utils/internal/error-handler/mappings.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +2 -0
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logger.d.ts +75 -3
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +181 -52
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +11 -0
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +46 -12
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +11 -5
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +50 -23
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/pacer.d.ts +38 -5
- package/dist/utils/network/pacer.d.ts.map +1 -1
- package/dist/utils/network/pacer.js +87 -25
- package/dist/utils/network/pacer.js.map +1 -1
- package/dist/utils/network/retry.d.ts +16 -8
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +19 -8
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.d.ts +18 -2
- package/dist/utils/overflow/outlineOnOverflow.d.ts.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.js +28 -3
- package/dist/utils/overflow/outlineOnOverflow.js.map +1 -1
- package/dist/utils/pagination/pagination.d.ts +3 -1
- package/dist/utils/pagination/pagination.d.ts.map +1 -1
- package/dist/utils/pagination/pagination.js +10 -2
- package/dist/utils/pagination/pagination.js.map +1 -1
- package/dist/utils/parsing/csvParser.d.ts.map +1 -1
- package/dist/utils/parsing/csvParser.js +4 -2
- package/dist/utils/parsing/csvParser.js.map +1 -1
- package/dist/utils/parsing/htmlExtractor.js +1 -1
- package/dist/utils/parsing/htmlExtractor.js.map +1 -1
- package/dist/utils/parsing/jsonParser.d.ts.map +1 -1
- package/dist/utils/parsing/jsonParser.js +3 -1
- package/dist/utils/parsing/jsonParser.js.map +1 -1
- package/dist/utils/parsing/xmlParser.d.ts.map +1 -1
- package/dist/utils/parsing/xmlParser.js +3 -1
- package/dist/utils/parsing/xmlParser.js.map +1 -1
- package/dist/utils/parsing/yamlParser.d.ts.map +1 -1
- package/dist/utils/parsing/yamlParser.js +3 -1
- package/dist/utils/parsing/yamlParser.js.map +1 -1
- package/dist/utils/security/idGenerator.d.ts.map +1 -1
- package/dist/utils/security/idGenerator.js +20 -4
- package/dist/utils/security/idGenerator.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +31 -0
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +98 -11
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +21 -2
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +21 -2
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts +9 -3
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +85 -13
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +3 -3
- package/framework-skills/add-export/SKILL.md +5 -16
- package/framework-skills/add-prompt/SKILL.md +7 -3
- package/framework-skills/add-resource/SKILL.md +7 -5
- package/framework-skills/add-tool/SKILL.md +12 -10
- package/framework-skills/api-auth/SKILL.md +4 -2
- package/framework-skills/api-canvas/SKILL.md +19 -10
- package/framework-skills/api-config/SKILL.md +9 -6
- package/framework-skills/api-context/SKILL.md +16 -5
- package/framework-skills/api-errors/SKILL.md +23 -17
- package/framework-skills/api-linter/SKILL.md +32 -9
- package/framework-skills/api-mirror/SKILL.md +2 -1
- package/framework-skills/api-telemetry/SKILL.md +34 -14
- package/framework-skills/api-testing/SKILL.md +5 -3
- package/framework-skills/api-utils/SKILL.md +10 -10
- package/framework-skills/api-utils/references/formatting.md +1 -1
- package/framework-skills/api-utils/references/parsing.md +2 -2
- package/framework-skills/api-utils/references/security.md +6 -4
- package/framework-skills/design-mcp-server/SKILL.md +2 -2
- package/framework-skills/field-test/SKILL.md +4 -4
- package/framework-skills/git-wrapup/SKILL.md +12 -7
- package/framework-skills/maintenance/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +7 -6
- package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
- package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
- package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
- package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
- package/framework-skills/polish-docs-meta/SKILL.md +4 -4
- package/framework-skills/polish-docs-meta/references/readme.md +1 -0
- package/framework-skills/release-and-publish/SKILL.md +7 -5
- package/framework-skills/release-pr-review/SKILL.md +37 -23
- package/framework-skills/report-issue-framework/SKILL.md +7 -5
- package/framework-skills/report-issue-local/SKILL.md +8 -6
- package/framework-skills/security-pass/SKILL.md +8 -8
- package/framework-skills/techniques/SKILL.md +1 -1
- package/framework-skills/techniques/references/outline-on-overflow.md +12 -7
- package/package.json +20 -5
- package/scripts/check-skill-versions.ts +103 -22
- package/scripts/devcheck.ts +11 -9
- package/scripts/lint-mcp.ts +87 -27
- package/scripts/lint-packaging.ts +99 -1
- package/scripts/release-github.ts +117 -5
- package/templates/.env.example +4 -0
- package/templates/Dockerfile +26 -6
- package/templates/_.mcpbignore +2 -0
- package/templates/package.json +1 -0
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
* @module src/mcp-server/tools/utils/toolHandlerFactory
|
|
5
5
|
*/
|
|
6
6
|
import { ZodError, z } from 'zod';
|
|
7
|
+
import { config } from '../../../config/index.js';
|
|
7
8
|
import { readContentStore, readEnrichmentStore } from '../../../core/context.js';
|
|
8
9
|
import { buildHandlerContext, handlerParentContext, resolveHandlerRequest, } from '../../handlerContext.js';
|
|
9
10
|
import { isInputRequiredSignal } from '../../inputRequired.js';
|
|
11
|
+
import { parseOutputContract } from '../../outputContract.js';
|
|
10
12
|
import { withRequiredScopes } from '../../transports/auth/lib/authUtils.js';
|
|
11
13
|
import { internalError, JsonRpcErrorCode, McpError, } from '../../../types-global/errors.js';
|
|
12
14
|
import { resolvePartialResultKeys } from '../../../utils/formatting/partialResult.js';
|
|
13
15
|
import { asRequestCancelled, ErrorHandler } from '../../../utils/internal/error-handler/errorHandler.js';
|
|
14
|
-
import { measureToolExecution } from '../../../utils/internal/performance.js';
|
|
15
|
-
import { requestContextService } from '../../../utils/internal/requestContext.js';
|
|
16
|
+
import { measureToolExecution, recordToolRejection } from '../../../utils/internal/performance.js';
|
|
17
|
+
import { requestContextService, withExtra, } from '../../../utils/internal/requestContext.js';
|
|
18
|
+
import { sanitization } from '../../../utils/security/sanitization.js';
|
|
16
19
|
import { ATTR_MCP_TOOL_ENRICHED } from '../../../utils/telemetry/attributes.js';
|
|
17
|
-
import { countCoerced, prevalidateToolArguments, repairRepresentations, } from './inputPrevalidation.js';
|
|
18
|
-
import { isZodObjectSchema } from './schemaShape.js';
|
|
20
|
+
import { countCoerced, prevalidateAliasFirst, prevalidateToolArguments, recordPrevalidation, repairRepresentations, } from './inputPrevalidation.js';
|
|
21
|
+
import { isZodObjectSchema, zodDef } from './schemaShape.js';
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
20
23
|
// Default formatter
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
@@ -91,12 +94,13 @@ function renderBranchableTerms(data) {
|
|
|
91
94
|
* stay JSON-only.
|
|
92
95
|
*
|
|
93
96
|
* The `Recovery:` line is dropped when the message already contains the hint
|
|
94
|
-
* verbatim (#459) — `buildArgumentRecoveryHint`
|
|
95
|
-
*
|
|
96
|
-
* the
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* populated either way, so
|
|
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.
|
|
100
104
|
*
|
|
101
105
|
* Note: `_meta.error` is intentionally NOT emitted — the error code, message,
|
|
102
106
|
* and data live on `structuredContent.error` instead, mirroring the success
|
|
@@ -143,36 +147,91 @@ const ABSENT = Symbol('absent');
|
|
|
143
147
|
* is how Zod itself reads it.
|
|
144
148
|
*/
|
|
145
149
|
function readArgumentAt(args, path) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
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;
|
|
155
158
|
}
|
|
156
159
|
/** The accepted-value half of an `invalid_value` sentence, from the issue's own values. */
|
|
157
160
|
function expectedValuesText(values) {
|
|
158
161
|
const rendered = values.map((value) => JSON.stringify(value)).join('|');
|
|
159
162
|
return values.length === 1 ? `Expected ${rendered}` : `Expected one of ${rendered}`;
|
|
160
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
|
+
}
|
|
161
172
|
/**
|
|
162
|
-
* The union branches worth rendering
|
|
163
|
-
*
|
|
173
|
+
* The union branches worth rendering. Two filters, both reading issue shape
|
|
174
|
+
* only, never message text:
|
|
164
175
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* `z.enum([...])
|
|
169
|
-
* 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.
|
|
170
186
|
*/
|
|
171
187
|
function selectUnionBranches(branches) {
|
|
172
|
-
|
|
173
|
-
const only = branch
|
|
188
|
+
const selected = branches.filter((branch) => {
|
|
189
|
+
const only = onlyIssue(branch);
|
|
174
190
|
return !(only?.code === 'invalid_value' && only.values.length === 1);
|
|
175
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;
|
|
176
235
|
}
|
|
177
236
|
/**
|
|
178
237
|
* The readable half of one issue's rendered line.
|
|
@@ -225,7 +284,7 @@ function renderIssueMessage(issue, absent) {
|
|
|
225
284
|
*/
|
|
226
285
|
function renderBranchIssue(issue, absent) {
|
|
227
286
|
const message = renderIssueMessage(issue, absent);
|
|
228
|
-
return issue.path.length > 0 ? `${issue.path
|
|
287
|
+
return issue.path.length > 0 ? `${dottedPath(issue.path)}: ${message}` : message;
|
|
229
288
|
}
|
|
230
289
|
/**
|
|
231
290
|
* Renders an argument-validation failure the way the MCP SDK renders its own,
|
|
@@ -238,11 +297,8 @@ function renderBranchIssue(issue, absent) {
|
|
|
238
297
|
* {@link readArgumentAt}; see {@link renderIssueMessage} for what that decides.
|
|
239
298
|
*/
|
|
240
299
|
export function formatInputValidationMessage(toolName, error, args) {
|
|
241
|
-
const detail = error.issues
|
|
242
|
-
.map((
|
|
243
|
-
const message = renderIssueMessage(issue, readArgumentAt(args, issue.path) === ABSENT);
|
|
244
|
-
return issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ${message}` : message;
|
|
245
|
-
})
|
|
300
|
+
const detail = renderedIssues(error.issues)
|
|
301
|
+
.map((entry) => renderIssueLine(entry, args))
|
|
246
302
|
.join(', ');
|
|
247
303
|
return `Input validation error: Invalid arguments for tool ${toolName}: ${detail}`;
|
|
248
304
|
}
|
|
@@ -274,30 +330,145 @@ function joinNames(names) {
|
|
|
274
330
|
function rootPropertyNames(input) {
|
|
275
331
|
return isZodObjectSchema(input) ? Object.keys(input.shape) : [];
|
|
276
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
|
+
}
|
|
277
437
|
/**
|
|
278
438
|
* Synthesizes `data.recovery.hint` from the Zod issues, the raw arguments, and
|
|
279
439
|
* the root schema (#445) — so the one failure a weaker model hits most often
|
|
280
440
|
* carries the same next step every handler-thrown error does, instead of
|
|
281
441
|
* costing a round trip for the schema.
|
|
282
442
|
*
|
|
283
|
-
* One sentence per
|
|
284
|
-
* required field collapses into one `Provide …`
|
|
285
|
-
* positions. An issue no bucket claims
|
|
286
|
-
*
|
|
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.
|
|
287
460
|
*/
|
|
288
|
-
function buildArgumentRecoveryHint(def, error, args) {
|
|
461
|
+
function buildArgumentRecoveryHint(def, error, args, report) {
|
|
289
462
|
const sentences = [];
|
|
463
|
+
const restatements = [];
|
|
290
464
|
const missing = [];
|
|
291
465
|
let missingSlot = -1;
|
|
292
|
-
for (const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
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);
|
|
295
470
|
if (issue.code === 'unrecognized_keys') {
|
|
296
|
-
|
|
297
|
-
const accepted = rootPropertyNames(def.input);
|
|
298
|
-
sentences.push(accepted.length > 0
|
|
299
|
-
? `${label} ${issue.keys.join(', ')}. This tool accepts: ${accepted.join(', ')}.`
|
|
300
|
-
: `${label} ${issue.keys.join(', ')}.`);
|
|
471
|
+
sentences.push(unknownKeySentence(def.input, issue.keys, entry.path, args));
|
|
301
472
|
continue;
|
|
302
473
|
}
|
|
303
474
|
if (path.length > 0 && arrived === ABSENT) {
|
|
@@ -309,18 +480,26 @@ function buildArgumentRecoveryHint(def, error, args) {
|
|
|
309
480
|
continue;
|
|
310
481
|
}
|
|
311
482
|
if (issue.code === 'invalid_type') {
|
|
312
|
-
|
|
313
|
-
const expected = withArticle(issue.expected);
|
|
314
|
-
sentences.push(arrived === ABSENT
|
|
315
|
-
? `Send ${subject} as ${expected}.`
|
|
316
|
-
: `Send ${subject} as ${expected}, not ${arrivedTypeText(arrived)}.`);
|
|
483
|
+
sentences.push(wrongTypeSentence(path.length > 0 ? path : 'the arguments', issue.expected, arrived));
|
|
317
484
|
continue;
|
|
318
485
|
}
|
|
319
|
-
|
|
486
|
+
const line = renderIssueLine(entry, args);
|
|
487
|
+
restatements.push(line);
|
|
488
|
+
sentences.push(terminateSentence(line));
|
|
320
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)}.`);
|
|
497
|
+
}
|
|
498
|
+
if (restatements.length === sentences.length)
|
|
499
|
+
return restatements.join(', ');
|
|
321
500
|
if (missingSlot >= 0)
|
|
322
501
|
sentences[missingSlot] = `Provide ${joinNames(missing)}.`;
|
|
323
|
-
return sentences.join(' ');
|
|
502
|
+
return [...new Set(sentences)].join(' ');
|
|
324
503
|
}
|
|
325
504
|
/**
|
|
326
505
|
* Validates raw tool arguments against the definition's `input` schema, or
|
|
@@ -334,39 +513,80 @@ function buildArgumentRecoveryHint(def, error, args) {
|
|
|
334
513
|
* An ordered pre-validation step wraps the parse. Before it,
|
|
335
514
|
* {@link prevalidateToolArguments} drops client-added keys (#453) and rewrites
|
|
336
515
|
* key aliases (#452); after a failure — and only then —
|
|
337
|
-
* {@link repairRepresentations} undoes a stringified array
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
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`.
|
|
341
532
|
*
|
|
342
533
|
* The single argument-rejection path. {@link createToolHandler} and the
|
|
343
534
|
* `runToolContract` test helper both route through it, so a test written to
|
|
344
535
|
* the helper pins the code, message, and `content[]` text a deployment
|
|
345
|
-
* actually produces (#416).
|
|
346
|
-
* `ValidationError`
|
|
347
|
-
*
|
|
536
|
+
* actually produces (#416). A `ZodError` a handler throws from its own
|
|
537
|
+
* validation classifies as `ValidationError` and does not come through here;
|
|
538
|
+
* nor does the output-schema parse, which fails as `InternalError`
|
|
539
|
+
* ({@link parseToolOutput}).
|
|
348
540
|
*/
|
|
349
541
|
export function parseToolArguments(def, input, options = {}) {
|
|
350
|
-
const
|
|
351
|
-
const parsed = def.input
|
|
542
|
+
const first = prevalidateToolArguments(def, input, options.input);
|
|
543
|
+
const parsed = parseAttempt(def, first.args, options.input);
|
|
352
544
|
if (parsed.success)
|
|
353
|
-
return parsed.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
}
|
|
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 };
|
|
363
553
|
}
|
|
364
|
-
|
|
365
|
-
|
|
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,
|
|
366
559
|
reason: INVALID_ARGUMENTS_REASON,
|
|
367
|
-
|
|
560
|
+
...(report && { input: report }),
|
|
561
|
+
recovery: { hint: buildArgumentRecoveryHint(def, error, attempt.args, report) },
|
|
368
562
|
});
|
|
369
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
|
+
}
|
|
370
590
|
/**
|
|
371
591
|
* Builds an error `CallToolResult` from a raw thrown value. Classifies via
|
|
372
592
|
* {@link ErrorHandler.classifyOnly} when the value isn't already an
|
|
@@ -404,15 +624,29 @@ export function effectiveOutputSchema(def) {
|
|
|
404
624
|
return def.output.extend(def.enrichment);
|
|
405
625
|
}
|
|
406
626
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
627
|
+
* Parses a handler's returned value against the tool's `output` schema. A value
|
|
628
|
+
* that breaks it fails as `InternalError` naming the tool and the output
|
|
629
|
+
* contract — a server fault, never the caller's `ValidationError` (#480).
|
|
630
|
+
* {@link createToolHandler} and the `runToolContract` test helper both route
|
|
631
|
+
* through it.
|
|
632
|
+
*/
|
|
633
|
+
export function parseToolOutput(def, value) {
|
|
634
|
+
return parseOutputContract(def.output, value, {
|
|
635
|
+
kind: 'Tool',
|
|
636
|
+
name: def.name,
|
|
637
|
+
contract: 'output',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
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).
|
|
412
646
|
*/
|
|
413
|
-
function
|
|
414
|
-
const
|
|
415
|
-
return /[.?!]$/.test(
|
|
647
|
+
function terminateSentence(text) {
|
|
648
|
+
const trimmed = text.trim();
|
|
649
|
+
return /[.?!]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
416
650
|
}
|
|
417
651
|
/**
|
|
418
652
|
* The error envelope a tool can put on `structuredContent` when it fails,
|
|
@@ -439,7 +673,7 @@ function toolErrorEnvelopeSchema(def) {
|
|
|
439
673
|
? z
|
|
440
674
|
.string()
|
|
441
675
|
.describe(`Machine-readable failure mode. Declared by this tool: ${contract
|
|
442
|
-
.map((entry) => `\`${entry.reason}\`: ${
|
|
676
|
+
.map((entry) => `\`${entry.reason}\`: ${terminateSentence(entry.when)}`)
|
|
443
677
|
.join(' ')} Other values are possible when a failure originates below the handler.`)
|
|
444
678
|
.meta({ examples: reasons })
|
|
445
679
|
: z.string().describe('Machine-readable failure mode.');
|
|
@@ -624,7 +858,8 @@ function renderEnrichmentTrailer(store, trailer, parsed) {
|
|
|
624
858
|
* merged object — keeps enrichment out of the JSON blob and avoids double-render.
|
|
625
859
|
*
|
|
626
860
|
* A required enrichment field the handler never populated fails the parse here,
|
|
627
|
-
* surfacing the authoring bug as a loud
|
|
861
|
+
* surfacing the authoring bug as a loud `InternalError` naming the enrichment
|
|
862
|
+
* contract rather than dropping it silently (#480).
|
|
628
863
|
*/
|
|
629
864
|
export function buildToolSuccessResult(def, ctx, domainValidated, domainContent) {
|
|
630
865
|
if (!def.enrichment) {
|
|
@@ -632,10 +867,7 @@ export function buildToolSuccessResult(def, ctx, domainValidated, domainContent)
|
|
|
632
867
|
}
|
|
633
868
|
const store = readEnrichmentStore(ctx);
|
|
634
869
|
const values = store?.values ?? {};
|
|
635
|
-
const structuredContent = effectiveOutputSchema(def).
|
|
636
|
-
...domainValidated,
|
|
637
|
-
...values,
|
|
638
|
-
});
|
|
870
|
+
const structuredContent = parseOutputContract(effectiveOutputSchema(def), { ...domainValidated, ...values }, { kind: 'Tool', name: def.name, contract: 'enrichment' });
|
|
639
871
|
const trailer = store && Object.keys(values).length > 0
|
|
640
872
|
? renderEnrichmentTrailer(store, def.enrichmentTrailer, structuredContent)
|
|
641
873
|
: [];
|
|
@@ -679,7 +911,8 @@ function declaredSeverity(def, error) {
|
|
|
679
911
|
* - Validates input via Zod schema
|
|
680
912
|
* - Measures execution time
|
|
681
913
|
* - Formats response via `format` or JSON default
|
|
682
|
-
* - Catches errors and returns `isError: true
|
|
914
|
+
* - Catches errors and returns `isError: true`, counting one raised before the
|
|
915
|
+
* measured region (the scope check, argument validation) on `mcp.tool.rejections`
|
|
683
916
|
*/
|
|
684
917
|
export function createToolHandler(def, services, notifiers, inputGate) {
|
|
685
918
|
// The handler's return value carries no marker, so the arrays partial-success
|
|
@@ -692,6 +925,9 @@ export function createToolHandler(def, services, notifiers, inputGate) {
|
|
|
692
925
|
operation: 'HandleToolRequest',
|
|
693
926
|
additionalContext: { toolName: def.name },
|
|
694
927
|
});
|
|
928
|
+
// Set once the call reaches the measured region; a failure before it is a
|
|
929
|
+
// rejection the call and error counters never see (#546).
|
|
930
|
+
let measured = false;
|
|
695
931
|
try {
|
|
696
932
|
// Check inline auth scopes
|
|
697
933
|
if (def.auth && def.auth.length > 0) {
|
|
@@ -713,6 +949,7 @@ export function createToolHandler(def, services, notifiers, inputGate) {
|
|
|
713
949
|
// merge, and the trailer render all decide the client-visible outcome,
|
|
714
950
|
// so a failure in any of them is a failed call — closing the span when
|
|
715
951
|
// the handler returned recorded those as successes (#346).
|
|
952
|
+
measured = true;
|
|
716
953
|
return await measureToolExecution(async (spanContext, recordOutput) => {
|
|
717
954
|
const handlerCtx = buildHandlerContext(request, services, spanContext, def.errors, inputGate);
|
|
718
955
|
ctx = handlerCtx;
|
|
@@ -725,7 +962,7 @@ export function createToolHandler(def, services, notifiers, inputGate) {
|
|
|
725
962
|
recordOutput(handlerResult);
|
|
726
963
|
// Render content[] from the domain payload only (Resolution B), then
|
|
727
964
|
// merge enrichment into structuredContent and append the content[] trailer.
|
|
728
|
-
const validatedResult = def
|
|
965
|
+
const validatedResult = parseToolOutput(def, handlerResult);
|
|
729
966
|
return buildToolSuccessResult(def, handlerCtx, validatedResult, renderToolContent(def, validatedResult, handlerCtx));
|
|
730
967
|
}
|
|
731
968
|
catch (error) {
|
|
@@ -756,8 +993,39 @@ export function createToolHandler(def, services, notifiers, inputGate) {
|
|
|
756
993
|
context: appContext,
|
|
757
994
|
...(severity !== undefined && { severity }),
|
|
758
995
|
});
|
|
759
|
-
|
|
996
|
+
if (!measured)
|
|
997
|
+
recordToolRejection(def.name, error);
|
|
998
|
+
const result = classifyAndBuildToolErrorResult(error);
|
|
999
|
+
if (config.logToolFailurePayloads) {
|
|
1000
|
+
logFailurePayload(services.logger, def.name, appContext, input, result, severity);
|
|
1001
|
+
}
|
|
1002
|
+
return result;
|
|
760
1003
|
}
|
|
761
1004
|
};
|
|
762
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Writes the opt-in failed-call payload record (#291): the arguments as the
|
|
1008
|
+
* caller sent them — before pre-validation drops or renames a key — and the
|
|
1009
|
+
* `CallToolResult` the client receives, each redacted, serialized, and capped
|
|
1010
|
+
* on its own by `sanitization.serializeForLogging`.
|
|
1011
|
+
*
|
|
1012
|
+
* Logged at the level of the call's own error record and with the same request
|
|
1013
|
+
* context, so the two filter and correlate together. A cancellation writes
|
|
1014
|
+
* nothing: its error record is a routine `info` line, and the caller that would
|
|
1015
|
+
* have read the result is gone.
|
|
1016
|
+
*/
|
|
1017
|
+
function logFailurePayload(log, toolName, context, input, result, severity) {
|
|
1018
|
+
const { code } = result.structuredContent.error;
|
|
1019
|
+
if (code === JsonRpcErrorCode.RequestCancelled)
|
|
1020
|
+
return;
|
|
1021
|
+
const maxBytes = config.logToolFailurePayloadMaxBytes;
|
|
1022
|
+
const toolInput = sanitization.serializeForLogging(input, maxBytes);
|
|
1023
|
+
const toolResult = sanitization.serializeForLogging(result, maxBytes);
|
|
1024
|
+
log[severity ?? 'error'](`Tool failure payload: ${toolName}`, withExtra(context, {
|
|
1025
|
+
toolInput: toolInput.text,
|
|
1026
|
+
toolInputTruncated: toolInput.truncated,
|
|
1027
|
+
toolResult: toolResult.text,
|
|
1028
|
+
toolResultTruncated: toolResult.truncated,
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
763
1031
|
//# sourceMappingURL=toolHandlerFactory.js.map
|