1
1
import { homedir } from 'os' ;
2
2
import { arch } from 'process' ;
3
3
import type { ConfigurationChangeEvent } from 'vscode' ;
4
- import { Disposable , env , ProgressLocation , Uri , window , workspace } from 'vscode' ;
4
+ import { version as codeVersion , Disposable , env , ProgressLocation , Uri , window , workspace } from 'vscode' ;
5
5
import type { Container } from '../../../../container' ;
6
6
import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService' ;
7
7
import { registerCommand } from '../../../../system/-webview/command' ;
8
8
import { configuration } from '../../../../system/-webview/configuration' ;
9
9
import { getContext } from '../../../../system/-webview/context' ;
10
+ import { openUrl } from '../../../../system/-webview/vscode/uris' ;
10
11
import { Logger } from '../../../../system/logger' ;
12
+ import { compare } from '../../../../system/version' ;
11
13
import { run } from '../../git/shell' ;
12
14
import { getPlatform , isWeb } from '../../platform' ;
13
15
import { CliCommandHandlers } from './commands' ;
@@ -33,9 +35,12 @@ export class GkCliIntegrationProvider implements Disposable {
33
35
) ;
34
36
35
37
this . onConfigurationChanged ( ) ;
36
- setTimeout ( ( ) => {
37
- void this . installMCPIfNeeded ( true ) ;
38
- } , 10000 + Math . floor ( Math . random ( ) * 20000 ) ) ;
38
+ setTimeout (
39
+ ( ) => {
40
+ void this . installMCPIfNeeded ( true ) ;
41
+ } ,
42
+ 10000 + Math . floor ( Math . random ( ) * 20000 ) ,
43
+ ) ;
39
44
}
40
45
41
46
dispose ( ) : void {
@@ -74,6 +79,16 @@ export class GkCliIntegrationProvider implements Disposable {
74
79
75
80
private async installMCPIfNeeded ( silent ?: boolean ) : Promise < void > {
76
81
try {
82
+ if (
83
+ ( env . appName === 'Visual Studio Code' || env . appName === 'Visual Studio Code - Insiders' ) &&
84
+ compare ( codeVersion , '1.102' ) < 0
85
+ ) {
86
+ if ( ! silent ) {
87
+ void window . showInformationMessage ( 'Use of this command requires VS Code 1.102 or later.' ) ;
88
+ }
89
+ return ;
90
+ }
91
+
77
92
if ( silent && this . container . storage . get ( 'ai:mcp:attemptInstall' ) ) {
78
93
return ;
79
94
}
@@ -89,7 +104,7 @@ export class GkCliIntegrationProvider implements Disposable {
89
104
return ;
90
105
}
91
106
92
- if ( getContext ( 'gitlens:gk:organization:ai:enabled' , true ) !== true ) {
107
+ if ( getContext ( 'gitlens:gk:organization:ai:enabled' , true ) !== true ) {
93
108
const message = 'Cannot install MCP: AI is disabled by your organization' ;
94
109
Logger . log ( message ) ;
95
110
if ( silent !== true ) {
@@ -140,7 +155,9 @@ export class GkCliIntegrationProvider implements Disposable {
140
155
const message = `Skipping MCP installation: unsupported platform ${ platform } ` ;
141
156
Logger . log ( message ) ;
142
157
if ( silent !== true ) {
143
- void window . showErrorMessage ( `Cannot install MCP integration: unsupported platform ${ platform } ` ) ;
158
+ void window . showErrorMessage (
159
+ `Cannot install MCP integration: unsupported platform ${ platform } ` ,
160
+ ) ;
144
161
}
145
162
return ;
146
163
}
@@ -156,7 +173,14 @@ export class GkCliIntegrationProvider implements Disposable {
156
173
157
174
try {
158
175
// Download the MCP proxy installer
159
- const proxyUrl = this . container . urls . getGkApiUrl ( 'releases' , 'gkcli-proxy' , 'production' , platformName , architecture , 'active' ) ;
176
+ const proxyUrl = this . container . urls . getGkApiUrl (
177
+ 'releases' ,
178
+ 'gkcli-proxy' ,
179
+ 'production' ,
180
+ platformName ,
181
+ architecture ,
182
+ 'active' ,
183
+ ) ;
160
184
161
185
let response = await fetch ( proxyUrl ) ;
162
186
if ( ! response . ok ) {
@@ -167,7 +191,8 @@ export class GkCliIntegrationProvider implements Disposable {
167
191
168
192
let downloadUrl : string | undefined ;
169
193
try {
170
- const mcpInstallerInfo : { version ?: string ; packages ?: { zip ?: string } } | undefined = await response . json ( ) as any ;
194
+ const mcpInstallerInfo : { version ?: string ; packages ?: { zip ?: string } } | undefined =
195
+ ( await response . json ( ) ) as any ;
171
196
downloadUrl = mcpInstallerInfo ?. packages ?. zip ;
172
197
} catch ( ex ) {
173
198
const errorMsg = `Failed to parse MCP installer info: ${ ex } ` ;
@@ -211,7 +236,10 @@ export class GkCliIntegrationProvider implements Disposable {
211
236
// On Windows, use PowerShell to extract the zip file
212
237
await run (
213
238
'powershell.exe' ,
214
- [ '-Command' , `Expand-Archive -Path "${ mcpInstallerPath . fsPath } " -DestinationPath "${ this . container . context . globalStorageUri . fsPath } "` ] ,
239
+ [
240
+ '-Command' ,
241
+ `Expand-Archive -Path "${ mcpInstallerPath . fsPath } " -DestinationPath "${ this . container . context . globalStorageUri . fsPath } "` ,
242
+ ] ,
215
243
'utf8' ,
216
244
) ;
217
245
} else {
@@ -224,7 +252,10 @@ export class GkCliIntegrationProvider implements Disposable {
224
252
}
225
253
// The gk file should be in a subfolder named after the installer file name
226
254
const extractedFolderName = installerFileName . replace ( / \. z i p $ / , '' ) ;
227
- mcpExtractedFolderPath = Uri . joinPath ( this . container . context . globalStorageUri , extractedFolderName ) ;
255
+ mcpExtractedFolderPath = Uri . joinPath (
256
+ this . container . context . globalStorageUri ,
257
+ extractedFolderName ,
258
+ ) ;
228
259
mcpExtractedPath = Uri . joinPath ( mcpExtractedFolderPath , mcpFileName ) ;
229
260
230
261
// Check using stat to make sure the newly extracted file exists.
@@ -263,12 +294,19 @@ export class GkCliIntegrationProvider implements Disposable {
263
294
264
295
// Configure the MCP server in settings.json
265
296
try {
266
- const installOutput = await run ( platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` , [ 'install' ] , 'utf8' , { cwd : mcpExtractedFolderPath . fsPath } ) ;
297
+ const installOutput = await run (
298
+ platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` ,
299
+ [ 'install' ] ,
300
+ 'utf8' ,
301
+ { cwd : mcpExtractedFolderPath . fsPath } ,
302
+ ) ;
267
303
const directory = installOutput . match ( / D i r e c t o r y : ( .* ) / ) ;
304
+ let directoryPath ;
268
305
if ( directory != null ) {
269
306
try {
270
- const directoryPath = directory [ 1 ] ;
307
+ directoryPath = directory [ 1 ] ;
271
308
await this . container . storage . store ( 'gk:cli:installedPath' , directoryPath ) ;
309
+ // Add to PATH
272
310
if ( platform === 'windows' ) {
273
311
await run (
274
312
'powershell.exe' ,
@@ -279,32 +317,35 @@ export class GkCliIntegrationProvider implements Disposable {
279
317
'utf8' ,
280
318
) ;
281
319
} else {
282
- // For Unix-like systems, detect and modify the appropriate shell profile
283
- const homeDir = homedir ( ) ;
284
- // Try to detect which shell profile exists and is in use
285
- const possibleProfiles = [
286
- { path : `${ homeDir } /.zshrc` , shell : 'zsh' } ,
320
+ // For Unix-like systems, detect and modify the appropriate shell profile
321
+ const homeDir = homedir ( ) ;
322
+ // Try to detect which shell profile exists and is in use
323
+ const possibleProfiles = [
324
+ { path : `${ homeDir } /.zshrc` , shell : 'zsh' } ,
287
325
{ path : `${ homeDir } /.zprofile` , shell : 'zsh' } ,
288
- { path : `${ homeDir } /.bashrc` , shell : 'bash' } ,
289
- { path : `${ homeDir } /.profile` , shell : 'sh' }
290
- ] ;
291
-
292
- // Find the first profile that exists
293
- let shellProfile ;
294
- for ( const profile of possibleProfiles ) {
295
- try {
296
- await workspace . fs . stat ( Uri . file ( profile . path ) ) ;
297
- shellProfile = profile . path ;
298
- break ;
299
- } catch {
300
- // Profile doesn't exist, try next one
301
- }
302
- }
326
+ { path : `${ homeDir } /.bashrc` , shell : 'bash' } ,
327
+ { path : `${ homeDir } /.profile` , shell : 'sh' } ,
328
+ ] ;
329
+
330
+ // Find the first profile that exists
331
+ let shellProfile ;
332
+ for ( const profile of possibleProfiles ) {
333
+ try {
334
+ await workspace . fs . stat ( Uri . file ( profile . path ) ) ;
335
+ shellProfile = profile . path ;
336
+ break ;
337
+ } catch {
338
+ // Profile doesn't exist, try next one
339
+ }
340
+ }
303
341
304
342
if ( shellProfile != null ) {
305
343
await run (
306
344
'sh' ,
307
- [ '-c' , `echo '# Added by GitLens for MCP support' >> ${ shellProfile } && echo 'export PATH="$PATH:${ directoryPath } "' >> ${ shellProfile } ` ] ,
345
+ [
346
+ '-c' ,
347
+ `echo '# Added by GitLens for MCP support' >> ${ shellProfile } && echo 'export PATH="$PATH:${ directoryPath } "' >> ${ shellProfile } ` ,
348
+ ] ,
308
349
'utf8' ,
309
350
) ;
310
351
} else {
@@ -316,12 +357,37 @@ export class GkCliIntegrationProvider implements Disposable {
316
357
}
317
358
} else {
318
359
Logger . warn ( 'MCP Install: Failed to find directory in install output' ) ;
360
+ if ( appName === 'vscode' ) {
361
+ throw new Error ( 'MCP command path not availavle' ) ;
362
+ }
363
+ }
364
+
365
+ if ( appName === 'vscode' ) {
366
+ const config = {
367
+ name : 'GitKraken' ,
368
+ command : Uri . file ( `${ directoryPath } \\${ mcpFileName } ` ) . fsPath ,
369
+ args : [ 'mcp' ] ,
370
+ type : 'stdio' ,
371
+ } ;
372
+ const installDeepLinkUrl = `${ isInsiders ? 'vscode-insiders' : 'vscode' } :mcp/install?${ encodeURIComponent ( JSON . stringify ( config ) ) } ` ;
373
+ await openUrl ( installDeepLinkUrl ) ;
374
+ } else {
375
+ await run (
376
+ platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` ,
377
+ [ 'mcp' , 'install' , appName , ...( isInsiders ? [ '--file-path' , settingsPath ] : [ ] ) ] ,
378
+ 'utf8' ,
379
+ { cwd : mcpExtractedFolderPath . fsPath } ,
380
+ ) ;
319
381
}
320
382
321
- await run ( platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` , [ 'mcp' , 'install' , appName , ...isInsiders ? [ '--file-path' , settingsPath ] : [ ] ] , 'utf8' , { cwd : mcpExtractedFolderPath . fsPath } ) ;
322
383
const gkAuth = ( await this . container . subscription . getAuthenticationSession ( ) ) ?. accessToken ;
323
384
if ( gkAuth != null ) {
324
- await run ( platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` , [ 'auth' , 'login' , '-t' , gkAuth ] , 'utf8' , { cwd : mcpExtractedFolderPath . fsPath } ) ;
385
+ await run (
386
+ platform === 'windows' ? mcpFileName : `./${ mcpFileName } ` ,
387
+ [ 'auth' , 'login' , '-t' , gkAuth ] ,
388
+ 'utf8' ,
389
+ { cwd : mcpExtractedFolderPath . fsPath } ,
390
+ ) ;
325
391
}
326
392
327
393
Logger . log ( 'MCP configuration completed' ) ;
@@ -369,7 +435,7 @@ export class GkCliIntegrationProvider implements Disposable {
369
435
} ,
370
436
async ( ) => {
371
437
await installationTask ( ) ;
372
- }
438
+ } ,
373
439
) ;
374
440
} else {
375
441
await installationTask ( ) ;
@@ -383,7 +449,9 @@ export class GkCliIntegrationProvider implements Disposable {
383
449
384
450
// Show error notification if not silent
385
451
if ( silent !== true ) {
386
- void window . showErrorMessage ( `Failed to install MCP integration: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
452
+ void window . showErrorMessage (
453
+ `Failed to install MCP integration: ${ error instanceof Error ? error . message : String ( error ) } ` ,
454
+ ) ;
387
455
}
388
456
}
389
457
}
@@ -392,17 +460,26 @@ export class GkCliIntegrationProvider implements Disposable {
392
460
const mcpInstallStatus = this . container . storage . get ( 'ai:mcp:attemptInstall' ) ;
393
461
const mcpDirectoryPath = this . container . storage . get ( 'gk:cli:installedPath' ) ;
394
462
const platform = getPlatform ( ) ;
395
- if ( e . current ?. account ?. id != null && e . current . account . id !== e . previous ?. account ?. id && mcpInstallStatus === 'completed' && mcpDirectoryPath != null ) {
463
+ if (
464
+ e . current ?. account ?. id != null &&
465
+ e . current . account . id !== e . previous ?. account ?. id &&
466
+ mcpInstallStatus === 'completed' &&
467
+ mcpDirectoryPath != null
468
+ ) {
396
469
const currentSessionToken = ( await this . container . subscription . getAuthenticationSession ( ) ) ?. accessToken ;
397
470
if ( currentSessionToken != null ) {
398
471
try {
399
- await run ( platform === 'windows' ? 'gk.exe' : './gk' , [ 'auth' , 'login' , '-t' , currentSessionToken ] , 'utf8' , { cwd : mcpDirectoryPath } ) ;
472
+ await run (
473
+ platform === 'windows' ? 'gk.exe' : './gk' ,
474
+ [ 'auth' , 'login' , '-t' , currentSessionToken ] ,
475
+ 'utf8' ,
476
+ { cwd : mcpDirectoryPath } ,
477
+ ) ;
400
478
} catch { }
401
479
}
402
480
}
403
481
}
404
482
405
-
406
483
private registerCommands ( ) : Disposable [ ] {
407
484
return [ registerCommand ( 'gitlens.ai.mcp.install' , ( ) => this . installMCPIfNeeded ( ) ) ] ;
408
485
}
0 commit comments