crash-mcp
Version:
MCP server for CRASH - Cascaded Reasoning with Adaptive Step Handling
332 lines (331 loc) • 13.5 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { CRASH_TOOL } from './schema.js';
import { loadConfig } from './config.js';
import { CrashFormatter } from './formatter.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
const { name, version } = pkg;
// Create MCP server instance with tools capability
const server = new Server({
name,
version,
}, {
capabilities: {
tools: {},
},
});
class CrashServer {
constructor(config) {
this.branches = new Map();
this.sessions = new Map();
this.config = config;
this.formatter = new CrashFormatter(config.display.colorOutput);
this.history = this.createNewHistory();
this.startTime = Date.now();
}
createNewHistory() {
return {
steps: [],
branches: [],
completed: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
metadata: {
total_duration_ms: 0,
revisions_count: 0,
branches_created: 0,
tools_used: [],
}
};
}
validateThoughtPrefix(thought) {
if (!this.config.validation.requireThoughtPrefix) {
return true; // Skip validation if not required
}
const validPrefixes = [
'OK, I ',
'But ',
'Wait ',
'Therefore ',
'I see the issue now. ',
'I have completed '
];
return validPrefixes.some(prefix => thought.startsWith(prefix));
}
validateRationale(rationale) {
if (!this.config.validation.requireRationalePrefix) {
return true; // Skip validation if not required
}
return rationale.startsWith('To ');
}
validatePurpose(purpose) {
if (this.config.validation.allowCustomPurpose) {
return true; // Any string is valid
}
const validPurposes = [
'analysis', 'action', 'reflection', 'decision', 'summary',
'validation', 'exploration', 'hypothesis', 'correction', 'planning'
];
return validPurposes.includes(purpose.toLowerCase());
}
extractToolsUsed(step) {
const tools = [];
// From explicit tools_used field
if (step.tools_used) {
tools.push(...step.tools_used);
}
// From structured action
if (typeof step.next_action === 'object' && step.next_action.tool) {
tools.push(step.next_action.tool);
}
return [...new Set(tools)]; // Remove duplicates
}
handleRevision(step) {
if (step.revises_step && this.config.features.enableRevisions) {
// Mark the original step as revised
const originalStep = this.history.steps.find(s => s.step_number === step.revises_step);
if (originalStep) {
console.error(`📝 Revising step ${step.revises_step}: ${step.revision_reason || 'No reason provided'}`);
}
if (this.history.metadata) {
this.history.metadata.revisions_count = (this.history.metadata.revisions_count || 0) + 1;
}
}
}
handleBranching(step) {
if (step.branch_from && this.config.features.enableBranching) {
const branchId = step.branch_id || `branch-${Date.now()}`;
const branchName = step.branch_name || `Alternative ${this.branches.size + 1}`;
if (!this.branches.has(branchId)) {
const newBranch = {
id: branchId,
name: branchName,
from_step: step.branch_from,
steps: [],
status: 'active',
created_at: new Date().toISOString()
};
this.branches.set(branchId, newBranch);
this.history.branches = this.history.branches || [];
this.history.branches.push(newBranch);
if (this.history.metadata) {
this.history.metadata.branches_created = (this.history.metadata.branches_created || 0) + 1;
}
console.error(`🌿 Created branch "${branchName}" from step ${step.branch_from}`);
}
// Add step to branch
const branch = this.branches.get(branchId);
if (branch) {
step.branch_id = branchId;
branch.steps.push(step);
}
}
}
formatOutput(step) {
switch (this.config.display.outputFormat) {
case 'json':
return this.formatter.formatStepJSON(step);
case 'markdown':
return this.formatter.formatStepMarkdown(step);
default:
return this.formatter.formatStepConsole(step);
}
}
async processStep(input) {
try {
const step = input;
const stepStartTime = Date.now();
// Validate required fields
if (!step.step_number || !step.estimated_total || !step.purpose ||
!step.context || !step.thought || !step.outcome ||
!step.next_action || !step.rationale) {
throw new Error('Missing required fields');
}
// Apply strict mode if enabled
if (this.config.validation.strictMode) {
if (!this.validateThoughtPrefix(step.thought)) {
throw new Error('Thought must start with one of the required phrases (strict mode)');
}
if (!this.validateRationale(step.rationale)) {
throw new Error('Rationale must start with "To " (strict mode)');
}
if (!this.validatePurpose(step.purpose)) {
throw new Error(`Invalid purpose: ${step.purpose} (strict mode)`);
}
}
else {
// Flexible validation
if (!this.validatePurpose(step.purpose)) {
console.error(`⚠️ Using custom purpose: ${step.purpose}`);
}
}
// Add timestamp and duration
step.timestamp = new Date().toISOString();
// Handle session management
if (step.session_id && this.config.features.enableSessions) {
if (!this.sessions.has(step.session_id)) {
this.sessions.set(step.session_id, this.createNewHistory());
}
this.history = this.sessions.get(step.session_id);
}
// Check if completed
const completionPhrases = ['I have completed', 'Task completed', 'Solution found'];
if (completionPhrases.some(phrase => step.thought.toLowerCase().includes(phrase.toLowerCase()))) {
this.history.completed = true;
}
// Handle special features
this.handleRevision(step);
this.handleBranching(step);
// Track tools used
const toolsUsed = this.extractToolsUsed(step);
if (toolsUsed.length > 0 && this.history.metadata) {
this.history.metadata.tools_used = [
...new Set([...(this.history.metadata.tools_used || []), ...toolsUsed])
];
}
// Add to history
this.history.steps.push(step);
// Update metadata
if (this.history.metadata) {
step.duration_ms = Date.now() - stepStartTime;
this.history.metadata.total_duration_ms = Date.now() - this.startTime;
}
this.history.updated_at = new Date().toISOString();
// Trim history if needed
if (this.history.steps.length > this.config.system.maxHistorySize) {
this.history.steps = this.history.steps.slice(-this.config.system.maxHistorySize);
console.error(`History trimmed to ${this.config.system.maxHistorySize} steps`);
}
// Display formatted step
const formattedOutput = this.formatOutput(step);
console.error(formattedOutput);
// Show confidence warning if low
if (step.confidence !== undefined && step.confidence < 0.5) {
console.error(`⚠️ Low confidence (${Math.round(step.confidence * 100)}%): ${step.uncertainty_notes || 'Consider verification'}`);
}
// Prepare response
const responseData = {
step_number: step.step_number,
estimated_total: step.estimated_total,
completed: this.history.completed,
total_steps: this.history.steps.length,
next_action: step.next_action,
};
// Add optional response fields
if (step.confidence !== undefined) {
responseData.confidence = step.confidence;
}
if (step.revises_step) {
responseData.revised_step = step.revises_step;
}
if (step.branch_id) {
responseData.branch = {
id: step.branch_id,
name: step.branch_name,
from: step.branch_from
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(responseData, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
status: 'failed',
hint: this.config.validation.strictMode
? 'Strict mode is enabled. Set CRASH_STRICT_MODE=false for flexible validation.'
: 'Check that all required fields are provided.',
}, null, 2),
},
],
isError: true,
};
}
}
clearHistory() {
this.history = this.createNewHistory();
this.branches.clear();
this.startTime = Date.now();
console.error('🔄 CRASH history cleared');
}
getHistorySummary() {
return this.formatter.formatHistorySummary(this.history);
}
getBranchTree() {
return this.formatter.formatBranchTree(this.history);
}
exportHistory(format = 'json') {
switch (format) {
case 'markdown':
return this.history.steps.map(step => this.formatter.formatStepMarkdown(step)).join('\n\n');
case 'text':
return this.history.steps.map(step => this.formatter.formatStepConsole(step)).join('\n\n');
default:
return JSON.stringify(this.history, null, 2);
}
}
}
// Load configuration
const config = loadConfig();
// Show configuration on startup
console.error('🚀 CRASH MCP Server Starting...');
console.error(`📋 Configuration:`);
console.error(` - Strict Mode: ${config.validation.strictMode}`);
console.error(` - Revisions: ${config.features.enableRevisions ? 'Enabled' : 'Disabled'}`);
console.error(` - Branching: ${config.features.enableBranching ? 'Enabled' : 'Disabled'}`);
console.error(` - Confidence Tracking: ${config.features.enableConfidence ? 'Enabled' : 'Disabled'}`);
console.error(` - Output Format: ${config.display.outputFormat}`);
console.error(` - Max History: ${config.system.maxHistorySize} steps`);
const crashServer = new CrashServer(config);
// Expose tool
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [CRASH_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'crash') {
return crashServer.processStep(request.params.arguments);
}
return {
content: [
{
type: 'text',
text: `Unknown tool: ${request.params.name}`,
},
],
isError: true,
};
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('✅ CRASH MCP Server running on stdio');
console.error('🧠 CRASH - Cascaded Reasoning with Adaptive Step Handling v2.0');
console.error('📚 Use "crash" tool for structured reasoning');
if (config.validation.strictMode) {
console.error('⚠️ Running in STRICT MODE - legacy behavior enforced');
}
else {
console.error('🎯 Running in FLEXIBLE MODE - enhanced features available');
}
}
runServer().catch((error) => {
console.error('Fatal error running server:', error);
process.exit(1);
});