Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {
}

// Setup tooltip hover for string context attachments
if (isStringVariableEntry(attachment) && attachment.tooltip) {
if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) {
this._setupTooltipHover(attachment.tooltip);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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 { hash } from '../../../../../../../base/common/hash.js';
import { MarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
import { autorun, autorunSelfDisposable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js';
import { basename } from '../../../../../../../base/common/resources.js';
Expand Down Expand Up @@ -396,8 +398,12 @@ export class ChatMcpAppModel extends Disposable {
result = await this._handleUiMessage(request.params);
break;

case 'ui/update-model-context':
result = await this._handleUpdateModelContext(request.params);
break;

case 'notifications/message':
await this._mcpToolCallUI.log(request.params);
await this._mcpToolCallUI.log(request.params as MCP.LoggingMessageNotification['params']);
break;

default: {
Expand Down Expand Up @@ -474,8 +480,15 @@ export class ChatMcpAppModel extends Disposable {
logging: {},
sandbox: {
csp: this._latestCsp,
permissions: { clipboardWrite: true },
permissions: { clipboardWrite: {} },
},
updateModelContext: {
audio: {},
image: {},
resourceLink: {},
resource: {},
Comment thread
connor4312 marked this conversation as resolved.
structuredContent: {},
}
},
hostContext: this.hostContext.get(),
} satisfies Required<McpApps.McpUiInitializeResult>;
Expand Down Expand Up @@ -520,6 +533,68 @@ export class ChatMcpAppModel extends Disposable {
return { isError: false };
}

private async _handleUpdateModelContext(params: McpApps.McpUiUpdateModelContextRequest['params']): Promise<MCP.EmptyResult> {
const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource);
if (!widget) {
return {};
}

const idPrefix = `mcpui-context-${hash(this.renderData.serverDefinitionId)}-`;
const toDelete = widget.attachmentModel.getAttachmentIDs();
const idsToDelete = Array.from(toDelete).filter(id => id.startsWith(idPrefix));
const entries: IChatRequestVariableEntry[] = [];
let entryIndex = 0;

if (params.content) {
for (const block of params.content) {
const id = `${idPrefix}${entryIndex++}`;
if (block.type === 'image') {
entries.push({
kind: 'image',
value: decodeBase64(block.data).buffer,
id,
name: 'Image',
mimeType: block.mimeType,
});
} else if (block.type === 'resource_link') {
const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, block.uri);
entries.push({
kind: 'file',
value: uri,
id,
name: basename(uri),
});
} else if (block.type === 'text') {
const preview = block.text.replaceAll(/\s+/g, ' ').trim();
const truncateTo = 20;
entries.push({
kind: 'generic',
value: block.text,
id,
tooltip: new MarkdownString().appendCodeblock('plaintext', block.text),
name: preview.length > truncateTo ? preview.slice(0, truncateTo) + '…' : preview,
});
}
}
}

if (params.structuredContent && Object.keys(params.structuredContent).length > 0) {
const id = `${idPrefix}structured`;
const value = JSON.stringify(params.structuredContent, null, 2);
entries.push({
kind: 'generic',
value,
tooltip: new MarkdownString().appendCodeblock('json', value),
id,
name: 'UI Data',
});
}

widget.attachmentModel.updateContext(idsToDelete, entries);

return {};
}

private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void {
if (params.height !== undefined) {
this._height = params.height;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface IBaseChatRequestVariableEntry {

export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry {
kind: 'generic';
tooltip?: IMarkdownString;
}

export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry {
Expand Down
44 changes: 44 additions & 0 deletions src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export namespace McpApps {
| MCP.ReadResourceRequest
| MCP.PingRequest
| (McpUiOpenLinkRequest & MCP.JSONRPCRequest)
| (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest)
| (McpUiMessageRequest & MCP.JSONRPCRequest)
| (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest)
| (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest);
Expand Down Expand Up @@ -427,6 +428,28 @@ export namespace McpApps {
params: McpUiHostContext;
}

/**
* @description Request to update the agent's context without requiring a follow-up action (Guest UI -> Host).
*
* Unlike `notifications/message` which is for debugging/logging, this request is intended
* to update the Host's model context. Each request overwrites the previous context sent by the Guest UI.
* Unlike messages, context updates do not trigger follow-ups.
*
* The host will typically defer sending the context to the model until the next user message
* (including `ui/message`), and will only send the last update received.
*
* @see {@link app.App.updateModelContext} for the method that sends this request
*/
export interface McpUiUpdateModelContextRequest {
method: "ui/update-model-context";
params: {
/** @description Context content blocks (text, image, etc.). */
content?: ContentBlock[];
/** @description Structured content for machine-readable context data. */
structuredContent?: Record<string, unknown>;
};
}

/**
* @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
Expand All @@ -447,6 +470,21 @@ export namespace McpApps {
[key: string]: unknown;
}

export interface McpUiSupportedContentBlockModalities {
/** @description Host supports text content blocks. */
text?: {};
/** @description Host supports image content blocks. */
image?: {};
/** @description Host supports audio content blocks. */
audio?: {};
/** @description Host supports resource content blocks. */
resource?: {};
/** @description Host supports resource link content blocks. */
resourceLink?: {};
/** @description Host supports structured content. */
structuredContent?: {};
}

/**
* @description Capabilities supported by the host application.
* @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities
Expand Down Expand Up @@ -475,6 +513,10 @@ export namespace McpApps {
/** @description CSP domains approved by the host. */
csp?: McpUiResourceCsp;
};
/** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */
updateModelContext?: McpUiSupportedContentBlockModalities;
/** @description Host supports receiving content messages (ui/message) from the Guest UI. */
message?: McpUiSupportedContentBlockModalities;
}

/**
Expand Down Expand Up @@ -674,4 +716,6 @@ export namespace McpApps {
"ui/notifications/initialized";
export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] =
"ui/request-display-mode";
export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] =
"ui/update-model-context";
}
Loading