* fix(sequentialthinking): restore nextThoughtNeeded in the advertised inputSchema required array
commit 1cdf806d (#3533) wrapped nextThoughtNeeded in a z.preprocess-based
coercedBoolean to fix a real footgun (string "false" coercing to true).
zod's toJSONSchema(..., { io: "input" }) treats a z.preprocess()-wrapped
field's input type as unknown, so it silently drops that field from the
emitted required array, even though it carries no .optional(). A client
that builds its call arguments from the advertised schema then omits
nextThoughtNeeded and gets a -32602 Invalid params from the runtime
validator, which still requires it.
Rebuild coercedBoolean as a transform on an explicit z.union([z.boolean(),
z.string()]) instead of z.preprocess. The union gives toJSONSchema a
concrete input type to report, so nextThoughtNeeded stays in required,
while parse behavior (including the case-insensitive string coercion
#3533 added) is unchanged.
Fixes #4651
* test(sequentialthinking): pin nextThoughtNeeded in required and string coercion
Runs against the built server so it checks the schema the SDK emits.
Skips when dist/ is absent, matching server-version.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Wo28CHPXyM3DHoLWvKnCK
---------
Co-authored-by: olaservo <olahungerford@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
|
|
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const distIndexPath = path.join(packageRoot, 'dist', 'index.js');
|
|
|
|
// Regression coverage for #4651: nextThoughtNeeded must stay in the advertised
|
|
// `required` array, and string coercion must keep accepting "True"/"FALSE"
|
|
// while rejecting anything else. Runs against the built server so it checks
|
|
// the schema the SDK actually emits, not the zod object.
|
|
describe.skipIf(!existsSync(distIndexPath))('sequentialthinking input schema', () => {
|
|
let client: Client;
|
|
|
|
beforeAll(async () => {
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [distIndexPath],
|
|
cwd: packageRoot,
|
|
stderr: 'pipe',
|
|
});
|
|
client = new Client({ name: 'input-schema-test', version: '0.0.0' });
|
|
await client.connect(transport);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await client?.close();
|
|
});
|
|
|
|
it('advertises nextThoughtNeeded as required', async () => {
|
|
const { tools } = await client.listTools();
|
|
const tool = tools.find(t => t.name === 'sequentialthinking');
|
|
expect(tool).toBeDefined();
|
|
expect(tool!.inputSchema.required).toEqual(
|
|
expect.arrayContaining(['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts'])
|
|
);
|
|
});
|
|
|
|
it('rejects a call that omits nextThoughtNeeded', async () => {
|
|
const result = await client.callTool({
|
|
name: 'sequentialthinking',
|
|
arguments: { thought: 't', thoughtNumber: 1, totalThoughts: 1 },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
expect(JSON.stringify(result.content)).toMatch(/nextThoughtNeeded/);
|
|
});
|
|
|
|
it.each(['True', 'FALSE', 'true', 'false'])('accepts the string %s', async (value) => {
|
|
const result = await client.callTool({
|
|
name: 'sequentialthinking',
|
|
arguments: { thought: 't', nextThoughtNeeded: value, thoughtNumber: 1, totalThoughts: 1 },
|
|
});
|
|
expect(result.isError).toBeFalsy();
|
|
});
|
|
|
|
it.each(['yes', '', '1'])('rejects the string %j', async (value) => {
|
|
const result = await client.callTool({
|
|
name: 'sequentialthinking',
|
|
arguments: { thought: 't', nextThoughtNeeded: value, thoughtNumber: 1, totalThoughts: 1 },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|