Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/mcpServerEditor.css';6import { $, Dimension, append, clearNode, setParentFlowTo } from '../../../../base/browser/dom.js';7import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';8import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';9import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';10import { Action, IAction } from '../../../../base/common/actions.js';11import * as arrays from '../../../../base/common/arrays.js';12import { Cache, CacheResult } from '../../../../base/common/cache.js';13import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';14import { isCancellationError } from '../../../../base/common/errors.js';15import { Emitter, Event } from '../../../../base/common/event.js';16import { Disposable, DisposableStore, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';17import { Schemas, matchesScheme } from '../../../../base/common/network.js';18import { language } from '../../../../base/common/platform.js';19import { URI } from '../../../../base/common/uri.js';20import { generateUuid } from '../../../../base/common/uuid.js';21import { TokenizationRegistry } from '../../../../editor/common/languages.js';22import { ILanguageService } from '../../../../editor/common/languages/language.js';23import { generateTokensCSSForColorMap } from '../../../../editor/common/languages/supports/tokenization.js';24import { localize } from '../../../../nls.js';25import { IContextKeyService, IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';26import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';27import { INotificationService } from '../../../../platform/notification/common/notification.js';28import { IOpenerService } from '../../../../platform/opener/common/opener.js';29import { IStorageService } from '../../../../platform/storage/common/storage.js';30import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';31import { IThemeService } from '../../../../platform/theme/common/themeService.js';32import { EditorPane } from '../../../browser/parts/editor/editorPane.js';33import { IEditorOpenContext } from '../../../common/editor.js';34import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js';35import { IWebview, IWebviewService } from '../../webview/browser/webview.js';36import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';37import { IExtensionService } from '../../../services/extensions/common/extensions.js';38import { IHoverService } from '../../../../platform/hover/browser/hover.js';39import { IMcpServerContainer, IMcpServerEditorOptions, IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers, McpServerInstallState } from '../common/mcpTypes.js';40import { StarredWidget, McpServerIconWidget, McpServerStatusWidget, McpServerWidget, onClick, PublisherWidget, McpServerScopeBadgeWidget } from './mcpServerWidgets.js';41import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction, McpServerStatusAction, UninstallAction } from './mcpServerActions.js';42import { McpServerEditorInput } from './mcpServerEditorInput.js';43import { ILocalMcpServer, IGalleryMcpServerConfiguration, IMcpServerPackage, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js';44import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';45import { McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';4647const enum McpServerEditorTab {48Readme = 'readme',49Configuration = 'configuration',50Manifest = 'manifest',51}5253function toDateString(date: Date) {54return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`;55}5657class NavBar extends Disposable {5859private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());60get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; }6162private _currentId: string | null = null;63get currentId(): string | null { return this._currentId; }6465private actions: Action[];66private actionbar: ActionBar;6768constructor(container: HTMLElement) {69super();70const element = append(container, $('.navbar'));71this.actions = [];72this.actionbar = this._register(new ActionBar(element));73}7475push(id: string, label: string, tooltip: string, index?: number): void {76const action = new Action(id, label, undefined, true, () => this.update(id, true));7778action.tooltip = tooltip;7980if (typeof index === 'number') {81this.actions.splice(index, 0, action);82} else {83this.actions.push(action);84}85this.actionbar.push(action, { index });8687if (this.actions.length === 1) {88this.update(id);89}90}9192remove(id: string): void {93const index = this.actions.findIndex(action => action.id === id);94if (index !== -1) {95this.actions.splice(index, 1);96this.actionbar.pull(index);97if (this._currentId === id) {98this.switch(this.actions[0]?.id);99}100}101}102103clear(): void {104this.actions = dispose(this.actions);105this.actionbar.clear();106}107108switch(id: string): boolean {109const action = this.actions.find(action => action.id === id);110if (action) {111action.run();112return true;113}114return false;115}116117has(id: string): boolean {118return this.actions.some(action => action.id === id);119}120121private update(id: string, focus?: boolean): void {122this._currentId = id;123this._onChange.fire({ id, focus: !!focus });124this.actions.forEach(a => a.checked = a.id === id);125}126}127128interface ILayoutParticipant {129layout(): void;130}131132interface IActiveElement {133focus(): void;134}135136interface IExtensionEditorTemplate {137name: HTMLElement;138description: HTMLElement;139actionsAndStatusContainer: HTMLElement;140actionBar: ActionBar;141navbar: NavBar;142content: HTMLElement;143header: HTMLElement;144mcpServer: IWorkbenchMcpServer;145}146147const enum WebviewIndex {148Readme,149Changelog150}151152export class McpServerEditor extends EditorPane {153154static readonly ID: string = 'workbench.editor.mcpServer';155156private readonly _scopedContextKeyService = this._register(new MutableDisposable<IScopedContextKeyService>());157private template: IExtensionEditorTemplate | undefined;158159private mcpServerReadme: Cache<string> | null;160private mcpServerManifest: Cache<IGalleryMcpServerConfiguration> | null;161162// Some action bar items use a webview whose vertical scroll position we track in this map163private initialScrollProgress: Map<WebviewIndex, number> = new Map();164165// Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed166private currentIdentifier: string = '';167168private layoutParticipants: ILayoutParticipant[] = [];169private readonly contentDisposables = this._register(new DisposableStore());170private readonly transientDisposables = this._register(new DisposableStore());171private activeElement: IActiveElement | null = null;172private dimension: Dimension | undefined;173174constructor(175group: IEditorGroup,176@ITelemetryService telemetryService: ITelemetryService,177@IInstantiationService private readonly instantiationService: IInstantiationService,178@IThemeService themeService: IThemeService,179@INotificationService private readonly notificationService: INotificationService,180@IOpenerService private readonly openerService: IOpenerService,181@IStorageService storageService: IStorageService,182@IExtensionService private readonly extensionService: IExtensionService,183@IWebviewService private readonly webviewService: IWebviewService,184@ILanguageService private readonly languageService: ILanguageService,185@IContextKeyService private readonly contextKeyService: IContextKeyService,186@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,187@IHoverService private readonly hoverService: IHoverService,188) {189super(McpServerEditor.ID, group, telemetryService, themeService, storageService);190this.mcpServerReadme = null;191this.mcpServerManifest = null;192}193194override get scopedContextKeyService(): IContextKeyService | undefined {195return this._scopedContextKeyService.value;196}197198protected createEditor(parent: HTMLElement): void {199const root = append(parent, $('.extension-editor.mcp-server-editor'));200this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);201this._scopedContextKeyService.value.createKey('inExtensionEditor', true);202203root.tabIndex = 0; // this is required for the focus tracker on the editor204root.style.outline = 'none';205root.setAttribute('role', 'document');206const header = append(root, $('.header'));207208const iconContainer = append(header, $('.icon-container'));209const iconWidget = this.instantiationService.createInstance(McpServerIconWidget, iconContainer);210const scopeWidget = this.instantiationService.createInstance(McpServerScopeBadgeWidget, iconContainer);211212const details = append(header, $('.details'));213const title = append(details, $('.title'));214const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 }));215this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name")));216217const subtitle = append(details, $('.subtitle'));218const subTitleEntryContainers: HTMLElement[] = [];219220const publisherContainer = append(subtitle, $('.subtitle-entry'));221subTitleEntryContainers.push(publisherContainer);222const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false);223224const starredContainer = append(subtitle, $('.subtitle-entry'));225subTitleEntryContainers.push(starredContainer);226const installCountWidget = this.instantiationService.createInstance(StarredWidget, starredContainer, false);227228const widgets: McpServerWidget[] = [229iconWidget,230publisherWidget,231installCountWidget,232scopeWidget,233];234235const description = append(details, $('.description'));236237const actions = [238this.instantiationService.createInstance(InstallAction, true),239this.instantiationService.createInstance(InstallingLabelAction),240this.instantiationService.createInstance(UninstallAction),241this.instantiationService.createInstance(ManageMcpServerAction, true),242];243244const actionsAndStatusContainer = append(details, $('.actions-status-container.mcp-server-actions'));245const actionBar = this._register(new ActionBar(actionsAndStatusContainer, {246actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {247if (action instanceof DropDownAction) {248return action.createActionViewItem(options);249}250return undefined;251},252focusOnlyEnabledItems: true253}));254255actionBar.push(actions, { icon: true, label: true });256actionBar.setFocusable(true);257// update focusable elements when the enablement of an action changes258this._register(Event.any(...actions.map(a => Event.filter(a.onDidChange, e => e.enabled !== undefined)))(() => {259actionBar.setFocusable(false);260actionBar.setFocusable(true);261}));262263const otherContainers: IMcpServerContainer[] = [];264const mcpServerStatusAction = this.instantiationService.createInstance(McpServerStatusAction);265const mcpServerStatusWidget = this._register(this.instantiationService.createInstance(McpServerStatusWidget, append(actionsAndStatusContainer, $('.status')), mcpServerStatusAction));266this._register(Event.any(mcpServerStatusWidget.onDidRender)(() => {267if (this.dimension) {268this.layout(this.dimension);269}270}));271272otherContainers.push(mcpServerStatusAction, new class extends McpServerWidget {273render() {274actionsAndStatusContainer.classList.toggle('list-layout', this.mcpServer?.installState === McpServerInstallState.Installed);275}276}());277278const mcpServerContainers: McpServerContainers = this.instantiationService.createInstance(McpServerContainers, [...actions, ...widgets, ...otherContainers]);279for (const disposable of [...actions, ...widgets, ...otherContainers, mcpServerContainers]) {280this._register(disposable);281}282283const onError = Event.chain(actionBar.onDidRun, $ =>284$.map(({ error }) => error)285.filter(error => !!error)286);287288this._register(onError(this.onError, this));289290const body = append(root, $('.body'));291const navbar = new NavBar(body);292293const content = append(body, $('.content'));294content.id = generateUuid(); // An id is needed for the webview parent flow to295296this.template = {297content,298description,299header,300name,301navbar,302actionsAndStatusContainer,303actionBar: actionBar,304set mcpServer(mcpServer: IWorkbenchMcpServer) {305mcpServerContainers.mcpServer = mcpServer;306let lastNonEmptySubtitleEntryContainer;307for (const subTitleEntryElement of subTitleEntryContainers) {308subTitleEntryElement.classList.remove('last-non-empty');309if (subTitleEntryElement.children.length > 0) {310lastNonEmptySubtitleEntryContainer = subTitleEntryElement;311}312}313if (lastNonEmptySubtitleEntryContainer) {314lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty');315}316}317};318}319320override async setInput(input: McpServerEditorInput, options: IMcpServerEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {321await super.setInput(input, options, context, token);322if (this.template) {323await this.render(input.mcpServer, this.template, !!options?.preserveFocus);324}325}326327private async render(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {328this.activeElement = null;329this.transientDisposables.clear();330331const token = this.transientDisposables.add(new CancellationTokenSource()).token;332333this.mcpServerReadme = new Cache(() => mcpServer.getReadme(token));334this.mcpServerManifest = new Cache(() => mcpServer.getManifest(token));335template.mcpServer = mcpServer;336337template.name.textContent = mcpServer.label;338template.name.classList.toggle('clickable', !!mcpServer.url);339template.description.textContent = mcpServer.description;340if (mcpServer.url) {341this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(mcpServer.url!))));342}343344this.renderNavbar(mcpServer, template, preserveFocus);345}346347override setOptions(options: IMcpServerEditorOptions | undefined): void {348super.setOptions(options);349if (options?.tab) {350this.template?.navbar.switch(options.tab);351}352}353354private renderNavbar(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, preserveFocus: boolean): void {355template.content.innerText = '';356template.navbar.clear();357358if (this.currentIdentifier !== extension.id) {359this.initialScrollProgress.clear();360this.currentIdentifier = extension.id;361}362363if (extension.readmeUrl || extension.gallery?.readme) {364template.navbar.push(McpServerEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file"));365}366367if (extension.gallery || extension.local?.manifest) {368template.navbar.push(McpServerEditorTab.Manifest, localize('manifest', "Manifest"), localize('manifesttooltip', "Server manifest details"));369}370371if (extension.config) {372template.navbar.push(McpServerEditorTab.Configuration, localize('configuration', "Configuration"), localize('configurationtooltip', "Server configuration details"));373}374375this.transientDisposables.add(this.mcpWorkbenchService.onChange(e => {376if (e === extension) {377if (e.config && !template.navbar.has(McpServerEditorTab.Configuration)) {378template.navbar.push(McpServerEditorTab.Configuration, localize('configuration', "Configuration"), localize('configurationtooltip', "Server configuration details"), extension.readmeUrl ? 1 : 0);379}380if (!e.config && template.navbar.has(McpServerEditorTab.Configuration)) {381template.navbar.remove(McpServerEditorTab.Configuration);382}383}384}));385386if ((<IMcpServerEditorOptions | undefined>this.options)?.tab) {387template.navbar.switch((<IMcpServerEditorOptions>this.options).tab!);388}389390if (template.navbar.currentId) {391this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template);392}393template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables);394}395396override clearInput(): void {397this.contentDisposables.clear();398this.transientDisposables.clear();399400super.clearInput();401}402403override focus(): void {404super.focus();405this.activeElement?.focus();406}407408showFind(): void {409this.activeWebview?.showFind();410}411412runFindAction(previous: boolean): void {413this.activeWebview?.runFindAction(previous);414}415416public get activeWebview(): IWebview | undefined {417if (!this.activeElement || !(this.activeElement as IWebview).runFindAction) {418return undefined;419}420return this.activeElement as IWebview;421}422423private onNavbarChange(extension: IWorkbenchMcpServer, { id, focus }: { id: string | null; focus: boolean }, template: IExtensionEditorTemplate): void {424this.contentDisposables.clear();425template.content.innerText = '';426this.activeElement = null;427if (id) {428const cts = new CancellationTokenSource();429this.contentDisposables.add(toDisposable(() => cts.dispose(true)));430this.open(id, extension, template, cts.token)431.then(activeElement => {432if (cts.token.isCancellationRequested) {433return;434}435this.activeElement = activeElement;436if (focus) {437this.focus();438}439});440}441}442443private open(id: string, extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {444switch (id) {445case McpServerEditorTab.Configuration: return this.openConfiguration(extension, template, token);446case McpServerEditorTab.Readme: return this.openDetails(extension, template, token);447case McpServerEditorTab.Manifest: return extension.readmeUrl ? this.openManifest(extension, template.content, token) : this.openManifestWithAdditionalDetails(extension, template, token);448}449return Promise.resolve(null);450}451452private async openMarkdown(extension: IWorkbenchMcpServer, cacheResult: CacheResult<string>, noContentCopy: string, container: HTMLElement, webviewIndex: WebviewIndex, title: string, token: CancellationToken): Promise<IActiveElement | null> {453try {454const body = await this.renderMarkdown(extension, cacheResult, container, token);455if (token.isCancellationRequested) {456return Promise.resolve(null);457}458459const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay({460title,461options: {462enableFindWidget: true,463tryRestoreScrollPosition: true,464disableServiceWorker: true,465},466contentOptions: {},467extension: undefined,468}));469470webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0;471472webview.claim(this, this.window, this.scopedContextKeyService);473setParentFlowTo(webview.container, container);474webview.layoutWebviewOverElement(container);475476webview.setHtml(body);477webview.claim(this, this.window, undefined);478479this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire()));480481this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress)));482483const removeLayoutParticipant = arrays.insert(this.layoutParticipants, {484layout: () => {485webview.layoutWebviewOverElement(container);486}487});488this.contentDisposables.add(toDisposable(removeLayoutParticipant));489490let isDisposed = false;491this.contentDisposables.add(toDisposable(() => { isDisposed = true; }));492493this.contentDisposables.add(this.themeService.onDidColorThemeChange(async () => {494// Render again since syntax highlighting of code blocks may have changed495const body = await this.renderMarkdown(extension, cacheResult, container);496if (!isDisposed) { // Make sure we weren't disposed of in the meantime497webview.setHtml(body);498}499}));500501this.contentDisposables.add(webview.onDidClickLink(link => {502if (!link) {503return;504}505// Only allow links with specific schemes506if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) {507this.openerService.open(link);508}509}));510511return webview;512} catch (e) {513const p = append(container, $('p.nocontent'));514p.textContent = noContentCopy;515return p;516}517}518519private async renderMarkdown(extension: IWorkbenchMcpServer, cacheResult: CacheResult<string>, container: HTMLElement, token?: CancellationToken): Promise<string> {520const contents = await this.loadContents(() => cacheResult, container);521if (token?.isCancellationRequested) {522return '';523}524525const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, {}, token);526if (token?.isCancellationRequested) {527return '';528}529530return this.renderBody(content);531}532533private renderBody(body: TrustedHTML): string {534const nonce = generateUuid();535const colorMap = TokenizationRegistry.getColorMap();536const css = colorMap ? generateTokensCSSForColorMap(colorMap) : '';537return `<!DOCTYPE html>538<html>539<head>540<meta http-equiv="Content-type" content="text/html;charset=UTF-8">541<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src 'nonce-${nonce}';">542<style nonce="${nonce}">543${DEFAULT_MARKDOWN_STYLES}544545/* prevent scroll-to-top button from blocking the body text */546body {547padding-bottom: 75px;548}549550#scroll-to-top {551position: fixed;552width: 32px;553height: 32px;554right: 25px;555bottom: 25px;556background-color: var(--vscode-button-secondaryBackground);557border-color: var(--vscode-button-border);558border-radius: 50%;559cursor: pointer;560box-shadow: 1px 1px 1px rgba(0,0,0,.25);561outline: none;562display: flex;563justify-content: center;564align-items: center;565}566567#scroll-to-top:hover {568background-color: var(--vscode-button-secondaryHoverBackground);569box-shadow: 2px 2px 2px rgba(0,0,0,.25);570}571572body.vscode-high-contrast #scroll-to-top {573border-width: 2px;574border-style: solid;575box-shadow: none;576}577578#scroll-to-top span.icon::before {579content: "";580display: block;581background: var(--vscode-button-secondaryForeground);582/* Chevron up icon */583webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');584-webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');585width: 16px;586height: 16px;587}588${css}589</style>590</head>591<body>592<a id="scroll-to-top" role="button" aria-label="scroll to top" href="#"><span class="icon"></span></a>593${body}594</body>595</html>`;596}597598private async openDetails(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {599const details = append(template.content, $('.details'));600const readmeContainer = append(details, $('.readme-container'));601const additionalDetailsContainer = append(details, $('.additional-details-container'));602603const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500);604layout();605this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));606607const activeElement = await this.openMarkdown(extension, this.mcpServerReadme!.get(), localize('noReadme', "No README available."), readmeContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token);608this.renderAdditionalDetails(additionalDetailsContainer, extension);609return activeElement;610}611612private async openConfiguration(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {613const configContainer = append(template.content, $('.configuration'));614const content = $('div', { class: 'configuration-content' });615616this.renderConfigurationDetails(content, mcpServer);617618const scrollableContent = new DomScrollableElement(content, {});619const layout = () => scrollableContent.scanDomNode();620this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));621622append(configContainer, scrollableContent.getDomNode());623624return { focus: () => content.focus() };625}626627private async openManifestWithAdditionalDetails(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {628const details = append(template.content, $('.details'));629630const readmeContainer = append(details, $('.readme-container'));631const additionalDetailsContainer = append(details, $('.additional-details-container'));632633const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500);634layout();635this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));636637const activeElement = await this.openManifest(mcpServer, readmeContainer, token);638639this.renderAdditionalDetails(additionalDetailsContainer, mcpServer);640return activeElement;641}642643private async openManifest(mcpServer: IWorkbenchMcpServer, parent: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {644const manifestContainer = append(parent, $('.manifest'));645const content = $('div', { class: 'manifest-content' });646647try {648const manifest = await this.loadContents(() => this.mcpServerManifest!.get(), content);649if (token.isCancellationRequested) {650return null;651}652this.renderManifestDetails(content, manifest);653} catch (error) {654// Handle error - show no manifest message655while (content.firstChild) {656content.removeChild(content.firstChild);657}658const noManifestMessage = append(content, $('.no-manifest'));659noManifestMessage.textContent = localize('noManifest', "No manifest available for this MCP server.");660}661662const scrollableContent = new DomScrollableElement(content, {});663const layout = () => scrollableContent.scanDomNode();664this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));665666append(manifestContainer, scrollableContent.getDomNode());667668return { focus: () => content.focus() };669}670671private renderConfigurationDetails(container: HTMLElement, mcpServer: IWorkbenchMcpServer): void {672clearNode(container);673674const config = mcpServer.config;675676if (!config) {677const noConfigMessage = append(container, $('.no-config'));678noConfigMessage.textContent = localize('noConfig', "No configuration available for this MCP server.");679return;680}681682// Server Name683const nameSection = append(container, $('.config-section'));684const nameLabel = append(nameSection, $('.config-label'));685nameLabel.textContent = localize('serverName', "Name:");686const nameValue = append(nameSection, $('.config-value'));687nameValue.textContent = mcpServer.name;688689// Server Type690const typeSection = append(container, $('.config-section'));691const typeLabel = append(typeSection, $('.config-label'));692typeLabel.textContent = localize('serverType', "Type:");693const typeValue = append(typeSection, $('.config-value'));694typeValue.textContent = config.type;695696// Type-specific configuration697if (config.type === McpServerType.LOCAL) {698// Command699const commandSection = append(container, $('.config-section'));700const commandLabel = append(commandSection, $('.config-label'));701commandLabel.textContent = localize('command', "Command:");702const commandValue = append(commandSection, $('code.config-value'));703commandValue.textContent = config.command;704705// Arguments (if present)706if (config.args && config.args.length > 0) {707const argsSection = append(container, $('.config-section'));708const argsLabel = append(argsSection, $('.config-label'));709argsLabel.textContent = localize('arguments', "Arguments:");710const argsValue = append(argsSection, $('code.config-value'));711argsValue.textContent = config.args.join(' ');712}713} else if (config.type === McpServerType.REMOTE) {714// URL715const urlSection = append(container, $('.config-section'));716const urlLabel = append(urlSection, $('.config-label'));717urlLabel.textContent = localize('url', "URL:");718const urlValue = append(urlSection, $('code.config-value'));719urlValue.textContent = config.url;720}721}722723private renderManifestDetails(container: HTMLElement, manifest: IGalleryMcpServerConfiguration): void {724clearNode(container);725726if (manifest.packages && manifest.packages.length > 0) {727const packagesByType = new Map<RegistryType, IMcpServerPackage[]>();728for (const pkg of manifest.packages) {729const type = pkg.registry_type;730let packages = packagesByType.get(type);731if (!packages) {732packagesByType.set(type, packages = []);733}734packages.push(pkg);735}736737append(container, $('.manifest-section', undefined, $('.manifest-section-title', undefined, localize('packages', "Packages"))));738739for (const [packageType, packages] of packagesByType) {740const packageSection = append(container, $('.package-section', undefined, $('.package-section-title', undefined, packageType.toUpperCase())));741const packagesGrid = append(packageSection, $('.package-details'));742743for (let i = 0; i < packages.length; i++) {744const pkg = packages[i];745append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('packageName', "Package:")), $('.detail-value', undefined, pkg.identifier)));746if (pkg.package_arguments && pkg.package_arguments.length > 0) {747const argStrings: string[] = [];748for (const arg of pkg.package_arguments) {749if (arg.type === 'named') {750argStrings.push(arg.name);751if (arg.value) {752argStrings.push(arg.value);753}754}755if (arg.type === 'positional') {756argStrings.push(arg.value ?? arg.value_hint);757}758}759append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('packagearguments', "Package Arguments:")), $('code.detail-value', undefined, argStrings.join(' '))));760}761if (pkg.runtime_arguments && pkg.runtime_arguments.length > 0) {762const argStrings: string[] = [];763for (const arg of pkg.runtime_arguments) {764if (arg.type === 'named') {765argStrings.push(arg.name);766if (arg.value) {767argStrings.push(arg.value);768}769}770if (arg.type === 'positional') {771argStrings.push(arg.value ?? arg.value_hint);772}773}774append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('runtimeargs', "Runtime Arguments:")), $('code.detail-value', undefined, argStrings.join(' '))));775}776if (pkg.environment_variables && pkg.environment_variables.length > 0) {777const envStrings = pkg.environment_variables.map((envVar: any) => `${envVar.name}=${envVar.value}`);778append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('environmentVariables', "Environment Variables:")), $('code.detail-value', undefined, envStrings.join(' '))));779}780if (i < packages.length - 1) {781append(packagesGrid, $('.package-separator'));782}783}784}785}786787if (manifest.remotes && manifest.remotes.length > 0) {788const packageSection = append(container, $('.package-section', undefined, $('.package-section-title', undefined, localize('remotes', "Remote").toLocaleUpperCase())));789for (const remote of manifest.remotes) {790const packagesGrid = append(packageSection, $('.package-details'));791append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('url', "URL:")), $('.detail-value', undefined, remote.url)));792if (remote.transport_type) {793append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('transport', "Transport:")), $('.detail-value', undefined, remote.transport_type)));794}795if (remote.headers && remote.headers.length > 0) {796const headerStrings = remote.headers.map((header: any) => `${header.name}: ${header.value}`);797append(packagesGrid, $('.package-detail', undefined, $('.detail-label', undefined, localize('headers', "Headers:")), $('.detail-value', undefined, headerStrings.join(', '))));798}799}800}801}802803private renderAdditionalDetails(container: HTMLElement, extension: IWorkbenchMcpServer): void {804const content = $('div', { class: 'additional-details-content', tabindex: '0' });805const scrollableContent = new DomScrollableElement(content, {});806const layout = () => scrollableContent.scanDomNode();807const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });808this.contentDisposables.add(toDisposable(removeLayoutParticipant));809this.contentDisposables.add(scrollableContent);810811this.contentDisposables.add(this.instantiationService.createInstance(AdditionalDetailsWidget, content, extension));812813append(container, scrollableContent.getDomNode());814scrollableContent.scanDomNode();815}816817private loadContents<T>(loadingTask: () => CacheResult<T>, container: HTMLElement): Promise<T> {818container.classList.add('loading');819820const result = this.contentDisposables.add(loadingTask());821const onDone = () => container.classList.remove('loading');822result.promise.then(onDone, onDone);823824return result.promise;825}826827layout(dimension: Dimension): void {828this.dimension = dimension;829this.layoutParticipants.forEach(p => p.layout());830}831832private onError(err: any): void {833if (isCancellationError(err)) {834return;835}836837this.notificationService.error(err);838}839}840841class AdditionalDetailsWidget extends Disposable {842843private readonly disposables = this._register(new DisposableStore());844845constructor(846private readonly container: HTMLElement,847extension: IWorkbenchMcpServer,848@IHoverService private readonly hoverService: IHoverService,849@IOpenerService private readonly openerService: IOpenerService,850) {851super();852this.render(extension);853}854855private render(extension: IWorkbenchMcpServer): void {856this.container.innerText = '';857this.disposables.clear();858859if (extension.local) {860this.renderInstallInfo(this.container, extension.local);861}862863if (extension.gallery) {864this.renderMarketplaceInfo(this.container, extension);865}866this.renderTopics(this.container, extension);867this.renderExtensionResources(this.container, extension);868}869870private renderTopics(container: HTMLElement, extension: IWorkbenchMcpServer): void {871if (extension.gallery?.topics?.length) {872const categoriesContainer = append(container, $('.categories-container.additional-details-element'));873append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories")));874const categoriesElement = append(categoriesContainer, $('.categories'));875for (const category of extension.gallery.topics) {876append(categoriesElement, $('span.category', { tabindex: '0' }, category));877}878}879}880881private renderExtensionResources(container: HTMLElement, extension: IWorkbenchMcpServer): void {882const resources: [string, URI][] = [];883if (extension.repository) {884try {885resources.push([localize('repository', "Repository"), URI.parse(extension.repository)]);886} catch (error) {/* Ignore */ }887}888if (extension.publisherUrl && extension.publisherDisplayName) {889resources.push([extension.publisherDisplayName, URI.parse(extension.publisherUrl)]);890}891if (resources.length) {892const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element'));893append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources")));894const resourcesElement = append(extensionResourcesContainer, $('.resources'));895for (const [label, uri] of resources) {896const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label));897this.disposables.add(onClick(resource, () => this.openerService.open(uri)));898this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString()));899}900}901}902903private renderInstallInfo(container: HTMLElement, extension: ILocalMcpServer): void {904const installInfoContainer = append(container, $('.more-info-container.additional-details-element'));905append(installInfoContainer, $('.additional-details-title', undefined, localize('Install Info', "Installation")));906const installInfo = append(installInfoContainer, $('.more-info'));907append(installInfo,908$('.more-info-entry', undefined,909$('div.more-info-entry-name', undefined, localize('id', "Identifier")),910$('code', undefined, extension.name)911));912if (extension.version) {913append(installInfo,914$('.more-info-entry', undefined,915$('div.more-info-entry-name', undefined, localize('Version', "Version")),916$('code', undefined, extension.version)917)918);919}920}921922private renderMarketplaceInfo(container: HTMLElement, extension: IWorkbenchMcpServer): void {923const gallery = extension.gallery;924const moreInfoContainer = append(container, $('.more-info-container.additional-details-element'));925append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace")));926const moreInfo = append(moreInfoContainer, $('.more-info'));927if (gallery) {928if (!extension.local) {929append(moreInfo,930$('.more-info-entry', undefined,931$('div.more-info-entry-name', undefined, localize('id', "Identifier")),932$('code', undefined, extension.name)933));934if (gallery.version) {935append(moreInfo,936$('.more-info-entry', undefined,937$('div.more-info-entry-name', undefined, localize('Version', "Version")),938$('code', undefined, gallery.version)939)940);941}942}943if (gallery.publishDate) {944append(moreInfo,945$('.more-info-entry', undefined,946$('div.more-info-entry-name', undefined, localize('published', "Published")),947$('div', undefined, toDateString(new Date(gallery.publishDate)))948)949);950}951if (gallery.releaseDate) {952append(moreInfo,953$('.more-info-entry', undefined,954$('div.more-info-entry-name', undefined, localize('released', "Released")),955$('div', undefined, toDateString(new Date(gallery.releaseDate)))956)957);958}959if (gallery.lastUpdated) {960append(moreInfo,961$('.more-info-entry', undefined,962$('div.more-info-entry-name', undefined, localize('last released', "Last Released")),963$('div', undefined, toDateString(new Date(gallery.lastUpdated)))964)965);966}967}968}969}970971972