From de86f3e72cd6244615123f4202cc36c6f6bca839 Mon Sep 17 00:00:00 2001 From: Abhinav Prakash Date: Tue, 18 Aug 2026 01:08:29 +0530 Subject: [PATCH] fix(sequentialthinking): preserve required fields in schema and dynamic version resolution --- .../__tests__/index.test.ts | 175 ++++++++++++++++++ src/sequentialthinking/index.ts | 30 ++- 2 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 src/sequentialthinking/__tests__/index.test.ts diff --git a/src/sequentialthinking/__tests__/index.test.ts b/src/sequentialthinking/__tests__/index.test.ts new file mode 100644 index 0000000000..122304d313 --- /dev/null +++ b/src/sequentialthinking/__tests__/index.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('SequentialThinking MCP Server Integration', () => { + let client: Client; + let transport: StdioClientTransport; + + beforeEach(async () => { + const serverPath = path.resolve(__dirname, '../dist/index.js'); + transport = new StdioClientTransport({ + command: 'node', + args: [serverPath], + env: { + ...process.env, + DISABLE_THOUGHT_LOGGING: 'true', + }, + }); + + client = new Client( + { + name: 'test-client', + version: '1.0.0', + }, + { + capabilities: {}, + } + ); + + await client.connect(transport); + }); + + afterEach(async () => { + await client?.close(); + }); + + it('should match server version with package.json', async () => { + const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8') + ); + const serverVersion = client.getServerVersion(); + expect(serverVersion?.version).toBe(pkg.version); + expect(serverVersion?.name).toBe('sequential-thinking-server'); + }); + + it('should include nextThoughtNeeded in inputSchema.required', async () => { + const toolsResult = await client.listTools(); + const tool = toolsResult.tools.find((t) => t.name === 'sequentialthinking'); + expect(tool).toBeDefined(); + + const inputSchema = tool?.inputSchema as { + type: string; + required?: string[]; + properties?: Record; + }; + + expect(inputSchema.type).toBe('object'); + expect(inputSchema.required).toBeDefined(); + expect(inputSchema.required).toContain('thought'); + expect(inputSchema.required).toContain('nextThoughtNeeded'); + expect(inputSchema.required).toContain('thoughtNumber'); + expect(inputSchema.required).toContain('totalThoughts'); + + // Optional fields should NOT be in required + expect(inputSchema.required).not.toContain('isRevision'); + expect(inputSchema.required).not.toContain('revisesThought'); + expect(inputSchema.required).not.toContain('branchFromThought'); + expect(inputSchema.required).not.toContain('branchId'); + expect(inputSchema.required).not.toContain('needsMoreThoughts'); + }); + + it('should process thought with boolean nextThoughtNeeded (true and false)', async () => { + const resultTrue = await client.callTool({ + name: 'sequentialthinking', + arguments: { + thought: 'First step', + nextThoughtNeeded: true, + thoughtNumber: 1, + totalThoughts: 2, + }, + }); + expect(resultTrue.isError).toBeFalsy(); + const dataTrue = JSON.parse((resultTrue.content as Array<{ type: string; text: string }>)[0].text); + expect(dataTrue.nextThoughtNeeded).toBe(true); + + const resultFalse = await client.callTool({ + name: 'sequentialthinking', + arguments: { + thought: 'Second step', + nextThoughtNeeded: false, + thoughtNumber: 2, + totalThoughts: 2, + }, + }); + expect(resultFalse.isError).toBeFalsy(); + const dataFalse = JSON.parse((resultFalse.content as Array<{ type: string; text: string }>)[0].text); + expect(dataFalse.nextThoughtNeeded).toBe(false); + }); + + it('should coerce string "true" and "false" for nextThoughtNeeded and optional booleans', async () => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { + thought: 'Checking string booleans', + nextThoughtNeeded: 'false', + thoughtNumber: '1', + totalThoughts: '3', + isRevision: 'true', + revisesThought: '1', + needsMoreThoughts: 'FALSE', + }, + }); + + expect(result.isError).toBeFalsy(); + const data = JSON.parse((result.content as Array<{ type: string; text: string }>)[0].text); + expect(data.thoughtNumber).toBe(1); + expect(data.totalThoughts).toBe(3); + expect(data.nextThoughtNeeded).toBe(false); + }); + + it('should coerce uppercase and mixed-case strings for boolean fields', async () => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { + thought: 'Checking True and FALSE', + nextThoughtNeeded: 'True', + thoughtNumber: 1, + totalThoughts: 2, + isRevision: 'FALSE', + }, + }); + + expect(result.isError).toBeFalsy(); + const data = JSON.parse((result.content as Array<{ type: string; text: string }>)[0].text); + expect(data.nextThoughtNeeded).toBe(true); + }); + + it('should return error when required field nextThoughtNeeded is omitted', async () => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { + thought: 'Missing nextThoughtNeeded', + thoughtNumber: 1, + totalThoughts: 1, + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('-32602'); + expect(content[0].text).toContain('nextThoughtNeeded'); + }); + + it('should return error when required field thought is omitted', async () => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { + nextThoughtNeeded: true, + thoughtNumber: 1, + totalThoughts: 1, + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('-32602'); + expect(content[0].text).toContain('thought'); + }); +}); diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 217845bb3d..9b23f928cf 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -1,23 +1,35 @@ #!/usr/bin/env node +import { readFileSync, existsSync } from "fs"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; +function getPackageVersion(): string { + try { + const localPkg = new URL("./package.json", import.meta.url); + if (existsSync(localPkg)) { + return JSON.parse(readFileSync(localPkg, "utf-8")).version; + } + const parentPkg = new URL("../package.json", import.meta.url); + if (existsSync(parentPkg)) { + return JSON.parse(readFileSync(parentPkg, "utf-8")).version; + } + } catch {} + return "0.6.2"; +} + /** 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 coercedBoolean = z.union([ + z.boolean(), + z.enum(["true", "false"]).transform((v) => v === "true"), + z.string().transform((v) => v.toLowerCase() === "true"), +]); const server = new McpServer({ name: "sequential-thinking-server", - version: "0.2.0", + version: getPackageVersion(), }); const thinkingServer = new SequentialThinkingServer();