Files
server-deploy/wsl-dev-stack/godot-mcp-pro-v1.14.1/addons/godot_mcp/websocket_server.gd
Joywayer dd3eb24d0f refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack
将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 01:11:20 +08:00

255 lines
7.5 KiB
GDScript

@tool
extends Node
## Multi-connection WebSocket client.
## Connects to multiple Node.js MCP server instances on ports 6505-6514.
## Each Claude Code session gets its own port; Godot talks to all of them.
## Ports 6505-6509: MCP servers (stdio), 6510-6514: CLI tool connections.
signal client_connected()
signal client_disconnected()
signal message_received(text: String)
signal command_executed(method: String, success: bool)
signal command_completed(method: String, success: bool, response: String, source_port: int)
var command_router: Node
const BASE_PORT := 6505
const MAX_PORT := 6514
const RECONNECT_INTERVAL := 3.0
const BUFFER_SIZE := 16 * 1024 * 1024 # 16MB
const PING_INTERVAL := 5.0 # send ping every N seconds while connected
const INACTIVITY_TIMEOUT := 30.0 # force-close if no message received for N seconds
# Per-port connection state
var _peers: Dictionary = {} # port -> WebSocketPeer
var _connected: Dictionary = {} # port -> bool
var _timers: Dictionary = {} # port -> float (reconnect countdown)
var _connect_times: Dictionary = {} # port -> float (elapsed seconds since connect)
var _last_activity: Dictionary = {} # port -> float (seconds since last received message)
var _ping_timers: Dictionary = {} # port -> float (seconds since last sent ping)
var _stale_ports: Dictionary = {} # port -> bool (heartbeat timeout flag, exposed to UI)
var _running: bool = false
func start_server() -> void:
_running = true
for p in range(BASE_PORT, MAX_PORT + 1):
_connected[p] = false
_timers[p] = 0.0
_try_connect(p)
print("[MCP] Connecting to ports %d-%d" % [BASE_PORT, MAX_PORT])
func stop_server() -> void:
_running = false
for p in _peers:
var ws: WebSocketPeer = _peers[p]
if ws:
ws.close(1000, "Plugin shutting down")
_peers.clear()
_connected.clear()
_timers.clear()
_last_activity.clear()
_ping_timers.clear()
_stale_ports.clear()
print("[MCP] WebSocket client stopped")
func get_client_count() -> int:
var count: int = 0
for p in _connected:
if _connected[p]:
count += 1
return count
func get_connected_ports() -> Array[int]:
var ports: Array[int] = []
for p: int in _connected:
if _connected[p]:
ports.append(p)
return ports
func get_port_connect_time(port: int) -> float:
return _connect_times.get(port, -1.0)
func get_port_idle_time(port: int) -> float:
return _last_activity.get(port, -1.0)
func is_port_stale(port: int) -> bool:
return _stale_ports.get(port, false)
func _try_connect(p: int) -> void:
var ws := WebSocketPeer.new()
ws.outbound_buffer_size = BUFFER_SIZE
ws.inbound_buffer_size = BUFFER_SIZE
var err := ws.connect_to_url("ws://127.0.0.1:%d" % p)
if err == OK:
_peers[p] = ws
else:
_peers[p] = null
func _process(delta: float) -> void:
if not _running:
return
for p in range(BASE_PORT, MAX_PORT + 1):
var ws: WebSocketPeer = _peers.get(p)
# No peer - try reconnect on timer
if ws == null:
_timers[p] = _timers.get(p, 0.0) + delta
if _timers[p] >= RECONNECT_INTERVAL:
_timers[p] = 0.0
_try_connect(p)
continue
ws.poll()
var state := ws.get_ready_state()
match state:
WebSocketPeer.STATE_OPEN:
if not _connected.get(p, false):
_connected[p] = true
_connect_times[p] = 0.0
_last_activity[p] = 0.0
_ping_timers[p] = 0.0
_stale_ports[p] = false
_timers[p] = 0.0
print_verbose("[MCP] Connected on port %d" % p)
client_connected.emit()
else:
_connect_times[p] = _connect_times.get(p, 0.0) + delta
_last_activity[p] = _last_activity.get(p, 0.0) + delta
_ping_timers[p] = _ping_timers.get(p, 0.0) + delta
var received_any := false
while ws.get_available_packet_count() > 0:
var packet := ws.get_packet()
var text := packet.get_string_from_utf8()
received_any = true
_dispatch_message(text, p)
if received_any:
_last_activity[p] = 0.0
if _stale_ports.get(p, false):
_stale_ports[p] = false
print("[MCP] Port %d recovered from stale state" % p)
# Force-close if no message received for INACTIVITY_TIMEOUT.
# The MCP server pings every 10s, so 30s of silence means the
# connection is half-open and reconnect is the only way out.
if _last_activity.get(p, 0.0) > INACTIVITY_TIMEOUT:
push_warning("[MCP] Port %d silent for %.1fs — forcing reconnect" % [p, _last_activity[p]])
_stale_ports[p] = true
ws.close(4000, "Heartbeat timeout")
_connected[p] = false
_peers[p] = null
_timers[p] = 0.0
client_disconnected.emit()
continue
# Send periodic ping so the server can detect our death too,
# and so any reply resets our own inactivity timer.
if _ping_timers.get(p, 0.0) >= PING_INTERVAL:
_ping_timers[p] = 0.0
ws.send_text(JSON.stringify({"jsonrpc": "2.0", "method": "ping", "params": {}}))
WebSocketPeer.STATE_CLOSING:
pass
WebSocketPeer.STATE_CLOSED:
if _connected.get(p, false):
_connected[p] = false
print_verbose("[MCP] Disconnected from port %d" % p)
client_disconnected.emit()
_peers[p] = null
_timers[p] = 0.0
_last_activity[p] = 0.0
_ping_timers[p] = 0.0
WebSocketPeer.STATE_CONNECTING:
pass
func _send_to_port(p: int, text: String) -> void:
var ws: WebSocketPeer = _peers.get(p)
if ws and _connected.get(p, false):
ws.send_text(text)
func send_message(text: String) -> void:
# Broadcast to all connected peers
for p in _peers:
_send_to_port(p, text)
## Synchronous dispatch - parse JSON, handle ping/pong, queue command execution
func _dispatch_message(text: String, source_port: int) -> void:
message_received.emit(text)
var json := JSON.new()
var err := json.parse(text)
if err != OK:
_send_response(source_port, null, null, {"code": -32700, "message": "Parse error"})
return
var msg: Variant = json.data
if not msg is Dictionary:
_send_response(source_port, null, null, {"code": -32600, "message": "Invalid request"})
return
var msg_dict: Dictionary = msg
if msg_dict.get("method") == "ping":
_send_to_port(source_port, JSON.stringify({"jsonrpc": "2.0", "method": "pong", "params": {}}))
return
if msg_dict.get("method") == "pong":
return
var id: Variant = msg_dict.get("id")
var method: String = msg_dict.get("method", "")
var params: Dictionary = msg_dict.get("params", {})
if method.is_empty():
_send_response(source_port, id, null, {"code": -32600, "message": "Missing method"})
return
if not command_router:
_send_response(source_port, id, null, {"code": -32603, "message": "No command router"})
return
_execute_command.call_deferred(source_port, id, method, params)
func _execute_command(source_port: int, id: Variant, method: String, params: Dictionary) -> void:
var cmd_result: Dictionary = await command_router.execute(method, params)
if cmd_result.has("error"):
var err_data: Variant = cmd_result["error"]
_send_response(source_port, id, null, err_data)
var response_text := JSON.stringify(err_data)
command_executed.emit(method, false)
command_completed.emit(method, false, response_text, source_port)
else:
var result_data: Variant = cmd_result.get("result", {})
_send_response(source_port, id, result_data, null)
var response_text := JSON.stringify(result_data)
command_executed.emit(method, true)
command_completed.emit(method, true, response_text, source_port)
func _send_response(source_port: int, id: Variant, result: Variant, err: Variant) -> void:
var response: Dictionary = {"jsonrpc": "2.0", "id": id}
if err != null:
response["error"] = err
else:
response["result"] = result if result != null else {}
_send_to_port(source_port, JSON.stringify(response))