mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-18 00:03:23 +02:00
* fix(sequential-thinking): convert to modern TypeScript SDK APIs Convert the sequential-thinking server to use the modern McpServer API instead of the low-level Server API. Key changes: - Replace Server with McpServer from @modelcontextprotocol/sdk/server/mcp.js - Use registerTool() method instead of manual request handlers - Use Zod schemas directly in inputSchema/outputSchema - Add structuredContent to tool responses - Fix type literals to use 'as const' assertions The modern API provides: - Less boilerplate code - Better type safety with Zod - More declarative tool registration - Cleaner, more maintainable code * fix: exclude test files from TypeScript build Add exclude for test files and vitest.config.ts to tsconfig * refactor: remove redundant validation now handled by Zod Zod schema already validates all required fields and types. Removed validateThoughtData() method and kept only business logic validation (adjusting totalThoughts if needed). * fix(sequentialthinking): add Zod validation to processThought method The modern API migration removed manual validation from processThought(), but tests call this method directly, bypassing the Zod validation in the tool registration layer. This commit adds Zod validation directly in the processThought() method to ensure validation works both when called via MCP and when called directly (e.g., in tests). Also improves error message formatting to match the expected error messages in the tests. * refactor: simplify by removing redundant validation Since processThought() is only called through the tool registration in production, validation always happens via Zod schemas at that layer. Removed redundant validation logic from processThought() and updated tests to reflect this architectural decision. Changes: - Remove Zod validation from processThought() method - Accept ThoughtData type instead of unknown - Remove 10 validation tests that are now handled at tool registration - Add comment explaining validation approach
309 lines
8.4 KiB
TypeScript
309 lines
8.4 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|