1
+ import { arch } from 'process' ;
1
2
import type { ConfigurationChangeEvent } from 'vscode' ;
2
- import { Disposable } from 'vscode' ;
3
+ import { Disposable , ProgressLocation , Uri , window , workspace } from 'vscode' ;
3
4
import type { Container } from '../../../../container' ;
5
+ import { registerCommand } from '../../../../system/-webview/command' ;
4
6
import { 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' ;
5
11
import { CliCommandHandlers } from './commands' ;
6
12
import type { IpcServer } from './ipcServer' ;
7
13
import { createIpcServer } from './ipcServer' ;
@@ -18,9 +24,15 @@ export class GkCliIntegrationProvider implements Disposable {
18
24
private _runningDisposable : Disposable | undefined ;
19
25
20
26
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
+ ) ;
22
31
23
32
this . onConfigurationChanged ( ) ;
33
+ setTimeout ( ( ) => {
34
+ void this . installMCPIfNeeded ( true ) ;
35
+ } , 10000 + Math . floor ( Math . random ( ) * 20000 ) ) ;
24
36
}
25
37
26
38
dispose ( ) : void {
@@ -56,4 +68,292 @@ export class GkCliIntegrationProvider implements Disposable {
56
68
this . _runningDisposable ?. dispose ( ) ;
57
69
this . _runningDisposable = undefined ;
58
70
}
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
+ }
59
359
}
0 commit comments