Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpSamplingLog.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../../../base/common/lifecycle.js';6import { localize } from '../../../../nls.js';7import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js';8import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';9import { IMcpServer } from './mcpTypes.js';10import { MCP } from './modelContextProtocol.js';1112const enum Constants {13SamplingRetentionDays = 7,14MsPerDay = 24 * 60 * 60 * 1000,15SamplingRetentionMs = SamplingRetentionDays * MsPerDay,16SamplingLastNMessage = 30,17}1819interface ISamplingStoredData {20// UTC day ordinal of the first bin in the bins21head: number;22// Requests per day, max length of `Constants.SamplingRetentionDays`23bins: number[];24// Last sampling requests/responses25lastReqs: { request: MCP.SamplingMessage[]; response: string; at: number; model: string }[];26}2728const samplingMemento = observableMemento<ReadonlyMap<string, ISamplingStoredData>>({29defaultValue: new Map(),30key: 'mcp.sampling.logs',31toStorage: v => JSON.stringify(Array.from(v.entries())),32fromStorage: v => new Map(JSON.parse(v)),33});3435export class McpSamplingLog extends Disposable {36private readonly _logs: { [K in StorageScope]?: ObservableMemento<ReadonlyMap<string, ISamplingStoredData>> } = {};3738constructor(39@IStorageService private readonly _storageService: IStorageService,40) {41super();42}4344public has(server: IMcpServer): boolean {45const storage = this._getLogStorageForServer(server);46return storage.get().has(server.definition.id);47}4849public get(server: IMcpServer): Readonly<ISamplingStoredData | undefined> {50const storage = this._getLogStorageForServer(server);51return storage.get().get(server.definition.id);52}5354public getAsText(server: IMcpServer): string {55const storage = this._getLogStorageForServer(server);56const record = storage.get().get(server.definition.id);57if (!record) {58return '';59}6061const parts: string[] = [];62const total = record.bins.reduce((sum, value) => sum + value, 0);63parts.push(localize('mcp.sampling.rpd', '{0} total requests in the last 7 days.', total));6465parts.push(this._formatRecentRequests(record));66return parts.join('\n');67}6869private _formatRecentRequests(data: ISamplingStoredData): string {70if (!data.lastReqs.length) {71return '\nNo recent requests.';72}7374const result: string[] = [];75for (let i = 0; i < data.lastReqs.length; i++) {76const { request, response, at, model } = data.lastReqs[i];77result.push(`\n[${i + 1}] ${new Date(at).toISOString()} ${model}`);7879result.push(' Request:');80for (const msg of request) {81const role = msg.role.padEnd(9);82let content = '';83if ('text' in msg.content && msg.content.type === 'text') {84content = msg.content.text;85} else if ('data' in msg.content) {86content = `[${msg.content.type} data: ${msg.content.mimeType}]`;87}88result.push(` ${role}: ${content}`);89}90result.push(' Response:');91result.push(` ${response}`);92}9394return result.join('\n');95}9697public async add(server: IMcpServer, request: MCP.SamplingMessage[], response: string, model: string) {98const now = Date.now();99const utcOrdinal = Math.floor(now / Constants.MsPerDay);100const storage = this._getLogStorageForServer(server);101102const next = new Map(storage.get());103let record = next.get(server.definition.id);104if (!record) {105record = {106head: utcOrdinal,107bins: Array.from({ length: Constants.SamplingRetentionDays }, () => 0),108lastReqs: [],109};110} else {111// Shift bins back by daysSinceHead, dropping old days112for (let i = 0; i < (utcOrdinal - record.head) && i < Constants.SamplingRetentionDays; i++) {113record.bins.pop();114record.bins.unshift(0);115}116record.head = utcOrdinal;117}118119// Increment the current day's bin (head)120record.bins[0]++;121record.lastReqs.unshift({ request, response, at: now, model });122while (record.lastReqs.length > Constants.SamplingLastNMessage) {123record.lastReqs.pop();124}125126next.set(server.definition.id, record);127storage.set(next, undefined);128}129130private _getLogStorageForServer(server: IMcpServer) {131const scope = server.readDefinitions().get().collection?.scope ?? StorageScope.WORKSPACE;132return this._logs[scope] ??= this._register(samplingMemento(scope, StorageTarget.MACHINE, this._storageService));133}134}135136137