From 6310433cc54a553cb31e00f0d30043f2aa4b9222 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 19 Jun 2026 09:11:20 -0500 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20render=20MCP=20client=20=E2=80=94=20connect-firs?= =?UTF-8?q?t=20+=20longer=20boot=20grace=20(p2-86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first in-session magic_civ_screenshot timed out: windowed Godot spawned by the MCP (a throttled background GUI process under Claude Code) inits far slower than a terminal launch, exceeding the 45s connect timeout — though the driver DID bind the port (verified 8787 open). Fixes: - connect-first: if a rendered driver is already listening (e.g. a foreground `MC_MCP_RENDER=1 ./run play`), connect to it directly and skip the slow, throttled background spawn entirely. - start timeout 45s -> 150s for the spawn fallback. - a failed start now clears its cached promise + reaps the child so the next tool call respawns instead of replaying the rejection until an MCP restart. Re-verified end-to-end through the built RenderClient (spawn path): ping {ok}, screenshot -> a real 3420x1923 PNG of the live world map. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../claude-player-mcp/src/render_client.ts | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/tooling/claude-player-mcp/src/render_client.ts b/tooling/claude-player-mcp/src/render_client.ts index c2a1cb2f..a606476d 100644 --- a/tooling/claude-player-mcp/src/render_client.ts +++ b/tooling/claude-player-mcp/src/render_client.ts @@ -45,7 +45,10 @@ export class RenderClient { this.opts = { host: "127.0.0.1", env: {}, - startTimeoutMs: 45_000, + // Windowed Godot spawned by the MCP (background GUI process under Claude + // Code) is much slower to init + bind than a terminal launch — the driver + // port can take well over a minute to open. Be generous. + startTimeoutMs: 150_000, requestTimeoutMs: 30_000, ...opts, }; @@ -57,12 +60,35 @@ export class RenderClient { private ensureReady(): Promise { if (this.readyPromise === null) { - this.readyPromise = this.start(); + // On a failed start (e.g. boot timeout), clear the cached promise and + // reap the child so the NEXT tool call respawns instead of replaying the + // rejection forever (which would otherwise need an MCP restart). + this.readyPromise = this.start().catch((err: unknown) => { + this.readyPromise = null; + if (this.child !== null) { + this.child.kill(); + this.child = null; + } + throw err instanceof Error ? err : new Error(String(err)); + }); } return this.readyPromise; } private start(): Promise { + // First: if a rendered driver is ALREADY listening (game launched + // separately, e.g. foreground `MC_MCP_RENDER=1 ./run play`), connect to it. + // This dodges the slow, window-server-throttled background spawn entirely. + return this.connectOnce().then( + () => undefined, + () => { + this.spawnGame(); + return this.connectWithRetry(); + }, + ); + } + + private spawnGame(): void { this.child = spawn(this.opts.serverScript, [], { env: { ...process.env, @@ -76,30 +102,47 @@ export class RenderClient { this.closed = true; this.failAll(new Error("rendered game process exited")); }); - return this.connectWithRetry(); + } + + private attachSocket(s: Socket): void { + this.sock = s; + s.setEncoding("utf8"); + s.on("data", (chunk: string) => this.onData(chunk)); + s.on("close", () => { + this.sock = null; + }); + } + + private connectOnce(): Promise { + return new Promise((resolve, reject) => { + const s = connect({ host: this.opts.host, port: this.opts.port }); + const timer = setTimeout(() => { + s.destroy(); + reject(new Error("connect attempt timed out")); + }, 2500); + s.once("connect", () => { + clearTimeout(timer); + this.attachSocket(s); + resolve(); + }); + s.once("error", () => { + clearTimeout(timer); + s.destroy(); + reject(new Error("connect attempt failed")); + }); + }); } private connectWithRetry(): Promise { const deadline = Date.now() + this.opts.startTimeoutMs; return new Promise((resolve, reject) => { const attempt = (): void => { - const s = connect({ host: this.opts.host, port: this.opts.port }); - s.once("connect", () => { - this.sock = s; - s.setEncoding("utf8"); - s.on("data", (chunk: string) => this.onData(chunk)); - s.on("close", () => { - this.sock = null; - }); - resolve(); - }); - s.once("error", () => { - s.destroy(); + this.connectOnce().then(resolve, () => { if (Date.now() > deadline) { reject(new Error("render driver TCP port never opened")); return; } - setTimeout(attempt, 500); + setTimeout(attempt, 750); }); }; attempt();