Skip to main content
Glama
check_mcp_field_descriptions.py13.1 kB
#!/usr/bin/env -S uv run --script """Check that all MCP tool parameters have Field descriptions. MCP tools should have comprehensive Field descriptions for all parameters (except ctx: Context which is excluded from this requirement). This script scans the specified directories/files or defaults to src/ directory to identify parameters without proper Field descriptions. Usage: python scripts/check_mcp_field_descriptions.py # Check src/ directory python scripts/check_mcp_field_descriptions.py file1.py file2.py # Check specific files python scripts/check_mcp_field_descriptions.py src/databeak/servers/ # Check specific directory Exit codes: 0: No violations found 1: Missing Field descriptions detected (violations found) 2: Error during scanning """ import argparse import ast import sys from pathlib import Path from typing import NamedTuple class FieldDescriptionViolation(NamedTuple): """Represents a missing Field description violation.""" file_path: str function_name: str parameter_name: str line_number: int class MCPFieldChecker(ast.NodeVisitor): """AST visitor to find MCP tool parameters without Field descriptions.""" def __init__(self, file_path: str): """Initialize checker with file path.""" self.file_path = file_path self.violations: list[FieldDescriptionViolation] = [] def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Visit function definitions and check for MCP tool patterns.""" self._check_mcp_tool_function(node) self.generic_visit(node) def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: """Visit async function definitions and check for MCP tool patterns.""" self._check_mcp_tool_function(node) self.generic_visit(node) def _check_mcp_tool_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: """Check if function is an MCP tool and validate Field descriptions.""" # Skip functions that don't look like MCP tools if not self._is_mcp_tool_function(node): return # Check each parameter for Field description for arg in node.args.args: param_name = arg.arg # Skip 'self', 'cls', and 'ctx' parameters if param_name in ("self", "cls", "ctx"): continue # Check if parameter has proper Field annotation if not self._has_field_description(arg): violation = FieldDescriptionViolation( file_path=self.file_path, function_name=node.name, parameter_name=param_name, line_number=node.lineno, ) self.violations.append(violation) def _is_mcp_tool_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: """Determine if function appears to be an MCP tool.""" # MCP tools must have a Context parameter as first argument (after self) if not node.args.args: return False # Look for Context parameter (usually first or second parameter) has_context = False for arg in node.args.args[:2]: # Check first two args if arg.annotation: annotation_str = ast.dump(arg.annotation) if "Context" in annotation_str and "Field" in annotation_str: has_context = True break if not has_context: return False # Additional validation: should have Result return type if node.returns: returns_str = ast.dump(node.returns) if "Result" in returns_str: return True # Or should be in a servers/ file (MCP tools are typically in servers) return "/servers/" in self.file_path def _has_field_description(self, arg: ast.arg) -> bool: """Check if argument has Field annotation with description.""" if not arg.annotation: return False # Check if this is an Annotated type if not self._is_annotated_type(arg.annotation): return False # Look for Field with description in the annotation return self._has_field_with_description(arg.annotation) def _is_annotated_type(self, annotation: ast.AST) -> bool: """Check if annotation is Annotated[...].""" return ( isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name) and annotation.value.id == "Annotated" ) def _has_field_with_description(self, annotation: ast.AST) -> bool: """Check if Annotated type has Field with description.""" if not isinstance(annotation, ast.Subscript): return False # Get the slice (the contents inside the brackets) if isinstance(annotation.slice, ast.Tuple): # Handle Annotated[Type, Field(...), ...] for elt in annotation.slice.elts: if self._is_field_call_with_description(elt): return True else: # Handle single annotation Annotated[Type, Field(...)] return self._is_field_call_with_description(annotation.slice) return False def _is_field_call_with_description(self, node: ast.AST) -> bool: """Check if node is Field(...) call with description.""" if not isinstance(node, ast.Call): return False # Check if it's a Field call if isinstance(node.func, ast.Name) and node.func.id == "Field": # Look for description in keywords for keyword in node.keywords: if keyword.arg == "description": return True return False def scan_file(file_path: Path) -> list[FieldDescriptionViolation]: """Scan a single Python file for Field description violations. Returns: List of violations found in the file """ try: with file_path.open("r", encoding="utf-8") as f: content = f.read() # Parse the file using AST tree = ast.parse(content) # Check for Field description violations checker = MCPFieldChecker(str(file_path)) checker.visit(tree) return checker.violations except (SyntaxError, UnicodeDecodeError, OSError) as e: print(f"Warning: Could not parse {file_path}: {e}", file=sys.stderr) return [] def scan_directory(src_dir: Path) -> list[FieldDescriptionViolation]: """Scan all Python files in directory for Field description violations. Returns: List of all violations found """ all_violations = [] # Find all Python files in directory python_files = list(src_dir.rglob("*.py")) if not python_files: print(f"Warning: No Python files found in {src_dir}", file=sys.stderr) return [] print(f"Scanning {len(python_files)} Python files in {src_dir}...") for file_path in python_files: violations = scan_file(file_path) all_violations.extend(violations) return all_violations def scan_paths(paths: list[str]) -> list[FieldDescriptionViolation]: """Scan specified paths (files or directories) for Field description violations. Returns: List of all violations found """ all_violations = [] all_files = [] for path_str in paths: path = Path(path_str) if not path.exists(): print(f"Warning: Path does not exist: {path}", file=sys.stderr) continue if path.is_file(): if path.suffix == ".py": all_files.append(path) else: print(f"Warning: Skipping non-Python file: {path}", file=sys.stderr) elif path.is_dir(): python_files = list(path.rglob("*.py")) all_files.extend(python_files) print(f"Found {len(python_files)} Python files in {path}") else: print(f"Warning: Unknown path type: {path}", file=sys.stderr) if not all_files: print("Warning: No Python files found to scan", file=sys.stderr) return [] print(f"Scanning {len(all_files)} Python files total...") for file_path in all_files: violations = scan_file(file_path) all_violations.extend(violations) return all_violations def format_violations_report(violations: list[FieldDescriptionViolation]) -> str: """Format violations into a readable report. Returns: Formatted report string """ if not violations: return "✅ All MCP tool parameters have Field descriptions - Documentation standards met!" report_lines = [ f"❌ Found {len(violations)} parameter(s) without Field descriptions", "", "All MCP tool parameters should have comprehensive Field descriptions", "for proper documentation and client integration.", "", "Violations found:", "", ] # Group by file for cleaner output by_file: dict[str, list[FieldDescriptionViolation]] = {} for violation in violations: if violation.file_path not in by_file: by_file[violation.file_path] = [] by_file[violation.file_path].append(violation) for file_path, file_violations in sorted(by_file.items()): report_lines.append(f"📁 {file_path}") report_lines.extend( [ f" └─ {violation.function_name}() parameter '{violation.parameter_name}' " f"at line {violation.line_number}" for violation in file_violations ] ) report_lines.append("") report_lines.extend( [ "To fix these violations:", "1. Add Field() annotation with description to each parameter", '2. Use Annotated[Type, Field(description="...")]', "3. Provide clear, helpful parameter descriptions", "", "Example of proper MCP tool parameter documentation:", "def my_tool(", ' ctx: Annotated[Context, Field(description="FastMCP context for session access")],', ' data: Annotated[str, Field(description="Input data to process")],', ' option: Annotated[bool, Field(description="Enable special processing mode")] = False', ") -> Result:", ' """Process data with specified options."""', " # Implementation...", "", "Note: The 'ctx: Context' parameter is excluded from this requirement.", ] ) return "\n".join(report_lines) def parse_arguments() -> argparse.Namespace: """Parse command line arguments. Returns: Parsed arguments namespace """ parser = argparse.ArgumentParser( description="Check that all MCP tool parameters have Field descriptions", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s # Check default src/ directory %(prog)s file1.py file2.py # Check specific files %(prog)s src/databeak/servers/ # Check specific directory %(prog)s src/databeak/servers/ tests/ # Check multiple directories All MCP tool parameters should have comprehensive Field descriptions for proper documentation and client integration. """, ) parser.add_argument( "paths", nargs="*", help="Files or directories to check (default: src/ directory)" ) parser.add_argument( "--quiet", "-q", action="store_true", help="Only show summary, not detailed violations" ) return parser.parse_args() def main() -> int: """Execute main script logic. Returns: Exit code: 0 for success, 1 for violations found, 2 for errors """ try: args = parse_arguments() # Determine what to scan if args.paths: # Use specified paths violations = scan_paths(args.paths) else: # Default to src directory script_dir = Path(__file__).parent project_root = script_dir.parent src_dir = project_root / "src" if not src_dir.exists(): print(f"Error: src directory not found at {src_dir}", file=sys.stderr) return 2 violations = scan_directory(src_dir) # Generate and print report if args.quiet: if violations: print(f"❌ Found {len(violations)} parameter(s) without Field descriptions") return 1 print( "✅ All MCP tool parameters have Field descriptions - Documentation standards met!" ) return 0 report = format_violations_report(violations) print(report) return 1 if violations else 0 except Exception as e: print(f"Error during scanning: {e}", file=sys.stderr) return 2 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/jonpspri/databeak'

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