Skip to content

Commit c5ccd47

Browse files
authored
mcp: add request trace-level logging (microsoft#250898)
* mcp: add request trace-level logging Refs microsoft#250537 * fix * update headers for layers
1 parent c91a397 commit c5ccd47

File tree

2 files changed

+49
-19
lines changed

2 files changed

+49
-19
lines changed

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

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable }
1010
import { SSEParser } from '../../../base/common/sseParser.js';
1111
import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
1212
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
13-
import { LogLevel } from '../../../platform/log/common/log.js';
13+
import { canLog, ILogService, LogLevel } from '../../../platform/log/common/log.js';
1414
import { StorageScope } from '../../../platform/storage/common/storage.js';
1515
import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
1616
import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js';
@@ -40,6 +40,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
4040

4141
constructor(
4242
@IExtHostRpcService extHostRpc: IExtHostRpcService,
43+
@ILogService private readonly _logService: ILogService,
4344
@IExtHostInitDataService private readonly _extHostInitData: IExtHostInitDataService
4445
) {
4546
super();
@@ -52,7 +53,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
5253

5354
protected _startMcp(id: number, launch: McpServerLaunch): void {
5455
if (launch.type === McpServerTransportType.HTTP) {
55-
this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy));
56+
this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy, this._logService));
5657
return;
5758
}
5859

