Skip to content

sequential-thinking: nextThoughtNeeded missing from inputSchema.required but required at runtime (regression from #3533) #4651

Description

@seodduu

Summary

sequentialthinking declares nextThoughtNeeded as an optional parameter in its advertised inputSchema, but runtime validation requires it. Any client that builds arguments from the declared schema omits the field and gets -32602.

This is a regression from #3533, which replaced z.boolean() with a z.preprocess()-based coercedBoolean.

Reproduction

Start the server and call tools/list:

"required": ["thought", "thoughtNumber", "totalThoughts"]

nextThoughtNeeded is absent, yet it is declared as a plain (non-optional) property:

"nextThoughtNeeded": {"description": "Whether another thought step is needed", "type": "boolean"}

Calling the tool with exactly the declared required fields fails:

tools/call sequentialthinking {"thought": "example", "thoughtNumber": 1, "totalThoughts": 1}

MCP error -32602: Input validation error: Invalid arguments for tool sequentialthinking:
Invalid input: expected boolean, received undefined at nextThoughtNeeded

Observed with @modelcontextprotocol/server-sequential-thinking@2026.7.4. The current main of src/sequentialthinking/index.ts has the same definition, so it is not fixed there either.

Root cause

z.preprocess() produces a schema whose input type accepts unknown, so the zod-to-JSON-Schema conversion treats it as optional and omits it from required. Minimal repro with zod 4.4.3:

const coercedBoolean = z.preprocess((v) => v, z.boolean());
const schema = z.object({
  req: z.boolean(),
  pre: coercedBoolean,
  coerced: z.coerce.number(),
  opt: z.boolean().optional(),
});
z.toJSONSchema(schema, { io: "input" }).required;
// => ["req", "coerced"]   <-- "pre" is missing

The same file already contains the contrast: thoughtNumber: z.coerce.number() stays in required, because z.coerce does not wrap the schema the way z.preprocess does. Only the three coercedBoolean fields are affected, and two of them (isRevision, needsMoreThoughts) are intentionally .optional(), so nextThoughtNeeded is the only one where the declaration and the validation disagree.

Before #3533:

-      nextThoughtNeeded: z.boolean().describe("Whether another thought step is needed"),
+      nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"),

Impact

The advertised schema is the only contract a client has. A client that respects it produces a call the server rejects, and the rejection points at a field the schema said was optional. Clients that happen to always send nextThoughtNeeded (most LLM callers, since the description prompts for it) never see this.

Suggested fix

Keep the string coercion but produce a schema that survives JSON Schema conversion, for example by unioning instead of preprocessing:

const coercedBoolean = z.union([
  z.boolean(),
  z.enum(["true", "false"]).transform((v) => v === "true"),
]);

Any approach that leaves nextThoughtNeeded in the emitted required list works. Happy to send a PR if you would like one.

How this was found

Found by an automated MCP server test-generation tool that derives cases from the declared inputSchema. The happy-path case it generated used exactly the declared required fields, which is why the mismatch surfaced immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions