11import type { FetchLike , JSONRPCMessage , Transport } from '@modelcontextprotocol/core-internal' ;
22import {
33 brandedHasInstance ,
4- createFetchWithInit ,
54 JSONRPCMessageSchema ,
6- normalizeHeaders ,
75 SdkError ,
86 SdkErrorCode ,
97 SdkHttpError ,
@@ -23,8 +21,8 @@ import {
2321} from './auth' ;
2422// eslint-disable-next-line @typescript-eslint/no-unused-vars -- referenced in JSDoc {@linkcode }
2523import type { IssuerMismatchError } from './authErrors' ;
26- import type { RedirectPolicy } from './transportRedirect' ;
27- import { fetchWithRedirectPolicy } from './transportRedirect' ;
24+ import type { RedirectPolicy , TransportHttp } from './transportRedirect' ;
25+ import { createTransportHttp } from './transportRedirect' ;
2826
2927export class SseError extends Error {
3028 static {
@@ -133,20 +131,15 @@ export type SSEClientTransportOptions = {
133131 */
134132export class SSEClientTransport implements Transport {
135133 private _eventSource ?: EventSource ;
136- private _endpoint ?: URL ;
137134 private _abortController ?: AbortController ;
138135 private _url : URL ;
139136 private _resourceMetadataUrl ?: URL ;
140137 private _scope ?: string ;
141138 private _eventSourceInit ?: EventSourceInit ;
142- private _requestInit ?: RequestInit ;
143139 private _authProvider ?: AuthProvider ;
144140 private _oauthProvider ?: OAuthClientProvider ;
145141 private _skipIssuerMetadataValidation ?: boolean ;
146- private _fetch ?: FetchLike ;
147- /** Fetch for OAuth requests — deliberately merges `oauthRequestInit`, never `requestInit`. */
148- private _authFetch : FetchLike ;
149- private _redirectPolicy : RedirectPolicy ;
142+ private readonly _http : TransportHttp ;
150143 private _protocolVersion ?: string ;
151144
152145 onclose ?: ( ) => void ;
@@ -158,7 +151,6 @@ export class SSEClientTransport implements Transport {
158151 this . _resourceMetadataUrl = undefined ;
159152 this . _scope = undefined ;
160153 this . _eventSourceInit = opts ?. eventSourceInit ;
161- this . _requestInit = opts ?. requestInit ;
162154 this . _skipIssuerMetadataValidation = opts ?. skipIssuerMetadataValidation ;
163155 if ( isOAuthClientProvider ( opts ?. authProvider ) ) {
164156 this . _oauthProvider = opts . authProvider ;
@@ -168,9 +160,13 @@ export class SSEClientTransport implements Transport {
168160 } else {
169161 this . _authProvider = opts ?. authProvider ;
170162 }
171- this . _fetch = opts ?. fetch ;
172- this . _redirectPolicy = opts ?. redirectPolicy ?? 'manual' ;
173- this . _authFetch = createFetchWithInit ( opts ?. fetch , opts ?. oauthRequestInit ) ;
163+ this . _http = createTransportHttp ( {
164+ url,
165+ fetch : opts ?. fetch ,
166+ requestInit : opts ?. requestInit ,
167+ oauthRequestInit : opts ?. oauthRequestInit ,
168+ redirectPolicy : opts ?. redirectPolicy
169+ } ) ;
174170 }
175171
176172 private _last401Response ?: Response ;
@@ -185,7 +181,7 @@ export class SSEClientTransport implements Transport {
185181 headers [ 'mcp-protocol-version' ] = this . _protocolVersion ;
186182 }
187183
188- const extraHeaders = normalizeHeaders ( this . _requestInit ?. headers ) ;
184+ const extraHeaders = this . _http . requestInitHeaders ( ) ;
189185
190186 return new Headers ( {
191187 ...headers ,
@@ -194,7 +190,6 @@ export class SSEClientTransport implements Transport {
194190 }
195191
196192 private _startOrAuth ( ) : Promise < void > {
197- const fetchImpl = ( this ?. _eventSourceInit ?. fetch ?? this . _fetch ?? fetch ) as typeof fetch ;
198193 return new Promise ( ( resolve , reject ) => {
199194 this . _eventSource = new EventSource ( this . _url . href , {
200195 ...this . _eventSourceInit ,
@@ -207,14 +202,12 @@ export class SSEClientTransport implements Transport {
207202 }
208203 let response : Response ;
209204 try {
210- response = await fetchWithRedirectPolicy ( {
211- fetchFn : fetchImpl ,
205+ response = await this . _http . eventSourceGet ( {
206+ fetchOverride : this . _eventSourceInit ?. fetch as FetchLike | undefined ,
212207 url,
213- method : 'GET' ,
214208 headers,
215209 requestInit : init as RequestInit ,
216210 signal : ( init as RequestInit | undefined ) ?. signal ?? undefined ,
217- redirectPolicy : this . _redirectPolicy ,
218211 crossOriginHeaders
219212 } ) ;
220213 } catch ( error ) {
@@ -248,7 +241,7 @@ export class SSEClientTransport implements Transport {
248241 const response = this . _last401Response ;
249242 this . _last401Response = undefined ;
250243 this . _eventSource ?. close ( ) ;
251- this . _authProvider . onUnauthorized ( { response, serverUrl : this . _url , fetchFn : this . _authFetch } ) . then (
244+ this . _authProvider . onUnauthorized ( { response, serverUrl : this . _url , fetchFn : this . _http . oauthFetch } ) . then (
252245 // onUnauthorized succeeded → retry fresh. Its onerror handles its own onerror?.() + reject.
253246 ( ) => this . _startOrAuth ( ) . then ( resolve , reject ) ,
254247 // onUnauthorized failed → not yet reported.
@@ -278,10 +271,7 @@ export class SSEClientTransport implements Transport {
278271 const messageEvent = event as MessageEvent ;
279272
280273 try {
281- this . _endpoint = new URL ( messageEvent . data , this . _url ) ;
282- if ( this . _endpoint . origin !== this . _url . origin ) {
283- throw new Error ( `Endpoint origin does not match connection origin: ${ this . _endpoint . origin } ` ) ;
284- }
274+ this . _http . setMessageEndpoint ( new URL ( messageEvent . data , this . _url ) ) ;
285275 } catch ( error ) {
286276 reject ( error ) ;
287277 this . onerror ?.( error as Error ) ;
@@ -348,7 +338,7 @@ export class SSEClientTransport implements Transport {
348338 iss ,
349339 this . _oauthProvider ,
350340 this . _url ,
351- { fetchFn : this . _authFetch , resourceMetadataUrl : this . _resourceMetadataUrl }
341+ { fetchFn : this . _http . oauthFetch , resourceMetadataUrl : this . _resourceMetadataUrl }
352342 ) ;
353343
354344 const result = await auth ( this . _oauthProvider , {
@@ -357,7 +347,7 @@ export class SSEClientTransport implements Transport {
357347 iss : issParam ,
358348 resourceMetadataUrl : this . _resourceMetadataUrl ,
359349 scope : this . _scope ,
360- fetchFn : this . _authFetch ,
350+ fetchFn : this . _http . oauthFetch ,
361351 skipIssuerMetadataValidation : this . _skipIssuerMetadataValidation
362352 } ) ;
363353 if ( result !== 'AUTHORIZED' ) {
@@ -376,23 +366,14 @@ export class SSEClientTransport implements Transport {
376366 }
377367
378368 private async _send ( message : JSONRPCMessage , isAuthRetry : boolean ) : Promise < void > {
379- if ( ! this . _endpoint ) {
369+ if ( ! this . _http . messageEndpoint ) {
380370 throw new SdkError ( SdkErrorCode . NotConnected , 'Not connected' ) ;
381371 }
382372
383373 try {
384374 const headers = await this . _commonHeaders ( ) ;
385375 headers . set ( 'content-type' , 'application/json' ) ;
386- const response = await fetchWithRedirectPolicy ( {
387- fetchFn : this . _fetch ?? fetch ,
388- url : this . _endpoint ,
389- method : 'POST' ,
390- headers,
391- requestInit : this . _requestInit ,
392- body : JSON . stringify ( message ) ,
393- signal : this . _abortController ?. signal ,
394- redirectPolicy : this . _redirectPolicy
395- } ) ;
376+ const response = await this . _http . mcpRequest ( 'POST' , headers , JSON . stringify ( message ) , this . _abortController ?. signal ) ;
396377 if ( ! response . ok ) {
397378 if ( response . status === 401 && this . _authProvider ) {
398379 if ( response . headers . has ( 'www-authenticate' ) ) {
@@ -405,7 +386,7 @@ export class SSEClientTransport implements Transport {
405386 await this . _authProvider . onUnauthorized ( {
406387 response,
407388 serverUrl : this . _url ,
408- fetchFn : this . _authFetch
389+ fetchFn : this . _http . oauthFetch
409390 } ) ;
410391 await response . text ?.( ) . catch ( ( ) => { } ) ;
411392 // Purposely _not_ awaited, so we don't call onerror twice
0 commit comments