From d5669c47b60b6686977e00e25da039fa1fe0b46e Mon Sep 17 00:00:00 2001 From: hngpt52 <131925875+hngpt52@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:17:41 +0100 Subject: [PATCH] fix(server): check tool callback output schemas Closes #2754. Agent: codex-1 --- .changeset/typed-tool-output.md | 5 + docs/servers/tools.md | 6 +- packages/server/package.json | 3 + packages/server/src/server/mcp.ts | 65 +++++++-- .../server/test/server/mcp.compat.test.ts | 136 +++++++++++++++-- pnpm-lock.yaml | 9 ++ scripts/smoke-dist-types.mjs | 137 +++++++++++------- test/e2e/scenarios/standard-schema.test.ts | 4 +- test/e2e/scenarios/tools.test.ts | 13 +- test/integration/test/server/mcp.test.ts | 58 ++++---- 10 files changed, 321 insertions(+), 115 deletions(-) create mode 100644 .changeset/typed-tool-output.md diff --git a/.changeset/typed-tool-output.md b/.changeset/typed-tool-output.md new file mode 100644 index 0000000000..76165d27b8 --- /dev/null +++ b/.changeset/typed-tool-output.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Check `registerTool` callback results against the inferred `outputSchema` type. Successful callbacks with an output schema must return matching, non-undefined `structuredContent`; error and input-required results remain supported. This catches incompatible output at compile time for Standard Schema providers and deprecated raw Zod shapes, while retaining runtime validation. Previously accepted callbacks that omit structured output or return the wrong type now produce a TypeScript error. diff --git a/docs/servers/tools.md b/docs/servers/tools.md index aff0fc430f..c4b96a9b3b 100644 --- a/docs/servers/tools.md +++ b/docs/servers/tools.md @@ -116,7 +116,11 @@ server.registerTool( ); ``` -The SDK validates `structuredContent` against `outputSchema` before the result leaves your server, and advertises the derived JSON Schema in `tools/list` so clients can validate it too. +When you supply an `outputSchema` with an inferred output type, TypeScript checks that successful callbacks return matching `structuredContent`. In this example, a string, a missing `price`, or a non-numeric `price` produces a compiler error. Successful output cannot be `undefined`; `null` is valid when the schema permits it. Results with `isError: true` and [input-required results](input-required.md) do not need matching structured output. + +The callback returns the schema's output type, including when coercion or defaults make its input and output types differ. Output validation does not replace the returned value with parsed or transformed data. + +The SDK also validates `structuredContent` against `outputSchema` before the result leaves your server, and advertises the derived JSON Schema in `tools/list` so clients can validate it too. Runtime validation remains necessary for JavaScript callers and constraints TypeScript cannot check, such as numeric ranges. Calling `product-details` with `{ name: 'Travel mug' }` returns both renderings: diff --git a/packages/server/package.json b/packages/server/package.json index f481e019e7..ceecdc78a1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -148,6 +148,8 @@ "@modelcontextprotocol/vitest-config": "workspace:^", "@types/eventsource": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", + "@valibot/to-json-schema": "catalog:devTools", + "arktype": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", @@ -156,6 +158,7 @@ "tsdown": "catalog:devTools", "typescript": "catalog:devTools", "typescript-eslint": "catalog:devTools", + "valibot": "catalog:devTools", "vitest": "catalog:devTools" } } diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index d2e40181e4..d9a464ea3f 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -927,6 +927,10 @@ export class McpServer { /** * Registers a tool with a config object and callback. * + * When `outputSchema` is supplied, successful callbacks must return defined + * `structuredContent` matching the schema's inferred output type. Error and + * input-required results are exempt; runtime validation still applies. + * * @example * ```ts source="./mcp.examples.ts#McpServer_registerTool_basic" * server.registerTool( @@ -950,7 +954,10 @@ export class McpServer { * ); * ``` */ - registerTool( + registerTool< + OutputArgs extends StandardSchemaWithJSON | undefined = undefined, + InputArgs extends StandardSchemaWithJSON | undefined = undefined + >( name: string, config: { title?: string; @@ -961,7 +968,7 @@ export class McpServer { icons?: Icon[]; _meta?: Record; }, - cb: ToolCallback + cb: ToolCallback> ): RegisteredTool; /** @deprecated Wrap with `z.object({...})` instead. Raw-shape form: `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record; it is auto-wrapped with `z.object()`. */ registerTool( @@ -975,7 +982,7 @@ export class McpServer { icons?: Icon[]; _meta?: Record; }, - cb: LegacyToolCallback + cb: LegacyToolCallback> ): RegisteredTool; registerTool( name: string, @@ -1218,13 +1225,32 @@ export type ZodRawShape = Record; /** Infers the parsed-output type of a {@linkcode ZodRawShape}. */ export type InferRawShape = z.infer>; -/** {@linkcode ToolCallback} variant used when `inputSchema` is a {@linkcode ZodRawShape}. */ -export type LegacyToolCallback = Args extends ZodRawShape - ? ( - args: InferRawShape, - ctx: ServerContext - ) => CallToolResult | InputRequiredResult | Promise - : (ctx: ServerContext) => CallToolResult | InputRequiredResult | Promise; +/** + * {@linkcode ToolCallback} variant used when `inputSchema` is a {@linkcode ZodRawShape}. + * When an output schema is supplied, successful results must include matching + * `structuredContent`; error and input-required results remain available. + */ +export type LegacyToolCallback< + Args extends ZodRawShape | undefined, + OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = undefined +> = BaseToolCallback< + [OutputArgs] extends [ZodRawShape | StandardSchemaWithJSON] + ? + | (CallToolResult & { + structuredContent: (OutputArgs extends ZodRawShape + ? InferRawShape + : OutputArgs extends StandardSchemaWithJSON + ? StandardSchemaWithJSON.InferOutput + : never) & + (NonNullable | null); + isError?: false; + }) + | (CallToolResult & { isError: true }) + | InputRequiredResult + : CallToolResult | InputRequiredResult, + ServerContext, + Args extends ZodRawShape ? z.ZodObject : undefined +>; /** {@linkcode PromptCallback} variant used when `argsSchema` is a {@linkcode ZodRawShape}. */ export type LegacyPromptCallback = Args extends ZodRawShape @@ -1244,9 +1270,24 @@ export type BaseToolCallback< /** * Callback for a tool handler registered with {@linkcode McpServer.registerTool}. + * The second type parameter links a supplied output schema to successful + * `structuredContent` while preserving the broad result type when omitted. + * The defined-value intersection excludes `undefined` even for an `unknown` + * schema output, while preserving `null` (`Exclude` would not). */ -export type ToolCallback = BaseToolCallback< - CallToolResult | InputRequiredResult, +export type ToolCallback< + Args extends StandardSchemaWithJSON | undefined = undefined, + OutputArgs extends StandardSchemaWithJSON | undefined = undefined +> = BaseToolCallback< + [OutputArgs] extends [StandardSchemaWithJSON] + ? + | (CallToolResult & { + structuredContent: StandardSchemaWithJSON.InferOutput & (NonNullable | null); + isError?: false; + }) + | (CallToolResult & { isError: true }) + | InputRequiredResult + : CallToolResult | InputRequiredResult, ServerContext, Args >; diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index ae7a0438b5..58e6d04849 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -1,9 +1,12 @@ import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core-internal'; +import { toStandardJsonSchema } from '@valibot/to-json-schema'; +import { type } from 'arktype'; +import * as v from 'valibot'; import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import * as z from 'zod/v4'; -import { McpServer } from '../../src/index'; -import type { InferRawShape } from '../../src/server/mcp'; +import { inputRequired, McpServer } from '../../src/index'; +import type { InferRawShape, ToolCallback } from '../../src/server/mcp'; import { completable } from '../../src/server/completable'; describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () => { @@ -128,22 +131,127 @@ describe('InferRawShape', () => { }); }); -describe('SEP-2106: registerTool with non-object outputSchema (type-level)', () => { - it('accepts z.array(z.number()) as outputSchema and a number[] structuredContent compiles', () => { +describe('registerTool output schema callback typing', () => { + it('requires successful structuredContent to match object output schemas', () => { const server = new McpServer({ name: 's', version: '1' }); - server.registerTool('arr', { inputSchema: z.object({ n: z.number() }), outputSchema: z.array(z.number()) }, async ({ n }) => ({ + + const outputSchema = z.object({ data: z.string(), count: z.number() }); + server.registerTool('object-valid', { outputSchema }, () => ({ + content: [], + structuredContent: { data: 'ok', count: 1 } + })); + + // @ts-expect-error structuredContent must match outputSchema + server.registerTool('object-wrong-root', { outputSchema }, async () => ({ content: [], structuredContent: 'wrong' })); + // @ts-expect-error structuredContent must include every required output field + server.registerTool('object-missing-field', { outputSchema }, () => ({ content: [], structuredContent: { data: 'missing' } })); + // prettier-ignore + // @ts-expect-error structuredContent field types must match outputSchema + server.registerTool('object-wrong-field', { outputSchema }, () => ({ content: [], structuredContent: { data: 'wrong', count: 'one' } })); + // @ts-expect-error successful callbacks with outputSchema must return structuredContent + server.registerTool('object-missing-output', { outputSchema }, () => ({ content: [] })); + // @ts-expect-error undefined is treated as absent by runtime output validation + server.registerTool('undefined-output', { outputSchema: z.undefined() }, () => ({ content: [], structuredContent: undefined })); + }); + + it('supports every non-object output root without widening invalid results', () => { + const server = new McpServer({ name: 's', version: '1' }); + + server.registerTool('array-valid', { outputSchema: z.array(z.number()) }, () => ({ content: [], structuredContent: [1, 2] })); + // @ts-expect-error array output schema rejects a string + server.registerTool('array-invalid', { outputSchema: z.array(z.number()) }, () => ({ content: [], structuredContent: 'wrong' })); + + server.registerTool('primitive-valid', { outputSchema: z.string() }, async () => ({ content: [], structuredContent: 'ok' })); + // @ts-expect-error primitive output schema rejects a number + server.registerTool('primitive-invalid', { outputSchema: z.string() }, async () => ({ content: [], structuredContent: 1 })); + + const unionOutput = z.union([z.string(), z.number()]); + server.registerTool('union-valid', { outputSchema: unionOutput }, () => ({ content: [], structuredContent: 1 })); + // @ts-expect-error union output schema rejects values outside the union + server.registerTool('union-invalid', { outputSchema: unionOutput }, () => ({ content: [], structuredContent: false })); + + server.registerTool('null-valid', { outputSchema: z.null() }, () => ({ content: [], structuredContent: null })); + // @ts-expect-error null output schema rejects non-null values + server.registerTool('null-invalid', { outputSchema: z.null() }, () => ({ content: [], structuredContent: 'not-null' })); + + server.registerTool('unknown-valid', { outputSchema: z.unknown() }, () => ({ content: [], structuredContent: null })); + // @ts-expect-error runtime treats undefined structuredContent as absent even when the schema output is unknown + server.registerTool('unknown-undefined', { outputSchema: z.unknown() }, () => ({ content: [], structuredContent: undefined })); + }); + + it('preserves input inference and exceptional result branches', () => { + const server = new McpServer({ name: 's', version: '1' }); + const inputSchema = z.object({ succeed: z.boolean() }); + const outputSchema = z.object({ data: z.string() }); + + server.registerTool('mixed-valid', { inputSchema, outputSchema }, async ({ succeed }) => { + expectTypeOf(succeed).toEqualTypeOf(); + return succeed ? { content: [], structuredContent: { data: 'ok' } } : { content: [], isError: true }; + }); + server.registerTool('input-required', { outputSchema }, () => inputRequired({ requestState: 'opaque' })); + + // @ts-expect-error an invalid success branch cannot hide beside a valid error branch + server.registerTool('mixed-invalid', { inputSchema, outputSchema }, ({ succeed }) => + succeed ? { content: [], structuredContent: { data: 1 } } : { content: [], isError: true } + ); + }); + + it('retains broad callbacks when outputSchema is absent or optional', () => { + const server = new McpServer({ name: 's', version: '1' }); + const inputSchema = z.object({ value: z.string() }); + const callback: ToolCallback = ({ value }) => ({ content: [], structuredContent: value.length }); + + server.registerTool('no-output-schema', { inputSchema }, callback); + + const maybeOutputSchema = (enabled: boolean): typeof inputSchema | undefined => (enabled ? inputSchema : undefined); + server.registerTool('optional-output-schema', { outputSchema: maybeOutputSchema(false) }, () => ({ + content: [], + structuredContent: { value: 'ok' } + })); + }); + + it('uses schema output types for coercing schemas', () => { + const server = new McpServer({ name: 's', version: '1' }); + const outputSchema = z.coerce.number(); + + server.registerTool('coerce-valid', { outputSchema }, () => ({ content: [], structuredContent: 1 })); + // @ts-expect-error callbacks return the schema output type, not its broader input type + server.registerTool('coerce-invalid', { outputSchema }, () => ({ content: [], structuredContent: '1' })); + }); + + it('checks deprecated raw output shapes and preserves raw input inference', () => { + const server = new McpServer({ name: 's', version: '1' }); + const inputSchema = { n: z.number() }; + const outputSchema = { result: z.string() }; + + server.registerTool('raw-valid', { inputSchema, outputSchema }, ({ n }) => { + expectTypeOf(n).toEqualTypeOf(); + return { content: [], structuredContent: { result: String(n) } }; + }); + // prettier-ignore + // @ts-expect-error raw output shape is inferred as its object output type + server.registerTool('raw-invalid', { inputSchema, outputSchema }, ({ n }) => ({ content: [], structuredContent: { result: n } })); + }); + + it('checks ArkType and Valibot output schemas in the server typecheck target', () => { + const server = new McpServer({ name: 's', version: '1' }); + + const arkOutput = type({ result: 'string' }); + server.registerTool('ark-output-valid', { outputSchema: arkOutput }, () => ({ content: [], - structuredContent: [n, n + 1] satisfies number[] + structuredContent: { result: 'ok' } })); - // NOTE (SEP-2106 PR-B verification item): the OutputArgs generic on registerTool is - // captured but does NOT currently flow into the callback's return type — ToolCallback's - // SendResultT is `CallToolResult | InputRequiredResult` (structuredContent: unknown), so - // a wrong-typed structuredContent ALSO compiles. Runtime validation (validateToolOutput) - // is the guard. Tightening the generic is out of this commit's scope. - server.registerTool('arr-loose', { outputSchema: z.array(z.number()) }, async () => ({ + // prettier-ignore + // @ts-expect-error ArkType output schema rejects the wrong field type + server.registerTool('ark-output-invalid', { outputSchema: arkOutput }, () => ({ content: [], structuredContent: { result: 1 } })); + + const valibotOutput = toStandardJsonSchema(v.object({ result: v.string() })); + server.registerTool('valibot-output-valid', { outputSchema: valibotOutput }, () => ({ content: [], - structuredContent: 'not-an-array' // compiles: structuredContent is `unknown` + structuredContent: { result: 'ok' } })); - expectTypeOf().toMatchTypeOf>>>(); + // prettier-ignore + // @ts-expect-error Valibot output schema rejects a missing required field + server.registerTool('valibot-output-invalid', { outputSchema: valibotOutput }, () => ({ content: [], structuredContent: {} })); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c663ad7086..112dba33f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1818,12 +1818,18 @@ importers: '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260327.2 + '@valibot/to-json-schema': + specifier: catalog:devTools + version: 1.6.0(valibot@1.3.1(typescript@5.9.3)) ajv: specifier: catalog:runtimeShared version: 8.18.0 ajv-formats: specifier: catalog:runtimeShared version: 3.0.1(ajv@8.18.0) + arktype: + specifier: catalog:devTools + version: 2.2.0 eslint: specifier: catalog:devTools version: 9.39.4 @@ -1848,6 +1854,9 @@ importers: typescript-eslint: specifier: catalog:devTools version: 8.57.2(eslint@9.39.4)(typescript@5.9.3) + valibot: + specifier: catalog:devTools + version: 1.3.1(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) diff --git a/scripts/smoke-dist-types.mjs b/scripts/smoke-dist-types.mjs index 7effbb92cd..3e7a4f3414 100644 --- a/scripts/smoke-dist-types.mjs +++ b/scripts/smoke-dist-types.mjs @@ -9,62 +9,91 @@ import path from 'node:path'; const repo = path.resolve(import.meta.dirname, '..'); const dir = mkdtempSync(path.join(tmpdir(), 'dist-types-smoke-')); +const serverConsumerLines = [ + "import { McpServer } from '@modelcontextprotocol/server';", + "import type { StandardSchemaWithJSON } from '@modelcontextprotocol/server';", + "export const s = new McpServer({ name: 'smoke', version: '1.0.0' });", + 'declare const outputSchema: StandardSchemaWithJSON;', + "s.registerTool('typed-output', { outputSchema }, async () => ({ content: [], structuredContent: { answer: 'ok' } }));", + '// @ts-expect-error built declarations must reject output that does not match the schema', + "s.registerTool('invalid-output', { outputSchema }, async () => ({ content: [], structuredContent: { answer: 1 } }));", + 'declare const unknownOutputSchema: StandardSchemaWithJSON;', + "s.registerTool('unknown-null-output', { outputSchema: unknownOutputSchema }, async () => ({ content: [], structuredContent: null }));", + '// @ts-expect-error runtime treats undefined structuredContent as absent even when schema output is unknown', + "s.registerTool('unknown-undefined-output', { outputSchema: unknownOutputSchema }, async () => ({ content: [], structuredContent: undefined }));" +]; +const esmConsumerSource = [ + "import { Client } from '@modelcontextprotocol/client';", + "import type { JsonSchemaType as ClientSchema } from '@modelcontextprotocol/client';", + "import { AjvJsonSchemaValidator } from '@modelcontextprotocol/client/validators/ajv';", + "import { CfWorkerJsonSchemaValidator as ClientCf } from '@modelcontextprotocol/client/validators/cf-worker';", + "import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';", + "import { AjvJsonSchemaValidator as ServerAjv } from '@modelcontextprotocol/server/validators/ajv';", + "import { CfWorkerJsonSchemaValidator as ServerCf } from '@modelcontextprotocol/server/validators/cf-worker';", + "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", + "export const c = new Client({ name: 'smoke', version: '1.0.0' });", + ...serverConsumerLines, + 'export type T = ClientSchema;', + 'export { AjvJsonSchemaValidator, ServerAjv, ClientCf, ServerCf, StdioClientTransport, StdioServerTransport };', + '' +].join('\n'); + +const declarationPaths = extension => ({ + '@modelcontextprotocol/client': [path.join(repo, `packages/client/dist/index${extension}`)], + '@modelcontextprotocol/client/validators/ajv': [path.join(repo, `packages/client/dist/validators/ajv${extension}`)], + '@modelcontextprotocol/client/validators/cf-worker': [path.join(repo, `packages/client/dist/validators/cfWorker${extension}`)], + '@modelcontextprotocol/client/stdio': [path.join(repo, `packages/client/dist/stdio${extension}`)], + '@modelcontextprotocol/server': [path.join(repo, `packages/server/dist/index${extension}`)], + '@modelcontextprotocol/server/validators/ajv': [path.join(repo, `packages/server/dist/validators/ajv${extension}`)], + '@modelcontextprotocol/server/validators/cf-worker': [path.join(repo, `packages/server/dist/validators/cfWorker${extension}`)], + '@modelcontextprotocol/server/stdio': [path.join(repo, `packages/server/dist/stdio${extension}`)] +}); + try { - writeFileSync( - path.join(dir, 'consumer.ts'), - [ - "import { Client } from '@modelcontextprotocol/client';", - "import type { JsonSchemaType as ClientSchema } from '@modelcontextprotocol/client';", - "import { AjvJsonSchemaValidator } from '@modelcontextprotocol/client/validators/ajv';", - "import { CfWorkerJsonSchemaValidator as ClientCf } from '@modelcontextprotocol/client/validators/cf-worker';", - "import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';", - "import { McpServer } from '@modelcontextprotocol/server';", - "import { AjvJsonSchemaValidator as ServerAjv } from '@modelcontextprotocol/server/validators/ajv';", - "import { CfWorkerJsonSchemaValidator as ServerCf } from '@modelcontextprotocol/server/validators/cf-worker';", - "import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';", - "export const c = new Client({ name: 'smoke', version: '1.0.0' });", - "export const s = new McpServer({ name: 'smoke', version: '1.0.0' });", - 'export type T = ClientSchema;', - 'export { AjvJsonSchemaValidator, ServerAjv, ClientCf, ServerCf, StdioClientTransport, StdioServerTransport };', - '' - ].join('\n') - ); - writeFileSync( - path.join(dir, 'tsconfig.json'), - JSON.stringify( - { - compilerOptions: { - strict: true, - noEmit: true, - skipLibCheck: false, - module: 'esnext', - moduleResolution: 'bundler', - target: 'es2022', - types: ['node'], - typeRoots: [path.join(repo, 'node_modules', '@types')], - paths: { - '@modelcontextprotocol/client': [path.join(repo, 'packages/client/dist/index.d.mts')], - '@modelcontextprotocol/client/validators/ajv': [path.join(repo, 'packages/client/dist/validators/ajv.d.mts')], - '@modelcontextprotocol/client/validators/cf-worker': [ - path.join(repo, 'packages/client/dist/validators/cfWorker.d.mts') - ], - '@modelcontextprotocol/client/stdio': [path.join(repo, 'packages/client/dist/stdio.d.mts')], - '@modelcontextprotocol/server': [path.join(repo, 'packages/server/dist/index.d.mts')], - '@modelcontextprotocol/server/validators/ajv': [path.join(repo, 'packages/server/dist/validators/ajv.d.mts')], - '@modelcontextprotocol/server/validators/cf-worker': [ - path.join(repo, 'packages/server/dist/validators/cfWorker.d.mts') - ], - '@modelcontextprotocol/server/stdio': [path.join(repo, 'packages/server/dist/stdio.d.mts')] - } + for (const format of [ + { + name: 'esm', + source: 'consumer.mts', + consumerSource: esmConsumerSource, + extension: '.d.mts', + module: 'esnext', + moduleResolution: 'bundler' + }, + { + name: 'cjs', + source: 'consumer.cts', + consumerSource: [...serverConsumerLines, ''].join('\n'), + extension: '.d.cts', + module: 'node16', + moduleResolution: 'node16' + } + ]) { + writeFileSync(path.join(dir, format.source), format.consumerSource); + const configPath = path.join(dir, `tsconfig.${format.name}.json`); + writeFileSync( + configPath, + JSON.stringify( + { + compilerOptions: { + strict: true, + noEmit: true, + skipLibCheck: false, + module: format.module, + moduleResolution: format.moduleResolution, + target: 'es2022', + types: ['node'], + typeRoots: [path.join(repo, 'node_modules', '@types')], + paths: declarationPaths(format.extension) + }, + include: [format.source] }, - include: ['consumer.ts'] - }, - null, - 2 - ) - ); - execFileSync('pnpm', ['exec', 'tsc', '-p', dir], { cwd: repo, stdio: 'inherit' }); - console.log('dist-types smoke: clean (skipLibCheck: false)'); + null, + 2 + ) + ); + execFileSync('pnpm', ['exec', 'tsc', '-p', configPath], { cwd: repo, stdio: 'inherit' }); + } + console.log('dist-types smoke: ESM and CJS clean (skipLibCheck: false)'); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/test/e2e/scenarios/standard-schema.test.ts b/test/e2e/scenarios/standard-schema.test.ts index a8f3406577..6e743d6923 100644 --- a/test/e2e/scenarios/standard-schema.test.ts +++ b/test/e2e/scenarios/standard-schema.test.ts @@ -138,8 +138,8 @@ verifies('standardschema:tool:output-schema-validation', async ({ transport }: T s.registerTool( 'get-server-status-corrupt', { inputSchema: type({}), outputSchema }, - // intentionally nonconforming structuredContent (server-side output validation must reject it) - () => ({ structuredContent: { healthy: 'definitely', uptimeSeconds: 'a while' }, content: [] }) + // Deliberately malformed runtime fixture: compile-time callers are rejected. + () => ({ structuredContent: { healthy: 'definitely', uptimeSeconds: 'a while' }, content: [] }) as never ); return s; }; diff --git a/test/e2e/scenarios/tools.test.ts b/test/e2e/scenarios/tools.test.ts index 741665143f..9b7a83f86e 100644 --- a/test/e2e/scenarios/tools.test.ts +++ b/test/e2e/scenarios/tools.test.ts @@ -91,12 +91,15 @@ function schemaServer(): McpServer { s.registerTool( 'structured-mismatch', { inputSchema: z.object({}), outputSchema: z.object({ value: z.number() }) }, - // intentionally invalid structuredContent (tests server-side validation rejects it) - () => ({ structuredContent: { value: 'not-a-number' }, content: [] }) + // Deliberately malformed runtime fixture: compile-time callers are rejected. + () => ({ structuredContent: { value: 'not-a-number' }, content: [] }) as never + ); + s.registerTool( + 'structured-missing', + { inputSchema: z.object({}), outputSchema: z.object({ value: z.number() }) }, + // Deliberately malformed runtime fixture: compile-time callers are rejected. + () => ({ content: [{ type: 'text', text: 'handler-body-no-structured' }] }) as never ); - s.registerTool('structured-missing', { inputSchema: z.object({}), outputSchema: z.object({ value: z.number() }) }, () => ({ - content: [{ type: 'text', text: 'handler-body-no-structured' }] - })); s.registerTool('structured-error-skip', { inputSchema: z.object({}), outputSchema: z.object({ value: z.number() }) }, () => ({ isError: true, content: [{ type: 'text', text: 'handler-returned-isError' }] diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 4b9a3865f0..6c0bd96728 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1386,15 +1386,17 @@ describe('Zod v4', () => { resultType: z.string() }) }, - async ({ input }) => ({ - // Only return content without structuredContent - content: [ - { - type: 'text', - text: `Processed: ${input}` - } - ] - }) + // Deliberately malformed runtime fixture: compile-time callers are rejected. + async ({ input }) => + ({ + // Only return content without structuredContent + content: [ + { + type: 'text', + text: `Processed: ${input}` + } + ] + }) as never ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -1508,25 +1510,27 @@ describe('Zod v4', () => { timestamp: z.string() }) }, - async ({ input }) => ({ - content: [ - { - type: 'text', - text: JSON.stringify({ - processedInput: input, - resultType: 'structured', - // Missing required 'timestamp' field - someExtraField: 'unexpected' // Extra field not in schema - }) + // Deliberately malformed runtime fixture: compile-time callers are rejected. + async ({ input }) => + ({ + content: [ + { + type: 'text', + text: JSON.stringify({ + processedInput: input, + resultType: 'structured', + // Missing required 'timestamp' field + someExtraField: 'unexpected' // Extra field not in schema + }) + } + ], + structuredContent: { + processedInput: input, + resultType: 'structured', + // Missing required 'timestamp' field + someExtraField: 'unexpected' // Extra field not in schema } - ], - structuredContent: { - processedInput: input, - resultType: 'structured', - // Missing required 'timestamp' field - someExtraField: 'unexpected' // Extra field not in schema - } - }) + }) as never ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();