Skip to content

Commit ff360be

Browse files
authored
1 parent f250ad8 commit ff360be

File tree

5 files changed

+201
-14
lines changed

5 files changed

+201
-14
lines changed

src/vs/workbench/contrib/mcp/browser/mcpCommands.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ export class OpenUserMcpResourceCommand extends Action2 {
716716
constructor() {
717717
super({
718718
id: McpCommandIds.OpenUserMcp,
719-
title: localize2('mcp.command.openUserMcp', "Open User MCP Servers"),
719+
title: localize2('mcp.command.openUserMcp', "Open User MCP Configuration"),
720720
category,
721721
f1: true
722722
});
@@ -733,7 +733,7 @@ export class OpenRemoteUserMcpResourceCommand extends Action2 {
733733
constructor() {
734734
super({
735735
id: McpCommandIds.OpenRemoteUserMcp,
736-
title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User MCP Servers"),
736+
title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User MCP Configuration"),
737737
category,
738738
f1: true,
739739
precondition: RemoteNameContext.notEqualsTo('')
@@ -753,7 +753,7 @@ export class OpenWorkspaceFolderMcpResourceCommand extends Action2 {
753753
constructor() {
754754
super({
755755
id: McpCommandIds.OpenWorkspaceFolderMcp,
756-
title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Servers"),
756+
title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Configuration"),
757757
category,
758758
f1: true,
759759
precondition: WorkspaceFolderCountContext.notEqualsTo(0)
@@ -776,7 +776,7 @@ export class OpenWorkspaceMcpResourceCommand extends Action2 {
776776
constructor() {
777777
super({
778778
id: McpCommandIds.OpenWorkspaceMcp,
779-
title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Servers"),
779+
title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Configuration"),
780780
category,
781781
f1: true,
782782
precondition: WorkbenchStateContext.isEqualTo('workspace')

src/vs/workbench/contrib/mcp/browser/mcpMigration.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { parse } from '../../../../base/common/jsonc.js';
1818
import { isObject, Mutable } from '../../../../base/common/types.js';
1919
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
2020
import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';
21+
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
22+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
23+
import { McpCommandIds } from '../common/mcpCommandIds.js';
24+
import { localize } from '../../../../nls.js';
2125

2226
interface IMcpConfiguration {
2327
inputs?: IMcpServerVariable[];
@@ -35,6 +39,8 @@ export class McpConfigMigrationContribution extends Disposable implements IWorkb
3539
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
3640
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
3741
@ILogService private readonly logService: ILogService,
42+
@INotificationService private readonly notificationService: INotificationService,
43+
@ICommandService private readonly commandService: ICommandService,
3844
) {
3945
super();
4046
this.migrateMcpConfig();
@@ -50,22 +56,65 @@ export class McpConfigMigrationContribution extends Disposable implements IWorkb
5056
} catch (error) {
5157
this.logService.error(`MCP migration: Failed to migrate user MCP config`, error);
5258
}
59+
this.watchForMcpConfiguration(this.userDataProfileService.currentProfile.settingsResource, false);
5360

5461
const remoteEnvironment = await this.remoteAgentService.getEnvironment();
55-
if (!remoteEnvironment) {
56-
return;
62+
if (remoteEnvironment) {
63+
try {
64+
const userRemoteMcpConfig = await this.parseMcpConfig(remoteEnvironment.settingsPath);
65+
if (userRemoteMcpConfig && userRemoteMcpConfig.servers && Object.keys(userRemoteMcpConfig.servers).length > 0) {
66+
await Promise.all(Object.entries(userRemoteMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userRemoteMcpConfig.inputs : undefined }, { target: ConfigurationTarget.USER_REMOTE })));
67+
await this.removeMcpConfig(remoteEnvironment.settingsPath);
68+
}
69+
} catch (error) {
70+
this.logService.error(`MCP migration: Failed to migrate remote MCP config`, error);
71+
}
72+
this.watchForMcpConfiguration(remoteEnvironment.settingsPath, true);
5773
}
74+
75+
}
76+
77+
private watchForMcpConfiguration(file: URI, isRemote: boolean): void {
78+
this._register(this.fileService.watch(file));
79+
this._register(this.fileService.onDidFilesChange(e => {
80+
if (e.contains(file)) {
81+
this.checkForMcpConfigInFile(file, isRemote);
82+
}
83+
}));
84+
}
85+
86+
private async checkForMcpConfigInFile(settingsFile: URI, isRemote: boolean): Promise<void> {
5887
try {
59-
const userRemoteMcpConfig = await this.parseMcpConfig(remoteEnvironment.mcpResource);
60-
if (userRemoteMcpConfig && userRemoteMcpConfig.servers && Object.keys(userRemoteMcpConfig.servers).length > 0) {
61-
await Promise.all(Object.entries(userRemoteMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userRemoteMcpConfig.inputs : undefined }, { target: ConfigurationTarget.USER_REMOTE })));
62-
await this.removeMcpConfig(remoteEnvironment.mcpResource);
88+
const mcpConfig = await this.parseMcpConfig(settingsFile);
89+
if (mcpConfig && mcpConfig.servers && Object.keys(mcpConfig.servers).length > 0) {
90+
this.showMcpConfigErrorNotification(isRemote);
6391
}
6492
} catch (error) {
65-
this.logService.error(`MCP migration: Failed to migrate remote MCP config`, error);
93+
// Ignore parsing errors - file might not exist or be malformed
6694
}
6795
}
6896

97+
private showMcpConfigErrorNotification(isRemote: boolean): void {
98+
const message = isRemote
99+
? localize('mcp.migration.remoteConfigFound', 'MCP servers should not be configured in remote user settings. Use the dedicated MCP configuration instead.')
100+
: localize('mcp.migration.userConfigFound', 'MCP servers should not be configured in user settings. Use the dedicated MCP configuration instead.');
101+
102+
const openConfigLabel = isRemote
103+
? localize('mcp.migration.openRemoteConfig', 'Open Remote User MCP Configuration')
104+
: localize('mcp.migration.openUserConfig', 'Open User MCP Configuration');
105+
106+
const commandId = isRemote ? McpCommandIds.OpenRemoteUserMcp : McpCommandIds.OpenUserMcp;
107+
108+
this.notificationService.prompt(
109+
Severity.Error,
110+
message,
111+
[{
112+
label: openConfigLabel,
113+
run: () => this.commandService.executeCommand(commandId)
114+
}]
115+
);
116+
}
117+
69118
private async parseMcpConfig(settingsFile: URI): Promise<IMcpConfiguration | undefined> {
70119
try {
71120
const content = await this.fileService.readFile(settingsFile);

src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditor
4646
import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from '../../../services/preferences/common/preferencesModels.js';
4747
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
4848
import { EXPERIMENTAL_INDICATOR_DESCRIPTION, PREVIEW_INDICATOR_DESCRIPTION } from '../common/preferences.js';
49+
import { mcpConfigurationSection } from '../../mcp/common/mcpConfiguration.js';
50+
import { McpCommandIds } from '../../mcp/common/mcpCommandIds.js';
4951

5052
export interface IPreferencesRenderer extends IDisposable {
5153
render(): void;
@@ -63,6 +65,7 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend
6365
private associatedPreferencesModel!: IPreferencesEditorModel<ISetting>;
6466

6567
private unsupportedSettingsRenderer: UnsupportedSettingsRenderer;
68+
private mcpSettingsRenderer: McpSettingsRenderer;
6669

6770
constructor(protected editor: ICodeEditor, readonly preferencesModel: SettingsEditorModel,
6871
@IPreferencesService protected preferencesService: IPreferencesService,
@@ -75,11 +78,13 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend
7578
this._register(this.editSettingActionRenderer.onUpdateSetting(({ key, value, source }) => this.updatePreference(key, value, source)));
7679
this._register(this.editor.getModel()!.onDidChangeContent(() => this.modelChangeDelayer.trigger(() => this.onModelChanged())));
7780
this.unsupportedSettingsRenderer = this._register(instantiationService.createInstance(UnsupportedSettingsRenderer, editor, preferencesModel));
81+
this.mcpSettingsRenderer = this._register(instantiationService.createInstance(McpSettingsRenderer, editor, preferencesModel));
7882
}
7983

8084
render(): void {
8185
this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this.associatedPreferencesModel);
8286
this.unsupportedSettingsRenderer.render();
87+
this.mcpSettingsRenderer.render();
8388
}
8489

8590
updatePreference(key: string, value: any, source: IIndexedSetting): void {
@@ -784,6 +789,126 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc
784789

785790
}
786791

792+
class McpSettingsRenderer extends Disposable implements languages.CodeActionProvider {
793+
794+
private renderingDelayer: Delayer<void> = new Delayer<void>(200);
795+
private readonly codeActions = new ResourceMap<[Range, languages.CodeAction[]][]>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
796+
797+
constructor(
798+
private readonly editor: ICodeEditor,
799+
private readonly settingsEditorModel: SettingsEditorModel,
800+
@IMarkerService private readonly markerService: IMarkerService,
801+
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
802+
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
803+
) {
804+
super();
805+
this._register(this.editor.getModel()!.onDidChangeContent(() => this.delayedRender()));
806+
this._register(languageFeaturesService.codeActionProvider.register({ pattern: settingsEditorModel.uri.path }, this));
807+
}
808+
809+
private delayedRender(): void {
810+
this.renderingDelayer.trigger(() => this.render());
811+
}
812+
813+
public render(): void {
814+
this.codeActions.clear();
815+
const markerData: IMarkerData[] = this.generateMarkerData();
816+
if (markerData.length) {
817+
this.markerService.changeOne('McpSettingsRenderer', this.settingsEditorModel.uri, markerData);
818+
} else {
819+
this.markerService.remove('McpSettingsRenderer', [this.settingsEditorModel.uri]);
820+
}
821+
}
822+
823+
async provideCodeActions(model: ITextModel, range: Range | Selection, context: languages.CodeActionContext, token: CancellationToken): Promise<languages.CodeActionList> {
824+
const actions: languages.CodeAction[] = [];
825+
const codeActionsByRange = this.codeActions.get(model.uri);
826+
if (codeActionsByRange) {
827+
for (const [codeActionsRange, codeActions] of codeActionsByRange) {
828+
if (codeActionsRange.containsRange(range)) {
829+
actions.push(...codeActions);
830+
}
831+
}
832+
}
833+
return {
834+
actions,
835+
dispose: () => { }
836+
};
837+
}
838+
839+
private generateMarkerData(): IMarkerData[] {
840+
const markerData: IMarkerData[] = [];
841+
842+
// Only check for MCP configuration in user local and user remote settings
843+
if (this.settingsEditorModel.configurationTarget !== ConfigurationTarget.USER_LOCAL &&
844+
this.settingsEditorModel.configurationTarget !== ConfigurationTarget.USER_REMOTE) {
845+
return markerData;
846+
}
847+
848+
for (const settingsGroup of this.settingsEditorModel.settingsGroups) {
849+
for (const section of settingsGroup.sections) {
850+
for (const setting of section.settings) {
851+
if (setting.key === mcpConfigurationSection) {
852+
const marker = this.generateMcpConfigurationMarker(setting);
853+
markerData.push(marker);
854+
const codeActions = this.generateMcpConfigurationCodeActions([marker]);
855+
this.addCodeActions(setting.range, codeActions);
856+
}
857+
}
858+
}
859+
}
860+
return markerData;
861+
}
862+
863+
private generateMcpConfigurationMarker(setting: ISetting): IMarkerData {
864+
const isRemote = this.settingsEditorModel.configurationTarget === ConfigurationTarget.USER_REMOTE;
865+
const message = isRemote
866+
? nls.localize('mcp.renderer.remoteConfigFound', 'MCP servers should not be configured in remote user settings. Use the dedicated MCP configuration instead.')
867+
: nls.localize('mcp.renderer.userConfigFound', 'MCP servers should not be configured in user settings. Use the dedicated MCP configuration instead.');
868+
869+
return {
870+
severity: MarkerSeverity.Warning,
871+
...setting.range,
872+
message
873+
};
874+
}
875+
876+
private generateMcpConfigurationCodeActions(diagnostics: IMarkerData[]): languages.CodeAction[] {
877+
const isRemote = this.settingsEditorModel.configurationTarget === ConfigurationTarget.USER_REMOTE;
878+
const openConfigLabel = isRemote
879+
? nls.localize('mcp.renderer.openRemoteConfig', 'Open Remote User MCP Configuration')
880+
: nls.localize('mcp.renderer.openUserConfig', 'Open User MCP Configuration');
881+
882+
const commandId = isRemote ? McpCommandIds.OpenRemoteUserMcp : McpCommandIds.OpenUserMcp;
883+
884+
return [{
885+
title: openConfigLabel,
886+
command: {
887+
id: commandId,
888+
title: openConfigLabel
889+
},
890+
diagnostics,
891+
kind: CodeActionKind.QuickFix.value
892+
}];
893+
}
894+
895+
private addCodeActions(range: IRange, codeActions: languages.CodeAction[]): void {
896+
let actions = this.codeActions.get(this.settingsEditorModel.uri);
897+
if (!actions) {
898+
actions = [];
899+
this.codeActions.set(this.settingsEditorModel.uri, actions);
900+
}
901+
actions.push([Range.lift(range), codeActions]);
902+
}
903+
904+
public override dispose(): void {
905+
this.markerService.remove('McpSettingsRenderer', [this.settingsEditorModel.uri]);
906+
this.codeActions.clear();
907+
super.dispose();
908+
}
909+
910+
}
911+
787912
class WorkspaceConfigurationRenderer extends Disposable {
788913
private static readonly supportedKeys = ['folders', 'tasks', 'launch', 'extensions', 'settings', 'remoteAuthority', 'transient'];
789914

src/vs/workbench/services/configuration/common/configurationEditing.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -590,16 +590,15 @@ export class ConfigurationEditing {
590590
const resource = this.getConfigurationFileResource(target, key, standaloneConfigurationMap[key], overrides.resource, undefined);
591591

592592
// Check for prefix
593-
const keyRemainsNested = this.isWorkspaceConfigurationResource(resource) || resource?.fsPath === this.userDataProfileService.currentProfile.settingsResource.fsPath;
594593
if (config.key === key) {
595-
const jsonPath = keyRemainsNested ? [key] : [];
594+
const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key] : [];
596595
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: resource ?? undefined, workspaceStandAloneConfigurationKey: key, target };
597596
}
598597

599598
// Check for prefix.<setting>
600599
const keyPrefix = `${key}.`;
601600
if (config.key.indexOf(keyPrefix) === 0) {
602-
const jsonPath = keyRemainsNested ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)];
601+
const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key, config.key.substring(keyPrefix.length)] : [config.key.substring(keyPrefix.length)];
603602
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: resource ?? undefined, workspaceStandAloneConfigurationKey: key, target };
604603
}
605604
}
@@ -629,6 +628,8 @@ export class ConfigurationEditing {
629628
if (target === EditableConfigurationTarget.USER_LOCAL) {
630629
if (key === TASKS_CONFIGURATION_KEY) {
631630
return this.userDataProfileService.currentProfile.tasksResource;
631+
} if (key === MCP_CONFIGURATION_KEY) {
632+
return this.userDataProfileService.currentProfile.mcpResource;
632633
} else {
633634
if (!this.userDataProfileService.currentProfile.isDefault && this.configurationService.isSettingAppliedForAllProfiles(key)) {
634635
return this.userDataProfilesService.defaultProfile.settingsResource;

src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,18 @@ suite('ConfigurationEditing', () => {
336336
assert.strictEqual(parsed['my.super.setting'], 'my.super.value');
337337
});
338338

339+
test('write user standalone mcp setting - existing file', async () => {
340+
const target = joinPath(environmentService.userRoamingDataHome, USER_STANDALONE_CONFIGURATIONS['mcp']);
341+
await fileService.writeFile(target, VSBuffer.fromString('{ "my.super.setting": "my.super.value" }'));
342+
343+
await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'mcp.service.testSetting', value: 'value' });
344+
345+
const contents = await fileService.readFile(target);
346+
const parsed = json.parse(contents.value.toString());
347+
assert.strictEqual(parsed['service.testSetting'], 'value');
348+
assert.strictEqual(parsed['my.super.setting'], 'my.super.value');
349+
});
350+
339351
test('write workspace standalone setting - empty file - full JSON', async () => {
340352
await testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } });
341353

0 commit comments

Comments
 (0)