Currently, the ValidationEngine supports JSONPath validation, schema validation, and custom validators, but lacks support for script-based validation. While we have completed the LuaEngine implementation (Issue #254), it's not integrated into the validation pipeline. We need to wire script execution into the validation system to support before/after validation phases as defined in YAML test specifications.
- McpValidationEngine: Main orchestrator with pluggable CustomValidator system
- ValidationSpec: Defines validation rules (JSONPath, schema, protocol, custom)
- CustomValidator trait: Interface for custom validation logic
- ValidationContext: Provides execution metadata (method, request_id, server_capabilities, test_metadata)
- LuaEngine: Complete implementation with context injection, sandboxing, and timeout enforcement
- ScriptContext: Provides request, response, and metadata to scripts
- ScriptResult: Returns success status, output, logs, duration, and memory usage
- ValidationScript: Defined in spec with
name,language,execution_phase,required,source - Test case integration: Can reference validation scripts by name via
validation_scriptsfield - Execution phases: Support for "before" and "after" validation phases
// New ScriptValidator implementing CustomValidator trait
pub struct ScriptValidator {
lua_engine: LuaEngine,
validation_scripts: HashMap<String, ValidationScript>,
execution_phase: ScriptExecutionPhase,
}
// Integration into ValidationEngine
impl McpValidationEngine {
pub fn add_script_validator(&mut self, scripts: Vec<ValidationScript>, phase: ScriptExecutionPhase);
pub async fn validate_response_with_scripts(&self, response: &Value, spec: &ValidationSpec) -> Result<McpValidationResult>;
}
// Enhanced ValidationSpec to include script references
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationSpec {
// ... existing fields ...
pub validation_scripts: Option<Vec<String>>, // References to script names
pub script_execution_phases: Option<ScriptExecutionConfig>,
}use crate::script_engines::{LuaEngine, ScriptContext, ScriptResult, ScriptConfig};
use crate::validation::{CustomValidator, ValidationContext, ValidationError};
use crate::spec::ValidationScript;
pub struct ScriptValidator {
lua_engine: LuaEngine,
validation_scripts: HashMap<String, ValidationScript>,
execution_phase: ScriptExecutionPhase,
config: ScriptValidationConfig,
}
#[derive(Debug, Clone)]
pub enum ScriptExecutionPhase {
Before, // Execute before standard validation
After, // Execute after standard validation
}
#[derive(Debug, Clone)]
pub struct ScriptValidationConfig {
pub timeout_seconds: u32,
pub memory_limit_mb: u32,
pub fail_on_script_error: bool,
pub capture_script_logs: bool,
}
impl CustomValidator for ScriptValidator {
fn name(&self) -> &str {
match self.execution_phase {
ScriptExecutionPhase::Before => "script_validator_before",
ScriptExecutionPhase::After => "script_validator_after",
}
}
fn validate(
&self,
data: &Value,
context: &ValidationContext,
) -> Result<Vec<ValidationError>, Box<dyn std::error::Error>> {
let mut errors = Vec::new();
for (script_name, script) in &self.validation_scripts {
// Check if script should execute in this phase
if !self.should_execute_script(script) {
continue;
}
// Create script execution context
let script_context = self.create_script_context(data, context, script_name);
// Execute script
match self.execute_validation_script(script, script_context) {
Ok(result) => {
if !result.success {
if script.required.unwrap_or(false) || self.config.fail_on_script_error {
errors.push(ValidationError::FieldError {
field: format!("script:{}", script_name),
expected: "script execution success".to_string(),
actual: result.error.unwrap_or("script failed".to_string()),
});
}
}
// Parse script output for additional validation errors
if let Some(script_errors) = self.parse_script_validation_output(&result) {
errors.extend(script_errors);
}
}
Err(e) => {
if script.required.unwrap_or(false) || self.config.fail_on_script_error {
errors.push(ValidationError::FieldError {
field: format!("script:{}", script_name),
expected: "script execution".to_string(),
actual: format!("execution error: {}", e),
});
}
}
}
}
Ok(errors)
}
}
impl ScriptValidator {
pub fn new(
scripts: Vec<ValidationScript>,
phase: ScriptExecutionPhase,
config: ScriptValidationConfig,
) -> Result<Self, ValidationEngineError> {
let script_config = ScriptConfig {
timeout_seconds: config.timeout_seconds,
memory_limit_mb: config.memory_limit_mb,
disable_filesystem: true,
disable_network: true,
};
let lua_engine = LuaEngine::new(&script_config)?;
let validation_scripts: HashMap<String, ValidationScript> = scripts
.into_iter()
.map(|script| (script.name.clone(), script))
.collect();
Ok(Self {
lua_engine,
validation_scripts,
execution_phase,
config,
})
}
fn should_execute_script(&self, script: &ValidationScript) -> bool {
match (&script.execution_phase, &self.execution_phase) {
(Some(phase), current_phase) => {
match (phase.as_str(), current_phase) {
("before", ScriptExecutionPhase::Before) => true,
("after", ScriptExecutionPhase::After) => true,
_ => false,
}
}
// Default to "after" if no phase specified
(None, ScriptExecutionPhase::After) => true,
_ => false,
}
}
fn create_script_context(
&self,
response: &Value,
validation_context: &ValidationContext,
script_name: &str,
) -> ScriptContext {
let mut metadata = HashMap::new();
metadata.insert("script_name".to_string(), script_name.to_string());
metadata.insert("validation_method".to_string(), validation_context.method.clone());
// Add test metadata from validation context
for (key, value) in &validation_context.test_metadata {
metadata.insert(format!("test_{}", key), value.clone());
}
ScriptContext {
request: validation_context.request_id.clone(),
response: Some(response.clone()),
metadata,
server_info: validation_context.server_capabilities.clone(),
}
}
async fn execute_validation_script(
&self,
script: &ValidationScript,
context: ScriptContext,
) -> Result<ScriptResult, ScriptError> {
let source = script.source.as_ref()
.ok_or_else(|| ScriptError::ConfigurationError {
message: format!("Script '{}' has no source code", script.name),
})?;
self.lua_engine.execute_script(source, context).await
}
fn parse_script_validation_output(&self, result: &ScriptResult) -> Option<Vec<ValidationError>> {
// Parse script output for validation errors
// Scripts can return structured validation results
if let Ok(output) = serde_json::from_value::<ScriptValidationOutput>(result.output.clone()) {
let mut errors = Vec::new();
for error in output.validation_errors {
errors.push(ValidationError::FieldError {
field: error.field,
expected: error.expected,
actual: error.actual,
});
}
Some(errors)
} else {
None
}
}
}
// Script output structure for validation results
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptValidationOutput {
pub validation_errors: Vec<ScriptValidationError>,
pub warnings: Vec<ScriptValidationWarning>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptValidationError {
pub field: String,
pub expected: String,
pub actual: String,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptValidationWarning {
pub field: String,
pub message: String,
pub suggestion: Option<String>,
}// Enhanced McpValidationEngine with script support
impl McpValidationEngine {
/// Add script validators for before/after phases
pub fn add_script_validators(
&mut self,
scripts: Vec<ValidationScript>,
config: ScriptValidationConfig,
) -> Result<(), ValidationEngineError> {
// Separate scripts by execution phase
let before_scripts: Vec<ValidationScript> = scripts.iter()
.filter(|s| s.execution_phase.as_deref() == Some("before"))
.cloned()
.collect();
let after_scripts: Vec<ValidationScript> = scripts.iter()
.filter(|s| s.execution_phase.as_deref().unwrap_or("after") == "after")
.cloned()
.collect();
// Create and add before-phase validator
if !before_scripts.is_empty() {
let before_validator = ScriptValidator::new(
before_scripts,
ScriptExecutionPhase::Before,
config.clone(),
)?;
self.add_custom_validator(Box::new(before_validator))?;
}
// Create and add after-phase validator
if !after_scripts.is_empty() {
let after_validator = ScriptValidator::new(
after_scripts,
ScriptExecutionPhase::After,
config,
)?;
self.add_custom_validator(Box::new(after_validator))?;
}
Ok(())
}
/// Enhanced validation method with script support
pub async fn validate_response_with_scripts(
&self,
response: &Value,
spec: &ValidationSpec,
scripts: Option<&[ValidationScript]>,
) -> Result<McpValidationResult, ValidationEngineError> {
// Create a temporary engine with scripts if provided
if let Some(scripts) = scripts {
let mut temp_engine = self.clone(); // Would need to make McpValidationEngine cloneable
let script_config = ScriptValidationConfig {
timeout_seconds: 30,
memory_limit_mb: 64,
fail_on_script_error: false,
capture_script_logs: true,
};
temp_engine.add_script_validators(scripts.to_vec(), script_config)?;
temp_engine.validate_response(response, spec).await
} else {
self.validate_response(response, spec).await
}
}
}// Enhanced test execution with script support
impl TestCaseExecutor {
pub async fn execute_test_case_with_scripts(
&self,
test_case: &TestCase,
validation_scripts: Option<&[ValidationScript]>,
) -> Result<TestResult, ExecutorError> {
// Execute MCP tool call
let response = self.execute_tool_call(&test_case.input).await?;
// Resolve script references from test case
let scripts_to_execute = if let Some(script_refs) = &test_case.validation_scripts {
self.resolve_script_references(script_refs, validation_scripts)?
} else {
Vec::new()
};
// Create validation spec from test case
let validation_spec = self.create_validation_spec_from_test_case(test_case)?;
// Execute validation with scripts
let validation_result = self.validation_engine
.validate_response_with_scripts(
&response,
&validation_spec,
if scripts_to_execute.is_empty() { None } else { Some(&scripts_to_execute) },
)
.await?;
// Convert validation result to test result
Ok(self.convert_validation_to_test_result(validation_result, response))
}
fn resolve_script_references(
&self,
script_names: &[String],
available_scripts: Option<&[ValidationScript]>,
) -> Result<Vec<ValidationScript>, ExecutorError> {
let scripts = available_scripts.ok_or_else(|| {
ExecutorError::ConfigurationError("No validation scripts available".to_string())
})?;
let mut resolved_scripts = Vec::new();
for name in script_names {
if let Some(script) = scripts.iter().find(|s| s.name == *name) {
resolved_scripts.push(script.clone());
} else {
return Err(ExecutorError::ConfigurationError(
format!("Validation script '{}' not found", name)
));
}
}
Ok(resolved_scripts)
}
}- Create ScriptValidator: Implement CustomValidator trait with LuaEngine integration
- Add ScriptExecutionPhase: Define before/after execution phases
- Implement script context mapping: Map ValidationContext to ScriptContext
- Add error handling: Proper error propagation from script execution to validation results
- Enhance McpValidationEngine: Add script validator management methods
- Implement script resolution: Resolve script references from YAML specifications
- Add configuration support: ScriptValidationConfig for timeout, memory limits, error handling
- Update validation pipeline: Integrate script execution into standard validation flow
- Extend ValidationSpec: Add script reference fields
- Update test case execution: Support script execution in TestCaseExecutor
- Add script output parsing: Parse structured validation results from scripts
- Implement script lifecycle: Load, validate, and cache validation scripts
- YAML loading integration: Load validation scripts from test specifications
- Script caching: Cache compiled scripts for performance
- Error aggregation: Properly aggregate script errors with standard validation errors
- Performance optimization: Minimize script execution overhead
- Configuration errors: Missing scripts, invalid references
- Runtime errors: Script execution failures, timeouts, memory limits
- Validation errors: Script-generated validation failures
- Network/filesystem errors: Blocked access attempts from scripts
- Required scripts: Failures block test execution
- Optional scripts: Failures logged but don't block validation
- Error aggregation: Combine script errors with standard validation errors
- Detailed diagnostics: Include script name, line numbers, execution context
- Script compilation caching: Cache compiled Lua bytecode for repeated execution
- Context reuse: Reuse script contexts where possible
- Parallel execution: Execute independent scripts concurrently
- Memory management: Proper cleanup of script execution environments
- Timeout enforcement: Prevent runaway script execution
- Memory limits: Sandbox script memory usage
- Execution isolation: Prevent scripts from affecting each other
- Resource monitoring: Track script resource usage for diagnostics
- ScriptValidator tests: Test CustomValidator implementation
- Script execution tests: Test before/after phase execution
- Error handling tests: Test script failure scenarios
- Context mapping tests: Test ValidationContext to ScriptContext conversion
- End-to-end validation: Test complete validation pipeline with scripts
- YAML integration tests: Test script loading from YAML specifications
- Performance tests: Test script execution under load
- Error scenario tests: Test validation behavior with script failures
- Unit tests: 95%+ coverage of ScriptValidator implementation
- Integration tests: Cover all script execution phases and error paths
- End-to-end tests: Real YAML specifications with working validation scripts
- Performance tests: Validate script execution within latency requirements
- Filesystem isolation: Scripts cannot access filesystem
- Network isolation: Scripts cannot make network requests
- Memory limits: Enforce strict memory usage limits
- Execution timeouts: Prevent infinite loops and long-running scripts
- Script source validation: Validate script syntax before execution
- Context sanitization: Sanitize data passed to scripts
- Output validation: Validate script output before processing
- Permission enforcement: Enforce script execution permissions
- ✅ Scripts execute in before/after validation phases
- ✅ YAML specifications support validation script references
- ✅ End-to-end tests pass with real validation scripts
- ✅ Error propagation works correctly from scripts to validation results
- ✅ Script context includes request, response, and metadata
- ✅ Script execution adds <10% overhead to validation time
- ✅ Memory usage remains within configured limits
- ✅ Script compilation and caching improves repeated execution performance
- ✅ Concurrent script execution scales with available resources
- ✅ 95%+ test coverage of script integration code
- ✅ Comprehensive error handling for all script failure modes
- ✅ Detailed logging and diagnostics for script execution
- ✅ Security sandboxing prevents malicious script behavior
- JavaScript engine: Add Node.js script execution support (Issue #252)
- Python engine: Add Python script execution support (Issue #253)
- Engine registry: Abstract script engine selection by language
- Script libraries: Support shared script libraries and imports
- Async scripts: Support asynchronous script execution
- Script debugging: Add debugging and profiling capabilities
- Script templates: Provide common validation script templates
- Script compilation pipeline: Pre-compile scripts at load time
- Hot path optimization: Optimize frequently executed validation paths
- Resource pooling: Pool script execution contexts for reuse
- Metrics and monitoring: Add detailed script execution metrics
- ✅ Issue #254: LuaEngine implementation with context injection and sandboxing
- Validation engine enhancements: May require ValidationEngine improvements
- YAML specification updates: May need additional YAML schema extensions
- Issue #256: Wire TestCaseExecutor to script validation (builds on this work)
- Issue #257: Update documentation and examples (requires this implementation)
This design provides a comprehensive foundation for integrating script execution into the ValidationEngine while maintaining performance, security, and maintainability standards.