astral-bridge
The Astral Bridge server connects QQ and Telegram chat platforms to a fixed Astral Code app-server session. Key capabilities include:
Retrieve messages: Get recent stored messages (up to 100, with pagination), fetch a specific message by OneBot
message_id, or access the latest unread message batch for QQ and Telegram conversations.Search messages: Full-text search through stored QQ messages in a group or private conversation.
Get conversation state: Check bridge state details and message counts for a specific conversation.
Download media: Download stored image or file attachments to local cache and retrieve their file paths.
Send messages: Post to QQ groups or private chats with support for ordered mixed parts (text, @mentions, images) and replies. Also supports Telegram messaging, including voice via TTS.
Send files: Upload local files or URLs to QQ group or private chats.
Recall/delete messages and set reactions on QQ and Telegram.
QQ group administration: Manage members, handle join requests, change group settings, manage notices, files, and read group info.
Message routing: Route QQ and Telegram messages to a configured Astral app-server thread based on mentions, replies, trigger keywords, or always-trigger settings.
External webhooks: Accept and process generic external event webhooks, forwarding merged batches to Astral.
Web UI: A read-only interface to monitor connection status, routing, recent messages, and logs.
Local storage: Conversation history is stored in SQLite for later context lookups.
Enables the AI agent to communicate with QQ via the Astral Code app-server, allowing it to send and receive messages, manage conversation history, and handle media files within allowed QQ groups and private chats.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@astral-bridgesend 'Hello from Astral' to group 123456"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Astral Bridge
Astral Bridge connects QQ, through NapCat's OneBot v11 reverse WebSocket, and optionally Telegram, through the Telegram Bot API, to one long-lived Astral Code app-server session. It also exposes MCP tools so the agent can reply back to chat platforms, send files, mention people, recall/delete messages, 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, Telegram, or Astral Code.
Features
Receive QQ private and group messages from NapCat over OneBot v11.
Receive Telegram private, group, supergroup, and channel-post messages through long polling.
Route every accepted message into one Astral app-server thread, either configured explicitly or auto-created on first startup.
Trigger group messages only when the bot is mentioned, replied to, or the chat is configured as always-trigger.
Trigger every message from configured private QQ or Telegram users/chats.
Store allowed conversation history locally in SQLite for later MCP lookups.
Include compact inbound context: platform, chat/group name/id, sender id, nickname, username/card, message id, reply id, trigger kind, unread count, and attachment metadata.
Support app-server
turn/steerwhen the current Astral thread already has an active turn.Expose Streamable HTTP or stdio MCP tools for QQ and Telegram replies, history, media, files, mentions, and replies to specific message ids.
Accept generic external event webhooks and forward them into the current 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.
Related MCP server: NapCat MCP Server
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.
Optional: a Telegram bot token from BotFather if Telegram support is enabled.
Quick Start
pnpm install
cp examples/config.example.json config.json
pnpm dev -- --config ./config.jsonBuild for production:
pnpm build
pnpm start -- --config ./config.jsonPoint NapCat's OneBot v11 reverse WebSocket to:
ws://127.0.0.1:6701/onebot/v11/wsIf 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/wsConfiguration
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": "auto",
"cwd": null,
"modelConfigPath": null,
"modelProvider": null,
"model": null,
"includeImageInputs": false,
"rotateThreadOnStart": false
},
"qq": {
"enabled": true,
"botUserId": "REPLACE_WITH_BOT_QQ",
"allowedGroupIds": ["REPLACE_GROUP_ID"],
"alwaysTriggerGroupIds": [],
"allowedPrivateUserIds": ["REPLACE_USER_ID"],
"triggerKeywords": ["astral", "bot"],
"recordUntriggered": true
},
"telegram": {
"enabled": false,
"botToken": "REPLACE_WITH_TELEGRAM_BOT_TOKEN",
"botUsername": "REPLACE_WITH_TELEGRAM_BOT_USERNAME",
"allowedChatIds": ["REPLACE_TELEGRAM_CHAT_ID"],
"alwaysTriggerChatIds": [],
"triggerKeywords": ["astral", "bot"],
"recordUntriggered": true,
"pollTimeoutSeconds": 25,
"pollIntervalMs": 1000,
"apiBaseUrl": "https://api.telegram.org"
},
"tts": {
"enabled": false,
"protocol": "openai_speech",
"apiKey": null,
"baseUrl": "http://127.0.0.1:8765/v1",
"model": "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16",
"voice": null,
"format": "mp3",
"language": "Chinese",
"referenceAudioPath": "/absolute/path/on/tts-server/atri.wav",
"referenceText": "参考音频中准确说出的文字",
"timeoutMs": 60000
},
"embedding": {
"enabled": false,
"baseUrl": "http://127.0.0.1:8766/v1",
"apiKey": null,
"model": "qwen3-embedding-0.6b",
"dimensions": 1024,
"batchSize": 32,
"timeoutMs": 60000,
"queryInstruction": "Given a user query about instant-message history, retrieve messages that answer the query or express the same meaning."
},
"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 |
| Path to the JSON config file. |
| Astral app-server WebSocket URL. |
| Bearer token for Astral app-server. |
| Optional fixed Astral thread/session id. Leave empty or set |
| Auto-managed mode only. Set |
| Optional Astral |
| Optional static provider override when |
| Optional static model override when |
| Enable the QQ/NapCat channel and expose QQ MCP tools. |
| Bot QQ user id. |
| Comma-separated allowed group ids. |
| Comma-separated group ids where every non-bot message is forwarded to Astral. |
| Comma-separated allowed private user ids. |
| Comma-separated QQ trigger keywords. Any allowed group/private message containing one keyword, case-insensitively, is forwarded to Astral. |
| Enable Telegram long polling. |
| Telegram bot token from BotFather. |
| Optional bot username, without or with |
| Comma-separated Telegram |
| Comma-separated Telegram chat ids where every non-bot message is forwarded to Astral. |
| Comma-separated Telegram trigger keywords. Any allowed chat message containing one keyword, case-insensitively, is forwarded to Astral. |
| Store non-triggering Telegram messages from allowed chats. |
| Telegram |
| Delay after a failed Telegram poll before retrying. |
| Enable TTS-backed QQ/Telegram voice message tools. |
|
|
| Optional API key. Required by |
| TTS API base URL, including |
| TTS model id sent to the configured endpoint. |
| Configured bot voice, hidden from MCP tool schemas. |
| Audio response format: |
| Optional language hint sent as |
| Optional reference audio path as seen by the TTS server. |
| Exact transcript of the reference audio; must be configured together with its path. |
| TTS request timeout in milliseconds. |
| Enable asynchronous message embeddings and semantic/hybrid |
| OpenAI-compatible embeddings API base URL, including |
| Optional bearer token for the embeddings API. |
| Embedding model id sent to the configured endpoint. |
| Stored vector dimensions. Changing this rebuilds the vector index. |
| Maximum messages embedded in one background request. |
| Embedding request timeout in milliseconds. |
| Retrieval instruction prepended to semantic search queries. |
|
|
| Enable or disable the external event API. |
| External event API path, default |
| Optional bearer token required by the external event API. |
| Window used to merge attention-worthy external events before forwarding to Astral. |
| Maximum external events included in one merged Astral turn. Extra events in the same window are counted and omitted. |
| Maximum merged event body characters sent to Astral. Longer batches are truncated. |
When astral.threadId / ASTRAL_BRIDGE_THREAD_ID is empty or auto, bridge creates one
Astral thread on first use and stores the id in SQLite store_meta. To rotate an
auto-managed thread manually, call POST /api/astral/thread/rotate with the same bearer
token as the external event API. If a turn is currently active, the endpoint returns 409
instead of interrupting it.
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. For QQ this also stores member-to-member poke notices in
allowed groups; only pokes targeting the bot trigger an Astral turn.
Telegram uses long polling. On startup the bridge calls deleteWebhook so Telegram will
allow getUpdates. Send /chatid to the bot in a private chat, group, supergroup, or
topic to get the exact chat_id; this command works even before the chat is allowlisted.
All other Telegram interaction requires the chat id to be present in
telegram.allowedChatIds or telegram.alwaysTriggerChatIds.
Web UI
When HTTP MCP is enabled, open:
GET /uiThe 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.bridge]
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.bridge]
url = "http://bridge:6710/mcp"The bridge exposes both QQ and Telegram tools from the same MCP endpoint, so register it
once. Tool names remain platform-specific, for example
mcp__bridge__qq_send_group_message and mcp__bridge__telegram_send_message.
QQ tools are registered only when qq.enabled is true, Telegram tools only when
telegram.enabled is true, and voice tools only when tts.enabled is true.
MCP Tools
Shared Tool | Purpose |
| Run isolated read-only JavaScript over QQ/TG history with |
query_messages is intended for complex recall and analysis rather than ordinary recent-message
lookups. Its code argument is an async JavaScript function body that must return JSON. Each
invocation runs in a separate process against a read-only SQLite connection and is terminated
after 30 seconds. Helper limit values are defaults rather than hard maxima; Astral applies its
normal MCP-output context limit after the bridge returns the result.
return search("电路图", { platform: "qq", context_limit: 2 });When embeddings are enabled, search() defaults to hybrid FTS5 plus semantic recall. Semantic
and hybrid searches are asynchronous, while explicit lexical searches retain the synchronous
FTS5 path:
return await search("上次谁说显卡坏了", { mode: "hybrid", platform: "qq", context_limit: 2 });For custom KNN joins, use await embed(text) and bind the returned float32 vector directly to
sql(). The vector is never printed into the tool result:
const vector = await embed("串流画面问题");
return sql(
"SELECT m.id AS row_id, m.text, e.distance FROM message_embeddings e JOIN messages m ON m.id = e.message_row_id WHERE e.embedding MATCH ? AND e.k = ? ORDER BY e.distance",
vector,
20,
);Message writes and FTS5 updates never wait for the embedding service. New and historical
messages are queued in SQLite and indexed in the background. If the local model is unavailable,
semantic/hybrid calls return a clear error and mode: "lexical" remains available.
Use schema() from inside the query when exact table fields are needed. Use sql() only for
joins, grouping, time buckets, and other shapes not covered naturally by the higher-level helpers.
QQ Tool | Purpose |
| Send a group message with text, images, ordered parts, mentions, or a reply target. |
| Send a private message with text, images, ordered parts, or a reply target. |
| Generate TTS audio and send it as a real QQ group voice message. |
| Generate TTS audio and send it as a real QQ private voice message. |
| Upload a local file or URL to a QQ group. |
| Upload a local file or URL to a QQ private chat. |
| Return the unread batch counted by the latest inbound Astral prompt. |
| Return recent stored messages for one group or private conversation. |
| Return one stored message by OneBot |
| Return bridge state and counts for one conversation. |
| Download a stored image/file attachment into the local media cache. |
| React to a stored QQ group message by OneBot |
| Explain grouped QQ administration tools and actions. |
| Kick, mute/unmute, set admin, set card, set special title, or list muted members. |
| Read or handle group join/invite requests. |
| Change group name/avatar, whole-group mute, or make the bot leave the group. |
| Recall messages, manage essence messages, check @all quota, or mark group read. |
| Send, list, or delete group notices. |
| Manage group files and folders. |
| Read group, member, and honor information. |
Telegram Tool | Purpose |
| Send text with ordered text/mention parts, optional reply target, and optional topic thread id. |
| Send structured rich text with Telegram Rich Message HTML or Markdown. |
| Send a local path, Telegram |
| Generate TTS audio and send it as a real Telegram voice message. |
| Delete/recall a message when Telegram permissions allow it. Requires |
| React to a Telegram group or private message with one standard reaction emoji; the tool description lists the allowed emoji values. |
| Return the unread batch counted by the latest inbound Astral prompt. |
| Return recent stored messages for one Telegram chat. |
| Return one stored Telegram message by |
| Return bridge state and counts for one Telegram chat. |
| Download a stored Telegram attachment into the local media cache. |
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.
Telegram text messages use ordered parts for mentions:
[
{ "type": "text", "text": "请 " },
{ "type": "mention", "username": "alice" },
{ "type": "text", "text": " 看一下;也同步给 " },
{ "type": "mention", "user_id": "123456789", "text": "Bob" }
]Use telegram_send_rich_message when a Telegram reply needs structured formatting
such as headings, lists, tables, collapsible details, code blocks, or formulas. Pass
exactly one of html or markdown:
{
"chat_id": "-1001234567890",
"html": "<h2>Summary</h2><ul><li>Done</li><li>Next step</li></ul>",
"reply_to_message_id": "123",
"message_thread_id": "456"
}For Telegram files, call telegram_send_file for images and non-images alike. This sends
through Telegram sendDocument, preserving the original file quality. Telegram rich
message media blocks are intended for HTTP/HTTPS URLs; local /workspace and
/app/media files should still be sent with telegram_send_file:
{
"chat_id": "-1001234567890",
"file": "/workspace/result.png",
"caption": "结果图",
"reply_to_message_id": "123",
"message_thread_id": "456"
}Message Routing
The bridge only forwards messages from configured QQ targets or Telegram chat ids:
Group messages: forwarded when the bot is at-mentioned or the message replies to a bot message.
Keyword messages: in any allowed QQ/Telegram group or private chat, messages containing any configured trigger keyword are forwarded. Keyword matching is case-insensitive.
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.Telegram
/chatid: always answered directly so you can discover the current Telegramchat.idbefore editing the allowlist.Telegram group/supergroup/channel messages: forwarded when the bot is mentioned, the message replies to a bot message, or the chat id is in
telegram.alwaysTriggerChatIds.Telegram private messages: forwarded for every message from configured private chat ids.
/stop: in any allowed QQ/Telegram group or private chat, interrupts the active Astral turn and replies in the same conversation.
Every forwarded turn includes conversation_unread_count, 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
or telegram_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/schemaReturns 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 current 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 |
| Yes | System or integration name, such as |
| No | Event kind. Defaults to |
| No | Short title. |
| No | Main event text. |
| No | Severity label. Defaults to |
| No | Entity that caused the event. |
| No | Structured event details. |
| No | Optional stable key supplied by the caller. |
| No | ISO timestamp, Unix seconds, or Unix milliseconds. |
| No | Defaults to |
| 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 current 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
the Chinese deployment guide at deploy/orbstack/README.md before running the compose
stack. It covers required QQ/Telegram/Astral settings, persistent volumes, memory storage,
and the memory phase2 sandbox option.
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 buildProject 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 gitAcknowledgements
The design of query_messages was inspired by
Obelisk and its agent-first approach to retrieval:
give the agent a programmable query surface over a local SQLite index, then
let the agent decide how to search, inspect context, and follow the evidence. Thank you for
showing how much more capable retrieval becomes when the agent owns the query plan.
License
Licensed under the Apache License, Version 2.0. See LICENSE.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceAn MCP server that enables AI clients to send and receive QQ messages through NapCatQQ (OneBot v11) for both private and group chats. It supports message context management, real-time WebSocket listening, and human-like typing simulation.724MIT
- Flicense-qualityBmaintenanceEnables interaction with NapCat QQ bot APIs for group management, messaging, and system operations. Supports HTTP and WebSocket modes with security features like group restrictions and readonly mode.3
- AlicenseAqualityAmaintenanceQQ MCP Server with Auto-Wake, message send/receive, group management, file sharing, and timed tasks. Connects via NapCatQQ (OneBot v11). One-click setup with quickstart.ps1.Based on Amadeus-QQ-MCP.333MIT
- Alicense-qualityCmaintenanceA MCP server that exposes QQ bot capabilities over Streamable HTTP, enabling clients to query bot status, read group and friend info, fetch chat history, and send group/private text messages.2MIT
Related MCP Connectors
MCP server for AI dialogue using various LLM models via AceDataCloud
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
Telegram bridge for your MCP-compatible agent. Bidirectional, no LLM in our stack.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/oines/astral-im-bridge'
If you have feedback or need assistance with the MCP directory API, please join our Discord server