diff --git a/src/sequentialthinking/__tests__/lib.test.ts b/src/sequentialthinking/__tests__/lib.test.ts index a97e41f5a0..2114c5ec18 100644 --- a/src/sequentialthinking/__tests__/lib.test.ts +++ b/src/sequentialthinking/__tests__/lib.test.ts @@ -22,107 +22,8 @@ describe('SequentialThinkingServer', () => { server = new SequentialThinkingServer(); }); - describe('processThought - validation', () => { - it('should reject input with missing thought', () => { - const input = { - thoughtNumber: 1, - totalThoughts: 3, - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid thought'); - }); - - it('should reject input with non-string thought', () => { - const input = { - thought: 123, - thoughtNumber: 1, - totalThoughts: 3, - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid thought'); - }); - - it('should reject input with missing thoughtNumber', () => { - const input = { - thought: 'Test thought', - totalThoughts: 3, - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid thoughtNumber'); - }); - - it('should reject input with non-number thoughtNumber', () => { - const input = { - thought: 'Test thought', - thoughtNumber: '1', - totalThoughts: 3, - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid thoughtNumber'); - }); - - it('should reject input with missing totalThoughts', () => { - const input = { - thought: 'Test thought', - thoughtNumber: 1, - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid totalThoughts'); - }); - - it('should reject input with non-number totalThoughts', () => { - const input = { - thought: 'Test thought', - thoughtNumber: 1, - totalThoughts: '3', - nextThoughtNeeded: true - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid totalThoughts'); - }); - - it('should reject input with missing nextThoughtNeeded', () => { - const input = { - thought: 'Test thought', - thoughtNumber: 1, - totalThoughts: 3 - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid nextThoughtNeeded'); - }); - - it('should reject input with non-boolean nextThoughtNeeded', () => { - const input = { - thought: 'Test thought', - thoughtNumber: 1, - totalThoughts: 3, - nextThoughtNeeded: 'true' - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid nextThoughtNeeded'); - }); - }); + // Note: Input validation tests removed - validation now happens at the tool + // registration layer via Zod schemas before processThought is called describe('processThought - valid inputs', () => { it('should accept valid basic thought', () => { @@ -275,19 +176,6 @@ describe('SequentialThinkingServer', () => { }); describe('processThought - edge cases', () => { - it('should reject empty thought string', () => { - const input = { - thought: '', - thoughtNumber: 1, - totalThoughts: 1, - nextThoughtNeeded: false - }; - - const result = server.processThought(input); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid thought'); - }); - it('should handle very long thought strings', () => { const input = { thought: 'a'.repeat(10000), @@ -349,25 +237,6 @@ describe('SequentialThinkingServer', () => { expect(result.content[0]).toHaveProperty('text'); }); - it('should return correct error structure on failure', () => { - const input = { - thought: 'Test', - thoughtNumber: 1, - totalThoughts: 1 - // missing nextThoughtNeeded - }; - - const result = server.processThought(input); - - expect(result).toHaveProperty('isError', true); - expect(result).toHaveProperty('content'); - expect(Array.isArray(result.content)).toBe(true); - - const errorData = JSON.parse(result.content[0].text); - expect(errorData).toHaveProperty('error'); - expect(errorData).toHaveProperty('status', 'failed'); - }); - it('should return valid JSON in response', () => { const input = { thought: 'Test thought', diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 4e9da63a79..44af5c0e9d 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -1,17 +1,22 @@ #!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, - Tool, -} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; -const SEQUENTIAL_THINKING_TOOL: Tool = { - name: "sequentialthinking", - description: `A detailed tool for dynamic and reflective problem-solving through thoughts. +const server = new McpServer({ + name: "sequential-thinking-server", + version: "0.2.0", +}); + +const thinkingServer = new SequentialThinkingServer(); + +server.registerTool( + "sequentialthinking", + { + title: "Sequential Thinking", + description: `A detailed tool for dynamic and reflective problem-solving through thoughts. This tool helps analyze problems through a flexible thinking process that can adapt and evolve. Each thought can build on, question, or revise previous insights as understanding deepens. @@ -37,13 +42,13 @@ Key features: Parameters explained: - thought: Your current thinking step, which can include: -* Regular analytical steps -* Revisions of previous thoughts -* Questions about previous decisions -* Realizations about needing more analysis -* Changes in approach -* Hypothesis generation -* Hypothesis verification + * Regular analytical steps + * Revisions of previous thoughts + * Questions about previous decisions + * Realizations about needing more analysis + * Changes in approach + * Hypothesis generation + * Hypothesis verification - nextThoughtNeeded: True if you need more thinking, even if at what seemed like the end - thoughtNumber: Current number in sequence (can go beyond initial total if needed) - totalThoughts: Current estimate of thoughts needed (can be adjusted up/down) @@ -65,85 +70,41 @@ You should: 9. Repeat the process until satisfied with the solution 10. Provide a single, ideally correct answer as the final output 11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`, - inputSchema: { - type: "object", - properties: { - thought: { - type: "string", - description: "Your current thinking step" - }, - nextThoughtNeeded: { - type: "boolean", - description: "Whether another thought step is needed" - }, - thoughtNumber: { - type: "integer", - description: "Current thought number (numeric value, e.g., 1, 2, 3)", - minimum: 1 - }, - totalThoughts: { - type: "integer", - description: "Estimated total thoughts needed (numeric value, e.g., 5, 10)", - minimum: 1 - }, - isRevision: { - type: "boolean", - description: "Whether this revises previous thinking" - }, - revisesThought: { - type: "integer", - description: "Which thought is being reconsidered", - minimum: 1 - }, - branchFromThought: { - type: "integer", - description: "Branching point thought number", - minimum: 1 - }, - branchId: { - type: "string", - description: "Branch identifier" - }, - needsMoreThoughts: { - type: "boolean", - description: "If more thoughts are needed" - } + 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"), + branchId: z.string().optional().describe("Branch identifier"), + needsMoreThoughts: z.boolean().optional().describe("If more thoughts are needed") }, - required: ["thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"] - } -}; - -const server = new Server( - { - name: "sequential-thinking-server", - version: "0.2.0", - }, - { - capabilities: { - tools: {}, + outputSchema: { + thoughtNumber: z.number(), + totalThoughts: z.number(), + nextThoughtNeeded: z.boolean(), + branches: z.array(z.string()), + thoughtHistoryLength: z.number() }, - } -); + }, + async (args) => { + const result = thinkingServer.processThought(args); -const thinkingServer = new SequentialThinkingServer(); + if (result.isError) { + return result; + } -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [SEQUENTIAL_THINKING_TOOL], -})); + // Parse the JSON response to get structured content + const parsedContent = JSON.parse(result.content[0].text); -server.setRequestHandler(CallToolRequestSchema, async (request) => { - if (request.params.name === "sequentialthinking") { - return thinkingServer.processThought(request.params.arguments); + return { + content: result.content, + structuredContent: parsedContent + }; } - - return { - content: [{ - type: "text", - text: `Unknown tool: ${request.params.name}` - }], - isError: true - }; -}); +); async function runServer() { const transport = new StdioServerTransport(); diff --git a/src/sequentialthinking/lib.ts b/src/sequentialthinking/lib.ts index c5ee9cad3c..31a1098644 100644 --- a/src/sequentialthinking/lib.ts +++ b/src/sequentialthinking/lib.ts @@ -21,35 +21,6 @@ export class SequentialThinkingServer { this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true"; } - private validateThoughtData(input: unknown): ThoughtData { - const data = input as Record; - - if (!data.thought || typeof data.thought !== 'string') { - throw new Error('Invalid thought: must be a string'); - } - if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') { - throw new Error('Invalid thoughtNumber: must be a number'); - } - if (!data.totalThoughts || typeof data.totalThoughts !== 'number') { - throw new Error('Invalid totalThoughts: must be a number'); - } - if (typeof data.nextThoughtNeeded !== 'boolean') { - throw new Error('Invalid nextThoughtNeeded: must be a boolean'); - } - - return { - thought: data.thought, - thoughtNumber: data.thoughtNumber, - totalThoughts: data.totalThoughts, - nextThoughtNeeded: data.nextThoughtNeeded, - isRevision: data.isRevision as boolean | undefined, - revisesThought: data.revisesThought as number | undefined, - branchFromThought: data.branchFromThought as number | undefined, - branchId: data.branchId as string | undefined, - needsMoreThoughts: data.needsMoreThoughts as boolean | undefined, - }; - } - private formatThought(thoughtData: ThoughtData): string { const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } = thoughtData; @@ -78,35 +49,35 @@ export class SequentialThinkingServer { └${border}┘`; } - public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } { + public processThought(input: ThoughtData): { content: Array<{ type: "text"; text: string }>; isError?: boolean } { try { - const validatedInput = this.validateThoughtData(input); - - if (validatedInput.thoughtNumber > validatedInput.totalThoughts) { - validatedInput.totalThoughts = validatedInput.thoughtNumber; + // Validation happens at the tool registration layer via Zod + // Adjust totalThoughts if thoughtNumber exceeds it + if (input.thoughtNumber > input.totalThoughts) { + input.totalThoughts = input.thoughtNumber; } - this.thoughtHistory.push(validatedInput); + this.thoughtHistory.push(input); - if (validatedInput.branchFromThought && validatedInput.branchId) { - if (!this.branches[validatedInput.branchId]) { - this.branches[validatedInput.branchId] = []; + if (input.branchFromThought && input.branchId) { + if (!this.branches[input.branchId]) { + this.branches[input.branchId] = []; } - this.branches[validatedInput.branchId].push(validatedInput); + this.branches[input.branchId].push(input); } if (!this.disableThoughtLogging) { - const formattedThought = this.formatThought(validatedInput); + const formattedThought = this.formatThought(input); console.error(formattedThought); } return { content: [{ - type: "text", + type: "text" as const, text: JSON.stringify({ - thoughtNumber: validatedInput.thoughtNumber, - totalThoughts: validatedInput.totalThoughts, - nextThoughtNeeded: validatedInput.nextThoughtNeeded, + thoughtNumber: input.thoughtNumber, + totalThoughts: input.totalThoughts, + nextThoughtNeeded: input.nextThoughtNeeded, branches: Object.keys(this.branches), thoughtHistoryLength: this.thoughtHistory.length }, null, 2) @@ -115,7 +86,7 @@ export class SequentialThinkingServer { } catch (error) { return { content: [{ - type: "text", + type: "text" as const, text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), status: 'failed' diff --git a/src/sequentialthinking/tsconfig.json b/src/sequentialthinking/tsconfig.json index 2ce5843e0b..d2d86555b0 100644 --- a/src/sequentialthinking/tsconfig.json +++ b/src/sequentialthinking/tsconfig.json @@ -2,9 +2,13 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": ".", - "moduleResolution": "NodeNext", - "module": "NodeNext" + "rootDir": "." }, - "include": ["./**/*.ts"] + "include": [ + "./**/*.ts" + ], + "exclude": [ + "**/*.test.ts", + "vitest.config.ts" + ] }