Skip to content

Commit 09d353c

Browse files
Fix SSE auth in MCP (microsoft#250686)
Do retries on 401.
1 parent 5969fdd commit 09d353c

File tree

1 file changed

+44
-23
lines changed

1 file changed

+44
-23
lines changed

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

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -242,27 +242,17 @@ class McpHTTPHandle extends Disposable {
242242
}
243243
await this._addAuthHeader(headers);
244244

245-
const doFetch = () => fetch(
245+
const res = await this._fetchWithAuthRetry(
246246
this._launch.uri.toString(true),
247247
{
248248
method: 'POST',
249249
signal: this._abortCtrl.signal,
250250
headers,
251251
body: asBytes,
252-
}
252+
},
253+
headers
253254
);
254255

255-
let res = await doFetch();
256-
if (res.status === 401) {
257-
if (!this._authMetadata) {
258-
await this._populateAuthMetadata(res);
259-
await this._addAuthHeader(headers);
260-
if (headers['Authorization']) {
261-
res = await doFetch();
262-
}
263-
}
264-
}
265-
266256
const wasUnknown = this._mode.value === HttpMode.Unknown;
267257

268258
// Mcp-Session-Id is the strongest signal that we're in streamable HTTP mode
@@ -511,11 +501,15 @@ class McpHTTPHandle extends Disposable {
511501
headers['Last-Event-ID'] = lastEventId;
512502
}
513503

514-
res = await fetch(this._launch.uri.toString(true), {
515-
method: 'GET',
516-
signal: this._abortCtrl.signal,
517-
headers,
518-
});
504+
res = await this._fetchWithAuthRetry(
505+
this._launch.uri.toString(true),
506+
{
507+
method: 'GET',
508+
signal: this._abortCtrl.signal,
509+
headers,
510+
},
511+
headers
512+
);
519513
} catch (e) {
520514
this._log(LogLevel.Info, `Error connecting to ${this._launch.uri} for async notifications, will retry`);
521515
continue;
@@ -559,11 +553,15 @@ class McpHTTPHandle extends Disposable {
559553

560554
let res: Response;
561555
try {
562-
res = await fetch(this._launch.uri.toString(true), {
563-
method: 'GET',
564-
signal: this._abortCtrl.signal,
565-
headers,
566-
});
556+
res = await this._fetchWithAuthRetry(
557+
this._launch.uri.toString(true),
558+
{
559+
method: 'GET',
560+
signal: this._abortCtrl.signal,
561+
headers,
562+
},
563+
headers
564+
);
567565
if (res.status >= 300) {
568566
this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${this._launch.uri} as SSE: ${await this._getErrText(res)}` });
569567
return;
@@ -666,6 +664,29 @@ class McpHTTPHandle extends Disposable {
666664
return res.statusText;
667665
}
668666
}
667+
668+
/**
669+
* Helper method to perform fetch with 401 authentication retry logic.
670+
* If the initial request returns 401 and we don't have auth metadata,
671+
* it will populate the auth metadata and retry once.
672+
*/
673+
private async _fetchWithAuthRetry(url: string, init: RequestInit, headers: Record<string, string>): Promise<Response> {
674+
const doFetch = () => fetch(url, init);
675+
676+
let res = await doFetch();
677+
if (res.status === 401) {
678+
if (!this._authMetadata) {
679+
await this._populateAuthMetadata(res);
680+
await this._addAuthHeader(headers);
681+
if (headers['Authorization']) {
682+
// Update the headers in the init object
683+
init.headers = headers;
684+
res = await doFetch();
685+
}
686+
}
687+
}
688+
return res;
689+
}
669690
}
670691

671692
function isJSON(str: string): boolean {

0 commit comments

Comments
 (0)