Repository navigation
[v2] Client.connect(): timeout and signal do not bound the notifications/initialized POST #3078
Description
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
YingzuoLiu commentedon Oct 23, 2026
I’m working on this issue and will open a PR with a fix for the
notifications/initializedhandshake timeout and abort handling shortly.