Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpToolCallUI.ts
4780 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 { Gesture } from '../../../../base/browser/touch.js';
7
import { decodeBase64 } from '../../../../base/common/buffer.js';
8
import { CancellationToken } from '../../../../base/common/cancellation.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js';
11
import { isMobile, isWeb, locale } from '../../../../base/common/platform.js';
12
import { hasKey } from '../../../../base/common/types.js';
13
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
14
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
15
import { McpServer } from '../common/mcpServer.js';
16
import { IMcpServer, IMcpService, IMcpToolCallUIData, McpToolVisibility } from '../common/mcpTypes.js';
17
import { findMcpServer, startServerAndWaitForLiveTools, translateMcpLogMessage } from '../common/mcpTypesUtils.js';
18
import { MCP } from '../common/modelContextProtocol.js';
19
import { McpApps } from '../common/modelContextProtocolApps.js';
20
21
/**
22
* Result from loading an MCP App UI resource.
23
*/
24
export interface IMcpAppResourceContent extends McpApps.McpUiResourceMeta {
25
/** The HTML content of the UI resource */
26
readonly html: string;
27
/** MIME type of the content */
28
readonly mimeType: string;
29
}
30
31
/**
32
* Wrapper class that "upgrades" serializable IMcpToolCallUIData into a functional
33
* object that can load UI resources and proxy tool/resource calls back to the MCP server.
34
*/
35
export class McpToolCallUI extends Disposable {
36
/**
37
* Basic host context reflecting the current UI and theme. Notably lacks
38
* the `toolInfo` or `viewport` sizes.
39
*/
40
public readonly hostContext: IObservable<McpApps.McpUiHostContext>;
41
42
constructor(
43
private readonly _uiData: IMcpToolCallUIData,
44
@IMcpService private readonly _mcpService: IMcpService,
45
@IThemeService themeService: IThemeService,
46
) {
47
super();
48
49
const colorTheme = observableFromEvent(
50
themeService.onDidColorThemeChange,
51
() => {
52
const type = themeService.getColorTheme().type;
53
return type === ColorScheme.DARK || type === ColorScheme.HIGH_CONTRAST_DARK ? 'dark' : 'light';
54
}
55
);
56
57
this.hostContext = derived((reader): McpApps.McpUiHostContext => {
58
return {
59
theme: colorTheme.read(reader),
60
styles: {
61
variables: {
62
'--color-background-primary': 'var(--vscode-editor-background)',
63
'--color-background-secondary': 'var(--vscode-sideBar-background)',
64
'--color-background-tertiary': 'var(--vscode-activityBar-background)',
65
'--color-background-inverse': 'var(--vscode-editor-foreground)',
66
'--color-background-ghost': 'transparent',
67
'--color-background-info': 'var(--vscode-inputValidation-infoBackground)',
68
'--color-background-danger': 'var(--vscode-inputValidation-errorBackground)',
69
'--color-background-success': 'var(--vscode-diffEditor-insertedTextBackground)',
70
'--color-background-warning': 'var(--vscode-inputValidation-warningBackground)',
71
'--color-background-disabled': 'var(--vscode-editor-inactiveSelectionBackground)',
72
73
'--color-text-primary': 'var(--vscode-foreground)',
74
'--color-text-secondary': 'var(--vscode-descriptionForeground)',
75
'--color-text-tertiary': 'var(--vscode-disabledForeground)',
76
'--color-text-inverse': 'var(--vscode-editor-background)',
77
'--color-text-info': 'var(--vscode-textLink-foreground)',
78
'--color-text-danger': 'var(--vscode-errorForeground)',
79
'--color-text-success': 'var(--vscode-testing-iconPassed)',
80
'--color-text-warning': 'var(--vscode-editorWarning-foreground)',
81
'--color-text-disabled': 'var(--vscode-disabledForeground)',
82
'--color-text-ghost': 'var(--vscode-descriptionForeground)',
83
84
'--color-border-primary': 'var(--vscode-widget-border)',
85
'--color-border-secondary': 'var(--vscode-editorWidget-border)',
86
'--color-border-tertiary': 'var(--vscode-panel-border)',
87
'--color-border-inverse': 'var(--vscode-foreground)',
88
'--color-border-ghost': 'transparent',
89
'--color-border-info': 'var(--vscode-inputValidation-infoBorder)',
90
'--color-border-danger': 'var(--vscode-inputValidation-errorBorder)',
91
'--color-border-success': 'var(--vscode-testing-iconPassed)',
92
'--color-border-warning': 'var(--vscode-inputValidation-warningBorder)',
93
'--color-border-disabled': 'var(--vscode-disabledForeground)',
94
95
'--color-ring-primary': 'var(--vscode-focusBorder)',
96
'--color-ring-secondary': 'var(--vscode-focusBorder)',
97
'--color-ring-inverse': 'var(--vscode-focusBorder)',
98
'--color-ring-info': 'var(--vscode-inputValidation-infoBorder)',
99
'--color-ring-danger': 'var(--vscode-inputValidation-errorBorder)',
100
'--color-ring-success': 'var(--vscode-testing-iconPassed)',
101
'--color-ring-warning': 'var(--vscode-inputValidation-warningBorder)',
102
103
'--font-sans': 'var(--vscode-font-family)',
104
'--font-mono': 'var(--vscode-editor-font-family)',
105
106
'--font-weight-normal': 'normal',
107
'--font-weight-medium': '500',
108
'--font-weight-semibold': '600',
109
'--font-weight-bold': 'bold',
110
111
'--font-text-xs-size': '10px',
112
'--font-text-sm-size': '11px',
113
'--font-text-md-size': '13px',
114
'--font-text-lg-size': '14px',
115
116
'--font-heading-xs-size': '16px',
117
'--font-heading-sm-size': '18px',
118
'--font-heading-md-size': '20px',
119
'--font-heading-lg-size': '24px',
120
'--font-heading-xl-size': '32px',
121
'--font-heading-2xl-size': '40px',
122
'--font-heading-3xl-size': '48px',
123
124
'--border-radius-xs': '2px',
125
'--border-radius-sm': '3px',
126
'--border-radius-md': '4px',
127
'--border-radius-lg': '6px',
128
'--border-radius-xl': '8px',
129
'--border-radius-full': '9999px',
130
131
'--border-width-regular': '1px',
132
133
'--font-text-xs-line-height': '1.5',
134
'--font-text-sm-line-height': '1.5',
135
'--font-text-md-line-height': '1.5',
136
'--font-text-lg-line-height': '1.5',
137
138
'--font-heading-xs-line-height': '1.25',
139
'--font-heading-sm-line-height': '1.25',
140
'--font-heading-md-line-height': '1.25',
141
'--font-heading-lg-line-height': '1.25',
142
'--font-heading-xl-line-height': '1.25',
143
'--font-heading-2xl-line-height': '1.25',
144
'--font-heading-3xl-line-height': '1.25',
145
146
'--shadow-hairline': '0 0 0 1px var(--vscode-widget-shadow)',
147
'--shadow-sm': '0 1px 2px 0 var(--vscode-widget-shadow)',
148
'--shadow-md': '0 4px 6px -1px var(--vscode-widget-shadow)',
149
'--shadow-lg': '0 10px 15px -3px var(--vscode-widget-shadow)',
150
}
151
},
152
displayMode: 'inline',
153
availableDisplayModes: ['inline'],
154
locale: locale,
155
platform: isWeb ? 'web' : isMobile ? 'mobile' : 'desktop',
156
deviceCapabilities: {
157
touch: Gesture.isTouchDevice(),
158
hover: Gesture.isHoverDevice(),
159
},
160
};
161
});
162
}
163
164
/**
165
* Gets the underlying UI data.
166
*/
167
public get uiData(): IMcpToolCallUIData {
168
return this._uiData;
169
}
170
171
/**
172
* Logs a message to the MCP server's logger.
173
*/
174
public async log(log: MCP.LoggingMessageNotificationParams) {
175
const server = await this._getServer(CancellationToken.None);
176
if (server) {
177
translateMcpLogMessage((server as McpServer).logger, log, `[App UI]`);
178
}
179
}
180
181
/**
182
* Gets or finds the MCP server for this UI.
183
*/
184
private async _getServer(token: CancellationToken): Promise<IMcpServer | undefined> {
185
return findMcpServer(this._mcpService, s =>
186
s.definition.id === this._uiData.serverDefinitionId &&
187
s.collection.id === this._uiData.collectionId,
188
token
189
);
190
}
191
192
/**
193
* Loads the UI resource from the MCP server.
194
* @param token Cancellation token
195
* @returns The HTML content and CSP configuration
196
*/
197
public async loadResource(token: CancellationToken): Promise<IMcpAppResourceContent> {
198
const server = await this._getServer(token);
199
if (!server) {
200
throw new Error('MCP server not found for UI resource');
201
}
202
203
const resourceResult = await McpServer.callOn(server, h => h.readResource({ uri: this._uiData.resourceUri }, token), token);
204
if (!resourceResult.contents || resourceResult.contents.length === 0) {
205
throw new Error('UI resource not found on server');
206
}
207
208
const content = resourceResult.contents[0];
209
let html: string;
210
const mimeType = content.mimeType || 'text/html';
211
212
if (hasKey(content, { text: true })) {
213
html = content.text;
214
} else if (hasKey(content, { blob: true })) {
215
html = decodeBase64(content.blob).toString();
216
} else {
217
throw new Error('UI resource has no content');
218
}
219
220
const meta = resourceResult._meta?.ui as McpApps.McpUiResourceMeta | undefined;
221
222
return {
223
...meta,
224
html,
225
mimeType,
226
};
227
}
228
229
/**
230
* Calls a tool on the MCP server.
231
* @param name Tool name
232
* @param params Tool parameters
233
* @param token Cancellation token
234
* @returns The tool call result
235
*/
236
public async callTool(name: string, params: Record<string, unknown>, token: CancellationToken): Promise<MCP.CallToolResult> {
237
const server = await this._getServer(token);
238
if (!server) {
239
throw new Error('MCP server not found for tool call');
240
}
241
242
await startServerAndWaitForLiveTools(server, undefined, token);
243
244
const tool = server.tools.get().find(t => t.definition.name === name);
245
if (!tool || !(tool.visibility & McpToolVisibility.App)) {
246
throw new Error(`Tool not found on server: ${name}`);
247
}
248
249
const res = await tool.call(params, undefined, token);
250
return {
251
content: res.content,
252
isError: res.isError,
253
_meta: res._meta,
254
structuredContent: res.structuredContent,
255
};
256
}
257
258
/**
259
* Reads a resource from the MCP server.
260
* @param uri Resource URI
261
* @param token Cancellation token
262
* @returns The resource content
263
*/
264
public async readResource(uri: string, token: CancellationToken): Promise<MCP.ReadResourceResult> {
265
const server = await this._getServer(token);
266
if (!server) {
267
throw new Error('MCP server not found');
268
}
269
270
return await McpServer.callOn(server, h => h.readResource({ uri }, token), token);
271
}
272
}
273
274