You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We connect to heterogeneous remote MCP servers using the C# SDK. Upgrading from 1.4.0 to 2.0.0 exposed connection-establishment failures that required application-level recovery. Our current workaround uses SDK 2.2.0, fresh client/transport factories, bounded recovery, explicit security exclusions, and structured attempt diagnostics.
In 1.4.0, an unpinned client started with initialize and could accept a supported legacy version returned by the server. In 2.x, unpinned negotiation starts with server/discover. The SDK's selective fallback within a connection attempt is not equivalent to reconstructing a failed client/session and retrying establishment.
This request is for the remaining connection-recovery and lifecycle contract, not a claim that every exception indicates an unsupported method or that 1.4.0 recovered every connection closure. The baseline is the published 2.2.0 package; proposed lifecycle guarantees below should be checked against current main before implementation.
Discovery HTTP/content-type interoperability, extending the HTTP exception catch, and optional standalone GET behavior. Its original title also covered AutoDetect-to-SSE fallback after JSON-RPC errors; that transport-selection request belongs there, not in a duplicate issue.
Missing correlated JSON-RPC replies, chunked-response investigation, and response-shape diagnostics. The discussion reports that a representative chunked response works locally; chunking alone must not be presented as the established root cause.
Protocol-version ceiling, flexible legacy negotiation, and eliminating tiny-discovery-timeout workarounds. No separate initialize-first/version-selection option is requested here.
Fixes already included in 2.2.0: provisional SSE shared-channel ownership, plain/empty HTTP 400/404 discovery fallback, and HTTP-status preservation.
1. Supported fresh-connection recovery after establishment failure
What is the issue?
Adding another exception to the discovery catch is insufficient when the session/channel has already closed or the initialization deadline has expired. Continuing initialize on that same client can reuse unusable state. Applications currently have to own a higher-level state machine that constructs another client and transport, disposes failed attempts, and decides whether recovery is safe.
Relevant failure families include ClientTransportClosedException (an IOException, not an McpException), other IO failures, and initialization timeouts. A discovery-probe timeout, initialization timeout, and caller cancellation are different events and must not receive the same recovery treatment.
Proposed fix
Provide an opt-in, bounded connection-establishment recovery policy, with a transport factory or equivalent SDK-owned reconstruction mechanism. Keep existing behavior unchanged unless enabled.
On an eligible establishment failure, create a genuinely fresh client/session/transport, rather than just catching the failure and continuing on a completed channel.
Expose structured failure context to the policy: establishment stage, closure reason, HTTP/MCP codes, and cancellation source. Allow applications to narrow eligibility or veto recovery, including for custom security exceptions from HTTP handlers.
Use a conservative default classification. Do not treat authentication/authorization denial, throttling, DNS/TLS failures, invalid configuration, fatal failures, or caller cancellation as evidence of protocol incompatibility. Retryable service failures, if supported, need a separate documented transient-retry policy, not automatic protocol downgrade.
Inspect all nested/aggregate exceptions when enforcing exclusions. A disallowed status in a later child must not be masked by an earlier compatible 400/404; do not infer eligibility by parsing exception messages.
Honor explicit protocol pins and any future ceiling from MCP Protocol-Version Ceiling for the C# Client SDK #1868. Applications may distinguish an advisory cached version from an explicit requirement, but the SDK must not silently clear a caller's exact pin.
Limit this policy to connection establishment. Do not replay tools/call or other application operations, and do not silently replace an established client behind the caller's back.
Our workaround permits at most an initial attempt, a fresh compatibility AutoDetect attempt, and an eligible explicit SSE attempt. That is an example of bounded orchestration, not a request to duplicate the SSE-selection changes tracked in #1848 or to make our broad catch policy the SDK default.
Why fix it?
Compatibility with 1.4.0: legacy servers now encounter discovery before the initialization path that previously worked. A failed discovery connection should not permanently rule out an explicitly permitted fresh compatibility attempt.
Full closure handling: a closed channel must be treated as a lifecycle boundary. Fresh recovery should not depend on each SDK consumer implementing client ownership and reconstruction correctly.
Acceptance coverage
Use controlled transports/loopback servers to fail the first establishment with a transport closure, IO failure, or initialization timeout and let a fresh attempt succeed. Verify fresh instance identity, finite attempts, preserved protocol constraints, and denial/security/caller-cancellation vetoes in either aggregate-child order. These are synthetic recovery tests, not claims of a new customer-server reproduction.
2. One connection-establishment deadline covering transport setup and recovery
What is the issue?
In the reviewed 2.2.0 flow, transport connection occurs before the SDK initialization timer covers discovery/initialize. Explicit SSE must open its GET stream and receive an endpoint event during transport setup. A stalled SSE establishment can therefore outlive what a caller might assume is the complete initialization budget.
A caller cancellation token can already bound this work. The gap is a first-class, documented overall deadline that composes transport setup, handshake, and any SDK-managed recovery without resetting the allowance for each stage. Our application currently supplies a separate linked recovery token for this reason.
Proposed fix
Add an overall connection-establishment timeout, or explicitly extend/document an existing option to provide that contract without unexpectedly changing compatibility-sensitive behavior.
Start the overall deadline before transport connection. Cover initial HTTP/SSE establishment, waiting for the SSE endpoint event, discovery, initialize, and every enabled recovery attempt.
Link it to caller cancellation. Each stage consumes the remaining allowance; a new attempt must not reset the overall deadline.
Keep probe and per-handshake limits distinct. Identify caller cancellation, probe expiry, handshake expiry, and overall deadline expiry through structured outcomes.
Specify cleanup behavior at deadline expiry: cancel active reads/sends, start disposal, and document whether cleanup can extend return latency or has its own bound. Do not promise an absolute deadline for arbitrary user-provided handlers/disposers that ignore cancellation.
Why fix it?
Full closure handling: stalled or closing transports must not leave establishment waiting under an unrelated, longer HTTP timeout. A composable deadline makes fallback latency predictable, prevents cumulative retry delays, and stops recovery immediately when the caller or total budget cancels.
This is timeout/lifecycle completeness, not a claim that 1.4.0 had an equivalent overall deadline.
Acceptance coverage
Exercise a GET that never sends headers, an SSE stream that never supplies its endpoint event, a stalled initialize, and multiple attempts that consume the same deadline. Check prompt caller cancellation and that no later attempt starts after budget expiry.
3. Explicit terminal-closure, cleanup, and error-precedence guarantees
What is the issue?
The provisional-SSE ownership defect is already fixed by #1768. That fix does not by itself define the complete contract for an adopted transport closing during establishment, cancellation racing with a late successful handshake, or disposal failing while an earlier connection error is already in flight.
Applications need to know whether establishment failed or returned a usable owned client, whether pending requests terminate, and which error remains observable after cleanup or recovery. This section asks for documented guarantees and regression coverage; it does not assert that every case currently leaks or hangs.
Proposed fix
Define terminal session transitions and verify that closure completes/faults pending handshake and request waiters, stops owned receive loops, and cannot produce a successfully returned client already known to be closed before ownership transfer.
For a caller-cancellation/late-success race, dispose a client that will not be returned and do not start another recovery stage.
Dispose partial clients, failed session transports, and owned streams exactly once; do not dispose caller-owned shared HttpClient instances or unrelated transports.
Preserve the original establishment exception and stack on ordinary recovery exhaustion, while exposing later attempt outcomes separately. Caller cancellation and security/fatal failures must not be hidden behind an earlier compatibility error. Ordinary cleanup failures should not replace the primary failure.
Provide payload-free lifecycle diagnostics: stage, elapsed time, configured transport mode, cancellation source, closure category, numeric status/error codes, attempt count, and exhaustion reason. Do not emit credentials, session IDs, bodies, or raw exception messages by default. This is attempt/lifecycle observability, not the response-parser diagnostics already requested in StreamableHttpClientSessionTransport loses a correlated JSON-RPC error in a chunked HTTP 200 response #1862.
Preserve valid terminal behavior: an established session's HTTP 404 expiry must not be ignored to keep using that session; optional standalone GET 405 is already nonfatal and is not a new requested fix. Established-session reconnect/replay is outside this request.
Why fix it?
Full closure handling: successful interoperability recovery must not trade a failed handshake for leaked streams, indefinitely pending tasks, reuse of expired sessions, lost security failures, or ambiguous ownership. Clear guarantees make a fresh-attempt policy safe to adopt and diagnose.
Acceptance coverage
Cover adopted transport EOF/reset during initialization, session expiry, pending waiters at closure, cancellation concurrent with handshake success, disposal throwing after connection failure, and exhausted recovery. Assert ownership/disposal counts, no application-operation replay, preserved exception identity/stack where promised, and absence of sensitive canaries in lifecycle diagnostics.
Expected outcome
Together with the separately tracked protocol/transport fixes and version-ceiling feature, these changes would let consumers replace custom recovery wrappers with a supported, bounded SDK lifecycle policy. They should not turn every server error into a downgrade signal or change strict negotiation/security behavior by default.
Context and scope
We connect to heterogeneous remote MCP servers using the C# SDK. Upgrading from 1.4.0 to 2.0.0 exposed connection-establishment failures that required application-level recovery. Our current workaround uses SDK 2.2.0, fresh client/transport factories, bounded recovery, explicit security exclusions, and structured attempt diagnostics.
In 1.4.0, an unpinned client started with
initializeand could accept a supported legacy version returned by the server. In 2.x, unpinned negotiation starts withserver/discover. The SDK's selective fallback within a connection attempt is not equivalent to reconstructing a failed client/session and retrying establishment.This request is for the remaining connection-recovery and lifecycle contract, not a claim that every exception indicates an unsupported method or that 1.4.0 recovered every connection closure. The baseline is the published 2.2.0 package; proposed lifecycle guarantees below should be checked against current main before implementation.
Already tracked or fixed — not requested again
1. Supported fresh-connection recovery after establishment failure
What is the issue?
Adding another exception to the discovery catch is insufficient when the session/channel has already closed or the initialization deadline has expired. Continuing
initializeon that same client can reuse unusable state. Applications currently have to own a higher-level state machine that constructs another client and transport, disposes failed attempts, and decides whether recovery is safe.Relevant failure families include
ClientTransportClosedException(anIOException, not anMcpException), other IO failures, and initialization timeouts. A discovery-probe timeout, initialization timeout, and caller cancellation are different events and must not receive the same recovery treatment.Proposed fix
tools/callor other application operations, and do not silently replace an established client behind the caller's back.Our workaround permits at most an initial attempt, a fresh compatibility AutoDetect attempt, and an eligible explicit SSE attempt. That is an example of bounded orchestration, not a request to duplicate the SSE-selection changes tracked in #1848 or to make our broad catch policy the SDK default.
Why fix it?
Compatibility with 1.4.0: legacy servers now encounter discovery before the initialization path that previously worked. A failed discovery connection should not permanently rule out an explicitly permitted fresh compatibility attempt.
Full closure handling: a closed channel must be treated as a lifecycle boundary. Fresh recovery should not depend on each SDK consumer implementing client ownership and reconstruction correctly.
Acceptance coverage
Use controlled transports/loopback servers to fail the first establishment with a transport closure, IO failure, or initialization timeout and let a fresh attempt succeed. Verify fresh instance identity, finite attempts, preserved protocol constraints, and denial/security/caller-cancellation vetoes in either aggregate-child order. These are synthetic recovery tests, not claims of a new customer-server reproduction.
2. One connection-establishment deadline covering transport setup and recovery
What is the issue?
In the reviewed 2.2.0 flow, transport connection occurs before the SDK initialization timer covers discovery/initialize. Explicit SSE must open its GET stream and receive an
endpointevent during transport setup. A stalled SSE establishment can therefore outlive what a caller might assume is the complete initialization budget.A caller cancellation token can already bound this work. The gap is a first-class, documented overall deadline that composes transport setup, handshake, and any SDK-managed recovery without resetting the allowance for each stage. Our application currently supplies a separate linked recovery token for this reason.
Proposed fix
Why fix it?
Full closure handling: stalled or closing transports must not leave establishment waiting under an unrelated, longer HTTP timeout. A composable deadline makes fallback latency predictable, prevents cumulative retry delays, and stops recovery immediately when the caller or total budget cancels.
This is timeout/lifecycle completeness, not a claim that 1.4.0 had an equivalent overall deadline.
Acceptance coverage
Exercise a GET that never sends headers, an SSE stream that never supplies its endpoint event, a stalled initialize, and multiple attempts that consume the same deadline. Check prompt caller cancellation and that no later attempt starts after budget expiry.
3. Explicit terminal-closure, cleanup, and error-precedence guarantees
What is the issue?
The provisional-SSE ownership defect is already fixed by #1768. That fix does not by itself define the complete contract for an adopted transport closing during establishment, cancellation racing with a late successful handshake, or disposal failing while an earlier connection error is already in flight.
Applications need to know whether establishment failed or returned a usable owned client, whether pending requests terminate, and which error remains observable after cleanup or recovery. This section asks for documented guarantees and regression coverage; it does not assert that every case currently leaks or hangs.
Proposed fix
HttpClientinstances or unrelated transports.Why fix it?
Full closure handling: successful interoperability recovery must not trade a failed handshake for leaked streams, indefinitely pending tasks, reuse of expired sessions, lost security failures, or ambiguous ownership. Clear guarantees make a fresh-attempt policy safe to adopt and diagnose.
Acceptance coverage
Cover adopted transport EOF/reset during initialization, session expiry, pending waiters at closure, cancellation concurrent with handshake success, disposal throwing after connection failure, and exhausted recovery. Assert ownership/disposal counts, no application-operation replay, preserved exception identity/stack where promised, and absence of sensitive canaries in lifecycle diagnostics.
Expected outcome
Together with the separately tracked protocol/transport fixes and version-ceiling feature, these changes would let consumers replace custom recovery wrappers with a supported, bounded SDK lifecycle policy. They should not turn every server error into a downgrade signal or change strict negotiation/security behavior by default.