Skip to content

Bug: bundled playwright-stealth MCP times out on Content-Length initialize #1923

@taariq

Description

@taariq

Summary

The Prophet arb one-shot flow is blocked because the packaged Seren Desktop Playwright stealth MCP server does not respond to the Content-Length-framed initialize request used by the Prophet skill's Python MCP gateway. The same packaged server does respond to Seren Desktop's newline-delimited JSON-RPC initialize path, so the binary is alive but the caller/server stdio framing contract is mismatched.

This reproduced after the earlier #1921/#1922 cold-start fix. The binary starts and logs readiness, but the Prophet gateway still times out waiting for an MCP response.

Observed failure

Invocation context:

  • Skill: prophet-arb-bot
  • Local skill root: /Users/taariqlewis/.config/seren/skills/prophet-arb-bot
  • Seren Desktop bundled MCP script: /Applications/SerenDesktop.app/Contents/Resources/mcp-servers/playwright-stealth/dist/index.js
  • Desktop repo audited: /Users/taariqlewis/Projects/Seren_Projects/seren-desktop
  • Prior run id: 04ce06ee91f646898d3117c5ce0e1a0c

The skill setup command succeeded and applied the Prophet schema. The first live-gated run then failed before any opportunity or order work:

{
  "status": "blocked",
  "reason": "blocked_auth_unexpected:TimeoutError:Timed out waiting for response from playwright-stealth MCP.",
  "run_id": "04ce06ee91f646898d3117c5ce0e1a0c",
  "opportunities": [],
  "orders": [],
  "pairs": [],
  "summary": {
    "execution_mode": "delta_neutral",
    "live_mode": true,
    "pairs_pre_discover": 1
  }
}

No trades were placed. The blocker happened while cold-starting authentication/browser automation.

Direct binary probe

The packaged node_modules are present:

  • /Applications/SerenDesktop.app/Contents/Resources/mcp-servers/playwright-stealth/node_modules exists
  • /Applications/SerenDesktop.app/Contents/Resources/mcp-servers/playwright-stealth/node_modules/@modelcontextprotocol/sdk exists

Content-Length-framed probe against the packaged binary timed out after 5s:

{
  "status": "timeout",
  "stdout": "",
  "stderr": "[playwright-stealth] Stdio transport ready; default browser: chrome\n"
}

Newline-delimited JSON-RPC probe against the same binary produced a valid initialize response on stdout:

{
  "stdout": "{\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"playwright-stealth\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\",\"id\":1}\n",
  "stderr": "[playwright-stealth] Stdio transport ready; default browser: chrome\n"
}

Conclusion: this is not simply a missing binary or missing dependency. The binary did not respond to the Prophet gateway's Content-Length framed initialize, while it did respond to Desktop's line-delimited initialize.

Audited code paths

Desktop MCP client

src-tauri/src/mcp.rs

  • PLAYWRIGHT_MCP_SCRIPT_RELATIVE_PATH = "mcp-servers/playwright-stealth/dist/index.js" resolves the bundled server path.
  • resolve_playwright_mcp_script_path validates script location and node_modules/@modelcontextprotocol/sdk.
  • mcp_connect spawns the server with stdin/stdout/stderr piped and applies embedded runtime env sanitation.
  • send_request writes newline-delimited JSON-RPC via writeln!(process.stdin, "{}", request_json) and reads one stdout line with read_line.
  • MCP_INITIALIZE_TIMEOUT bounds the initialize handshake at 15s.
  • Stderr is drained in the background, but the outer timeout path can still return a generic timeout because the blocking task owns the child while stuck in read_line.

Playwright stealth MCP server

mcp-servers/playwright-stealth/src/index.ts

  • Uses StdioServerTransport from @modelcontextprotocol/sdk/server/stdio.js.
  • main() connects stdio first, then logs:
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[playwright-stealth] Stdio transport ready; default browser: ${getActiveBrowserType()}`);

The #1921 fix moved browser detection after server.connect, which fixed one startup timing issue, but it did not prove compatibility with the Content-Length framing used by the Prophet gateway.

Cold-start tests

mcp-servers/playwright-stealth/src/__tests__/cold_start.test.ts

  • Regression coverage currently verifies lazy browser detection.
  • It does not spawn the compiled server and perform an end-to-end initialize handshake with the Prophet gateway framing.

Bundling path

scripts/prepare-mcp-servers.ts and src-tauri/tauri.conf.json

  • prepare:mcp-servers builds the TypeScript server and copies dependencies with hoisted node_modules.
  • Tauri resources include mcp-servers/playwright-stealth/.
  • The installed app has the expected built script and node_modules.

Settings migration

src/stores/settings.store.ts

  • Adds/repairs the Playwright stealth MCP server entry and resolves old relative script paths to the bundled resource path.
  • This path is not the primary failure, because the direct packaged binary probe found and started the bundled script.

External caller path that exposes the bug

/Users/taariqlewis/Projects/Seren_Projects/seren-skills/prophet/prophet-arb-bot/scripts/otp_worker/playwright_mcp_gateway.py

  • Sends Content-Length-framed MCP requests and waits for a Content-Length-framed response.
  • Against the packaged desktop binary, that path times out even though stderr says the server is ready.

Relevant prior commits audited

Root-cause hypothesis

There is a stdio transport/framing mismatch between at least two supported callers:

  1. Seren Desktop Rust MCP client uses newline-delimited JSON-RPC.
  2. Prophet arb Python gateway uses Content-Length-framed MCP.
  3. The packaged playwright-stealth binary answers the first style and times out on the second.

Because the server logs readiness on stderr and emits no stdout under Content-Length framing, the operator only sees Timed out waiting for response from playwright-stealth MCP, which makes this look like a dead binary even though the binary is running.

Proposed fix

  1. Decide and document the supported stdio framing contract for bundled MCP servers.
  2. Align all first-party callers and the bundled playwright-stealth server to that contract, or add an adapter/dual-framing layer if both callers must remain supported.
  3. Add an end-to-end contract test that spawns the built mcp-servers/playwright-stealth/dist/index.js and sends the exact initialize frame used by the Prophet Python gateway.
  4. Keep the existing newline-delimited Desktop test path, or explicitly migrate src-tauri/src/mcp.rs if the canonical contract becomes Content-Length framing.
  5. Improve timeout diagnostics so the blocked envelope includes at least: server command, resolved script path, framing mode attempted, stderr tail, whether stdout produced bytes, and node_modules validation status.
  6. Add a packaged-app validation step that probes /Applications/SerenDesktop.app/Contents/Resources/mcp-servers/playwright-stealth/dist/index.js or the equivalent release resource path before declaring a release good.

Acceptance criteria

  • The Prophet one-shot auth/browser startup no longer blocks with Timed out waiting for response from playwright-stealth MCP when using the Seren Desktop packaged binary.
  • The packaged binary returns an MCP initialize response within 5s for the framing used by the Prophet gateway, or the gateway is updated to the officially supported framing and covered by a test.
  • Desktop MCP initialize behavior remains covered and does not regress.
  • A failing initialize produces actionable diagnostics instead of only a generic timeout.
  • A regression test fails if the server starts and logs readiness but emits no initialize response to the supported framing.

Duplicate check

Searched GitHub issues for repo:serenorg/seren-desktop playwright-stealth Content-Length initialize timeout in:title,body; no matching open or closed issue was returned at filing time.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions