Skip to main content
Glama
config.py12.8 kB
import os from pathlib import Path from typing import Any, Dict, List, Optional import yaml class ConfigError(Exception): """Exception raised for errors in the HooksMCP configuration.""" pass class PromptArgument: """Represents an argument for a prompt template.""" def __init__( self, name: str, description: Optional[str] = None, required: bool = False, ): self.name = name self.description = description self.required = required @classmethod def from_dict(cls, data: Dict[str, Any]) -> "PromptArgument": """Create a PromptArgument from a dictionary.""" name = data.get("name") description = data.get("description") required = data.get("required", False) if not name: raise ConfigError( "HooksMCP Error: 'name' is required for each prompt argument" ) return cls(name, description, required) class Prompt: """Represents a prompt template.""" def __init__( self, name: str, description: str, prompt_text: Optional[str] = None, prompt_file: Optional[str] = None, arguments: Optional[List[PromptArgument]] = None, ): self.name = name self.description = description self.prompt_text = prompt_text self.prompt_file = prompt_file self.arguments = arguments or [] # Validate that exactly one of prompt_text or prompt_file is provided if prompt_text is None and prompt_file is None: raise ConfigError( f"HooksMCP Error: Prompt '{name}' must specify either 'prompt' or 'prompt-file'" ) if prompt_text is not None and prompt_file is not None: raise ConfigError( f"HooksMCP Error: Prompt '{name}' cannot specify both 'prompt' and 'prompt-file'" ) @classmethod def from_dict(cls, data: Dict[str, Any], config_dir: Path) -> "Prompt": """Create a Prompt from a dictionary.""" name = data.get("name") description = data.get("description") prompt_text = data.get("prompt") prompt_file = data.get("prompt-file") arguments_data = data.get("arguments") if not name: raise ConfigError("HooksMCP Error: 'name' is required for each prompt") if not description: raise ConfigError( "HooksMCP Error: 'description' is required for each prompt" ) # Validate name and description length limits if len(name) > 32: raise ConfigError( f"HooksMCP Error: Prompt name '{name}' exceeds 32 character limit" ) if len(description) > 256: raise ConfigError( f"HooksMCP Error: Prompt description for '{name}' exceeds 256 character limit" ) arguments = [] if arguments_data: if not isinstance(arguments_data, list): raise ConfigError( f"HooksMCP Error: 'arguments' must be a list for prompt '{name}'" ) for i, arg_data in enumerate(arguments_data): if not isinstance(arg_data, dict): raise ConfigError( f"HooksMCP Error: Each prompt argument must be an object (prompt '{name}', argument[{i}])" ) try: argument = PromptArgument.from_dict(arg_data) arguments.append(argument) except ConfigError: # Re-raise config errors as-is raise except Exception as e: raise ConfigError( f"HooksMCP Error: Failed to parse argument[{i}] for prompt '{name}': {str(e)}" ) prompt = cls(name, description, prompt_text, prompt_file, arguments) # If prompt-file is specified, verify the file exists if prompt.prompt_file: prompt_file_path = config_dir / prompt.prompt_file if not prompt_file_path.exists(): raise ConfigError( f"HooksMCP Error: Prompt file '{prompt.prompt_file}' for prompt '{name}' not found at {prompt_file_path}" ) return prompt class ParameterType: """Enumeration of parameter types.""" PROJECT_FILE_PATH = "project_file_path" REQUIRED_ENV_VAR = "required_env_var" OPTIONAL_ENV_VAR = "optional_env_var" INSECURE_STRING = "insecure_string" class ActionParameter: """Represents a parameter for an action.""" def __init__( self, name: str, param_type: str, description: Optional[str] = None, default: Optional[str] = None, ): self.name = name self.type = param_type self.description = description self.default = default def to_dict(self) -> Dict[str, Any]: """Convert parameter to dictionary for MCP tool definition.""" result = { "name": self.name, "type": self.type, "description": self.description or f"Parameter {self.name}", } if self.default is not None: result["default"] = self.default return result class Action: """Represents a single action defined in the configuration.""" def __init__( self, name: str, description: str, command: str, parameters: Optional[List[ActionParameter]] = None, run_path: Optional[str] = None, timeout: int = 60, ): self.name = name self.description = description self.command = command self.parameters = parameters or [] self.run_path = run_path self.timeout = timeout @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Action": """Create an Action from a dictionary.""" name = data.get("name") description = data.get("description") command = data.get("command") if not name: raise ConfigError("HooksMCP Error: 'name' is required for each action") if not description: raise ConfigError( "HooksMCP Error: 'description' is required for each action" ) if not command: raise ConfigError("HooksMCP Error: 'command' is required for each action") parameters = [] if "parameters" in data: for param_data in data["parameters"]: param_name = param_data.get("name") param_type = param_data.get("type") param_description = param_data.get("description") param_default = param_data.get("default") if not param_name: raise ConfigError( f"HooksMCP Error: 'name' is required for each parameter in action '{name}'" ) if not param_type: raise ConfigError( f"HooksMCP Error: 'type' is required for parameter '{param_name}' in action '{name}'" ) if param_type not in [ ParameterType.PROJECT_FILE_PATH, ParameterType.REQUIRED_ENV_VAR, ParameterType.OPTIONAL_ENV_VAR, ParameterType.INSECURE_STRING, ]: raise ConfigError( f"HooksMCP Error: Invalid parameter type '{param_type}' for parameter '{param_name}' in action '{name}'. " f"Valid types are: project_file_path, required_env_var, optional_env_var, insecure_string" ) parameters.append( ActionParameter( param_name, param_type, param_description, param_default ) ) return cls( name, description, command, parameters, data.get("run_path"), data.get("timeout", 60), ) class HooksMCPConfig: """Main configuration class for HooksMCP.""" def __init__( self, actions: List[Action], prompts: Optional[List[Prompt]] = None, get_prompt_tool_filter: Optional[List[str]] = None, server_name: Optional[str] = None, server_description: Optional[str] = None, ): self.actions = actions self.prompts = prompts or [] self.get_prompt_tool_filter = get_prompt_tool_filter self.server_name = server_name or "HooksMCP" self.server_description = ( server_description or "Project-specific development tools and prompts exposed via MCP" ) # Validate get_prompt_tool_filter if provided if self.get_prompt_tool_filter is not None: prompt_names = {prompt.name for prompt in self.prompts} for filter_name in self.get_prompt_tool_filter: if filter_name not in prompt_names: raise ConfigError( f"HooksMCP Error: Prompt '{filter_name}' in get_prompt_tool_filter not found in prompts list" ) @classmethod def from_yaml(cls, yaml_path: str) -> "HooksMCPConfig": """Load configuration from a YAML file.""" if not os.path.exists(yaml_path): raise ConfigError( f"HooksMCP Error: Configuration file '{yaml_path}' not found" ) try: with open(yaml_path, "r") as f: data = yaml.safe_load(f) except yaml.YAMLError as e: raise ConfigError( f"HooksMCP Error: Failed to parse YAML file '{yaml_path}': {str(e)}" ) except Exception as e: raise ConfigError( f"HooksMCP Error: Failed to read configuration file '{yaml_path}': {str(e)}" ) if not isinstance(data, dict): raise ConfigError( "HooksMCP Error: Configuration file must contain a YAML object" ) actions_data = data.get("actions", []) if not isinstance(actions_data, list): raise ConfigError("HooksMCP Error: 'actions' must be an array") actions = [] for i, action_data in enumerate(actions_data): if not isinstance(action_data, dict): raise ConfigError( f"HooksMCP Error: Each action must be an object (action[{i}])" ) try: action = Action.from_dict(action_data) actions.append(action) except ConfigError: # Re-raise config errors as-is raise except Exception as e: raise ConfigError( f"HooksMCP Error: Failed to parse action[{i}]: {str(e)}" ) # Parse prompts if present prompts = [] prompts_data = data.get("prompts") if prompts_data: if not isinstance(prompts_data, list): raise ConfigError("HooksMCP Error: 'prompts' must be an array") config_dir = Path(yaml_path).parent for i, prompt_data in enumerate(prompts_data): if not isinstance(prompt_data, dict): raise ConfigError( f"HooksMCP Error: Each prompt must be an object (prompt[{i}])" ) try: prompt = Prompt.from_dict(prompt_data, config_dir) prompts.append(prompt) except ConfigError: # Re-raise config errors as-is raise except Exception as e: raise ConfigError( f"HooksMCP Error: Failed to parse prompt[{i}]: {str(e)}" ) return cls( actions=actions, prompts=prompts, get_prompt_tool_filter=data.get("get_prompt_tool_filter"), server_name=data.get("server_name"), server_description=data.get("server_description"), ) def validate_required_env_vars(self) -> List[str]: """Check which required environment variables are not set and return their names.""" missing_vars = [] for action in self.actions: for param in action.parameters: if param.type == ParameterType.REQUIRED_ENV_VAR: if not os.environ.get(param.name): missing_vars.append(param.name) return missing_vars

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/scosman/actions_mcp'

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