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_editor_state.py when the first line read from the server does not contain the bytes 'WELCOME UNITY-MCP'. The Unity MCP bridge sends a greeting line on connect; its absence means either the connection was closed immediately (empty line) or the remote service is not the MCP bridge.

Source

Thrown at tools/stress_editor_state.py:80

async def read_frame(reader: asyncio.StreamReader) -> bytes:
    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:
    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_get_editor_state_frame() -> bytes:
    payload = {"type": "get_editor_state", "params": {}}
    return json.dumps(payload).encode("utf-8")


async def stress_loop(host: str, port: int, duration: float, interval: float, verbose: bool):
    stop_time = time.time() + duration
    stats = {"requests": 0, "errors": 0, "reconnects": 0}
    
    print(f"Starting editor state stress test...")
    print(f"  Target: {host}:{port}")
    print(f"  Duration: {duration}s")
    print(f"  Interval: {interval}s ({1/interval:.1f} requests/sec)")
    print(f"  Press Ctrl+C to stop early")
    print()
    

View on GitHub (pinned to c21bf496bc)

Solutions

  1. Confirm the port from ~/.unity-mcp/unity-mcp-status-*.json (unity_port field) and pass it via --port.
  2. Verify the Unity Editor is open with the MCP for Unity package active and the bridge started.
  3. If the greeting string changed, update the b'WELCOME UNITY-MCP' check in do_handshake.
Defensive patterns

Strategy: validation

Validate before calling

import json
from pathlib import Path

def discover_port() -> int | None:
    status_dir = Path.home() / ".unity-mcp"
    for f in sorted(status_dir.glob("unity-mcp-status-*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
        data = json.loads(f.read_text())
        port = int(data.get("unity_port", 0) or 0)
        if 0 < port < 65536:
            return port
    return None

port = discover_port()
if port is None:
    raise SystemExit("No active Unity bridge found. Open Unity with MCP for Unity.")

Try / catch

try:
    await asyncio.wait_for(do_handshake(reader), timeout=TIMEOUT)
except ConnectionError as e:
    print(f"Handshake failed: {e}. Is the bridge running on port {port}?")
    raise

Prevention

When it happens

Trigger: Connecting to a port that is not the Unity bridge (e.g. the HTTP server, a different Unity tool, or nothing at all); the bridge accepted the connection but crashed before sending the greeting; the bridge protocol changed and the greeting string was altered.

Common situations: Wrong port discovered or hardcoded; the bridge is mid-startup and hasn't sent the greeting yet; connecting to the Python MCP server's port instead of the Unity-side TCP bridge.

Understand the failure class

Related errors


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