Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpSamplingLog.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Disposable } from '../../../../base/common/lifecycle.js';
7
import { localize } from '../../../../nls.js';
8
import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js';
9
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
10
import { IMcpServer } from './mcpTypes.js';
11
import { MCP } from './modelContextProtocol.js';
12
13
const enum Constants {
14
SamplingRetentionDays = 7,
15
MsPerDay = 24 * 60 * 60 * 1000,
16
SamplingRetentionMs = SamplingRetentionDays * MsPerDay,
17
SamplingLastNMessage = 30,
18
}
19
20
interface ISamplingStoredData {
21
// UTC day ordinal of the first bin in the bins
22
head: number;
23
// Requests per day, max length of `Constants.SamplingRetentionDays`
24
bins: number[];
25
// Last sampling requests/responses
26
lastReqs: { request: MCP.SamplingMessage[]; response: string; at: number; model: string }[];
27
}
28
29
const samplingMemento = observableMemento<ReadonlyMap<string, ISamplingStoredData>>({
30
defaultValue: new Map(),
31
key: 'mcp.sampling.logs',
32
toStorage: v => JSON.stringify(Array.from(v.entries())),
33
fromStorage: v => new Map(JSON.parse(v)),
34
});
35
36
export class McpSamplingLog extends Disposable {
37
private readonly _logs: { [K in StorageScope]?: ObservableMemento<ReadonlyMap<string, ISamplingStoredData>> } = {};
38
39
constructor(
40
@IStorageService private readonly _storageService: IStorageService,
41
) {
42
super();
43
}
44
45
public has(server: IMcpServer): boolean {
46
const storage = this._getLogStorageForServer(server);
47
return storage.get().has(server.definition.id);
48
}
49
50
public get(server: IMcpServer): Readonly<ISamplingStoredData | undefined> {
51
const storage = this._getLogStorageForServer(server);
52
return storage.get().get(server.definition.id);
53
}
54
55
public getAsText(server: IMcpServer): string {
56
const storage = this._getLogStorageForServer(server);
57
const record = storage.get().get(server.definition.id);
58
if (!record) {
59
return '';
60
}
61
62
const parts: string[] = [];
63
const total = record.bins.reduce((sum, value) => sum + value, 0);
64
parts.push(localize('mcp.sampling.rpd', '{0} total requests in the last 7 days.', total));
65
66
parts.push(this._formatRecentRequests(record));
67
return parts.join('\n');
68
}
69
70
private _formatRecentRequests(data: ISamplingStoredData): string {
71
if (!data.lastReqs.length) {
72
return '\nNo recent requests.';
73
}
74
75
const result: string[] = [];
76
for (let i = 0; i < data.lastReqs.length; i++) {
77
const { request, response, at, model } = data.lastReqs[i];
78
result.push(`\n[${i + 1}] ${new Date(at).toISOString()} ${model}`);
79
80
result.push(' Request:');
81
for (const msg of request) {
82
const role = msg.role.padEnd(9);
83
let content = '';
84
if ('text' in msg.content && msg.content.type === 'text') {
85
content = msg.content.text;
86
} else if ('data' in msg.content) {
87
content = `[${msg.content.type} data: ${msg.content.mimeType}]`;
88
}
89
result.push(` ${role}: ${content}`);
90
}
91
result.push(' Response:');
92
result.push(` ${response}`);
93
}
94
95
return result.join('\n');
96
}
97
98
public async add(server: IMcpServer, request: MCP.SamplingMessage[], response: string, model: string) {
99
const now = Date.now();
100
const utcOrdinal = Math.floor(now / Constants.MsPerDay);
101
const storage = this._getLogStorageForServer(server);
102
103
const next = new Map(storage.get());
104
let record = next.get(server.definition.id);
105
if (!record) {
106
record = {
107
head: utcOrdinal,
108
bins: Array.from({ length: Constants.SamplingRetentionDays }, () => 0),
109
lastReqs: [],
110
};
111
} else {
112
// Shift bins back by daysSinceHead, dropping old days
113
for (let i = 0; i < (utcOrdinal - record.head) && i < Constants.SamplingRetentionDays; i++) {
114
record.bins.pop();
115
record.bins.unshift(0);
116
}
117
record.head = utcOrdinal;
118
}
119
120
// Increment the current day's bin (head)
121
record.bins[0]++;
122
record.lastReqs.unshift({ request, response, at: now, model });
123
while (record.lastReqs.length > Constants.SamplingLastNMessage) {
124
record.lastReqs.pop();
125
}
126
127
next.set(server.definition.id, record);
128
storage.set(next, undefined);
129
}
130
131
private _getLogStorageForServer(server: IMcpServer) {
132
const scope = server.readDefinitions().get().collection?.scope ?? StorageScope.WORKSPACE;
133
return this._logs[scope] ??= this._register(samplingMemento(scope, StorageTarget.MACHINE, this._storageService));
134
}
135
}
136
137