Skip to content

Commit e380a84

Browse files
Copilotsergeibbb
andcommitted
Implement JSON-based duplicate MCP server cleanup
Instead of using `gk mcp uninstall` command (which doesn't work properly), directly analyze and modify MCP configuration JSON files in user profile. The implementation: - Locates the IDE-specific settings.json file based on app name and platform - Reads and parses the file (handling JSON comments) - Identifies GitKraken MCP servers by name pattern, command path, and source flag - Removes duplicate manual installations - Saves the updated configuration This approach provides proper cleanup of duplicate manual MCP installations when the bundled MCP provider becomes available. Co-authored-by: sergeibbb <[email protected]>
1 parent bcd2e0f commit e380a84

File tree

1 file changed

+177
-24
lines changed

1 file changed

+177
-24
lines changed

src/env/node/gk/mcp/integration.ts

Lines changed: 177 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { homedir } from 'os';
2+
import { join } from 'path';
3+
import { env as processEnv } from 'process';
14
import type { Event, McpServerDefinition, McpServerDefinitionProvider } from 'vscode';
2-
import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition } from 'vscode';
5+
import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition, Uri, workspace } from 'vscode';
36
import type { Container } from '../../../../container';
47
import type { StorageChangeEvent } from '../../../../system/-webview/storage';
58
import { getHostAppName } from '../../../../system/-webview/vscode';
69
import { debug, log } from '../../../../system/decorators/log';
710
import type { Deferrable } from '../../../../system/function/debounce';
811
import { debounce } from '../../../../system/function/debounce';
912
import { Logger } from '../../../../system/logger';
13+
import { getPlatform } from '../../platform';
1014
import { runCLICommand, toMcpInstallProvider } from '../cli/utils';
1115

