mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
chat: support MCP apps (#285864)
* wip * wip * wip * works * add a config option * copilot comments * simplify and polish
This commit is contained in:
@@ -122,6 +122,9 @@ export class Gesture extends Disposable {
|
||||
return toDisposable(remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the device is able to represent touch events.
|
||||
*/
|
||||
@memoize
|
||||
static isTouchDevice(): boolean {
|
||||
// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
|
||||
@@ -129,6 +132,14 @@ export class Gesture extends Disposable {
|
||||
return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the device's primary input is able to hover.
|
||||
*/
|
||||
@memoize
|
||||
static isHoverDevice(): boolean {
|
||||
return mainWindow.matchMedia('(hover: hover)').matches;
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
|
||||
@@ -249,6 +249,7 @@ export const mcpAccessConfig = 'chat.mcp.access';
|
||||
export const mcpGalleryServiceUrlConfig = 'chat.mcp.gallery.serviceUrl';
|
||||
export const mcpGalleryServiceEnablementConfig = 'chat.mcp.gallery.enabled';
|
||||
export const mcpAutoStartConfig = 'chat.mcp.autostart';
|
||||
export const mcpAppsEnabledConfig = 'chat.mcp.apps.enabled';
|
||||
|
||||
export interface IMcpGalleryConfig {
|
||||
readonly serviceUrl?: string;
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurati
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { McpAccessValue, McpAutoStartValue, mcpAccessConfig, mcpAutoStartConfig, mcpGalleryServiceEnablementConfig, mcpGalleryServiceUrlConfig } from '../../../../platform/mcp/common/mcpManagement.js';
|
||||
import { McpAccessValue, McpAutoStartValue, mcpAccessConfig, mcpAutoStartConfig, mcpGalleryServiceEnablementConfig, mcpGalleryServiceUrlConfig, mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagement.js';
|
||||
import product from '../../../../platform/product/common/product.js';
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js';
|
||||
@@ -465,6 +465,12 @@ configurationRegistry.registerConfiguration({
|
||||
],
|
||||
tags: ['experimental'],
|
||||
},
|
||||
[mcpAppsEnabledConfig]: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('chat.mcp.ui.enabled', "Controls whether MCP servers can provide custom UI for tool invocations."),
|
||||
default: false,
|
||||
tags: ['experimental'],
|
||||
},
|
||||
[mcpServerSamplingSection]: {
|
||||
type: 'object',
|
||||
description: nls.localize('chat.mcp.serverSampling', "Configures which models are exposed to MCP servers for sampling (making model requests in the background). This setting can be edited in a graphical way under the `{0}` command.", 'MCP: ' + nls.localize('mcp.list', 'List Servers')),
|
||||
|
||||
@@ -29,6 +29,12 @@ export interface IChatContentPart extends IDisposable {
|
||||
*/
|
||||
hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean;
|
||||
|
||||
/**
|
||||
* Called when the content part is mounted to the DOM after being detached
|
||||
* due to virtualization.
|
||||
*/
|
||||
onDidRemount?(): void;
|
||||
|
||||
addDisposable?(disposable: IDisposable): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS
|
||||
this.getAutoApproveMessageContent(),
|
||||
context,
|
||||
toCodePart(input),
|
||||
processedOutput && {
|
||||
processedOutput && processedOutput.length > 0 ? {
|
||||
parts: processedOutput.map((o, i): ChatCollapsibleIOPart => {
|
||||
const permalinkBasename = o.type === 'ref' || o.uri
|
||||
? basename(o.uri!)
|
||||
@@ -124,7 +124,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS
|
||||
return { kind: 'data', value: decoded || new TextEncoder().encode(o.value), mimeType: o.mimeType, uri: permalinkUri, audience: o.audience };
|
||||
}
|
||||
}),
|
||||
},
|
||||
} : undefined,
|
||||
isError,
|
||||
ChatInputOutputMarkdownProgressPart._expandedByDefault.get(toolInvocation) ?? false,
|
||||
));
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from '../../../../../../../base/browser/dom.js';
|
||||
import { softAssertNever } from '../../../../../../../base/common/assert.js';
|
||||
import { disposableTimeout } from '../../../../../../../base/common/async.js';
|
||||
import { decodeBase64 } from '../../../../../../../base/common/buffer.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../../../../../base/common/event.js';
|
||||
import { Disposable, toDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { autorun, autorunSelfDisposable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js';
|
||||
import { basename } from '../../../../../../../base/common/resources.js';
|
||||
import { isFalsyOrWhitespace } from '../../../../../../../base/common/strings.js';
|
||||
import { hasKey, isDefined } from '../../../../../../../base/common/types.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILogService } from '../../../../../../../platform/log/common/log.js';
|
||||
import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js';
|
||||
import { IProductService } from '../../../../../../../platform/product/common/productService.js';
|
||||
import { IStorageService } from '../../../../../../../platform/storage/common/storage.js';
|
||||
import { IMcpAppResourceContent, McpToolCallUI } from '../../../../../mcp/browser/mcpToolCallUI.js';
|
||||
import { McpResourceURI } from '../../../../../mcp/common/mcpTypes.js';
|
||||
import { MCP } from '../../../../../mcp/common/modelContextProtocol.js';
|
||||
import { McpApps } from '../../../../../mcp/common/modelContextProtocolApps.js';
|
||||
import { IWebviewElement, IWebviewService, WebviewContentPurpose, WebviewOriginStore } from '../../../../../webview/browser/webview.js';
|
||||
import { IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js';
|
||||
import { isToolResultInputOutputDetails, IToolResult } from '../../../../common/tools/languageModelToolsService.js';
|
||||
import { IChatWidgetService } from '../../../chat.js';
|
||||
import { IMcpAppRenderData } from './chatMcpAppSubPart.js';
|
||||
|
||||
/** Storage key for persistent webview origins */
|
||||
const ORIGIN_STORE_KEY = 'chatMcpApp.origins';
|
||||
|
||||
/**
|
||||
* Load state for the MCP App model.
|
||||
*/
|
||||
export type McpAppLoadState =
|
||||
| { readonly status: 'loading' }
|
||||
| { readonly status: 'loaded' }
|
||||
| { readonly status: 'error'; readonly error: Error };
|
||||
|
||||
/**
|
||||
* Model that owns an MCP App webview and all its state/logic.
|
||||
* The webview is created lazily on first claim and survives across re-renders.
|
||||
*/
|
||||
export class ChatMcpAppModel extends Disposable {
|
||||
public static maxWebviewHeightPct = 0.8;
|
||||
|
||||
/** Origin store for persistent webview origins per server */
|
||||
private readonly _originStore: WebviewOriginStore;
|
||||
|
||||
/** The webview element instance */
|
||||
private readonly _webview: IWebviewElement;
|
||||
|
||||
/** Tool call UI for loading resources and proxying calls */
|
||||
private readonly _mcpToolCallUI: McpToolCallUI;
|
||||
|
||||
/** Cancellation source for async operations */
|
||||
private readonly _disposeCts = this._register(new CancellationTokenSource());
|
||||
|
||||
/** Whether ui/initialize has been called and capabilities announced */
|
||||
private _announcedCapabilities = false;
|
||||
|
||||
/** Current height of the webview */
|
||||
private _height: number = 300;
|
||||
|
||||
/** The persistent webview origin */
|
||||
private readonly _webviewOrigin: string;
|
||||
|
||||
/** Observable for load state */
|
||||
private readonly _loadState = observableValue<McpAppLoadState>(this, { status: 'loading' });
|
||||
public readonly loadState: IObservable<McpAppLoadState> = this._loadState;
|
||||
|
||||
/** Event fired when height changes */
|
||||
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeHeight: Event<void> = this._onDidChangeHeight.event;
|
||||
|
||||
/** Host context observable combining tool call UI context with viewport */
|
||||
private readonly _viewportObs = observableValue<Required<McpApps.McpUiHostContext['viewport']>>(this, undefined);
|
||||
|
||||
/** Full host context for the MCP App */
|
||||
public readonly hostContext: IObservable<McpApps.McpUiHostContext>;
|
||||
|
||||
constructor(
|
||||
public readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized,
|
||||
public readonly renderData: IMcpAppRenderData,
|
||||
private readonly _container: HTMLElement,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
|
||||
@IWebviewService private readonly _webviewService: IWebviewService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._originStore = new WebviewOriginStore(ORIGIN_STORE_KEY, storageService);
|
||||
this._webviewOrigin = this._originStore.getOrigin('mcpApp', renderData.serverDefinitionId);
|
||||
this._mcpToolCallUI = this._register(this._instantiationService.createInstance(McpToolCallUI, renderData));
|
||||
|
||||
// Create the webview element
|
||||
this._webview = this._register(this._webviewService.createWebviewElement({
|
||||
origin: this._webviewOrigin,
|
||||
title: localize('mcpAppTitle', 'MCP App'),
|
||||
options: {
|
||||
purpose: WebviewContentPurpose.ChatOutputItem,
|
||||
enableFindWidget: false,
|
||||
disableServiceWorker: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
contentOptions: {
|
||||
allowMultipleAPIAcquire: true,
|
||||
allowScripts: true,
|
||||
allowForms: true,
|
||||
},
|
||||
extension: undefined,
|
||||
}));
|
||||
|
||||
// Mount the webview to the container
|
||||
const targetWindow = dom.getWindow(this._container);
|
||||
this._webview.mountTo(this._container, targetWindow);
|
||||
|
||||
// Set up resize observer for viewport and size notifications
|
||||
const updateViewport = () => {
|
||||
this._viewportObs.set({
|
||||
width: targetWindow.innerWidth,
|
||||
height: targetWindow.innerHeight,
|
||||
maxWidth: targetWindow.innerWidth,
|
||||
maxHeight: targetWindow.innerHeight * ChatMcpAppModel.maxWebviewHeightPct,
|
||||
}, undefined);
|
||||
|
||||
if (this._announcedCapabilities) {
|
||||
this._sendNotification({
|
||||
method: 'ui/notifications/size-changed',
|
||||
params: { width: this._container.clientWidth, height: this._container.clientHeight },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateViewport);
|
||||
resizeObserver.observe(this._container);
|
||||
this._register(toDisposable(() => resizeObserver.disconnect()));
|
||||
updateViewport();
|
||||
|
||||
// Build host context observable
|
||||
this.hostContext = this._mcpToolCallUI.hostContext.map((context, reader) => ({
|
||||
...context,
|
||||
viewport: this._viewportObs.read(reader),
|
||||
toolCall: {
|
||||
toolCallId: this.toolInvocation.toolCallId,
|
||||
toolName: this.toolInvocation.toolId,
|
||||
},
|
||||
}));
|
||||
|
||||
// Set up host context change notifications
|
||||
this._register(autorun(reader => {
|
||||
const context = this.hostContext.read(reader);
|
||||
if (this._announcedCapabilities) {
|
||||
this._sendNotification({
|
||||
method: 'ui/notifications/host-context-changed',
|
||||
params: context
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Set up message handling
|
||||
this._register(this._webview.onMessage(async ({ message }) => {
|
||||
await this._handleWebviewMessage(message as McpApps.AppMessage);
|
||||
}));
|
||||
|
||||
const canScrollWithin = derived(reader => {
|
||||
const contentSize = this._webview.intrinsicContentSize.read(reader);
|
||||
const viewportSize = this._viewportObs.read(reader);
|
||||
if (!contentSize || !viewportSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return contentSize.height > viewportSize.maxHeight;
|
||||
});
|
||||
|
||||
// Handle wheel events for scroll delegation when the webview can scroll
|
||||
this._register(autorun(reader => {
|
||||
if (!canScrollWithin.read(reader)) {
|
||||
const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource);
|
||||
reader.store.add(this._webview.onDidWheel(e => {
|
||||
widget?.delegateScrollFromMouseWheelEvent({
|
||||
...e,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
// Start loading the content
|
||||
this._loadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current height of the webview.
|
||||
*/
|
||||
public get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public remount() {
|
||||
this._webview.reinitializeAfterDismount();
|
||||
this._announcedCapabilities = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries loading the MCP App content.
|
||||
*/
|
||||
public retry(): void {
|
||||
this._loadState.set({ status: 'loading' }, undefined);
|
||||
this._loadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the MCP App content into the webview.
|
||||
*/
|
||||
private async _loadContent(): Promise<void> {
|
||||
const token = this._disposeCts.token;
|
||||
|
||||
try {
|
||||
// Load the UI resource from the MCP server
|
||||
const resourceContent = await this._mcpToolCallUI.loadResource(token);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject CSP into the HTML
|
||||
const htmlWithCsp = this._injectPreamble(resourceContent);
|
||||
|
||||
// Reset the state
|
||||
this._announcedCapabilities = false;
|
||||
|
||||
// Set the HTML content
|
||||
this._webview.setHtml(htmlWithCsp);
|
||||
|
||||
this._loadState.set({ status: 'loaded' }, undefined);
|
||||
} catch (error) {
|
||||
this._logService.error('[MCP App] Error loading app:', error);
|
||||
this._loadState.set({ status: 'error', error: error as Error }, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a Content-Security-Policy meta tag into the HTML.
|
||||
*/
|
||||
private _injectPreamble({ html, csp }: IMcpAppResourceContent): string {
|
||||
// Note: this is not bulletproof against malformed domains. However it does not
|
||||
// need to be. The server is the one giving us both the CSP as well as the HTML
|
||||
// to render in the iframe. MCP Apps give the CSP separately so that systems that
|
||||
// proxy the HTML from a server can set it in a header, but the CSP and the HTML
|
||||
// come from the same source and are within the same trust boundary. We only
|
||||
// process the CSP enough (escaping HTML special characters) to avoid breaking it.
|
||||
//
|
||||
// It would certainly be more durable to use `DOMParser.parseFromString` here
|
||||
// and operate on the DocumentFragment of the HTML, however (even though keeping
|
||||
// it solely as a detached document is safe) this requires making the HTML trusted
|
||||
// in the renderer and bypassing various tsec warnings. I consider the string
|
||||
// munging here to be the lesser of two evils.
|
||||
const cleanDomains = (s: string[] | undefined) => (s?.join(' ') || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
|
||||
const cspContent = `
|
||||
default-src 'none';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
connect-src 'self' ${cleanDomains(csp?.connectDomains)};
|
||||
img-src 'self' data: ${cleanDomains(csp?.resourceDomains)};
|
||||
font-src 'self' ${cleanDomains(csp?.resourceDomains)};
|
||||
media-src 'self' data: ${cleanDomains(csp?.resourceDomains)};
|
||||
frame-src 'none';
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
`;
|
||||
|
||||
const cspTag = `<meta http-equiv="Content-Security-Policy" content="${cspContent}">`;
|
||||
|
||||
// window.top and window.parent get reset to `window` after the vscode API is made.
|
||||
// However, the MCP App SDK by default tries to use these for postMessage. So, wrap them.
|
||||
// https://github.com/microsoft/vscode/blob/2a4c8f5b8a715d45dd2a36778906b5810e4a1905/src/vs/workbench/contrib/webview/browser/pre/index.html#L242-L244
|
||||
const postMessageRehoist = `
|
||||
<script>(() => {
|
||||
const api = acquireVsCodeApi();
|
||||
const wrap = target => new Proxy(target, {
|
||||
get: (obj, prop) => {
|
||||
if (prop === 'postMessage') {
|
||||
return (message, transfer) => api.postMessage(message, transfer);
|
||||
}
|
||||
return obj[prop];
|
||||
},
|
||||
});
|
||||
window.parent = wrap(window.parent);
|
||||
window.top = wrap(window.top);
|
||||
})();</script>
|
||||
`;
|
||||
|
||||
return this._prependToHead(html, cspTag + postMessageRehoist);
|
||||
}
|
||||
|
||||
private _prependToHead(html: string, content: string): string {
|
||||
// Try to inject into <head>
|
||||
const headMatch = html.match(/<head[^>]*>/i);
|
||||
if (headMatch) {
|
||||
const insertIndex = headMatch.index! + headMatch[0].length;
|
||||
return html.slice(0, insertIndex) + '\n' + content + html.slice(insertIndex);
|
||||
}
|
||||
|
||||
// If no <head>, try to inject after <html>
|
||||
const htmlMatch = html.match(/<html[^>]*>/i);
|
||||
if (htmlMatch) {
|
||||
const insertIndex = htmlMatch.index! + htmlMatch[0].length;
|
||||
return html.slice(0, insertIndex) + '\n<head>' + content + '</head>' + html.slice(insertIndex);
|
||||
}
|
||||
|
||||
// If no <html>, prepend
|
||||
return `<!DOCTYPE html><html><head>${content}</head><body>${html}</body></html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming JSON-RPC messages from the webview.
|
||||
*/
|
||||
private async _handleWebviewMessage(message: McpApps.AppMessage): Promise<void> {
|
||||
const request = message;
|
||||
const token = this._disposeCts.token;
|
||||
|
||||
try {
|
||||
let result: McpApps.HostResult = {};
|
||||
|
||||
switch (request.method) {
|
||||
case 'ui/initialize':
|
||||
result = await this._handleInitialize(request.params);
|
||||
break;
|
||||
|
||||
case 'tools/call':
|
||||
result = await this._handleToolsCall(request.params, token);
|
||||
break;
|
||||
|
||||
case 'resources/read':
|
||||
result = await this._handleResourcesRead(request.params, token);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
break;
|
||||
|
||||
case 'ui/notifications/size-changed':
|
||||
this._handleSizeChanged(request.params);
|
||||
break;
|
||||
|
||||
case 'ui/open-link':
|
||||
result = await this._handleOpenLink(request.params);
|
||||
break;
|
||||
|
||||
case 'ui/request-display-mode':
|
||||
break; // not supported
|
||||
|
||||
case 'ui/notifications/initialized':
|
||||
break;
|
||||
|
||||
case 'ui/message':
|
||||
result = await this._handleUiMessage(request.params);
|
||||
break;
|
||||
|
||||
case 'notifications/message':
|
||||
await this._mcpToolCallUI.log(request.params);
|
||||
break;
|
||||
|
||||
default: {
|
||||
softAssertNever(request);
|
||||
const cast = request as MCP.JSONRPCRequest;
|
||||
if (cast.id !== undefined) {
|
||||
await this._sendError(cast.id, -32601, `Method not found: ${cast.method}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send response if this was a request (has id)
|
||||
if (hasKey(request, { id: true })) {
|
||||
await this._sendResponse(request.id, result);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this._logService.error(`[MCP App] Error handling ${request.method}:`, error);
|
||||
if (hasKey(request, { id: true })) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await this._sendError(request.id, -32000, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ui/initialize request from the MCP App.
|
||||
*/
|
||||
private async _handleInitialize(_params: McpApps.McpUiInitializeRequest['params']): Promise<McpApps.McpUiInitializeResult> {
|
||||
this._announcedCapabilities = true;
|
||||
|
||||
// "Host MUST send this notification with the complete tool arguments after the Guest UI's initialize request completes"
|
||||
// Cast to `any` due to https://github.com/modelcontextprotocol/ext-apps/issues/197
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let args: any;
|
||||
try {
|
||||
args = JSON.parse(this.renderData.input);
|
||||
} catch {
|
||||
args = this.renderData.input;
|
||||
}
|
||||
|
||||
const timeout = this._register(disposableTimeout(async () => {
|
||||
this._store.delete(timeout);
|
||||
await this._sendNotification({
|
||||
method: 'ui/notifications/tool-input',
|
||||
params: { arguments: args }
|
||||
});
|
||||
|
||||
if (this.toolInvocation.kind === 'toolInvocationSerialized') {
|
||||
this._sendToolResult(this.toolInvocation.resultDetails);
|
||||
} else if (this.toolInvocation.kind === 'toolInvocation') {
|
||||
const invocation = this.toolInvocation;
|
||||
this._register(autorunSelfDisposable(reader => {
|
||||
const state = invocation.state.read(reader);
|
||||
if (state.type === IChatToolInvocation.StateKind.Completed) {
|
||||
this._sendToolResult(state.resultDetails);
|
||||
reader.dispose();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
protocolVersion: McpApps.LATEST_PROTOCOL_VERSION,
|
||||
hostInfo: {
|
||||
name: this._productService.nameLong,
|
||||
version: this._productService.version,
|
||||
},
|
||||
hostCapabilities: {
|
||||
openLinks: {},
|
||||
serverTools: { listChanged: true },
|
||||
serverResources: { listChanged: true },
|
||||
logging: {},
|
||||
},
|
||||
hostContext: this.hostContext.get(),
|
||||
} satisfies Required<McpApps.McpUiInitializeResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the tool result notification when the result becomes available.
|
||||
*/
|
||||
private _sendToolResult(resultDetails: IToolResult['toolResultDetails'] | IChatToolInvocationSerialized['resultDetails']): void {
|
||||
if (isToolResultInputOutputDetails(resultDetails) && resultDetails.mcpOutput) {
|
||||
this._sendNotification({
|
||||
method: 'ui/notifications/tool-result',
|
||||
params: resultDetails.mcpOutput as MCP.CallToolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleUiMessage(params: McpApps.McpUiMessageRequest['params']): Promise<McpApps.McpUiMessageResult> {
|
||||
const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource);
|
||||
if (!widget) {
|
||||
return { isError: true };
|
||||
}
|
||||
|
||||
if (!isFalsyOrWhitespace(widget.getInput())) {
|
||||
return { isError: true };
|
||||
}
|
||||
|
||||
widget.setInput(params.content.filter(c => c.type === 'text').map(c => c.text).join('\n\n'));
|
||||
widget.attachmentModel.clearAndSetContext(...params.content.map((c, i): IChatRequestVariableEntry | undefined => {
|
||||
const id = `mcpui-${i}-${Date.now()}`;
|
||||
if (c.type === 'image') {
|
||||
return { kind: 'image', value: decodeBase64(c.data).buffer, id, name: 'Image' };
|
||||
} else if (c.type === 'resource_link') {
|
||||
const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, c.uri);
|
||||
return { kind: 'file', value: uri, id, name: basename(uri) };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}).filter(isDefined));
|
||||
widget.focusInput();
|
||||
|
||||
return { isError: false };
|
||||
}
|
||||
|
||||
private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void {
|
||||
if (params.height !== undefined) {
|
||||
this._height = params.height;
|
||||
this._onDidChangeHeight.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleOpenLink(params: McpApps.McpUiOpenLinkRequest['params']): Promise<McpApps.McpUiOpenLinkResult> {
|
||||
const ok = await this._openerService.open(params.url);
|
||||
return { isError: !ok };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles tools/call requests from the MCP App.
|
||||
*/
|
||||
private async _handleToolsCall(params: MCP.CallToolRequestParams, token: CancellationToken): Promise<MCP.CallToolResult> {
|
||||
if (!params?.name) {
|
||||
throw new Error('Missing tool name in tools/call request');
|
||||
}
|
||||
|
||||
return this._mcpToolCallUI.callTool(params.name, params.arguments || {}, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles resources/read requests from the MCP App.
|
||||
*/
|
||||
private async _handleResourcesRead(params: MCP.ReadResourceRequestParams, token: CancellationToken): Promise<MCP.ReadResourceResult> {
|
||||
if (!params?.uri) {
|
||||
throw new Error('Missing uri in resources/read request');
|
||||
}
|
||||
|
||||
return this._mcpToolCallUI.readResource(params.uri, token);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async _sendResponse(id: number | string, result: any): Promise<void> {
|
||||
await this._webview.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result,
|
||||
} satisfies MCP.JSONRPCResponse);
|
||||
}
|
||||
|
||||
private async _sendError(id: number | string, code: number, message: string): Promise<void> {
|
||||
await this._webview.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code, message },
|
||||
} satisfies MCP.JSONRPCError);
|
||||
}
|
||||
|
||||
private async _sendNotification(message: McpApps.HostNotification): Promise<void> {
|
||||
await this._webview.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
...message,
|
||||
});
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._disposeCts.dispose(true);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from '../../../../../../../base/browser/dom.js';
|
||||
import { Button } from '../../../../../../../base/browser/ui/button/button.js';
|
||||
import { Codicon } from '../../../../../../../base/common/codicons.js';
|
||||
import { Event } from '../../../../../../../base/common/event.js';
|
||||
import { MarkdownString } from '../../../../../../../base/common/htmlContent.js';
|
||||
import { MutableDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../../../../base/common/observable.js';
|
||||
import { ThemeIcon } from '../../../../../../../base/common/themables.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMarkdownRendererService } from '../../../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { defaultButtonStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { ChatErrorLevel, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js';
|
||||
import { IChatCodeBlockInfo } from '../../../chat.js';
|
||||
import { ChatErrorWidget } from '../chatErrorContentPart.js';
|
||||
import { ChatProgressSubPart } from '../chatProgressContentPart.js';
|
||||
import { ChatMcpAppModel, McpAppLoadState } from './chatMcpAppModel.js';
|
||||
import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js';
|
||||
|
||||
/**
|
||||
* Data needed to render an MCP App, available before tool completion.
|
||||
*/
|
||||
export interface IMcpAppRenderData {
|
||||
/** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */
|
||||
readonly resourceUri: string;
|
||||
/** Reference to the server definition for reconnection */
|
||||
readonly serverDefinitionId: string;
|
||||
/** Reference to the collection containing the server */
|
||||
readonly collectionId: string;
|
||||
/** The tool input arguments as a JSON string */
|
||||
readonly input: string;
|
||||
/** The session resource URI for the chat session */
|
||||
readonly sessionResource: URI;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sub-part for rendering MCP App webviews in chat tool output.
|
||||
* This is a thin view layer that delegates to ChatMcpAppModel.
|
||||
*/
|
||||
export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart {
|
||||
|
||||
public readonly domNode: HTMLElement;
|
||||
public override readonly codeblocks: IChatCodeBlockInfo[] = [];
|
||||
|
||||
/** The model that owns the webview */
|
||||
private readonly _model: ChatMcpAppModel;
|
||||
|
||||
/** The webview container */
|
||||
private readonly _webviewContainer: HTMLElement;
|
||||
|
||||
/** Current progress part for loading state */
|
||||
private readonly _progressPart = this._register(new MutableDisposable<ChatProgressSubPart>());
|
||||
|
||||
/** Current error node */
|
||||
private _errorNode: HTMLElement | undefined;
|
||||
|
||||
constructor(
|
||||
toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized,
|
||||
onDidRemount: Event<void>,
|
||||
private readonly _renderData: IMcpAppRenderData,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
|
||||
) {
|
||||
super(toolInvocation);
|
||||
|
||||
// Create the DOM structure
|
||||
this.domNode = dom.$('div.mcp-app-part');
|
||||
this._webviewContainer = dom.$('div.mcp-app-webview');
|
||||
this._webviewContainer.style.maxHeight = `${ChatMcpAppModel.maxWebviewHeightPct * 100}vh`;
|
||||
this._webviewContainer.style.minHeight = '100px';
|
||||
this._webviewContainer.style.height = '300px'; // Initial height, will be updated by model
|
||||
this.domNode.appendChild(this._webviewContainer);
|
||||
|
||||
// Create the model - it will mount the webview to the container
|
||||
this._model = this._register(this._instantiationService.createInstance(
|
||||
ChatMcpAppModel,
|
||||
toolInvocation,
|
||||
this._renderData,
|
||||
this._webviewContainer
|
||||
));
|
||||
|
||||
// Update container height from model
|
||||
this._updateContainerHeight();
|
||||
|
||||
// Set up load state handling
|
||||
this._register(autorun(reader => {
|
||||
const loadState = this._model.loadState.read(reader);
|
||||
this._handleLoadStateChange(this._webviewContainer, loadState);
|
||||
}));
|
||||
|
||||
// Subscribe to model height changes
|
||||
this._register(this._model.onDidChangeHeight(() => {
|
||||
this._updateContainerHeight();
|
||||
this._onDidChangeHeight.fire();
|
||||
}));
|
||||
|
||||
this._register(onDidRemount(() => {
|
||||
this._model.remount();
|
||||
}));
|
||||
}
|
||||
|
||||
private _handleLoadStateChange(container: HTMLElement, loadState: McpAppLoadState): void {
|
||||
// Remove any existing loading/error indicators
|
||||
if (this._progressPart.value) {
|
||||
this._progressPart.value.domNode.remove();
|
||||
}
|
||||
this._progressPart.clear();
|
||||
if (this._errorNode) {
|
||||
this._errorNode.remove();
|
||||
this._errorNode = undefined;
|
||||
}
|
||||
|
||||
switch (loadState.status) {
|
||||
case 'loading': {
|
||||
// Hide the webview container while loading
|
||||
container.style.display = 'none';
|
||||
|
||||
const progressMessage = dom.$('span');
|
||||
progressMessage.textContent = localize('loadingMcpApp', 'Loading MCP App...');
|
||||
const progressPart = this._instantiationService.createInstance(
|
||||
ChatProgressSubPart,
|
||||
progressMessage,
|
||||
ThemeIcon.modify(Codicon.loading, 'spin'),
|
||||
undefined
|
||||
);
|
||||
this._progressPart.value = progressPart;
|
||||
// Append to domNode (parent), not the webview container
|
||||
this.domNode.appendChild(progressPart.domNode);
|
||||
break;
|
||||
}
|
||||
case 'loaded': {
|
||||
// Show the webview container
|
||||
container.style.display = '';
|
||||
this._onDidChangeHeight.fire();
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
// Hide the webview container on error
|
||||
container.style.display = 'none';
|
||||
this._showError(this.domNode, loadState.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateContainerHeight(): void {
|
||||
this._webviewContainer.style.height = `${this._model.height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error message in the container.
|
||||
*/
|
||||
private _showError(container: HTMLElement, error: Error): void {
|
||||
const errorNode = dom.$('.mcp-app-error');
|
||||
|
||||
// Create error message with markdown
|
||||
const errorMessage = new MarkdownString();
|
||||
errorMessage.appendText(localize('mcpAppError', 'Error loading MCP App: {0}', error.message || String(error)));
|
||||
|
||||
// Use ChatErrorWidget for consistent error styling
|
||||
const errorWidget = new ChatErrorWidget(ChatErrorLevel.Error, errorMessage, this._markdownRendererService);
|
||||
errorNode.appendChild(errorWidget.domNode);
|
||||
|
||||
// Add retry button
|
||||
const buttonContainer = dom.append(errorNode, dom.$('.chat-buttons-container'));
|
||||
const retryButton = new Button(buttonContainer, defaultButtonStyles);
|
||||
retryButton.label = localize('retry', 'Retry');
|
||||
retryButton.onDidClick(() => {
|
||||
this._model.retry();
|
||||
});
|
||||
|
||||
container.appendChild(errorNode);
|
||||
this._errorNode = errorNode;
|
||||
this._onDidChangeHeight.fire();
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
import * as dom from '../../../../../../../base/browser/dom.js';
|
||||
import { Emitter } from '../../../../../../../base/common/event.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../../../../base/common/observable.js';
|
||||
import { autorun, derived } from '../../../../../../../base/common/observable.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js';
|
||||
import { IChatRendererContent } from '../../../../common/model/chatViewModel.js';
|
||||
import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js';
|
||||
import { isToolResultInputOutputDetails, isToolResultOutputDetails, ToolInvocationPresentation } from '../../../../common/tools/languageModelToolsService.js';
|
||||
@@ -19,6 +19,7 @@ import { IChatContentPart, IChatContentPartRenderContext } from '../chatContentP
|
||||
import { CollapsibleListPool } from '../chatReferencesContentPart.js';
|
||||
import { ExtensionsInstallConfirmationWidgetSubPart } from './chatExtensionsInstallToolSubPart.js';
|
||||
import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownProgressPart.js';
|
||||
import { ChatMcpAppSubPart, IMcpAppRenderData } from './chatMcpAppSubPart.js';
|
||||
import { ChatResultListSubPart } from './chatResultListSubPart.js';
|
||||
import { ChatTerminalToolConfirmationSubPart } from './chatTerminalToolConfirmationSubPart.js';
|
||||
import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js';
|
||||
@@ -35,7 +36,11 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
|
||||
|
||||
public get codeblocks(): IChatCodeBlockInfo[] {
|
||||
return this.subPart?.codeblocks ?? [];
|
||||
const codeblocks = this.subPart?.codeblocks ?? [];
|
||||
if (this.mcpAppPart) {
|
||||
codeblocks.push(...this.mcpAppPart.codeblocks);
|
||||
}
|
||||
return codeblocks;
|
||||
}
|
||||
|
||||
public get codeblocksPartId(): string | undefined {
|
||||
@@ -43,6 +48,9 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
}
|
||||
|
||||
private subPart!: BaseChatToolInvocationSubPart;
|
||||
private mcpAppPart: ChatMcpAppSubPart | undefined;
|
||||
|
||||
private readonly _onDidRemount = this._register(new Emitter<void>());
|
||||
|
||||
constructor(
|
||||
private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized,
|
||||
@@ -78,9 +86,12 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
// This part is a bit different, since IChatToolInvocation is not an immutable model object. So this part is able to rerender itself.
|
||||
// If this turns out to be a typical pattern, we could come up with a more reusable pattern, like telling the list to rerender an element
|
||||
// when the model changes, or trying to make the model immutable and swap out one content part for a new one based on user actions in the view.
|
||||
// Note that `node.replaceWith` is used to ensure order is preserved when an mpc app is present.
|
||||
const partStore = this._register(new DisposableStore());
|
||||
let subPartDomNode: HTMLElement = document.createElement('div');
|
||||
this.domNode.appendChild(subPartDomNode);
|
||||
|
||||
const render = () => {
|
||||
dom.clearNode(this.domNode);
|
||||
partStore.clear();
|
||||
|
||||
if (toolInvocation.presentation === ToolInvocationPresentation.HiddenAfterComplete && IChatToolInvocation.isComplete(toolInvocation)) {
|
||||
@@ -88,12 +99,44 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
}
|
||||
|
||||
this.subPart = partStore.add(this.createToolInvocationSubPart());
|
||||
this.domNode.appendChild(this.subPart.domNode);
|
||||
subPartDomNode.replaceWith(this.subPart.domNode);
|
||||
subPartDomNode = this.subPart.domNode;
|
||||
|
||||
partStore.add(this.subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
|
||||
partStore.add(this.subPart.onNeedsRerender(render));
|
||||
|
||||
this._onDidChangeHeight.fire();
|
||||
};
|
||||
|
||||
const mcpAppRenderData = this.getMcpAppRenderData();
|
||||
if (mcpAppRenderData) {
|
||||
const shouldRender = derived(r => {
|
||||
const outcome = IChatToolInvocation.executionConfirmedOrDenied(toolInvocation, r);
|
||||
return !!outcome && outcome.type !== ToolConfirmKind.Denied && outcome.type !== ToolConfirmKind.Skipped;
|
||||
});
|
||||
|
||||
let appDomNode: HTMLElement = document.createElement('div');
|
||||
this.domNode.appendChild(appDomNode);
|
||||
|
||||
this._register(autorun(r => {
|
||||
if (shouldRender.read(r)) {
|
||||
this.mcpAppPart = r.store.add(this.instantiationService.createInstance(
|
||||
ChatMcpAppSubPart,
|
||||
this.toolInvocation,
|
||||
this._onDidRemount.event,
|
||||
mcpAppRenderData
|
||||
));
|
||||
appDomNode.replaceWith(this.mcpAppPart.domNode);
|
||||
appDomNode = this.mcpAppPart.domNode;
|
||||
r.store.add(this.mcpAppPart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
|
||||
} else {
|
||||
this.mcpAppPart = undefined;
|
||||
dom.clearNode(appDomNode);
|
||||
}
|
||||
|
||||
this._onDidChangeHeight.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -159,6 +202,35 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
return this.instantiationService.createInstance(ChatToolProgressSubPart, this.toolInvocation, this.context, this.renderer, this.announcedToolProgressKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets MCP App render data if this tool invocation has MCP App UI.
|
||||
* Returns data from either:
|
||||
* - toolSpecificData.mcpAppData (for in-progress tools)
|
||||
* - result details mcpOutput (for completed tools)
|
||||
*/
|
||||
private getMcpAppRenderData(): IMcpAppRenderData | undefined {
|
||||
const toolSpecificData = this.toolInvocation.toolSpecificData;
|
||||
if (toolSpecificData?.kind === 'input' && toolSpecificData.mcpAppData) {
|
||||
const rawInput = typeof toolSpecificData.rawInput === 'string'
|
||||
? toolSpecificData.rawInput
|
||||
: JSON.stringify(toolSpecificData.rawInput, null, 2);
|
||||
|
||||
return {
|
||||
resourceUri: toolSpecificData.mcpAppData.resourceUri,
|
||||
serverDefinitionId: toolSpecificData.mcpAppData.serverDefinitionId,
|
||||
collectionId: toolSpecificData.mcpAppData.collectionId,
|
||||
input: rawInput,
|
||||
sessionResource: this.context.element.sessionResource,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onDidRemount(): void {
|
||||
this._onDidRemount.fire();
|
||||
}
|
||||
|
||||
hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean {
|
||||
return (other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && this.toolInvocation.toolCallId === other.toolCallId;
|
||||
}
|
||||
|
||||
@@ -105,6 +105,12 @@ export interface IChatListItemTemplate {
|
||||
* they are disposed in a separate cycle after diffing with the next content to render.
|
||||
*/
|
||||
renderedParts?: IChatContentPart[];
|
||||
/**
|
||||
* Whether the parts are mounted in the DOM. This is undefined after
|
||||
* the element is disposed so the `renderedParts.onDidMount` can be
|
||||
* called on the next render as appropriate.
|
||||
*/
|
||||
renderedPartsMounted?: boolean;
|
||||
readonly rowContainer: HTMLElement;
|
||||
readonly titleToolbar?: MenuWorkbenchToolBar;
|
||||
readonly header?: HTMLElement;
|
||||
@@ -663,6 +669,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
this.renderChatRequest(element, index, templateData);
|
||||
}
|
||||
}
|
||||
templateData.renderedPartsMounted = true;
|
||||
}
|
||||
|
||||
private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
|
||||
@@ -992,13 +999,16 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
const renderedParts = templateData.renderedParts ?? [];
|
||||
templateData.renderedParts = renderedParts;
|
||||
partsToRender.forEach((partToRender, contentIndex) => {
|
||||
const alreadyRenderedPart = templateData.renderedParts?.[contentIndex];
|
||||
|
||||
if (!partToRender) {
|
||||
// null=no change
|
||||
if (!templateData.renderedPartsMounted) {
|
||||
alreadyRenderedPart?.onDidRemount?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadyRenderedPart = templateData.renderedParts?.[contentIndex];
|
||||
|
||||
// keep existing thinking part instance during streaming and update it in place
|
||||
if (alreadyRenderedPart) {
|
||||
if (partToRender.kind === 'thinking' && alreadyRenderedPart instanceof ChatThinkingContentPart) {
|
||||
@@ -1751,6 +1761,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
disposeElement(node: ITreeNode<ChatTreeItem, FuzzyScore>, index: number, templateData: IChatListItemTemplate, details?: IListElementRenderDetails): void {
|
||||
this.traceLayout('disposeElement', `Disposing element, index=${index}`);
|
||||
templateData.elementDisposables.clear();
|
||||
templateData.renderedPartsMounted = false;
|
||||
|
||||
if (templateData.currentElement && !this.viewModel?.editing) {
|
||||
this.templateDataByRequestId.delete(templateData.currentElement.id);
|
||||
|
||||
@@ -416,6 +416,15 @@ export interface IChatToolInputInvocationData {
|
||||
kind: 'input';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
rawInput: any;
|
||||
/** Optional MCP App UI metadata for rendering during and after tool execution */
|
||||
mcpAppData?: {
|
||||
/** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */
|
||||
resourceUri: string;
|
||||
/** Reference to the server definition for reconnection */
|
||||
serverDefinitionId: string;
|
||||
/** Reference to the collection containing the server */
|
||||
collectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const enum ToolConfirmKind {
|
||||
|
||||
@@ -184,6 +184,8 @@ export interface IToolResultInputOutputDetails {
|
||||
readonly input: string;
|
||||
readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[];
|
||||
readonly isError?: boolean;
|
||||
/** Raw MCP tool result for MCP App UI rendering */
|
||||
readonly mcpOutput?: unknown;
|
||||
}
|
||||
|
||||
export interface IToolResultOutputDetails {
|
||||
|
||||
272
src/vs/workbench/contrib/mcp/browser/mcpToolCallUI.ts
Normal file
272
src/vs/workbench/contrib/mcp/browser/mcpToolCallUI.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Gesture } from '../../../../base/browser/touch.js';
|
||||
import { decodeBase64 } from '../../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js';
|
||||
import { isMobile, isWeb, locale } from '../../../../base/common/platform.js';
|
||||
import { hasKey } from '../../../../base/common/types.js';
|
||||
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { McpServer } from '../common/mcpServer.js';
|
||||
import { IMcpServer, IMcpService, IMcpToolCallUIData, McpToolVisibility } from '../common/mcpTypes.js';
|
||||
import { findMcpServer, startServerAndWaitForLiveTools, translateMcpLogMessage } from '../common/mcpTypesUtils.js';
|
||||
import { MCP } from '../common/modelContextProtocol.js';
|
||||
import { McpApps } from '../common/modelContextProtocolApps.js';
|
||||
|
||||
/**
|
||||
* Result from loading an MCP App UI resource.
|
||||
*/
|
||||
export interface IMcpAppResourceContent extends McpApps.McpUiResourceMeta {
|
||||
/** The HTML content of the UI resource */
|
||||
readonly html: string;
|
||||
/** MIME type of the content */
|
||||
readonly mimeType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class that "upgrades" serializable IMcpToolCallUIData into a functional
|
||||
* object that can load UI resources and proxy tool/resource calls back to the MCP server.
|
||||
*/
|
||||
export class McpToolCallUI extends Disposable {
|
||||
/**
|
||||
* Basic host context reflecting the current UI and theme. Notably lacks
|
||||
* the `toolInfo` or `viewport` sizes.
|
||||
*/
|
||||
public readonly hostContext: IObservable<McpApps.McpUiHostContext>;
|
||||
|
||||
constructor(
|
||||
private readonly _uiData: IMcpToolCallUIData,
|
||||
@IMcpService private readonly _mcpService: IMcpService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const colorTheme = observableFromEvent(
|
||||
themeService.onDidColorThemeChange,
|
||||
() => {
|
||||
const type = themeService.getColorTheme().type;
|
||||
return type === ColorScheme.DARK || type === ColorScheme.HIGH_CONTRAST_DARK ? 'dark' : 'light';
|
||||
}
|
||||
);
|
||||
|
||||
this.hostContext = derived((reader): McpApps.McpUiHostContext => {
|
||||
return {
|
||||
theme: colorTheme.read(reader),
|
||||
styles: {
|
||||
variables: {
|
||||
'--color-background-primary': 'var(--vscode-editor-background)',
|
||||
'--color-background-secondary': 'var(--vscode-sideBar-background)',
|
||||
'--color-background-tertiary': 'var(--vscode-activityBar-background)',
|
||||
'--color-background-inverse': 'var(--vscode-editor-foreground)',
|
||||
'--color-background-ghost': 'transparent',
|
||||
'--color-background-info': 'var(--vscode-inputValidation-infoBackground)',
|
||||
'--color-background-danger': 'var(--vscode-inputValidation-errorBackground)',
|
||||
'--color-background-success': 'var(--vscode-diffEditor-insertedTextBackground)',
|
||||
'--color-background-warning': 'var(--vscode-inputValidation-warningBackground)',
|
||||
'--color-background-disabled': 'var(--vscode-editor-inactiveSelectionBackground)',
|
||||
|
||||
'--color-text-primary': 'var(--vscode-foreground)',
|
||||
'--color-text-secondary': 'var(--vscode-descriptionForeground)',
|
||||
'--color-text-tertiary': 'var(--vscode-disabledForeground)',
|
||||
'--color-text-inverse': 'var(--vscode-editor-background)',
|
||||
'--color-text-info': 'var(--vscode-textLink-foreground)',
|
||||
'--color-text-danger': 'var(--vscode-errorForeground)',
|
||||
'--color-text-success': 'var(--vscode-testing-iconPassed)',
|
||||
'--color-text-warning': 'var(--vscode-editorWarning-foreground)',
|
||||
'--color-text-disabled': 'var(--vscode-disabledForeground)',
|
||||
'--color-text-ghost': 'var(--vscode-descriptionForeground)',
|
||||
|
||||
'--color-border-primary': 'var(--vscode-widget-border)',
|
||||
'--color-border-secondary': 'var(--vscode-editorWidget-border)',
|
||||
'--color-border-tertiary': 'var(--vscode-panel-border)',
|
||||
'--color-border-inverse': 'var(--vscode-foreground)',
|
||||
'--color-border-ghost': 'transparent',
|
||||
'--color-border-info': 'var(--vscode-inputValidation-infoBorder)',
|
||||
'--color-border-danger': 'var(--vscode-inputValidation-errorBorder)',
|
||||
'--color-border-success': 'var(--vscode-testing-iconPassed)',
|
||||
'--color-border-warning': 'var(--vscode-inputValidation-warningBorder)',
|
||||
'--color-border-disabled': 'var(--vscode-disabledForeground)',
|
||||
|
||||
'--color-ring-primary': 'var(--vscode-focusBorder)',
|
||||
'--color-ring-secondary': 'var(--vscode-focusBorder)',
|
||||
'--color-ring-inverse': 'var(--vscode-focusBorder)',
|
||||
'--color-ring-info': 'var(--vscode-inputValidation-infoBorder)',
|
||||
'--color-ring-danger': 'var(--vscode-inputValidation-errorBorder)',
|
||||
'--color-ring-success': 'var(--vscode-testing-iconPassed)',
|
||||
'--color-ring-warning': 'var(--vscode-inputValidation-warningBorder)',
|
||||
|
||||
'--font-sans': 'var(--vscode-font-family)',
|
||||
'--font-mono': 'var(--vscode-editor-font-family)',
|
||||
|
||||
'--font-weight-normal': 'normal',
|
||||
'--font-weight-medium': '500',
|
||||
'--font-weight-semibold': '600',
|
||||
'--font-weight-bold': 'bold',
|
||||
|
||||
'--font-text-xs-size': '10px',
|
||||
'--font-text-sm-size': '11px',
|
||||
'--font-text-md-size': '13px',
|
||||
'--font-text-lg-size': '14px',
|
||||
|
||||
'--font-heading-xs-size': '16px',
|
||||
'--font-heading-sm-size': '18px',
|
||||
'--font-heading-md-size': '20px',
|
||||
'--font-heading-lg-size': '24px',
|
||||
'--font-heading-xl-size': '32px',
|
||||
'--font-heading-2xl-size': '40px',
|
||||
'--font-heading-3xl-size': '48px',
|
||||
|
||||
'--border-radius-xs': '2px',
|
||||
'--border-radius-sm': '3px',
|
||||
'--border-radius-md': '4px',
|
||||
'--border-radius-lg': '6px',
|
||||
'--border-radius-xl': '8px',
|
||||
'--border-radius-full': '9999px',
|
||||
|
||||
'--border-width-regular': '1px',
|
||||
|
||||
'--font-text-xs-line-height': '1.5',
|
||||
'--font-text-sm-line-height': '1.5',
|
||||
'--font-text-md-line-height': '1.5',
|
||||
'--font-text-lg-line-height': '1.5',
|
||||
|
||||
'--font-heading-xs-line-height': '1.25',
|
||||
'--font-heading-sm-line-height': '1.25',
|
||||
'--font-heading-md-line-height': '1.25',
|
||||
'--font-heading-lg-line-height': '1.25',
|
||||
'--font-heading-xl-line-height': '1.25',
|
||||
'--font-heading-2xl-line-height': '1.25',
|
||||
'--font-heading-3xl-line-height': '1.25',
|
||||
|
||||
'--shadow-hairline': '0 0 0 1px var(--vscode-widget-shadow)',
|
||||
'--shadow-sm': '0 1px 2px 0 var(--vscode-widget-shadow)',
|
||||
'--shadow-md': '0 4px 6px -1px var(--vscode-widget-shadow)',
|
||||
'--shadow-lg': '0 10px 15px -3px var(--vscode-widget-shadow)',
|
||||
}
|
||||
},
|
||||
displayMode: 'inline',
|
||||
availableDisplayModes: ['inline'],
|
||||
locale: locale,
|
||||
platform: isWeb ? 'web' : isMobile ? 'mobile' : 'desktop',
|
||||
deviceCapabilities: {
|
||||
touch: Gesture.isTouchDevice(),
|
||||
hover: Gesture.isHoverDevice(),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying UI data.
|
||||
*/
|
||||
public get uiData(): IMcpToolCallUIData {
|
||||
return this._uiData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to the MCP server's logger.
|
||||
*/
|
||||
public async log(log: MCP.LoggingMessageNotificationParams) {
|
||||
const server = await this._getServer(CancellationToken.None);
|
||||
if (server) {
|
||||
translateMcpLogMessage((server as McpServer).logger, log, `[App UI]`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or finds the MCP server for this UI.
|
||||
*/
|
||||
private async _getServer(token: CancellationToken): Promise<IMcpServer | undefined> {
|
||||
return findMcpServer(this._mcpService, s =>
|
||||
s.definition.id === this._uiData.serverDefinitionId &&
|
||||
s.collection.id === this._uiData.collectionId,
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the UI resource from the MCP server.
|
||||
* @param token Cancellation token
|
||||
* @returns The HTML content and CSP configuration
|
||||
*/
|
||||
public async loadResource(token: CancellationToken): Promise<IMcpAppResourceContent> {
|
||||
const server = await this._getServer(token);
|
||||
if (!server) {
|
||||
throw new Error('MCP server not found for UI resource');
|
||||
}
|
||||
|
||||
const resourceResult = await McpServer.callOn(server, h => h.readResource({ uri: this._uiData.resourceUri }, token), token);
|
||||
if (!resourceResult.contents || resourceResult.contents.length === 0) {
|
||||
throw new Error('UI resource not found on server');
|
||||
}
|
||||
|
||||
const content = resourceResult.contents[0];
|
||||
let html: string;
|
||||
const mimeType = content.mimeType || 'text/html';
|
||||
|
||||
if (hasKey(content, { text: true })) {
|
||||
html = content.text;
|
||||
} else if (hasKey(content, { blob: true })) {
|
||||
html = decodeBase64(content.blob).toString();
|
||||
} else {
|
||||
throw new Error('UI resource has no content');
|
||||
}
|
||||
|
||||
const meta = resourceResult._meta?.ui as McpApps.McpUiResourceMeta | undefined;
|
||||
|
||||
return {
|
||||
...meta,
|
||||
html,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a tool on the MCP server.
|
||||
* @param name Tool name
|
||||
* @param params Tool parameters
|
||||
* @param token Cancellation token
|
||||
* @returns The tool call result
|
||||
*/
|
||||
public async callTool(name: string, params: Record<string, unknown>, token: CancellationToken): Promise<MCP.CallToolResult> {
|
||||
const server = await this._getServer(token);
|
||||
if (!server) {
|
||||
throw new Error('MCP server not found for tool call');
|
||||
}
|
||||
|
||||
await startServerAndWaitForLiveTools(server, undefined, token);
|
||||
|
||||
const tool = server.tools.get().find(t => t.definition.name === name);
|
||||
if (!tool || !(tool.visibility & McpToolVisibility.App)) {
|
||||
throw new Error(`Tool not found on server: ${name}`);
|
||||
}
|
||||
|
||||
const res = await tool.call(params, undefined, token);
|
||||
return {
|
||||
content: res.content,
|
||||
isError: res.isError,
|
||||
_meta: res._meta,
|
||||
structuredContent: res.structuredContent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a resource from the MCP server.
|
||||
* @param uri Resource URI
|
||||
* @param token Cancellation token
|
||||
* @returns The resource content
|
||||
*/
|
||||
public async readResource(uri: string, token: CancellationToken): Promise<MCP.ReadResourceResult> {
|
||||
const server = await this._getServer(token);
|
||||
if (!server) {
|
||||
throw new Error('MCP server not found');
|
||||
}
|
||||
|
||||
return await McpServer.callOn(server, h => h.readResource({ uri }, token), token);
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable }
|
||||
import { equals } from '../../../../base/common/objects.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { basename } from '../../../../base/common/resources.js';
|
||||
import { isDefined } from '../../../../base/common/types.js';
|
||||
import { isDefined, Mutable } from '../../../../base/common/types.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IImageResizeService } from '../../../../platform/imageResize/common/imageResizeService.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagement.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { StorageScope } from '../../../../platform/storage/common/storage.js';
|
||||
import { IWorkbenchContribution } from '../../../common/contributions.js';
|
||||
@@ -25,7 +27,7 @@ import { ChatResponseResource, getAttachableImageExtension } from '../../chat/co
|
||||
import { LanguageModelPartAudience } from '../../chat/common/languageModels.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/tools/languageModelToolsService.js';
|
||||
import { IMcpRegistry } from './mcpRegistryTypes.js';
|
||||
import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType } from './mcpTypes.js';
|
||||
import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType, McpToolVisibility } from './mcpTypes.js';
|
||||
import { mcpServerToSourceData } from './mcpTypesUtils.js';
|
||||
|
||||
interface ISyncedToolData {
|
||||
@@ -111,6 +113,11 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor
|
||||
|
||||
const collection = collectionObservable.read(reader);
|
||||
for (const tool of server.tools.read(reader)) {
|
||||
// Skip app-only tools - they should not be registered with the language model tools service
|
||||
if (!(tool.visibility & McpToolVisibility.Model)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = tools.get(tool.id);
|
||||
const icons = tool.icons.getUrl(22);
|
||||
const toolData: IToolData = {
|
||||
@@ -176,6 +183,7 @@ class McpToolImplementation implements IToolImpl {
|
||||
constructor(
|
||||
private readonly _tool: IMcpTool,
|
||||
private readonly _server: IMcpServer,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IImageResizeService private readonly _imageResizeService: IImageResizeService,
|
||||
@@ -205,6 +213,8 @@ class McpToolImplementation implements IToolImpl {
|
||||
confirm.confirmResults = true;
|
||||
}
|
||||
|
||||
const mcpUiEnabled = this._configurationService.getValue<boolean>(mcpAppsEnabledConfig);
|
||||
|
||||
return {
|
||||
confirmationMessages: confirm,
|
||||
invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)),
|
||||
@@ -212,7 +222,12 @@ class McpToolImplementation implements IToolImpl {
|
||||
originMessage: localize('msg.subtitle', "{0} (MCP Server)", server.definition.label),
|
||||
toolSpecificData: {
|
||||
kind: 'input',
|
||||
rawInput: context.parameters
|
||||
rawInput: context.parameters,
|
||||
mcpAppData: mcpUiEnabled && tool.uiResourceUri ? {
|
||||
resourceUri: tool.uiResourceUri,
|
||||
serverDefinitionId: server.definition.id,
|
||||
collectionId: server.collection.id,
|
||||
} : undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -224,7 +239,7 @@ class McpToolImplementation implements IToolImpl {
|
||||
};
|
||||
|
||||
const callResult = await this._tool.callWithProgress(invocation.parameters as Record<string, unknown>, progress, { chatRequestId: invocation.chatRequestId, chatSessionId: invocation.context?.sessionId }, token);
|
||||
const details: IToolResultInputOutputDetails = {
|
||||
const details: Mutable<IToolResultInputOutputDetails> = {
|
||||
input: JSON.stringify(invocation.parameters, undefined, 2),
|
||||
output: [],
|
||||
isError: callResult.isError === true,
|
||||
@@ -341,6 +356,11 @@ class McpToolImplementation implements IToolImpl {
|
||||
result.content.push({ kind: 'text', value: JSON.stringify(callResult.structuredContent), audience: [LanguageModelPartAudience.Assistant] });
|
||||
}
|
||||
|
||||
// Add raw MCP output for MCP App UI rendering if this tool has UI
|
||||
if (this._tool.uiResourceUri) {
|
||||
details.mcpOutput = callResult;
|
||||
}
|
||||
|
||||
result.toolResultDetails = details;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js
|
||||
import { IMcpRegistry } from './mcpRegistryTypes.js';
|
||||
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
|
||||
import { McpTaskManager } from './mcpTaskManager.js';
|
||||
import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js';
|
||||
import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js';
|
||||
import { MCP } from './modelContextProtocol.js';
|
||||
import { McpApps } from './modelContextProtocolApps.js';
|
||||
import { UriTemplate } from './uriTemplate.js';
|
||||
|
||||
type ServerBootData = {
|
||||
@@ -218,6 +219,18 @@ type ValidatedMcpTool = MCP.Tool & {
|
||||
* in {@link McpServer._getValidatedTools}.
|
||||
*/
|
||||
serverToolName: string;
|
||||
|
||||
/**
|
||||
* Visibility of the tool, parsed from `_meta.ui.visibility`.
|
||||
* Defaults to Model | App if not specified.
|
||||
*/
|
||||
visibility: McpToolVisibility;
|
||||
|
||||
/**
|
||||
* UI resource URI if this tool has an associated MCP App UI.
|
||||
* Parsed from `_meta.ui.resourceUri`.
|
||||
*/
|
||||
uiResourceUri?: string;
|
||||
};
|
||||
|
||||
interface StoredServerMetadata {
|
||||
@@ -394,6 +407,10 @@ export class McpServer extends Disposable implements IMcpServer {
|
||||
return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;
|
||||
});
|
||||
|
||||
public get logger(): ILogger {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
private readonly _loggerId: string;
|
||||
private readonly _logger: ILogger;
|
||||
private _lastModeDebugged = false;
|
||||
@@ -741,10 +758,28 @@ export class McpServer extends Disposable implements IMcpServer {
|
||||
}
|
||||
|
||||
private async _normalizeTool(originalTool: MCP.Tool): Promise<ValidatedMcpTool | { error: string[] }> {
|
||||
// Parse MCP Apps UI metadata from _meta.ui
|
||||
const uiMeta = originalTool._meta?.ui as McpApps.McpUiToolMeta | undefined;
|
||||
|
||||
// Compute visibility from _meta.ui.visibility, defaulting to Model | App
|
||||
let visibility: McpToolVisibility = McpToolVisibility.Model | McpToolVisibility.App;
|
||||
if (uiMeta?.visibility && Array.isArray(uiMeta.visibility)) {
|
||||
visibility &= 0;
|
||||
|
||||
if (uiMeta.visibility.includes('model')) {
|
||||
visibility |= McpToolVisibility.Model;
|
||||
}
|
||||
if (uiMeta.visibility.includes('app')) {
|
||||
visibility |= McpToolVisibility.App;
|
||||
}
|
||||
}
|
||||
|
||||
const tool: ValidatedMcpTool = {
|
||||
...originalTool,
|
||||
serverToolName: originalTool.name,
|
||||
_icons: this._parseIcons(originalTool),
|
||||
visibility,
|
||||
uiResourceUri: uiMeta?.resourceUri,
|
||||
};
|
||||
if (!tool.description) {
|
||||
// Ensure a description is provided for each tool, #243919
|
||||
@@ -980,8 +1015,10 @@ export class McpTool implements IMcpTool {
|
||||
readonly id: string;
|
||||
readonly referenceName: string;
|
||||
readonly icons: IMcpIcons;
|
||||
readonly visibility: McpToolVisibility;
|
||||
|
||||
public get definition(): MCP.Tool { return this._definition; }
|
||||
public get uiResourceUri(): string | undefined { return this._definition.uiResourceUri; }
|
||||
|
||||
constructor(
|
||||
private readonly _server: McpServer,
|
||||
@@ -992,6 +1029,7 @@ export class McpTool implements IMcpTool {
|
||||
this.referenceName = _definition.name.replaceAll('.', '_');
|
||||
this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);
|
||||
this.icons = McpIcons.fromStored(this._definition._icons);
|
||||
this.visibility = _definition.visibility;
|
||||
}
|
||||
|
||||
async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
|
||||
@@ -1052,6 +1090,7 @@ export class McpTool implements IMcpTool {
|
||||
|
||||
// Wait for tools to refresh for dynamic servers (#261611)
|
||||
await this._server.awaitToolRefresh();
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
// Handle URL elicitation required error
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IProductService } from '../../../../platform/product/common/productServ
|
||||
import { IMcpMessageTransport } from './mcpRegistryTypes.js';
|
||||
import { IMcpTaskInternal, McpTaskManager } from './mcpTaskManager.js';
|
||||
import { IMcpClientMethods, McpConnectionState, McpError, MpcResponseError } from './mcpTypes.js';
|
||||
import { isTaskResult } from './mcpTypesUtils.js';
|
||||
import { isTaskResult, translateMcpLogMessage } from './mcpTypesUtils.js';
|
||||
import { MCP } from './modelContextProtocol.js';
|
||||
|
||||
/**
|
||||
@@ -454,32 +454,7 @@ export class McpServerRequestHandler extends Disposable {
|
||||
}
|
||||
|
||||
private handleLoggingNotification(request: MCP.LoggingMessageNotification): void {
|
||||
let contents = typeof request.params.data === 'string' ? request.params.data : JSON.stringify(request.params.data);
|
||||
if (request.params.logger) {
|
||||
contents = `${request.params.logger}: ${contents}`;
|
||||
}
|
||||
|
||||
switch (request.params?.level) {
|
||||
case 'debug':
|
||||
this.logger.debug(contents);
|
||||
break;
|
||||
case 'info':
|
||||
case 'notice':
|
||||
this.logger.info(contents);
|
||||
break;
|
||||
case 'warning':
|
||||
this.logger.warn(contents);
|
||||
break;
|
||||
case 'error':
|
||||
case 'critical':
|
||||
case 'alert':
|
||||
case 'emergency':
|
||||
this.logger.error(contents);
|
||||
break;
|
||||
default:
|
||||
this.logger.info(contents);
|
||||
break;
|
||||
}
|
||||
translateMcpLogMessage(this.logger, request.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -438,6 +438,30 @@ export interface IMcpToolCallContext {
|
||||
chatRequestId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility of an MCP tool, based on the MCP Apps `_meta.ui.visibility` field.
|
||||
* @see https://github.com/anthropics/mcp/blob/main/apps.md
|
||||
*/
|
||||
export const enum McpToolVisibility {
|
||||
/** Tool is visible to and callable by the language model */
|
||||
Model = 1 << 0,
|
||||
/** Tool is callable by the MCP App UI */
|
||||
App = 1 << 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable data for MCP App UI rendering.
|
||||
* This contains all the information needed to render an MCP App webview.
|
||||
*/
|
||||
export interface IMcpToolCallUIData {
|
||||
/** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */
|
||||
readonly resourceUri: string;
|
||||
/** Reference to the server definition for reconnection */
|
||||
readonly serverDefinitionId: string;
|
||||
/** Reference to the collection containing the server */
|
||||
readonly collectionId: string;
|
||||
}
|
||||
|
||||
export interface IMcpTool {
|
||||
|
||||
readonly id: string;
|
||||
@@ -445,6 +469,10 @@ export interface IMcpTool {
|
||||
readonly referenceName: string;
|
||||
readonly icons: IMcpIcons;
|
||||
readonly definition: MCP.Tool;
|
||||
/** Visibility of the tool (Model, App, or both). Defaults to Model | App. */
|
||||
readonly visibility: McpToolVisibility;
|
||||
/** Optional UI resource URI for MCP App rendering */
|
||||
readonly uiResourceUri?: string;
|
||||
|
||||
/**
|
||||
* Calls a tool
|
||||
|
||||
@@ -7,7 +7,8 @@ import { disposableTimeout, timeout } from '../../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CancellationError } from '../../../../base/common/errors.js';
|
||||
import { DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun, IReader } from '../../../../base/common/observable.js';
|
||||
import { autorun, autorunSelfDisposable, IReader } from '../../../../base/common/observable.js';
|
||||
import { ILogger } from '../../../../platform/log/common/log.js';
|
||||
import { ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js';
|
||||
import { IMcpServer, IMcpServerStartOpts, IMcpService, McpConnectionState, McpServerCacheState, McpServerTransportType } from './mcpTypes.js';
|
||||
import { MCP } from './modelContextProtocol.js';
|
||||
@@ -118,3 +119,61 @@ export function canLoadMcpNetworkResourceDirectly(resource: URL, server: IMcpSer
|
||||
export function isTaskResult(obj: MCP.Result | MCP.CreateTaskResult): obj is MCP.CreateTaskResult {
|
||||
return (obj as MCP.CreateTaskResult).task !== undefined;
|
||||
}
|
||||
|
||||
export function findMcpServer(mcpService: IMcpService, filter: (s: IMcpServer) => boolean, token?: CancellationToken) {
|
||||
return new Promise<IMcpServer | undefined>((resolve) => {
|
||||
autorunSelfDisposable(reader => {
|
||||
if (token) {
|
||||
if (token.isCancellationRequested) {
|
||||
reader.dispose();
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
reader.store.add(token.onCancellationRequested(() => {
|
||||
reader.dispose();
|
||||
resolve(undefined);
|
||||
}));
|
||||
}
|
||||
|
||||
const servers = mcpService.servers.read(reader);
|
||||
const server = servers.find(filter);
|
||||
if (server) {
|
||||
resolve(server);
|
||||
reader.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function translateMcpLogMessage(logger: ILogger, params: MCP.LoggingMessageNotificationParams, prefix = '') {
|
||||
let contents = typeof params.data === 'string' ? params.data : JSON.stringify(params.data);
|
||||
if (params.logger) {
|
||||
contents = `${params.logger}: ${contents}`;
|
||||
}
|
||||
if (prefix) {
|
||||
contents = `${prefix} ${contents}`;
|
||||
}
|
||||
|
||||
switch (params?.level) {
|
||||
case 'debug':
|
||||
logger.debug(contents);
|
||||
break;
|
||||
case 'info':
|
||||
case 'notice':
|
||||
logger.info(contents);
|
||||
break;
|
||||
case 'warning':
|
||||
logger.warn(contents);
|
||||
break;
|
||||
case 'error':
|
||||
case 'critical':
|
||||
case 'alert':
|
||||
case 'emergency':
|
||||
logger.error(contents);
|
||||
break;
|
||||
default:
|
||||
logger.info(contents);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
608
src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts
Normal file
608
src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MCP } from './modelContextProtocol.js';
|
||||
|
||||
type CallToolResult = MCP.CallToolResult;
|
||||
type ContentBlock = MCP.ContentBlock;
|
||||
type Implementation = MCP.Implementation;
|
||||
type RequestId = MCP.RequestId;
|
||||
type Tool = MCP.Tool;
|
||||
|
||||
//#region utilities
|
||||
|
||||
export namespace McpApps {
|
||||
export type AppRequest =
|
||||
| MCP.CallToolRequest
|
||||
| MCP.ReadResourceRequest
|
||||
| MCP.PingRequest
|
||||
| (McpUiOpenLinkRequest & MCP.JSONRPCRequest)
|
||||
| (McpUiMessageRequest & MCP.JSONRPCRequest)
|
||||
| (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest)
|
||||
| (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest);
|
||||
|
||||
export type AppNotification =
|
||||
| McpUiInitializedNotification
|
||||
| McpUiSizeChangedNotification
|
||||
| MCP.LoggingMessageNotification;
|
||||
|
||||
export type AppMessage = AppRequest | AppNotification;
|
||||
|
||||
export type HostResult =
|
||||
| MCP.CallToolResult
|
||||
| MCP.ReadResourceResult
|
||||
| MCP.EmptyResult
|
||||
| McpApps.McpUiInitializeResult
|
||||
| McpUiMessageResult
|
||||
| McpUiOpenLinkResult
|
||||
| McpUiRequestDisplayModeResult;
|
||||
|
||||
export type HostNotification =
|
||||
| McpUiHostContextChangedNotification
|
||||
| McpUiResourceTeardownRequest
|
||||
| McpUiToolInputNotification
|
||||
| McpUiToolInputPartialNotification
|
||||
| McpUiToolResultNotification
|
||||
| McpUiToolCancelledNotification
|
||||
| McpUiSizeChangedNotification;
|
||||
|
||||
export type HostMessage = HostResult | HostNotification;
|
||||
}
|
||||
|
||||
/* eslint-disable local/code-no-unexternalized-strings */
|
||||
|
||||
|
||||
/**
|
||||
* Schema updated from the Model Context Protocol Apps repository at
|
||||
* https://github.com/modelcontextprotocol/ext-apps/blob/main/src/spec.types.ts
|
||||
*
|
||||
* ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️
|
||||
*/
|
||||
export namespace McpApps {
|
||||
|
||||
|
||||
/**
|
||||
* MCP Apps Protocol Types (spec.types.ts)
|
||||
*
|
||||
* This file contains pure TypeScript interface definitions for the MCP Apps protocol.
|
||||
* These types are the source of truth and are used to generate Zod schemas via ts-to-zod.
|
||||
*
|
||||
* - Use `@description` JSDoc tags to generate `.describe()` calls on schemas
|
||||
* - Run `npm run generate:schemas` to regenerate schemas from these types
|
||||
*
|
||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx
|
||||
*/
|
||||
|
||||
/**
|
||||
* Current protocol version supported by this SDK.
|
||||
*
|
||||
* The SDK automatically handles version negotiation during initialization.
|
||||
* Apps and hosts don't need to manage protocol versions manually.
|
||||
*/
|
||||
export const LATEST_PROTOCOL_VERSION = "2025-11-21";
|
||||
|
||||
/**
|
||||
* @description Color theme preference for the host environment.
|
||||
*/
|
||||
export type McpUiTheme = "light" | "dark";
|
||||
|
||||
/**
|
||||
* @description Display mode for UI presentation.
|
||||
*/
|
||||
export type McpUiDisplayMode = "inline" | "fullscreen" | "pip";
|
||||
|
||||
/**
|
||||
* @description CSS variable keys available to MCP apps for theming.
|
||||
*/
|
||||
export type McpUiStyleVariableKey =
|
||||
// Background colors
|
||||
| "--color-background-primary"
|
||||
| "--color-background-secondary"
|
||||
| "--color-background-tertiary"
|
||||
| "--color-background-inverse"
|
||||
| "--color-background-ghost"
|
||||
| "--color-background-info"
|
||||
| "--color-background-danger"
|
||||
| "--color-background-success"
|
||||
| "--color-background-warning"
|
||||
| "--color-background-disabled"
|
||||
// Text colors
|
||||
| "--color-text-primary"
|
||||
| "--color-text-secondary"
|
||||
| "--color-text-tertiary"
|
||||
| "--color-text-inverse"
|
||||
| "--color-text-info"
|
||||
| "--color-text-danger"
|
||||
| "--color-text-success"
|
||||
| "--color-text-warning"
|
||||
| "--color-text-disabled"
|
||||
| "--color-text-ghost"
|
||||
// Border colors
|
||||
| "--color-border-primary"
|
||||
| "--color-border-secondary"
|
||||
| "--color-border-tertiary"
|
||||
| "--color-border-inverse"
|
||||
| "--color-border-ghost"
|
||||
| "--color-border-info"
|
||||
| "--color-border-danger"
|
||||
| "--color-border-success"
|
||||
| "--color-border-warning"
|
||||
| "--color-border-disabled"
|
||||
// Ring colors
|
||||
| "--color-ring-primary"
|
||||
| "--color-ring-secondary"
|
||||
| "--color-ring-inverse"
|
||||
| "--color-ring-info"
|
||||
| "--color-ring-danger"
|
||||
| "--color-ring-success"
|
||||
| "--color-ring-warning"
|
||||
// Typography - Family
|
||||
| "--font-sans"
|
||||
| "--font-mono"
|
||||
// Typography - Weight
|
||||
| "--font-weight-normal"
|
||||
| "--font-weight-medium"
|
||||
| "--font-weight-semibold"
|
||||
| "--font-weight-bold"
|
||||
// Typography - Text Size
|
||||
| "--font-text-xs-size"
|
||||
| "--font-text-sm-size"
|
||||
| "--font-text-md-size"
|
||||
| "--font-text-lg-size"
|
||||
// Typography - Heading Size
|
||||
| "--font-heading-xs-size"
|
||||
| "--font-heading-sm-size"
|
||||
| "--font-heading-md-size"
|
||||
| "--font-heading-lg-size"
|
||||
| "--font-heading-xl-size"
|
||||
| "--font-heading-2xl-size"
|
||||
| "--font-heading-3xl-size"
|
||||
// Typography - Text Line Height
|
||||
| "--font-text-xs-line-height"
|
||||
| "--font-text-sm-line-height"
|
||||
| "--font-text-md-line-height"
|
||||
| "--font-text-lg-line-height"
|
||||
// Typography - Heading Line Height
|
||||
| "--font-heading-xs-line-height"
|
||||
| "--font-heading-sm-line-height"
|
||||
| "--font-heading-md-line-height"
|
||||
| "--font-heading-lg-line-height"
|
||||
| "--font-heading-xl-line-height"
|
||||
| "--font-heading-2xl-line-height"
|
||||
| "--font-heading-3xl-line-height"
|
||||
// Border radius
|
||||
| "--border-radius-xs"
|
||||
| "--border-radius-sm"
|
||||
| "--border-radius-md"
|
||||
| "--border-radius-lg"
|
||||
| "--border-radius-xl"
|
||||
| "--border-radius-full"
|
||||
// Border width
|
||||
| "--border-width-regular"
|
||||
// Shadows
|
||||
| "--shadow-hairline"
|
||||
| "--shadow-sm"
|
||||
| "--shadow-md"
|
||||
| "--shadow-lg";
|
||||
|
||||
/**
|
||||
* @description Style variables for theming MCP apps.
|
||||
*
|
||||
* Individual style keys are optional - hosts may provide any subset of these values.
|
||||
* Values are strings containing CSS values (colors, sizes, font stacks, etc.).
|
||||
*
|
||||
* Note: This type uses `Record<K, string | undefined>` rather than `Partial<Record<K, string>>`
|
||||
* for compatibility with Zod schema generation. Both are functionally equivalent for validation.
|
||||
*/
|
||||
export type McpUiStyles = Record<McpUiStyleVariableKey, string | undefined>;
|
||||
|
||||
/**
|
||||
* @description Request to open an external URL in the host's default browser.
|
||||
* @see {@link app.App.sendOpenLink} for the method that sends this request
|
||||
*/
|
||||
export interface McpUiOpenLinkRequest {
|
||||
method: "ui/open-link";
|
||||
params: {
|
||||
/** @description URL to open in the host's browser */
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Result from opening a URL.
|
||||
* @see {@link McpUiOpenLinkRequest}
|
||||
*/
|
||||
export interface McpUiOpenLinkResult {
|
||||
/** @description True if the host failed to open the URL (e.g., due to security policy). */
|
||||
isError?: boolean;
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
* Note: The schema intentionally omits this to enforce strict validation.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request to send a message to the host's chat interface.
|
||||
* @see {@link app.App.sendMessage} for the method that sends this request
|
||||
*/
|
||||
export interface McpUiMessageRequest {
|
||||
method: "ui/message";
|
||||
params: {
|
||||
/** @description Message role, currently only "user" is supported. */
|
||||
role: "user";
|
||||
/** @description Message content blocks (text, image, etc.). */
|
||||
content: ContentBlock[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Result from sending a message.
|
||||
* @see {@link McpUiMessageRequest}
|
||||
*/
|
||||
export interface McpUiMessageResult {
|
||||
/** @description True if the host rejected or failed to deliver the message. */
|
||||
isError?: boolean;
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
* Note: The schema intentionally omits this to enforce strict validation.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification that the sandbox proxy iframe is ready to receive content.
|
||||
* @internal
|
||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
||||
*/
|
||||
export interface McpUiSandboxProxyReadyNotification {
|
||||
method: "ui/notifications/sandbox-proxy-ready";
|
||||
params: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification containing HTML resource for the sandbox proxy to load.
|
||||
* @internal
|
||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
||||
*/
|
||||
export interface McpUiSandboxResourceReadyNotification {
|
||||
method: "ui/notifications/sandbox-resource-ready";
|
||||
params: {
|
||||
/** @description HTML content to load into the inner iframe. */
|
||||
html: string;
|
||||
/** @description Optional override for the inner iframe's sandbox attribute. */
|
||||
sandbox?: string;
|
||||
/** @description CSP configuration from resource metadata. */
|
||||
csp?: {
|
||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||
connectDomains?: string[];
|
||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||
resourceDomains?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification of UI size changes (bidirectional: Guest <-> Host).
|
||||
* @see {@link app.App.sendSizeChanged} for the method to send this from Guest UI
|
||||
*/
|
||||
export interface McpUiSizeChangedNotification {
|
||||
method: "ui/notifications/size-changed";
|
||||
params: {
|
||||
/** @description New width in pixels. */
|
||||
width?: number;
|
||||
/** @description New height in pixels. */
|
||||
height?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification containing complete tool arguments (Host -> Guest UI).
|
||||
*/
|
||||
export interface McpUiToolInputNotification {
|
||||
method: "ui/notifications/tool-input";
|
||||
params: {
|
||||
/** @description Complete tool call arguments as key-value pairs. */
|
||||
arguments?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification containing partial/streaming tool arguments (Host -> Guest UI).
|
||||
*/
|
||||
export interface McpUiToolInputPartialNotification {
|
||||
method: "ui/notifications/tool-input-partial";
|
||||
params: {
|
||||
/** @description Partial tool call arguments (incomplete, may change). */
|
||||
arguments?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification containing tool execution result (Host -> Guest UI).
|
||||
*/
|
||||
export interface McpUiToolResultNotification {
|
||||
method: "ui/notifications/tool-result";
|
||||
/** @description Standard MCP tool execution result. */
|
||||
params: CallToolResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification that tool execution was cancelled (Host -> Guest UI).
|
||||
* Host MUST send this if tool execution was cancelled for any reason (user action,
|
||||
* sampling error, classifier intervention, etc.).
|
||||
*/
|
||||
export interface McpUiToolCancelledNotification {
|
||||
method: "ui/notifications/tool-cancelled";
|
||||
params: {
|
||||
/** @description Optional reason for the cancellation (e.g., "user action", "timeout"). */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description CSS blocks that can be injected by apps.
|
||||
*/
|
||||
export interface McpUiHostCss {
|
||||
/** @description CSS for font loading (@font-face rules or @import statements). Apps must apply using applyHostFonts(). */
|
||||
fonts?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Style configuration for theming MCP apps.
|
||||
*/
|
||||
export interface McpUiHostStyles {
|
||||
/** @description CSS variables for theming the app. */
|
||||
variables?: McpUiStyles;
|
||||
/** @description CSS blocks that apps can inject. */
|
||||
css?: McpUiHostCss;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Rich context about the host environment provided to Guest UIs.
|
||||
*/
|
||||
export interface McpUiHostContext {
|
||||
/** @description Allow additional properties for forward compatibility. */
|
||||
[key: string]: unknown;
|
||||
/** @description Metadata of the tool call that instantiated this App. */
|
||||
toolInfo?: {
|
||||
/** @description JSON-RPC id of the tools/call request. */
|
||||
id: RequestId;
|
||||
/** @description Tool definition including name, inputSchema, etc. */
|
||||
tool: Tool;
|
||||
};
|
||||
/** @description Current color theme preference. */
|
||||
theme?: McpUiTheme;
|
||||
/** @description Style configuration for theming the app. */
|
||||
styles?: McpUiHostStyles;
|
||||
/** @description How the UI is currently displayed. */
|
||||
displayMode?: McpUiDisplayMode;
|
||||
/** @description Display modes the host supports. */
|
||||
availableDisplayModes?: string[];
|
||||
/** @description Current and maximum dimensions available to the UI. */
|
||||
viewport?: {
|
||||
/** @description Current viewport width in pixels. */
|
||||
width: number;
|
||||
/** @description Current viewport height in pixels. */
|
||||
height: number;
|
||||
/** @description Maximum available height in pixels (if constrained). */
|
||||
maxHeight?: number;
|
||||
/** @description Maximum available width in pixels (if constrained). */
|
||||
maxWidth?: number;
|
||||
};
|
||||
/** @description User's language and region preference in BCP 47 format. */
|
||||
locale?: string;
|
||||
/** @description User's timezone in IANA format. */
|
||||
timeZone?: string;
|
||||
/** @description Host application identifier. */
|
||||
userAgent?: string;
|
||||
/** @description Platform type for responsive design decisions. */
|
||||
platform?: "web" | "desktop" | "mobile";
|
||||
/** @description Device input capabilities. */
|
||||
deviceCapabilities?: {
|
||||
/** @description Whether the device supports touch input. */
|
||||
touch?: boolean;
|
||||
/** @description Whether the device supports hover interactions. */
|
||||
hover?: boolean;
|
||||
};
|
||||
/** @description Mobile safe area boundaries in pixels. */
|
||||
safeAreaInsets?: {
|
||||
/** @description Top safe area inset in pixels. */
|
||||
top: number;
|
||||
/** @description Right safe area inset in pixels. */
|
||||
right: number;
|
||||
/** @description Bottom safe area inset in pixels. */
|
||||
bottom: number;
|
||||
/** @description Left safe area inset in pixels. */
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification that host context has changed (Host -> Guest UI).
|
||||
* @see {@link McpUiHostContext} for the full context structure
|
||||
*/
|
||||
export interface McpUiHostContextChangedNotification {
|
||||
method: "ui/notifications/host-context-changed";
|
||||
/** @description Partial context update containing only changed fields. */
|
||||
params: McpUiHostContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request for graceful shutdown of the Guest UI (Host -> Guest UI).
|
||||
* @see {@link app-bridge.AppBridge.teardownResource} for the host method that sends this
|
||||
*/
|
||||
export interface McpUiResourceTeardownRequest {
|
||||
method: "ui/resource-teardown";
|
||||
params: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Result from graceful shutdown request.
|
||||
* @see {@link McpUiResourceTeardownRequest}
|
||||
*/
|
||||
export interface McpUiResourceTeardownResult {
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Capabilities supported by the host application.
|
||||
* @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities
|
||||
*/
|
||||
export interface McpUiHostCapabilities {
|
||||
/** @description Experimental features (structure TBD). */
|
||||
experimental?: {};
|
||||
/** @description Host supports opening external URLs. */
|
||||
openLinks?: {};
|
||||
/** @description Host can proxy tool calls to the MCP server. */
|
||||
serverTools?: {
|
||||
/** @description Host supports tools/list_changed notifications. */
|
||||
listChanged?: boolean;
|
||||
};
|
||||
/** @description Host can proxy resource reads to the MCP server. */
|
||||
serverResources?: {
|
||||
/** @description Host supports resources/list_changed notifications. */
|
||||
listChanged?: boolean;
|
||||
};
|
||||
/** @description Host accepts log messages. */
|
||||
logging?: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Capabilities provided by the Guest UI (App).
|
||||
* @see {@link McpUiInitializeRequest} for the initialization request that includes these capabilities
|
||||
*/
|
||||
export interface McpUiAppCapabilities {
|
||||
/** @description Experimental features (structure TBD). */
|
||||
experimental?: {};
|
||||
/** @description App exposes MCP-style tools that the host can call. */
|
||||
tools?: {
|
||||
/** @description App supports tools/list_changed notifications. */
|
||||
listChanged?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initialization request sent from Guest UI to Host.
|
||||
* @see {@link app.App.connect} for the method that sends this request
|
||||
*/
|
||||
export interface McpUiInitializeRequest {
|
||||
method: "ui/initialize";
|
||||
params: {
|
||||
/** @description App identification (name and version). */
|
||||
appInfo: Implementation;
|
||||
/** @description Features and capabilities this app provides. */
|
||||
appCapabilities: McpUiAppCapabilities;
|
||||
/** @description Protocol version this app supports. */
|
||||
protocolVersion: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initialization result returned from Host to Guest UI.
|
||||
* @see {@link McpUiInitializeRequest}
|
||||
*/
|
||||
export interface McpUiInitializeResult {
|
||||
/** @description Negotiated protocol version string (e.g., "2025-11-21"). */
|
||||
protocolVersion: string;
|
||||
/** @description Host application identification and version. */
|
||||
hostInfo: Implementation;
|
||||
/** @description Features and capabilities provided by the host. */
|
||||
hostCapabilities: McpUiHostCapabilities;
|
||||
/** @description Rich context about the host environment. */
|
||||
hostContext: McpUiHostContext;
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
* Note: The schema intentionally omits this to enforce strict validation.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification that Guest UI has completed initialization (Guest UI -> Host).
|
||||
* @see {@link app.App.connect} for the method that sends this notification
|
||||
*/
|
||||
export interface McpUiInitializedNotification {
|
||||
method: "ui/notifications/initialized";
|
||||
params?: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Content Security Policy configuration for UI resources.
|
||||
*/
|
||||
export interface McpUiResourceCsp {
|
||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||
connectDomains?: string[];
|
||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||
resourceDomains?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description UI Resource metadata for security and rendering configuration.
|
||||
*/
|
||||
export interface McpUiResourceMeta {
|
||||
/** @description Content Security Policy configuration. */
|
||||
csp?: McpUiResourceCsp;
|
||||
/** @description Dedicated origin for widget sandbox. */
|
||||
domain?: string;
|
||||
/** @description Visual boundary preference - true if UI prefers a visible border. */
|
||||
prefersBorder?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request to change the display mode of the UI.
|
||||
* The host will respond with the actual display mode that was set,
|
||||
* which may differ from the requested mode if not supported.
|
||||
* @see {@link app.App.requestDisplayMode} for the method that sends this request
|
||||
*/
|
||||
export interface McpUiRequestDisplayModeRequest {
|
||||
method: "ui/request-display-mode";
|
||||
params: {
|
||||
/** @description The display mode being requested. */
|
||||
mode: McpUiDisplayMode;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Result from requesting a display mode change.
|
||||
* @see {@link McpUiRequestDisplayModeRequest}
|
||||
*/
|
||||
export interface McpUiRequestDisplayModeResult {
|
||||
/** @description The display mode that was actually set. May differ from requested if not supported. */
|
||||
mode: McpUiDisplayMode;
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
* Note: The schema intentionally omits this to enforce strict validation.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Tool visibility scope - who can access the tool.
|
||||
*/
|
||||
export type McpUiToolVisibility = "model" | "app";
|
||||
|
||||
/**
|
||||
* @description UI-related metadata for tools.
|
||||
*/
|
||||
export interface McpUiToolMeta {
|
||||
/**
|
||||
* URI of the UI resource to display for this tool.
|
||||
* This is converted to `_meta["ui/resourceUri"]`.
|
||||
*
|
||||
* @example "ui://weather/widget.html"
|
||||
*/
|
||||
resourceUri: string;
|
||||
/**
|
||||
* @description Who can access this tool. Default: ["model", "app"]
|
||||
* - "model": Tool visible to and callable by the agent
|
||||
* - "app": Tool callable by the app from this server only
|
||||
*/
|
||||
visibility?: McpUiToolVisibility[];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user