11import type { ConnectorInitOptions } from './base.js'
22import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3+ import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
34import { logger } from '../logging.js'
45import { SseConnectionManager } from '../task_managers/sse.js'
6+ import { StreamableHttpConnectionManager } from '../task_managers/streamable_http.js'
57import { BaseConnector } from './base.js'
68
79export interface HttpConnectorOptions extends ConnectorInitOptions {
@@ -10,6 +12,7 @@ export interface HttpConnectorOptions extends ConnectorInitOptions {
1012 timeout ?: number // HTTP request timeout (s)
1113 sseReadTimeout ?: number // SSE read timeout (s)
1214 clientInfo ?: { name : string , version : string }
15+ preferSse ?: boolean // Force SSE transport instead of trying streamable HTTP first
1316}
1417
1518export class HttpConnector extends BaseConnector {
@@ -18,6 +21,8 @@ export class HttpConnector extends BaseConnector {
1821 private readonly timeout : number
1922 private readonly sseReadTimeout : number
2023 private readonly clientInfo : { name : string , version : string }
24+ private readonly preferSse : boolean
25+ private transportType : 'streamable-http' | 'sse' | null = null
2126
2227 constructor ( baseUrl : string , opts : HttpConnectorOptions = { } ) {
2328 super ( opts )
@@ -31,24 +36,116 @@ export class HttpConnector extends BaseConnector {
3136 this . timeout = opts . timeout ?? 5
3237 this . sseReadTimeout = opts . sseReadTimeout ?? 60 * 5
3338 this . clientInfo = opts . clientInfo ?? { name : 'http-connector' , version : '1.0.0' }
39+ this . preferSse = opts . preferSse ?? false
3440 }
3541
36- /** Establish connection to the MCP implementation via SSE. */
42+ /** Establish connection to the MCP implementation via HTTP (streamable or SSE) . */
3743 async connect ( ) : Promise < void > {
3844 if ( this . connected ) {
3945 logger . debug ( 'Already connected to MCP implementation' )
4046 return
4147 }
4248
43- logger . debug ( `Connecting to MCP implementation via HTTP/SSE: ${ this . baseUrl } ` )
49+ const baseUrl = this . baseUrl
4450
51+ // If preferSse is set, skip directly to SSE
52+ if ( this . preferSse ) {
53+ logger . debug ( `Connecting to MCP implementation via HTTP/SSE: ${ baseUrl } ` )
54+ await this . connectWithSse ( baseUrl )
55+ return
56+ }
57+
58+ // Try streamable HTTP first, then fall back to SSE
59+ logger . debug ( `Connecting to MCP implementation via HTTP: ${ baseUrl } ` )
60+
61+ try {
62+ // Try streamable HTTP transport first
63+ logger . debug ( 'Attempting streamable HTTP transport...' )
64+ await this . connectWithStreamableHttp ( baseUrl )
65+ }
66+ catch ( err ) {
67+ // Check if this is a 4xx error that indicates we should try SSE fallback
68+ let fallbackReason = 'Unknown error'
69+
70+ if ( err instanceof StreamableHTTPError ) {
71+ if ( err . code === 404 || err . code === 405 ) {
72+ fallbackReason = `Server returned ${ err . code } - server likely doesn't support streamable HTTP`
73+ logger . debug ( fallbackReason )
74+ }
75+ else {
76+ fallbackReason = `Server returned ${ err . code } : ${ err . message } `
77+ logger . debug ( fallbackReason )
78+ }
79+ }
80+ else if ( err instanceof Error ) {
81+ // Check for 404/405 in error message as fallback detection
82+ const errorStr = err . toString ( )
83+ if ( errorStr . includes ( '405 Method Not Allowed' ) || errorStr . includes ( '404 Not Found' ) ) {
84+ fallbackReason = 'Server doesn\'t support streamable HTTP (405/404)'
85+ logger . debug ( fallbackReason )
86+ }
87+ else {
88+ fallbackReason = `Streamable HTTP failed: ${ err . message } `
89+ logger . debug ( fallbackReason )
90+ }
91+ }
92+
93+ // Always try SSE fallback for maximum compatibility
94+ logger . debug ( 'Falling back to SSE transport...' )
95+
96+ try {
97+ await this . connectWithSse ( baseUrl )
98+ }
99+ catch ( sseErr ) {
100+ logger . error ( `Failed to connect with both transports:` )
101+ logger . error ( ` Streamable HTTP: ${ fallbackReason } ` )
102+ logger . error ( ` SSE: ${ sseErr } ` )
103+ await this . cleanupResources ( )
104+ throw new Error ( 'Could not connect to server with any available transport' )
105+ }
106+ }
107+ }
108+
109+ private async connectWithStreamableHttp ( baseUrl : string ) : Promise < void > {
45110 try {
46- // Build the SSE URL (root of server endpoint)
47- const sseUrl = this . baseUrl
111+ // Create and start the streamable HTTP connection manager
112+ this . connectionManager = new StreamableHttpConnectionManager (
113+ baseUrl ,
114+ {
115+ requestInit : {
116+ headers : this . headers ,
117+ } ,
118+ // Pass through timeout and other options
119+ reconnectionOptions : {
120+ maxReconnectionDelay : 30000 ,
121+ initialReconnectionDelay : 1000 ,
122+ reconnectionDelayGrowFactor : 1.5 ,
123+ maxRetries : 2 ,
124+ } ,
125+ } ,
126+ )
127+ const transport = await this . connectionManager . start ( )
128+
129+ // Create and connect the client
130+ this . client = new Client ( this . clientInfo , this . opts . clientOptions )
131+ await this . client . connect ( transport )
132+
133+ this . connected = true
134+ this . transportType = 'streamable-http'
135+ logger . debug ( `Successfully connected to MCP implementation via streamable HTTP: ${ baseUrl } ` )
136+ }
137+ catch ( err ) {
138+ // Clean up partial resources before throwing
139+ await this . cleanupResources ( )
140+ throw err
141+ }
142+ }
48143
49- // Create and start the connection manager -> returns an SSE transport
144+ private async connectWithSse ( baseUrl : string ) : Promise < void > {
145+ try {
146+ // Create and start the SSE connection manager
50147 this . connectionManager = new SseConnectionManager (
51- sseUrl ,
148+ baseUrl ,
52149 {
53150 requestInit : {
54151 headers : this . headers ,
@@ -62,10 +159,11 @@ export class HttpConnector extends BaseConnector {
62159 await this . client . connect ( transport )
63160
64161 this . connected = true
65- logger . debug ( `Successfully connected to MCP implementation via HTTP/SSE: ${ this . baseUrl } ` )
162+ this . transportType = 'sse'
163+ logger . debug ( `Successfully connected to MCP implementation via HTTP/SSE: ${ baseUrl } ` )
66164 }
67165 catch ( err ) {
68- logger . error ( `Failed to connect to MCP implementation via HTTP/SSE: ${ err } ` )
166+ // Clean up partial resources before throwing
69167 await this . cleanupResources ( )
70168 throw err
71169 }
@@ -75,6 +173,14 @@ export class HttpConnector extends BaseConnector {
75173 return {
76174 type : 'http' ,
77175 url : this . baseUrl ,
176+ transport : this . transportType || 'unknown' ,
78177 }
79178 }
179+
180+ /**
181+ * Get the transport type being used (streamable-http or sse)
182+ */
183+ getTransportType ( ) : 'streamable-http' | 'sse' | null {
184+ return this . transportType
185+ }
80186}
0 commit comments