Added sequential thinking module

This commit is contained in:
Schrody
2026-02-12 17:43:43 +01:00
parent 561d858a7f
commit e1a0d82864
8 changed files with 817 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
FROM node:22.12-alpine AS builder
COPY . /app
COPY tsconfig.json /tsconfig.json
WORKDIR /app
RUN npm install
RUN npm ci --ignore-scripts --omit-dev
FROM node:22-alpine AS release
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
WORKDIR /app
RUN npm ci --ignore-scripts --omit-dev
ENTRYPOINT ["node", "dist/index.js"]

View File

@@ -0,0 +1,155 @@
# Sequential Thinking MCP Server
An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.
## Features
- Break down complex problems into manageable steps
- Revise and refine thoughts as understanding deepens
- Branch into alternative paths of reasoning
- Adjust the total number of thoughts dynamically
- Generate and verify solution hypotheses
## Tool
### sequential_thinking
Facilitates a detailed, step-by-step thinking process for problem-solving and analysis.
**Inputs:**
- `thought` (string): The current thinking step
- `nextThoughtNeeded` (boolean): Whether another thought step is needed
- `thoughtNumber` (integer): Current thought number
- `totalThoughts` (integer): Estimated total thoughts needed
- `isRevision` (boolean, optional): Whether this revises previous thinking
- `revisesThought` (integer, optional): Which thought is being reconsidered
- `branchFromThought` (integer, optional): Branching point thought number
- `branchId` (string, optional): Branch identifier
- `needsMoreThoughts` (boolean, optional): If more thoughts are needed
## Usage
The Sequential Thinking tool is designed for:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out
## Configuration
### Usage with Claude Desktop
Add this to your `claude_desktop_config.json`:
#### npx
```json
{
"mcpServers": {
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
}
}
}
```
#### docker
```json
{
"mcpServers": {
"sequentialthinking": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"mcp/sequentialthinking"
]
}
}
}
```
To disable logging of thought information set env var: `DISABLE_THOUGHT_LOGGING` to `true`.
Comment
### Usage with VS Code
For quick installation, click one of the installation buttons below...
[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-sequential-thinking%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-sequential-thinking%22%5D%7D&quality=insiders)
[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22mcp%2Fsequentialthinking%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22mcp%2Fsequentialthinking%22%5D%7D&quality=insiders)
For manual installation, you can configure the MCP server using one of these methods:
**Method 1: User Configuration (Recommended)**
Add the configuration to your user-level MCP configuration file. Open the Command Palette (`Ctrl + Shift + P`) and run `MCP: Open User Configuration`. This will open your user `mcp.json` file where you can add the server configuration.
**Method 2: Workspace Configuration**
Alternatively, you can add the configuration to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
> For more details about MCP configuration in VS Code, see the [official VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).
For NPX installation:
```json
{
"servers": {
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
}
}
}
```
For Docker installation:
```json
{
"servers": {
"sequential-thinking": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"mcp/sequentialthinking"
]
}
}
}
```
### Usage with Codex CLI
Run the following:
#### npx
```bash
codex mcp add sequential-thinking npx -y @modelcontextprotocol/server-sequential-thinking
```
## Building
Docker:
```bash
docker build -t mcp/sequentialthinking -f src/sequentialthinking/Dockerfile .
```
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.

View File

