@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
348 lines • 13.8 kB
JavaScript
/**
* MCP Quotes Server - Template Validator
*
* Validates quote templates against schema and business rules
*/
import { VariableType, OutputFormat } from '../../../types/templates.js';
/**
* Template validator class
*/
export class TemplateValidator {
/**
* Validate a complete template
*/
static validate(template) {
const errors = [];
const warnings = [];
// Validate metadata
this.validateMetadata(template, errors, warnings);
// Validate content
this.validateContent(template, errors, warnings);
// Validate variables
this.validateVariables(template, errors, warnings);
// Validate output format
this.validateOutputFormat(template, errors, warnings);
// Validate examples
this.validateExamples(template, errors, warnings);
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate template metadata
*/
static validateMetadata(template, errors, warnings) {
const { metadata } = template;
// Required fields
if (!metadata.id || metadata.id.trim() === '') {
errors.push({
code: 'MISSING_ID',
message: 'Template ID is required',
field: 'metadata.id',
});
}
if (!metadata.name || metadata.name.trim() === '') {
errors.push({
code: 'MISSING_NAME',
message: 'Template name is required',
field: 'metadata.name',
});
}
if (!metadata.version || !this.isValidVersion(metadata.version)) {
errors.push({
code: 'INVALID_VERSION',
message: 'Template version must follow semantic versioning (e.g., 1.0.0)',
field: 'metadata.version',
});
}
// Warnings
if (!metadata.description || metadata.description.trim() === '') {
warnings.push({
code: 'MISSING_DESCRIPTION',
message: 'Template description is recommended',
field: 'metadata.description',
});
}
if (metadata.tags.length === 0) {
warnings.push({
code: 'NO_TAGS',
message: 'Adding tags helps with template discovery',
field: 'metadata.tags',
});
}
if (metadata.deprecated && !metadata.deprecationMessage) {
warnings.push({
code: 'MISSING_DEPRECATION_MESSAGE',
message: 'Deprecated templates should include a deprecation message',
field: 'metadata.deprecationMessage',
});
}
}
/**
* Validate template content
*/
static validateContent(template, errors, warnings) {
if (!template.content || template.content.trim() === '') {
errors.push({
code: 'MISSING_CONTENT',
message: 'Template content is required',
field: 'content',
});
return;
}
// Extract variable placeholders from content
const placeholders = this.extractPlaceholders(template.content);
const definedVariables = new Set(template.variables.map((v) => v.name));
// Check for undefined variables
placeholders.forEach((placeholder) => {
if (!definedVariables.has(placeholder)) {
errors.push({
code: 'UNDEFINED_VARIABLE',
message: `Variable "${placeholder}" used in template but not defined`,
field: 'content',
});
}
});
// Check for unused variables
template.variables.forEach((variable) => {
if (!placeholders.has(variable.name) && variable.required) {
warnings.push({
code: 'UNUSED_VARIABLE',
message: `Required variable "${variable.name}" is defined but not used in template`,
field: `variables.${variable.name}`,
});
}
});
}
/**
* Validate template variables
*/
static validateVariables(template, errors, warnings) {
const variableNames = new Set();
template.variables.forEach((variable, index) => {
// Check for duplicate variable names
if (variableNames.has(variable.name)) {
errors.push({
code: 'DUPLICATE_VARIABLE',
message: `Duplicate variable name: ${variable.name}`,
field: `variables[${index}].name`,
});
}
variableNames.add(variable.name);
// Validate variable name format
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(variable.name)) {
errors.push({
code: 'INVALID_VARIABLE_NAME',
message: `Invalid variable name: ${variable.name}. Must start with letter and contain only letters, numbers, and underscores`,
field: `variables[${index}].name`,
});
}
// Validate enum values
if (variable.type === VariableType.ENUM &&
(!variable.enumValues || variable.enumValues.length === 0)) {
errors.push({
code: 'MISSING_ENUM_VALUES',
message: `Enum variable "${variable.name}" must define enumValues`,
field: `variables[${index}].enumValues`,
});
}
// Validate default value type
if (variable.defaultValue !== undefined) {
const isValidDefault = this.validateVariableValue(variable, variable.defaultValue);
if (!isValidDefault) {
errors.push({
code: 'INVALID_DEFAULT_VALUE',
message: `Default value for "${variable.name}" does not match variable type ${variable.type}`,
field: `variables[${index}].defaultValue`,
});
}
}
// Validate examples
if (variable.examples) {
variable.examples.forEach((example, exampleIndex) => {
const isValidExample = this.validateVariableValue(variable, example);
if (!isValidExample) {
warnings.push({
code: 'INVALID_EXAMPLE',
message: `Example ${exampleIndex + 1} for "${variable.name}" does not match variable type ${variable.type}`,
field: `variables[${index}].examples[${exampleIndex}]`,
});
}
});
}
});
}
/**
* Validate output format configuration
*/
static validateOutputFormat(template, errors, _warnings) {
if (!template.outputFormat || !template.outputFormat.format) {
errors.push({
code: 'MISSING_OUTPUT_FORMAT',
message: 'Output format is required',
field: 'outputFormat.format',
});
}
// Validate alternative formats
if (template.outputFormat.alternativeFormats) {
const validFormats = Object.values(OutputFormat);
template.outputFormat.alternativeFormats.forEach((format, index) => {
if (!validFormats.includes(format)) {
errors.push({
code: 'INVALID_ALTERNATIVE_FORMAT',
message: `Invalid alternative format: ${format}`,
field: `outputFormat.alternativeFormats[${index}]`,
});
}
});
}
}
/**
* Validate template examples
*/
static validateExamples(template, errors, warnings) {
if (!template.examples || template.examples.length === 0) {
warnings.push({
code: 'NO_EXAMPLES',
message: 'Including examples helps users understand template usage',
field: 'examples',
});
return;
}
const requiredVariables = template.variables
.filter((v) => v.required)
.map((v) => v.name);
template.examples.forEach((example, index) => {
// Check for missing required variables
requiredVariables.forEach((varName) => {
if (!(varName in example.variables)) {
errors.push({
code: 'MISSING_REQUIRED_VARIABLE_IN_EXAMPLE',
message: `Example "${example.name}" missing required variable: ${varName}`,
field: `examples[${index}].variables`,
});
}
});
// Validate variable values in examples
Object.entries(example.variables).forEach(([varName, value]) => {
const variable = template.variables.find((v) => v.name === varName);
if (!variable) {
warnings.push({
code: 'UNKNOWN_VARIABLE_IN_EXAMPLE',
message: `Example "${example.name}" uses unknown variable: ${varName}`,
field: `examples[${index}].variables.${varName}`,
});
}
else if (!this.validateVariableValue(variable, value)) {
errors.push({
code: 'INVALID_VARIABLE_VALUE_IN_EXAMPLE',
message: `Example "${example.name}" has invalid value for variable "${varName}"`,
field: `examples[${index}].variables.${varName}`,
});
}
});
});
}
/**
* Extract variable placeholders from template content
*/
static extractPlaceholders(content) {
const placeholders = new Set();
const regex = /\{([a-zA-Z][a-zA-Z0-9_]*)\}/g;
let match;
while ((match = regex.exec(content)) !== null) {
if (match[1]) {
placeholders.add(match[1]);
}
}
return placeholders;
}
/**
* Validate semantic version string
*/
static isValidVersion(version) {
const semverRegex = /^(\d+)\.(\d+)\.(\d+)(?:-(\w+(?:\.\w+)*))?(?:\+(\w+(?:\.\w+)*))?$/;
return semverRegex.test(version);
}
/**
* Validate variable value against type
*/
static validateVariableValue(variable, value) {
switch (variable.type) {
case VariableType.STRING:
return typeof value === 'string';
case VariableType.NUMBER:
return typeof value === 'number' && !isNaN(value);
case VariableType.BOOLEAN:
return typeof value === 'boolean';
case VariableType.ENUM:
return variable.enumValues?.includes(value) || false;
case VariableType.DATE:
return value instanceof Date || !isNaN(Date.parse(value));
case VariableType.ARRAY:
return Array.isArray(value);
case VariableType.OBJECT:
return typeof value === 'object' && value !== null && !Array.isArray(value);
default:
return false;
}
}
/**
* Validate variable value with validation rules
*/
static validateVariableWithRules(variable, value) {
// Type validation
if (!this.validateVariableValue(variable, value)) {
return {
isValid: false,
error: `Value must be of type ${variable.type}`,
};
}
// Apply validation rules
if (variable.validation) {
const { min, max, pattern, errorMessage } = variable.validation;
if (variable.type === VariableType.STRING || variable.type === VariableType.ARRAY) {
const length = variable.type === VariableType.STRING ? value.length : value.length;
if (min !== undefined && length < min) {
return {
isValid: false,
error: errorMessage || `Length must be at least ${min}`,
};
}
if (max !== undefined && length > max) {
return {
isValid: false,
error: errorMessage || `Length must not exceed ${max}`,
};
}
}
if (variable.type === VariableType.NUMBER) {
if (min !== undefined && value < min) {
return {
isValid: false,
error: errorMessage || `Value must be at least ${min}`,
};
}
if (max !== undefined && value > max) {
return {
isValid: false,
error: errorMessage || `Value must not exceed ${max}`,
};
}
}
if (pattern && variable.type === VariableType.STRING) {
const regex = new RegExp(pattern);
if (!regex.test(value)) {
return {
isValid: false,
error: errorMessage || `Value must match pattern: ${pattern}`,
};
}
}
}
return { isValid: true };
}
}
//# sourceMappingURL=templateValidator.js.map