@eladtest/mcp
Version:
MCP server for shellfirm - provides interactive command validation with captcha
495 lines (490 loc) ⢠22.6 kB
JavaScript
;
/**
* 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