2026-10-09 12:49:07 +02:00

82 lines
3.7 KiB
GDScript

@tool
extends RefCounted
## Builds and sends the JSON-RPC result / error / notification frames to a peer,
## size-guarding results against the peer's send buffer; broadcasts a notification
## to a supplied set of authenticated peers. Shared by the editor server (Mode A)
## and the runtime autoload (Mode B), so it stays export-clean — no editor symbols,
## only WebSocketPeer / JSON / MCPToolkitError.
const JSONRPC_VERSION := "2.0"
## Sends a JSON-RPC result frame, guarding it against the peer's outbound buffer.
## A response larger than that buffer is rejected wholesale by the native WS path
## (no chunking) — without the guard it would vanish silently and the bridge would
## see a hung request. guard_response_size swaps it for a compact, deliverable
## RESPONSE_TOO_LARGE error so the caller learns to narrow/paginate. max_bytes is
## the peer's own buffer (set at accept), so this needs no ProjectSetting read.
## log_prefix tags the send-failure warning with the calling server's name.
static func send_result(peer: WebSocketPeer, id, result, log_prefix: String) -> void:
var response := {
"jsonrpc": JSONRPC_VERSION,
"id": id,
"result": result,
}
response = MCPToolkitError.guard_response_size(response, peer.outbound_buffer_size)
var send_err := peer.send_text(JSON.stringify(response))
if send_err != OK:
push_warning("%s send_text failed for id %s (err %d) - response not delivered" % [log_prefix, str(id), send_err])
## Sends a JSON-RPC error frame. Not size-guarded: error frames are compact by
## construction, so there is nothing to truncate.
static func send_error(peer: WebSocketPeer, id, code: int, error_message: String) -> void:
var response := {
"jsonrpc": JSONRPC_VERSION,
"id": id,
"error": {
"code": code,
"message": error_message,
},
}
peer.send_text(JSON.stringify(response))
## Sends a JSON-RPC notification frame (no id) to one peer, skipping a peer that
## is not OPEN. A notification carries no request id, so a too-large frame can't be
## answered with an error — at minimum surface a send failure so an over-buffer
## notification is never silently dropped. log_prefix tags that warning.
static func send_notification(peer: WebSocketPeer, method: String, params: Dictionary, log_prefix: String) -> void:
if peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var send_err := peer.send_text(JSON.stringify({
"jsonrpc": JSONRPC_VERSION,
"method": method,
"params": params,
}))
if send_err != OK:
push_warning("%s notification '%s' send_text failed (err %d)" % [log_prefix, method, send_err])
## Broadcasts a config-change notification to every OPEN peer in authed_peers,
## carrying no request id (callers reload their tool list, not answer a request).
## Uses the dock-broadcast envelope ({notification, params?}), distinct from the
## per-request send_notification envelope. Returns the number of peers a frame was
## sent to, so the caller can log a summary. A too-large frame can't be answered
## with an error here either — surface a send failure so a dropped frame is visible.
static func broadcast(authed_peers: Array, notification_type: String, params: Dictionary, log_prefix: String) -> int:
var payload := {"notification": notification_type}
if not params.is_empty():
payload["params"] = params
var message := JSON.stringify(payload)
var count := 0
for peer in authed_peers:
if peer is WebSocketPeer and peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
# Cast: the loop var is Variant (caller-supplied Array element), so the
# call needs a typed receiver for the Error return.
var send_err := (peer as WebSocketPeer).send_text(message)
if send_err != OK:
push_warning("%s broadcast '%s' send_text failed (err %d)" % [log_prefix, notification_type, send_err])
count += 1
return count