Skip to main content
Glama

Deskaid

by ezyang
#!/usr/bin/env python3 import logging import os import re from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Set, Tuple from codemcp.glob import match as glob_match __all__ = [ "Rule", "find_applicable_rules", "load_rule_from_file", "match_file_with_glob", "get_applicable_rules_content", ] @dataclass class Rule: """Represents a cursor rule loaded from an MDC file.""" description: Optional[str] # Description of when the rule is useful globs: List[str] # List of glob patterns to match files always_apply: bool # Whether the rule should always be applied payload: str # The markdown content of the rule file_path: str # Path to the MDC file def load_rule_from_file(file_path: str) -> Optional[Rule]: """Load a rule from an MDC file. Args: file_path: Path to the MDC file Returns: A Rule object if the file is valid, None otherwise """ try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() # Parse the frontmatter and content frontmatter_match = re.match(r"^---\n(.*?)\n---\n(.*)", content, re.DOTALL) if not frontmatter_match: return None frontmatter_text = frontmatter_match.group(1) payload = frontmatter_match.group(2).strip() # We need to manually parse the frontmatter to handle unquoted glob patterns frontmatter = {} for line in frontmatter_text.strip().split("\n"): if ":" in line: key, value = line.split(":", 1) key = key.strip() value = value.strip() frontmatter[key] = value # Extract rule properties description = frontmatter.get("description") # Handle globs - can be comma-separated string or a list globs: List[str] = [] globs_value = frontmatter.get("globs") if globs_value: globs = [g.strip() for g in globs_value.split(",")] # Convert alwaysApply string to boolean always_apply_value = frontmatter.get("alwaysApply", "false") always_apply = always_apply_value.lower() == "true" return Rule( description=description, globs=globs, always_apply=always_apply, payload=payload, file_path=file_path, ) except Exception as e: # If there's any error parsing the file, return None logging.error(f"Error loading rule from {file_path}: {e}") return None def match_file_with_glob(file_path: str, glob_pattern: str) -> bool: """Check if a file path matches a glob pattern. Args: file_path: Path to check glob_pattern: Glob pattern to match against Returns: True if the file matches the pattern, False otherwise """ logging.debug( f"match_file_with_glob: checking if '{file_path}' matches pattern '{glob_pattern}'" ) # Normalize path for matching # Paths are normalized to use / for consistent matching across platforms path = Path(file_path) normalized_path = str(path).replace(os.sep, "/") # File paths must be relative, not absolute # (since glob patterns typically use relative paths) assert not os.path.isabs(normalized_path), ( f"File path must be relative, got absolute path: {normalized_path}" ) # For filename-only patterns (without path separators), we can match just the filename if "/" not in glob_pattern: file_name = path.name result = glob_match(glob_pattern, file_name) logging.debug( f"Filename match: pattern='{glob_pattern}', file='{file_name}', result={result}" ) return result # Use the glob matcher from glob.py for full path matching # Cursor rules use vanilla glob patterns (not editorconfig features) result = glob_match(glob_pattern, normalized_path) logging.debug( f"Path match: pattern='{glob_pattern}', path='{normalized_path}', result={result}" ) return result def find_applicable_rules( repo_root: str, file_path: Optional[str] = None ) -> Tuple[List[Rule], List[Tuple[str, str]]]: """Find all applicable rules for the given file path. Walks up the directory tree from the file path to the repo root, looking for .cursor/rules directories and loading MDC files. Args: repo_root: Root of the repository file_path: Optional path to a file to match against rules Returns: A tuple containing (applicable_rules, suggested_rules) - applicable_rules: List of Rule objects that match the file - suggested_rules: List of (description, file_path) tuples for rules with descriptions """ applicable_rules: List[Rule] = [] suggested_rules: List[Tuple[str, str]] = [] processed_rule_files: Set[str] = set() # Normalize paths repo_root = os.path.abspath(repo_root) logging.debug( f"Finding applicable rules for repo_root={repo_root}, file_path={file_path}" ) # If file_path is provided, walk up from its directory to repo_root # Otherwise, just check repo_root start_dir = os.path.dirname(os.path.abspath(file_path)) if file_path else repo_root current_dir = start_dir logging.debug(f"Starting directory search at: {current_dir}") # Ensure we don't go beyond repo_root while current_dir.startswith(repo_root): # Look for .cursor/rules directory rules_dir = os.path.join(current_dir, ".cursor", "rules") logging.debug(f"Checking for rules directory: {rules_dir}") if os.path.isdir(rules_dir): logging.debug(f"Found rules directory: {rules_dir}") # Find all MDC files in this directory for root, _, files in os.walk(rules_dir): for filename in files: if filename.endswith(".mdc"): rule_file_path = os.path.join(root, filename) logging.debug(f"Considering rule file: {rule_file_path}") # Skip if we've already processed this file if rule_file_path in processed_rule_files: logging.debug( f"Skipping already processed rule file: {rule_file_path}" ) continue processed_rule_files.add(rule_file_path) # Load the rule rule = load_rule_from_file(rule_file_path) if rule is None: logging.debug( f"Failed to load rule from file: {rule_file_path}" ) continue # Check if this rule applies if rule.always_apply: logging.debug(f"Rule always applies: {rule_file_path}") applicable_rules.append(rule) elif file_path and rule.globs: # Check if any glob pattern matches the file logging.debug( f"Checking glob patterns for rule: {rule_file_path}" ) for glob_pattern in rule.globs: logging.debug( f"Testing glob pattern: {glob_pattern} against file: {file_path}" ) # Convert absolute path to relative path if needed rel_file_path = file_path if os.path.isabs(file_path): rel_file_path = os.path.relpath( file_path, repo_root ) logging.debug( f"Converting absolute path to relative: {file_path} → {rel_file_path}" ) if match_file_with_glob(rel_file_path, glob_pattern): logging.debug( f"Glob pattern matched: {glob_pattern}" ) applicable_rules.append(rule) break else: logging.debug( f"Glob pattern did not match: {glob_pattern}" ) elif rule.description: # Add to suggested rules if it has a description logging.debug( f"Adding rule to suggested rules: {rule_file_path}" ) suggested_rules.append((rule.description, rule_file_path)) else: logging.debug( f"Rule not applicable (no globs match or missing description): {rule_file_path}" ) # Move up one directory parent_dir = os.path.dirname(current_dir) if parent_dir == current_dir: # We've reached the root logging.debug("Reached filesystem root, stopping directory traversal") break current_dir = parent_dir logging.debug(f"Moving up to parent directory: {current_dir}") logging.debug( f"Found {len(applicable_rules)} applicable rules and {len(suggested_rules)} suggested rules" ) return applicable_rules, suggested_rules def get_applicable_rules_content( repo_root: str, file_path: Optional[str] = None ) -> str: """Generate a string with all applicable rules for a file or the current directory. This is a helper function used by multiple tools to format rule content in a consistent way. Args: repo_root: Root of the repository file_path: Optional path to a file to match against rules Returns: A formatted string containing all applicable rules, or an empty string if no rules apply """ try: logging.debug( f"get_applicable_rules_content called with repo_root={repo_root}, file_path={file_path}" ) result = "" # Find applicable rules applicable_rules, suggested_rules = find_applicable_rules(repo_root, file_path) logging.debug( f"Retrieved {len(applicable_rules)} applicable rules and {len(suggested_rules)} suggested rules" ) # If we have applicable rules, add them to the output if applicable_rules or suggested_rules: result += "\n\n// .cursor/rules results:" logging.debug("Adding rule results to output") # Add directly applicable rules for i, rule in enumerate(applicable_rules): rel_path = os.path.relpath(rule.file_path, repo_root) logging.debug( f"Adding applicable rule {i + 1}/{len(applicable_rules)}: {rel_path}" ) rule_content = f"\n\n// Rule from {rel_path}:\n{rule.payload}" result += rule_content # Add suggestions for rules with descriptions for i, (description, rule_path) in enumerate(suggested_rules): rel_path = os.path.relpath(rule_path, repo_root) logging.debug( f"Adding suggested rule {i + 1}/{len(suggested_rules)}: {rel_path} ('{description}')" ) result += f"\n\n// If {description} applies, load {rel_path}" else: logging.debug("No applicable or suggested rules found") logging.debug(f"Returning {len(result)} characters of rule content") return result except Exception as e: # Log the exception but don't propagate it logging.error(f"Error generating applicable rules content: {e}", exc_info=True) return ""

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/ezyang/codemcp'

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