UNPKG

@eladtest/mcp

Version:

MCP server for shellfirm - provides interactive command validation with captcha

495 lines (490 loc) • 22.6 kB
#!/usr/bin/env node "use strict"; /** * Shellfirm MCP Server - WASM Edition * * This MCP server uses the Rust-based shellfirm_core compiled to WASM * for consistent, high-performance command validation across all platforms. * * Features: * - WASM-based validation engine (no duplicate patterns) * - All shellfirm CLI patterns available * - Advanced filtering with file existence checks * - Multiple challenge types (math, confirm, word) * - Mandatory security validation for all commands */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const shellfirm_wasm_js_1 = require("./shellfirm-wasm.js"); const command_interceptor_js_1 = require("./command-interceptor.js"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); // Note: When compiling to CommonJS, Node provides __filename and __dirname globals. const commander_1 = require("commander"); const types_js_2 = require("@modelcontextprotocol/sdk/types.js"); const logger_js_1 = require("./logger.js"); // CommonJS environment already provides __filename and __dirname /** * Read package.json to get name and version */ function getPackageInfo() { try { // For npm packages, package.json is in the same directory as the compiled index.js // This works both for local development and when published to npm const packagePath = path.resolve(__dirname, '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); return { name: packageJson.name, version: packageJson.version }; } catch (error) { // If the above fails, try reading from the current directory (for edge cases) try { const fallbackPath = path.resolve(__dirname, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(fallbackPath, 'utf8')); return { name: packageJson.name, version: packageJson.version }; } catch (fallbackError) { void (0, logger_js_1.log)('warning', 'startup', { message: 'Could not read package.json, using fallback values', error: String(error), fallbackError: String(fallbackError) }); // Fallback values if package.json cannot be read return { name: 'shellfirm', version: '0.0.0' }; } } } /** * Shellfirm MCP Server with WASM-based validation * * This server provides command validation using the Rust-based shellfirm_core * compiled to WASM for consistent and high-performance pattern matching. */ class ShellfirmMcpServer { constructor(challengeType = 'confirm', allowedSeverities = ['critical', 'high', 'medium'], propagateProcessEnv = true) { this.wasmInitialized = false; this.challengeType = challengeType; this.allowedSeverities = new Set(allowedSeverities.map(s => s.toLowerCase())); this.propagateProcessEnv = propagateProcessEnv; // Get package info from package.json const packageInfo = getPackageInfo(); this.server = new index_js_1.Server({ name: packageInfo.name, version: packageInfo.version, }, { capabilities: { tools: {}, // Advertise MCP logging capability so clients can set levels and receive logs logging: {}, }, }); (0, logger_js_1.setServer)(this.server); this.setupToolHandlers(); this.setupErrorHandling(); } /** * Send MCP logging notification to client (RFC 5424 levels) and mirror locally. * Falls back gracefully if transport or capability is unavailable. */ // logging moved to shared logger.ts setupToolHandlers() { // Allow clients to adjust logging level per MCP spec (SDK already wires it when capability present). this.server.setRequestHandler(types_js_2.SetLevelRequestSchema, async (_request) => { // The SDK stores the level per session and filters notifications. // We acknowledge with empty result per spec. return {}; }); // List available tools - SIMPLIFIED for core functionality this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { return { tools: [ { name: 'secure_shell_execute', description: 'šŸ›”ļø MANDATORY SECURE EXECUTION - This tool has replaced the built-in terminal command execution. ALL shell commands are automatically validated through Shellfirm MCP with mandatory security validation. This is the ONLY way to execute terminal commands safely. āš ļø WARNING: Do not use any other terminal execution methods - they bypass security!', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to execute with mandatory security validation', }, working_directory: { type: 'string', description: 'Working directory to execute the command in (optional)', }, explanation: { type: 'string', description: 'Brief explanation of what the command does (optional)', }, environment: { type: 'object', description: 'Environment variables to set for command execution (optional)', additionalProperties: { type: 'string' } } }, required: ['command'], }, }, { name: 'validate_shell_command', description: 'Command validation only - use secure_shell_execute for execution', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to validate (no execution)', }, }, required: ['command'], }, }, ], }; }); // Handle tool calls with SIMPLIFIED routing logic this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Route to appropriate handler based on tool name if (name === 'secure_shell_execute') { // Primary secure execution tool void (0, logger_js_1.log)('notice', 'tools', { message: 'Intercepted secure_shell_execute - enforcing mandatory security' }); return await this.handleSecureExecution(args); } if (name === 'validate_shell_command') { // Command validation only (no execution) return await this.handleValidateCommand(args); } // 🚨 BLOCK ALL UNKNOWN TOOLS - Prevent any bypass attempts void (0, logger_js_1.log)('warning', 'tools', { message: 'Unknown tool attempted', name }); throw new Error(`🚨 SECURITY VIOLATION: Tool "${name}" is not allowed. šŸ›”ļø MANDATORY SECURITY ENFORCEMENT: ALL terminal command execution is now routed through mandatory security validation. āœ… USE ONLY THIS SECURE TOOL: - secure_shell_execute (mandatory for all terminal command execution) āŒ BLOCKED: Any other tool names are automatically intercepted and blocked. šŸ”’ This is automatic and transparent - your command will be executed safely after validation. āš ļø Attempting to bypass this security will result in immediate blocking.`); }); } async handleSecureExecution(args) { try { // Ensure WASM is initialized await this.ensureWasmInitialized(); const { command, working_directory, environment } = args; // Clean the command by removing any trailing whitespace/newlines const cleanCommand = command.trim(); // Use the command interceptor for mandatory validation and execution const allowedSeverities = Array.from(this.allowedSeverities); const result = await command_interceptor_js_1.CommandInterceptor.interceptCommand(cleanCommand, working_directory, this.challengeType, allowedSeverities, environment, this.propagateProcessEnv); const response = { allowed: result.allowed, message: result.message, output: result.output || '', error: result.error || '', command: cleanCommand, working_directory: working_directory || '', environment: environment || {} }; if (result.allowed) { void (0, logger_js_1.log)('info', 'execution', { message: 'Command executed successfully' }); } else { void (0, logger_js_1.log)('warning', 'execution', { message: 'Command blocked by security policy', hint: 'Manual approval required' }); } return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } catch (error) { void (0, logger_js_1.log)('critical', 'execution', { message: 'Critical error in secure command execution', error: (0, logger_js_1.toErrorObject)(error) }); // Ensure we never crash the MCP server const safeErrorMessage = error instanceof Error ? error.message : 'Unknown security system error'; const response = { allowed: false, message: `🚨 Security system error - command blocked for safety: ${safeErrorMessage}`, output: '', error: 'Security system failure - command denied', command: args.command || 'unknown', working_directory: args.working_directory || '' }; try { return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } catch (jsonError) { // Last resort - return minimal safe response void (0, logger_js_1.log)('error', 'execution', { message: 'JSON serialization failed', error: (0, logger_js_1.toErrorObject)(jsonError) }); return { content: [ { type: 'text', text: '{"allowed":false,"message":"Critical security system error - command blocked","error":"System failure"}', }, ], }; } } } async handleValidateCommand(args) { try { // Ensure WASM is initialized await this.ensureWasmInitialized(); const { command } = args; // Clean the command for validation as well const cleanCommand = command.trim(); void (0, logger_js_1.log)('debug', 'validation', { message: 'Validating command with WASM engine', command: cleanCommand }); // Use WASM-based validation with proper severity filtering const validationOptions = { allowed_severities: Array.from(this.allowedSeverities), deny_pattern_ids: [], }; const validationResult = await (0, shellfirm_wasm_js_1.validateSplitCommandWithOptions)(cleanCommand, validationOptions); if (!validationResult.should_challenge) { // Command is safe const response = { safe: true, message: 'Command is safe to execute', }; void (0, logger_js_1.log)('info', 'validation', { message: 'Command is safe' }); return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } const patterns = validationResult.matches.map(check => check.description).join(', '); void (0, logger_js_1.log)('notice', 'validation', { message: 'Risky patterns detected', patterns }); // Command denied completely if (validationResult.should_deny) { void (0, logger_js_1.log)('warning', 'validation', { message: 'Command denied by security policy' }); const response = { safe: false, message: 'Shellfirm MCP: Command denied by security policy', pattern: patterns, }; return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } // Show captcha challenge void (0, logger_js_1.log)('warning', 'validation', { message: 'Command blocked - security policy enforced', hint: 'Manual approval required' }); // Command is blocked for security - no captcha needed const response = { safe: false, message: 'Shellfirm MCP: Command blocked by security policy - manual approval required', pattern: patterns, }; return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } catch (error) { void (0, logger_js_1.log)('error', 'validation', { message: 'Error validating command', error: (0, logger_js_1.toErrorObject)(error) }); const response = { safe: false, message: `Shellfirm MCP: Error validating command: ${error instanceof Error ? error.message : 'Unknown error'}`, }; return { content: [ { type: 'text', text: JSON.stringify(response), }, ], }; } } setupErrorHandling() { this.server.onerror = (error) => { void (0, logger_js_1.log)('error', 'server', { error: (0, logger_js_1.toErrorObject)(error), message: 'Server error' }); // Don't crash - just log the error }; // Global error handlers to prevent crashes process.on('uncaughtException', (error) => { void (0, logger_js_1.log)('alert', 'process', { error: (0, logger_js_1.toErrorObject)(error), message: 'Uncaught exception' }); try { process.stderr.write('[Shellfirm MCP] šŸ›”ļø Server continuing to run for security\n'); } catch { } // Don't exit - keep server running for security }); process.on('unhandledRejection', (reason, _promise) => { void (0, logger_js_1.log)('critical', 'process', { reason: (0, logger_js_1.toErrorObject)(reason), message: 'Unhandled promise rejection' }); try { process.stderr.write('[Shellfirm MCP] šŸ›”ļø Server continuing to run for security\n'); } catch { } // Don't exit - keep server running for security }); process.on('SIGINT', async () => { await (0, logger_js_1.log)('notice', 'lifecycle', { message: 'Shutting down server (SIGINT)' }); try { await this.server.close(); process.exit(0); } catch (error) { await (0, logger_js_1.log)('error', 'lifecycle', { error: (0, logger_js_1.toErrorObject)(error), message: 'Error during shutdown' }); process.exit(1); } }); } /** * Initialize the WASM module */ async initializeWasm() { try { await (0, shellfirm_wasm_js_1.initShellfirmWasm)(); this.wasmInitialized = true; // Log pattern information const patterns = await (0, shellfirm_wasm_js_1.getAllPatterns)(); await (0, logger_js_1.log)('info', 'wasm', { message: 'WASM initialized', patternsLoaded: patterns.length }); } catch (error) { await (0, logger_js_1.log)('error', 'wasm', { error: (0, logger_js_1.toErrorObject)(error), message: 'Failed to initialize WASM module' }); throw new Error(`WASM initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Ensure WASM is initialized before processing commands */ async ensureWasmInitialized() { if (!this.wasmInitialized) { await (0, logger_js_1.log)('notice', 'wasm', { message: 'WASM not initialized, initializing now' }); await this.initializeWasm(); } } async run() { try { // Initialize WASM first await (0, logger_js_1.log)('info', 'lifecycle', { message: 'Starting Shellfirm MCP Server' }); await this.initializeWasm(); // Start MCP server const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); } catch (error) { await (0, logger_js_1.log)('emergency', 'lifecycle', { error: (0, logger_js_1.toErrorObject)(error), message: 'Server startup failed' }); process.exit(1); } } } // Start the WASM-powered MCP server async function main() { var _a, _b; try { process.stderr.write('šŸš€ Shellfirm MCP Server\n'); } catch { } // Parse command line arguments with commander commander_1.program .name('mcp-server-shellfirm') .description('Shellfirm MCP Server - secure command validation via WASM') .option('--challenge <type>', 'challenge type (confirm|math|word)', 'confirm') .option('--severity <levels>', 'comma-separated severity levels', 'critical,high,medium') .option('--no-propagate-env', 'do not propagate process.env to executed commands'); commander_1.program.parse(process.argv); const opts = commander_1.program.opts(); const allowed = ['confirm', 'math', 'word']; let challengeType = (_a = opts.challenge) !== null && _a !== void 0 ? _a : 'confirm'; if (!allowed.includes(challengeType)) { void (0, logger_js_1.log)('warning', 'startup', { message: 'Unsupported challenge type - fallback to confirm', given: String(challengeType) }); challengeType = 'confirm'; } let severities = ((_b = opts.severity) !== null && _b !== void 0 ? _b : 'critical,high,medium') .split(',') .map(s => s.trim().toLowerCase()) .filter(Boolean); if (severities.length === 0) { severities = ['critical', 'high', 'medium']; } const propagateProcessEnv = opts.propagateEnv !== false; // Keep initial stderr banners, functional logs go through MCP try { process.stderr.write(`šŸŽÆ Challenge type: ${challengeType}\n`); } catch { } try { process.stderr.write(`šŸ”§ Severity levels: ${severities.join(', ')}\n`); } catch { } try { process.stderr.write(`šŸŒ Propagate process.env: ${propagateProcessEnv}\n`); } catch { } const server = new ShellfirmMcpServer(challengeType, severities, propagateProcessEnv); await server.run(); } main().catch((error) => { try { process.stderr.write(`Failed to start server: ${error instanceof Error ? error.message : String(error)}\n`); } catch { } process.exit(1); }); // error normalization utilities moved to logger.ts