Skip to main content
Glama
cli_utils.py10.3 kB
""" Shared CLI utilities for formatting and validation. Used by both nisaba and nabu CLI tools. """ import sys from pathlib import Path from typing import Any, List, Optional, Dict import click class AutoRegisteringGroup(click.Group): """ A click.Group subclass that automatically registers any click.Command attributes defined on the class into the group. After initialization, it inspects its own class for attributes that are instances of click.Command (typically created via @click.command) and calls self.add_command(cmd) on each. This lets you define your commands as static methods on the subclass for IDE-friendly organization without manual registration. Example: class MyCommands(AutoRegisteringGroup): def __init__(self): super().__init__(name="mygroup", help="My commands") @staticmethod @click.command("list") def list_items(): click.echo("Listing items...") """ def __init__(self, name: str, help: str): super().__init__(name=name, help=help) # Scan class attributes for click.Command instances and register them. for attr in dir(self.__class__): cmd = getattr(self.__class__, attr) if isinstance(cmd, click.Command): self.add_command(cmd) class ToolsCommandGroup(AutoRegisteringGroup): """ Base class for 'tools' command groups across MCPs. Provides common structure for tool listing and inspection. Subclasses should implement tool_registry property. """ def __init__(self): super().__init__( name="tools", help="Commands for discovering and inspecting MCP tools." ) @property def tool_registry(self): """Override this to return the tool registry instance.""" raise NotImplementedError("Subclasses must implement tool_registry property") class ContextCommandGroup(AutoRegisteringGroup): """ Base class for 'context' command groups across MCPs. Provides common structure for context management. Subclasses should implement context_config property. """ def __init__(self): super().__init__( name="context", help="Commands for managing and inspecting contexts." ) @property def context_config(self): """Override this to return the context configuration class.""" raise NotImplementedError("Subclasses must implement context_config property") class PromptCommandGroup(AutoRegisteringGroup): """ Base class for 'prompt' command groups across MCPs. Provides common structure for prompt inspection. """ def __init__(self): super().__init__( name="prompt", help="Commands for inspecting and managing MCP prompts." ) class OutputFormat: """Utilities for formatting CLI output in different formats.""" @staticmethod def format_json(data: dict, **kwargs) -> str: """Format data as JSON with pretty printing.""" import json return json.dumps(data, indent=kwargs.get('indent', 2)) @staticmethod def format_yaml(data: dict, **kwargs) -> str: """ Format data as YAML (requires pyyaml). Raises: ImportError: If PyYAML is not installed """ try: import yaml return yaml.dump(data, default_flow_style=False, sort_keys=False) except ImportError: raise ImportError("PyYAML not installed. Install with: pip install pyyaml") @staticmethod def print_header(title: str, width: int = 80, emoji: str = ""): """ Print formatted header with optional emoji and separator. Args: title: Header title width: Width of separator line (default 80) emoji: Optional emoji prefix """ prefix = f"{emoji} " if emoji else "" click.echo(f"\n{prefix}{title}") click.echo("=" * width) @staticmethod def print_separator(width: int = 80): """Print separator line.""" click.echo("\n" + "=" * width + "\n") @staticmethod def print_error(message: str, suggestions: Optional[List[str]] = None): """ Print error message with optional suggestions. Args: message: Error message suggestions: Optional list of suggestions to display """ click.echo(f"❌ Error: {message}", err=True) if suggestions: click.echo("\nSuggestions:", err=True) for suggestion in suggestions: click.echo(f" • {suggestion}", err=True) @staticmethod def print_markdown(content: str, fallback_to_plain: bool = True): """ Print markdown with rich if available, fall back to plain text. Args: content: Markdown content to print fallback_to_plain: If True, fall back to plain text on ImportError Raises: ImportError: If rich is not installed and fallback_to_plain is False """ try: from rich.markdown import Markdown from rich.console import Console console = Console() console.print(Markdown(content)) except ImportError: if fallback_to_plain: click.echo(content) else: raise ImportError("rich not installed. Install with: pip install rich") def format_tool_list( tools: List[str], registry: Any, format_type: str = "text", show_descriptions: bool = True, show_tags: bool = True ) -> str: """ Format a list of tools for output. Args: tools: List of tool names registry: Tool registry instance format_type: "text", "json", or "names-only" show_descriptions: Include tool descriptions (text format only) show_tags: Include tool tags (text format only) Returns: Formatted string ready for output """ if format_type == "names-only": return "\n".join(sorted(tools)) if format_type == "json": import json tools_data = [] for name in sorted(tools): tool_class = registry.get_tool_class(name) tool_info = {"name": name} if hasattr(tool_class, 'get_tool_description'): tool_info["description"] = tool_class.get_tool_description() if hasattr(tool_class, 'is_optional'): tool_info["is_optional"] = tool_class.is_optional() if hasattr(tool_class, 'is_dev_only'): tool_info["is_dev_only"] = tool_class.is_dev_only() if hasattr(tool_class, 'is_mutating'): tool_info["is_mutating"] = tool_class.is_mutating() tools_data.append(tool_info) return json.dumps(tools_data, indent=2) # Text format output_lines = [f"\n📋 Available Tools ({len(tools)} tools)\n", "=" * 80] for name in sorted(tools): tool_class = registry.get_tool_class(name) description = None if hasattr(tool_class, 'get_tool_description'): description = tool_class.get_tool_description() tags = [] if show_tags: if hasattr(tool_class, 'is_optional') and tool_class.is_optional(): tags.append("optional") if hasattr(tool_class, 'is_dev_only') and tool_class.is_dev_only(): tags.append("dev-only") if hasattr(tool_class, 'is_mutating') and tool_class.is_mutating(): tags.append("mutating") tag_str = f" [{', '.join(tags)}]" if tags else "" output_lines.append(f"\n • {name}{tag_str}") if show_descriptions and description: desc_lines = description.split('\n') for line in desc_lines[:3]: # First 3 lines if len(line) > 70: output_lines.append(f" {line[:70]}...") else: output_lines.append(f" {line}") output_lines.append("\n" + "=" * 80 + "\n") return "\n".join(output_lines) def validate_file_or_exit(path: Path, error_message: Optional[str] = None) -> None: """ Validate that a file exists, exit with error if not. Args: path: Path to validate error_message: Custom error message (optional) """ if not path.exists(): msg = error_message or f"File does not exist: {path}" OutputFormat.print_error(msg) sys.exit(1) def validate_dir_or_exit(path: Path, error_message: Optional[str] = None) -> None: """ Validate that a directory exists, exit with error if not. Args: path: Path to validate error_message: Custom error message (optional) """ if not path.exists(): msg = error_message or f"Directory does not exist: {path}" OutputFormat.print_error(msg) sys.exit(1) if not path.is_dir(): OutputFormat.print_error(f"Path is not a directory: {path}") sys.exit(1) def format_context_list(contexts: List[Dict[str, Any]], format_type: str = "text") -> str: """ Format a list of contexts for output. Args: contexts: List of context dictionaries format_type: "text" or "json" Returns: Formatted string ready for output """ if format_type == "json": import json return json.dumps(contexts, indent=2) # Text format output_lines = ["\n📁 Available Contexts\n", "=" * 80] for ctx in sorted(contexts, key=lambda x: x.get('name', '')): name = ctx.get('name', 'unknown') description = ctx.get('description', '') enabled_tools = ctx.get('enabled_tools_count', 0) disabled_tools = ctx.get('disabled_tools_count', 0) output_lines.append(f"\n • {name}") if description: output_lines.append(f" {description}") output_lines.append(f" Tools enabled: {enabled_tools}") if disabled_tools: output_lines.append(f" Tools disabled: {disabled_tools}") output_lines.append("\n" + "=" * 80 + "\n") return "\n".join(output_lines)

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/y3i12/nabu_nisaba'

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