Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpToolCallUI.ts
4780 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 { Gesture } from '../../../../base/browser/touch.js';6import { decodeBase64 } from '../../../../base/common/buffer.js';7import { CancellationToken } from '../../../../base/common/cancellation.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js';10import { isMobile, isWeb, locale } from '../../../../base/common/platform.js';11import { hasKey } from '../../../../base/common/types.js';12import { ColorScheme } from '../../../../platform/theme/common/theme.js';13import { IThemeService } from '../../../../platform/theme/common/themeService.js';14import { McpServer } from '../common/mcpServer.js';15import { IMcpServer, IMcpService, IMcpToolCallUIData, McpToolVisibility } from '../common/mcpTypes.js';16import { findMcpServer, startServerAndWaitForLiveTools, translateMcpLogMessage } from '../common/mcpTypesUtils.js';17import { MCP } from '../common/modelContextProtocol.js';18import { McpApps } from '../common/modelContextProtocolApps.js';1920/**21* Result from loading an MCP App UI resource.22*/23export interface IMcpAppResourceContent extends McpApps.McpUiResourceMeta {24/** The HTML content of the UI resource */25readonly html: string;26/** MIME type of the content */27readonly mimeType: string;28}2930/**31* Wrapper class that "upgrades" serializable IMcpToolCallUIData into a functional32* object that can load UI resources and proxy tool/resource calls back to the MCP server.33*/34export class McpToolCallUI extends Disposable {35/**36* Basic host context reflecting the current UI and theme. Notably lacks37* the `toolInfo` or `viewport` sizes.38*/39public readonly hostContext: IObservable<McpApps.McpUiHostContext>;4041constructor(42private readonly _uiData: IMcpToolCallUIData,43@IMcpService private readonly _mcpService: IMcpService,44@IThemeService themeService: IThemeService,45) {46super();4748const colorTheme = observableFromEvent(49themeService.onDidColorThemeChange,50() => {51const type = themeService.getColorTheme().type;52return type === ColorScheme.DARK || type === ColorScheme.HIGH_CONTRAST_DARK ? 'dark' : 'light';53}54);5556this.hostContext = derived((reader): McpApps.McpUiHostContext => {57return {58theme: colorTheme.read(reader),59styles: {60variables: {61'--color-background-primary': 'var(--vscode-editor-background)',62'--color-background-secondary': 'var(--vscode-sideBar-background)',63'--color-background-tertiary': 'var(--vscode-activityBar-background)',64'--color-background-inverse': 'var(--vscode-editor-foreground)',65'--color-background-ghost': 'transparent',66'--color-background-info': 'var(--vscode-inputValidation-infoBackground)',67'--color-background-danger': 'var(--vscode-inputValidation-errorBackground)',68'--color-background-success': 'var(--vscode-diffEditor-insertedTextBackground)',69'--color-background-warning': 'var(--vscode-inputValidation-warningBackground)',70'--color-background-disabled': 'var(--vscode-editor-inactiveSelectionBackground)',7172'--color-text-primary': 'var(--vscode-foreground)',73'--color-text-secondary': 'var(--vscode-descriptionForeground)',74'--color-text-tertiary': 'var(--vscode-disabledForeground)',75'--color-text-inverse': 'var(--vscode-editor-background)',76'--color-text-info': 'var(--vscode-textLink-foreground)',77'--color-text-danger': 'var(--vscode-errorForeground)',78'--color-text-success': 'var(--vscode-testing-iconPassed)',79'--color-text-warning': 'var(--vscode-editorWarning-foreground)',80'--color-text-disabled': 'var(--vscode-disabledForeground)',81'--color-text-ghost': 'var(--vscode-descriptionForeground)',8283'--color-border-primary': 'var(--vscode-widget-border)',84'--color-border-secondary': 'var(--vscode-editorWidget-border)',85'--color-border-tertiary': 'var(--vscode-panel-border)',86'--color-border-inverse': 'var(--vscode-foreground)',87'--color-border-ghost': 'transparent',88'--color-border-info': 'var(--vscode-inputValidation-infoBorder)',89'--color-border-danger': 'var(--vscode-inputValidation-errorBorder)',90'--color-border-success': 'var(--vscode-testing-iconPassed)',91'--color-border-warning': 'var(--vscode-inputValidation-warningBorder)',92'--color-border-disabled': 'var(--vscode-disabledForeground)',9394'--color-ring-primary': 'var(--vscode-focusBorder)',95'--color-ring-secondary': 'var(--vscode-focusBorder)',96'--color-ring-inverse': 'var(--vscode-focusBorder)',97'--color-ring-info': 'var(--vscode-inputValidation-infoBorder)',98'--color-ring-danger': 'var(--vscode-inputValidation-errorBorder)',99'--color-ring-success': 'var(--vscode-testing-iconPassed)',100'--color-ring-warning': 'var(--vscode-inputValidation-warningBorder)',101102'--font-sans': 'var(--vscode-font-family)',103'--font-mono': 'var(--vscode-editor-font-family)',104105'--font-weight-normal': 'normal',106'--font-weight-medium': '500',107'--font-weight-semibold': '600',108'--font-weight-bold': 'bold',109110'--font-text-xs-size': '10px',111'--font-text-sm-size': '11px',112'--font-text-md-size': '13px',113'--font-text-lg-size': '14px',114115'--font-heading-xs-size': '16px',116'--font-heading-sm-size': '18px',117'--font-heading-md-size': '20px',118'--font-heading-lg-size': '24px',119'--font-heading-xl-size': '32px',120'--font-heading-2xl-size': '40px',121'--font-heading-3xl-size': '48px',122123'--border-radius-xs': '2px',124'--border-radius-sm': '3px',125'--border-radius-md': '4px',126'--border-radius-lg': '6px',127'--border-radius-xl': '8px',128'--border-radius-full': '9999px',129130'--border-width-regular': '1px',131132'--font-text-xs-line-height': '1.5',133'--font-text-sm-line-height': '1.5',134'--font-text-md-line-height': '1.5',135'--font-text-lg-line-height': '1.5',136137'--font-heading-xs-line-height': '1.25',138'--font-heading-sm-line-height': '1.25',139'--font-heading-md-line-height': '1.25',140'--font-heading-lg-line-height': '1.25',141'--font-heading-xl-line-height': '1.25',142'--font-heading-2xl-line-height': '1.25',143'--font-heading-3xl-line-height': '1.25',144145'--shadow-hairline': '0 0 0 1px var(--vscode-widget-shadow)',146'--shadow-sm': '0 1px 2px 0 var(--vscode-widget-shadow)',147'--shadow-md': '0 4px 6px -1px var(--vscode-widget-shadow)',148'--shadow-lg': '0 10px 15px -3px var(--vscode-widget-shadow)',149}150},151displayMode: 'inline',152availableDisplayModes: ['inline'],153locale: locale,154platform: isWeb ? 'web' : isMobile ? 'mobile' : 'desktop',155deviceCapabilities: {156touch: Gesture.isTouchDevice(),157hover: Gesture.isHoverDevice(),158},159};160});161}162163/**164* Gets the underlying UI data.165*/166public get uiData(): IMcpToolCallUIData {167return this._uiData;168}169170/**171* Logs a message to the MCP server's logger.172*/173public async log(log: MCP.LoggingMessageNotificationParams) {174const server = await this._getServer(CancellationToken.None);175if (server) {176translateMcpLogMessage((server as McpServer).logger, log, `[App UI]`);177}178}179180/**181* Gets or finds the MCP server for this UI.182*/183private async _getServer(token: CancellationToken): Promise<IMcpServer | undefined> {184return findMcpServer(this._mcpService, s =>185s.definition.id === this._uiData.serverDefinitionId &&186s.collection.id === this._uiData.collectionId,187token188);189}190191/**192* Loads the UI resource from the MCP server.193* @param token Cancellation token194* @returns The HTML content and CSP configuration195*/196public async loadResource(token: CancellationToken): Promise<IMcpAppResourceContent> {197const server = await this._getServer(token);198if (!server) {199throw new Error('MCP server not found for UI resource');200}201202const resourceResult = await McpServer.callOn(server, h => h.readResource({ uri: this._uiData.resourceUri }, token), token);203if (!resourceResult.contents || resourceResult.contents.length === 0) {204throw new Error('UI resource not found on server');205}206207const content = resourceResult.contents[0];208let html: string;209const mimeType = content.mimeType || 'text/html';210211if (hasKey(content, { text: true })) {212html = content.text;213} else if (hasKey(content, { blob: true })) {214html = decodeBase64(content.blob).toString();215} else {216throw new Error('UI resource has no content');217}218219const meta = resourceResult._meta?.ui as McpApps.McpUiResourceMeta | undefined;220221return {222...meta,223html,224mimeType,225};226}227228/**229* Calls a tool on the MCP server.230* @param name Tool name231* @param params Tool parameters232* @param token Cancellation token233* @returns The tool call result234*/235public async callTool(name: string, params: Record<string, unknown>, token: CancellationToken): Promise<MCP.CallToolResult> {236const server = await this._getServer(token);237if (!server) {238throw new Error('MCP server not found for tool call');239}240241await startServerAndWaitForLiveTools(server, undefined, token);242243const tool = server.tools.get().find(t => t.definition.name === name);244if (!tool || !(tool.visibility & McpToolVisibility.App)) {245throw new Error(`Tool not found on server: ${name}`);246}247248const res = await tool.call(params, undefined, token);249return {250content: res.content,251isError: res.isError,252_meta: res._meta,253structuredContent: res.structuredContent,254};255}256257/**258* Reads a resource from the MCP server.259* @param uri Resource URI260* @param token Cancellation token261* @returns The resource content262*/263public async readResource(uri: string, token: CancellationToken): Promise<MCP.ReadResourceResult> {264const server = await this._getServer(token);265if (!server) {266throw new Error('MCP server not found');267}268269return await McpServer.callOn(server, h => h.readResource({ uri }, token), token);270}271}272273274