@@ -197,7 +198,8 @@ class McpHTTPHandle extends Disposable {
197198
constructor(
198199
private readonly _id: number,
199200
private readonly _launch: McpServerTransportHTTP,
200-
private readonly _proxy: MainThreadMcpShape
201+
private readonly _proxy: MainThreadMcpShape,
202+
private readonly _logService: ILogService,
201203
) {
202204
super();
203205

@@ -246,7 +248,6 @@ class McpHTTPHandle extends Disposable {
246248
this._launch.uri.toString(true),
247249
{
248250
method: 'POST',
249-
signal: this._abortCtrl.signal,
250251
headers,
251252
body: asBytes,
252253
},
@@ -280,7 +281,7 @@ class McpHTTPHandle extends Disposable {
280281

281282
this._proxy.$onDidChangeState(this._id, {
282283
state: McpConnectionState.Kind.Error,
283-
message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + retryWithSessionId ? `; will retry with new session ID` : '',
284+
message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + (retryWithSessionId ? `; will retry with new session ID` : ''),
284285
shouldRetry: retryWithSessionId,
285286
});
286287
return;
@@ -374,9 +375,8 @@ class McpHTTPHandle extends Disposable {
374375
...Object.fromEntries(this._launch.headers)
375376
};
376377
}
377-
const resourceMetadataResponse = await fetch(resourceMetadata, {
378+
const resourceMetadataResponse = await this._fetch(resourceMetadata, {
378379
method: 'GET',
379-
signal: this._abortCtrl.signal,
380380
headers: {
381381
...additionalHeaders,
382382
'Accept': 'application/json',
@@ -401,9 +401,8 @@ class McpHTTPHandle extends Disposable {
401401
const authorizationServerUrl = new URL(authorizationServer);
402402
const extraPath = authorizationServerUrl.pathname === '/' ? '' : authorizationServerUrl.pathname;
403403
const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath;
404-
let authServerMetadataResponse = await fetch(pathToFetch, {
404+
let authServerMetadataResponse = await this._fetch(pathToFetch, {
405405
method: 'GET',
406-
signal: this._abortCtrl.signal,
407406
headers: {
408407
...addtionalHeaders,
409408
'Accept': 'application/json',
@@ -414,11 +413,10 @@ class McpHTTPHandle extends Disposable {
414413
// Try fetching the other discovery URL. For the openid metadata discovery
415414
// path, we _ADD_ the well known path after the existing path.
416415
// https://datatracker.ietf.org/doc/html/rfc8414#section-3
417-
authServerMetadataResponse = await fetch(
416+
authServerMetadataResponse = await this._fetch(
418417
URI.joinPath(URI.parse(authorizationServer), '.well-known', 'openid-configuration').toString(true),
419418
{
420419
method: 'GET',
421-
signal: this._abortCtrl.signal,
422420
headers: {
423421
...addtionalHeaders,
424422
'Accept': 'application/json',
@@ -505,7 +503,6 @@ class McpHTTPHandle extends Disposable {
505503
this._launch.uri.toString(true),
506504
{
507505
method: 'GET',
508-
signal: this._abortCtrl.signal,
509506
headers,
510507
},
511508
headers
@@ -557,7 +554,6 @@ class McpHTTPHandle extends Disposable {
557554
this._launch.uri.toString(true),
558555
{
559556
method: 'GET',
560-
signal: this._abortCtrl.signal,
561557
headers,
562558
},
563559
headers
@@ -599,9 +595,8 @@ class McpHTTPHandle extends Disposable {
599595
'Content-Length': String(asBytes.length),
600596
};
601597
await this._addAuthHeader(headers);
602-
const res = await fetch(url, {
598+
const res = await this._fetch(url, {
603599
method: 'POST',
604-
signal: this._abortCtrl.signal,
605600
headers,
606601
body: asBytes,
607602
});
@@ -670,8 +665,8 @@ class McpHTTPHandle extends Disposable {
670665
* If the initial request returns 401 and we don't have auth metadata,
671666
* it will populate the auth metadata and retry once.
672667
*/
673-
private async _fetchWithAuthRetry(url: string, init: RequestInit, headers: Record<string, string>): Promise<Response> {
674-
const doFetch = () => fetch(url, init);
668+
private async _fetchWithAuthRetry(url: string, init: MinimalRequestInit, headers: Record<string, string>): Promise<Response> {
669+
const doFetch = () => this._fetch(url, init);
675670

676671
let res = await doFetch();
677672
if (res.status === 401) {
@@ -687,6 +682,40 @@ class McpHTTPHandle extends Disposable {
687682
}
688683
return res;
689684
}
685+
686+
private async _fetch(url: string, init: MinimalRequestInit): Promise<Response> {
687+
if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
688+
const traceObj: any = { ...init, headers: { ...init.headers } };
689+
if (traceObj.body) {
690+
traceObj.body = new TextDecoder().decode(traceObj.body);
691+
}
692+
if (traceObj.headers?.Authorization) {
693+
traceObj.headers.Authorization = '***'; // don't log the auth header
694+
}
695+
this._log(LogLevel.Trace, `Fetching ${url} with options: ${JSON.stringify(traceObj)}`);
696+
}
697+
const res = await fetch(url, {
698+
...init,
699+
signal: this._abortCtrl.signal,
700+
});
701+
702+
if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
703+
const headers: Record<string, string> = {};
704+
res.headers.forEach((value, key) => { headers[key] = value; });
705+
this._log(LogLevel.Trace, `Fetched ${url}: ${JSON.stringify({
706+
status: res.status,
707+
headers: headers,
708+
})}`);
709+
}
710+
711+
return res;
712+
}
713+
}
714+
715+
interface MinimalRequestInit {
716+
method: string;
717+
headers: Record<string, string>;
718+
body?: Uint8Array<ArrayBuffer>;
690719
}
691720

692721
function isJSON(str: string): boolean {

src/vs/workbench/api/node/extHostMcpNode.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { parseEnvFile } from '../../../base/common/envfile.js';
1010
import { untildify } from '../../../base/common/labels.js';
1111
import { StreamSplitter } from '../../../base/node/nodeStreams.js';
1212
import { findExecutable } from '../../../base/node/processes.js';
13-
import { LogLevel } from '../../../platform/log/common/log.js';
13+
import { ILogService, LogLevel } from '../../../platform/log/common/log.js';
1414
import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
1515
import { ExtHostMcpService } from '../common/extHostMcp.js';
1616
import { IExtHostRpcService } from '../common/extHostRpcService.js';
@@ -21,8 +21,9 @@ export class NodeExtHostMpcService extends ExtHostMcpService {
2121
constructor(
2222
@IExtHostRpcService extHostRpc: IExtHostRpcService,
2323
@IExtHostInitDataService initDataService: IExtHostInitDataService,
24+
@ILogService logService: ILogService,
2425
) {
25-
super(extHostRpc, initDataService);
26+
super(extHostRpc, logService, initDataService);
2627
}
2728

2829
private nodeServers = new Map<number, {

0 commit comments

Comments
 (0)