CoplayDev/unity-mcp · error · ConnectionError

Unexpected handshake from server: {line!r}

Error message

Unexpected handshake from server: {line!r}

What it means

Raised by do_handshake() in stress_mcp.py when the first line from the server does not contain 'WELCOME UNITY-MCP'. The bridge sends 'WELCOME UNITY-MCP 1 FRAMING=1\n' on connect; a mismatch means the connection was closed before the greeting (empty line) or the remote service is not the Unity bridge.

Source

Thrown at tools/stress_mcp.py:82

    header = await read_exact(reader, 8)
    (length,) = struct.unpack(">Q", header)
    if length <= 0 or length > (64 * 1024 * 1024):
        raise ValueError(f"Invalid frame length: {length}")
    return await read_exact(reader, length)


async def write_frame(writer: asyncio.StreamWriter, payload: bytes) -> None:
    header = struct.pack(">Q", len(payload))
    writer.write(header)
    writer.write(payload)
    await asyncio.wait_for(writer.drain(), timeout=TIMEOUT)


async def do_handshake(reader: asyncio.StreamReader) -> None:
    # Server sends a single line handshake: "WELCOME UNITY-MCP 1 FRAMING=1\n"
    line = await reader.readline()
    if not line or b"WELCOME UNITY-MCP" not in line:
        raise ConnectionError(f"Unexpected handshake from server: {line!r}")


def make_ping_frame() -> bytes:
    return b"ping"


def make_execute_menu_item(menu_path: str) -> bytes:
    # Retained for manual debugging; not used in normal stress runs
    payload = {"type": "execute_menu_item", "params": {
        "action": "execute", "menu_path": menu_path}}
    return json.dumps(payload).encode("utf-8")


async def client_loop(idx: int, host: str, port: int, stop_time: float, stats: dict):
    reconnect_delay = 0.2
    while time.time() < stop_time:
        writer = None
        try:

View on GitHub (pinned to c21bf496bc)

Solutions

  1. Check ~/.unity-mcp/unity-mcp-status-*.json for the current unity_port and pass it explicitly.
  2. Ensure Unity is fully loaded with the MCP for Unity package before starting the stress test.
  3. If the greeting changed in a bridge update, update the b'WELCOME UNITY-MCP' sentinel in do_handshake.
Defensive patterns

Strategy: validation

Validate before calling

import json, socket
from pathlib import Path

def bridge_ready(host: str, port: int) -> bool:
    if not (0 < port < 65536):
        return False
    try:
        with socket.create_connection((host, port), timeout=2.0) as s:
            greeting = s.recv(128)
            return b"WELCOME UNITY-MCP" in greeting
    except OSError:
        return False

Try / catch

try:
    await asyncio.wait_for(do_handshake(reader), timeout=TIMEOUT)
except ConnectionError:
    stats["disconnects"] += 1
    await asyncio.sleep(reconnect_delay)
    continue

Prevention

When it happens

Trigger: Connecting to the wrong port (HTTP server, a different tool, or nothing); the bridge accepted then dropped the connection before sending the greeting; the bridge is still starting up and the connection was reset; the greeting protocol string was changed in a newer bridge version.

Common situations: discover_port returned the default 6400 but the bridge is on a different port; the editor hasn't finished initializing the bridge; connecting to the MCP Python server port instead of the Unity-side bridge.

Understand the failure class

Related errors


AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13). Data as JSON: /api/errors/6ee42153d0a31445. Report an issue: GitHub.