21 KiB
MCP agent features
These additions preserve the existing tool names, action parameters, text results, and read/write gates. No endpoint is contacted by catalog tools or by a local runtime overview. The action catalog remains a bundled snapshot, not a claim about the connected server's capabilities.
Protocol compatibility and catalog caching
The same snowluma-mcp stdio command accepts MCP 2026-07-28 clients and
older clients using the initialize handshake. The official server SDK selects
the protocol for the connection. Modern clients can use server/discover and
per-request metadata without initialization; legacy clients keep their existing
configuration. This does not add a publicly listening MCP HTTP endpoint.
Modern replies carry the protocol's completion and server metadata. Cache hints depend on whether a response contains static catalog data or private live state:
| Response | Lifetime | Scope |
|---|---|---|
tools/list |
Five minutes | private; mode and client capabilities affect tool metadata and availability. |
resources/list |
Five minutes, or zero when events are enabled | private; UI capability and active event resources affect the list. |
resources/templates/list |
Five minutes | public; the bundled template list is static. |
resources/read for the action catalog or message UI |
Five minutes | public; these are bundled static assets. |
resources/read for event resources |
Zero | private; these contain account events and connection state. |
These are client cache hints, not a server-side result cache. Tool calls, including the runtime overview, never receive these hints. The SDK omits modern-only result fields for legacy clients. Unsupported protocol revisions are rejected before action execution; they do not trigger an action retry.
Call tracing
Every tool invocation gets a W3C trace context. Clients can supply traceparent
and optional tracestate in the tool request's _meta:
{
"name": "query_action",
"arguments": { "action": "get_status" },
"_meta": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
MCP preserves a valid caller trace ID and creates a new span ID for each call.
Without a valid parent it creates an independent trace. The returned
_meta.traceparent identifies that call, including tool errors; the existing
text and structured result envelopes remain unchanged. This works with both
protocol generations.
Ordinary requests, streams, automatic upload chunks, and upload cleanup forward
the call context in HTTP headers, never in OneBot action parameters. Concurrent
calls keep independent headers. To follow a call, match its traceparent in MCP
stderr diagnostics with SnowLuma's http_trace entry, then follow that entry's
existing req# through action lifecycle logs. SnowLuma correlation requires
TRACE logging and a valid authenticated HTTP request; the remote trace ID does
not replace its local request ID. No telemetry exporter is installed.
MCP's call diagnostics contain the known tool name, traceparent, outcome, failure
category and duration, not message contents, tokens, file paths or arbitrary metadata.
tracestate is validated with a maximum of 512 characters and 32 members; it is
forwarded but not logged. Invalid trace metadata produces a fixed diagnostic and
does not reject an otherwise valid action. baggage is neither forwarded nor
logged. Trace identifiers are correlation data, not authentication or permission.
Optional execution services
Existing configurations keep working. Input completion and message previews are additive tools; event connections and durable tasks require explicit configuration. For example, set these variables in the MCP client's server environment, replacing addresses, credentials and paths with the intended SnowLuma instance:
{
"SNOWLUMA_MCP_MODE": "write",
"SNOWLUMA_MCP_ENDPOINT": "http://127.0.0.1:3000/",
"SNOWLUMA_MCP_TOKEN": "<your access token>",
"SNOWLUMA_MCP_EVENTS_URL": "ws://127.0.0.1:3001/event",
"SNOWLUMA_MCP_TASK_DIR": "/absolute/private/mcp-tasks",
"SNOWLUMA_MCP_UPLOAD_ROOT": "/absolute/allowed/uploads"
}
The HTTP and WebSocket addresses are independently configured server endpoints;
the WebSocket address is never guessed from the HTTP address. Events reuse
SNOWLUMA_MCP_TOKEN in an authorization header. Upload and task directories are
on the MCP host. Omit services that are not needed. Docs mode remains offline.
Completing missing parameters
query_action_with_input and invoke_action_with_input use the same arguments,
action lookup and mode gates as query_action and invoke_action. The existing
tools continue to behave exactly as before. The additional tools ask for input
only when a field listed in the action catalog's top-level required array is
absent; they do not infer conditional requirements or optional values.
For example, invoke_action_with_input with
{"action":"send_group_msg","params":{"group_id":20001}} asks for the
missing message. The form contains one params_json string, whose contents are
a JSON object such as {"message":"Hello"}. The answer is validated against
the missing fields' catalog schemas before normal action execution. Already
provided fields, the action name and execution options cannot be overwritten;
extra answer fields are rejected. A declined, cancelled or invalid answer does
not execute the action.
Clients must advertise form elicitation support. Modern clients receive
resultType: "input_required" and resume using the SDK's request-state and input
response mechanism. Legacy clients with elicitation support use the SDK's
elicitation/create flow. Clients without form support receive a tool error and
can provide complete arguments to the original tools instead.
Continuation state is signed, tied to the original tool and arguments, expires after ten minutes and is consumed once before execution. Concurrent or repeated answers cannot execute the same continuation twice. A runtime holds at most 256 pending prompts; restart invalidates them rather than replaying work. Completed arguments bypass the input round entirely. This feature is not a general approval gate and does not add confirmation prompts to existing action calls.
Live event resources
With SNOWLUMA_MCP_EVENTS_URL configured, read and write modes expose
subscribe_events and unsubscribe_events. The URL must use ws: or wss:
without embedded credentials, query parameters or fragments. Listing tools or
resources does not connect; only subscribe_events opens the event connection.
{
"self_id": 10001,
"types": ["message", "notice"],
"group_ids": [20001],
"user_ids": [30001]
}
Use the expected logged-in account for required self_id; obtain it through
get_login_info if necessary. Every incoming event must match this account.
An account mismatch or malformed event stops the connection and is surfaced.
The optional types are message, notice and request; omitted types mean all
three. message_sent is included in the message category. Group and user filters
are combined with AND, so a group filter excludes events without a matching
group_id. Each ID filter accepts at most 100 distinct positive integers.
The result contains a canonical snowluma://onebot/events/<id> resource URI.
Read it with resources/read; use ?after=<next_cursor> on subsequent reads to
obtain newer buffered entries. Cursors belong to that one resource. The response
includes status, next_cursor, oldest_cursor, dropped_events, gap and
notification_failed. Reads do not consume events. Only events received after
subscription creation are available; this is not message-history storage.
Each resource retains at most 256 events and 1 MiB of event data. One runtime supports at most 16 resources, sharing one account connection. Incoming frames are limited to 256 KiB. Buffer eviction and disconnection remain visible through the cursor/gap and status fields; the service does not silently reconnect or claim missing events were recovered. Release existing resources before changing accounts or creating a new connection after disconnection.
For updates, use the canonical URI without ?after:
- Modern clients use
subscriptions/listenwithresourceSubscriptions: [uri](the TypeScript SDK exposesclient.listen(...)). Closing that notification stream does not release the event buffer; callunsubscribe_eventsexplicitly. - Legacy clients first call
resources/subscribewith the canonical URI, then receivenotifications/resources/updated.resources/unsubscribealso releases that event resource. The explicit unsubscribe tool works for both generations.
An update notification indicates that the resource changed; fetch the resource to read its data. Releasing the last resource closes the WebSocket. Closing the MCP connection releases its event resources and buffers. No event operation executes a OneBot business action or sends a QQ message.
Persistent background transfers
Set an absolute SNOWLUMA_MCP_TASK_DIR and use write mode to enable persistent
tasks. Task tools additionally require MCP 2026-07-28 clients advertising
extensions: {"io.modelcontextprotocol/tasks": {}}. Legacy clients keep the
existing synchronous execution tools; this server does not expose the obsolete
tasks/list or tasks/result protocol methods.
invoke_action_background takes the same arguments as invoke_action, but only
accepts catalog actions marked as streams. For example, it can run
upload_file_stream with execution.input_file inside the configured upload root.
It writes the task record before starting work and returns a resultType: "task"
handle. Ordinary actions continue through the existing tools. The current
background transfer entry does not automatically combine with _with_input;
provide the required transfer parameters before starting it.
tasks/getwith{ "taskId": "..." }returns current state and, when finished, the saved result or error. Acompletedtask can contain a failed business response: inspect the nested result's diagnostics,statusandretcode.tasks/cancelrequests cooperative cancellation. Cancellation does not undo remote effects, and a successful completion already in flight may win the race. Poll the task again for its final state.tasks/updateaccepts input responses only for outstanding task input requests. It is the protocol lifecycle endpoint, not an arbitrary task-editing API. The current background transfer entry does not itself request interactive input.list_tasksis an MCP tool returning task metadata without result bodies. It accepts optionalcursorandlimit(1–100; default 50) and returnsnextCursorwhen another page is available.
Task handles advertise pollIntervalMs: 1000 and ttlMs: null. Results survive
restart and are not automatically expired or deleted. Restart marks unfinished
work failed with an explicit unknown remote outcome message; it never
replays the operation. This persists transfer status/results, not resumable
upload or download execution. There are at most four concurrent tasks and a
16 MiB limit on each persisted task record.
Storage uses an exclusive runtime lease, private directories/files (0700/0600 on POSIX) and validated records. Corruption, permission errors and write failures remain errors. The directory is bound to a digest of the effective mode, endpoint and token; changing that configuration rejects reuse of the directory. Choose a new private directory for the new configuration and retain the old history. Credentials are not stored in the scope record, but task results can contain private account data and should be protected accordingly.
Message preview panel
preview_message is available in every mode and only constructs a draft:
{
"message_type": "group",
"target_id": 20001,
"text": "Hello",
"reply_to": -123456
}
Use private for a private conversation. target_id must be a positive safe
integer; optional reply_to is a non-zero signed 32-bit message ID. The preview
supports literal text and optional quoted replies, not media rendering. Text
that resembles message control codes remains literal text.
Every client receives the same readable text draft, can_send flag and
corresponding invoke_action arguments. Only clients advertising the MCP Apps
extension io.modelcontextprotocol/ui with MIME type
text/html;profile=mcp-app receive the tool's UI metadata and listed UI resource.
The panel is a bundled, self-contained HTML asset built with the official Apps
client. It loads no external scripts, fonts or media, opens no network connection
of its own and displays external text without interpreting it as HTML. A missing
packaged asset is a visible packaging error.
Loading the panel sends nothing. Its send button calls the existing
invoke_action through the host only after the user clicks, and the server's
write gate still applies. Read/docs mode or a host without server-tool support
keeps sending disabled. The panel prevents duplicate clicks and does not retry
failed or uncertain sends; create a new draft only after checking the outcome.
Successful acknowledgement is not proof of delivery or rendering in QQ.
Action result schemas and examples
get_action and snowluma://onebot/actions include returnsSchema and
examples for every bundled action, including aliases. Both are authored beside
the OneBot action definition and copied into the generated catalog. List/search
responses remain compact; use get_action to retrieve the complete contract.
returnsSchema describes successful OneBot data, not the full OneBot
envelope or the MCP structuredContent wrapper. Check status and retcode
before reading it. For stream actions, the schema describes each successful
frame's data; MCP downloads instead return saved-file metadata and generic
streams return a frame collection. Error frames and failed envelopes are not
successful results. Unsupported actions use { "not": {} } because they have no
successful result. Compatibility placeholders explicitly document their limits.
Each example has summary, params, and optional notes explaining prerequisites
and identifier sources. These are illustrative calls, never run by catalog tools.
Replace example account/group IDs, opaque handles and paths with values from the
current account before execution. Notes identify operations with multiple steps,
such as completing an upload after all chunks have been acknowledged.
For example, get_action({ "name": "get_msg" }) documents signed message IDs:
{
"action": "get_msg",
"params": { "message_id": -123456 }
}
Pass this object to query_action with a real message ID. Writes and streams use
invoke_action in write mode. For automatic upload, pass
{ "action": "upload_file_stream", "execution": { "input_file": "/allowed/hello.txt" } }
to invoke_action, with a real file inside the configured upload root. Ordinary
action paths refer to the SnowLuma server, whereas execution.input_file and MCP
download paths refer to the MCP host.
Do not substitute zero for a signed message ID. Resource-only forward uploads
and flash shares have a documented message_id: 0 sentinel; it cannot be used
for message lookup or recall. Also preserve null versus {}, optional fields,
and decimal strings such as media file_size exactly as documented.
Structured results
Completed tool results use the advertised outputSchema and return
structuredContent alongside the existing content text block. Modern
input_required and background task handles are protocol continuations; inspect
their resultType first and obtain the completed result through the relevant
continuation or task lifecycle:
{
"result": { "status": "ok", "retcode": 0, "data": { "message_id": -123 } },
"diagnostics": { "outcome": "success" }
}
result is the original payload, including arrays such as the category list.
It is null for an MCP tool error. diagnostics.outcome is success, failed,
or unknown; failures can include kind, message, and next_step.
Ordinary OneBot business failures still return their original envelope without
setting isError. Their structured diagnostics indicate failure. An asynchronous
acknowledgement or a response that does not confirm success has outcome unknown.
Transport errors and existing tool/stream errors still set isError: true.
Error categories describe known causes; unclassified execution errors are not
guessed from message strings. Neither tool execution nor connection checks retry
automatically. Negative message IDs remain valid and are preserved unchanged.
Search, filters, and pagination
search_actions retains its original case-insensitive substring matching and
catalog order unless match: "keywords" is requested. Keyword mode combines
space-separated terms with AND, recognizes a small set of common Chinese/English
synonyms, and ranks exact action names or aliases first.
{
"query": "群 禁言",
"match": "keywords",
"read_only": false,
"stream": false,
"limit": 10,
"offset": 0
}
Both list_actions and search_actions accept optional category, read_only,
stream, limit (1–200), and offset (non-negative integer). Omitting pagination
returns the complete matching list and preserves {count, actions}. With limit
or offset, count is the returned page size, total is the matching count, and
next_offset is the next page offset or null. Pagination applies to the bundled
catalog; it does not issue server requests.
Runtime overview
get_runtime_info({}) returns:
- The effective
mode,execution_enabled, andwrite_enabled. - The bundled catalog version and action count.
server_supportremainsunknown; a matching version alone does not prove action support. - In execution mode, effective request timeout, file size and directory quota
limits, concurrent stream limit, download directory, upload root, and whether
automatic upload is enabled. In docs mode,
transportisnull. connection_checked: false. Local configuration is not proof of a live login.
The overview does not expose the endpoint, access token, or authorization headers.
Configured file paths are paths on the MCP host. All stream actions still require
write mode; upload_enabled describes the configured file root, not permission
to bypass that gate.
get_runtime_info({"check_connection": true}) is available only in read/write
mode with a client. It explicitly queries get_login_info, get_status, and
get_version_info, using the ordinary read-only execution gate. Each entry under
checks contains its original response and diagnostics. Partial failures remain
visible. Check the returned status data for online state; a successful HTTP
exchange alone does not establish that the account is online.
Cancellation and progress
MCP cancellation now reaches ordinary HTTP calls as well as stream actions, including response-body reads. Ordinary calls retain their request timeout; streams retain separate connection and per-read idle timeouts. A cancelled call does not automatically retry. Cancellation or timeout can leave a remote write's outcome unknown: stopping the wait does not undo an operation already received by SnowLuma.
When a caller includes _meta.progressToken, stream execution emits standard
notifications/progress notifications with the same token. File downloads report
decoded bytes written; automatic uploads report bytes acknowledged by the server.
Generic streams report accepted frame counts. A total is included only when a
positive file size is known. Empty files and unknown-size downloads omit it.
Notifications are limited to one per 250 ms, plus the final pending update before a successful response. Values increase, notifications stop on cancellation or completion, and callers without a progress token receive none. Notification delivery failures are surfaced. Progress describes transferred data, not final commit: only a successful tool result confirms file validation and completion.
The existing file containment, quotas, partial-file cleanup and upload-reset behavior are unchanged. File bytes never appear in progress messages.