Skip to main content
Glama
knishioka

IB Analytics MCP Server

by knishioka
validate_claude_config.py20.1 kB
#!/usr/bin/env python3 """ Claude Code Configuration Validator Validates sub-agents and slash commands for proper structure and configuration. No external dependencies required - uses only Python standard library. Usage: python validate_claude_config.py # Validate all files python validate_claude_config.py path/to/file.md # Validate specific file python validate_claude_config.py --changed-only # Git changed files only python validate_claude_config.py --fix # Auto-fix common issues """ import re import sys from dataclasses import dataclass from enum import Enum from pathlib import Path class Severity(Enum): """Validation message severity levels""" ERROR = "❌" WARNING = "⚠️" INFO = "ℹ️" SUCCESS = "✅" @dataclass class ValidationMessage: """A validation finding""" severity: Severity file_path: Path line_number: int | None message: str suggestion: str | None = None class FrontmatterParser: """Parse YAML frontmatter from Markdown files""" @staticmethod def extract(content: str) -> tuple[dict | None, int]: """Extract frontmatter and return (dict, end_line_number)""" lines = content.split("\n") if not lines or lines[0].strip() != "---": return None, 0 # Find closing --- end_idx = None for i in range(1, len(lines)): if lines[i].strip() == "---": end_idx = i break if end_idx is None: return None, 0 # Parse YAML manually (simple key: value format) frontmatter = {} for line in lines[1:end_idx]: line = line.strip() if not line or line.startswith("#"): continue if ":" in line: key, value = line.split(":", 1) frontmatter[key.strip()] = value.strip() return frontmatter, end_idx + 1 class ToolsValidator: """Validate tools configuration""" # Known valid tools VALID_TOOLS = { # Core tools "Task", "Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash", "WebSearch", "WebFetch", "TodoWrite", "SlashCommand", # MCP tools pattern: mcp__ib-sec-mcp__* } # Bash command patterns BASH_PATTERN = re.compile(r"Bash\([^)]+\)") # MCP tool pattern MCP_PATTERN = re.compile(r"mcp__ib-sec-mcp__\w+") @classmethod def validate(cls, tools_str: str, file_type: str) -> list[ValidationMessage]: """Validate tools list""" messages = [] if not tools_str: return [ ValidationMessage( Severity.ERROR, Path(), None, f"Empty tools list - {file_type} must specify allowed tools", ) ] # Parse tools (comma separated, handle Bash() patterns specially) # Split by comma first to preserve Bash(command:*) patterns tools = [] for part in tools_str.split(","): part = part.strip() if part: tools.append(part) for tool in tools: # Check if valid tool if tool in cls.VALID_TOOLS: continue # Check Bash pattern if cls.BASH_PATTERN.match(tool): continue # Check MCP pattern if cls.MCP_PATTERN.match(tool): continue # Unknown tool messages.append( ValidationMessage( Severity.WARNING, Path(), None, f"Unknown tool: {tool}", "Check if this is a valid tool or MCP server name", ) ) return messages class SubAgentValidator: """Validate sub-agent configuration files""" REQUIRED_FIELDS = ["name", "description", "tools", "model"] VALID_MODELS = ["opus", "sonnet", "haiku"] @staticmethod def validate(file_path: Path) -> list[ValidationMessage]: """Validate a sub-agent file""" messages = [] content = file_path.read_text() frontmatter, fm_end = FrontmatterParser.extract(content) # Check frontmatter exists if frontmatter is None: messages.append( ValidationMessage( Severity.ERROR, file_path, 1, "Missing YAML frontmatter", "Add frontmatter block starting with --- on line 1", ) ) return messages # Check required fields for field in SubAgentValidator.REQUIRED_FIELDS: if field not in frontmatter: messages.append( ValidationMessage( Severity.ERROR, file_path, None, f"Missing required field: {field}", f"Add '{field}: value' to frontmatter", ) ) # Validate name format (kebab-case) if "name" in frontmatter: name = frontmatter["name"] if not re.match(r"^[a-z]+(-[a-z]+)*$", name): messages.append( ValidationMessage( Severity.WARNING, file_path, None, f"Name should be kebab-case: {name}", f"Consider: {name.lower().replace('_', '-')}", ) ) # Validate model if "model" in frontmatter: model = frontmatter["model"] if model not in SubAgentValidator.VALID_MODELS: messages.append( ValidationMessage( Severity.ERROR, file_path, None, f"Invalid model: {model}", f"Use one of: {', '.join(SubAgentValidator.VALID_MODELS)}", ) ) # Validate tools if "tools" in frontmatter: tool_messages = ToolsValidator.validate(frontmatter["tools"], "Sub-agent") for msg in tool_messages: msg.file_path = file_path messages.extend(tool_messages) # Check description quality if "description" in frontmatter: desc = frontmatter["description"] if len(desc) < 20: messages.append( ValidationMessage( Severity.WARNING, file_path, None, "Description too short - should be descriptive", "Add clear description of when and how to use this agent", ) ) # Recommend PROACTIVELY for auto-activation if "proactive" not in desc.lower(): messages.append( ValidationMessage( Severity.INFO, file_path, None, "Consider adding 'Use PROACTIVELY' for auto-activation", "Add to description if this agent should activate automatically", ) ) # Content validation lines = content.split("\n") has_workflow = any("## Workflow" in line or "## workflow" in line for line in lines) has_responsibilities = any( "## Responsibilities" in line or "## responsibilities" in line for line in lines ) if not has_workflow: messages.append( ValidationMessage( Severity.INFO, file_path, None, "Recommended: Add '## Workflow' section", "Document step-by-step process for this agent", ) ) if not has_responsibilities: messages.append( ValidationMessage( Severity.INFO, file_path, None, "Recommended: Add '## Responsibilities' section", "Clearly list what this agent is responsible for", ) ) return messages class CommandValidator: """Validate slash command configuration files""" REQUIRED_FIELDS = ["description", "allowed-tools"] @staticmethod def validate(file_path: Path) -> list[ValidationMessage]: """Validate a slash command file""" messages = [] content = file_path.read_text() frontmatter, fm_end = FrontmatterParser.extract(content) # Check frontmatter exists if frontmatter is None: messages.append( ValidationMessage( Severity.ERROR, file_path, 1, "Missing YAML frontmatter", "Add frontmatter block starting with --- on line 1", ) ) return messages # Check required fields for field in CommandValidator.REQUIRED_FIELDS: if field not in frontmatter: messages.append( ValidationMessage( Severity.ERROR, file_path, None, f"Missing required field: {field}", f"Add '{field}: value' to frontmatter", ) ) # Validate description (should be concise) if "description" in frontmatter: desc = frontmatter["description"] if len(desc) > 100: messages.append( ValidationMessage( Severity.WARNING, file_path, None, "Description too long - should be one concise line", "Keep under 100 characters for slash command menu", ) ) # Validate allowed-tools if "allowed-tools" in frontmatter: tools_str = frontmatter["allowed-tools"] # Check delegation pattern has_task = "Task" in tools_str has_mcp = "mcp__ib-sec-mcp__" in tools_str if has_task and has_mcp: messages.append( ValidationMessage( Severity.INFO, file_path, None, "Hybrid pattern detected: Task + MCP tools", "This is valid but consider if direct MCP calls are needed", ) ) elif not has_task and not has_mcp: messages.append( ValidationMessage( Severity.INFO, file_path, None, "No Task or MCP tools - direct execution pattern", "Ensure command can complete with available tools", ) ) # Validate tools list tool_messages = ToolsValidator.validate(tools_str, "Command") for msg in tool_messages: msg.file_path = file_path messages.extend(tool_messages) # Content validation lines = content.split("\n")[fm_end:] content_body = "\n".join(lines) # Check for $ARGUMENTS usage if "$ARGUMENTS" in content_body or "$1" in content_body: if "argument-hint" not in frontmatter: messages.append( ValidationMessage( Severity.WARNING, file_path, None, "Uses $ARGUMENTS but no argument-hint in frontmatter", "Add 'argument-hint: [expected-args]' for documentation", ) ) # Check Task delegation syntax task_pattern = re.compile(r"Task\([a-z-]+\)") task_matches = task_pattern.findall(content_body) if task_matches: if "Task" not in frontmatter.get("allowed-tools", ""): messages.append( ValidationMessage( Severity.ERROR, file_path, None, "Uses Task delegation but Task not in allowed-tools", "Add 'Task' to allowed-tools in frontmatter", ) ) # Validate sub-agent references agents_dir = file_path.parent.parent / "agents" for match in task_matches: agent_name = match.replace("Task(", "").replace(")", "") agent_file = agents_dir / f"{agent_name}.md" if not agent_file.exists(): messages.append( ValidationMessage( Severity.ERROR, file_path, None, f"References non-existent sub-agent: {agent_name}", f"Create {agent_file} or fix reference", ) ) # Check for examples section has_examples = "## Examples" in content_body or "### Examples" in content_body if not has_examples: messages.append( ValidationMessage( Severity.INFO, file_path, None, "Recommended: Add usage examples section", "Show how to use this command with different arguments", ) ) return messages class ConfigValidator: """Main validator orchestrator""" def __init__(self, auto_fix: bool = False): self.auto_fix = auto_fix self.messages: list[ValidationMessage] = [] def validate_file(self, file_path: Path) -> list[ValidationMessage]: """Validate a single file""" if not file_path.exists(): return [ ValidationMessage(Severity.ERROR, file_path, None, f"File not found: {file_path}") ] # Determine file type if "/agents/" in str(file_path): return SubAgentValidator.validate(file_path) elif "/commands/" in str(file_path): return CommandValidator.validate(file_path) else: return [ ValidationMessage( Severity.WARNING, file_path, None, "Unknown file type - expected agents/ or commands/", ) ] def validate_all(self, base_path: Path) -> list[ValidationMessage]: """Validate all config files""" messages = [] # Find all agent and command files agents_dir = base_path / "agents" commands_dir = base_path / "commands" files_to_check = [] if agents_dir.exists(): files_to_check.extend(agents_dir.glob("*.md")) if commands_dir.exists(): files_to_check.extend(commands_dir.glob("*.md")) for file_path in sorted(files_to_check): file_messages = self.validate_file(file_path) messages.extend(file_messages) return messages @staticmethod def print_results(messages: list[ValidationMessage], verbose: bool = False): """Print validation results""" # Group by severity errors = [m for m in messages if m.severity == Severity.ERROR] warnings = [m for m in messages if m.severity == Severity.WARNING] info = [m for m in messages if m.severity == Severity.INFO] # Group by file by_file = {} for msg in messages: if msg.file_path not in by_file: by_file[msg.file_path] = [] by_file[msg.file_path].append(msg) # Print results print("\n" + "=" * 60) print("Claude Code Configuration Validation Results") print("=" * 60 + "\n") for file_path, file_messages in sorted(by_file.items()): file_errors = [m for m in file_messages if m.severity == Severity.ERROR] file_warnings = [m for m in file_messages if m.severity == Severity.WARNING] # File header if file_errors: status = Severity.ERROR.value elif file_warnings: status = Severity.WARNING.value else: status = Severity.SUCCESS.value print(f"{status} {file_path.relative_to(file_path.parent.parent.parent)}") # Print messages for msg in file_messages: if not verbose and msg.severity == Severity.INFO: continue indent = " " line_info = f"Line {msg.line_number}: " if msg.line_number else "" print(f"{indent}{msg.severity.value} {line_info}{msg.message}") if msg.suggestion: print(f"{indent} 💡 {msg.suggestion}") print() # Summary print("=" * 60) print("Summary:") print(f" {Severity.ERROR.value} Errors: {len(errors)}") print(f" {Severity.WARNING.value} Warnings: {len(warnings)}") print(f" {Severity.INFO.value} Info: {len(info)}") print("=" * 60 + "\n") # Return code return 1 if errors else 0 def main(): """Main entry point""" import argparse parser = argparse.ArgumentParser(description="Validate Claude Code configuration files") parser.add_argument("files", nargs="*", help="Specific files to validate (default: all)") parser.add_argument( "--changed-only", action="store_true", help="Only validate git changed files" ) parser.add_argument( "--fix", action="store_true", help="Auto-fix common issues (not yet implemented)" ) parser.add_argument("--verbose", "-v", action="store_true", help="Show info-level messages") args = parser.parse_args() # Determine files to check base_path = Path(__file__).parent.parent validator = ConfigValidator(auto_fix=args.fix) if args.files: # Validate specific files messages = [] for file_arg in args.files: file_path = Path(file_arg) messages.extend(validator.validate_file(file_path)) elif args.changed_only: # Validate git changed files import subprocess try: result = subprocess.run( ["git", "diff", "--name-only", "HEAD"], capture_output=True, text=True, check=True ) changed_files = result.stdout.strip().split("\n") messages = [] for file_path_str in changed_files: if ".claude/" in file_path_str and file_path_str.endswith(".md"): file_path = Path(file_path_str) if file_path.exists(): messages.extend(validator.validate_file(file_path)) except subprocess.CalledProcessError: print("Error: Git not available or not in a git repository") return 1 else: # Validate all files messages = validator.validate_all(base_path) # Print results exit_code = validator.print_results(messages, verbose=args.verbose) if args.fix: print("Note: Auto-fix not yet implemented") return exit_code if __name__ == "__main__": sys.exit(main())

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/knishioka/ib-sec-mcp'

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