Skip to main content
Glama
message.py31.7 kB
"""Message handling functionality for WeCom Bot MCP Server.""" # Import built-in modules from typing import Annotated from typing import Any from typing import Literal # Import third-party modules from loguru import logger from mcp.server.fastmcp import Context from notify_bridge import NotifyBridge from pydantic import Field # Import local modules from wecom_bot_mcp_server.app import mcp from wecom_bot_mcp_server.bot_config import get_bot_registry from wecom_bot_mcp_server.bot_config import get_multi_bot_instructions from wecom_bot_mcp_server.bot_config import list_available_bots from wecom_bot_mcp_server.errors import ErrorCode from wecom_bot_mcp_server.errors import WeComError from wecom_bot_mcp_server.utils import encode_text # Type alias for message types MessageType = Literal["markdown", "markdown_v2"] # Constants MESSAGE_HISTORY_KEY = "history://messages" MARKDOWN_CAPABILITIES_RESOURCE_KEY = "wecom://markdown-capabilities" MULTI_BOT_INSTRUCTIONS_KEY = "wecom://multi-bot-instructions" # Message history storage message_history: list[dict[str, str]] = [] @mcp.resource(MESSAGE_HISTORY_KEY) def get_message_history_resource() -> str: """Resource endpoint to access message history. Returns: str: Formatted message history """ return get_formatted_message_history() @mcp.resource(MULTI_BOT_INSTRUCTIONS_KEY) def get_multi_bot_instructions_resource() -> str: """Resource endpoint providing multi-bot usage instructions. Returns: str: Instructions for using multiple bots """ return get_multi_bot_instructions() def get_formatted_message_history() -> str: """Get formatted message history. Returns: str: Formatted message history as markdown """ if not message_history: return "No message history available." formatted_history = "# Message History\n\n" for idx, msg in enumerate(message_history, 1): role = msg.get("role", "unknown") content = msg.get("content", "") formatted_history += f"## {idx}. {role.capitalize()}\n\n{content}\n\n---\n\n" return formatted_history @mcp.resource(MARKDOWN_CAPABILITIES_RESOURCE_KEY) def get_markdown_capabilities_resource() -> str: """Resource endpoint describing WeCom markdown capabilities. This can be used by MCP clients or models to decide which message type to use based on the desired formatting (tables, images, colors, mentions, etc.). """ return ( "# WeCom Markdown Capabilities\n\n" "## Message Type Selection Guide\n" "**IMPORTANT**: Choose the correct msg_type based on your content:\n\n" "### Use `markdown` when:\n" "- Content contains user mentions using `<@userid>` syntax\n" '- Content uses font colors: `<font color="info|comment|warning">text</font>`\n' "- You need to @mention specific users in the message\n\n" "### Use `markdown_v2` when:\n" "- Content contains tables (using | columns |)\n" "- Content contains ordered/unordered lists\n" "- Content contains embedded images: `![alt](url)`\n" "- Content contains URLs with underscores (markdown_v2 preserves them)\n" "- Content uses multi-level quotes (>>, >>>)\n" "- Content uses horizontal rules (---)\n" "- Default choice for general formatted content without mentions\n\n" "## Common to both markdown and markdown_v2\n" "- Headers (# to ######)\n" "- Bold (**text**) and italic (*text*)\n" "- Links: [text](url)\n" "- Inline code: `code`\n" "- Block quotes: > quote\n\n" "## Only markdown\n" '- Font colors: <font color="info|comment|warning">text</font>\n' "- Mentions: <@userid> - Use this to @mention users!\n\n" "## Only markdown_v2\n" "- Tables (using | columns | and separator rows)\n" "- Lists (ordered and unordered)\n" "- Multi-level quotes (>>, >>>)\n" "- Images embedded with ![alt](url), e.g. ![chart](https://example.com/chart.png)\n" "- Horizontal rules (---)\n" "- Preserves underscores in URLs\n" "- Auto-escapes slashes in URLs\n\n" "## Image sending recommendations\n" "- If the main content is a standalone image file or screenshot, " "send it with the send_wecom_image tool (msg_type=image), passing the local file path as `image_path`.\n" "- If the image is just an illustration inside a larger report, " "use markdown_v2 and embed it with ![alt](url).\n\n" "## File sending recommendations\n" "- Use the send_wecom_file tool when sending non-image files such as " "reports, logs, or archives.\n" "- Local file paths are acceptable for `file_path`; this server will upload the file to WeCom " "via NotifyBridge and recipients will see it as an attached file message.\n" ) @mcp.prompt(title="WeCom Message Guidelines") def wecom_message_guidelines() -> str: """High-level guidelines for planning WeCom messages. This prompt explains how to choose between `markdown` and `markdown_v2` message types, when to call the image/file tools, and how to use multiple bots. """ base_guidelines = ( "When sending messages to WeCom via this MCP server, follow these rules:\n\n" "## Message Type Selection (IMPORTANT)\n" "This server supports two markdown message types. Choose based on content:\n\n" "### Use `markdown` when:\n" "- **Content contains @mentions**: If you need to mention users with `<@userid>` syntax, " "you MUST use `markdown` type. The `<@userid>` syntax ONLY works in `markdown` type.\n" '- **Content uses font colors**: `<font color="info|comment|warning">text</font>`\n' "- Example: `<@john_doe> Please review this report` → use `markdown`\n\n" "### Use `markdown_v2` when:\n" "- Content contains **tables** (using | columns |)\n" "- Content contains **lists** (ordered or unordered)\n" "- Content contains **embedded images**: `![alt](url)`\n" "- Content contains **URLs with underscores** (markdown_v2 preserves them)\n" "- Content uses **multi-level quotes** (>>, >>>)\n" "- **Default choice** for general formatted content without @mentions\n\n" "## Quick Decision Rule\n" "**If the content has `<@userid>` patterns → use `markdown`**\n" "**Otherwise → use `markdown_v2`**\n\n" "## Other Guidelines\n" "- For plain text without special formatting, use `markdown_v2`.\n" "- When you have an image URL that should appear inside the text, embed it inline " "using markdown_v2 image syntax: ![description](image_url).\n" "- You may reference local filesystem images (e.g. C:\\path\\to\\image.png or /tmp/image.png); " "this server will attempt to upload them via the WeCom file API and rewrite the markdown to use a " "remote URL when possible. If upload fails, the original path is kept as a fallback.\n" "- If the main content is an image file (local path or URL), " "call the `send_wecom_image` tool instead of embedding it in markdown.\n" "- If the user asks to send a non-image file (reports, logs, archives), " "call the `send_wecom_file` tool with the local file path.\n" "- It is safe to pass local file/image paths to these tools; this server will call NotifyBridge " "to upload the file to WeCom so recipients can access it.\n" "- URLs must be preserved exactly; do not change underscores or other " "characters inside URLs.\n\n" ) # Add multi-bot instructions multi_bot_info = get_multi_bot_instructions() return base_guidelines + multi_bot_info async def send_message( content: str, msg_type: str = "markdown_v2", mentioned_list: list[str] | None = None, mentioned_mobile_list: list[str] | None = None, bot_id: str | None = None, ctx: Context | None = None, ) -> dict[str, str]: """Send message to WeCom. Args: content: Message content msg_type: Message type. Supported values: - 'markdown': Use when content contains <@userid> mentions or font colors - 'markdown_v2': Use for tables, lists, embedded images, or general content (default) mentioned_list: List of mentioned users mentioned_mobile_list: List of mentioned mobile numbers bot_id: Bot identifier for multi-bot setups. If None, uses the default bot. ctx: FastMCP context Returns: dict: Response containing status and message Raises: WeComError: If message sending fails """ if ctx: await ctx.report_progress(0.1) await ctx.info(f"Sending {msg_type} message" + (f" via bot '{bot_id}'" if bot_id else "")) try: # Validate inputs await _validate_message_inputs(content, msg_type, ctx) # Get webhook URL for the specified bot base_url = await _get_webhook_url(bot_id, ctx) fixed_content = await _prepare_message_content(content, msg_type, ctx) # Add message to history message_history.append({"role": "assistant", "content": content}) if ctx: await ctx.report_progress(0.5) await ctx.info("Sending message...") # Send message to WeCom response = await _send_message_to_wecom( base_url, msg_type, fixed_content, mentioned_list, mentioned_mobile_list ) # Process response return await _process_message_response(response, ctx) except Exception as e: error_msg = f"Error sending message: {e!s}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.NETWORK_ERROR) from e async def _validate_message_inputs(content: str, msg_type: str, ctx: Context | None = None) -> None: """Validate message inputs. Args: content: Message content msg_type: Message type ctx: FastMCP context Raises: WeComError: If validation fails """ if not content: error_msg = "Message content cannot be empty" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) # Validate message type - only markdown and markdown_v2 are supported valid_msg_types = ("markdown", "markdown_v2") if msg_type not in valid_msg_types: error_msg = f"Invalid message type: {msg_type}. Supported types: {', '.join(valid_msg_types)}." logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) async def _get_webhook_url(bot_id: str | None = None, ctx: Context | None = None) -> str: """Get webhook URL for a specific bot. Args: bot_id: Bot identifier. If None, uses the default bot. ctx: FastMCP context Returns: str: Webhook URL Raises: WeComError: If webhook URL is not found """ try: return get_bot_registry().get_webhook_url(bot_id) except WeComError as e: if ctx: await ctx.error(str(e)) raise async def _prepare_message_content(content: str, msg_type: str = "markdown_v2", ctx: Context | None = None) -> str: """Prepare message content for sending. Args: content: Message content msg_type: Message type ('markdown' or 'markdown_v2') ctx: FastMCP context Returns: str: Encoded message content Raises: WeComError: If text encoding fails """ try: fixed_content = encode_text(content, msg_type) logger.info(f"Sending message: {fixed_content}") return fixed_content except ValueError as e: logger.error(f"Text encoding error: {e}") if ctx: await ctx.error(f"Text encoding error: {e}") raise WeComError(f"Text encoding error: {e}", ErrorCode.VALIDATION_ERROR) from e async def _send_message_to_wecom( base_url: str, msg_type: str, content: str, mentioned_list: list[str] | None = None, mentioned_mobile_list: list[str] | None = None, ) -> Any: """Send message to WeCom using NotifyBridge. This uses the latest NotifyBridge wecom interface, which expects keyword arguments rather than a payload dict. The semantics of ``msg_type`` (currently only "markdown_v2" is supported here) are implemented inside NotifyBridge. Args: base_url: Webhook URL msg_type: Message type content: Message content mentioned_list: List of mentioned users mentioned_mobile_list: List of mentioned mobile numbers Returns: Any: Response from NotifyBridge Raises: WeComError: If URL is invalid or request fails """ # Validate base_url format again before sending if not base_url.startswith("http://") and not base_url.startswith("https://"): error_msg = f"Invalid webhook URL format: '{base_url}'. URL must start with 'http://' or 'https://'" logger.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) # Use NotifyBridge to send message via the wecom channel try: async with NotifyBridge() as nb: return await nb.send_async( "wecom", webhook_url=base_url, msg_type=msg_type, content=content, mentioned_list=mentioned_list or [], mentioned_mobile_list=mentioned_mobile_list or [], ) except Exception as e: error_msg = f"Failed to send message via NotifyBridge: {e}. URL: {base_url}, Type: {msg_type}" logger.error(error_msg) raise WeComError(error_msg, ErrorCode.NETWORK_ERROR) from e async def _process_message_response(response: Any, ctx: Context | None = None) -> dict[str, str]: """Process response from WeCom API. Args: response: Response from NotifyBridge ctx: FastMCP context Returns: dict: Response containing status and message Raises: WeComError: If API call fails """ # Check response if not getattr(response, "success", False): error_msg = f"Failed to send message: {response}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.API_FAILURE) # Check WeChat API response data = getattr(response, "data", {}) if data.get("errcode", -1) != 0: error_msg = f"WeChat API error: {data.get('errmsg', 'Unknown error')}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.API_FAILURE) success_msg = "Message sent successfully" logger.info(success_msg) if ctx: await ctx.report_progress(1.0) await ctx.info(success_msg) return {"status": "success", "message": success_msg} @mcp.tool(name="send_message") async def send_message_mcp( content: str, msg_type: Annotated[ MessageType, Field( description=( "Message type. Choose based on content:\n" "- 'markdown': Use when content contains <@userid> mentions or " '<font color="...">text</font> colors. The <@userid> syntax ONLY works in markdown type.\n' "- 'markdown_v2': Use for tables, lists, embedded images ![alt](url), " "URLs with underscores, or general formatted content. This is the default.\n" "Quick rule: If content has <@userid> → use 'markdown', otherwise → use 'markdown_v2'" ) ), ] = "markdown_v2", mentioned_list: Annotated[list[str], Field(description="List of user IDs to mention")] = [], mentioned_mobile_list: Annotated[list[str], Field(description="List of mobile numbers to mention")] = [], bot_id: Annotated[ str | None, Field( description=( "Bot identifier for multi-bot setups. If not specified, uses the default bot. " "Use `list_wecom_bots` tool to see available bots. " "Example values: 'default', 'alert', 'ci', 'notify'" ) ), ] = None, ) -> dict[str, str]: """Send message to WeCom. Args: content: Message content to send msg_type: Message type. Use 'markdown' for @mentions (<@userid>), use 'markdown_v2' for tables, lists, images, or general content. mentioned_list: List of user IDs to mention mentioned_mobile_list: List of mobile numbers to mention bot_id: Bot identifier for multi-bot setups. If None, uses the default bot. Returns: dict: Response with status and message Raises: WeComError: If sending message fails """ return await send_message( content=content, msg_type=msg_type, mentioned_list=mentioned_list, mentioned_mobile_list=mentioned_mobile_list, bot_id=bot_id, ctx=None, ) async def send_wecom_template_card( template_card_type: str, *, template_card_source: dict[str, Any] | None = None, template_card_main_title: dict[str, Any] | None = None, template_card_emphasis_content: dict[str, Any] | None = None, template_card_quote_area: dict[str, Any] | None = None, template_card_sub_title_text: str | None = None, template_card_horizontal_content_list: list[dict[str, Any]] | None = None, template_card_vertical_content_list: list[dict[str, Any]] | None = None, template_card_jump_list: list[dict[str, Any]] | None = None, template_card_card_action: dict[str, Any] | None = None, template_card_image: dict[str, Any] | None = None, template_card_image_text_area: dict[str, Any] | None = None, bot_id: str | None = None, ctx: Context | None = None, ) -> dict[str, str]: """Send a WeCom template card message. This wraps notify-bridge ``msg_type="template_card"`` with the supported ``template_card_type`` values ``text_notice`` and ``news_notice``. Note: All image URLs in template cards (icon_url, image_url, etc.) must be HTTP(S) URLs. Local file paths are not supported. If you have a local image, please upload it to a public server or CDN first. Args: template_card_type: Type of template card ('text_notice' or 'news_notice') template_card_source: Source information (icon_url, desc, desc_color) template_card_main_title: Main title (title, desc) template_card_emphasis_content: Emphasis content (title, desc) template_card_quote_area: Quote area configuration template_card_sub_title_text: Sub title text template_card_horizontal_content_list: List of horizontal content items template_card_vertical_content_list: List of vertical content items template_card_jump_list: List of jump links template_card_card_action: Card action configuration (type, url, appid, pagepath) template_card_image: Image configuration for news_notice type template_card_image_text_area: Image text area for news_notice type bot_id: Bot identifier for multi-bot setups. If None, uses the default bot. ctx: FastMCP context """ if ctx: await ctx.report_progress(0.1) await ctx.info( f"Sending template_card ({template_card_type}) message" + (f" via bot '{bot_id}'" if bot_id else "") ) valid_types = ("text_notice", "news_notice") if template_card_type not in valid_types: error_msg = f"Invalid template_card_type: {template_card_type}. Allowed values: {', '.join(valid_types)}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) required_fields = { "template_card_source": template_card_source, "template_card_main_title": template_card_main_title, "template_card_card_action": template_card_card_action, } missing = [name for name, value in required_fields.items() if value is None] if missing: error_msg = f"Missing required template card fields: {', '.join(missing)}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) try: base_url = await _get_webhook_url(bot_id, ctx) if ctx: await ctx.report_progress(0.3) await ctx.info("Sending template card via notify-bridge...") template_kwargs: dict[str, Any] = { "template_card_source": template_card_source, "template_card_main_title": template_card_main_title, "template_card_card_action": template_card_card_action, } if template_card_emphasis_content is not None: template_kwargs["template_card_emphasis_content"] = template_card_emphasis_content if template_card_quote_area is not None: template_kwargs["template_card_quote_area"] = template_card_quote_area if template_card_sub_title_text is not None: template_kwargs["template_card_sub_title_text"] = template_card_sub_title_text if template_card_horizontal_content_list: template_kwargs["template_card_horizontal_content_list"] = template_card_horizontal_content_list if template_card_vertical_content_list: template_kwargs["template_card_vertical_content_list"] = template_card_vertical_content_list if template_card_jump_list: template_kwargs["template_card_jump_list"] = template_card_jump_list if template_card_image is not None: template_kwargs["template_card_image"] = template_card_image if template_card_image_text_area is not None: template_kwargs["template_card_image_text_area"] = template_card_image_text_area response = await _send_template_card_to_wecom( base_url=base_url, template_card_type=template_card_type, **template_kwargs, ) return await _process_template_card_response(response, ctx) except Exception as e: error_msg = f"Error sending template card: {e!s}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.NETWORK_ERROR) from e async def _send_template_card_to_wecom( base_url: str, template_card_type: str, **template_kwargs: Any, ) -> Any: """Send a template card message to WeCom using NotifyBridge. Args: base_url: Webhook URL template_card_type: Template card type ("text_notice" or "news_notice") template_kwargs: Template card-specific keyword arguments Returns: Any: Response from NotifyBridge """ if not base_url.startswith("http://") and not base_url.startswith("https://"): error_msg = f"Invalid webhook URL format: '{base_url}'. URL must start with 'http://' or 'https://'" logger.error(error_msg) raise WeComError(error_msg, ErrorCode.VALIDATION_ERROR) try: # Debug: log the template_kwargs logger.debug(f"Sending template card with kwargs: {template_kwargs}") async with NotifyBridge() as nb: return await nb.send_async( "wecom", webhook_url=base_url, msg_type="template_card", template_card_type=template_card_type, **template_kwargs, ) except Exception as e: error_msg = ( f"Failed to send template card via NotifyBridge: {e}. URL: {base_url}, " f"template_card_type: {template_card_type}" ) logger.error(error_msg) raise WeComError(error_msg, ErrorCode.NETWORK_ERROR) from e async def _process_template_card_response( response: Any, ctx: Context | None = None, ) -> dict[str, str]: """Process response from WeCom template_card API. Args: response: Response from NotifyBridge ctx: FastMCP context Returns: dict: Response containing status and message Raises: WeComError: If API call fails """ if not getattr(response, "success", False): error_msg = f"Failed to send template card: {response}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.API_FAILURE) data = getattr(response, "data", {}) or {} if data.get("errcode", -1) != 0: error_msg = f"WeChat API error: {data.get('errmsg', 'Unknown error')}" logger.error(error_msg) if ctx: await ctx.error(error_msg) raise WeComError(error_msg, ErrorCode.API_FAILURE) success_msg = "Template card sent successfully" logger.info(success_msg) if ctx: await ctx.report_progress(1.0) await ctx.info(success_msg) return {"status": "success", "message": success_msg} @mcp.tool(name="send_wecom_template_card_text_notice") async def send_wecom_template_card_text_notice_mcp( template_card_source: Annotated[ dict[str, Any], Field(description="Source info for the card (icon_url, desc, desc_color, etc.)"), ], template_card_main_title: Annotated[ dict[str, Any], Field(description="Main title and description for the card."), ], template_card_card_action: Annotated[ dict[str, Any], Field(description="Primary click action for the card."), ], template_card_emphasis_content: Annotated[ dict[str, Any] | None, Field(description="Emphasised numeric content area."), ] = None, template_card_quote_area: Annotated[ dict[str, Any] | None, Field(description="Quote area configuration."), ] = None, template_card_sub_title_text: Annotated[ str | None, Field(description="Subtitle text under the main title."), ] = None, template_card_horizontal_content_list: Annotated[ list[dict[str, Any]] | None, Field(description="Horizontal content items."), ] = None, template_card_jump_list: Annotated[ list[dict[str, Any]] | None, Field(description="Jump (link) buttons."), ] = None, bot_id: Annotated[ str | None, Field( description=( "Bot identifier for multi-bot setups. If not specified, uses the default bot. " "Use `list_wecom_bots` tool to see available bots." ) ), ] = None, ctx: Context | None = None, ) -> dict[str, str]: """MCP tool wrapper for sending a text_notice template card. The structure of the template card fields follows the WeCom template_card documentation and notify-bridge examples. """ return await send_wecom_template_card( template_card_type="text_notice", template_card_source=template_card_source, template_card_main_title=template_card_main_title, template_card_emphasis_content=template_card_emphasis_content, template_card_quote_area=template_card_quote_area, template_card_sub_title_text=template_card_sub_title_text, template_card_horizontal_content_list=template_card_horizontal_content_list, template_card_vertical_content_list=None, template_card_jump_list=template_card_jump_list, template_card_card_action=template_card_card_action, template_card_image=None, template_card_image_text_area=None, bot_id=bot_id, ctx=ctx, ) @mcp.tool(name="send_wecom_template_card_news_notice") async def send_wecom_template_card_news_notice_mcp( template_card_source: Annotated[ dict[str, Any], Field(description="Source info for the card (icon_url, desc, desc_color, etc.)"), ], template_card_main_title: Annotated[ dict[str, Any], Field(description="Main title and description for the card."), ], template_card_card_action: Annotated[ dict[str, Any], Field(description="Primary click action for the card."), ], template_card_image: Annotated[ dict[str, Any] | None, Field( description="Main image configuration for the news_notice card. " "Should contain 'url' and optionally 'aspect_ratio'." ), ] = None, template_card_image_text_area: Annotated[ dict[str, Any] | None, Field(description="Image text area configuration. Should contain 'image_url', 'title', 'desc', etc."), ] = None, template_card_quote_area: Annotated[ dict[str, Any] | None, Field(description="Quote area configuration."), ] = None, template_card_vertical_content_list: Annotated[ list[dict[str, Any]] | None, Field(description="Vertical content items."), ] = None, template_card_horizontal_content_list: Annotated[ list[dict[str, Any]] | None, Field(description="Horizontal content items."), ] = None, template_card_jump_list: Annotated[ list[dict[str, Any]] | None, Field(description="Jump (link) buttons."), ] = None, bot_id: Annotated[ str | None, Field( description=( "Bot identifier for multi-bot setups. If not specified, uses the default bot. " "Use `list_wecom_bots` tool to see available bots." ) ), ] = None, ctx: Context | None = None, ) -> dict[str, str]: """MCP tool wrapper for sending a news_notice template card.""" return await send_wecom_template_card( template_card_type="news_notice", template_card_source=template_card_source, template_card_main_title=template_card_main_title, template_card_emphasis_content=None, template_card_quote_area=template_card_quote_area, template_card_sub_title_text=None, template_card_horizontal_content_list=template_card_horizontal_content_list, template_card_vertical_content_list=template_card_vertical_content_list, template_card_jump_list=template_card_jump_list, template_card_card_action=template_card_card_action, template_card_image=template_card_image, template_card_image_text_area=template_card_image_text_area, bot_id=bot_id, ctx=ctx, ) @mcp.tool(name="list_wecom_bots") async def list_wecom_bots_mcp() -> dict[str, Any]: """List all configured WeCom bots. Use this tool to discover available bots before sending messages. Each bot has an id, name, and optional description. Returns: dict: Contains 'bots' list and 'count' of available bots. Each bot entry has: id, name, description, has_webhook """ bots = list_available_bots() registry = get_bot_registry() return { "bots": bots, "count": len(bots), "has_multiple_bots": registry.has_multiple_bots(), "default_bot": "default" if registry.has_bot("default") else (bots[0]["id"] if bots else None), "instructions": ( "To send a message to a specific bot, use the 'bot_id' parameter in send_message, " "send_wecom_image, or send_wecom_file tools. " "If bot_id is not specified, the default bot will be used." ), }

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