UNPKG

@mettamatt/code-reasoning

Version:

Enhanced MCP server for code reasoning using sequential thinking methodology, optimized for programming tasks

490 lines (484 loc) • 21.9 kB
#!/usr/bin/env node /** * @fileoverview Code Reasoning MCP Server Implementation. * * This server provides a tool for reflective problem-solving in software development, * allowing decomposition of tasks into sequential, revisable, and branchable thoughts. * It adheres to the Model Context Protocol (MCP) using SDK version 1.11.0 and is designed * to integrate seamlessly with Claude Desktop or similar MCP-compliant clients. * * ## Key Features * - Processes "thoughts" in structured JSON with sequential numbering * - Supports advanced reasoning patterns through branching and revision semantics * - Branching: Explore alternative approaches from any existing thought * - Revision: Correct or update earlier thoughts when new insights emerge * - Implements MCP capabilities for tools, resources, and prompts * - Uses custom FilteredStdioServerTransport for improved stability * - Provides detailed validation and error handling with helpful guidance * - Logs thought evolution to stderr for debugging and visibility * * ## Usage in Claude Desktop * - In your Claude Desktop settings, add a "tool" definition referencing this server * - Ensure the tool name is "code-reasoning" * - Configure Claude to use this tool for complex reasoning and problem-solving tasks * - Upon connecting, Claude can call the tool with an argument schema matching the * `ThoughtDataSchema` defined in this file * * ## MCP Protocol Communication * - IMPORTANT: Local MCP servers must never log to stdout (standard output) * - All logging must be directed to stderr using console.error() instead of console.log() * - The stdout channel is reserved exclusively for JSON-RPC protocol messages * - Using console.log() or console.info() will cause client-side parsing errors * * ## Example Thought Data * ```json * { * "thought": "Start investigating the root cause of bug #1234", * "thought_number": 1, * "total_thoughts": 5, * "next_thought_needed": true * } * ``` * * @version 0.7.0 * @mcp-sdk-version 1.11.0 */ import process from 'node:process'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, CompleteRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z, ZodError } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { PromptManager } from './prompts/manager.js'; import { configManager } from './utils/config-manager.js'; import { CONFIG_DIR, MAX_THOUGHT_LENGTH, MAX_THOUGHTS, CUSTOM_PROMPTS_DIR, } from './utils/config.js'; /* -------------------------------------------------------------------------- */ /* CONFIGURATION */ /* -------------------------------------------------------------------------- */ // Compile-time enum -> const enum would be erased, but we keep values for logs. export var LogLevel; (function (LogLevel) { LogLevel[LogLevel["ERROR"] = 0] = "ERROR"; LogLevel[LogLevel["WARN"] = 1] = "WARN"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG"; })(LogLevel || (LogLevel = {})); const ThoughtDataSchema = z .object({ thought: z .string() .trim() .min(1, 'Thought cannot be empty.') .max(MAX_THOUGHT_LENGTH, `Thought exceeds ${MAX_THOUGHT_LENGTH} chars.`), thought_number: z.number().int().positive(), total_thoughts: z.number().int().positive(), next_thought_needed: z.boolean(), is_revision: z.boolean().optional(), revises_thought: z.number().int().positive().optional(), branch_from_thought: z.number().int().positive().optional(), branch_id: z.string().trim().min(1).optional(), needs_more_thoughts: z.boolean().optional(), }) .refine(d => d.is_revision ? typeof d.revises_thought === 'number' && !d.branch_id && !d.branch_from_thought : true, { message: 'If is_revision=true, provide revises_thought and omit branch_* fields.', }) .refine(d => (!d.is_revision && d.revises_thought === undefined) || d.is_revision, { message: 'revises_thought only allowed when is_revision=true.', }) .refine(d => d.branch_id || d.branch_from_thought ? d.branch_id !== undefined && d.branch_from_thought !== undefined && !d.is_revision : true, { message: 'branch_id and branch_from_thought required together and not with revision.', }); /** * Cached JSON schema: avoids rebuilding on every ListTools call. */ const THOUGHT_DATA_JSON_SCHEMA = Object.freeze(zodToJsonSchema(ThoughtDataSchema, { target: 'jsonSchema7' })); /* -------------------------------------------------------------------------- */ /* TOOL DEF */ /* -------------------------------------------------------------------------- */ const CODE_REASONING_TOOL = { name: 'code-reasoning', description: `🧠 A detailed tool for dynamic and reflective problem-solving through sequential thinking. This tool helps you 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. šŸ“‹ KEY PARAMETERS: - thought: Your current reasoning step (required) - thought_number: Current number in sequence (required) - total_thoughts: Estimated final count (required, can adjust as needed) - next_thought_needed: Set to FALSE ONLY when completely done (required) - branch_from_thought + branch_id: When exploring alternative approaches (🌿) - is_revision + revises_thought: When correcting earlier thinking (šŸ”„) āœ… CRITICAL CHECKLIST (review every 3 thoughts): 1. Need to explore alternatives? → Use BRANCH (🌿) with branch_from_thought + branch_id 2. Need to correct earlier thinking? → Use REVISION (šŸ”„) with is_revision + revises_thought 3. Scope changed? → Adjust total_thoughts up or down as needed 4. Only set next_thought_needed = false when you have a complete, verified solution šŸ’” BEST PRACTICES: - Start with an initial estimate of total_thoughts, but adjust as you go - Don't hesitate to revise earlier conclusions when new insights emerge - Use branching to explore multiple approaches to the same problem - Express uncertainty when present - Ignore information that is irrelevant to the current step - End with a clear, validated conclusion before setting next_thought_needed = false āœļø End each thought by asking: "What am I missing or need to reconsider?"`, // eslint-disable-next-line @typescript-eslint/no-explicit-any inputSchema: THOUGHT_DATA_JSON_SCHEMA, // SDK expects unknown JSON schema shape annotations: { title: 'Code Reasoning', readOnlyHint: true, }, }; /* -------------------------------------------------------------------------- */ /* STDIO TRANSPORT WITH FILTERING */ /* -------------------------------------------------------------------------- */ class FilteredStdioServerTransport extends StdioServerTransport { originalStdoutWrite; constructor() { super(); // Store the original implementation before making any changes this.originalStdoutWrite = process.stdout.write; // Create a bound version that preserves the original context const boundOriginalWrite = this.originalStdoutWrite.bind(process.stdout); // Override with a new function that avoids recursion process.stdout.write = ((data) => { if (typeof data === 'string') { const s = data.trimStart(); if (s.startsWith('{') || s.startsWith('[')) { // Call the bound function directly to avoid circular reference return boundOriginalWrite(data); } // Silent handling of non-JSON strings return true; } // For non-string data, use the original implementation return boundOriginalWrite(data); // eslint-disable-next-line @typescript-eslint/no-explicit-any }); } // Add cleanup to restore the original when the transport is closed async close() { // Restore the original stdout.write before closing if (this.originalStdoutWrite) { process.stdout.write = this.originalStdoutWrite; } // Call the parent class's close method await super.close(); } } /* -------------------------------------------------------------------------- */ /* SERVER IMPLEMENTATION */ /* -------------------------------------------------------------------------- */ class CodeReasoningServer { cfg; thoughtHistory = []; branches = new Map(); constructor(cfg) { this.cfg = cfg; console.error('Code-Reasoning logic ready', { cfg }); } /* ----------------------------- Helper Methods ---------------------------- */ formatThought(t) { const { thought_number, total_thoughts, thought, is_revision, revises_thought, branch_id, branch_from_thought, } = t; const header = is_revision ? `šŸ”„ Revision ${thought_number}/${total_thoughts} (of ${revises_thought})` : branch_id ? `🌿 Branch ${thought_number}/${total_thoughts} (from ${branch_from_thought}, id:${branch_id})` : `šŸ’­ Thought ${thought_number}/${total_thoughts}`; const body = thought .split('\n') .map(l => ` ${l}`) .join('\n'); return `\n${header}\n---\n${body}\n---`; } /** * Provides example thought data based on error message to help users correct input. */ getExampleThought(errorMsg) { if (errorMsg.includes('branch')) { return { thought: 'Exploring alternative: Consider algorithm X.', thought_number: 3, total_thoughts: 7, next_thought_needed: true, branch_from_thought: 2, branch_id: 'alternative-algo-x', }; } else if (errorMsg.includes('revis')) { return { thought: 'Revisiting earlier point: Assumption Y was flawed.', thought_number: 4, total_thoughts: 6, next_thought_needed: true, is_revision: true, revises_thought: 2, }; } else if (errorMsg.includes('length') || errorMsg.includes('Thought cannot be empty')) { return { thought: 'Breaking down the thought into smaller parts...', thought_number: 2, total_thoughts: 5, next_thought_needed: true, }; } // Default fallback return { thought: 'Initial exploration of the problem.', thought_number: 1, total_thoughts: 5, next_thought_needed: true, }; } buildSuccess(t) { const payload = { status: 'processed', thought_number: t.thought_number, total_thoughts: t.total_thoughts, next_thought_needed: t.next_thought_needed, branches: Array.from(this.branches.keys()), thought_history_length: this.thoughtHistory.length, }; return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: false }; } buildError(error) { let errorMessage = error.message; let guidance = 'Check the tool description and schema for correct usage.'; const example = this.getExampleThought(errorMessage); if (error instanceof ZodError) { errorMessage = `Validation Error: ${error.errors .map(e => `${e.path.join('.')}: ${e.message}`) .join(', ')}`; // Provide specific guidance based on error path const firstPath = error.errors[0]?.path.join('.'); if (firstPath?.includes('thought') && !firstPath.includes('number')) { guidance = `The 'thought' field is empty or invalid. Must be a non-empty string below ${MAX_THOUGHT_LENGTH} characters.`; } else if (firstPath?.includes('thought_number')) { guidance = 'Ensure thought_number is a positive integer and increments correctly.'; } else if (firstPath?.includes('branch')) { guidance = 'When branching, provide both "branch_from_thought" (number) and "branch_id" (string), and do not combine with revision.'; } else if (firstPath?.includes('revision')) { guidance = 'When revising, set is_revision=true and provide revises_thought (positive number). Do not combine with branching.'; } } else if (errorMessage.includes('length')) { guidance = `The thought is too long. Keep it under ${MAX_THOUGHT_LENGTH} characters.`; } else if (errorMessage.includes('Max thought_number exceeded')) { guidance = `The maximum thought limit (${MAX_THOUGHTS}) was reached.`; } const payload = { status: 'failed', error: errorMessage, guidance, example, }; return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true }; } /* ------------------------------ Main Handler ----------------------------- */ async processThought(input) { const t0 = performance.now(); try { const data = ThoughtDataSchema.parse(input); // Sanity limits ------------------------------------------------------- if (data.thought_number > MAX_THOUGHTS) { throw new Error(`Max thought_number exceeded (${MAX_THOUGHTS}).`); } if (data.branch_from_thought && data.branch_from_thought > this.thoughtHistory.length) { throw new Error(`Invalid branch_from_thought ${data.branch_from_thought}.`); } // Stats & storage ----------------------------------------------------- this.thoughtHistory.push(data); if (data.branch_id) { const arr = this.branches.get(data.branch_id) ?? []; arr.push(data); this.branches.set(data.branch_id, arr); } console.error(this.formatThought(data)); console.error('āœ”ļø processed', { num: data.thought_number, elapsedMs: +(performance.now() - t0).toFixed(1), }); return this.buildSuccess(data); } catch (err) { const e = err; console.error('āŒ error', { err: e.message, elapsedMs: +(performance.now() - t0).toFixed(1), }); if (err instanceof ZodError && this.cfg.debug) console.error(err.errors); return this.buildError(e); } } } /* -------------------------------------------------------------------------- */ /* BOOTSTRAP */ /* -------------------------------------------------------------------------- */ export async function runServer(debugFlag = false) { // Initialize config manager and get config await configManager.init(); const config = await configManager.getConfig(); // Apply debug flag if specified if (debugFlag) { await configManager.setValue('debug', true); } const serverMeta = { name: 'code-reasoning-server', version: '0.7.0' }; // Configure server capabilities based on config const capabilities = { tools: {}, resources: {}, completions: {}, // Add completions capability }; // Only add prompts capability if enabled if (config.promptsEnabled) { capabilities.prompts = { list: true, get: true, }; } const srv = new Server(serverMeta, { capabilities }); const logic = new CodeReasoningServer(config); // Initialize prompt manager if enabled let promptManager; if (config.promptsEnabled) { promptManager = new PromptManager(CONFIG_DIR); console.error('Prompts capability enabled'); // Load custom prompts from the standard location console.error(`Loading custom prompts from ${CUSTOM_PROMPTS_DIR}`); await promptManager.loadCustomPrompts(CUSTOM_PROMPTS_DIR); // Add prompt handlers srv.setRequestHandler(ListPromptsRequestSchema, async () => { const prompts = promptManager?.getAllPrompts() || []; console.error(`Returning ${prompts.length} prompts`); return { prompts }; }); srv.setRequestHandler(GetPromptRequestSchema, async (req) => { try { if (!promptManager) { throw new Error('Prompt manager not initialized'); } const promptName = req.params.name; const args = req.params.arguments || {}; console.error(`Getting prompt: ${promptName} with args:`, args); // Get the prompt result const result = promptManager.applyPrompt(promptName, args); // Return the result in the format expected by MCP return { messages: result.messages, _meta: {}, }; } catch (err) { const e = err; console.error('Prompt error:', e.message); return { isError: true, content: [{ type: 'text', text: e.message }], }; } }); // Add handler for completion/complete requests srv.setRequestHandler(CompleteRequestSchema, async (req) => { try { if (!promptManager) { throw new Error('Prompt manager not initialized'); } // Check if this is a prompt reference if (req.params.ref.type !== 'ref/prompt') { return { completion: { values: [], }, }; } const promptName = req.params.ref.name; const argName = req.params.argument.name; console.error(`Completing argument: ${argName} for prompt: ${promptName}`); // Get stored values for this prompt using the public method const storedValues = promptManager.getStoredValues(promptName); // Return the stored value for this argument if available if (storedValues[argName]) { return { completion: { values: [storedValues[argName]], }, }; } // Return empty array if no stored value return { completion: { values: [], }, }; } catch (err) { const e = err; console.error('Completion error:', e.message); return { completion: { values: [], }, }; } }); } else { // Keep the empty handlers if prompts disabled srv.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] })); // Add empty handler for completion requests as well when prompts are disabled srv.setRequestHandler(CompleteRequestSchema, async () => ({ completion: { values: [], }, })); } // Existing handlers srv.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] })); srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [CODE_REASONING_TOOL] })); srv.setRequestHandler(CallToolRequestSchema, req => req.params.name === CODE_REASONING_TOOL.name ? logic.processThought(req.params.arguments) : Promise.resolve({ isError: true, content: [ { type: 'text', text: JSON.stringify({ code: -32601, message: `Unknown tool ${req.params.name}` }), }, ], })); const transport = new FilteredStdioServerTransport(); await srv.connect(transport); console.error('šŸš€ Code-Reasoning MCP Server ready.'); const shutdown = async (sig) => { console.error(`ā†©ļøŽ shutdown on ${sig}`); await srv.close(); await transport.close(); process.exit(0); }; ['SIGINT', 'SIGTERM'].forEach(s => process.on(s, () => shutdown(s))); process.on('uncaughtException', err => { console.error('šŸ’„ uncaught', err); shutdown('uncaughtException'); }); process.on('unhandledRejection', r => { console.error('šŸ’„ unhandledRejection', r); shutdown('unhandledRejection'); }); } // Self-execute when run directly ------------------------------------------------ if (import.meta.url === `file://${process.argv[1]}`) { runServer(process.argv.includes('--debug')).catch(err => { console.error('FATAL: failed to start', err); process.exit(1); }); }