Skip to main content
Glama
boecht

BitSight Community MCP Server

by boecht
config.py28.7 kB
"""Config management commands for BiRRe CLI.""" from __future__ import annotations import logging import os import shutil from collections.abc import Callable, Mapping, Sequence from pathlib import Path from typing import Any, Final import click import typer from click.core import ParameterSource from rich import box from rich.console import Console from rich.table import Table from birre.cli import options as cli_options from birre.cli.formatting import ( RichStyles, create_config_table, flatten_to_dotted, format_config_value, ) from birre.cli.invocation import ( AuthCliInputs, LoggingCliInputs, RuntimeCliInputs, SubscriptionCliInputs, TlsCliInputs, build_invocation, resolve_runtime_and_logging, ) from birre.cli.models import CliInvocation from birre.cli.runtime import CONTEXT_CHOICES from birre.cli.validation import parse_toml_file, require_file_exists from birre.config.constants import ( DEFAULT_CONFIG_FILENAME, LOCAL_CONFIG_FILENAME, ) from birre.config.settings import ( BITSIGHT_API_KEY_KEY, BITSIGHT_SUBSCRIPTION_FOLDER_KEY, BITSIGHT_SUBSCRIPTION_TYPE_KEY, LOGGING_BACKUP_COUNT_KEY, LOGGING_FILE_KEY, LOGGING_FORMAT_KEY, LOGGING_LEVEL_KEY, LOGGING_MAX_BYTES_KEY, ROLE_CONTEXT_KEY, ROLE_MAX_FINDINGS_KEY, ROLE_RISK_VECTOR_FILTER_KEY, RUNTIME_ALLOW_INSECURE_TLS_KEY, RUNTIME_CA_BUNDLE_PATH_KEY, RUNTIME_DEBUG_KEY, RUNTIME_SKIP_STARTUP_CHECKS_KEY, load_settings, resolve_config_file_candidates, ) # Constants SOURCE_USER_INPUT: Final = "User Input" # Helper functions for config commands def _prompt_bool(prompt: str, default: bool) -> bool: """Prompt for a boolean value.""" return typer.confirm(prompt, default=default) def _prompt_str(prompt: str, default: str | None, secret: bool = False) -> str | None: """Prompt for a string value.""" value = typer.prompt(prompt, default=default or "", hide_input=secret).strip() return value or None def _validate_and_apply_normalizer( response: str | None, *, required: bool, normalizer: Callable[[str, str | None], str | None] | None, ) -> str | None: """Apply normalizer and validate the result.""" def _apply(value: str | None) -> str | None: cleaned = cli_options.clean_string(value) if cleaned is None: return None if normalizer is None: return cleaned return normalizer(cleaned, None) try: normalized = _apply(response) except typer.BadParameter: return None if normalized is None and required: return None return normalized def _collect_or_prompt_string( provided: str | None, *, prompt: str, default: str | None, secret: bool = False, required: bool = False, normalizer: Callable[[str, str | None], str | None] | None = None, ) -> str | None: """Return a CLI-provided string or interactively prompt for one.""" def _apply(value: str | None) -> str | None: cleaned = cli_options.clean_string(value) if cleaned is None: return None if normalizer is None: return cleaned return normalizer(cleaned, None) if provided is not None: return _apply(provided) while True: response = _prompt_str(prompt, default, secret=secret) if response is None: if required: typer.echo("A value is required") continue return None normalized = _validate_and_apply_normalizer( response, required=required, normalizer=normalizer ) if normalized is not None or not required: return normalized def _format_config_value(value: Any) -> str: """Format a value for TOML configuration file.""" if value is None: return '""' if isinstance(value, bool): return "true" if value else "false" if isinstance(value, list | tuple): formatted = ", ".join(_format_config_value(item) for item in value) return f"[{formatted}]" return f'"{value}"' def _format_config_section(section: str, section_values: dict[str, Any]) -> list[str]: """Format a single config section.""" if not isinstance(section_values, dict) or not section_values: return [] lines = ["", f"[{section}]"] for key, entry in sorted(section_values.items()): if entry is None: continue if isinstance(entry, str) and not entry.strip(): continue lines.append(f"{key} = {_format_config_value(entry)}") return lines def _generate_local_config_content( values: dict[str, Any], *, include_header: bool = True ) -> str: """Generate TOML content from config values.""" lines: list[str] = [] if include_header: lines.append("## Generated local configuration") for section, section_values in sorted(values.items()): lines.extend(_format_config_section(section, section_values)) lines.append("") return "\n".join(lines) def _determine_value_source(value: Any, default_value: Any, normalizer: Any) -> str: """Determine the source of a prompted value (Default vs User Input).""" if default_value and value == default_value: return "Default" if normalizer: normalized_default = normalizer(default_value, None) if default_value else None normalized_value = normalizer(value, None) else: normalized_default = default_value normalized_value = value if normalized_default and normalized_value == normalized_default: return "Default" return SOURCE_USER_INPUT def _prompt_and_record_string( cli_value: Any, prompt_text: Any, default_value: Any, summary_rows: Any, config_key: Any, *, normalizer: Any = None, secret: bool = False, required: bool = False, cli_source: str = "CLI Option", ) -> Any: """Prompt for a string value and add to summary if provided.""" if cli_value is not None: value = cli_value source = cli_source else: value = _collect_or_prompt_string( None, prompt=prompt_text, default=default_value, secret=secret, required=required, normalizer=normalizer, ) if value is not None: source = _determine_value_source(value, default_value, normalizer) else: return None if value and value not in (None, ""): display_value = format_config_value( config_key, value, log_file_key=LOGGING_FILE_KEY ) summary_rows.append((config_key, display_value, source)) return value def _prompt_and_record_bool( cli_value: Any, prompt_text: Any, default_value: Any, summary_rows: Any, config_key: Any, ) -> bool: """Prompt for a boolean value and add to summary.""" value: bool if cli_value is not None: value = cli_value source = "CLI Option" else: value = _prompt_bool(prompt_text, default=default_value) source = "Default" if value == default_value else SOURCE_USER_INPUT display_value = format_config_value( config_key, value, log_file_key=LOGGING_FILE_KEY ) summary_rows.append((config_key, display_value, source)) return value def _check_overwrite_destination( destination: Path, overwrite: bool, stdout_console: Console ) -> None: """Check if destination exists and handle overwrite logic.""" if not destination.exists(): return if overwrite: stdout_console.print( f"[yellow]Overwriting existing configuration at[/yellow] {destination}" ) else: stdout_console.print(f"[yellow]{destination} already exists.[/yellow]") if not typer.confirm("Overwrite this file?", default=False): stdout_console.print( "[red]Aborted without changing the existing configuration.[/red]" ) raise typer.Exit(code=1) def _display_config_preview( summary_rows: list[tuple[str, str, str]], stdout_console: Console ) -> None: """Display configuration preview table.""" if not summary_rows: return summary_rows.sort(key=lambda entry: entry[0]) preview = Table(title="Local configuration preview") preview.add_column("Config Key", style=RichStyles.ACCENT) preview.add_column("Value", style=RichStyles.SECONDARY) preview.add_column("Source", style=RichStyles.SUCCESS) for dotted_key, display_value, source in summary_rows: preview.add_row(dotted_key, display_value, source) stdout_console.print() stdout_console.print(preview) def _collect_config_file_entries( files: Sequence[Path], ) -> dict[str, tuple[Any, str]]: """Collect all config entries from files with their source filenames.""" result: dict[str, tuple[Any, str]] = {} for file in files: if not file.exists(): continue try: parsed = parse_toml_file(file, param_hint="--config") except typer.BadParameter: continue dotted = flatten_to_dotted(parsed) for key, value in dotted.items(): if key not in result: result[key] = (value, file.name) return result # Mapping of (invocation_path, settings_key) for extracting CLI overrides _CLI_OVERRIDE_MAPPINGS: Final[list[tuple[tuple[str, ...], str]]] = [ (("auth", "api_key"), BITSIGHT_API_KEY_KEY), (("subscription", "folder"), BITSIGHT_SUBSCRIPTION_FOLDER_KEY), (("subscription", "type"), BITSIGHT_SUBSCRIPTION_TYPE_KEY), (("runtime", "context"), ROLE_CONTEXT_KEY), (("runtime", "risk_vector_filter"), ROLE_RISK_VECTOR_FILTER_KEY), (("runtime", "max_findings"), ROLE_MAX_FINDINGS_KEY), (("runtime", "debug"), RUNTIME_DEBUG_KEY), (("runtime", "skip_startup_checks"), RUNTIME_SKIP_STARTUP_CHECKS_KEY), (("tls", "allow_insecure"), RUNTIME_ALLOW_INSECURE_TLS_KEY), (("tls", "ca_bundle_path"), RUNTIME_CA_BUNDLE_PATH_KEY), (("logging", "level"), LOGGING_LEVEL_KEY), (("logging", "format"), LOGGING_FORMAT_KEY), (("logging", "file_path"), LOGGING_FILE_KEY), (("logging", "max_bytes"), LOGGING_MAX_BYTES_KEY), (("logging", "backup_count"), LOGGING_BACKUP_COUNT_KEY), ] def _get_invocation_value(invocation: CliInvocation, path: tuple[str, ...]) -> Any: """Extract a value from invocation using a dotted path.""" obj: Any = invocation for key in path: obj = getattr(obj, key, None) if obj is None: return None return obj def _collect_cli_override_values(invocation: CliInvocation) -> dict[str, Any]: """Extract all CLI override values from invocation.""" details: dict[str, Any] = {} for path, settings_key in _CLI_OVERRIDE_MAPPINGS: value = _get_invocation_value(invocation, path) if value is not None: details[settings_key] = value return details def _build_cli_source_labels(invocation: CliInvocation) -> dict[str, str]: """Build source labels for CLI overrides.""" labels: dict[str, str] = {} for path, settings_key in _CLI_OVERRIDE_MAPPINGS: value = _get_invocation_value(invocation, path) if value is not None: labels[settings_key] = "CLI" return labels def _build_env_source_labels(env_overrides: Mapping[str, str]) -> dict[str, str]: """Build source labels for environment variable overrides.""" from birre.config.settings import ENVVAR_TO_SETTINGS_KEY labels: dict[str, str] = {} for env_var in env_overrides: config_key = ENVVAR_TO_SETTINGS_KEY.get(env_var) if config_key: labels[config_key] = f"ENV ({env_var})" return labels def _build_cli_override_rows( invocation: CliInvocation, ) -> Sequence[tuple[str, str, str]]: """Build table rows for CLI overrides.""" rows: list[tuple[str, str, str]] = [] for key, value in _collect_cli_override_values(invocation).items(): rows.append( (key, format_config_value(key, value, log_file_key=LOGGING_FILE_KEY), "CLI") ) return rows def _build_env_override_rows( env_overrides: Mapping[str, str], ) -> Sequence[tuple[str, str, str]]: """Build table rows for environment variable overrides.""" from birre.config.settings import ENVVAR_TO_SETTINGS_KEY rows: list[tuple[str, str, str]] = [] for env_var, value in env_overrides.items(): config_key = ENVVAR_TO_SETTINGS_KEY.get(env_var) if not config_key: continue formatted_value = format_config_value( config_key, value, log_file_key=LOGGING_FILE_KEY ) rows.append((config_key, formatted_value, f"ENV ({env_var})")) return rows _EFFECTIVE_CONFIG_KEY_ORDER: tuple[str, ...] = ( BITSIGHT_API_KEY_KEY, BITSIGHT_SUBSCRIPTION_FOLDER_KEY, BITSIGHT_SUBSCRIPTION_TYPE_KEY, ROLE_CONTEXT_KEY, ROLE_RISK_VECTOR_FILTER_KEY, ROLE_MAX_FINDINGS_KEY, RUNTIME_DEBUG_KEY, RUNTIME_SKIP_STARTUP_CHECKS_KEY, RUNTIME_ALLOW_INSECURE_TLS_KEY, RUNTIME_CA_BUNDLE_PATH_KEY, LOGGING_LEVEL_KEY, LOGGING_FORMAT_KEY, LOGGING_FILE_KEY, LOGGING_MAX_BYTES_KEY, LOGGING_BACKUP_COUNT_KEY, ) def _effective_configuration_values( runtime_settings: Any, logging_settings: Any ) -> dict[str, Any]: """Extract effective configuration values from resolved settings.""" values: dict[str, Any] = { BITSIGHT_API_KEY_KEY: getattr(runtime_settings, "api_key", None), BITSIGHT_SUBSCRIPTION_FOLDER_KEY: getattr( runtime_settings, "subscription_folder", None ), BITSIGHT_SUBSCRIPTION_TYPE_KEY: getattr( runtime_settings, "subscription_type", None ), ROLE_CONTEXT_KEY: getattr(runtime_settings, "context", None), ROLE_RISK_VECTOR_FILTER_KEY: getattr( runtime_settings, "risk_vector_filter", None ), ROLE_MAX_FINDINGS_KEY: getattr(runtime_settings, "max_findings", None), RUNTIME_DEBUG_KEY: getattr(runtime_settings, "debug", None), RUNTIME_SKIP_STARTUP_CHECKS_KEY: getattr( runtime_settings, "skip_startup_checks", None ), RUNTIME_ALLOW_INSECURE_TLS_KEY: getattr( runtime_settings, "allow_insecure_tls", None ), RUNTIME_CA_BUNDLE_PATH_KEY: getattr(runtime_settings, "ca_bundle_path", None), LOGGING_LEVEL_KEY: logging.getLevelName( getattr(logging_settings, "level", logging.INFO) ), LOGGING_FORMAT_KEY: getattr(logging_settings, "format", None), LOGGING_FILE_KEY: getattr(logging_settings, "file_path", None), LOGGING_MAX_BYTES_KEY: getattr(logging_settings, "max_bytes", None), LOGGING_BACKUP_COUNT_KEY: getattr(logging_settings, "backup_count", None), } return values def _determine_source_label( key: str, cli_labels: Mapping[str, str], env_labels: Mapping[str, str], config_entries: Mapping[str, tuple[Any, str]], ) -> str: """Determine the source label for a config key.""" if key in cli_labels: return cli_labels[key] if key in env_labels: return env_labels[key] if key in config_entries: return f"Config File ({config_entries[key][1]})" return "Default" def _print_config_table( title: str, rows: Sequence[tuple[str, str, str]], stdout_console: Console, ) -> None: """Print a formatted config table.""" table = create_config_table(title) table.columns[1].overflow = "fold" # Set overflow for value column for key, value, source in rows: table.add_row(key, value, source) stdout_console.print(table) def _cmd_config_init( output: Path, config_path: Path, subscription_type: str | None, debug: bool | None, overwrite: bool, stdout_console: Console, ) -> None: """Implementation of config init command.""" ctx = click.get_current_context() config_source = ctx.get_parameter_source("config_path") destination = Path( config_path if config_source is ParameterSource.COMMANDLINE else output ) _check_overwrite_destination(destination, overwrite, stdout_console) defaults_settings = load_settings( str(config_path) if config_source is ParameterSource.COMMANDLINE else None ) default_subscription_folder = defaults_settings.get( BITSIGHT_SUBSCRIPTION_FOLDER_KEY ) default_subscription_type = defaults_settings.get(BITSIGHT_SUBSCRIPTION_TYPE_KEY) default_context = defaults_settings.get(ROLE_CONTEXT_KEY, "standard") default_debug = bool(defaults_settings.get(RUNTIME_DEBUG_KEY, False)) summary_rows: list[tuple[str, str, str]] = [] stdout_console.print("[bold]BiRRe local configuration generator[/bold]") api_key = _collect_or_prompt_string( None, prompt="BitSight API key", default=None, secret=True, required=True, ) if api_key: display_value = format_config_value( BITSIGHT_API_KEY_KEY, api_key, log_file_key=LOGGING_FILE_KEY, ) summary_rows.append((BITSIGHT_API_KEY_KEY, display_value, SOURCE_USER_INPUT)) subscription_folder = _prompt_and_record_string( None, "Default subscription folder", (str(default_subscription_folder) if default_subscription_folder else ""), summary_rows, BITSIGHT_SUBSCRIPTION_FOLDER_KEY, ) subscription_type_value = _prompt_and_record_string( subscription_type, "Default subscription type", str(default_subscription_type) if default_subscription_type else "", summary_rows, BITSIGHT_SUBSCRIPTION_TYPE_KEY, ) context_value = _prompt_and_record_string( None, "Default persona (standard or risk_manager)", str(default_context or "standard"), summary_rows, ROLE_CONTEXT_KEY, normalizer=lambda value, _: cli_options.normalize_context( value, choices=CONTEXT_CHOICES ), ) debug_value = _prompt_and_record_bool( debug, "Enable debug mode?", default_debug, summary_rows, RUNTIME_DEBUG_KEY, ) generated = { "bitsight": { "api_key": api_key, "subscription_folder": subscription_folder, "subscription_type": subscription_type_value, }, "runtime": { "debug": debug_value, }, "roles": { "context": context_value, }, } serializable: dict[str, dict[str, Any]] = {} for section, section_values in generated.items(): filtered = {k: v for k, v in section_values.items() if v not in (None, "")} if filtered: serializable[section] = filtered if not serializable: stdout_console.print( "[red]No values provided; aborting local configuration generation.[/red]" ) raise typer.Exit(code=1) _display_config_preview(summary_rows, stdout_console) content = _generate_local_config_content(serializable) destination.parent.mkdir(parents=True, exist_ok=True) try: destination.write_text(content, encoding="utf-8") except OSError as error: stdout_console.print(f"[red]Failed to write configuration:[/red] {error}") raise typer.Exit(code=1) from error stdout_console.print(f"[green]Local configuration saved to[/green] {destination}") def _cmd_config_show( # NOSONAR python:S107 config: Path, invocation: CliInvocation, stdout_console: Console, ) -> None: """Implementation of config show command.""" runtime_settings, logging_settings, _ = resolve_runtime_and_logging(invocation) files = resolve_config_file_candidates(invocation.config_path) config_entries = _collect_config_file_entries(files) files_table = Table(title="Configuration files", box=box.SIMPLE_HEAVY) files_table.add_column("File", style=RichStyles.ACCENT) files_table.add_column("Status", style=RichStyles.SECONDARY) for file in files: status = "exists" if file.exists() else "missing" files_table.add_row(str(file), status) stdout_console.print(files_table) env_overrides = { name: os.getenv(name) for name in ( "BIRRE_CONFIG", "BITSIGHT_API_KEY", "BIRRE_SUBSCRIPTION_FOLDER", "BIRRE_SUBSCRIPTION_TYPE", "BIRRE_CONTEXT", "BIRRE_RISK_VECTOR_FILTER", "BIRRE_MAX_FINDINGS", "BIRRE_SKIP_STARTUP_CHECKS", "BIRRE_DEBUG", "BIRRE_ALLOW_INSECURE_TLS", "BIRRE_CA_BUNDLE", "BIRRE_LOG_LEVEL", "BIRRE_LOG_FORMAT", "BIRRE_LOG_FILE", "BIRRE_LOG_MAX_BYTES", "BIRRE_LOG_BACKUP_COUNT", ) if os.getenv(name) is not None } # Filter out None values for type compatibility env_overrides_filtered: dict[str, str] = { k: v for k, v in env_overrides.items() if v is not None } env_labels = _build_env_source_labels(env_overrides_filtered) env_rows = list(_build_env_override_rows(env_overrides_filtered)) if env_rows: stdout_console.print() _print_config_table("Environment overrides", env_rows, stdout_console) cli_labels = { key: label for key, label in _build_cli_source_labels(invocation).items() if key not in env_labels } cli_rows = [ row for row in _build_cli_override_rows(invocation) if row[0] not in env_labels ] if cli_rows: stdout_console.print() _print_config_table("CLI overrides", cli_rows, stdout_console) effective_values = _effective_configuration_values( runtime_settings, logging_settings ) effective_rows: list[tuple[str, str, str]] = [] for key in _EFFECTIVE_CONFIG_KEY_ORDER: display_value = format_config_value( key, effective_values.get(key), log_file_key=LOGGING_FILE_KEY, ) source_label = _determine_source_label( key, cli_labels, env_labels, config_entries ) effective_rows.append((key, display_value, source_label)) stdout_console.print() _print_config_table("Effective configuration", effective_rows, stdout_console) def _cmd_config_validate( config: Path | None, debug: bool | None, minimize: bool, stdout_console: Console, ) -> None: """Implementation of config validate command.""" ctx = click.get_current_context() config_source = ctx.get_parameter_source("config") if config is None and config_source == ParameterSource.DEFAULT: typer.echo(ctx.get_help()) raise typer.Exit() # Validate config file exists and is valid TOML config = require_file_exists(config, param_hint="--config") parsed = parse_toml_file(config, param_hint="--config") allowed_sections = {"bitsight", "runtime", "roles", "logging"} warnings: list[str] = [] for section in parsed: if section not in allowed_sections: warnings.append(f"Unknown section '{section}' will be ignored by BiRRe") stdout_console.print(f"[green]TOML parsing succeeded[/green] for {config}") if warnings: stdout_console.print("[yellow]Warnings:[/yellow]") for warning in warnings: stdout_console.print(f"- {warning}") if debug: stdout_console.print("\n[bold]Parsed data[/bold]") stdout_console.print(parsed) if minimize: minimized = _generate_local_config_content(parsed, include_header=False) backup_path = config.with_suffix(f"{config.suffix}.bak") shutil.copy2(config, backup_path) config.write_text(minimized, encoding="utf-8") stdout_console.print( f"[green]Minimized configuration written to[/green] {config} " f"[dim](backup: {backup_path})[/dim]" ) def register( app: typer.Typer, *, stdout_console: Console, ) -> None: """Register config commands with the app.""" # Config subcommands group config_app = typer.Typer( help="Manage BiRRe configuration files and settings.", invoke_without_command=True, no_args_is_help=True, ) app.add_typer(config_app, name="config") @config_app.callback(invoke_without_command=True) def config_group_callback(ctx: typer.Context) -> None: """Display help when config group is invoked without a subcommand.""" if ctx.invoked_subcommand is None: typer.echo(ctx.get_help()) raise typer.Exit() @config_app.command( "init", help="Interactively create or update a local BiRRe configuration file.", ) def config_init( output: cli_options.LocalConfOutputOption = Path(LOCAL_CONFIG_FILENAME), config_path: cli_options.ConfigPathOption = Path(LOCAL_CONFIG_FILENAME), subscription_type: cli_options.SubscriptionTypeOption = None, debug: cli_options.DebugOption = None, overwrite: cli_options.OverwriteOption = False, ) -> None: """Guide the user through generating a configuration file.""" _cmd_config_init( output, config_path, subscription_type, debug, overwrite, stdout_console ) @config_app.command( "show", help=( "Inspect configuration sources and resolved settings.\n\n" "Example: uv run birre config show --config custom.toml" ), ) def config_show( # NOSONAR python:S107 config: cli_options.ConfigPathOption = Path(DEFAULT_CONFIG_FILENAME), bitsight_api_key: cli_options.BitsightApiKeyOption = None, subscription_folder: cli_options.SubscriptionFolderOption = None, subscription_type: cli_options.SubscriptionTypeOption = None, context: cli_options.ContextOption = None, debug: cli_options.DebugOption = None, allow_insecure_tls: cli_options.AllowInsecureTlsOption = None, ca_bundle: cli_options.CaBundleOption = None, risk_vector_filter: cli_options.RiskVectorFilterOption = None, max_findings: cli_options.MaxFindingsOption = None, log_level: cli_options.LogLevelOption = None, log_format: cli_options.LogFormatOption = None, log_file: cli_options.LogFileOption = None, log_max_bytes: cli_options.LogMaxBytesOption = None, log_backup_count: cli_options.LogBackupCountOption = None, ) -> None: """Display configuration files, overrides, and effective values.""" invocation = build_invocation( config_path=str(config) if config is not None else None, context_choices=CONTEXT_CHOICES, auth=AuthCliInputs(api_key=bitsight_api_key), subscription=SubscriptionCliInputs( folder=subscription_folder, type=subscription_type, ), runtime=RuntimeCliInputs( context=context, debug=debug, risk_vector_filter=risk_vector_filter, max_findings=max_findings, skip_startup_checks=None, ), tls=TlsCliInputs( allow_insecure_tls=allow_insecure_tls, ca_bundle=ca_bundle, ), logging=LoggingCliInputs( level=log_level, format=log_format, file_path=log_file, max_bytes=log_max_bytes, backup_count=log_backup_count, ), ) _cmd_config_show(config, invocation, stdout_console) @config_app.command( "validate", help="Validate or minimize a BiRRe configuration file before use.", ) def config_validate( config: Path | None = typer.Option( None, "--config", help="Configuration TOML file to validate", envvar="BIRRE_CONFIG", show_envvar=True, rich_help_panel="Configuration", exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True, ), debug: cli_options.DebugOption = None, minimize: cli_options.MinimizeOption = False, ) -> None: """Validate configuration syntax and optionally rewrite it in minimal form.""" _cmd_config_validate(config, debug, minimize, stdout_console)

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/boecht/bitsight-community-mcp-server'

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