Skip to content

[v2] Client.connect(): timeout and signal do not bound the notifications/initialized POST #3078

Open

Description

@JarvisMaxDev

What happened?

Client.connect(transport, { timeout, signal }) can stay pending indefinitely when a Streamable HTTP server answers initialize but never answers the POST carrying notifications/initialized. Neither the timeout nor an aborted signal settles the connect promise.

In the repro below, connect is still pending 8 s after starting, although timeout: 2000 was passed and the signal was aborted at 3 s.

Likely cause: _legacyHandshake awaits this.notification({ method: "notifications/initialized" }). The timeout and signal passed to connect apply to the initialize request only, so the notification POST has no deadline and cannot be cancelled.

Impact: clients that rely on the connect timeout can hang far longer than configured. In OpenCode 2.0.32 (SDK 2.0.0, 30 s startup timeout), connections to a remote Streamable HTTP MCP server sometimes stayed pending for 300–360 s before a fetch-level timeout failed them. That is consistent with this issue, though we could not confirm from logs which POST was pending.

What did you expect?

connect rejects after about 2000 ms, or when the signal aborts at 3000 ms.

Code to reproduce

import http from "node:http"
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"

const server = http.createServer((req, res) => {
  if (req.method !== "POST") return res.writeHead(405).end()
  let body = ""
  req.on("data", (c) => (body += c))
  req.on("end", () => {
    const msg = JSON.parse(body)
    if (msg.method === "initialize") {
      res.writeHead(200, { "content-type": "application/json", "mcp-session-id": "s1" })
      res.end(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {
        protocolVersion: msg.params.protocolVersion, capabilities: { tools: {} },
        serverInfo: { name: "hang", version: "1" } } }))
    }
    // notifications/initialized: never answered
  })
})
await new Promise((r) => server.listen(0, "127.0.0.1", r))

const client = new Client({ name: "repro", version: "1" }, { capabilities: {} })
const abort = new AbortController()
setTimeout(() => abort.abort(), 3000)
const started = Date.now()
const outcome = await Promise.race([
  client.connect(
    new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${server.address().port}/mcp`)),
    { timeout: 2000, signal: abort.signal },
  ).then(() => "connected", (e) => "rejected: " + e.message),
  new Promise((r) => setTimeout(() => r("still pending"), 8000)),
])
console.log({ observedAfterMs: Date.now() - started, outcome })
process.exit(0)

Output on both versions: { observedAfterMs: 8000, outcome: 'still pending' }.

SDK version

@modelcontextprotocol/client 2.0.0 and 2.3.1 (Node 24)

Area

Client

Activity

  1. YingzuoLiu commented on Oct 23, 2026

    @YingzuoLiu

    I’m working on this issue and will open a PR with a fix for the notifications/initialized handshake timeout and abort handling shortly.

  2. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    v1Issues / PRs related to v1.x
    on Oct 24, 2026
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

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions