Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts
5252 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 { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { Codicon } from '../../../../base/common/codicons.js';
9
import { MarkdownString } from '../../../../base/common/htmlContent.js';
10
import { Lazy } from '../../../../base/common/lazy.js';
11
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { equals } from '../../../../base/common/objects.js';
13
import { autorun } from '../../../../base/common/observable.js';
14
import { basename } from '../../../../base/common/resources.js';
15
import { isDefined, Mutable } from '../../../../base/common/types.js';
16
import { URI } from '../../../../base/common/uri.js';
17
import { localize } from '../../../../nls.js';
18
import { IFileService } from '../../../../platform/files/common/files.js';
19
import { IImageResizeService } from '../../../../platform/imageResize/common/imageResizeService.js';
20
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
21
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
22
import { mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagement.js';
23
import { IProductService } from '../../../../platform/product/common/productService.js';
24
import { StorageScope } from '../../../../platform/storage/common/storage.js';
25
import { IWorkbenchContribution } from '../../../common/contributions.js';
26
import { ChatResponseResource, getAttachableImageExtension } from '../../chat/common/model/chatModel.js';
27
import { LanguageModelPartAudience } from '../../chat/common/languageModels.js';
28
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/tools/languageModelToolsService.js';
29
import { IMcpRegistry } from './mcpRegistryTypes.js';
30
import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType, McpToolVisibility } from './mcpTypes.js';
31
import { mcpServerToSourceData } from './mcpTypesUtils.js';
32
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
33
34
interface ISyncedToolData {
35
toolData: IToolData;
36
store: DisposableStore;
37
}
38
39
export class McpLanguageModelToolContribution extends Disposable implements IWorkbenchContribution {
40
41
public static readonly ID = 'workbench.contrib.mcp.languageModelTools';
42
43
constructor(
44
@ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService,
45
@IMcpService mcpService: IMcpService,
46
@IInstantiationService private readonly _instantiationService: IInstantiationService,
47
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
48
@ILifecycleService private readonly lifecycleService: ILifecycleService,
49
) {
50
super();
51
52
type Rec = { source?: ToolDataSource } & IDisposable;
53
54
// Keep tools in sync with the tools service.
55
const previous = this._register(new DisposableMap<IMcpServer, Rec>());
56
this._register(autorun(reader => {
57
const servers = mcpService.servers.read(reader);
58
59
const toDelete = new Set(previous.keys());
60
for (const server of servers) {
61
const previousRec = previous.get(server);
62
if (previousRec) {
63
toDelete.delete(server);
64
if (!previousRec.source || equals(previousRec.source, mcpServerToSourceData(server, reader))) {
65
continue; // same definition, no need to update
66
}
67
68
previousRec.dispose();
69
}
70
71
const store = new DisposableStore();
72
const rec: Rec = { dispose: () => store.dispose() };
73
const toolSet = new Lazy(() => {
74
const source = rec.source = mcpServerToSourceData(server);
75
const referenceName = server.definition.label.toLowerCase().replace(/\s+/g, '-'); // see issue https://github.com/microsoft/vscode/issues/278152
76
const toolSet = store.add(this._toolsService.createToolSet(
77
source,
78
server.definition.id,
79
referenceName,
80
{
81
icon: Codicon.mcp,
82
description: localize('mcp.toolset', "{0}: All Tools", server.definition.label)
83
}
84
));
85
86
return { toolSet, source };
87
});
88
89
this._syncTools(server, toolSet, store);
90
previous.set(server, rec);
91
}
92
93
for (const key of toDelete) {
94
previous.deleteAndDispose(key);
95
}
96
}));
97
}
98
99
private _syncTools(server: IMcpServer, collectionData: Lazy<{ toolSet: ToolSet; source: ToolDataSource }>, store: DisposableStore) {
100
const tools = new Map</* tool ID */string, ISyncedToolData>();
101
102
const collectionObservable = this._mcpRegistry.collections.map(collections =>
103
collections.find(c => c.id === server.collection.id));
104
105
store.add(autorun(reader => {
106
const toDelete = new Set(tools.keys());
107
108
// toRegister is deferred until deleting tools that moving a tool between
109
// servers (or deleting one instance of a multi-instance server) doesn't cause an error.
110
const toRegister: (() => void)[] = [];
111
const registerTool = (tool: IMcpTool, toolData: IToolData, store: DisposableStore) => {
112
store.add(this._toolsService.registerTool(toolData, this._instantiationService.createInstance(McpToolImplementation, tool, server)));
113
store.add(collectionData.value.toolSet.addTool(toolData));
114
};
115
116
// Don't bother cleaning up tools internally during shutdown. This just costs time for no benefit.
117
if (this.lifecycleService.willShutdown) {
118
return;
119
}
120
121
const collection = collectionObservable.read(reader);
122
if (!collection) {
123
tools.forEach(t => t.store.dispose());
124
tools.clear();
125
return;
126
}
127
128
for (const tool of server.tools.read(reader)) {
129
// Skip app-only tools - they should not be registered with the language model tools service
130
if (!(tool.visibility & McpToolVisibility.Model)) {
131
continue;
132
}
133
134
const existing = tools.get(tool.id);
135
const icons = tool.icons.getUrl(22);
136
const toolData: IToolData = {
137
id: tool.id,
138
source: collectionData.value.source,
139
icon: icons || Codicon.tools,
140
// duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813
141
displayName: tool.definition.annotations?.title || tool.definition.title || tool.definition.name,
142
toolReferenceName: tool.referenceName,
143
modelDescription: tool.definition.description ?? '',
144
userDescription: tool.definition.description ?? '',
145
inputSchema: tool.definition.inputSchema,
146
canBeReferencedInPrompt: true,
147
alwaysDisplayInputOutput: true,
148
canRequestPreApproval: !tool.definition.annotations?.readOnlyHint,
149
canRequestPostApproval: !!tool.definition.annotations?.openWorldHint,
150
runsInWorkspace: collection?.scope === StorageScope.WORKSPACE || !!collection?.remoteAuthority,
151
tags: ['mcp'],
152
};
153
154
if (existing) {
155
if (!equals(existing.toolData, toolData)) {
156
existing.toolData = toolData;
157
existing.store.clear();
158
// We need to re-register both the data and implementation, as the
159
// implementation is discarded when the data is removed (#245921)
160
registerTool(tool, toolData, existing.store);
161
}
162
toDelete.delete(tool.id);
163
} else {
164
const store = new DisposableStore();
165
toRegister.push(() => registerTool(tool, toolData, store));
166
tools.set(tool.id, { toolData, store });
167
}
168
}
169
170
for (const id of toDelete) {
171
const tool = tools.get(id);
172
if (tool) {
173
tool.store.dispose();
174
tools.delete(id);
175
}
176
}
177
178
for (const fn of toRegister) {
179
fn();
180
}
181
182
// Important: flush tool updates when the server is fully registered so that
183
// any consuming (e.g. autostarting) requests have the tools available immediately.
184
this._toolsService.flushToolUpdates();
185
}));
186
187
store.add(toDisposable(() => {
188
for (const tool of tools.values()) {
189
tool.store.dispose();
190
}
191
}));
192
}
193
}
194
195
class McpToolImplementation implements IToolImpl {
196
constructor(
197
private readonly _tool: IMcpTool,
198
private readonly _server: IMcpServer,
199
@IConfigurationService private readonly _configurationService: IConfigurationService,
200
@IProductService private readonly _productService: IProductService,
201
@IFileService private readonly _fileService: IFileService,
202
@IImageResizeService private readonly _imageResizeService: IImageResizeService,
203
) { }
204
205
async prepareToolInvocation(context: IToolInvocationPreparationContext): Promise<IPreparedToolInvocation> {
206
const tool = this._tool;
207
const server = this._server;
208
209
const mcpToolWarning = localize(
210
'mcp.tool.warning',
211
"Note that MCP servers or malicious conversation content may attempt to misuse '{0}' through tools.",
212
this._productService.nameShort
213
);
214
215
// duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813
216
const title = tool.definition.annotations?.title || tool.definition.title || ('`' + tool.definition.name + '`');
217
218
const confirm: IToolConfirmationMessages = {};
219
if (!tool.definition.annotations?.readOnlyHint) {
220
confirm.title = new MarkdownString(localize('msg.title', "Run {0}", title));
221
confirm.message = new MarkdownString(tool.definition.description, { supportThemeIcons: true });
222
confirm.disclaimer = mcpToolWarning;
223
confirm.allowAutoConfirm = true;
224
}
225
if (tool.definition.annotations?.openWorldHint) {
226
confirm.confirmResults = true;
227
}
228
229
const mcpUiEnabled = this._configurationService.getValue<boolean>(mcpAppsEnabledConfig);
230
231
return {
232
confirmationMessages: confirm,
233
invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)),
234
pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran {0} ", title)),
235
originMessage: localize('msg.subtitle', "{0} (MCP Server)", server.definition.label),
236
toolSpecificData: {
237
kind: 'input',
238
rawInput: context.parameters,
239
mcpAppData: mcpUiEnabled && tool.uiResourceUri ? {
240
resourceUri: tool.uiResourceUri,
241
serverDefinitionId: server.definition.id,
242
collectionId: server.collection.id,
243
} : undefined,
244
}
245
};
246
}
247
248
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) {
249
250
const result: IToolResult = {
251
content: []
252
};
253
254
const callResult = await this._tool.callWithProgress(invocation.parameters as Record<string, unknown>, progress, { chatRequestId: invocation.chatRequestId, chatSessionId: invocation.context?.sessionId }, token);
255
const details: Mutable<IToolResultInputOutputDetails> = {
256
input: JSON.stringify(invocation.parameters, undefined, 2),
257
output: [],
258
isError: callResult.isError === true,
259
};
260
261
for (const item of callResult.content) {
262
const audience = item.annotations?.audience?.map(a => {
263
if (a === 'assistant') {
264
return LanguageModelPartAudience.Assistant;
265
} else if (a === 'user') {
266
return LanguageModelPartAudience.User;
267
} else {
268
return undefined;
269
}
270
}).filter(isDefined);
271
272
// Explicit user parts get pushed to progress to show in the status UI
273
if (audience?.includes(LanguageModelPartAudience.User)) {
274
if (item.type === 'text') {
275
progress.report({ message: item.text });
276
}
277
}
278
279
// Rewrite image resources to images so they are inlined nicely
280
const addAsInlineData = async (mimeType: string, value: string, uri?: URI): Promise<VSBuffer | void> => {
281
details.output.push({ type: 'embed', mimeType, value, uri, audience });
282
if (isForModel) {
283
let finalData: VSBuffer;
284
try {
285
const resized = await this._imageResizeService.resizeImage(decodeBase64(value).buffer, mimeType);
286
finalData = VSBuffer.wrap(resized);
287
} catch {
288
finalData = decodeBase64(value);
289
}
290
result.content.push({ kind: 'data', value: { mimeType, data: finalData }, audience });
291
}
292
};
293
294
const addAsLinkedResource = (uri: URI, mimeType?: string) => {
295
const json: IMcpToolResourceLinkContents = { uri, underlyingMimeType: mimeType };
296
result.content.push({
297
kind: 'data',
298
audience,
299
value: {
300
mimeType: McpToolResourceLinkMimeType,
301
data: VSBuffer.fromString(JSON.stringify(json)),
302
},
303
});
304
};
305
306
const isForModel = !audience || audience.includes(LanguageModelPartAudience.Assistant);
307
if (item.type === 'text') {
308
details.output.push({ type: 'embed', isText: true, value: item.text });
309
// structured content 'represents the result of the tool call', so take
310
// that in place of any textual description when present.
311
if (isForModel && !callResult.structuredContent) {
312
result.content.push({
313
kind: 'text',
314
audience,
315
value: item.text
316
});
317
}
318
} else if (item.type === 'image' || item.type === 'audio') {
319
// default to some image type if not given to hint
320
await addAsInlineData(item.mimeType || 'image/png', item.data);
321
} else if (item.type === 'resource_link') {
322
const uri = McpResourceURI.fromServer(this._server.definition, item.uri);
323
details.output.push({
324
type: 'ref',
325
uri,
326
audience,
327
mimeType: item.mimeType,
328
});
329
330
if (isForModel) {
331
if (item.mimeType && getAttachableImageExtension(item.mimeType)) {
332
result.content.push({
333
kind: 'data',
334
audience,
335
value: {
336
mimeType: item.mimeType,
337
data: await this._fileService.readFile(uri).then(f => f.value).catch(() => VSBuffer.alloc(0)),
338
}
339
});
340
} else {
341
addAsLinkedResource(uri, item.mimeType);
342
}
343
}
344
} else if (item.type === 'resource') {
345
const uri = McpResourceURI.fromServer(this._server.definition, item.resource.uri);
346
if (item.resource.mimeType && getAttachableImageExtension(item.resource.mimeType) && 'blob' in item.resource) {
347
await addAsInlineData(item.resource.mimeType, item.resource.blob, uri);
348
} else {
349
details.output.push({
350
type: 'embed',
351
uri,
352
isText: 'text' in item.resource,
353
mimeType: item.resource.mimeType,
354
value: 'blob' in item.resource ? item.resource.blob : item.resource.text,
355
audience,
356
asResource: true,
357
});
358
359
if (isForModel) {
360
const permalink = invocation.context && ChatResponseResource.createUri(invocation.context.sessionResource, invocation.callId, result.content.length, basename(uri));
361
addAsLinkedResource(permalink || uri, item.resource.mimeType);
362
}
363
}
364
}
365
}
366
367
if (callResult.structuredContent) {
368
details.output.push({ type: 'embed', isText: true, value: JSON.stringify(callResult.structuredContent, null, 2), audience: [LanguageModelPartAudience.Assistant] });
369
result.content.push({ kind: 'text', value: JSON.stringify(callResult.structuredContent), audience: [LanguageModelPartAudience.Assistant] });
370
}
371
372
// Add raw MCP output for MCP App UI rendering if this tool has UI
373
if (this._tool.uiResourceUri) {
374
details.mcpOutput = callResult;
375
}
376
377
result.toolResultDetails = details;
378
return result;
379
}
380
381
}
382
383