@@ -0,0 +1,308 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { SequentialThinkingServer, ThoughtData } from '../lib.js';
// Mock chalk to avoid ESM issues
vi.mock('chalk', () => {
const chalkMock = {
yellow: (str: string) => str,
green: (str: string) => str,
blue: (str: string) => str,
};
return {
default: chalkMock,
};
});
describe('SequentialThinkingServer', () => {
let server: SequentialThinkingServer;
beforeEach(() => {
// Disable thought logging for tests
process.env.DISABLE_THOUGHT_LOGGING = 'true';
server = new SequentialThinkingServer();
});
// 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', () => {
const input = {
thought: 'This is my first thought',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
};
const result = server.processThought(input);
expect(result.isError).toBeUndefined();
const data = JSON.parse(result.content[0].text);
expect(data.thoughtNumber).toBe(1);
expect(data.totalThoughts).toBe(3);
expect(data.nextThoughtNeeded).toBe(true);
expect(data.thoughtHistoryLength).toBe(1);
});
it('should accept thought with optional fields', () => {
const input = {
thought: 'Revising my earlier idea',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: true,
revisesThought: 1,
needsMoreThoughts: false
};
const result = server.processThought(input);
expect(result.isError).toBeUndefined();
const data = JSON.parse(result.content[0].text);
expect(data.thoughtNumber).toBe(2);
expect(data.thoughtHistoryLength).toBe(1);
});
it('should track multiple thoughts in history', () => {
const input1 = {
thought: 'First thought',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
};
const input2 = {
thought: 'Second thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true
};
const input3 = {
thought: 'Final thought',
thoughtNumber: 3,
totalThoughts: 3,
nextThoughtNeeded: false
};
server.processThought(input1);
server.processThought(input2);
const result = server.processThought(input3);
const data = JSON.parse(result.content[0].text);
expect(data.thoughtHistoryLength).toBe(3);
expect(data.nextThoughtNeeded).toBe(false);
});
it('should auto-adjust totalThoughts if thoughtNumber exceeds it', () => {
const input = {
thought: 'Thought 5',
thoughtNumber: 5,
totalThoughts: 3,
nextThoughtNeeded: true
};
const result = server.processThought(input);
const data = JSON.parse(result.content[0].text);
expect(data.totalThoughts).toBe(5);
});
});
describe('processThought - branching', () => {
it('should track branches correctly', () => {
const input1 = {
thought: 'Main thought',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
};
const input2 = {
thought: 'Branch A thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
branchFromThought: 1,
branchId: 'branch-a'
};
const input3 = {
thought: 'Branch B thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: false,
branchFromThought: 1,
branchId: 'branch-b'
};
server.processThought(input1);
server.processThought(input2);
const result = server.processThought(input3);
const data = JSON.parse(result.content[0].text);
expect(data.branches).toContain('branch-a');
expect(data.branches).toContain('branch-b');
expect(data.branches.length).toBe(2);
expect(data.thoughtHistoryLength).toBe(3);
});
it('should allow multiple thoughts in same branch', () => {
const input1 = {
thought: 'Branch thought 1',
thoughtNumber: 1,
totalThoughts: 2,
nextThoughtNeeded: true,
branchFromThought: 1,
branchId: 'branch-a'
};
const input2 = {
thought: 'Branch thought 2',
thoughtNumber: 2,
totalThoughts: 2,
nextThoughtNeeded: false,
branchFromThought: 1,
branchId: 'branch-a'
};
server.processThought(input1);
const result = server.processThought(input2);
const data = JSON.parse(result.content[0].text);
expect(data.branches).toContain('branch-a');
expect(data.branches.length).toBe(1);
});
});
describe('processThought - edge cases', () => {
it('should handle very long thought strings', () => {
const input = {
thought: 'a'.repeat(10000),
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
};
const result = server.processThought(input);
expect(result.isError).toBeUndefined();
});
it('should handle thoughtNumber = 1, totalThoughts = 1', () => {
const input = {
thought: 'Only thought',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
};
const result = server.processThought(input);
expect(result.isError).toBeUndefined();
const data = JSON.parse(result.content[0].text);
expect(data.thoughtNumber).toBe(1);
expect(data.totalThoughts).toBe(1);
});
it('should handle nextThoughtNeeded = false', () => {
const input = {
thought: 'Final thought',
thoughtNumber: 3,
totalThoughts: 3,
nextThoughtNeeded: false
};
const result = server.processThought(input);
const data = JSON.parse(result.content[0].text);
expect(data.nextThoughtNeeded).toBe(false);
});
});
describe('processThought - response format', () => {
it('should return correct response structure on success', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
};
const result = server.processThought(input);
expect(result).toHaveProperty('content');
expect(Array.isArray(result.content)).toBe(true);
expect(result.content.length).toBe(1);
expect(result.content[0]).toHaveProperty('type', 'text');
expect(result.content[0]).toHaveProperty('text');
});
it('should return valid JSON in response', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
};
const result = server.processThought(input);
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
});
});
describe('processThought - with logging enabled', () => {
let serverWithLogging: SequentialThinkingServer;
beforeEach(() => {
// Enable thought logging for these tests
delete process.env.DISABLE_THOUGHT_LOGGING;
serverWithLogging = new SequentialThinkingServer();
});
afterEach(() => {
// Reset to disabled for other tests
process.env.DISABLE_THOUGHT_LOGGING = 'true';
});
it('should format and log regular thoughts', () => {
const input = {
thought: 'Test thought with logging',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
};
const result = serverWithLogging.processThought(input);
expect(result.isError).toBeUndefined();
});
it('should format and log revision thoughts', () => {
const input = {
thought: 'Revised thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: true,
revisesThought: 1
};
const result = serverWithLogging.processThought(input);
expect(result.isError).toBeUndefined();
});
it('should format and log branch thoughts', () => {
const input = {
thought: 'Branch thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: false,
branchFromThought: 1,
branchId: 'branch-a'
};
const result = serverWithLogging.processThought(input);
expect(result.isError).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env node
import * as http from "http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";
import { SequentialThinkingServer } from "./lib.js";
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.
When to use this tool:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Problems that require a multi-step solution
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out
Key features:
- You can adjust total_thoughts up or down as you progress
- You can question or revise previous thoughts
- You can add more thoughts even after reaching what seemed like the end
- You can express uncertainty and explore alternative approaches
- Not every thought needs to build linearly - you can branch or backtrack
- Generates a solution hypothesis
- Verifies the hypothesis based on the Chain of Thought steps
- Repeats the process until satisfied
- Provides a correct answer
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
- 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)
- isRevision: A boolean indicating if this thought revises previous thinking
- revisesThought: If is_revision is true, which thought number is being reconsidered
- branchFromThought: If branching, which thought number is the branching point
- branchId: Identifier for the current branch (if any)
- needsMoreThoughts: If reaching end but realizing more thoughts needed
You should:
1. Start with an initial estimate of needed thoughts, but be ready to adjust
2. Feel free to question or revise previous thoughts
3. Don't hesitate to add more thoughts if needed, even at the "end"
4. Express uncertainty when present
5. Mark thoughts that revise previous thinking or branch into new paths
6. Ignore information that is irrelevant to the current step
7. Generate a solution hypothesis when appropriate
8. Verify the hypothesis based on the Chain of Thought steps
9. Repeat the process until satisfied with the solution
10. Provide a single, ideally correct answer as the final output
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"),
branchId: z.string().optional().describe("Branch identifier"),
needsMoreThoughts: z.boolean().optional().describe("If more thoughts are needed")
},
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);
if (result.isError) {
return result;
}
// Parse the JSON response to get structured content
const parsedContent = JSON.parse(result.content[0].text);
return {
content: result.content,
structuredContent: parsedContent
};
}
);
const sseTransportsBySessionId = new Map<string, SSEServerTransport>();
function runServer() {
const port = Number(process.env.MCP_PORT ?? process.env.SSE_PORT ?? 3000);
const httpServer = http.createServer(async (req, res) => {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
const pathname = url.pathname;
if (req.method === "GET" && (pathname === "/sse" || pathname === "/")) {
try {
const transport = new SSEServerTransport("/messages", res);
sseTransportsBySessionId.set(transport.sessionId, transport);
transport.onclose = () => {
sseTransportsBySessionId.delete(transport.sessionId);
};
await server.connect(transport);
console.error("Sequential Thinking MCP Server: new SSE client connected");
} catch (error) {
console.error("SSE connection error:", error);
if (!res.headersSent) {
res.writeHead(500).end("Internal server error");
}
}
return;
}
if (req.method === "POST" && pathname === "/messages") {
const sessionId = url.searchParams.get("sessionId");
if (!sessionId) {
res.writeHead(400).end("Missing sessionId query parameter");
return;
}
const transport = sseTransportsBySessionId.get(sessionId);
if (!transport) {
res.writeHead(404).end("Unknown session");
return;
}
await transport.handlePostMessage(req, res);
return;
}
res.writeHead(404).end("Not found");
});
httpServer.listen(port, () => {
console.error(`Sequential Thinking MCP Server running on SSE at http://localhost:${port}`);
console.error(" GET /sse open SSE stream (then POST to /messages?sessionId=...)");
console.error(" POST /messages?sessionId=<id> send MCP messages");
});
}
runServer();

