@@ -92,15 +92,24 @@ type ListChangedKinds = Record<
9292
9393/**
9494 * Returns true for `server/discover` failures that should fall through to the
95- * legacy `initialize` handshake (server doesn't speak 2026-06).
95+ * legacy `initialize` handshake (server doesn't speak 2026-06). Auth failures
96+ * (401/403) are NOT fallbackable: a server that requires auth for `discover`
97+ * will require it for `initialize` too, so falling back would only mask the
98+ * real error and skip the transport's re-auth path.
9699 */
97100function isFallbackable ( e : unknown ) : boolean {
98101 if ( e instanceof ProtocolError ) {
99102 return e . code === ProtocolErrorCode . MethodNotFound ;
100103 }
101104 if ( e instanceof SdkError ) {
102105 const status = ( e . data as { status ?: number } | undefined ) ?. status ;
103- return e . code === SdkErrorCode . InvalidResult || ( typeof status === 'number' && status >= 400 && status < 500 ) ;
106+ // Any 4xx except 401/403 (auth) means the server doesn't speak 2026-06.
107+ // 400 in particular is what a pre-2026 StreamableHTTP server returns for
108+ // a non-initialize POST without an mcp-session-id.
109+ return (
110+ e . code === SdkErrorCode . InvalidResult ||
111+ ( typeof status === 'number' && status >= 400 && status < 500 && status !== 401 && status !== 403 )
112+ ) ;
104113 }
105114 return false ;
106115}
@@ -475,33 +484,53 @@ export class Client extends Protocol<ClientContext> {
475484 * Probes `server/discover` via `transport.sendAndReceive`. On success,
476485 * marks this client stateless and populates server identity/capabilities
477486 * from the result. On {@linkcode isFallbackable} failure, leaves state
478- * untouched (the legacy `initialize` already populated it via `connect()`).
479- *
480- * Called from {@linkcode connect} (in C13).
487+ * untouched so {@linkcode connect} falls through to the legacy
488+ * `initialize` handshake.
481489 */
482- private async _negotiate ( transport : Transport ) : Promise < void > {
490+ private async _negotiate ( transport : Transport , options ?: RequestOptions ) : Promise < void > {
483491 const sar = transport . sendAndReceive ?. bind ( transport ) ;
484492 const preferred = this . _supportedProtocolVersions . find ( v => isStatelessProtocolVersion ( v ) ) ;
485493 if ( ! sar || ! preferred ) return ;
486494
487495 transport . setProtocolVersion ?.( preferred ) ;
496+ const signal =
497+ options ?. timeout === undefined
498+ ? ( options ?. signal ?? AbortSignal . timeout ( DEFAULT_REQUEST_TIMEOUT_MSEC ) )
499+ : options ?. signal
500+ ? AbortSignal . any ( [ options . signal , AbortSignal . timeout ( options . timeout ) ] )
501+ : AbortSignal . timeout ( options . timeout ) ;
488502 try {
489- const raw = await this . _collect ( sar ( { method : 'server/discover' , params : { _meta : this . _buildMeta ( preferred ) } } ) ) ;
490- const dr = DiscoverResultSchema . parse ( raw ) ;
491- const negotiated = dr . supportedVersions . find ( v => this . _supportedProtocolVersions . includes ( v ) ) ;
492- if ( negotiated && isStatelessProtocolVersion ( negotiated ) ) {
493- this . _serverCapabilities = dr . capabilities ;
494- this . _serverVersion = dr . serverInfo ;
495- this . _instructions = dr . instructions ;
496- this . _negotiatedProtocolVersion = negotiated ;
497- this . _isStateless = true ;
498- transport . setProtocolVersion ?.( negotiated ) ;
499- return ;
503+ const raw = await this . _collect ( sar ( { method : 'server/discover' , params : { _meta : this . _buildMeta ( preferred ) } } , { signal } ) , {
504+ signal
505+ } ) ;
506+ const drParsed = DiscoverResultSchema . safeParse ( raw ) ;
507+ if ( drParsed . success ) {
508+ const dr = drParsed . data ;
509+ // The probe only counts as success when there is a mutual
510+ // *stateless* version; otherwise fall through to legacy initialize.
511+ const negotiated = dr . supportedVersions . find (
512+ v => isStatelessProtocolVersion ( v ) && this . _supportedProtocolVersions . includes ( v )
513+ ) ;
514+ if ( negotiated ) {
515+ this . _serverCapabilities = dr . capabilities ;
516+ this . _serverVersion = dr . serverInfo ;
517+ this . _instructions = dr . instructions ;
518+ this . _negotiatedProtocolVersion = negotiated ;
519+ this . _isStateless = true ;
520+ transport . setProtocolVersion ?.( negotiated ) ;
521+ return ;
522+ }
500523 }
501524 } catch ( error ) {
502- if ( ! isFallbackable ( error ) ) throw error ;
525+ if ( ! isFallbackable ( error ) ) {
526+ // Reset the version we set before re-throwing so the
527+ // transport is not left advertising a stateless version.
528+ transport . setProtocolVersion ?.( this . _negotiatedProtocolVersion ?? '' ) ;
529+ throw error ;
530+ }
503531 }
504- // Reset header to whatever legacy initialize set.
532+ // Fallback path: reset the version header so the subsequent legacy
533+ // `_initialize()` (run by `connect()`) can set it.
505534 transport . setProtocolVersion ?.( this . _negotiatedProtocolVersion ?? '' ) ;
506535 }
507536
@@ -609,10 +638,11 @@ export class Client extends Protocol<ClientContext> {
609638 // ═══════════════════════════════════════════════════════════════════════
610639 // session-dependent (existing — bodies unchanged unless noted dual-mode above)
611640 //
612- // `connect()` performs the legacy `initialize` handshake. The 2026-06
613- // discover auto-probe is wired in C13. `ping`, `subscribeResource`,
614- // `unsubscribeResource`, and `_setupListChangedHandler*` use the
615- // persistent connection; `_listChangedLoop` (above) is the 2026 path.
641+ // `_initialize()` (extracted verbatim from the previous inline `connect()`
642+ // body) performs the legacy `initialize` handshake. `ping`,
643+ // `subscribeResource`, `unsubscribeResource`, and
644+ // `_setupListChangedHandler*` use the persistent connection;
645+ // `_listChangedLoop` (above) is the 2026 path.
616646 // ═══════════════════════════════════════════════════════════════════════
617647
618648 /**
@@ -787,46 +817,13 @@ export class Client extends Protocol<ClientContext> {
787817 return ;
788818 }
789819 try {
790- const result = await this . _requestWithSchema (
791- {
792- method : 'initialize' ,
793- params : {
794- protocolVersion : this . _supportedProtocolVersions [ 0 ] ?? LATEST_PROTOCOL_VERSION ,
795- capabilities : this . _capabilities ,
796- clientInfo : this . _clientInfo
797- }
798- } ,
799- InitializeResultSchema ,
800- options
801- ) ;
802-
803- if ( result === undefined ) {
804- throw new Error ( `Server sent invalid initialize result: ${ result } ` ) ;
805- }
806-
807- if ( ! this . _supportedProtocolVersions . includes ( result . protocolVersion ) ) {
808- throw new Error ( `Server's protocol version is not supported: ${ result . protocolVersion } ` ) ;
809- }
810-
811- this . _serverCapabilities = result . capabilities ;
812- this . _serverVersion = result . serverInfo ;
813- this . _negotiatedProtocolVersion = result . protocolVersion ;
814- // HTTP transports must set the protocol version in each header after initialization.
815- if ( transport . setProtocolVersion ) {
816- transport . setProtocolVersion ( result . protocolVersion ) ;
817- }
818-
819- this . _instructions = result . instructions ;
820-
821- await this . notification ( {
822- method : 'notifications/initialized'
823- } ) ;
824-
825- // Set up list changed handlers now that we know server capabilities
826- if ( this . _pendingListChangedConfig ) {
827- this . _setupListChangedHandlers ( this . _pendingListChangedConfig ) ;
828- this . _pendingListChangedConfig = undefined ;
820+ // Probe `server/discover` (SEP-2575). If it succeeds, this client is
821+ // stateless and the legacy `initialize` is skipped.
822+ await this . _negotiate ( transport , options ) ;
823+ if ( ! this . _isStateless ) {
824+ await this . _initialize ( transport , options ) ;
829825 }
826+ this . _setupListChanged ( ) ;
830827 } catch ( error ) {
831828 // Disconnect if initialization fails.
832829 void this . close ( ) ;
0 commit comments