Skip to main content
Glama

send_platform_message_direct

Send messages directly to platform groups or users, bypassing LLM processing, with support for text, images, files, videos, and audio attachments.

Instructions

Directly send a message chain to a platform group/user (bypass LLM).

This calls AstrBot dashboard endpoint: POST /api/platform/send_message

Notes:

  • This is for sending to a real platform target (group/user), not WebChat.

  • Media parts:

    • If file_path is a local path, this tool will upload it to AstrBot first, then send it as an AstrBot-hosted URL.

    • If file_path/url is an http(s) URL, it will be forwarded as-is.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
platform_idYes
target_idYes
message_chainNo
messageNo
imagesNo
filesNo
videosNo
recordsNo
message_typeNoGroupMessage

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • The primary handler function for the 'send_platform_message_direct' tool. Processes input parameters, handles local media files by uploading them or using URLs/local paths based on config, normalizes the message chain, and invokes the AstrBotClient to send the message directly to the platform via /api/platform/send_message.
    async def send_platform_message_direct(
        platform_id: str,
        target_id: str,
        message_chain: Optional[List[MessagePart]] = None,
        message: Optional[str] = None,
        images: Optional[List[str]] = None,
        files: Optional[List[str]] = None,
        videos: Optional[List[str]] = None,
        records: Optional[List[str]] = None,
        message_type: Literal["GroupMessage", "FriendMessage"] = "GroupMessage",
    ) -> Dict[str, Any]:
        """
        Directly send a message chain to a platform group/user (bypass LLM).
    
        This calls AstrBot dashboard endpoint: POST /api/platform/send_message
    
        Notes:
          - This is for sending to a real platform target (group/user), not WebChat.
          - Media parts:
            - If `file_path` is a local path, this tool will upload it to AstrBot first, then send it as an AstrBot-hosted URL.
            - If `file_path`/`url` is an http(s) URL, it will be forwarded as-is.
        """
        client = AstrBotClient.from_env()
        onebot_like = platform_id.strip().lower() in {
            "napcat",
            "onebot",
            "cqhttp",
            "gocqhttp",
            "llonebot",
        }
    
        if message_chain is None:
            message_chain = []
            if message:
                message_chain.append({"type": "plain", "text": message})
            for src in images or []:
                message_chain.append({"type": "image", "file_path": src})
            for src in files or []:
                message_chain.append({"type": "file", "file_path": src})
            for src in records or []:
                message_chain.append({"type": "record", "file_path": src})
            for src in videos or []:
                message_chain.append({"type": "video", "file_path": src})
    
        async def build_chain(mode: str) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
            normalized_chain: List[Dict[str, Any]] = []
            uploaded_attachments: List[Dict[str, Any]] = []
    
            for part in message_chain or []:
                p_type = part.get("type")
                if p_type in ("image", "file", "record", "video"):
                    file_path = part.get("file_path")
                    url = part.get("url")
                    file_name = part.get("file_name")
                    mime_type = part.get("mime_type")
                    src = url or file_path
                    if not src:
                        continue
    
                    normalized = dict(part)
                    if not isinstance(src, str):
                        raise ValueError(f"Invalid media source (expected str): {src!r}")
    
                    if src.startswith(("http://", "https://")):
                        normalized["file_path"] = src
                        if onebot_like:
                            normalized.setdefault("file", src)
                        normalized.pop("url", None)
                        normalized_chain.append(normalized)
                        continue
    
                    try:
                        local_path = _resolve_local_file_path(client, src)
                    except ValueError as e:
                        raise ValueError(str(e)) from e
                    except FileNotFoundError as e:
                        raise FileNotFoundError(f"Local file_path does not exist: {src!r}") from e
    
                    if mode == "local":
                        normalized["file_path"] = local_path
                        normalized.pop("url", None)
                        if onebot_like:
                            uri = _as_file_uri(local_path)
                            normalized.setdefault("file", uri or local_path)
                        normalized_chain.append(normalized)
                        continue
    
                    if mode != "upload":
                        raise ValueError(f"Unknown direct media mode: {mode!r}")
    
                    if not file_name:
                        file_name = os.path.basename(local_path) or None
    
                    attach_resp = await client.post_attachment_file(
                        local_path,
                        file_name=file_name,
                        mime_type=mime_type,
                    )
    
                    if attach_resp.get("status") != "ok":
                        raise RuntimeError(attach_resp.get("message") or "Attachment upload failed")
    
                    attach_data = attach_resp.get("data") or {}
                    attachment_id = attach_data.get("attachment_id")
                    if not attachment_id:
                        raise RuntimeError(
                            "Attachment upload succeeded but attachment_id is missing"
                        )
    
                    download_url = _attachment_download_url(client, str(attachment_id))
                    normalized["file_path"] = download_url
                    if onebot_like:
                        normalized.setdefault("file", download_url)
                    normalized.pop("url", None)
                    normalized.pop("file_name", None)
                    normalized.pop("mime_type", None)
                    uploaded_attachments.append(attach_data)
                    normalized_chain.append(normalized)
                else:
                    normalized_chain.append(dict(part))
    
            return normalized_chain, uploaded_attachments
    
        # Prefer local paths (more compatible with Napcat / Windows), but keep an upload fallback.
        try:
            mode = _direct_media_mode(client)
        except ValueError as e:
            return {
                "status": "error",
                "message": str(e),
                "platform_id": platform_id,
                "session_id": str(target_id),
                "message_type": message_type,
            }
        modes_to_try = ["local", "upload"] if mode == "auto" else [mode]
        last_error: Dict[str, Any] | None = None
    
        for attempt_mode in modes_to_try:
            try:
                normalized_chain, uploaded_attachments = await build_chain(attempt_mode)
            except FileNotFoundError as e:
                return {
                    "status": "error",
                    "message": str(e),
                    "platform_id": platform_id,
                    "session_id": str(target_id),
                    "message_type": message_type,
                    "hint": "If you passed a relative path, set ASTRBOTMCP_FILE_ROOT (or run the server in the correct working directory).",
                }
            except ValueError as e:
                return {
                    "status": "error",
                    "message": str(e),
                    "platform_id": platform_id,
                    "session_id": str(target_id),
                    "message_type": message_type,
                    "hint": "Set ASTRBOTMCP_FILE_ROOT to control how relative paths are resolved.",
                }
            except Exception as e:
                return {
                    "status": "error",
                    "message": str(e),
                    "platform_id": platform_id,
                    "session_id": str(target_id),
                    "message_type": message_type,
                    "attempt_mode": attempt_mode,
                }
    
            if not normalized_chain:
                return {
                    "status": "error",
                    "message": "message_chain did not produce any valid message parts",
                    "platform_id": platform_id,
                    "session_id": str(target_id),
                    "message_type": message_type,
                }
    
            try:
                direct_resp = await client.send_platform_message_direct(
                    platform_id=platform_id,
                    message_type=message_type,
                    session_id=str(target_id),
                    message_chain=normalized_chain,
                )
            except Exception as e:
                status_code = getattr(getattr(e, "response", None), "status_code", None)
                hint = "Ensure AstrBot includes /api/platform/send_message and you are authenticated."
                if status_code in (404, 405):
                    hint = (
                        "Your AstrBot may not expose /api/platform/send_message (some versions only provide "
                        "/api/platform/stats and /api/platform/webhook). Upgrade AstrBot or add an HTTP route for sending."
                    )
                return {
                    "status": "error",
                    "message": (
                        f"AstrBot API error: HTTP {status_code}"
                        if status_code is not None
                        else f"AstrBot API error: {e}"
                    ),
                    "platform_id": platform_id,
                    "session_id": str(target_id),
                    "message_type": message_type,
                    "attempt_mode": attempt_mode,
                    "detail": _httpx_error_detail(e),
                    "hint": hint,
                }
    
            status = direct_resp.get("status")
            if status == "ok":
                data = direct_resp.get("data") or {}
                return {
                    "status": "ok",
                    "platform_id": data.get("platform_id", platform_id),
                    "session_id": data.get("session_id", str(target_id)),
                    "message_type": data.get("message_type", message_type),
                    "attempt_mode": attempt_mode,
                    "uploaded_attachments": uploaded_attachments,
                }
    
            last_error = {
                "status": status,
                "platform_id": platform_id,
                "session_id": str(target_id),
                "message_type": message_type,
                "attempt_mode": attempt_mode,
                "message": direct_resp.get("message"),
                "raw": direct_resp,
            }
    
        return last_error or {
            "status": "error",
            "message": "Failed to send message",
            "platform_id": platform_id,
            "session_id": str(target_id),
            "message_type": message_type,
        }
  • Registers the send_platform_message_direct tool handler with the FastMCP server instance.
    server.tool(astrbot_tools.send_platform_message_direct, name="send_platform_message_direct")
  • Supporting method in AstrBotClient that executes the HTTP POST request to AstrBot's /api/platform/send_message endpoint with the normalized message chain.
    async def send_platform_message_direct(
        self,
        *,
        platform_id: str,
        message_type: str,
        session_id: str,
        message_chain: List[Dict[str, Any]],
    ) -> Dict[str, Any]:
        """
        Send a message via AstrBot platform adapter (bypass LLM).
    
        Calls /api/platform/send_message (requires AstrBot >= version that includes this route).
        """
        payload: Dict[str, Any] = {
            "platform_id": platform_id,
            "message_type": message_type,
            "session_id": session_id,
            "message_chain": message_chain,
        }
        response = await self._request("POST", "/api/platform/send_message", json_body=payload)
        return response.json()
  • Function signature defines the input schema (parameters and types), including MessagePart type import. Output is Dict[str, Any] with status, platform_id, etc.
    from ..types import MessagePart
    
    
    async def send_platform_message_direct(
        platform_id: str,
        target_id: str,
        message_chain: Optional[List[MessagePart]] = None,
        message: Optional[str] = None,
        images: Optional[List[str]] = None,
        files: Optional[List[str]] = None,
        videos: Optional[List[str]] = None,
        records: Optional[List[str]] = None,
        message_type: Literal["GroupMessage", "FriendMessage"] = "GroupMessage",
    ) -> Dict[str, Any]:
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: it calls a specific endpoint (POST /api/platform/send_message), handles media uploads for local files, and forwards URLs as-is. However, it doesn't mention authentication requirements, rate limits, error conditions, or what the output contains, which are important for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the core purpose in the first sentence. The 'Notes' section efficiently covers key behavioral details. However, the second sentence about the endpoint is somewhat technical and could be integrated more smoothly, and there's minor redundancy in the media parts explanation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex mutation tool with 9 parameters, 0% schema coverage, no annotations, but an output schema, the description is moderately complete. It covers the purpose, some behavioral traits, and media handling, but lacks parameter explanations, error handling, authentication needs, and doesn't leverage the output schema to describe return values. It's adequate but has significant gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for 9 parameters. It only provides semantics for 'file_path' and 'url' in media parts, ignoring 'platform_id', 'target_id', 'message_chain', 'message', 'images', 'files', 'videos', 'records', and 'message_type'. The description adds minimal value beyond what the bare schema provides, failing to explain parameter purposes or relationships.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Directly send a message chain to a platform group/user (bypass LLM).' It specifies the verb ('send'), resource ('message chain'), and target ('platform group/user'), but doesn't explicitly differentiate from its sibling 'send_platform_message' (without '_direct'), leaving some ambiguity about what 'bypass LLM' means in practice.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides some usage context with 'This is for sending to a real platform target (group/user), not WebChat,' which implies when to use it (real platforms) and when not (WebChat). However, it doesn't explain when to choose this tool over its sibling 'send_platform_message' or other messaging alternatives, leaving the 'bypass LLM' distinction unclear for agent decision-making.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/xunxiing/astrbotmcp'

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