View File

@@ -0,0 +1,99 @@
import chalk from 'chalk';
export interface ThoughtData {
thought: string;
thoughtNumber: number;
totalThoughts: number;
isRevision?: boolean;
revisesThought?: number;
branchFromThought?: number;
branchId?: string;
needsMoreThoughts?: boolean;
nextThoughtNeeded: boolean;
}
export class SequentialThinkingServer {
private thoughtHistory: ThoughtData[] = [];
private branches: Record<string, ThoughtData[]> = {};
private disableThoughtLogging: boolean;
constructor() {
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
}
private formatThought(thoughtData: ThoughtData): string {
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } = thoughtData;
let prefix = '';
let context = '';
if (isRevision) {
prefix = chalk.yellow('🔄 Revision');
context = ` (revising thought ${revisesThought})`;
} else if (branchFromThought) {
prefix = chalk.green('🌿 Branch');
context = ` (from thought ${branchFromThought}, ID: ${branchId})`;
} else {
prefix = chalk.blue('💭 Thought');
context = '';
}
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`;
const border = '─'.repeat(Math.max(header.length, thought.length) + 4);
return `
${border}
${header}
${border}
${thought.padEnd(border.length - 2)}
${border}`;
}
public processThought(input: ThoughtData): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
try {
// 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(input);
if (input.branchFromThought && input.branchId) {
if (!this.branches[input.branchId]) {
this.branches[input.branchId] = [];
}
this.branches[input.branchId].push(input);
}
if (!this.disableThoughtLogging) {
const formattedThought = this.formatThought(input);
console.error(formattedThought);
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
thoughtNumber: input.thoughtNumber,
totalThoughts: input.totalThoughts,
nextThoughtNeeded: input.nextThoughtNeeded,
branches: Object.keys(this.branches),
thoughtHistoryLength: this.thoughtHistory.length
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
status: 'failed'
}, null, 2)
}],
isError: true
};
}
}
}

View File

@@ -0,0 +1,40 @@
{
"name": "@modelcontextprotocol/server-sequential-thinking",
"version": "0.6.2",
"description": "MCP server for sequential thinking and problem solving",
"license": "SEE LICENSE IN LICENSE",
"mcpName": "io.github.modelcontextprotocol/server-sequential-thinking",
"author": "Model Context Protocol a Series of LF Projects, LLC.",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/servers.git"
},
"type": "module",
"bin": {
"mcp-server-sequential-thinking": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"chalk": "^5.3.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^22",
"@types/yargs": "^17.0.32",
"@vitest/coverage-v8": "^2.1.8",
"shx": "^0.3.4",
"typescript": "^5.3.3",
"vitest": "^2.1.8"
}
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"target": "ES2022",
"lib": ["ES2022","DOM"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["./**/*.ts"],
"exclude": ["**/*.test.ts", "vitest.config.ts"]
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/__tests__/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['**/*.ts'],
exclude: ['**/__tests__/**', '**/dist/**'],
},
},
});