The Mandrel MCP Test Harness needs a JavaScriptEngine to execute JavaScript validation scripts alongside the existing Lua engine. This will enable testing MCP servers with JavaScript-based validation logic and expand the scripting capabilities of the test harness.
Existing Infrastructure:
- ✅ ScriptConfig, ScriptContext, ScriptResult, ScriptError types in types.rs
- ✅ Memory tracking system implemented and working
- ✅ LuaEngine as reference implementation with all features
- ✅ QuickJS dependency available (rquickjs) with basic tests in mod.rs
- ✅ Sandbox and security infrastructure available
Missing Implementation:
- ❌ JavaScriptEngine struct and implementation
- ❌ JavaScript context injection
- ❌ JavaScript-specific error handling and mapping
- ❌ Integration with existing timeout and memory tracking
- ❌ Comprehensive test coverage
QuickJS (via rquickjs):
- Lightweight, fast JavaScript engine
- Good sandboxing capabilities
- No Node.js dependencies - pure JS execution
- Suitable for validation scripts and testing logic
/// JavaScript execution engine using QuickJS
pub struct JavaScriptEngine {
runtime: rquickjs::Runtime,
config: ScriptConfig,
}
/// Precompiled JavaScript script (future enhancement)
pub struct JavaScriptScript {
source: String,
function_name: Option<String>, // For callable scripts
}impl JavaScriptEngine {
/// Create new JavaScript engine with configuration
pub fn new(config: &ScriptConfig) -> Result<Self, ScriptError>;
/// Execute JavaScript code with context injection
pub async fn execute_script(
&self,
script: &str,
context: ScriptContext,
) -> Result<ScriptResult, ScriptError>;
/// Precompile JavaScript for future execution (placeholder)
pub fn precompile_script(
&self,
script: &str,
function_name: Option<String>,
) -> Result<JavaScriptScript, ScriptError>;
/// Execute precompiled JavaScript script
pub async fn execute_precompiled(
&self,
script: &JavaScriptScript,
context: ScriptContext,
) -> Result<ScriptResult, ScriptError>;
}JavaScript Context Object:
// Injected global object 'context'
{
request: { /* MCP request data */ },
response: { /* MCP response data */ },
test_case: "test_case_name",
tool: "tool_name",
metadata: {
server_info: { /* server details */ },
execution_id: "uuid",
timestamp: "2025-01-20T10:30:00Z"
},
// Utility functions
log: function(level, message) { /* logging */ },
assert: function(condition, message) { /* assertions */ }
}Example Script:
// Access injected context
if (context.response.error) {
context.log('error', 'Unexpected error in response');
return { success: false, error: 'Response contains error' };
}
context.assert(context.response.result, 'Response must have result field');
return {
success: true,
message: `Tool ${context.tool} executed successfully`,
result_type: typeof context.response.result
};JavaScript Error Mapping:
fn map_js_error(js_error: rquickjs::Error) -> ScriptError {
match js_error {
rquickjs::Error::Exception => {
// Extract JS exception details
ScriptError::RuntimeError { message: extract_js_exception() }
}
rquickjs::Error::Syntax => {
ScriptError::SyntaxError {
message: extract_syntax_error(),
line: extract_line_number()
}
}
_ => ScriptError::ExecutionError { message: js_error.to_string() }
}
}Security Features:
- No file system access by default
- No network access (unless explicitly enabled)
- Memory limits via existing ResourceMonitor
- Execution timeout via tokio::time::timeout
- No access to dangerous JavaScript APIs
Implementation:
impl JavaScriptEngine {
fn create_secure_context(&self) -> Result<rquickjs::Context, ScriptError> {
let context = rquickjs::Context::full(&self.runtime)?;
// Disable dangerous globals
context.with(|ctx| {
// Remove or restrict dangerous APIs
ctx.globals().delete("eval")?;
ctx.globals().delete("Function")?;
// Add safe utility functions
self.inject_safe_utilities(ctx)?;
Ok(())
})?;
Ok(context)
}
}Memory Tracking Integration:
pub async fn execute_script(
&self,
script: &str,
context: ScriptContext,
) -> Result<ScriptResult, ScriptError> {
let start_time = Instant::now();
// Initialize memory tracking
let memory_tracker = MemoryTracker::new(MemoryTrackingConfig::default());
let memory_before = memory_tracker.snapshot()?;
// Create secure context and execute
let js_context = self.create_secure_context()?;
let result = js_context.with(|ctx| {
self.inject_context(ctx, &context)?;
self.execute_with_timeout(ctx, script)
});
// Calculate metrics
let memory_after = memory_tracker.snapshot()?;
let memory_delta = memory_tracker.calculate_delta(&memory_before, &memory_after);
let memory_used_mb = memory_tracker.delta_to_mb(&memory_delta);
let duration_ms = start_time.elapsed().as_millis() as u64;
// Process result with metrics
self.build_script_result(result, duration_ms, Some(memory_used_mb))
}- Create JavaScriptEngine struct with QuickJS runtime
- Implement basic script execution without context injection
- Add timeout and error handling following Lua engine patterns
- Basic tests for script execution and error scenarios
- Implement context injection with MCP data
- Add utility functions (log, assert) for script convenience
- Integrate memory tracking and performance monitoring
- Comprehensive error mapping from QuickJS to ScriptError
- Implement precompile_script (basic source storage)
- Add execute_precompiled method
- Performance benchmarks comparing with Lua engine
- Comprehensive test suite covering all functionality
- Security hardening - remove dangerous JavaScript APIs
- Resource limiting integration with existing sandbox
- Validation scripts for real MCP testing scenarios
- Integration tests with actual MCP servers
#[tokio::test]
async fn test_js_simple_script_execution() {
let engine = JavaScriptEngine::new(&ScriptConfig::new()).unwrap();
let context = create_test_context();
let script = r#"
return {
success: true,
message: "Hello from JavaScript",
input_type: typeof context.request
};
"#;
let result = engine.execute_script(script, context).await.unwrap();
assert!(result.success);
assert!(result.duration_ms > 0);
}
#[tokio::test]
async fn test_js_context_injection() {
let engine = JavaScriptEngine::new(&ScriptConfig::new()).unwrap();
let context = create_test_context_with_data();
let script = r#"
if (!context.request || !context.response) {
throw new Error("Context not properly injected");
}
return {
success: true,
test_case: context.test_case,
tool: context.tool
};
"#;
let result = engine.execute_script(script, context).await.unwrap();
assert!(result.success);
}
#[tokio::test]
async fn test_js_error_handling() {
let engine = JavaScriptEngine::new(&ScriptConfig::new()).unwrap();
let context = create_test_context();
let script = "throw new Error('Test error');";
let result = engine.execute_script(script, context).await.unwrap();
assert!(!result.success);
assert!(result.error.is_some());
}
#[tokio::test]
async fn test_js_timeout_handling() {
let mut config = ScriptConfig::new();
config.timeout_ms = 100; // Very short timeout
let engine = JavaScriptEngine::new(&config).unwrap();
let context = create_test_context();
let script = "while(true) { /* infinite loop */ }";
let result = engine.execute_script(script, context).await.unwrap();
assert!(!result.success);
assert!(matches!(result.error, Some(ScriptError::TimeoutError { .. })));
}#[tokio::test]
async fn test_js_mcp_validation_script() {
let engine = JavaScriptEngine::new(&ScriptConfig::new()).unwrap();
// Real MCP server response validation
let context = ScriptContext::new(
json!({"tool": "list_files", "arguments": {"path": "/tmp"}}),
json!({"content": [{"type": "text", "text": "file1.txt\nfile2.txt"}]}),
"test_mcp_list_files".to_string(),
"list_files".to_string(),
config,
);
let validation_script = r#"
// Validate MCP response structure
if (!context.response.content) {
return { success: false, error: "Missing content field" };
}
if (!Array.isArray(context.response.content)) {
return { success: false, error: "Content must be array" };
}
const textContent = context.response.content.find(c => c.type === 'text');
if (!textContent) {
return { success: false, error: "No text content found" };
}
const lines = textContent.text.split('\n').filter(l => l.trim());
context.log('info', `Found ${lines.length} files`);
return {
success: true,
files_count: lines.length,
files: lines
};
"#;
let result = engine.execute_script(validation_script, context).await.unwrap();
assert!(result.success);
}#[tokio::test]
async fn test_js_performance_requirements() {
let engine = JavaScriptEngine::new(&ScriptConfig::new()).unwrap();
let context = create_test_context();
let script = r#"
// Simple computation
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += i;
}
return { success: true, sum: sum };
"#;
let start = Instant::now();
let result = engine.execute_script(script, context).await.unwrap();
let duration = start.elapsed();
assert!(result.success);
assert!(duration.as_millis() < 100, "JS execution should be <100ms");
assert!(result.memory_used_mb.unwrap_or(0.0) < 10.0, "Memory usage should be <10MB");
}- ✅ Basic Execution: JavaScript scripts execute successfully with QuickJS
- ✅ Context Injection: Scripts can access request/response data and metadata
- ✅ Error Handling: Proper error mapping from JS exceptions to ScriptError
- ✅ Timeout Support: Scripts respect timeout configuration
- ✅ Memory Tracking: Integration with existing memory monitoring
- ✅ Test Coverage: 90%+ test coverage for all JavaScript engine functionality
- ✅ Performance: <100ms execution for typical validation scripts
- ✅ Memory Efficiency: <10MB memory usage for standard scripts
- ✅ Security: Safe execution without access to dangerous APIs
- ✅ API Compatibility: Same interface patterns as LuaEngine
- ✅ Configuration: Uses existing ScriptConfig infrastructure
- ✅ Monitoring: Integrates with ResourceMonitor and memory tracking
- ✅ Error Reporting: Consistent error handling across engines
- Node.js Integration: Optional Node.js subprocess execution for npm packages
- Module System: Support for JavaScript modules and imports
- Bytecode Compilation: QuickJS bytecode precompilation for performance
- Debugging Support: Source maps and debugging capabilities
- TypeScript Support: TypeScript script compilation and execution
- Async/Await Support: Full async JavaScript execution patterns
- Web APIs: Selective Web API support (fetch, setTimeout) for testing
- ES6+ Features: Modern JavaScript feature support verification
- Performance Profiling: Detailed execution profiling and optimization
Implementation Priority: High - Required for issue #252 Dependencies: QuickJS (rquickjs), existing script engine infrastructure Estimated Effort: 2-3 days for full implementation with comprehensive testing