Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit 64fc075

Browse files
committed
fallback logic in http connector
1 parent f40a2fb commit 64fc075

2 files changed

Lines changed: 120 additions & 9 deletions

File tree

src/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ export function createConnectorFromConfig(
2121
}
2222

2323
if ('url' in serverConfig) {
24+
// HttpConnector automatically handles streamable HTTP with SSE fallback
25+
const transport = serverConfig.transport || 'http'
26+
2427
return new HttpConnector(serverConfig.url, {
2528
headers: serverConfig.headers,
26-
authToken: serverConfig.auth_token,
29+
authToken: serverConfig.auth_token || serverConfig.authToken,
30+
// Only force SSE if explicitly requested
31+
preferSse: serverConfig.preferSse || transport === 'sse',
2732
})
2833
}
2934

src/connectors/http.ts

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ConnectorInitOptions } from './base.js'
22
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3+
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
34
import { logger } from '../logging.js'
45
import { SseConnectionManager } from '../task_managers/sse.js'
6+
import { StreamableHttpConnectionManager } from '../task_managers/streamable_http.js'
57
import { BaseConnector } from './base.js'
68

79
export 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

1518
export 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

Comments
 (0)