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
JavaScript
#!/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" },