Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts
5250 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 { mapFindFirst } from '../../../../base/common/arraysFind.js';6import { assertNever } from '../../../../base/common/assert.js';7import { disposableTimeout } from '../../../../base/common/async.js';8import { parse as parseJsonc } from '../../../../base/common/jsonc.js';9import { DisposableStore } from '../../../../base/common/lifecycle.js';10import { Schemas } from '../../../../base/common/network.js';11import { autorun } from '../../../../base/common/observable.js';12import { basename } from '../../../../base/common/resources.js';13import { URI } from '../../../../base/common/uri.js';14import { generateUuid } from '../../../../base/common/uuid.js';15import { localize } from '../../../../nls.js';16import { ICommandService } from '../../../../platform/commands/common/commands.js';17import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { ILabelService } from '../../../../platform/label/common/label.js';20import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';21import { IGalleryMcpServerConfiguration, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js';22import { INotificationService } from '../../../../platform/notification/common/notification.js';23import { IOpenerService } from '../../../../platform/opener/common/opener.js';24import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js';25import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';26import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';27import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';28import { IEditorService } from '../../../services/editor/common/editorService.js';29import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';30import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';31import { McpCommandIds } from '../common/mcpCommandIds.js';32import { allDiscoverySources, DiscoverySource, mcpDiscoverySection, mcpStdioServerSchema } from '../common/mcpConfiguration.js';33import { IMcpRegistry } from '../common/mcpRegistryTypes.js';34import { IMcpService, McpConnectionState } from '../common/mcpTypes.js';35import { ILogService } from '../../../../platform/log/common/log.js';3637export const enum AddConfigurationType {38Stdio,39HTTP,4041NpmPackage,42PipPackage,43NuGetPackage,44DockerImage,45}4647type AssistedConfigurationType = AddConfigurationType.NpmPackage | AddConfigurationType.PipPackage | AddConfigurationType.NuGetPackage | AddConfigurationType.DockerImage;4849export const AssistedTypes = {50[AddConfigurationType.NpmPackage]: {51title: localize('mcp.npm.title', "Enter NPM Package Name"),52placeholder: localize('mcp.npm.placeholder', "Package name (e.g., @org/package)"),53pickLabel: localize('mcp.serverType.npm', "NPM Package"),54pickDescription: localize('mcp.serverType.npm.description', "Install from an NPM package name"),55enabledConfigKey: null, // always enabled56},57[AddConfigurationType.PipPackage]: {58title: localize('mcp.pip.title', "Enter Pip Package Name"),59placeholder: localize('mcp.pip.placeholder', "Package name (e.g., package-name)"),60pickLabel: localize('mcp.serverType.pip', "Pip Package"),61pickDescription: localize('mcp.serverType.pip.description', "Install from a Pip package name"),62enabledConfigKey: null, // always enabled63},64[AddConfigurationType.NuGetPackage]: {65title: localize('mcp.nuget.title', "Enter NuGet Package Name"),66placeholder: localize('mcp.nuget.placeholder', "Package name (e.g., Package.Name)"),67pickLabel: localize('mcp.serverType.nuget', "NuGet Package"),68pickDescription: localize('mcp.serverType.nuget.description', "Install from a NuGet package name"),69enabledConfigKey: 'chat.mcp.assisted.nuget.enabled',70},71[AddConfigurationType.DockerImage]: {72title: localize('mcp.docker.title', "Enter Docker Image Name"),73placeholder: localize('mcp.docker.placeholder', "Image name (e.g., mcp/imagename)"),74pickLabel: localize('mcp.serverType.docker', "Docker Image"),75pickDescription: localize('mcp.serverType.docker.description', "Install from a Docker image"),76enabledConfigKey: null, // always enabled77},78};7980const enum AddConfigurationCopilotCommand {81/** Returns whether MCP enhanced setup is enabled. */82IsSupported = 'github.copilot.chat.mcp.setup.check',8384/** Takes an npm/pip package name, validates its owner. */85ValidatePackage = 'github.copilot.chat.mcp.setup.validatePackage',8687/** Returns the resolved MCP configuration. */88StartFlow = 'github.copilot.chat.mcp.setup.flow',89}9091type ValidatePackageResult =92{ state: 'ok'; publisher: string; name?: string; version?: string }93| { state: 'error'; error: string; helpUri?: string; helpUriLabel?: string };9495type AddServerData = {96packageType: string;97};98type AddServerClassification = {99owner: 'digitarald';100comment: 'Generic details for adding a new MCP server';101packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' };102};103type AddServerCompletedData = {104packageType: string;105serverType: string | undefined;106target: string;107};108type AddServerCompletedClassification = {109owner: 'digitarald';110comment: 'Generic details for successfully adding model-assisted MCP server';111packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' };112serverType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server' };113target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The target of the MCP server configuration' };114};115116type AssistedServerConfiguration = {117type?: 'assisted';118name?: string;119server: Omit<IMcpStdioServerConfiguration, 'type'>;120inputs?: IMcpServerVariable[];121inputValues?: Record<string, string>;122} | {123type: 'mapped';124name?: string;125server: Omit<IMcpStdioServerConfiguration, 'type'>;126inputs?: IMcpServerVariable[];127};128129export class McpAddConfigurationCommand {130constructor(131private readonly workspaceFolder: IWorkspaceFolder | undefined,132@IQuickInputService private readonly _quickInputService: IQuickInputService,133@IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService,134@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,135@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,136@ICommandService private readonly _commandService: ICommandService,137@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,138@IOpenerService private readonly _openerService: IOpenerService,139@IEditorService private readonly _editorService: IEditorService,140@IFileService private readonly _fileService: IFileService,141@INotificationService private readonly _notificationService: INotificationService,142@ITelemetryService private readonly _telemetryService: ITelemetryService,143@IMcpService private readonly _mcpService: IMcpService,144@ILabelService private readonly _label: ILabelService,145@IConfigurationService private readonly _configurationService: IConfigurationService,146) { }147148private async getServerType(): Promise<AddConfigurationType | undefined> {149type TItem = { kind: AddConfigurationType | 'browse' | 'discovery' } & IQuickPickItem;150const items: QuickPickInput<TItem>[] = [151{ kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") },152{ kind: AddConfigurationType.HTTP, label: localize('mcp.serverType.http', "HTTP (HTTP or Server-Sent Events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") }153];154155let aiSupported: boolean | undefined;156try {157aiSupported = await this._commandService.executeCommand<boolean>(AddConfigurationCopilotCommand.IsSupported);158} catch {159// ignored160}161162if (aiSupported) {163items.unshift({ type: 'separator', label: localize('mcp.serverType.manual', "Manual Install") });164165const elligableTypes = Object.entries(AssistedTypes).map(([type, { pickLabel, pickDescription, enabledConfigKey }]) => {166if (enabledConfigKey) {167const enabled = this._configurationService.getValue<boolean>(enabledConfigKey) ?? false;168if (!enabled) {169return;170}171}172return {173kind: Number(type) as AddConfigurationType,174label: pickLabel,175description: pickDescription,176};177}).filter(x => !!x);178179items.push(180{ type: 'separator', label: localize('mcp.serverType.copilot', "Model-Assisted") },181...elligableTypes182);183}184185items.push({ type: 'separator' });186187const discovery = this._configurationService.getValue<{ [K in DiscoverySource]: boolean }>(mcpDiscoverySection);188if (discovery && typeof discovery === 'object' && allDiscoverySources.some(d => !discovery[d])) {189items.push({190kind: 'discovery',191label: localize('mcp.servers.discovery', "Add from another application..."),192});193}194195items.push({196kind: 'browse',197label: localize('mcp.servers.browse', "Browse MCP Servers..."),198});199200const result = await this._quickInputService.pick<TItem>(items, {201placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add"),202});203204if (result?.kind === 'browse') {205this._commandService.executeCommand(McpCommandIds.Browse);206return undefined;207}208209if (result?.kind === 'discovery') {210this._commandService.executeCommand('workbench.action.openSettings', mcpDiscoverySection);211return undefined;212}213214return result?.kind;215}216217private async getStdioConfig(): Promise<IMcpStdioServerConfiguration | undefined> {218const command = await this._quickInputService.input({219title: localize('mcp.command.title', "Enter Command"),220placeHolder: localize('mcp.command.placeholder', "Command to run (with optional arguments)"),221ignoreFocusLost: true,222});223224if (!command) {225return undefined;226}227228this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {229packageType: 'stdio'230});231232// Split command into command and args, handling quotes233const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g)!;234return {235type: McpServerType.LOCAL,236command: parts[0].replace(/"/g, ''),237238args: parts.slice(1).map(arg => arg.replace(/"/g, ''))239};240}241242private async getSSEConfig(): Promise<IMcpRemoteServerConfiguration | undefined> {243const url = await this._quickInputService.input({244title: localize('mcp.url.title', "Enter Server URL"),245placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"),246ignoreFocusLost: true,247});248249if (!url) {250return undefined;251}252253this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {254packageType: 'sse'255});256257return { url, type: McpServerType.REMOTE };258}259260private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise<string | undefined> {261const id = await this._quickInputService.input({262title: localize('mcp.serverId.title', "Enter Server ID"),263placeHolder: localize('mcp.serverId.placeholder', "Unique identifier for this server"),264value: suggestion,265ignoreFocusLost: true,266});267268return id;269}270271private async getConfigurationTarget(): Promise<ConfigurationTarget | IWorkspaceFolder | undefined> {272const options: (IQuickPickItem & { target?: ConfigurationTarget | IWorkspaceFolder })[] = [273{ target: ConfigurationTarget.USER_LOCAL, label: localize('mcp.target.user', "Global"), description: localize('mcp.target.user.description', "Available in all workspaces, runs locally") }274];275276const raLabel = this._environmentService.remoteAuthority && this._label.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);277if (raLabel) {278options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote"), description: localize('mcp.target..remote.description', "Available on this remote machine, runs on {0}", raLabel) });279}280281const workbenchState = this._workspaceService.getWorkbenchState();282if (workbenchState !== WorkbenchState.EMPTY) {283const target = workbenchState === WorkbenchState.FOLDER ? this._workspaceService.getWorkspace().folders[0] : ConfigurationTarget.WORKSPACE;284if (this._environmentService.remoteAuthority) {285options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description.remote', "Available in this workspace, runs on {0}", raLabel) });286} else {287options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description', "Available in this workspace, runs locally") });288}289}290291if (options.length === 1) {292return options[0].target;293}294295const targetPick = await this._quickInputService.pick(options, {296title: localize('mcp.target.title', "Add MCP Server"),297placeHolder: localize('mcp.target.placeholder', "Select the configuration target")298});299300return targetPick?.target;301}302303private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name?: string; server: Omit<IMcpStdioServerConfiguration, 'type'>; inputs?: IMcpServerVariable[]; inputValues?: Record<string, string> } | undefined> {304const packageName = await this._quickInputService.input({305ignoreFocusLost: true,306title: AssistedTypes[type].title,307placeHolder: AssistedTypes[type].placeholder,308});309310if (!packageName) {311return undefined;312}313314const enum LoadAction {315Retry = 'retry',316Cancel = 'cancel',317Allow = 'allow',318OpenUri = 'openUri',319}320321const loadingQuickPickStore = new DisposableStore();322const loadingQuickPick = loadingQuickPickStore.add(this._quickInputService.createQuickPick<IQuickPickItem & { id: LoadAction; helpUri?: URI }>());323loadingQuickPick.title = localize('mcp.loading.title', "Loading package details...");324loadingQuickPick.busy = true;325loadingQuickPick.ignoreFocusOut = true;326327const packageType = this.getPackageType(type);328329this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {330packageType: packageType!331});332333this._commandService.executeCommand<ValidatePackageResult>(334AddConfigurationCopilotCommand.ValidatePackage,335{336type: packageType,337name: packageName,338targetConfig: {339...mcpStdioServerSchema,340properties: {341...mcpStdioServerSchema.properties,342name: {343type: 'string',344description: 'Suggested name of the server, alphanumeric and hyphen only',345}346},347required: [...(mcpStdioServerSchema.required || []), 'name'],348},349}350).then(result => {351if (!result || result.state === 'error') {352loadingQuickPick.title = result?.error || 'Unknown error loading package';353354const items: Array<IQuickPickItem & { id: LoadAction; helpUri?: URI }> = [];355356if (result?.helpUri) {357items.push({358id: LoadAction.OpenUri,359label: result.helpUriLabel ?? localize('mcp.error.openHelpUri', 'Open help URL'),360helpUri: URI.parse(result.helpUri),361});362}363364items.push(365{ id: LoadAction.Retry, label: localize('mcp.error.retry', 'Try a different package') },366{ id: LoadAction.Cancel, label: localize('cancel', 'Cancel') },367);368369loadingQuickPick.items = items;370} else {371loadingQuickPick.title = localize(372'mcp.confirmPublish', 'Install {0}{1} from {2}?',373result.name ?? packageName,374result.version ? `@${result.version}` : '',375result.publisher);376loadingQuickPick.items = [377{ id: LoadAction.Allow, label: localize('allow', "Allow") },378{ id: LoadAction.Cancel, label: localize('cancel', 'Cancel') }379];380}381loadingQuickPick.busy = false;382});383384const loadingAction = await new Promise<{ id: LoadAction; helpUri?: URI } | undefined>(resolve => {385loadingQuickPickStore.add(loadingQuickPick.onDidAccept(() => resolve(loadingQuickPick.selectedItems[0])));386loadingQuickPickStore.add(loadingQuickPick.onDidHide(() => resolve(undefined)));387loadingQuickPick.show();388}).finally(() => loadingQuickPickStore.dispose());389390switch (loadingAction?.id) {391case LoadAction.Retry:392return this.getAssistedConfig(type);393case LoadAction.OpenUri:394if (loadingAction.helpUri) { this._openerService.open(loadingAction.helpUri); }395return undefined;396case LoadAction.Allow:397break;398case LoadAction.Cancel:399default:400return undefined;401}402403const config = await this._commandService.executeCommand<AssistedServerConfiguration>(404AddConfigurationCopilotCommand.StartFlow,405{406name: packageName,407type: packageType408}409);410411if (config?.type === 'mapped') {412return {413name: config.name,414server: config.server,415inputs: config.inputs,416};417} else if (config?.type === 'assisted' || !config?.type) {418return config;419} else {420assertNever(config?.type);421}422}423424/** Shows the location of a server config once it's discovered. */425private showOnceDiscovered(name: string) {426const store = new DisposableStore();427store.add(autorun(reader => {428const colls = this._mcpRegistry.collections.read(reader);429const servers = this._mcpService.servers.read(reader);430const match = mapFindFirst(colls, collection => mapFindFirst(collection.serverDefinitions.read(reader),431server => server.label === name ? { server, collection } : undefined));432const server = match && servers.find(s => s.definition.id === match.server.id);433434435if (match && server) {436if (match.collection.presentation?.origin) {437this._editorService.openEditor({438resource: match.collection.presentation.origin,439options: {440selection: match.server.presentation?.origin?.range,441preserveFocus: true,442}443});444} else {445this._commandService.executeCommand(McpCommandIds.ServerOptions, name);446}447448server.start({ promptType: 'all-untrusted' }).then(state => {449if (state.state === McpConnectionState.Kind.Error) {450server.showOutput();451}452});453454store.dispose();455}456}));457458store.add(disposableTimeout(() => store.dispose(), 5000));459}460461public async run(): Promise<void> {462// Step 1: Choose server type463const serverType = await this.getServerType();464if (serverType === undefined) {465return;466}467468// Step 2: Get server details based on type469let config: IMcpServerConfiguration | undefined;470let suggestedName: string | undefined;471let inputs: IMcpServerVariable[] | undefined;472let inputValues: Record<string, string> | undefined;473switch (serverType) {474case AddConfigurationType.Stdio:475config = await this.getStdioConfig();476break;477case AddConfigurationType.HTTP:478config = await this.getSSEConfig();479break;480case AddConfigurationType.NpmPackage:481case AddConfigurationType.PipPackage:482case AddConfigurationType.NuGetPackage:483case AddConfigurationType.DockerImage: {484const r = await this.getAssistedConfig(serverType);485config = r?.server ? { ...r.server, type: McpServerType.LOCAL } : undefined;486suggestedName = r?.name;487inputs = r?.inputs;488inputValues = r?.inputValues;489break;490}491default:492assertNever(serverType);493}494495if (!config) {496return;497}498499// Step 3: Get server ID500const name = await this.getServerId(suggestedName);501if (!name) {502return;503}504505// Step 4: Choose configuration target if no configUri provided506let target: ConfigurationTarget | IWorkspaceFolder | undefined = this.workspaceFolder;507if (!target) {508target = await this.getConfigurationTarget();509if (!target) {510return;511}512}513514await this._mcpManagementService.install({ name, config, inputs }, { target });515516if (inputValues) {517for (const [key, value] of Object.entries(inputValues)) {518await this._mcpRegistry.setSavedInput(key, (isWorkspaceFolder(target) ? ConfigurationTarget.WORKSPACE_FOLDER : target) ?? ConfigurationTarget.WORKSPACE, value);519}520}521522const packageType = this.getPackageType(serverType);523if (packageType) {524this._telemetryService.publicLog2<AddServerCompletedData, AddServerCompletedClassification>('mcp.addserver.completed', {525packageType,526serverType: config.type,527target: target === ConfigurationTarget.WORKSPACE ? 'workspace' : 'user'528});529}530531this.showOnceDiscovered(name);532}533534public async pickForUrlHandler(resource: URI, showIsPrimary = false): Promise<void> {535const name = decodeURIComponent(basename(resource)).replace(/\.json$/, '');536const placeHolder = localize('install.title', 'Install MCP server {0}', name);537538const items: IQuickPickItem[] = [539{ id: 'install', label: localize('install.start', 'Install Server') },540{ id: 'show', label: localize('install.show', 'Show Configuration', name) },541{ id: 'rename', label: localize('install.rename', 'Rename "{0}"', name) },542{ id: 'cancel', label: localize('cancel', 'Cancel') },543];544if (showIsPrimary) {545[items[0], items[1]] = [items[1], items[0]];546}547548const pick = await this._quickInputService.pick(items, { placeHolder, ignoreFocusLost: true });549const getEditors = () => this._editorService.findEditors(resource);550551switch (pick?.id) {552case 'show':553await this._editorService.openEditor({ resource });554break;555case 'install':556await this._editorService.save(getEditors());557try {558const contents = await this._fileService.readFile(resource);559const { inputs, ...config }: IMcpServerConfiguration & { inputs?: IMcpServerVariable[] } = parseJsonc(contents.value.toString());560await this._mcpManagementService.install({ name, config, inputs });561this._editorService.closeEditors(getEditors());562this.showOnceDiscovered(name);563} catch (e) {564this._notificationService.error(localize('install.error', 'Error installing MCP server {0}: {1}', name, e.message));565await this._editorService.openEditor({ resource });566}567break;568case 'rename': {569const newName = await this._quickInputService.input({ placeHolder: localize('install.newName', 'Enter new name'), value: name });570if (newName) {571const newURI = resource.with({ path: `/${encodeURIComponent(newName)}.json` });572await this._editorService.save(getEditors());573await this._fileService.move(resource, newURI);574return this.pickForUrlHandler(newURI, showIsPrimary);575}576break;577}578}579}580581private getPackageType(serverType: AddConfigurationType): string | undefined {582switch (serverType) {583case AddConfigurationType.NpmPackage:584return 'npm';585case AddConfigurationType.PipPackage:586return 'pip';587case AddConfigurationType.NuGetPackage:588return 'nuget';589case AddConfigurationType.DockerImage:590return 'docker';591case AddConfigurationType.Stdio:592return 'stdio';593case AddConfigurationType.HTTP:594return 'sse';595default:596return undefined;597}598}599}600601export class McpInstallFromManifestCommand {602constructor(603@IFileDialogService private readonly _fileDialogService: IFileDialogService,604@IFileService private readonly _fileService: IFileService,605@IQuickInputService private readonly _quickInputService: IQuickInputService,606@INotificationService private readonly _notificationService: INotificationService,607@IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService,608@ILogService private readonly _logService: ILogService,609) { }610611async run(): Promise<void> {612// Step 1: Open file dialog to select the manifest file613const result = await this._fileDialogService.showOpenDialog({614title: localize('mcp.installFromManifest.title', "Select MCP Server Manifest"),615filters: [{ name: localize('mcp.installFromManifest.filter', "MCP Manifest"), extensions: ['json'] }],616canSelectFiles: true,617canSelectMany: false,618openLabel: localize({ key: 'mcp.installFromManifest.openLabel', comment: ['&& denotes a mnemonic'] }, "&&Install")619});620621if (!result?.[0]) {622return;623}624625const manifestUri = result[0];626627// Step 2: Read and parse the manifest file628let manifest: unknown;629try {630const contents = await this._fileService.readFile(manifestUri);631manifest = parseJsonc(contents.value.toString());632} catch (e) {633this._notificationService.error(localize('mcp.installFromManifest.readError', "Failed to read manifest file: {0}", e.message));634return;635}636637if (!manifest || typeof manifest !== 'object') {638this._notificationService.error(localize('mcp.installFromManifest.invalidJson', "Invalid manifest file: expected a JSON object"));639return;640}641642// Step 3: Validate and extract configuration from gallery manifest643const galleryManifest = manifest as IGalleryMcpServerConfiguration & { name?: string };644645// Determine package type from manifest646let packageType: RegistryType;647if (Array.isArray(galleryManifest.packages) && galleryManifest.packages.length > 0) {648packageType = galleryManifest.packages[0].registryType;649} else if (Array.isArray(galleryManifest.remotes) && galleryManifest.remotes.length > 0) {650packageType = RegistryType.REMOTE;651} else {652this._notificationService.error(localize('mcp.installFromManifest.invalidManifest', "Invalid manifest: expected 'packages' or 'remotes' with at least one entry"));653return;654}655656let config: IMcpServerConfiguration;657let inputs: IMcpServerVariable[] | undefined;658try {659const { mcpServerConfiguration, notices } = this._mcpManagementService.getMcpServerConfigurationFromManifest(galleryManifest, packageType);660config = mcpServerConfiguration.config;661inputs = mcpServerConfiguration.inputs;662663if (notices.length > 0) {664this._logService.warn(`MCP Management Service: Warnings while installing the MCP server from ${manifestUri.path}`, notices);665}666} catch (e) {667this._notificationService.error(localize('mcp.installFromManifest.parseError', "Failed to parse manifest: {0}", e.message));668return;669}670671// Step 4: Get server name from manifest or prompt user672let name = galleryManifest.name;673if (!name) {674name = await this._quickInputService.input({675title: localize('mcp.installFromManifest.serverId.title', "Enter Server ID"),676placeHolder: localize('mcp.installFromManifest.serverId.placeholder', "Unique identifier for this server"),677value: basename(manifestUri).replace(/\.json$/i, ''),678ignoreFocusLost: true,679});680681if (!name) {682return;683}684}685686// Step 5: Install to user settings687try {688await this._mcpManagementService.install({ name, config, inputs });689this._notificationService.info(localize('mcp.installFromManifest.success', "MCP server '{0}' installed successfully", name));690} catch (e) {691this._notificationService.error(localize('mcp.installFromManifest.installError', "Failed to install MCP server: {0}", e.message));692}693}694}695696697