mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user