Skip to main content
Glama
bot_config.py12.6 kB
"""Multi-bot configuration management for WeCom Bot MCP Server. This module provides support for configuring and managing multiple WeCom bots. It maintains backward compatibility with the single WECOM_WEBHOOK_URL environment variable while adding support for multiple bots via WECOM_BOTS configuration. Configuration Methods: 1. Single bot (backward compatible): - Set WECOM_WEBHOOK_URL environment variable 2. Multiple bots: - Set WECOM_BOTS environment variable as JSON: WECOM_BOTS='{"bot1": {"name": "Alert Bot", "webhook_url": "https://..."}, ...}' - Or set individual WECOM_BOT_<NAME>_URL variables: WECOM_BOT_ALERT_URL="https://..." WECOM_BOT_NOTIFY_URL="https://..." 3. Combined mode: - WECOM_WEBHOOK_URL becomes the "default" bot - Additional bots can be configured via WECOM_BOTS or WECOM_BOT_<NAME>_URL """ # Import built-in modules from dataclasses import dataclass from dataclasses import field from functools import lru_cache import json import os import re from typing import Any # Import third-party modules from loguru import logger # Import local modules from wecom_bot_mcp_server.errors import ErrorCode from wecom_bot_mcp_server.errors import WeComError # Constants DEFAULT_BOT_NAME = "default" ENV_WEBHOOK_URL = "WECOM_WEBHOOK_URL" ENV_BOTS_CONFIG = "WECOM_BOTS" ENV_BOT_URL_PATTERN = re.compile(r"^WECOM_BOT_(\w+)_URL$") @dataclass class BotConfig: """Configuration for a single WeCom bot. Attributes: name: Human-readable name for the bot (e.g., "Alert Bot", "CI Notify") webhook_url: The webhook URL for sending messages description: Optional description of the bot's purpose """ name: str webhook_url: str description: str = "" metadata: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: """Validate the bot configuration after initialization.""" if not self.webhook_url: raise WeComError( f"Bot '{self.name}' has empty webhook_url", ErrorCode.VALIDATION_ERROR, ) if not self.webhook_url.startswith(("http://", "https://")): raise WeComError( f"Bot '{self.name}' webhook_url must start with 'http://' or 'https://'. Got: '{self.webhook_url}'", ErrorCode.VALIDATION_ERROR, ) class BotRegistry: """Registry for managing multiple WeCom bots. This class provides methods to register, retrieve, and list bots. It automatically loads configuration from environment variables. """ def __init__(self) -> None: """Initialize the bot registry.""" self._bots: dict[str, BotConfig] = {} self._loaded = False def _ensure_loaded(self) -> None: """Ensure bots are loaded from environment.""" if not self._loaded: self._load_from_environment() self._loaded = True def _load_from_environment(self) -> None: """Load bot configurations from environment variables. Loading priority: 1. WECOM_WEBHOOK_URL -> "default" bot (backward compatible) 2. WECOM_BOTS JSON -> multiple bots 3. WECOM_BOT_<NAME>_URL -> individual bot URLs """ # 1. Load default bot from WECOM_WEBHOOK_URL (backward compatible) default_url = os.getenv(ENV_WEBHOOK_URL) if default_url: try: self._bots[DEFAULT_BOT_NAME] = BotConfig( name=DEFAULT_BOT_NAME, webhook_url=default_url.strip(), description="Default bot (from WECOM_WEBHOOK_URL)", ) logger.debug(f"Loaded default bot from {ENV_WEBHOOK_URL}") except WeComError as e: logger.warning(f"Invalid {ENV_WEBHOOK_URL}: {e}") # 2. Load from WECOM_BOTS JSON configuration bots_json = os.getenv(ENV_BOTS_CONFIG) if bots_json: try: bots_data = json.loads(bots_json) if isinstance(bots_data, dict): for bot_id, bot_info in bots_data.items(): self._register_bot_from_dict(bot_id, bot_info) logger.debug(f"Loaded {len(bots_data)} bot(s) from {ENV_BOTS_CONFIG}") except json.JSONDecodeError as e: logger.warning(f"Invalid JSON in {ENV_BOTS_CONFIG}: {e}") # 3. Load from individual WECOM_BOT_<NAME>_URL variables for key, value in os.environ.items(): match = ENV_BOT_URL_PATTERN.match(key) if match and value: bot_id = match.group(1).lower() if bot_id not in self._bots: try: self._bots[bot_id] = BotConfig( name=bot_id, webhook_url=value.strip(), description=f"Bot from {key}", ) logger.debug(f"Loaded bot '{bot_id}' from {key}") except WeComError as e: logger.warning(f"Invalid {key}: {e}") def _register_bot_from_dict(self, bot_id: str, bot_info: dict[str, Any] | str) -> None: """Register a bot from dictionary or string configuration. Args: bot_id: Unique identifier for the bot bot_info: Bot configuration (dict with name/webhook_url/description or just URL string) """ bot_id = bot_id.lower() try: if isinstance(bot_info, str): # Simple format: {"bot_id": "webhook_url"} self._bots[bot_id] = BotConfig( name=bot_id, webhook_url=bot_info.strip(), ) elif isinstance(bot_info, dict): # Full format: {"bot_id": {"name": "...", "webhook_url": "...", "description": "..."}} self._bots[bot_id] = BotConfig( name=bot_info.get("name", bot_id), webhook_url=bot_info.get("webhook_url", "").strip(), description=bot_info.get("description", ""), metadata=bot_info.get("metadata", {}), ) except WeComError as e: logger.warning(f"Failed to register bot '{bot_id}': {e}") def register(self, bot_id: str, config: BotConfig) -> None: """Register a bot configuration. Args: bot_id: Unique identifier for the bot config: Bot configuration """ self._ensure_loaded() self._bots[bot_id.lower()] = config logger.info(f"Registered bot '{bot_id}' ({config.name})") def get(self, bot_id: str | None = None) -> BotConfig: """Get a bot configuration by ID. Args: bot_id: Bot identifier. If None or empty, returns the default bot. Returns: BotConfig: The bot configuration Raises: WeComError: If bot is not found or no bots are configured """ self._ensure_loaded() # Use default bot if no bot_id specified if not bot_id: bot_id = DEFAULT_BOT_NAME bot_id = bot_id.lower() if bot_id not in self._bots: available = list(self._bots.keys()) if not available: raise WeComError( "No bots configured. Set WECOM_WEBHOOK_URL or WECOM_BOTS environment variable.", ErrorCode.VALIDATION_ERROR, ) raise WeComError( f"Bot '{bot_id}' not found. Available bots: {', '.join(available)}", ErrorCode.VALIDATION_ERROR, ) return self._bots[bot_id] def get_webhook_url(self, bot_id: str | None = None) -> str: """Get webhook URL for a bot. Args: bot_id: Bot identifier. If None, returns the default bot's URL. Returns: str: The webhook URL """ return self.get(bot_id).webhook_url def list_bots(self) -> list[dict[str, str | bool]]: """List all configured bots. Returns: list: List of bot information dictionaries """ self._ensure_loaded() return [ { "id": bot_id, "name": config.name, "description": config.description, "has_webhook": bool(config.webhook_url), } for bot_id, config in self._bots.items() ] def has_bot(self, bot_id: str) -> bool: """Check if a bot is registered. Args: bot_id: Bot identifier Returns: bool: True if bot exists """ self._ensure_loaded() return bot_id.lower() in self._bots def has_multiple_bots(self) -> bool: """Check if multiple bots are configured. Returns: bool: True if more than one bot is configured """ self._ensure_loaded() return len(self._bots) > 1 def get_bot_count(self) -> int: """Get the number of configured bots. Returns: int: Number of bots """ self._ensure_loaded() return len(self._bots) def clear(self) -> None: """Clear all registered bots (mainly for testing).""" self._bots.clear() self._loaded = False def reload(self) -> None: """Reload bot configurations from environment.""" self.clear() self._ensure_loaded() # Global bot registry instance _bot_registry: BotRegistry | None = None def get_bot_registry() -> BotRegistry: """Get the global bot registry instance. Returns: BotRegistry: The global bot registry """ global _bot_registry if _bot_registry is None: _bot_registry = BotRegistry() return _bot_registry @lru_cache def get_default_webhook_url() -> str: """Get the default webhook URL (backward compatible). This function maintains backward compatibility with code that expects a single webhook URL. Returns: str: The default webhook URL Raises: WeComError: If no default bot is configured """ return get_bot_registry().get_webhook_url() def get_webhook_url_for_bot(bot_id: str | None = None) -> str: """Get webhook URL for a specific bot. Args: bot_id: Bot identifier. If None, returns the default bot's URL. Returns: str: The webhook URL """ return get_bot_registry().get_webhook_url(bot_id) def list_available_bots() -> list[dict[str, str | bool]]: """List all available bots. Returns: list: List of bot information """ return get_bot_registry().list_bots() def get_multi_bot_instructions() -> str: """Get instructions for AI on how to use multiple bots. Returns: str: Instructions text for AI assistants """ registry = get_bot_registry() bots = registry.list_bots() if not bots: return ( "No WeCom bots are configured. Please set the WECOM_WEBHOOK_URL environment " "variable or configure multiple bots via WECOM_BOTS." ) if len(bots) == 1: bot = bots[0] return ( f"One WeCom bot is configured: '{bot['name']}' (id: {bot['id']}). " "All messages will be sent to this bot. You don't need to specify a bot_id." ) # Multiple bots configured bot_list = "\n".join( f" - **{bot['id']}**: {bot['name']}" + (f" - {bot['description']}" if bot["description"] else "") for bot in bots ) return ( f"## Multiple WeCom Bots Available\n\n" f"This server has {len(bots)} bots configured:\n{bot_list}\n\n" f"### How to Send Messages to Specific Bots\n" f"When calling `send_message`, `send_wecom_image`, or `send_wecom_file`, " f"you can specify the `bot_id` parameter to choose which bot to use:\n\n" f"- If `bot_id` is not specified, messages go to the **default** bot.\n" f"- To send to a specific bot, set `bot_id` to one of: {', '.join(b['id'] for b in bots)}\n\n" f"### Use Case Examples\n" f"- **Alert notifications**: Use the bot designated for alerts\n" f"- **CI/CD notifications**: Use the bot for build/deploy notifications\n" f"- **Team updates**: Use team-specific bots for targeted messaging\n\n" f"### Listing Bots\n" f"Call the `list_wecom_bots` tool to see all available bots and their configurations." )

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/loonghao/wecom-bot-mcp-server'

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