Skip to content

Commit 8ce0cc5

Browse files
authored
mcp: add a second-step fallback for legacy SSE (microsoft#249914)
Refs microsoft#246753 (comment) / antfu/nuxt-mcp#21 This server implementation was legacy SSE but (incorrectly) returned a 2xx from a POST request. This PR adds a second attempt at falling back to SSE to solve this. Streamable HTTP should never emit an 'endpoint' event, so if we see this then we will now also trigger a fallback. This is less correct than the normal fallback route for a well-behaving server (messages could get lost or reordered) but in the current initialization behavior this works alright.
1 parent 67d32f2 commit 8ce0cc5

File tree

1 file changed

+17
-7
lines changed

1 file changed

+17
-7
lines changed

src/vs/workbench/api/common/extHostMcp.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as Convert from './extHostTypeConverters.js';
1919
import { AUTH_SERVER_METADATA_DISCOVERY_PATH, getDefaultMetadataForUrl, getMetadataWithDefaultValues, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationProtectedResourceMetadata, isAuthorizationServerMetadata, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js';
2020
import { URI } from '../../../base/common/uri.js';
2121
import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js';
22+
import { CancellationError } from '../../../base/common/errors.js';
2223

2324
export const IExtHostMpcService = createDecorator<IExtHostMpcService>('IExtHostMpcService');
2425

@@ -264,11 +265,7 @@ class McpHTTPHandle extends Disposable {
264265

265266
if (this._mode.value === HttpMode.Unknown && res.status >= 400 && res.status < 500) {
266267
this._log(LogLevel.Info, `${res.status} status sending message to ${this._launch.uri}, will attempt to fall back to legacy SSE`);
267-
const endpoint = await this._attachSSE();
268-
if (endpoint) {
269-
this._mode = { value: HttpMode.SSE, endpoint };
270-
await this._sendLegacySSE(endpoint, message);
271-
}
268+
this._sseFallbackWithMessage(message);
272269
return;
273270
}
274271

@@ -294,7 +291,15 @@ class McpHTTPHandle extends Disposable {
294291
}
295292

296293
// Not awaited, we don't need to block the sequencer while we read the response
297-
this._handleSuccessfulStreamableHttp(res);
294+
this._handleSuccessfulStreamableHttp(res, message);
295+
}
296+
297+
private async _sseFallbackWithMessage(message: string) {
298+
const endpoint = await this._attachSSE();
299+
if (endpoint) {
300+
this._mode = { value: HttpMode.SSE, endpoint };
301+
await this._sendLegacySSE(endpoint, message);
302+
}
298303
}
299304

300305
private async _populateAuthMetadata(originalResponse: Response): Promise<void> {
@@ -428,7 +433,7 @@ class McpHTTPHandle extends Disposable {
428433
throw new Error(`Invalid authorization server metadata: ${JSON.stringify(body)}`);
429434
}
430435

431-
private async _handleSuccessfulStreamableHttp(res: Response) {
436+
private async _handleSuccessfulStreamableHttp(res: Response, message: string) {
432437
if (res.status === 202) {
433438
return; // no body
434439
}
@@ -438,6 +443,11 @@ class McpHTTPHandle extends Disposable {
438443
const parser = new SSEParser(event => {
439444
if (event.type === 'message') {
440445
this._proxy.$onDidReceiveMessage(this._id, event.data);
446+
} else if (event.type === 'endpoint') {
447+
// An SSE server that didn't correctly return a 4xx status when we POSTed
448+
this._log(LogLevel.Warning, `Received SSE endpoint from a POST to ${this._launch.uri}, will fall back to legacy SSE`);
449+
this._sseFallbackWithMessage(message);
450+
throw new CancellationError(); // just to end the SSE stream
441451
}
442452
});
443453

0 commit comments

Comments
 (0)