UNPKG

structured-thinking-mcp

Version:

MCP server for structured thinking framework - Step-by-step reasoning and systematic problem-solving for LLMs

1,002 lines (999 loc) 56 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // Fixed chalk import for ESM import chalk from 'chalk'; import YAML from 'yaml'; import { homedir } from 'os'; import { join, dirname } from 'path'; import { readFileSync, existsSync, readdirSync } from 'fs'; import { fileURLToPath } from 'url'; // Get directory of current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load prompts configuration with flexible priority const loadPrompts = () => { let promptFile; // Priority 1: Command line argument (--prompt filename.json or just filename.json) const args = process.argv.slice(2); const promptArgIndex = args.findIndex(arg => arg === '--prompt'); if (promptArgIndex !== -1 && args[promptArgIndex + 1]) { promptFile = args[promptArgIndex + 1]; } else if (args.length > 0) { // Accept any first argument as config name promptFile = args[0]; // Auto-append extension if not present if (!args[0].includes('.')) { // Check which file exists if (existsSync(join(__dirname, 'prompts', `${args[0]}.yaml`))) { promptFile = `${args[0]}.yaml`; } else if (existsSync(join(__dirname, 'prompts', `${args[0]}.json`))) { promptFile = `${args[0]}.json`; } } } // Priority 2: Environment variable if (!promptFile && process.env.CHAIN_OF_THOUGHT_CONFIG) { promptFile = process.env.CHAIN_OF_THOUGHT_CONFIG; } // Priority 3: Default fallback if (!promptFile) { // Prefer YAML if present; otherwise fall back to JSON const candidates = ['default.yaml', 'default.yml', 'default.json']; const baseDir = join(__dirname, '..', 'prompts'); const selected = candidates.find(name => existsSync(join(baseDir, name))); promptFile = selected || 'default.json'; } // Resolve the path - look for prompts in the source directory const promptPath = promptFile.includes('/') || promptFile.includes('\\') ? promptFile : join(__dirname, '..', 'prompts', promptFile); try { const raw = readFileSync(promptPath, 'utf-8'); const lower = promptPath.toLowerCase(); if (lower.endsWith('.yaml') || lower.endsWith('.yml')) { return YAML.parse(raw); } if (lower.endsWith('.json')) { return JSON.parse(raw); } // Unknown extension: try JSON first, then YAML try { return JSON.parse(raw); } catch { return YAML.parse(raw); } } catch (error) { console.error(`Failed to load prompts from ${promptPath}:`, error); // Fallback to hardcoded minimal prompts (provide keys that code expects) return { errors: { invalidThought: "Invalid thought: must be a string", invalidThoughtNumber: "Invalid thoughtNumber: must be a number", invalidTotalThoughts: "Invalid totalThoughts: must be a number", invalidNextThoughtNeeded: "Invalid nextThoughtNeeded: must be a boolean", commandFieldRequired: "commandSelection type \"command\" requires a \"command\" field", reasonFieldRequired: "commandSelection type \"{type}\" requires a \"reason\" field", invalidSelectionType: "Invalid commandSelection type: {type}. Must be \"command\", \"skip\", or \"skip_reason\"", unexpectedThoughtNumber: "Unexpected thought number: {number}.", mandatoryViolation: "Step {step} requires commandSelection", unknownTool: "Unknown tool: {name}" }, messages: { duplicateDocumentRead: "Document already read this session: {command}", serverRunning: "MCP Server running", fatalError: "Fatal error: {error}" }, console: { thoughtPrefix: { default: "Thought", revision: "Revision", branch: "Branch" }, thoughtContext: { revision: " (revising thought {number})", branch: " (from thought {from}, ID: {id})" } }, config: {} }; } }; const prompts = loadPrompts(); class StructuredThinkingMcpServer { thoughtHistory = []; branches = {}; disableThoughtLogging; selectedCommand = null; readFiles = new Set(); config; hasCommandBeenSelected = false; // Agents extracted from last-read TOML command extractedAgentsFromToml = []; extractedFromCommand = null; suggestedAgentFromCommand; constructor(config) { this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true"; // Default configuration (can be overridden) this.config = { commandPath: config?.commandPath || '', commandExtension: config?.commandExtension || '.md', // Default to .md availableCommands: config?.availableCommands || [], requireSelectionByStep: config?.requireSelectionByStep ?? 2, enableAutoRead: config?.enableAutoRead ?? true, agentsPath: config?.agentsPath || '~/.gemini/agents/', agentsExtension: config?.agentsExtension || '.md', historyMaxLength: typeof config?.historyMaxLength === 'number' ? config?.historyMaxLength : 200, ...config }; } // Extract agents array from TOML content in a permissive way parseAgentsFromToml(tomlContent) { try { const match = tomlContent.match(/\bagents\s*=\s*\[([\\s\\S]*?)\]/i); if (!match) return []; const inner = match[1]; const items = inner.split(','); const safe = /^[a-z0-9-]+$/; const agents = items .map(s => s.replace(/#.*$/m, '')) // remove comments in the same slice .map(s => s.trim()) .map(s => s.replace(/^['\"]/, '').replace(/['\"]$/, '')) .map(s => s.toLowerCase()) .filter(Boolean) .filter(s => safe.test(s)); const seen = new Set(); return agents.filter(a => (seen.has(a) ? false : (seen.add(a), true))); } catch { return []; } } expandPath(path) { if (path.startsWith('~/')) { return join(homedir(), path.slice(2)); } return path; } // Resolve available agents from config or by scanning the agents directory resolveAvailableAgents() { const safe = /^[a-z0-9-]+$/; if (this.config.availableAgents && this.config.availableAgents.length > 0) { return this.config.availableAgents .map(a => a.toLowerCase()) .filter(a => safe.test(a)); } try { const base = this.expandPath(this.config.agentsPath || '~/.gemini/agents/'); const ext = (this.config.agentsExtension || '.md').toLowerCase(); const entries = readdirSync(base, { withFileTypes: true }); return entries .filter(e => e.isFile()) .map(e => e.name) .filter(name => name.toLowerCase().endsWith(ext)) .map(name => name.slice(0, name.length - ext.length).toLowerCase()) .filter(a => safe.test(a)); } catch { return []; } } // Simple parameter normalizer for handling various AI input formats normalizeParameters(input) { if (!input || typeof input !== 'object') return input; const result = { ...input }; // Convert string to object for commandSelection/agentSelection ['commandSelection', 'agentSelection'].forEach(key => { if (typeof result[key] === 'string') { const str = result[key]; try { result[key] = JSON.parse(str); } catch { // Handle any XML-like format with type parameter if (str.includes('name="type">') || str.includes("name='type'>")) { const typeMap = { 'command': { type: 'command', command: 'command' }, 'skip': { type: 'skip', reason: 'User requested skip' }, 'agents': { type: 'agents', agents: ['default-agent'] } }; for (const [typeName, typeObj] of Object.entries(typeMap)) { if (str.includes(`>${typeName}`)) { result[key] = typeObj; break; } } } // If not JSON and not XML, keep as string (will be handled by existing validation) } } }); // Convert string numbers to numbers ['thoughtNumber', 'totalThoughts', 'revisesThought', 'branchFromThought'].forEach(key => { if (typeof result[key] === 'string') { const num = parseInt(result[key], 10); if (!isNaN(num)) result[key] = num; } }); // Convert string booleans to booleans ['nextThoughtNeeded', 'isRevision', 'needsMoreThoughts'].forEach(key => { if (result[key] === 'true') result[key] = true; else if (result[key] === 'false') result[key] = false; }); return result; } validateThoughtData(input) { // Apply normalization first const normalized = this.normalizeParameters(input); const data = normalized; if (!data.thought || typeof data.thought !== 'string') { throw new Error(prompts.errors.invalidThought); } // Coerce numeric strings to numbers for robustness if (typeof data.thoughtNumber === 'string') { const n = parseInt(data.thoughtNumber, 10); if (!Number.isNaN(n)) data.thoughtNumber = n; } if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') { throw new Error(prompts.errors.invalidThoughtNumber); } if (typeof data.totalThoughts === 'string') { const n = parseInt(data.totalThoughts, 10); if (!Number.isNaN(n)) data.totalThoughts = n; } if (!data.totalThoughts || typeof data.totalThoughts !== 'number') { throw new Error(prompts.errors.invalidTotalThoughts); } // Fix: Handle various formats of nextThoughtNeeded (Gemini sends different types) if (data.nextThoughtNeeded === undefined || data.nextThoughtNeeded === null) { data.nextThoughtNeeded = true; // Default to true if missing } if (typeof data.nextThoughtNeeded !== 'boolean') { // Try to convert to boolean data.nextThoughtNeeded = Boolean(data.nextThoughtNeeded); } return { thought: data.thought, thoughtNumber: data.thoughtNumber, totalThoughts: data.totalThoughts, nextThoughtNeeded: Boolean(data.nextThoughtNeeded), // Ensure it's always boolean commandSelection: data.commandSelection, agentSelection: data.agentSelection, isRevision: data.isRevision, revisesThought: data.revisesThought, branchFromThought: data.branchFromThought, branchId: data.branchId, needsMoreThoughts: data.needsMoreThoughts, }; } recordThought(thoughtData) { this.thoughtHistory.push(thoughtData); const maxLength = this.config.historyMaxLength ?? 200; if (maxLength > 0 && this.thoughtHistory.length > maxLength) { const overflow = this.thoughtHistory.length - maxLength; this.thoughtHistory.splice(0, overflow); } } formatThought(thoughtData) { const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } = thoughtData; let prefix = ''; let context = ''; if (isRevision) { prefix = chalk.yellow(prompts.console.thoughtPrefix.revision); context = prompts.console.thoughtContext.revision.replace('{number}', String(revisesThought)); } else if (branchFromThought) { prefix = chalk.green(prompts.console.thoughtPrefix.branch); context = prompts.console.thoughtContext.branch .replace('{from}', String(branchFromThought)) .replace('{id}', branchId || ''); } else { prefix = chalk.blue(prompts.console.thoughtPrefix.default); context = ''; } const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`; const border = '\u2500'.repeat(Math.max(header.length, thought.length) + 4); return ` \u250c${border}\u2510 \u2502 ${header} \u2502 \u251c${border}\u2524 \u2502 ${thought.padEnd(border.length - 2)} \u2502 \u2514${border}\u2518`; } // Decide whether to append reference hint to a guidance message maybeAppendReferenceHint(base) { // Check for duplicate - don't add if @matrix already exists if (base.includes('@matrix')) { return base; } // Default to 'off' so presence of referenceHintMode enables behavior const modeSetting = (this.config.referenceHintMode || 'off'); const hintText = (this.config.referenceHintText || (prompts.messages && prompts.messages.referenceHintText) || '*Reference: @matrix[selection-guide] precision*'); if (modeSetting === 'off') return base; // Detect current framework mode const configuredMode = prompts?.config?.mode; const inferredMode = (this.config.commandExtension || '').toLowerCase() === '.toml' ? 'supergemini' : 'superclaude'; const currentMode = configuredMode || inferredMode; if (modeSetting === 'superclaude' && currentMode !== 'superclaude') return base; if (modeSetting === 'supergemini' && currentMode !== 'supergemini') return base; return `${base}\\n\\n${hintText}`; } processCommandSelection(selection) { let documentStatus = null; let documentContent; // Enhanced validation: Ensure selection object exists and has required structure if (!selection || typeof selection !== 'object') { throw new Error(prompts.errors.invalidSelectionType.replace('{type}', 'undefined')); } // Fix: Handle when Claude/Gemini sends command name as type instead of "command" if (selection && selection.type) { const rawType = String(selection.type).toLowerCase(); if (rawType !== "command" && rawType !== "skip" && rawType !== "skip_reason") { // If type is actually a command name, convert to proper format if (this.config.availableCommands && this.config.availableCommands.includes(rawType)) { selection = { type: "command", command: rawType }; } } } else if (selection?.command && !selection?.type) { // If type missing but command present, assume command type selection.type = 'command'; } // Final validation: Ensure type field exists after all processing if (!selection.type) { throw new Error(prompts.errors.invalidSelectionType.replace('{type}', 'undefined')); } switch (selection.type) { case "command": if (!selection.command) { throw new Error(prompts.errors.commandFieldRequired); } // Normalize and validate command name (security) // Light normalization: trim, lowercase, remove leading slashes (AI sometimes adds it) selection.command = selection.command .trim() .toLowerCase() .replace(/^\/+/, ''); // Smart parsing: Handle task[agent] format let suggestedAgent; // Strict pattern (no extra spaces) const taskAgentPattern = /^(task|spawn|workflow)\[([a-z0-9-]+)\]$/; // Tolerant pattern (allows spaces around tokens) const taskAgentPatternLoose = /^\s*(task|spawn|workflow)\s*\[\s*([a-z0-9-]+)\s*\]\s*$/; // Loose detector to guide when inner agent contains invalid chars const taskAgentDetector = /^\s*(task|spawn|workflow)\s*\[\s*([^\]]+)\s*\]\s*$/i; let match = selection.command.match(taskAgentPattern) || selection.command.match(taskAgentPatternLoose); if (match) { selection.command = match[1]; // Extract base command (task/spawn/workflow) suggestedAgent = match[2]; // Extract agent name // Store for later use in Step 3 via class property this.suggestedAgentFromCommand = suggestedAgent; } else { // If it looks like task[...], but agent contains invalid chars, guide instead of erroring const detector = selection.command.match(taskAgentDetector); if (detector) { // Interpret as base command only and provide guidance for Step 3 selection.command = detector[1].toLowerCase(); const rawAgent = (detector[2] || '').trim(); documentStatus = 'interpreted-command'; documentContent = `Interpreted as '${selection.command}'. Select agent at Step 3 using agentSelection { type: "agents", agents: ["..."] }. Hint: Allowed agent name characters: lowercase letters, numbers, hyphens. Received: '${rawAgent}'.`; // Do not suggest invalid agent; proceed with base command } } const safeNamePattern = /^[a-z0-9-]+$/; // Validate command exists in availableCommands list before processing if (this.config.availableCommands && this.config.availableCommands.length > 0) { const commandExists = this.config.availableCommands.includes(selection.command); if (!commandExists) { // Soft guidance instead of hard error: show available commands and ask to reselect const availableList = this.config.availableCommands.join(', '); documentStatus = 'unknown-command'; documentContent = this.maybeAppendReferenceHint(`Unknown command '${selection.command}'. Please choose within: ${availableList}`); // Do not mark selection as finalized; let caller prompt for correction return { documentStatus, documentContent }; } } else { // If no allow-list configured, enforce safe pattern to prevent path traversal if (!safeNamePattern.test(selection.command)) { // Cooperative guidance instead of hard error for invalid formats const cmd = selection.command; const looksLikeTool = /(^mcp__|__)/.test(cmd) || cmd.includes('tool') || cmd.includes('mcp'); const looksComposite = /[+,&]|\\s+/.test(cmd); if (looksComposite) { documentStatus = 'composite-command'; documentContent = this.maybeAppendReferenceHint(`Composite command detected: '${cmd}'. Step 2 expects single commands. You may select 1-3 commands by sending an array (commandSelection: [...]).`); return { documentStatus, documentContent }; } if (looksLikeTool) { documentStatus = 'tool-like-input'; documentContent = this.maybeAppendReferenceHint(`Tool-like input detected: '${cmd}'. Step 2 is for selecting a command document (e.g., 'task', 'spawn', 'workflow', 'bash'). Please select a command, then use tools in subsequent steps.`); return { documentStatus, documentContent }; } documentStatus = 'invalid-command-format'; documentContent = this.maybeAppendReferenceHint(`Invalid command format '${cmd}'. Expected lowercase letters, numbers, and hyphens. You can also provide an array of 1-3 commands.`); return { documentStatus, documentContent }; } } // Check for duplicate document read (SuperClaude Framework SSOT principle - Single Source of Truth) const commandKey = selection.command.toLowerCase(); if (this.readFiles.has(commandKey)) { // Document already read - return system reminder message documentStatus = "system-reminder"; documentContent = prompts.messages?.duplicateDocumentRead?.replace('{command}', selection.command) || `Document already read this session: ${selection.command}.md\\n\\nPlease refer to system-reminder content and apply that information to proceed with next step analysis.`; this.selectedCommand = commandKey; this.hasCommandBeenSelected = true; return { documentStatus, documentContent }; } this.selectedCommand = commandKey; this.hasCommandBeenSelected = true; const extension = this.config.commandExtension || '.md'; documentStatus = `${selection.command}${extension}`; // Use configured path and extension (safe join) const basePath = this.expandPath(this.config.commandPath || ''); const expandedFileToRead = join(basePath, `${selection.command}${extension}`); // Try to read the document content directly try { if (existsSync(expandedFileToRead)) { documentContent = readFileSync(expandedFileToRead, 'utf-8'); // Add to read files set (SSOT principle) this.readFiles.add(commandKey); // If TOML, extract agents for Step 3 suggestions const isToml = (this.config.commandExtension || '.md').toLowerCase() === '.toml' || expandedFileToRead.toLowerCase().endsWith('.toml'); if (isToml && typeof documentContent === 'string') { const extracted = this.parseAgentsFromToml(documentContent); this.extractedAgentsFromToml = extracted; this.extractedFromCommand = commandKey; } else { this.extractedAgentsFromToml = []; this.extractedFromCommand = null; } } else { // File doesn't exist - set error message with @matrix reference console.error(`Command document not found: ${expandedFileToRead}`); documentContent = `Error: Selected command document not found. Please refer to @matrix[selection-guide] to choose the correct command.`; } } catch (error) { console.error(`Failed to read command document ${expandedFileToRead}:`, error); // If file can't be read due to permissions or other errors documentContent = `Error: Selected command document not found. Please refer to @matrix[selection-guide] to choose the correct command.`; } break; case "skip": if (!selection.reason) { throw new Error(prompts.errors.reasonFieldRequired.replace('{type}', 'skip')); } this.selectedCommand = "skip"; this.hasCommandBeenSelected = true; documentStatus = "skip"; documentContent = `Skip reason: ${selection.reason}`; break; case "skip_reason": if (!selection.reason) { throw new Error(prompts.errors.reasonFieldRequired.replace('{type}', 'skip_reason')); } // Extract command from reason if it follows pattern const commandFromReason = selection.reason.match(/(\\w+)\\.md/); if (commandFromReason) { this.selectedCommand = commandFromReason[1].toLowerCase(); } this.hasCommandBeenSelected = true; documentStatus = "system-reminder"; documentContent = `System reminder: ${selection.reason}`; break; default: throw new Error(prompts.errors.invalidSelectionType.replace('{type}', selection.type)); } return { documentStatus, documentContent }; } processMultipleCommandSelections(selections) { // Aggregate behavior: read up to 3 documents, concatenate contents, respect duplicates and unknowns gracefully const maxDocs = 3; const picked = selections.slice(0, maxDocs); const pieces = []; let finalStatus = null; for (const sel of picked) { try { const { documentStatus, documentContent } = this.processCommandSelection(sel); // If any returns content, collect it if (documentContent) { // Label each block for clarity const label = sel.command ? sel.command : (documentStatus || 'document'); pieces.push(`### ${label}\\n${documentContent}`); } // Prefer a concrete status if present if (!finalStatus && documentStatus) finalStatus = documentStatus; } catch (error) { // Convert hard error from an individual item into soft guidance for the batch const message = error instanceof Error ? error.message : String(error); const label = sel.command || 'unknown'; pieces.push(`### ${label}\\n${message}`); if (!finalStatus) finalStatus = 'partial'; } } return { documentStatus: finalStatus || 'multi', documentContent: pieces.join('\\n\\n') }; } processAgentSelection(selection) { let documentStatus = null; let documentContent; switch (selection.type) { case "agents": if (!selection.agents || selection.agents.length === 0) { throw new Error("Agent selection type 'agents' requires an 'agents' array"); } // Validate against available agents and provide guidance on invalid ones { const available = new Set(this.resolveAvailableAgents()); const invalid = selection.agents .map(a => a.toLowerCase()) .filter(a => !available.has(a)); if (invalid.length > 0) { const availableList = Array.from(available).join(', ') || 'N/A'; documentStatus = "agents-invalid"; documentContent = `Invalid agents: ${invalid.join(', ')}\\n\\nPlease choose within: ${availableList}`; break; } } // If all selected agents already read, consolidate into system-reminder response { const selectedLower = selection.agents.map(a => a.toLowerCase()); const allAlreadyRead = selectedLower.every(a => this.readFiles.has(`agent-${a}`)); if (allAlreadyRead) { documentStatus = "system-reminder"; const selectedList = selection.agents.join(', '); const template = prompts.templates?.agentDuplicateReminder || prompts.messages?.useSystemReminderAgents || `Agent documents already read: ${selectedList}\\n\\nPlease refer to system-reminder content and apply agent perspectives to proceed.`; documentContent = template .replaceAll('{agents}', selectedList) .replaceAll('{agentList}', selectedList); break; } } documentStatus = "agents-selected"; { // Check if agent documents should be returned if (this.config.returnAgentDocuments === false) { // SuperClaude mode: Task tool command format const agentList = selection.agents.join(', '); if (selection.agents.length === 1) { documentContent = `**Agent Selection Complete**\\n\\nSelected agent: ${selection.agents[0]}. Use Task tool with ${selection.agents[0]} agent.`; } else { documentContent = `**Agent Selection Complete**\\n\\nSelected agents: ${agentList}. Use Task tool with appropriate agent based on task needs.`; } // Still mark as read for tracking for (const agent of selection.agents) { const agentKey = agent.toLowerCase(); const safeNamePattern = /^[a-z0-9-]+$/; if (safeNamePattern.test(agentKey)) { this.readFiles.add(`agent-${agentKey}`); } } } else { // SuperGemini mode: Return full agent document contents let agentContents = `**SuperGemini Agent Selection Complete**\\n\\n`; agentContents += `Selected agents: [${selection.agents.map(a => `"${a}"`).join(', ')}]\\n\\n`; for (const agent of selection.agents) { const agentKey = agent.toLowerCase(); const safeNamePattern = /^[a-z0-9-]+$/; if (!safeNamePattern.test(agentKey)) { continue; } if (this.readFiles.has(`agent-${agentKey}`)) { agentContents += `\\n### ${agent}.md (already read)\\nRefer to system-reminder content for this agent.\\n`; continue; } const agentsBase = this.expandPath(this.config.agentsPath || '~/.gemini/agents/'); const agentsExt = this.config.agentsExtension || '.md'; const agentPath = join(agentsBase, `${agent}${agentsExt}`); try { if (existsSync(agentPath)) { const content = readFileSync(agentPath, 'utf-8'); agentContents += `\\n### ${agent}.md\\n${content}\\n`; this.readFiles.add(`agent-${agentKey}`); } else { agentContents += `\\n### ${agent}.md\\nError: Agent file not found at ${agentPath}\\n`; } } catch (error) { agentContents += `\\n### ${agent}.md\\nError: Could not read agent file - ${error}\\n`; } } documentContent = agentContents; } } break; case "skip": if (!selection.reason) { throw new Error("Agent selection type 'skip' requires a 'reason' field"); } documentStatus = "skip"; documentContent = `Skip reason: ${selection.reason}`; break; case "skip_reason": if (!selection.reason) { throw new Error("Agent selection type 'skip_reason' requires a 'reason' field"); } documentStatus = "system-reminder"; documentContent = `System reminder: ${selection.reason}`; break; default: throw new Error(`Invalid agent selection type: ${selection.type}`); } return { documentStatus, documentContent }; } processStep1(thoughtData) { // STEP 1: Focus only on understanding user request // Optional command selection processing (not mandatory) let documentStatus = null; let documentContent; // Optional commandSelection processing for Step 1 // Wrap in try-catch to prevent errors in Step 1 (not mandatory step) if (thoughtData.commandSelection) { try { const result = Array.isArray(thoughtData.commandSelection) ? this.processMultipleCommandSelections(thoughtData.commandSelection) : this.processCommandSelection(thoughtData.commandSelection); documentStatus = result.documentStatus; documentContent = result.documentContent; } catch (error) { // In Step 1, command selection errors should not fail the step // Just log and continue without document if (!this.disableThoughtLogging) { console.error(`Step 1 optional command selection issue: ${error}`); } documentStatus = null; documentContent = undefined; } } this.recordThought(thoughtData); if (!this.disableThoughtLogging) { const formattedThought = this.formatThought(thoughtData); console.error(formattedThought); } const response = { thoughtNumber: 1, totalThoughts: thoughtData.totalThoughts, nextThoughtNeeded: thoughtData.nextThoughtNeeded, documentStatus }; // Add document content to response if available if (documentContent) { response.documentContent = documentContent; } const contents = [{ type: "text", text: JSON.stringify(response, null, 2) }]; return { content: contents }; } validateAndProcessStep(thoughtData, stepNumber) { let documentStatus = null; let documentContent; let forceNextThought = false; // Check if command selection is needed at this step const needsCommand = stepNumber >= this.config.requireSelectionByStep && !this.hasCommandBeenSelected; if (needsCommand && !thoughtData.commandSelection) { // Convert to soft guidance instead of hard error const availableList = this.config.availableCommands?.join(', ') || 'N/A'; const exampleCommand = (this.config.availableCommands && this.config.availableCommands.length > 0) ? this.config.availableCommands[0] : 'command-name'; documentStatus = 'command-required'; documentContent = this.maybeAppendReferenceHint(`Step 2 requires command selection. ` + `Available commands: ${availableList} ` + `Please provide: { "commandSelection": { "type": "command", "command": "${exampleCommand}" } }`); forceNextThought = true; // Return guidance instead of throwing error const response = { thoughtNumber: stepNumber, totalThoughts: thoughtData.totalThoughts, nextThoughtNeeded: true, documentStatus, documentContent }; return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] }; } // Process commandSelection if provided (single or multiple) if (thoughtData.commandSelection) { if (Array.isArray(thoughtData.commandSelection)) { const result = this.processMultipleCommandSelections(thoughtData.commandSelection); documentStatus = result.documentStatus; documentContent = result.documentContent; // If at least one valid command was selected, mark as selected this.hasCommandBeenSelected = true; } else { const result = this.processCommandSelection(thoughtData.commandSelection); documentStatus = result.documentStatus; documentContent = result.documentContent; } } this.recordThought(thoughtData); if (!this.disableThoughtLogging) { const formattedThought = this.formatThought(thoughtData); console.error(formattedThought); } const response = { thoughtNumber: stepNumber, totalThoughts: thoughtData.totalThoughts, nextThoughtNeeded: forceNextThought ? true : thoughtData.nextThoughtNeeded, documentStatus: documentStatus || null }; // Add document content to response if available if (documentContent) { response.documentContent = documentContent; } const contents = [{ type: "text", text: JSON.stringify(response, null, 2) }]; return { content: contents }; } processStep2(thoughtData) { return this.validateAndProcessStep(thoughtData, 2); } processStep3(thoughtData) { // STEP 3: Agent Persona Selection & Reading (same pattern as step 2) let documentStatus = null; let documentContent; let forceNextThought = false; let suggestedAgents; let availableAgentsForGuidance; let extractedFromCommandForGuidance; // Check if we have a suggested agent from Step 2's smart parsing if (this.suggestedAgentFromCommand && !thoughtData.agentSelection) { // Auto-inject agent selection if parsed from Step 2 thoughtData.agentSelection = { type: 'agents', agents: [this.suggestedAgentFromCommand] }; // Clear after use to prevent reuse in subsequent calls this.suggestedAgentFromCommand = undefined; } // Process agent selection based on configuration if (!this.config.step3RequiresAgentSelection) { // SuperClaude mode - recommend Task tool delegation instead of agent selection if (thoughtData.agentSelection) { const result = this.processAgentSelection(thoughtData.agentSelection); documentStatus = result.documentStatus; documentContent = result.documentContent; } else { documentStatus = 'superclaude_mode'; documentContent = prompts.messages?.superclaudeMode || `**SuperClaude Mode Active**\\n\\nTask delegation recommended: Use Task tool with specialized agents for complex operations requiring domain expertise.\\n\\nRefer to selection-guide.md @matrix coordinates for optimal command selection.`; } } else if (thoughtData.agentSelection) { // Agent selection provided - process it const result = this.processAgentSelection(thoughtData.agentSelection); documentStatus = result.documentStatus; documentContent = result.documentContent; if (documentStatus === 'agents-invalid') { forceNextThought = true; availableAgentsForGuidance = this.resolveAvailableAgents(); } } else { // Agent selection required but not provided if (this.extractedAgentsFromToml?.length > 0) { // Suggest agents from TOML const from = this.extractedFromCommand || this.selectedCommand || 'unknown'; const list = this.extractedAgentsFromToml.join(', '); documentStatus = "agents-suggested"; const extractedMsg = prompts.messages?.agentExtracted || `Agents extracted from {command}.toml: {agents}`; documentContent = extractedMsg.replace('{command}', from).replace('{agents}', list) + '\\n\\nPlease select agent personas at Step 3 by sending agentSelection { type: "agents", agents: [...] }.'; forceNextThought = true; suggestedAgents = [...this.extractedAgentsFromToml]; extractedFromCommandForGuidance = from; } else { // No agents found - show available agents const availableList = this.config.availableAgents?.join(', ') || 'N/A'; documentStatus = "agents-needed"; const msg = prompts.errors?.missingAgents || `No agents found in Step 3. Available agents: {availableAgents}`; documentContent = msg.replace('{availableAgents}', availableList); forceNextThought = true; availableAgentsForGuidance = this.resolveAvailableAgents(); } } this.thoughtHistory.push(thoughtData); if (!this.disableThoughtLogging) { const formattedThought = this.formatThought(thoughtData); console.error(formattedThought); } const response = { thoughtNumber: 3, totalThoughts: thoughtData.totalThoughts, nextThoughtNeeded: forceNextThought ? true : thoughtData.nextThoughtNeeded, documentStatus }; // Add document content to response if available if (documentContent) { response.documentContent = documentContent; } // Add machine-friendly guidance to help clients proceed reliably if (suggestedAgents && suggestedAgents.length > 0) { response.suggestedAgents = suggestedAgents; } if (availableAgentsForGuidance && availableAgentsForGuidance.length > 0) { response.availableAgents = availableAgentsForGuidance; } if (extractedFromCommandForGuidance) { response.extractedFromCommand = extractedFromCommandForGuidance; } if (forceNextThought) { response.requiredAction = 'select_agents'; } const contents = [{ type: "text", text: JSON.stringify(response, null, 2) }]; return { content: contents }; } processThought(input) { try { const validatedInput = this.validateThoughtData(input); // Respect client-provided totalThoughts (no forced override) // STEP 1: User Input Analysis Only (no command selection yet) if (validatedInput.thoughtNumber === 1) { return this.processStep1(validatedInput); } // STEP 2: MANDATORY Command/Document Selection if (validatedInput.thoughtNumber === 2) { return this.processStep2(validatedInput); } // STEP 3: Agent Persona Extraction & Reading if (validatedInput.thoughtNumber === 3) { return this.processStep3(validatedInput); } // STEP 4: Agent Embodiment & Problem Solving Execution if (validatedInput.thoughtNumber === 4) { return this.validateAndProcessStep(validatedInput, 4); } // Handle thoughts beyond 4 (expansion if needed) if (validatedInput.thoughtNumber > 4) { if (validatedInput.thoughtNumber >= validatedInput.totalThoughts && validatedInput.nextThoughtNeeded) { validatedInput.totalThoughts = validatedInput.thoughtNumber + 1; } return this.validateAndProcessStep(validatedInput, validatedInput.thoughtNumber); } // Fallback for unexpected thought numbers throw new Error(prompts.errors.unexpectedThoughtNumber.replace('{number}', String(validatedInput.thoughtNumber))); } catch (error) { // Simple error handling - just restart the same step const inputData = this.extractInputData(input); return { content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), status: 'failed', guidance: 'Refer to selection-guide.md @matrix for command selection', thoughtNumber: inputData.thoughtNumber, totalThoughts: inputData.totalThoughts, nextThoughtNeeded: true }, null, 2) }], isError: true }; } } extractInputData(input) { try { const inputData = input; return { thoughtNumber: inputData?.thoughtNumber || 1, totalThoughts: inputData?.totalThoughts || 4 }; } catch { return { thoughtNumber: 1, totalThoughts: 4 }; } } } const SUPERCLAUDE_THINKING_TOOL = { name: prompts?.tool?.name || 'chain_of_thought', description: prompts?.tool?.description || 'Structured thinking tool', inputSchema: { type: "object", properties: { thought: { type: "string", description: prompts.inputSchema?.thought?.description || 'Your current thinking step and analysis' }, nextThoughtNeeded: { type: "boolean", description: prompts.inputSchema?.nextThoughtNeeded?.description || 'Whether another thought step is needed' }, thoughtNumber: { type: "integer", description: prompts.inputSchema?.thoughtNumber?.description || 'Current step number', minimum: 1 }, totalThoughts: { type: "integer", description: prompts.inputSchema?.totalThoughts?.description || 'Estimated total steps', minimum: 1 }, commandSelection: { oneOf: [ { type: "object", description: prompts.inputSchema?.commandSelection?.description || 'Optional command or document selection at any step', properties: { type: { type: "string", enum: ["command", "skip", "skip_reason"], description: prompts.inputSchema?.commandSelection?.type?.description || 'Type of selection' }, command: { type: "string", description: prompts.inputSchema?.commandSelection?.command?.description || 'Command name' }, reason: { type: "string", description: prompts.inputSchema?.commandSelection?.reason?.description || 'Skip reason' } }, required: ["type"] }, { type: "array", description: 'You may select 1-3 commands. Provide an array to read multiple documents; duplicates will trigger system-reminders.', minItems: 1, maxItems: 3, items: { type: "object", properties: { type: { type: "string", enum: ["command", "skip", "skip_reason"] }, command: { type: "string" },