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:
czlonkowski
2025-06-16 12:37:45 +02:00
parent 4cfc3cc5c8
commit 1884d5babf
28 changed files with 8122 additions and 4 deletions

View 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;
}
}
}
}
}
}