Skip to main content
Glama
check_docstring_args.py11.2 kB
#!/usr/bin/env -S uv run --script """Check for Args sections in docstrings that violate MCP documentation standards. MCP tools should use Field descriptions for parameter documentation rather than Args sections in docstrings. This script scans the specified directories/files or defaults to src/ directory to identify violations and reports them with file locations and function names. Usage: python scripts/check_docstring_args.py # Check src/ directory python scripts/check_docstring_args.py file1.py file2.py # Check specific files python scripts/check_docstring_args.py src/databeak/servers/ # Check specific directory Exit codes: 0: No violations found 1: Args sections detected (violations found) 2: Error during scanning """ import argparse import ast import sys from pathlib import Path from typing import NamedTuple class ArgsViolation(NamedTuple): """Represents a docstring Args violation.""" file_path: str function_name: str line_number: int args_line_number: int class DocstringArgsChecker(ast.NodeVisitor): """AST visitor to find Args sections in function docstrings.""" def __init__(self, file_path: str): """Initialize checker with file path.""" self.file_path = file_path self.violations: list[ArgsViolation] = [] def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Visit function definitions and check their docstrings.""" self._check_function_docstring(node) self.generic_visit(node) def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: """Visit async function definitions and check their docstrings.""" self._check_function_docstring(node) self.generic_visit(node) def _check_function_docstring(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: """Check if function docstring contains Args section.""" # Only check MCP tools for Args violations if not self._is_mcp_tool_function(node): return # Get docstring from function docstring = ast.get_docstring(node) if not docstring: return # Split docstring into lines and check for Args sections lines = docstring.split("\n") for i, line in enumerate(lines): stripped = line.strip() # Check for Args section indicators if stripped.startswith(("Args:", "Arguments:", "Parameters:")): # Calculate line number in original file # AST line numbers are 1-based, docstring starts after function definition args_line_number = node.lineno + 1 + i violation = ArgsViolation( file_path=self.file_path, function_name=node.name, line_number=node.lineno, args_line_number=args_line_number, ) self.violations.append(violation) break # Only report first Args section per function 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 scan_file(file_path: Path) -> list[ArgsViolation]: """Scan a single Python file for Args violations. Args: file_path: Path to Python file to scan 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 Args violations checker = DocstringArgsChecker(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[ArgsViolation]: """Scan all Python files in directory for Args violations. Args: src_dir: Path to directory to scan 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[ArgsViolation]: """Scan specified paths (files or directories) for Args violations. Args: paths: List of file or directory paths to scan 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[ArgsViolation]) -> str: """Format violations into a readable report. Args: violations: List of violations to format Returns: Formatted report string """ if not violations: return "✅ No Args sections found in docstrings - MCP documentation standards met!" report_lines = [ f"❌ Found {len(violations)} Args section(s) in docstrings", "", "MCP tools should use Field descriptions for parameter documentation", "instead of Args sections in docstrings.", "", "Violations found:", "", ] # Group by file for cleaner output by_file: dict[str, list[ArgsViolation]] = {} 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}() at line {violation.line_number} " f"(Args section at line {violation.args_line_number})" for violation in file_violations ] ) report_lines.append("") report_lines.extend( [ "To fix these violations:", "1. Remove the Args section from the docstring", "2. Ensure Field descriptions are comprehensive", "3. Keep only the function summary and examples in docstrings", "", "Example of proper MCP tool documentation:", "def my_tool(", ' ctx: Annotated[Context, Field(description="FastMCP context")],', ' param: Annotated[str, Field(description="Parameter description")]', ") -> Result:", ' """Brief tool description."""', " # Implementation...", ] ) return "\n".join(report_lines) def parse_arguments() -> argparse.Namespace: """Parse command line arguments. Returns: Parsed arguments namespace """ parser = argparse.ArgumentParser( description="Check for Args sections in docstrings that violate MCP documentation standards", 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 MCP tools should use Field descriptions for parameter documentation instead of Args sections in docstrings. """, ) 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)} Args section(s) in docstrings") return 1 print("✅ No Args sections found - MCP 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