1+ import { arch } from 'process' ;
12import type { ConfigurationChangeEvent } from 'vscode' ;
2- import { Disposable } from 'vscode' ;
3+ import { Disposable , ProgressLocation , Uri , window , workspace } from 'vscode' ;
34import type { Container } from '../../../../container' ;
5+ import { registerCommand } from '../../../../system/-webview/command' ;
46import { configuration } from '../../../../system/-webview/configuration' ;
7+ import { getContext } from '../../../../system/-webview/context' ;
8+ import { Logger } from '../../../../system/logger' ;
9+ import { run } from '../../git/shell' ;
10+ import { getPlatform , isWeb } from '../../platform' ;
511import { CliCommandHandlers } from './commands' ;
612import type { IpcServer } from './ipcServer' ;
713import { createIpcServer } from './ipcServer' ;
@@ -18,9 +24,15 @@ export class GkCliIntegrationProvider implements Disposable {
1824 private _runningDisposable : Disposable | undefined ;
1925
2026 constructor ( private readonly container : Container ) {
21- this . _disposable = configuration . onDidChange ( e => this . onConfigurationChanged ( e ) ) ;
27+ this . _disposable = Disposable . from (
28+ configuration . onDidChange ( e => this . onConfigurationChanged ( e ) ) ,
29+ ...this . registerCommands ( ) ,
30+ ) ;
2231
2332 this . onConfigurationChanged ( ) ;
33+ setTimeout ( ( ) => {
34+ void this . installMCPIfNeeded ( true ) ;
35+ } , 10000 + Math . floor ( Math . random ( ) * 20000 ) ) ;
2436 }
2537
2638 dispose ( ) : void {
@@ -56,4 +68,292 @@ export class GkCliIntegrationProvider implements Disposable {
5668 this . _runningDisposable ?. dispose ( ) ;
5769 this . _runningDisposable = undefined ;
5870 }
71+
72+ private async installMCPIfNeeded ( silent ?: boolean ) : Promise < void > {
73+ try {
74+ if ( silent && this . container . storage . get ( 'ai:mcp:attemptInstall' , false ) ) {
75+ return ;
76+ }
77+
78+ // Store the flag to indicate that we have made the attempt
79+ await this . container . storage . store ( 'ai:mcp:attemptInstall' , true ) ;
80+
81+ if ( configuration . get ( 'ai.enabled' ) === false ) {
82+ const message = 'Cannot install MCP: AI is disabled in settings' ;
83+ Logger . log ( message ) ;
84+ if ( silent !== true ) {
85+ void window . showErrorMessage ( message ) ;
86+ }
87+ return ;
88+ }
89+
90+ if ( getContext ( 'gitlens:gk:organization:ai:enabled' , true ) !== true ) {
91+ const message = 'Cannot install MCP: AI is disabled by your organization' ;
92+ Logger . log ( message ) ;
93+ if ( silent !== true ) {
94+ void window . showErrorMessage ( message ) ;
95+ }
96+ return ;
97+ }
98+
99+ if ( isWeb ) {
100+ const message = 'Cannot install MCP: web environment is not supported' ;
101+ Logger . log ( message ) ;
102+ if ( silent !== true ) {
103+ void window . showErrorMessage ( message ) ;
104+ }
105+ return ;
106+ }
107+
108+ // Detect platform and architecture
109+ const platform = getPlatform ( ) ;
110+
111+ // Map platform names for the API and get architecture
112+ let platformName : string ;
113+ let architecture : string ;
114+
115+ switch ( arch ) {
116+ case 'x64' :
117+ architecture = 'x64' ;
118+ break ;
119+ case 'arm64' :
120+ architecture = 'arm64' ;
121+ break ;
122+ default :
123+ architecture = 'x86' ; // Default to x86 for other architectures
124+ break ;
125+ }
126+
127+ switch ( platform ) {
128+ case 'windows' :
129+ platformName = 'windows' ;
130+ break ;
131+ case 'macOS' :
132+ platformName = 'darwin' ;
133+ break ;
134+ case 'linux' :
135+ platformName = 'linux' ;
136+ break ;
137+ default : {
138+ const message = `Skipping MCP installation: unsupported platform ${ platform } ` ;
139+ Logger . log ( message ) ;
140+ if ( silent !== true ) {
141+ void window . showErrorMessage ( `Cannot install MCP integration: unsupported platform ${ platform } ` ) ;
142+ }
143+ return ;
144+ }
145+ }
146+
147+ // Wrap the main installation process with progress indicator if not silent
148+ const installationTask = async ( ) => {
149+ let mcpInstallerPath : Uri | undefined ;
150+ let mcpExtractedFolderPath : Uri | undefined ;
151+ let mcpExtractedPath : Uri | undefined ;
152+
153+ try {
154+ // Download the MCP proxy installer
155+ const proxyUrl = `https://api.gitkraken.dev/releases/gkcli-proxy/production/${ platformName } /${ architecture } /active` ;
156+
157+ let response = await fetch ( proxyUrl ) ;
158+ if ( ! response . ok ) {
159+ const errorMsg = `Failed to get MCP installer proxy: ${ response . status } ${ response . statusText } ` ;
160+ Logger . error ( errorMsg ) ;
161+ throw new Error ( errorMsg ) ;
162+ }
163+
164+ let downloadUrl : string | undefined ;
165+ try {
166+ const mcpInstallerInfo : { version ?: string ; packages ?: { zip ?: string } } | undefined = await response . json ( ) as any ;
167+ downloadUrl = mcpInstallerInfo ?. packages ?. zip ;
168+ } catch ( ex ) {
169+ const errorMsg = `Failed to parse MCP installer info: ${ ex } ` ;
170+ Logger . error ( errorMsg ) ;
171+ throw new Error ( errorMsg ) ;
172+ }
173+
174+ if ( downloadUrl == null ) {
175+ const errorMsg = 'Failed to find download URL for MCP proxy installer' ;
176+ Logger . error ( errorMsg ) ;
177+ throw new Error ( errorMsg ) ;
178+ }
179+
180+ response = await fetch ( downloadUrl ) ;
181+ if ( ! response . ok ) {
182+ const errorMsg = `Failed to download MCP proxy installer: ${ response . status } ${ response . statusText } ` ;
183+ Logger . error ( errorMsg ) ;
184+ throw new Error ( errorMsg ) ;
185+ }
186+
187+ const installerData = await response . arrayBuffer ( ) ;
188+ if ( installerData . byteLength === 0 ) {
189+ const errorMsg = 'Downloaded installer is empty' ;
190+ Logger . error ( errorMsg ) ;
191+ throw new Error ( errorMsg ) ;
192+ }
193+ // installer file name is the last part of the download URL
194+ const installerFileName = downloadUrl . substring ( downloadUrl . lastIndexOf ( '/' ) + 1 ) ;
195+ mcpInstallerPath = Uri . joinPath ( this . container . context . globalStorageUri , installerFileName ) ;
196+
197+ // Ensure the global storage directory exists
198+ await workspace . fs . createDirectory ( this . container . context . globalStorageUri ) ;
199+
200+ // Write the installer to the extension storage
201+ await workspace . fs . writeFile ( mcpInstallerPath , new Uint8Array ( installerData ) ) ;
202+ Logger . log ( `Downloaded MCP proxy installer successfully` ) ;
203+
204+ try {
205+ // Use the run function to extract the installer file from the installer zip
206+ if ( platform === 'windows' ) {
207+ // On Windows, use PowerShell to extract the zip file
208+ await run (
209+ 'powershell.exe' ,
210+ [ '-Command' , `Expand-Archive -Path "${ mcpInstallerPath . fsPath } " -DestinationPath "${ this . container . context . globalStorageUri . fsPath } "` ] ,
211+ 'utf8' ,
212+ ) ;
213+ } else {
214+ // On Unix-like systems, use the unzip command to extract the zip file
215+ await run (
216+ 'unzip' ,
217+ [ '-o' , mcpInstallerPath . fsPath , '-d' , this . container . context . globalStorageUri . fsPath ] ,
218+ 'utf8' ,
219+ ) ;
220+ }
221+ // The gk.exe file should be in a subfolder named after the installer file name
222+ const extractedFolderName = installerFileName . replace ( / \. z i p $ / , '' ) ;
223+ mcpExtractedFolderPath = Uri . joinPath ( this . container . context . globalStorageUri , extractedFolderName ) ;
224+ mcpExtractedPath = Uri . joinPath ( mcpExtractedFolderPath , 'gk.exe' ) ;
225+
226+ // Check using stat to make sure the newly extracted file exists.
227+ await workspace . fs . stat ( mcpExtractedPath ) ;
228+ } catch ( error ) {
229+ const errorMsg = `Failed to extract MCP installer: ${ error } ` ;
230+ Logger . error ( errorMsg ) ;
231+ throw new Error ( errorMsg ) ;
232+ }
233+
234+ // Get the VS Code settings.json file path
235+ // TODO: Make this path point to the current vscode profile's settings.json once the API supports it
236+ const settingsPath = `${ this . container . context . globalStorageUri . fsPath } \\..\\..\\settings.json` ;
237+
238+ // Configure the MCP server in settings.json
239+ try {
240+ await run ( mcpExtractedPath . fsPath , [ 'mcp' , 'install' , 'vscode' , '--file-path' , settingsPath ] , 'utf8' ) ;
241+ } catch {
242+ // Try alternative execution methods based on platform
243+ try {
244+ Logger . log ( 'Attempting alternative execution method for MCP install...' ) ;
245+ if ( platform === 'windows' ) {
246+ // On Windows, try running with cmd.exe
247+ await run (
248+ 'cmd.exe' ,
249+ [
250+ '/c' ,
251+ `"${ mcpExtractedPath . fsPath } "` ,
252+ 'mcp' ,
253+ 'install' ,
254+ 'vscode' ,
255+ '--file-path' ,
256+ `"${ settingsPath } "` ,
257+ ] ,
258+ 'utf8' ,
259+ ) ;
260+ } else {
261+ // On Unix-like systems, try running with sh
262+ await run (
263+ '/bin/sh' ,
264+ [ '-c' , `"${ mcpExtractedPath . fsPath } " mcp install vscode --file-path "${ settingsPath } "` ] ,
265+ 'utf8' ,
266+ ) ;
267+ }
268+ } catch ( altError ) {
269+ const errorMsg = `MCP server configuration failed: ${ altError } ` ;
270+ Logger . error ( errorMsg ) ;
271+ throw new Error ( errorMsg ) ;
272+ }
273+ }
274+
275+ // Verify that the MCP server was actually configured in settings.json
276+ try {
277+ const settingsUri = Uri . file ( settingsPath ) ;
278+ const settingsData = await workspace . fs . readFile ( settingsUri ) ;
279+ const settingsJson = JSON . parse ( settingsData . toString ( ) ) ;
280+
281+ if ( ! settingsJson ?. [ 'mcp' ] ?. [ 'servers' ] ?. [ 'GitKraken' ] ) {
282+ const errorMsg = 'MCP server configuration verification failed: Unable to update MCP settings' ;
283+ Logger . error ( errorMsg ) ;
284+ throw new Error ( errorMsg ) ;
285+ }
286+
287+ Logger . log ( 'MCP configured successfully - GitKraken server verified in settings.json' ) ;
288+ } catch ( verifyError ) {
289+ if ( verifyError instanceof Error && verifyError . message . includes ( 'verification failed' ) ) {
290+ // Re-throw verification errors as-is
291+ throw verifyError ;
292+ }
293+ // Handle file read/parse errors
294+ const errorMsg = `Failed to verify MCP configuration in settings.json: ${ verifyError } ` ;
295+ Logger . error ( errorMsg ) ;
296+ throw new Error ( errorMsg ) ;
297+ }
298+ } finally {
299+ // Always clean up downloaded/extracted files, even if something failed
300+ if ( mcpInstallerPath != null ) {
301+ try {
302+ await workspace . fs . delete ( mcpInstallerPath ) ;
303+ } catch ( error ) {
304+ Logger . warn ( `Failed to delete MCP installer zip file: ${ error } ` ) ;
305+ }
306+ }
307+
308+ if ( mcpExtractedPath != null ) {
309+ try {
310+ await workspace . fs . delete ( mcpExtractedPath ) ;
311+ } catch ( error ) {
312+ Logger . warn ( `Failed to delete MCP extracted executable: ${ error } ` ) ;
313+ }
314+ }
315+
316+ if ( mcpExtractedFolderPath != null ) {
317+ try {
318+ await workspace . fs . delete ( Uri . joinPath ( mcpExtractedFolderPath , 'README.md' ) ) ;
319+ await workspace . fs . delete ( mcpExtractedFolderPath ) ;
320+ } catch ( error ) {
321+ Logger . warn ( `Failed to delete MCP extracted folder: ${ error } ` ) ;
322+ }
323+ }
324+ }
325+ } ;
326+
327+ // Execute the installation task with or without progress indicator
328+ if ( silent !== true ) {
329+ await window . withProgress (
330+ {
331+ location : ProgressLocation . Notification ,
332+ title : 'Installing MCP integration...' ,
333+ cancellable : false ,
334+ } ,
335+ async ( ) => {
336+ await installationTask ( ) ;
337+ }
338+ ) ;
339+
340+ // Show success notification if not silent
341+ void window . showInformationMessage ( 'MCP integration installed successfully' ) ;
342+ } else {
343+ await installationTask ( ) ;
344+ }
345+
346+ } catch ( error ) {
347+ Logger . error ( `Error during MCP installation: ${ error } ` ) ;
348+
349+ // Show error notification if not silent
350+ if ( silent !== true ) {
351+ void window . showErrorMessage ( `Failed to install MCP integration: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
352+ }
353+ }
354+ }
355+
356+ private registerCommands ( ) : Disposable [ ] {
357+ return [ registerCommand ( 'gitlens.ai.mcp.install' , ( ) => this . installMCPIfNeeded ( ) ) ] ;
358+ }
59359}
0 commit comments