mcp: add request trace-level logging (#250898)

* mcp: add request trace-level logging

Refs https://github.com/microsoft/vscode/issues/250537

* fix

* update headers for layers
This commit is contained in:
Connor Peet
2025-06-07 10:05:06 -07:00
committed by GitHub
parent c91a397553
commit c5ccd47aff
2 changed files with 49 additions and 19 deletions

View File

@@ -10,7 +10,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable }
import { SSEParser } from '../../../base/common/sseParser.js';
import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { LogLevel } from '../../../platform/log/common/log.js';
import { canLog, ILogService, LogLevel } from '../../../platform/log/common/log.js';
import { StorageScope } from '../../../platform/storage/common/storage.js';
import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js';
@@ -40,6 +40,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@ILogService private readonly _logService: ILogService,
@IExtHostInitDataService private readonly _extHostInitData: IExtHostInitDataService
) {
super();
@@ -52,7 +53,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
protected _startMcp(id: number, launch: McpServerLaunch): void {
if (launch.type === McpServerTransportType.HTTP) {
this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy));
this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy, this._logService));
return;
}
@@ -197,7 +198,8 @@ class McpHTTPHandle extends Disposable {
constructor(
private readonly _id: number,
private readonly _launch: McpServerTransportHTTP,
private readonly _proxy: MainThreadMcpShape
private readonly _proxy: MainThreadMcpShape,
private readonly _logService: ILogService,
) {
super();
@@ -246,7 +248,6 @@ class McpHTTPHandle extends Disposable {
this._launch.uri.toString(true),
{
method: 'POST',
signal: this._abortCtrl.signal,
headers,
body: asBytes,
},
@@ -280,7 +281,7 @@ class McpHTTPHandle extends Disposable {
this._proxy.$onDidChangeState(this._id, {
state: McpConnectionState.Kind.Error,
message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + retryWithSessionId ? `; will retry with new session ID` : '',
message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + (retryWithSessionId ? `; will retry with new session ID` : ''),
shouldRetry: retryWithSessionId,
});
return;
@@ -374,9 +375,8 @@ class McpHTTPHandle extends Disposable {
...Object.fromEntries(this._launch.headers)
};
}
const resourceMetadataResponse = await fetch(resourceMetadata, {
const resourceMetadataResponse = await this._fetch(resourceMetadata, {
method: 'GET',
signal: this._abortCtrl.signal,
headers: {
...additionalHeaders,
'Accept': 'application/json',
@@ -401,9 +401,8 @@ class McpHTTPHandle extends Disposable {
const authorizationServerUrl = new URL(authorizationServer);
const extraPath = authorizationServerUrl.pathname === '/' ? '' : authorizationServerUrl.pathname;
const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath;
let authServerMetadataResponse = await fetch(pathToFetch, {
let authServerMetadataResponse = await this._fetch(pathToFetch, {
method: 'GET',
signal: this._abortCtrl.signal,
headers: {
...addtionalHeaders,
'Accept': 'application/json',
@@ -414,11 +413,10 @@ class McpHTTPHandle extends Disposable {
// Try fetching the other discovery URL. For the openid metadata discovery
// path, we _ADD_ the well known path after the existing path.
// https://datatracker.ietf.org/doc/html/rfc8414#section-3
authServerMetadataResponse = await fetch(
authServerMetadataResponse = await this._fetch(
URI.joinPath(URI.parse(authorizationServer), '.well-known', 'openid-configuration').toString(true),
{
method: 'GET',
signal: this._abortCtrl.signal,
headers: {
...addtionalHeaders,
'Accept': 'application/json',
@@ -505,7 +503,6 @@ class McpHTTPHandle extends Disposable {
this._launch.uri.toString(true),
{
method: 'GET',
signal: this._abortCtrl.signal,
headers,
},
headers
@@ -557,7 +554,6 @@ class McpHTTPHandle extends Disposable {
this._launch.uri.toString(true),
{
method: 'GET',
signal: this._abortCtrl.signal,
headers,
},
headers
@@ -599,9 +595,8 @@ class McpHTTPHandle extends Disposable {
'Content-Length': String(asBytes.length),
};
await this._addAuthHeader(headers);
const res = await fetch(url, {
const res = await this._fetch(url, {
method: 'POST',
signal: this._abortCtrl.signal,
headers,
body: asBytes,
});
@@ -670,8 +665,8 @@ class McpHTTPHandle extends Disposable {
* If the initial request returns 401 and we don't have auth metadata,
* it will populate the auth metadata and retry once.
*/
private async _fetchWithAuthRetry(url: string, init: RequestInit, headers: Record<string, string>): Promise<Response> {
const doFetch = () => fetch(url, init);
private async _fetchWithAuthRetry(url: string, init: MinimalRequestInit, headers: Record<string, string>): Promise<Response> {
const doFetch = () => this._fetch(url, init);
let res = await doFetch();
if (res.status === 401) {
@@ -687,6 +682,40 @@ class McpHTTPHandle extends Disposable {
}
return res;
}
private async _fetch(url: string, init: MinimalRequestInit): Promise<Response> {
if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
const traceObj: any = { ...init, headers: { ...init.headers } };
if (traceObj.body) {
traceObj.body = new TextDecoder().decode(traceObj.body);
}
if (traceObj.headers?.Authorization) {
traceObj.headers.Authorization = '***'; // don't log the auth header
}
this._log(LogLevel.Trace, `Fetching ${url} with options: ${JSON.stringify(traceObj)}`);
}
const res = await fetch(url, {
...init,
signal: this._abortCtrl.signal,
});
if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
const headers: Record<string, string> = {};
res.headers.forEach((value, key) => { headers[key] = value; });
this._log(LogLevel.Trace, `Fetched ${url}: ${JSON.stringify({
status: res.status,
headers: headers,
})}`);
}
return res;
}
}
interface MinimalRequestInit {
method: string;
headers: Record<string, string>;
body?: Uint8Array<ArrayBuffer>;
}
function isJSON(str: string): boolean {

View File

@@ -10,7 +10,7 @@ import { parseEnvFile } from '../../../base/common/envfile.js';
import { untildify } from '../../../base/common/labels.js';
import { StreamSplitter } from '../../../base/node/nodeStreams.js';
import { findExecutable } from '../../../base/node/processes.js';
import { LogLevel } from '../../../platform/log/common/log.js';
import { ILogService, LogLevel } from '../../../platform/log/common/log.js';
import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
import { ExtHostMcpService } from '../common/extHostMcp.js';
import { IExtHostRpcService } from '../common/extHostRpcService.js';
@@ -21,8 +21,9 @@ export class NodeExtHostMpcService extends ExtHostMcpService {
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initDataService: IExtHostInitDataService,
@ILogService logService: ILogService,
) {
super(extHostRpc, initDataService);
super(extHostRpc, logService, initDataService);
}
private nodeServers = new Map<number, {