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
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 { 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, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { equals } from '../../../../base/common/objects.js';
13
import { autorun, autorunSelfDisposable } from '../../../../base/common/observable.js';
14
import { basename } from '../../../../base/common/resources.js';
15
import { URI } from '../../../../base/common/uri.js';
16
import { localize } from '../../../../nls.js';
17
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.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 { IProductService } from '../../../../platform/product/common/productService.js';
22
import { StorageScope } from '../../../../platform/storage/common/storage.js';
23
import { IWorkbenchContribution } from '../../../common/contributions.js';
24
import { ChatResponseResource, getAttachableImageExtension } from '../../chat/common/chatModel.js';
25
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/languageModelToolsService.js';
26
import { IMcpRegistry } from './mcpRegistryTypes.js';
27
import { IMcpServer, IMcpService, IMcpTool, LazyCollectionState, McpResourceURI, McpServerCacheState } from './mcpTypes.js';
28
import { mcpServerToSourceData } from './mcpTypesUtils.js';
29
30
interface ISyncedToolData {
31
toolData: IToolData;
32
store: DisposableStore;
33
}
34
35
export class McpLanguageModelToolContribution extends Disposable implements IWorkbenchContribution {
36
37
public static readonly ID = 'workbench.contrib.mcp.languageModelTools';
38
39
constructor(
40
@ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService,
41
@IMcpService mcpService: IMcpService,
42
@IInstantiationService private readonly _instantiationService: IInstantiationService,
43
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
44
) {
45
super();
46
47
// 1. Auto-discover extensions with new MCP servers
48
const lazyCollectionState = mcpService.lazyCollectionState.map(s => s.state);
49
this._register(autorun(reader => {
50
if (lazyCollectionState.read(reader) === LazyCollectionState.HasUnknown) {
51
mcpService.activateCollections();
52
}
53
}));
54
55
// 2. Keep tools in sync with the tools service.
56
const previous = this._register(new DisposableMap<IMcpServer, DisposableStore>());
57
this._register(autorun(reader => {
58
const servers = mcpService.servers.read(reader);
59
60
const toDelete = new Set(previous.keys());
61
for (const server of servers) {
62
if (previous.has(server)) {
63
toDelete.delete(server);
64
continue;
65
}
66
67
const store = new DisposableStore();
68
const toolSet = new Lazy(() => {
69
const source = mcpServerToSourceData(server);
70
const toolSet = store.add(this._toolsService.createToolSet(
71
source,
72
server.definition.id, server.definition.label,
73
{
74
icon: Codicon.mcp,
75
description: localize('mcp.toolset', "{0}: All Tools", server.definition.label)
76
}
77
));
78
79
return { toolSet, source };
80
});
81
82
this._syncTools(server, toolSet, store);
83
previous.set(server, store);
84
}
85
86
for (const key of toDelete) {
87
previous.deleteAndDispose(key);
88
}
89
}));
90
}
91
92
private _syncTools(server: IMcpServer, collectionData: Lazy<{ toolSet: ToolSet; source: ToolDataSource }>, store: DisposableStore) {
93
const tools = new Map</* tool ID */string, ISyncedToolData>();
94
95
const collectionObservable = this._mcpRegistry.collections.map(collections =>
96
collections.find(c => c.id === server.collection.id));
97
98
// If the server is extension-provided and was marked outdated automatically start it
99
store.add(autorunSelfDisposable(reader => {
100
const collection = collectionObservable.read(reader);
101
if (!collection) {
102
return;
103
}
104
105
if (!(collection.source instanceof ExtensionIdentifier)) {
106
reader.dispose();
107
return;
108
}
109
110
const cacheState = server.cacheState.read(reader);
111
if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) {
112
reader.dispose();
113
server.start();
114
}
115
}));
116
117
store.add(autorun(reader => {
118
const toDelete = new Set(tools.keys());
119
120
// toRegister is deferred until deleting tools that moving a tool between
121
// servers (or deleting one instance of a multi-instance server) doesn't cause an error.
122
const toRegister: (() => void)[] = [];
123
const registerTool = (tool: IMcpTool, toolData: IToolData, store: DisposableStore) => {
124
store.add(this._toolsService.registerTool(toolData, this._instantiationService.createInstance(McpToolImplementation, tool, server)));
125
store.add(collectionData.value.toolSet.addTool(toolData));
126
};
127
128
const collection = collectionObservable.read(reader);
129
for (const tool of server.tools.read(reader)) {
130
const existing = tools.get(tool.id);
131
const toolData: IToolData = {
132
id: tool.id,
133
source: collectionData.value.source,
134
icon: Codicon.tools,
135
// duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813
136
displayName: tool.definition.annotations?.title || tool.definition.title || tool.definition.name,
137
toolReferenceName: tool.referenceName,
138
modelDescription: tool.definition.description ?? '',
139
userDescription: tool.definition.description ?? '',
140
inputSchema: tool.definition.inputSchema,
141
canBeReferencedInPrompt: true,
142
alwaysDisplayInputOutput: true,
143
runsInWorkspace: collection?.scope === StorageScope.WORKSPACE || !!collection?.remoteAuthority,
144
tags: ['mcp'],
145
};
146
147
if (existing) {
148
if (!equals(existing.toolData, toolData)) {
149
existing.toolData = toolData;
150
existing.store.clear();
151
// We need to re-register both the data and implementation, as the
152
// implementation is discarded when the data is removed (#245921)
153
registerTool(tool, toolData, existing.store);
154
}
155
toDelete.delete(tool.id);
156
} else {
157
const store = new DisposableStore();
158
toRegister.push(() => registerTool(tool, toolData, store));
159
tools.set(tool.id, { toolData, store });
160
}
161
}
162
163
for (const id of toDelete) {
164
const tool = tools.get(id);
165
if (tool) {
166
tool.store.dispose();
167
tools.delete(id);
168
}
169
}
170
171
for (const fn of toRegister) {
172
fn();
173
}
174
}));
175
176
store.add(toDisposable(() => {
177
for (const tool of tools.values()) {
178
tool.store.dispose();
179
}
180
}));
181
}
182
}
183
184
class McpToolImplementation implements IToolImpl {
185
constructor(
186
private readonly _tool: IMcpTool,
187
private readonly _server: IMcpServer,
188
@IProductService private readonly _productService: IProductService,
189
@IFileService private readonly _fileService: IFileService,
190
@IImageResizeService private readonly _imageResizeService: IImageResizeService,
191
) { }
192
193
async prepareToolInvocation(context: IToolInvocationPreparationContext): Promise<IPreparedToolInvocation> {
194
const tool = this._tool;
195
const server = this._server;
196
197
const mcpToolWarning = localize(
198
'mcp.tool.warning',
199
"Note that MCP servers or malicious conversation content may attempt to misuse '{0}' through tools.",
200
this._productService.nameShort
201
);
202
203
const needsConfirmation = !tool.definition.annotations?.readOnlyHint;
204
// duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813
205
const title = tool.definition.annotations?.title || tool.definition.title || ('`' + tool.definition.name + '`');
206
207
return {
208
confirmationMessages: needsConfirmation ? {
209
title: new MarkdownString(localize('msg.title', "Run {0}", title)),
210
message: new MarkdownString(tool.definition.description, { supportThemeIcons: true }),
211
disclaimer: mcpToolWarning,
212
allowAutoConfirm: true,
213
} : undefined,
214
invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)),
215
pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran {0} ", title)),
216
originMessage: localize('msg.subtitle', "{0} (MCP Server)", server.definition.label),
217
toolSpecificData: {
218
kind: 'input',
219
rawInput: context.parameters
220
}
221
};
222
}
223
224
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) {
225
226
const result: IToolResult = {
227
content: []
228
};
229
230
const callResult = await this._tool.callWithProgress(invocation.parameters as Record<string, any>, progress, { chatRequestId: invocation.chatRequestId, chatSessionId: invocation.context?.sessionId }, token);
231
const details: IToolResultInputOutputDetails = {
232
input: JSON.stringify(invocation.parameters, undefined, 2),
233
output: [],
234
isError: callResult.isError === true,
235
};
236
237
for (const item of callResult.content) {
238
const audience = item.annotations?.audience || ['assistant'];
239
if (audience.includes('user')) {
240
if (item.type === 'text') {
241
progress.report({ message: item.text });
242
}
243
}
244
245
// Rewrite image resources to images so they are inlined nicely
246
const addAsInlineData = async (mimeType: string, value: string, uri?: URI): Promise<VSBuffer | void> => {
247
details.output.push({ type: 'embed', mimeType, value, uri });
248
if (isForModel) {
249
let finalData: VSBuffer;
250
try {
251
const resized = await this._imageResizeService.resizeImage(decodeBase64(value).buffer, mimeType);
252
finalData = VSBuffer.wrap(resized);
253
} catch {
254
finalData = decodeBase64(value);
255
}
256
result.content.push({ kind: 'data', value: { mimeType, data: finalData } });
257
}
258
};
259
260
const isForModel = audience.includes('assistant');
261
if (item.type === 'text') {
262
details.output.push({ type: 'embed', isText: true, value: item.text });
263
// structured content 'represents the result of the tool call', so take
264
// that in place of any textual description when present.
265
if (isForModel && !callResult.structuredContent) {
266
result.content.push({
267
kind: 'text',
268
value: item.text
269
});
270
}
271
} else if (item.type === 'image' || item.type === 'audio') {
272
// default to some image type if not given to hint
273
await addAsInlineData(item.mimeType || 'image/png', item.data);
274
} else if (item.type === 'resource_link') {
275
const uri = McpResourceURI.fromServer(this._server.definition, item.uri);
276
details.output.push({
277
type: 'ref',
278
uri,
279
mimeType: item.mimeType,
280
});
281
282
if (isForModel) {
283
if (item.mimeType && getAttachableImageExtension(item.mimeType)) {
284
result.content.push({
285
kind: 'data',
286
value: {
287
mimeType: item.mimeType,
288
data: await this._fileService.readFile(uri).then(f => f.value).catch(() => VSBuffer.alloc(0)),
289
}
290
});
291
} else {
292
result.content.push({
293
kind: 'text',
294
value: `The tool returns a resource which can be read from the URI ${uri}\n`,
295
});
296
}
297
}
298
} else if (item.type === 'resource') {
299
const uri = McpResourceURI.fromServer(this._server.definition, item.resource.uri);
300
if (item.resource.mimeType && getAttachableImageExtension(item.resource.mimeType) && 'blob' in item.resource) {
301
await addAsInlineData(item.resource.mimeType, item.resource.blob, uri);
302
} else {
303
details.output.push({
304
type: 'embed',
305
uri,
306
isText: 'text' in item.resource,
307
mimeType: item.resource.mimeType,
308
value: 'blob' in item.resource ? item.resource.blob : item.resource.text,
309
asResource: true,
310
});
311
312
if (isForModel) {
313
const permalink = invocation.chatRequestId && invocation.context && ChatResponseResource.createUri(invocation.context.sessionId, invocation.chatRequestId, invocation.callId, result.content.length, basename(uri));
314
315
result.content.push({
316
kind: 'text',
317
value: 'text' in item.resource ? item.resource.text : `The tool returns a resource which can be read from the URI ${permalink || uri}\n`,
318
});
319
}
320
}
321
}
322
}
323
324
if (callResult.structuredContent) {
325
details.output.push({ type: 'embed', isText: true, value: JSON.stringify(callResult.structuredContent, null, 2) });
326
result.content.push({ kind: 'text', value: JSON.stringify(callResult.structuredContent) });
327
}
328
329
result.toolResultDetails = details;
330
return result;
331
}
332
333
}
334
335