astral-bridge
Connects QQ via NapCat OneBot v11 to an Astral Code app-server, exposing MCP tools for sending messages, files, images, and fetching conversation history.
README
Astral Bridge
Astral Bridge connects QQ, through NapCat's OneBot v11 reverse WebSocket, to one fixed Astral Code app-server session. It also exposes MCP tools so the agent can reply back to QQ, send files or images, and fetch recent conversation context when needed.
This project is intended for self-hosted personal or team automation. It is not affiliated with QQ, Tencent, NapCat, OneBot, or Astral Code.
Features
- Receive QQ private and group messages from NapCat over OneBot v11.
- Route every accepted message into one configured Astral app-server thread.
- Trigger group messages only when the bot is mentioned or replied to.
- Trigger every message from configured private QQ users.
- Store allowed conversation history locally in SQLite for later MCP lookups.
- Include compact inbound context: group name/id, sender QQ, nickname, group card, sender role, message id, trigger kind, unread count, and attachment metadata.
- Support app-server
turn/steerwhen the fixed Astral thread already has an active turn. - Expose Streamable HTTP or stdio MCP tools for QQ replies, history, media, files, images, mentions, and replies to specific QQ message ids.
- Accept generic external event webhooks and forward them into the fixed Astral session.
- Expose a read-only Web UI for connection status, routing, recent messages, recent conversations, and recent bridge logs.
- Add a random 3-5 second delay before outbound QQ send actions.
Requirements
- Node.js 26 or newer. The bridge uses
node:sqlite. - pnpm 10 or newer.
- A running Astral Code app-server.
- NapCat configured with OneBot v11 reverse WebSocket.
Quick Start
pnpm install
cp examples/config.example.json config.json
pnpm dev -- --config ./config.json
Build for production:
pnpm build
pnpm start -- --config ./config.json
Point NapCat's OneBot v11 reverse WebSocket to:
ws://127.0.0.1:6701/onebot/v11/ws
If the bridge is running in Docker or on another host, replace the host and port with the address reachable from NapCat, for example:
ws://bridge:6701/onebot/v11/ws
Configuration
Start from examples/config.example.json:
{
"onebot": {
"host": "127.0.0.1",
"port": 6701,
"path": "/onebot/v11/ws",
"accessToken": null,
"actionTimeoutMs": 10000
},
"mcp": {
"transport": "stdio",
"host": "127.0.0.1",
"port": 6710,
"path": "/mcp"
},
"astral": {
"appServerUrl": "ws://127.0.0.1:4222",
"authToken": null,
"threadId": "REPLACE_WITH_FIXED_ASTRAL_THREAD_ID",
"cwd": null,
"model": null,
"includeImageInputs": true
},
"qq": {
"botUserId": "REPLACE_WITH_BOT_QQ",
"allowedGroupIds": ["REPLACE_GROUP_ID"],
"alwaysTriggerGroupIds": [],
"allowedPrivateUserIds": ["REPLACE_USER_ID"],
"recordUntriggered": true
},
"externalEvents": {
"enabled": true,
"path": "/api/events",
"authToken": "REPLACE_WITH_EVENT_API_TOKEN",
"maxBodyBytes": 65536,
"debounceMs": 2000,
"maxBatchEvents": 20,
"maxBatchBodyChars": 6000
},
"storage": {
"dbPath": "./data/astral-bridge.db",
"mediaDir": "./media",
"downloadMedia": false
}
}
Environment overrides:
| Variable | Description |
|---|---|
ASTRAL_BRIDGE_CONFIG |
Path to the JSON config file. |
ASTRAL_BRIDGE_APP_SERVER_URL |
Astral app-server WebSocket URL. |
ASTRAL_BRIDGE_APP_SERVER_AUTH_TOKEN |
Bearer token for Astral app-server. |
ASTRAL_BRIDGE_THREAD_ID |
Fixed Astral thread/session id. |
ASTRAL_BRIDGE_BOT_QQ |
Bot QQ user id. |
ASTRAL_BRIDGE_ALLOWED_GROUP_IDS |
Comma-separated allowed group ids. |
ASTRAL_BRIDGE_ALWAYS_TRIGGER_GROUP_IDS |
Comma-separated group ids where every non-bot message is forwarded to Astral. |
ASTRAL_BRIDGE_ALLOWED_PRIVATE_USER_IDS |
Comma-separated allowed private user ids. |
ASTRAL_BRIDGE_MCP_TRANSPORT |
stdio or http. |
ASTRAL_BRIDGE_EVENT_API_ENABLED |
Enable or disable the external event API. |
ASTRAL_BRIDGE_EVENT_API_PATH |
External event API path, default /api/events. |
ASTRAL_BRIDGE_EVENT_API_TOKEN |
Optional bearer token required by the external event API. |
ASTRAL_BRIDGE_EVENT_API_DEBOUNCE_MS |
Window used to merge attention-worthy external events before forwarding to Astral. |
ASTRAL_BRIDGE_EVENT_API_MAX_BATCH_EVENTS |
Maximum external events included in one merged Astral turn. Extra events in the same window are counted and omitted. |
ASTRAL_BRIDGE_EVENT_API_MAX_BATCH_BODY_CHARS |
Maximum merged event body characters sent to Astral. Longer batches are truncated. |
recordUntriggered controls whether non-triggering messages from allowed conversations
are stored. Keeping it enabled lets the agent fetch surrounding context without forwarding
every group message into Astral.
Web UI
When HTTP MCP is enabled, open:
GET /ui
The dashboard polls GET /api/dashboard/state and shows NapCat/Astral connection status,
the active Astral turn id when one is running, configured routing, external event API
status, recent conversations, recent stored messages, and recent bridge logs.
Astral MCP Setup
For stdio MCP, build first and point Astral at the compiled entrypoint:
[mcp_servers.qq]
command = "node"
args = ["/path/to/astral-bridge/dist/index.js", "--config", "/path/to/astral-bridge/config.json"]
Use node directly instead of a package-manager wrapper. stdio MCP requires stdout to
contain only JSON-RPC messages.
For container or multi-process deployments, run HTTP MCP:
{
"mcp": {
"transport": "http",
"host": "0.0.0.0",
"port": 6710,
"path": "/mcp"
}
}
Then configure Astral:
[mcp_servers.qq]
url = "http://bridge:6710/mcp"
MCP Tools
| Tool | Purpose |
|---|---|
qq_send_group_message |
Send a group message with text, images, ordered parts, mentions, or a reply target. |
qq_send_private_message |
Send a private message with text, images, ordered parts, or a reply target. |
qq_send_group_file |
Upload a local file or URL to a QQ group. |
qq_send_private_file |
Upload a local file or URL to a QQ private chat. |
qq_get_unread_messages |
Return the unread batch counted by the latest inbound Astral prompt. |
qq_get_recent_messages |
Return recent stored messages for one group or private conversation. |
qq_get_message |
Return one stored message by OneBot message_id. |
qq_search_messages |
Search stored text messages in one conversation. |
qq_get_conversation_state |
Return bridge state and counts for one conversation. |
qq_download_media |
Download a stored image/file attachment into the local media cache. |
qq_group_admin_help |
Explain grouped QQ administration tools and actions. |
qq_group_member_admin |
Kick, mute/unmute, set admin, set card, set special title, or list muted members. |
qq_group_request_admin |
Read or handle group join/invite requests. |
qq_group_settings_admin |
Change group name/avatar, whole-group mute, or make the bot leave the group. |
qq_group_message_admin |
Recall messages, manage essence messages, check @all quota, or mark group read. |
qq_group_notice_admin |
Send, list, or delete group notices. |
qq_group_file_admin |
Manage group files and folders. |
qq_group_info_admin |
Read group, member, and honor information. |
Group Administration
Group administration tools are grouped to keep the MCP tool list small. Call
qq_group_admin_help to discover action names and required fields. Disbanding groups is
intentionally not implemented; leave_group only makes the bot leave the group.
All group-specific administration tools reject group_id values outside
qq.allowedGroupIds when that allowlist is configured. Actions that mutate group state
require confirm: true; use that only after the user explicitly asks for the operation.
Mentions, Images, and Replies
Use parts when a group message needs exact ordering of text, mentions, and images:
[
{ "type": "text", "text": "请 " },
{ "type": "at", "user_id": "TARGET_QQ" },
{ "type": "text", "text": " 看一下 " },
{ "type": "image", "file": "/workspace/result.png" }
]
Reply to a specific QQ message by passing reply_to_message_id with the OneBot
message_id from the inbound prompt or one of the history tools.
Images in outbound messages use OneBot image segments. Non-image files use
NapCat-compatible upload_group_file and upload_private_file actions.
Message Routing
The bridge only forwards messages from configured QQ targets:
- Group messages: forwarded when the bot is at-mentioned or the message replies to a bot message.
- Always-trigger group messages: for group ids in
qq.alwaysTriggerGroupIds, every non-bot message is forwarded, even without an @mention or reply. - Private messages: forwarded for every message from configured private users.
- Other allowed conversation messages: optionally stored when
recordUntriggeredis true, but not forwarded to Astral.
Every forwarded turn includes a conversation_unread section. unread_count is the
number of stored messages in the same group/private conversation since the previous Astral
prompt, including the current trigger message. The agent can call qq_get_unread_messages
when that context is useful; it does not need to call it for every message.
External Event API
When HTTP MCP is enabled, the same HTTP server can accept generic external events:
GET /api/events/schema
Returns a machine-readable OpenAPI 3.1 schema with examples and curl usage.
POST /api/events
Authorization: Bearer REPLACE_WITH_EVENT_API_TOKEN
Content-Type: application/json
{
"source": "minecraft:survival-main",
"event_type": "player_join",
"title": "Player joined",
"body": "Steve joined the server",
"actor": { "id": "uuid", "name": "Steve" },
"metadata": { "world": "world", "x": 120, "y": 64, "z": -33 }
}
The bridge queues attention-worthy events, merges short bursts into one bounded Astral
turn, and submits that merged event to the fixed Astral thread using the same queue as QQ
messages. Set wants_agent_attention to false to validate and accept an event without
forwarding it into Astral. Batching is controlled by externalEvents.debounceMs,
externalEvents.maxBatchEvents, and externalEvents.maxBatchBodyChars.
Request fields:
| Field | Required | Description |
|---|---|---|
source |
Yes | System or integration name, such as minecraft:survival-main or monitoring. |
event_type / type |
No | Event kind. Defaults to event. |
title |
No | Short title. |
body / text |
No | Main event text. |
severity |
No | Severity label. Defaults to info. |
actor |
No | Entity that caused the event. |
metadata |
No | Structured event details. |
dedupe_key |
No | Optional stable key supplied by the caller. |
occurred_at |
No | ISO timestamp, Unix seconds, or Unix milliseconds. |
wants_agent_attention |
No | Defaults to true; set false for validation-only events. |
id |
No | Caller-supplied event id. A UUID is generated when omitted. |
Successful requests return 202 Accepted with { ok, accepted_for_astral, event }.
Unauthorized requests return 401 when externalEvents.authToken is configured.
Astral App-Server Behavior
The bridge talks to Astral app-server over WebSocket and uses:
initializethread/resumeturn/startturn/steerwhen the fixed thread already has an active turn
When starting a turn, the bridge requests approvalPolicy = "never" and
sandboxPolicy = { type = "dangerFullAccess" }. If you use this mode, isolate Astral at
the container, VM, or host level and only mount directories you are willing to expose.
Server approval requests for command execution and file changes are canceled by default. QQ sending should happen through the MCP tools and Astral MCP tool approval settings, not through the bridge approving arbitrary app-server actions.
Docker and OrbStack
The generic bridge image is defined by Dockerfile.
OrbStack deployment templates live under deploy/orbstack. They include:
bridge: OneBot reverse WebSocket plus Streamable HTTP MCP.astral-code: an Ubuntu-based Astral app-server runtime with mapped config, binary, source, workspace, and build cache directories.napcat: NapCat Docker service.
Copy deploy/orbstack/.env.example to .env, fill in your local values, and read
deploy/orbstack/README.md before running the compose stack.
Security Notes
- Do not commit real QQ ids, app-server tokens, API keys, NapCat WebUI tokens, SQLite databases, downloaded media, or local config files.
- Bind services to loopback unless you intentionally need LAN access.
- The app-server danger-full-access mode is powerful. Use a dedicated container or host account and mount only the workspace/config directories needed by the agent.
- Review allowed groups and private users before exposing the bot to busy chats.
Development
pnpm install
pnpm typecheck
pnpm build
Project layout:
src/ Bridge source
examples/ Example JSON config
deploy/orbstack/ Self-hosted OrbStack deployment template
data/ Runtime SQLite state, ignored by git
media/ Runtime media cache, ignored by git
License
Licensed under the Apache License, Version 2.0. See LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。