Skip to content

[MCP]: first tools/call stalls ~60s for Streamable HTTP clients that never open the standalone SSE stream #42256

Description

@edri2or-commits

A Streamable HTTP client that advertises capabilities.roots but never opens the standalone GET SSE stream pays a ~60s stall on its first tools/call. The call then succeeds normally — it is a delay, not a failure, which is why it is easy to misattribute to browser start-up or to the client.

Where: packages/playwright-core/src/tools/utils/mcp/server.ts, initializeServer.

Mechanism: server.listRoots() is awaited with no timeout option, so it falls back to the TypeScript SDK's DEFAULT_REQUEST_TIMEOUT_MSEC (60000). It is a server-to-client request with no relatedRequestId, so StreamableHTTPServerTransport.send() routes it to the standalone stream and returns early when none exists — the request is discarded silently and is never answered. The existing Promise.race([transportInitialized, 5000]) bounds waiting for the transport to become ready; it does not bound listRoots() itself. The .catch() then recovers with roots: [], which is why this presents as a stall followed by success rather than an error.

Measured, two clients against the same server and the same container, minutes apart:

client standalone GET open? first tools/call (browser_navigate)
opens it yes 708 ms
never opens it no 60,683 ms

The only variable is the standalone stream: both clients send byte-identical POST headers (accept: application/json, text/event-stream, mcp-protocol-version: 2025-06-18).

Browser start-up is excluded as a cause: the fast client ran against a cold browser and the slow one against a warm one. The stall also lands on tools/call, not on initialize — initialize returned in 239 ms in the same session.

Observed on @playwright/mcp 0.0.41 (as reported by serverInfo); the same code path is present in 0.0.79.

Related: PR microsoft/playwright-mcp#826 (fix: cursor does not respond to listRoots) addressed the same failure family for a client that does not answer roots/list. This is the transport-level case, where the request cannot be delivered at all.

Suggested fix: bound the call — server.listRoots(undefined, { timeout: 2000 }) — or skip it when no standalone stream is open.

Workaround for anyone hitting this: stop advertising capabilities.roots, or open the standalone GET stream and keep it open for the session.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions