mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-20 17:33:08 +00:00
feat: implement AI-optimized MCP tools with 95% size reduction
- Add get_node_essentials tool for 10-20 essential properties only - Add search_node_properties for targeted property search - Add get_node_for_task with 14 pre-configured templates - Add validate_node_config for comprehensive validation - Add get_property_dependencies for visibility analysis - Implement PropertyFilter service with curated essentials - Implement ExampleGenerator with working examples - Implement TaskTemplates for common workflows - Implement ConfigValidator with security checks - Implement PropertyDependencies for dependency analysis - Enhance property descriptions to 100% coverage - Add version information to essentials response - Update documentation with new tools Response sizes reduced from 100KB+ to <5KB for better AI agent usability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
467
src/services/config-validator.ts
Normal file
467
src/services/config-validator.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* Configuration Validator Service
|
||||
*
|
||||
* Validates node configurations to catch errors before execution.
|
||||
* Provides helpful suggestions and identifies missing or misconfigured properties.
|
||||
*/
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationWarning[];
|
||||
suggestions: string[];
|
||||
visibleProperties: string[];
|
||||
hiddenProperties: string[];
|
||||
autofix?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible';
|
||||
property: string;
|
||||
message: string;
|
||||
fix?: string;
|
||||
}
|
||||
|
||||
export interface ValidationWarning {
|
||||
type: 'missing_common' | 'deprecated' | 'inefficient' | 'security';
|
||||
property?: string;
|
||||
message: string;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
export class ConfigValidator {
|
||||
/**
|
||||
* Validate a node configuration
|
||||
*/
|
||||
static validate(
|
||||
nodeType: string,
|
||||
config: Record<string, any>,
|
||||
properties: any[]
|
||||
): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
const warnings: ValidationWarning[] = [];
|
||||
const suggestions: string[] = [];
|
||||
const visibleProperties: string[] = [];
|
||||
const hiddenProperties: string[] = [];
|
||||
const autofix: Record<string, any> = {};
|
||||
|
||||
// Check required properties
|
||||
this.checkRequiredProperties(properties, config, errors);
|
||||
|
||||
// Check property visibility
|
||||
const { visible, hidden } = this.getPropertyVisibility(properties, config);
|
||||
visibleProperties.push(...visible);
|
||||
hiddenProperties.push(...hidden);
|
||||
|
||||
// Validate property types and values
|
||||
this.validatePropertyTypes(properties, config, errors);
|
||||
|
||||
// Node-specific validations
|
||||
this.performNodeSpecificValidation(nodeType, config, errors, warnings, suggestions, autofix);
|
||||
|
||||
// Check for common issues
|
||||
this.checkCommonIssues(nodeType, config, properties, warnings, suggestions);
|
||||
|
||||
// Security checks
|
||||
this.performSecurityChecks(nodeType, config, warnings);
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
suggestions,
|
||||
visibleProperties,
|
||||
hiddenProperties,
|
||||
autofix: Object.keys(autofix).length > 0 ? autofix : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for missing required properties
|
||||
*/
|
||||
private static checkRequiredProperties(
|
||||
properties: any[],
|
||||
config: Record<string, any>,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
for (const prop of properties) {
|
||||
if (prop.required && !(prop.name in config)) {
|
||||
errors.push({
|
||||
type: 'missing_required',
|
||||
property: prop.name,
|
||||
message: `Required property '${prop.displayName || prop.name}' is missing`,
|
||||
fix: `Add ${prop.name} to your configuration`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible and hidden properties based on displayOptions
|
||||
*/
|
||||
private static getPropertyVisibility(
|
||||
properties: any[],
|
||||
config: Record<string, any>
|
||||
): { visible: string[]; hidden: string[] } {
|
||||
const visible: string[] = [];
|
||||
const hidden: string[] = [];
|
||||
|
||||
for (const prop of properties) {
|
||||
if (this.isPropertyVisible(prop, config)) {
|
||||
visible.push(prop.name);
|
||||
} else {
|
||||
hidden.push(prop.name);
|
||||
}
|
||||
}
|
||||
|
||||
return { visible, hidden };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a property is visible given current config
|
||||
*/
|
||||
private static isPropertyVisible(prop: any, config: Record<string, any>): boolean {
|
||||
if (!prop.displayOptions) return true;
|
||||
|
||||
// Check show conditions
|
||||
if (prop.displayOptions.show) {
|
||||
for (const [key, values] of Object.entries(prop.displayOptions.show)) {
|
||||
const configValue = config[key];
|
||||
const expectedValues = Array.isArray(values) ? values : [values];
|
||||
|
||||
if (!expectedValues.includes(configValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check hide conditions
|
||||
if (prop.displayOptions.hide) {
|
||||
for (const [key, values] of Object.entries(prop.displayOptions.hide)) {
|
||||
const configValue = config[key];
|
||||
const expectedValues = Array.isArray(values) ? values : [values];
|
||||
|
||||
if (expectedValues.includes(configValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate property types and values
|
||||
*/
|
||||
private static validatePropertyTypes(
|
||||
properties: any[],
|
||||
config: Record<string, any>,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
const prop = properties.find(p => p.name === key);
|
||||
if (!prop) continue;
|
||||
|
||||
// Type validation
|
||||
if (prop.type === 'string' && typeof value !== 'string') {
|
||||
errors.push({
|
||||
type: 'invalid_type',
|
||||
property: key,
|
||||
message: `Property '${key}' must be a string, got ${typeof value}`,
|
||||
fix: `Change ${key} to a string value`
|
||||
});
|
||||
} else if (prop.type === 'number' && typeof value !== 'number') {
|
||||
errors.push({
|
||||
type: 'invalid_type',
|
||||
property: key,
|
||||
message: `Property '${key}' must be a number, got ${typeof value}`,
|
||||
fix: `Change ${key} to a number`
|
||||
});
|
||||
} else if (prop.type === 'boolean' && typeof value !== 'boolean') {
|
||||
errors.push({
|
||||
type: 'invalid_type',
|
||||
property: key,
|
||||
message: `Property '${key}' must be a boolean, got ${typeof value}`,
|
||||
fix: `Change ${key} to true or false`
|
||||
});
|
||||
}
|
||||
|
||||
// Options validation
|
||||
if (prop.type === 'options' && prop.options) {
|
||||
const validValues = prop.options.map((opt: any) =>
|
||||
typeof opt === 'string' ? opt : opt.value
|
||||
);
|
||||
|
||||
if (!validValues.includes(value)) {
|
||||
errors.push({
|
||||
type: 'invalid_value',
|
||||
property: key,
|
||||
message: `Invalid value for '${key}'. Must be one of: ${validValues.join(', ')}`,
|
||||
fix: `Change ${key} to one of the valid options`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform node-specific validation
|
||||
*/
|
||||
private static performNodeSpecificValidation(
|
||||
nodeType: string,
|
||||
config: Record<string, any>,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationWarning[],
|
||||
suggestions: string[],
|
||||
autofix: Record<string, any>
|
||||
): void {
|
||||
switch (nodeType) {
|
||||
case 'nodes-base.httpRequest':
|
||||
this.validateHttpRequest(config, errors, warnings, suggestions, autofix);
|
||||
break;
|
||||
|
||||
case 'nodes-base.webhook':
|
||||
this.validateWebhook(config, warnings, suggestions);
|
||||
break;
|
||||
|
||||
case 'nodes-base.postgres':
|
||||
case 'nodes-base.mysql':
|
||||
this.validateDatabase(config, warnings, suggestions);
|
||||
break;
|
||||
|
||||
case 'nodes-base.code':
|
||||
this.validateCode(config, errors, warnings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate HTTP Request configuration
|
||||
*/
|
||||
private static validateHttpRequest(
|
||||
config: Record<string, any>,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationWarning[],
|
||||
suggestions: string[],
|
||||
autofix: Record<string, any>
|
||||
): void {
|
||||
// URL validation
|
||||
if (config.url && typeof config.url === 'string') {
|
||||
if (!config.url.startsWith('http://') && !config.url.startsWith('https://')) {
|
||||
errors.push({
|
||||
type: 'invalid_value',
|
||||
property: 'url',
|
||||
message: 'URL must start with http:// or https://',
|
||||
fix: 'Add https:// to the beginning of your URL'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// POST/PUT/PATCH without body
|
||||
if (['POST', 'PUT', 'PATCH'].includes(config.method) && !config.sendBody) {
|
||||
warnings.push({
|
||||
type: 'missing_common',
|
||||
property: 'sendBody',
|
||||
message: `${config.method} requests typically send a body`,
|
||||
suggestion: 'Set sendBody=true and configure the body content'
|
||||
});
|
||||
|
||||
autofix.sendBody = true;
|
||||
autofix.contentType = 'json';
|
||||
}
|
||||
|
||||
// Authentication warnings
|
||||
if (!config.authentication || config.authentication === 'none') {
|
||||
if (config.url?.includes('api.') || config.url?.includes('/api/')) {
|
||||
warnings.push({
|
||||
type: 'security',
|
||||
message: 'API endpoints typically require authentication',
|
||||
suggestion: 'Consider setting authentication if the API requires it'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// JSON body validation
|
||||
if (config.sendBody && config.contentType === 'json' && config.jsonBody) {
|
||||
try {
|
||||
JSON.parse(config.jsonBody);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
type: 'invalid_value',
|
||||
property: 'jsonBody',
|
||||
message: 'jsonBody contains invalid JSON',
|
||||
fix: 'Ensure jsonBody contains valid JSON syntax'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Webhook configuration
|
||||
*/
|
||||
private static validateWebhook(
|
||||
config: Record<string, any>,
|
||||
warnings: ValidationWarning[],
|
||||
suggestions: string[]
|
||||
): void {
|
||||
// Path validation
|
||||
if (config.path) {
|
||||
if (config.path.startsWith('/')) {
|
||||
warnings.push({
|
||||
type: 'inefficient',
|
||||
property: 'path',
|
||||
message: 'Webhook path should not start with /',
|
||||
suggestion: 'Remove the leading / from the path'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.path.includes(' ')) {
|
||||
warnings.push({
|
||||
type: 'inefficient',
|
||||
property: 'path',
|
||||
message: 'Webhook path contains spaces',
|
||||
suggestion: 'Use hyphens or underscores instead of spaces'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Response mode suggestions
|
||||
if (config.responseMode === 'responseNode' && !config.responseData) {
|
||||
suggestions.push('When using responseMode=responseNode, add a "Respond to Webhook" node to send custom responses');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate database queries
|
||||
*/
|
||||
private static validateDatabase(
|
||||
config: Record<string, any>,
|
||||
warnings: ValidationWarning[],
|
||||
suggestions: string[]
|
||||
): void {
|
||||
if (config.query) {
|
||||
const query = config.query.toLowerCase();
|
||||
|
||||
// SQL injection warning
|
||||
if (query.includes('${') || query.includes('{{')) {
|
||||
warnings.push({
|
||||
type: 'security',
|
||||
message: 'Query contains template expressions that might be vulnerable to SQL injection',
|
||||
suggestion: 'Use parameterized queries with additionalFields.queryParams instead'
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE without WHERE
|
||||
if (query.includes('delete') && !query.includes('where')) {
|
||||
warnings.push({
|
||||
type: 'security',
|
||||
message: 'DELETE query without WHERE clause will delete all records',
|
||||
suggestion: 'Add a WHERE clause to limit the deletion'
|
||||
});
|
||||
}
|
||||
|
||||
// SELECT * warning
|
||||
if (query.includes('select *')) {
|
||||
suggestions.push('Consider selecting specific columns instead of * for better performance');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Code node
|
||||
*/
|
||||
private static validateCode(
|
||||
config: Record<string, any>,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationWarning[]
|
||||
): void {
|
||||
const codeField = config.language === 'python' ? 'pythonCode' : 'jsCode';
|
||||
const code = config[codeField];
|
||||
|
||||
if (!code || code.trim() === '') {
|
||||
errors.push({
|
||||
type: 'missing_required',
|
||||
property: codeField,
|
||||
message: 'Code cannot be empty',
|
||||
fix: 'Add your code logic'
|
||||
});
|
||||
}
|
||||
|
||||
if (code?.includes('eval(') || code?.includes('exec(')) {
|
||||
warnings.push({
|
||||
type: 'security',
|
||||
message: 'Code contains eval/exec which can be a security risk',
|
||||
suggestion: 'Avoid using eval/exec with untrusted input'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for common configuration issues
|
||||
*/
|
||||
private static checkCommonIssues(
|
||||
nodeType: string,
|
||||
config: Record<string, any>,
|
||||
properties: any[],
|
||||
warnings: ValidationWarning[],
|
||||
suggestions: string[]
|
||||
): void {
|
||||
// Check for properties that won't be used
|
||||
const visibleProps = properties.filter(p => this.isPropertyVisible(p, config));
|
||||
const configuredKeys = Object.keys(config);
|
||||
|
||||
for (const key of configuredKeys) {
|
||||
if (!visibleProps.find(p => p.name === key)) {
|
||||
warnings.push({
|
||||
type: 'inefficient',
|
||||
property: key,
|
||||
message: `Property '${key}' is configured but won't be used due to current settings`,
|
||||
suggestion: 'Remove this property or adjust other settings to make it visible'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest commonly used properties
|
||||
const commonProps = ['authentication', 'errorHandling', 'timeout'];
|
||||
for (const prop of commonProps) {
|
||||
const propDef = properties.find(p => p.name === prop);
|
||||
if (propDef && this.isPropertyVisible(propDef, config) && !(prop in config)) {
|
||||
suggestions.push(`Consider setting '${prop}' for better control`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform security checks
|
||||
*/
|
||||
private static performSecurityChecks(
|
||||
nodeType: string,
|
||||
config: Record<string, any>,
|
||||
warnings: ValidationWarning[]
|
||||
): void {
|
||||
// Check for hardcoded credentials
|
||||
const sensitivePatterns = [
|
||||
/api[_-]?key/i,
|
||||
/password/i,
|
||||
/secret/i,
|
||||
/token/i,
|
||||
/credential/i
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (typeof value === 'string') {
|
||||
for (const pattern of sensitivePatterns) {
|
||||
if (pattern.test(key) && value.length > 0 && !value.includes('{{')) {
|
||||
warnings.push({
|
||||
type: 'security',
|
||||
property: key,
|
||||
message: `Hardcoded ${key} detected`,
|
||||
suggestion: 'Use n8n credentials or expressions instead of hardcoding sensitive values'
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user