1
1
import { Transport } from "../shared/transport.js" ;
2
2
import { JSONRPCMessage , JSONRPCMessageSchema } from "../types.js" ;
3
3
import { auth , AuthResult , OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
4
- import { EventSourceParserStream } from 'eventsource-parser/stream' ;
4
+ import { EventSourceParserStream } from "eventsource-parser/stream" ;
5
+
5
6
export class StreamableHTTPError extends Error {
6
7
constructor (
7
8
public readonly code : number | undefined ,
@@ -17,16 +18,16 @@ export class StreamableHTTPError extends Error {
17
18
export type StreamableHTTPClientTransportOptions = {
18
19
/**
19
20
* An OAuth client provider to use for authentication.
20
- *
21
+ *
21
22
* When an `authProvider` is specified and the connection is started:
22
23
* 1. The connection is attempted with any existing access token from the `authProvider`.
23
24
* 2. If the access token has expired, the `authProvider` is used to refresh the token.
24
25
* 3. If token refresh fails or no access token exists, and auth is required, `OAuthClientProvider.redirectToAuthorization` is called, and an `UnauthorizedError` will be thrown from `connect`/`start`.
25
- *
26
+ *
26
27
* After the user has finished authorizing via their user agent, and is redirected back to the MCP client application, call `StreamableHTTPClientTransport.finishAuth` with the authorization code before retrying the connection.
27
- *
28
+ *
28
29
* If an `authProvider` is not provided, and auth is required, an `UnauthorizedError` will be thrown.
29
- *
30
+ *
30
31
* `UnauthorizedError` might also be thrown when sending any message over the transport, indicating that the session has expired, and needs to be re-authed and reconnected.
31
32
*/
32
33
authProvider ?: OAuthClientProvider ;
@@ -83,7 +84,7 @@ export class StreamableHTTPClientTransport implements Transport {
83
84
return await this . _startOrAuthStandaloneSSE ( ) ;
84
85
}
85
86
86
- private async _commonHeaders ( ) : Promise < HeadersInit > {
87
+ private async _commonHeaders ( ) : Promise < Headers > {
87
88
const headers : HeadersInit = { } ;
88
89
if ( this . _authProvider ) {
89
90
const tokens = await this . _authProvider . tokens ( ) ;
@@ -96,24 +97,25 @@ export class StreamableHTTPClientTransport implements Transport {
96
97
headers [ "mcp-session-id" ] = this . _sessionId ;
97
98
}
98
99
99
- return headers ;
100
+ return new Headers (
101
+ { ...headers , ...this . _requestInit ?. headers }
102
+ ) ;
100
103
}
101
104
102
105
private async _startOrAuthStandaloneSSE ( ) : Promise < void > {
103
106
try {
104
107
// Try to open an initial SSE stream with GET to listen for server messages
105
108
// This is optional according to the spec - server may not support it
106
- const commonHeaders = await this . _commonHeaders ( ) ;
107
- const headers = new Headers ( commonHeaders ) ;
108
- headers . set ( 'Accept' , 'text/event-stream' ) ;
109
+ const headers = await this . _commonHeaders ( ) ;
110
+ headers . set ( "Accept" , "text/event-stream" ) ;
109
111
110
112
// Include Last-Event-ID header for resumable streams
111
113
if ( this . _lastEventId ) {
112
- headers . set ( ' last-event-id' , this . _lastEventId ) ;
114
+ headers . set ( " last-event-id" , this . _lastEventId ) ;
113
115
}
114
116
115
117
const response = await fetch ( this . _url , {
116
- method : ' GET' ,
118
+ method : " GET" ,
117
119
headers,
118
120
signal : this . _abortController ?. signal ,
119
121
} ) ;
@@ -124,12 +126,10 @@ export class StreamableHTTPClientTransport implements Transport {
124
126
return await this . _authThenStart ( ) ;
125
127
}
126
128
127
- const error = new StreamableHTTPError (
129
+ throw new StreamableHTTPError (
128
130
response . status ,
129
131
`Failed to open SSE stream: ${ response . statusText } ` ,
130
132
) ;
131
- this . onerror ?.( error ) ;
132
- throw error ;
133
133
}
134
134
135
135
// Successful connection, handle the SSE stream as a standalone listener
@@ -144,42 +144,32 @@ export class StreamableHTTPClientTransport implements Transport {
144
144
if ( ! stream ) {
145
145
return ;
146
146
}
147
- // Create a pipeline: binary stream -> text decoder -> SSE parser
148
- const eventStream = stream
149
- . pipeThrough ( new TextDecoderStream ( ) )
150
- . pipeThrough ( new EventSourceParserStream ( ) ) ;
151
147
152
- const reader = eventStream . getReader ( ) ;
153
148
const processStream = async ( ) => {
154
- try {
155
- while ( true ) {
156
- const { done, value : event } = await reader . read ( ) ;
157
- if ( done ) {
158
- break ;
159
- }
160
-
161
- // Update last event ID if provided
162
- if ( event . id ) {
163
- this . _lastEventId = event . id ;
164
- }
165
-
166
- // Handle message events (default event type is undefined per docs)
167
- // or explicit 'message' event type
168
- if ( ! event . event || event . event === 'message' ) {
169
- try {
170
- const message = JSONRPCMessageSchema . parse ( JSON . parse ( event . data ) ) ;
171
- this . onmessage ?.( message ) ;
172
- } catch ( error ) {
173
- this . onerror ?.( error as Error ) ;
174
- }
149
+ // Create a pipeline: binary stream -> text decoder -> SSE parser
150
+ const eventStream = stream
151
+ . pipeThrough ( new TextDecoderStream ( ) )
152
+ . pipeThrough ( new EventSourceParserStream ( ) ) ;
153
+
154
+ for await ( const event of eventStream ) {
155
+ // Update last event ID if provided
156
+ if ( event . id ) {
157
+ this . _lastEventId = event . id ;
158
+ }
159
+ // Handle message events (default event type is undefined per docs)
160
+ // or explicit 'message' event type
161
+ if ( ! event . event || event . event === "message" ) {
162
+ try {
163
+ const message = JSONRPCMessageSchema . parse ( JSON . parse ( event . data ) ) ;
164
+ this . onmessage ?.( message ) ;
165
+ } catch ( error ) {
166
+ this . onerror ?.( error as Error ) ;
175
167
}
176
168
}
177
- } catch ( error ) {
178
- this . onerror ?.( error as Error ) ;
179
169
}
180
170
} ;
181
171
182
- processStream ( ) ;
172
+ processStream ( ) . catch ( err => this . onerror ?. ( err ) ) ;
183
173
}
184
174
185
175
async start ( ) {
@@ -215,8 +205,7 @@ export class StreamableHTTPClientTransport implements Transport {
215
205
216
206
async send ( message : JSONRPCMessage | JSONRPCMessage [ ] ) : Promise < void > {
217
207
try {
218
- const commonHeaders = await this . _commonHeaders ( ) ;
219
- const headers = new Headers ( { ...commonHeaders , ...this . _requestInit ?. headers } ) ;
208
+ const headers = await this . _commonHeaders ( ) ;
220
209
headers . set ( "content-type" , "application/json" ) ;
221
210
headers . set ( "accept" , "application/json, text/event-stream" ) ;
222
211
@@ -261,20 +250,13 @@ export class StreamableHTTPClientTransport implements Transport {
261
250
// Get original message(s) for detecting request IDs
262
251
const messages = Array . isArray ( message ) ? message : [ message ] ;
263
252
264
- // Extract IDs from request messages for tracking responses
265
- const requestIds = messages . filter ( msg => 'method' in msg && 'id' in msg )
266
- . map ( msg => 'id' in msg ? msg . id : undefined )
267
- . filter ( id => id !== undefined ) ;
268
-
269
- // If we have request IDs and an SSE response, create a unique stream ID
270
- const hasRequests = requestIds . length > 0 ;
253
+ const hasRequests = messages . filter ( msg => "method" in msg && "id" in msg && msg . id !== undefined ) . length > 0 ;
271
254
272
255
// Check the response type
273
256
const contentType = response . headers . get ( "content-type" ) ;
274
257
275
258
if ( hasRequests ) {
276
259
if ( contentType ?. includes ( "text/event-stream" ) ) {
277
- // For streaming responses, create a unique stream ID based on request IDs
278
260
this . _handleSseStream ( response . body ) ;
279
261
} else if ( contentType ?. includes ( "application/json" ) ) {
280
262
// For non-streaming servers, we might get direct JSON responses
@@ -286,6 +268,11 @@ export class StreamableHTTPClientTransport implements Transport {
286
268
for ( const msg of responseMessages ) {
287
269
this . onmessage ?.( msg ) ;
288
270
}
271
+ } else {
272
+ throw new StreamableHTTPError (
273
+ - 1 ,
274
+ `Unexpected content type: ${ contentType } ` ,
275
+ ) ;
289
276
}
290
277
}
291
278
} catch ( error ) {
@@ -296,7 +283,7 @@ export class StreamableHTTPClientTransport implements Transport {
296
283
297
284
/**
298
285
* Opens SSE stream to receive messages from the server.
299
- *
286
+ *
300
287
* This allows the server to push messages to the client without requiring the client
301
288
* to first send a request via HTTP POST. Some servers may not support this feature.
302
289
* If authentication is required but fails, this method will throw an UnauthorizedError.
@@ -309,4 +296,4 @@ export class StreamableHTTPClientTransport implements Transport {
309
296
}
310
297
await this . _startOrAuthStandaloneSSE ( ) ;
311
298
}
312
- }
299
+ }
0 commit comments