1216
const CLIProxyMCPConfigOutputs = {
@@ -96,7 +100,7 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable {
96100
}
97101

98102
// Clean up any duplicate manual installations before registering the bundled version
99-
await this.uninstallManualMcpIfExists(appName, cliPath);
103+
await this.removeDuplicateManualMcpConfigurations(appName);
100104

101105
let output = await runCLICommand(['mcp', 'config', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], {
102106
cwd: cliPath,
@@ -124,35 +128,184 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable {
124128
}
125129

126130
@debug()
127-
private async uninstallManualMcpIfExists(appName: string, cliPath: string): Promise<void> {
131+
private async removeDuplicateManualMcpConfigurations(appName: string): Promise<void> {
128132
try {
129-
// Check if a manual MCP installation exists by attempting to uninstall it
130-
// The uninstall command will only succeed if there's an existing manual installation
131-
const output = await runCLICommand(
132-
['mcp', 'uninstall', appName, '--source=gitlens', `--scheme=${env.uriScheme}`],
133-
{
134-
cwd: cliPath,
135-
},
136-
);
137-
138-
// If uninstall succeeded, log and send telemetry
139-
if (output.trim().length > 0) {
140-
Logger.log(`Uninstalled duplicate manual MCP installation for ${appName}`);
141-
142-
if (this.container.telemetry.enabled) {
143-
this.container.telemetry.sendEvent('mcp/uninstall/duplicate', {
144-
app: appName,
145-
source: 'gk-mcp-provider',
146-
});
133+
const settingsPath = this.getUserSettingsPath(appName);
134+
if (settingsPath == null) {
135+
Logger.debug(`Unable to determine settings path for ${appName}`);
136+
return;
137+
}
138+
139+
const settingsUri = Uri.file(settingsPath);
140+
141+
// Check if settings file exists
142+
try {
143+
await workspace.fs.stat(settingsUri);
144+
} catch {
145+
// Settings file doesn't exist, nothing to clean up
146+
Logger.debug(`Settings file does not exist: ${settingsPath}`);
147+
return;
148+
}
149+
150+
// Read and parse settings file
151+
const settingsBytes = await workspace.fs.readFile(settingsUri);
152+
const settingsText = new TextDecoder().decode(settingsBytes);
153+
154+
// Parse JSON with comments support (VS Code settings.json allows comments)
155+
const settings = this.parseJsonWithComments(settingsText);
156+
157+
// Check for MCP server configurations
158+
const mcpServersKey = 'languageModels.chat.mcpServers';
159+
if (!settings[mcpServersKey] || typeof settings[mcpServersKey] !== 'object') {
160+
Logger.debug('No MCP server configurations found');
161+
return;
162+
}
163+
164+
const mcpServers = settings[mcpServersKey] as Record<string, unknown>;
165+
let removedCount = 0;
166+
const serversToRemove: string[] = [];
167+
168+
// Look for GitKraken MCP servers that were manually installed
169+
// These typically have names like "gitkraken" or "GitKraken" and contain
170+
// the GK CLI executable path
171+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
172+
if (this.isGitKrakenMcpServer(serverName, serverConfig)) {
173+
serversToRemove.push(serverName);
174+
Logger.log(`Found duplicate manual MCP configuration: ${serverName}`);
147175
}
148176
}
177+
178+
// Remove the servers
179+
for (const serverName of serversToRemove) {
180+
mcpServers[serverName] = undefined;
181+
removedCount++;
182+
}
183+
184+
if (removedCount === 0) {
185+
Logger.debug('No duplicate manual MCP configurations found');
186+
return;
187+
}
188+
189+
// Save updated settings
190+
const updatedSettingsText = JSON.stringify(settings, null, '\t');
191+
await workspace.fs.writeFile(settingsUri, new TextEncoder().encode(updatedSettingsText));
192+
193+
Logger.log(`Removed ${removedCount} duplicate manual MCP configuration(s) from ${settingsPath}`);
194+
195+
if (this.container.telemetry.enabled) {
196+
this.container.telemetry.sendEvent('mcp/uninstall/duplicate', {
197+
app: appName,
198+
source: 'gk-mcp-provider',
199+
});
200+
}
149201
} catch (ex) {
150-
// If uninstall fails, it likely means no manual installation exists
151-
// Log the error at debug level but don't fail the overall process
152-
Logger.debug(`No manual MCP installation to uninstall for ${appName}: ${ex}`);
202+
// Log error but don't fail the overall process
203+
Logger.error(`Error removing duplicate MCP configurations: ${ex}`);
204+
}
205+
}
206+
207+
private getUserSettingsPath(appName: string): string | null {
208+
const platform = getPlatform();
209+
const home = homedir();
210+
const appData = processEnv.APPDATA || join(home, 'AppData', 'Roaming');
211+
212+
switch (appName) {
213+
case 'vscode':
214+
switch (platform) {
215+
case 'windows':
216+
return join(appData, 'Code', 'User', 'settings.json');
217+
case 'macOS':
218+
return join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json');
219+
default: // linux
220+
return join(home, '.config', 'Code', 'User', 'settings.json');
221+
}
222+
case 'vscode-insiders':
223+
switch (platform) {
224+
case 'windows':
225+
return join(appData, 'Code - Insiders', 'User', 'settings.json');
226+
case 'macOS':
227+
return join(home, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json');
228+
default: // linux
229+
return join(home, '.config', 'Code - Insiders', 'User', 'settings.json');
230+
}
231+
case 'vscode-exploration':
232+
switch (platform) {
233+
case 'windows':
234+
return join(appData, 'Code - Exploration', 'User', 'settings.json');
235+
case 'macOS':
236+
return join(home, 'Library', 'Application Support', 'Code - Exploration', 'User', 'settings.json');
237+
default: // linux
238+
return join(home, '.config', 'Code - Exploration', 'User', 'settings.json');
239+
}
240+
case 'cursor':
241+
switch (platform) {
242+
case 'windows':
243+
return join(appData, 'Cursor', 'User', 'settings.json');
244+
case 'macOS':
245+
return join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json');
246+
default: // linux
247+
return join(home, '.config', 'Cursor', 'User', 'settings.json');
248+
}
249+
case 'windsurf':
250+
switch (platform) {
251+
case 'windows':
252+
return join(appData, 'Windsurf', 'User', 'settings.json');
253+
case 'macOS':
254+
return join(home, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json');
255+
default: // linux
256+
return join(home, '.config', 'Windsurf', 'User', 'settings.json');
257+
}
258+
case 'codium':
259+
switch (platform) {
260+
case 'windows':
261+
return join(appData, 'VSCodium', 'User', 'settings.json');
262+
case 'macOS':
263+
return join(home, 'Library', 'Application Support', 'VSCodium', 'User', 'settings.json');
264+
default: // linux
265+
return join(home, '.config', 'VSCodium', 'User', 'settings.json');
266+
}
267+
default:
268+
return null;
153269
}
154270
}
155271

272+
private parseJsonWithComments(text: string): Record<string, unknown> {
273+
// Simple JSON comment remover - removes // and /* */ comments
274+
// This is a simplified version; VS Code uses jsonc-parser for full support
275+
const withoutComments = text
276+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
277+
.replace(/\/\/.*/g, ''); // Remove // comments
278+
279+
return JSON.parse(withoutComments) as Record<string, unknown>;
280+
}
281+
282+
private isGitKrakenMcpServer(serverName: string, serverConfig: unknown): boolean {
283+
// Check if this is a GitKraken MCP server by looking for:
284+
// 1. Server name matches GitKraken variants
285+
// 2. Command contains 'gk' executable
286+
// 3. Args contain '--source=gitlens' or scheme parameter
287+
288+
const nameMatches = /^git[_-]?kraken$/i.test(serverName);
289+
290+
if (typeof serverConfig !== 'object' || serverConfig == null) {
291+
return false;
292+
}
293+
294+
const config = serverConfig as Record<string, unknown>;
295+
const command = typeof config.command === 'string' ? config.command : '';
296+
const args = Array.isArray(config.args) ? config.args : [];
297+
298+
// Check if command contains gk executable
299+
const commandMatches = command.includes('/gk') || command.includes('\\gk') || command.endsWith('gk.exe');
300+
301+
// Check if args contain source=gitlens
302+
const argsMatch = args.some((arg: unknown) =>
303+
typeof arg === 'string' && arg.includes('--source=gitlens')
304+
);
305+
306+
return nameMatches && commandMatches && argsMatch;
307+
}
308+
156309
private onRegistrationCompleted(_cliVersion?: string | undefined) {
157310
if (!this.container.telemetry.enabled) return;
158311

0 commit comments

Comments
 (0)