From 1cdf806d21c3afdeb203c04705e8892d0f9d832f Mon Sep 17 00:00:00 2001 From: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:09:04 +0200 Subject: [PATCH] fix(sequential-thinking): use z.coerce for number and boolean params (#3533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(sequential-thinking): use z.coerce for number and safe preprocess for boolean params Uses z.coerce.number() for number fields and a z.preprocess() helper for boolean fields to handle string-typed parameters from LLM clients. The preprocess approach correctly handles "false" → false, avoiding the z.coerce.boolean() footgun where Boolean("false") === true. Fixes #3428 --- src/sequentialthinking/index.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index a3c34cca..217845bb 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -5,6 +5,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; +/** Safe boolean coercion that correctly handles string "false" */ +const coercedBoolean = z.preprocess((val) => { + if (typeof val === "boolean") return val; + if (typeof val === "string") { + if (val.toLowerCase() === "true") return true; + if (val.toLowerCase() === "false") return false; + } + return val; +}, z.boolean()); + const server = new McpServer({ name: "sequential-thinking-server", version: "0.2.0", @@ -72,14 +82,14 @@ You should: 11. Only set nextThoughtNeeded to false when truly done and a satisfactory answer is reached`, inputSchema: { thought: z.string().describe("Your current thinking step"), - nextThoughtNeeded: z.boolean().describe("Whether another thought step is needed"), - thoughtNumber: z.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), - totalThoughts: z.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), - isRevision: z.boolean().optional().describe("Whether this revises previous thinking"), - revisesThought: z.number().int().min(1).optional().describe("Which thought is being reconsidered"), - branchFromThought: z.number().int().min(1).optional().describe("Branching point thought number"), + nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"), + thoughtNumber: z.coerce.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), + totalThoughts: z.coerce.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), + isRevision: coercedBoolean.optional().describe("Whether this revises previous thinking"), + revisesThought: z.coerce.number().int().min(1).optional().describe("Which thought is being reconsidered"), + branchFromThought: z.coerce.number().int().min(1).optional().describe("Branching point thought number"), branchId: z.string().optional().describe("Branch identifier"), - needsMoreThoughts: z.boolean().optional().describe("If more thoughts are needed") + needsMoreThoughts: coercedBoolean.optional().describe("If more thoughts are needed") }, annotations: { readOnlyHint: true,