Skip to main content
Glama
register_validate_diagram_tool.py25.3 kB
""" Validate Diagram Tool for Ilograph MCP Server. This tool provides comprehensive validation of Ilograph diagram syntax and structure. It first validates YAML syntax, then validates Ilograph-specific schema requirements using the official specification and documentation for context. """ import logging import re from typing import Any, Dict, List, Optional, Set import yaml from fastmcp import Context, FastMCP from pydantic import BaseModel from ..core.fetcher import get_fetcher logger = logging.getLogger(__name__) class ValidationError(BaseModel): """Represents a validation error with detailed information.""" level: str # 'error', 'warning', 'info' message: str line: Optional[int] = None column: Optional[int] = None path: Optional[str] = None suggestion: Optional[str] = None class ValidationResult(BaseModel): """Represents the complete validation result.""" success: bool errors: List[ValidationError] = [] warnings: List[ValidationError] = [] info: List[ValidationError] = [] yaml_valid: bool = False schema_valid: bool = False @property def total_issues(self) -> int: return len(self.errors) + len(self.warnings) def add_error( self, message: str, line: Optional[int] = None, column: Optional[int] = None, path: Optional[str] = None, suggestion: Optional[str] = None, ) -> None: """Add a validation error.""" self.errors.append( ValidationError( level="error", message=message, line=line, column=column, path=path, suggestion=suggestion, ) ) self.success = False def add_warning( self, message: str, line: Optional[int] = None, column: Optional[int] = None, path: Optional[str] = None, suggestion: Optional[str] = None, ) -> None: """Add a validation warning.""" self.warnings.append( ValidationError( level="warning", message=message, line=line, column=column, path=path, suggestion=suggestion, ) ) def add_info( self, message: str, path: Optional[str] = None, suggestion: Optional[str] = None ) -> None: """Add validation info.""" self.info.append( ValidationError(level="info", message=message, path=path, suggestion=suggestion) ) class IlographValidator: """Core validator for Ilograph diagrams.""" def __init__(self) -> None: self.known_top_level_properties = { "resources", "perspectives", "contexts", "imports", "layout", } self.known_resource_properties = { "id", "name", "subtitle", "description", "icon", "iconStyle", "color", "children", "instanceOf", "abstract", "alias", "for", } self.known_perspective_properties = { "name", "description", "notes", "extends", "aliases", "overrides", "relations", "sequences", } self.known_relation_properties = { "from", "to", "via", "label", "description", "color", "arrowDirection", "secondary", } def validate_yaml_syntax( self, content: str, result: ValidationResult ) -> Optional[Dict[str, Any]]: """Validate YAML syntax and return parsed data if valid.""" try: # Try to parse the YAML data = yaml.safe_load(content) result.yaml_valid = True return data if isinstance(data, dict) else None except yaml.YAMLError as e: # Handle problem_mark attribute safely line_num = None col_num = None if hasattr(e, "problem_mark") and e.problem_mark is not None: if hasattr(e.problem_mark, "line"): line_num = e.problem_mark.line if hasattr(e.problem_mark, "column"): col_num = e.problem_mark.column result.add_error( f"Invalid YAML syntax: {str(e)}", line=line_num, column=col_num, suggestion="Check for indentation issues, missing colons, or invalid characters", ) return None def validate_top_level_structure(self, data: Dict[str, Any], result: ValidationResult) -> None: """Validate the top-level structure of the Ilograph diagram.""" if not isinstance(data, dict): result.add_error( "Ilograph diagram must be a YAML object (dictionary) at the top level", suggestion="Ensure your diagram starts with properties like 'resources:', 'perspectives:', etc.", ) return # Check for unknown top-level properties for key in data.keys(): if key not in self.known_top_level_properties: result.add_warning( f"Unknown top-level property: '{key}'", path=key, suggestion=f"Valid top-level properties are: {', '.join(sorted(self.known_top_level_properties))}", ) # Check if we have at least resources if "resources" not in data: result.add_warning( "No 'resources' section found - diagrams typically need resources to be meaningful", suggestion="Add a 'resources:' section with your diagram's components", ) def validate_resources(self, resources: Any, result: ValidationResult) -> None: """Validate the resources section.""" if not isinstance(resources, list): result.add_error( "The 'resources' property must be a list", path="resources", suggestion="Change 'resources: ...' to 'resources: [...]' or use YAML list syntax with dashes", ) return resource_ids: Set[str] = set() for i, resource in enumerate(resources): self.validate_resource(resource, result, f"resources[{i}]", resource_ids) def validate_resource( self, resource: Any, result: ValidationResult, path: str, resource_ids: Set[str] ) -> None: """Validate a single resource.""" if not isinstance(resource, dict): result.add_error( "Each resource must be an object (dictionary)", path=path, suggestion="Use 'name: ResourceName' format for resources", ) return # Check for required properties - either 'name' or 'id' should be present if "name" not in resource and "id" not in resource: result.add_error( "Resource must have either 'name' or 'id' property", path=path, suggestion="Add 'name: YourResourceName' or 'id: your-resource-id'", ) # Check for duplicate IDs resource_id = resource.get("id") if resource_id: if resource_id in resource_ids: result.add_error( f"Duplicate resource ID: '{resource_id}'", path=f"{path}.id", suggestion="Resource IDs must be unique across the diagram", ) else: resource_ids.add(resource_id) # Check for unknown properties for key in resource.keys(): if key not in self.known_resource_properties: result.add_warning( f"Unknown resource property: '{key}'", path=f"{path}.{key}", suggestion=f"Valid resource properties include: {', '.join(sorted(self.known_resource_properties))}", ) # Validate instanceOf format instance_of = resource.get("instanceOf") if instance_of and isinstance(instance_of, str): if "::" in instance_of: # This looks like a namespace reference - that's good pass else: result.add_info( f"Resource uses instanceOf without namespace: '{instance_of}'", path=f"{path}.instanceOf", suggestion="Consider using namespace format like 'AWS::EC2::Instance' or define the resource locally", ) # Validate children if present children = resource.get("children") if children: if not isinstance(children, list): result.add_error( "The 'children' property must be a list", path=f"{path}.children", suggestion="Use list format: 'children: [{name: Child1}, {name: Child2}]'", ) else: for j, child in enumerate(children): self.validate_resource(child, result, f"{path}.children[{j}]", resource_ids) def validate_perspectives(self, perspectives: Any, result: ValidationResult) -> None: """Validate the perspectives section.""" if not isinstance(perspectives, list): result.add_error( "The 'perspectives' property must be a list", path="perspectives", suggestion="Use list format with dashes: '- name: PerspectiveName'", ) return for i, perspective in enumerate(perspectives): self.validate_perspective(perspective, result, f"perspectives[{i}]") def validate_perspective(self, perspective: Any, result: ValidationResult, path: str) -> None: """Validate a single perspective.""" if not isinstance(perspective, dict): result.add_error( "Each perspective must be an object (dictionary)", path=path, suggestion="Use 'name: PerspectiveName' format for perspectives", ) return # Check for name (recommended) if "name" not in perspective: result.add_warning( "Perspective should have a 'name' property", path=path, suggestion="Add 'name: YourPerspectiveName' for better clarity", ) # Check for unknown properties for key in perspective.keys(): if key not in self.known_perspective_properties: result.add_warning( f"Unknown perspective property: '{key}'", path=f"{path}.{key}", suggestion=f"Valid perspective properties include: {', '.join(sorted(self.known_perspective_properties))}", ) # Validate relations if present relations = perspective.get("relations") if relations: self.validate_relations(relations, result, f"{path}.relations") def validate_relations(self, relations: Any, result: ValidationResult, path: str) -> None: """Validate relations in a perspective.""" if not isinstance(relations, list): result.add_error( "The 'relations' property must be a list", path=path, suggestion="Use list format with dashes for each relation", ) return for i, relation in enumerate(relations): self.validate_relation(relation, result, f"{path}[{i}]") def validate_relation(self, relation: Any, result: ValidationResult, path: str) -> None: """Validate a single relation.""" if not isinstance(relation, dict): result.add_error( "Each relation must be an object (dictionary)", path=path, suggestion="Use 'from: source, to: target' format", ) return # Check for required properties if "from" not in relation or "to" not in relation: result.add_error( "Relation must have both 'from' and 'to' properties", path=path, suggestion="Add both 'from: SourceResource' and 'to: TargetResource'", ) # Check for unknown properties for key in relation.keys(): if key not in self.known_relation_properties: result.add_warning( f"Unknown relation property: '{key}'", path=f"{path}.{key}", suggestion=f"Valid relation properties include: {', '.join(sorted(self.known_relation_properties))}", ) # Validate arrowDirection if present arrow_direction = relation.get("arrowDirection") if arrow_direction: valid_directions = {"forward", "backward", "bidirectional"} if arrow_direction not in valid_directions: result.add_error( f"Invalid arrowDirection: '{arrow_direction}'", path=f"{path}.arrowDirection", suggestion=f"Valid values are: {', '.join(valid_directions)}", ) def validate_imports(self, imports: Any, result: ValidationResult) -> None: """Validate the imports section.""" if not isinstance(imports, list): result.add_error( "The 'imports' property must be a list", path="imports", suggestion="Use list format: '- from: namespace'", ) return for i, import_item in enumerate(imports): if not isinstance(import_item, dict): result.add_error( "Each import must be an object (dictionary)", path=f"imports[{i}]", suggestion="Use format: 'from: namespace, namespace: alias'", ) continue if "from" not in import_item: result.add_error( "Import must have 'from' property", path=f"imports[{i}]", suggestion="Add 'from: namespace/path' to specify what to import", ) def validate_schema(self, data: Dict[str, Any], result: ValidationResult) -> None: """Validate the Ilograph schema structure.""" try: self.validate_top_level_structure(data, result) if "resources" in data: self.validate_resources(data["resources"], result) if "perspectives" in data: self.validate_perspectives(data["perspectives"], result) if "imports" in data: self.validate_imports(data["imports"], result) # If we got here without critical errors, schema is basically valid if not result.errors: result.schema_valid = True if not result.warnings: result.add_info( "Diagram structure looks good!", suggestion="Consider adding descriptions to your resources and perspectives for better documentation", ) except Exception as e: result.add_error( f"Unexpected error during schema validation: {str(e)}", suggestion="This might indicate a complex structure that needs manual review", ) def validate(self, content: str) -> ValidationResult: """Main validation method.""" result = ValidationResult(success=True) # Step 1: Validate YAML syntax data = self.validate_yaml_syntax(content, result) if data is None: return result # YAML is invalid, no point in continuing # Step 2: Validate Ilograph schema self.validate_schema(data, result) # Set final success status result.success = len(result.errors) == 0 return result def format_validation_result(result: ValidationResult) -> Dict[str, Any]: """Format validation result for JSON response.""" formatted = { "success": result.success, "yaml_valid": result.yaml_valid, "schema_valid": result.schema_valid, "summary": { "total_errors": len(result.errors), "total_warnings": len(result.warnings), "total_info": len(result.info), }, } if result.errors: formatted["errors"] = [error.model_dump() for error in result.errors] if result.warnings: formatted["warnings"] = [warning.model_dump() for warning in result.warnings] if result.info: formatted["info"] = [info.model_dump() for info in result.info] # Add overall assessment if result.success: if result.warnings: formatted["assessment"] = "Valid with suggestions" else: formatted["assessment"] = "Valid" else: formatted["assessment"] = "Invalid - contains errors" return formatted def register_validate_diagram_tool(mcp: FastMCP) -> None: """Register the validate diagram tool with the FastMCP server.""" @mcp.tool( annotations={ "title": "Validate Ilograph Diagram", "readOnlyHint": True, "description": "Validates Ilograph diagram syntax and provides detailed error messages with suggestions", } ) async def validate_diagram_tool(content: str, ctx: Context) -> Dict[str, Any]: """ Validates Ilograph YAML syntax and structure. This tool performs comprehensive validation of Ilograph diagrams: 1. First validates YAML syntax for structural correctness 2. Then validates Ilograph-specific schema requirements 3. Provides detailed error messages, warnings, and suggestions 4. Can optionally use official Ilograph specification for context Args: content: The Ilograph diagram content as a string Returns: dict: Validation result with success/failure, errors, warnings, and suggestions Format: { "success": bool, "yaml_valid": bool, "schema_valid": bool, "summary": {"total_errors": int, "total_warnings": int, "total_info": int}, "errors": [{"level": str, "message": str, "line": int, "suggestion": str}, ...], "warnings": [...], "info": [...], "assessment": str } """ try: await ctx.info("Starting Ilograph diagram validation") # Validate input if not isinstance(content, str): error_result = { "success": False, "yaml_valid": False, "schema_valid": False, "errors": [ { "level": "error", "message": "Content parameter is required and must be a non-empty string", "suggestion": "Provide the Ilograph diagram content as a string parameter", } ], "assessment": "Invalid - no content provided", } await ctx.error("Validation failed: no content provided") return error_result content = content.strip() if not content: error_result = { "success": False, "yaml_valid": False, "schema_valid": False, "errors": [ { "level": "error", "message": "Content is empty", "suggestion": "Provide actual Ilograph diagram content", } ], "assessment": "Invalid - no content provided", } await ctx.error("Validation failed: empty content") return error_result # Create validator and run validation validator = IlographValidator() result = validator.validate(content) # Format the result formatted_result = format_validation_result(result) # Log results if result.success: await ctx.info( f"Validation successful - {len(result.warnings)} warnings, {len(result.info)} suggestions" ) else: await ctx.error( f"Validation failed - {len(result.errors)} errors, {len(result.warnings)} warnings" ) return formatted_result except Exception as e: error_msg = f"Unexpected error during validation: {str(e)}" await ctx.error(error_msg) logger.exception("Error in validate_diagram_tool") return { "success": False, "yaml_valid": False, "schema_valid": False, "errors": [ { "level": "error", "message": "An unexpected error occurred during validation", "suggestion": "Please check your diagram format and try again", } ], "assessment": "Error - validation failed", } @mcp.tool( annotations={ "title": "Get Validation Help", "readOnlyHint": True, "description": "Provides guidance on Ilograph diagram validation and common issues", } ) async def get_validation_help(ctx: Context) -> str: """ Provides comprehensive help for Ilograph diagram validation. Returns guidance on common validation issues, proper syntax, and best practices for creating valid Ilograph diagrams. Returns: str: Detailed validation help in markdown format """ try: await ctx.info("Providing Ilograph validation help") help_content = """# Ilograph Diagram Validation Help ## Overview This validation tool checks your Ilograph diagrams for both YAML syntax correctness and Ilograph-specific schema compliance. ## Validation Process 1. **YAML Syntax Check**: Ensures your diagram is valid YAML 2. **Schema Validation**: Checks Ilograph-specific structure and properties 3. **Best Practice Suggestions**: Provides recommendations for improvement ## Common Issues and Solutions ### YAML Syntax Errors - **Indentation**: Use consistent spaces (2 or 4), not tabs - **Missing Colons**: Properties need colons (`name: value`) - **List Format**: Use dashes for lists or bracket notation - **Quoted Strings**: Quote strings with special characters ### Ilograph Schema Issues - **Missing Required Properties**: Resources need `name` or `id` - **Unknown Properties**: Check property names against specification - **Duplicate IDs**: Resource IDs must be unique - **Invalid Relations**: Relations need both `from` and `to` ## Valid Top-Level Properties - `resources`: Your diagram components (required for meaningful diagrams) - `perspectives`: Different views of your architecture - `contexts`: Multiple context views - `imports`: External namespace imports - `layout`: Diagram layout properties ## Example Valid Structure ```yaml imports: - from: ilograph/aws namespace: AWS resources: - name: Web Server subtitle: Frontend description: Serves the web application icon: server.svg children: - name: Load Balancer instanceOf: AWS::ElasticLoadBalancer perspectives: - name: System Overview relations: - from: User to: Web Server label: HTTPS requests ``` ## Getting More Help - Use `fetch_spec_tool()` to get the complete Ilograph specification - Use `fetch_documentation_tool(section='tutorial')` for detailed tutorials - Use `fetch_example_tool()` to see working example diagrams ## Tips for Success 1. Start with simple structures and build complexity gradually 2. Use consistent naming conventions 3. Add descriptions to improve documentation 4. Validate frequently during development 5. Check examples for pattern guidance """ await ctx.info("Validation help provided successfully") return help_content except Exception as e: error_msg = f"Error providing validation help: {str(e)}" await ctx.error(error_msg) return f"Error: {error_msg}" def get_tool_info() -> dict: """Get information about the validation tools for registration.""" return { "name": "validate_diagram_tool", "description": "Comprehensive Ilograph diagram validation with detailed error reporting", "tools": [ "validate_diagram_tool", "get_validation_help", ], }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/QuincyMillerDev/ilograph-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server