fix(@projects/@magic-civilization): 🐛 render MCP client — connect-first + longer boot grace (p2-86)
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) <noreply@anthropic.com>
This commit is contained in:
parent
0349a4e8fd
commit
6310433cc5
1 changed files with 59 additions and 16 deletions
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
const deadline = Date.now() + this.opts.startTimeoutMs;
|
||||
return new Promise<void>((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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue