@@ -10,21 +10,31 @@ import { Event } from '../../../../base/common/event.js';
10
10
import { Disposable , DisposableStore } from '../../../../base/common/lifecycle.js' ;
11
11
import { autorun , derived } from '../../../../base/common/observable.js' ;
12
12
import { ThemeIcon } from '../../../../base/common/themables.js' ;
13
+ import { URI } from '../../../../base/common/uri.js' ;
14
+ import { generateUuid } from '../../../../base/common/uuid.js' ;
13
15
import { ILocalizedString , localize , localize2 } from '../../../../nls.js' ;
14
16
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js' ;
15
17
import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js' ;
16
18
import { Action2 , MenuId , MenuItemAction } from '../../../../platform/actions/common/actions.js' ;
17
19
import { ICommandService } from '../../../../platform/commands/common/commands.js' ;
20
+ import { ConfigurationTarget , getConfigValueInTarget , IConfigurationService } from '../../../../platform/configuration/common/configuration.js' ;
18
21
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js' ;
19
22
import { IInstantiationService , ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js' ;
23
+ import { IMcpConfiguration , IMcpConfigurationSSE } from '../../../../platform/mcp/common/mcpPlatformTypes.js' ;
20
24
import { IQuickInputService , IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js' ;
21
25
import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js' ;
26
+ import { IWorkspace , IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' ;
27
+ import { ActiveEditorContext , ResourceContextKey } from '../../../common/contextkeys.js' ;
22
28
import { IWorkbenchContribution } from '../../../common/contributions.js' ;
29
+ import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js' ;
30
+ import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js' ;
23
31
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js' ;
24
32
import { ChatMode } from '../../chat/common/constants.js' ;
33
+ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js' ;
34
+ import { IMcpConfigurationStdio } from '../common/mcpConfiguration.js' ;
25
35
import { McpContextKeys } from '../common/mcpContextKeys.js' ;
26
36
import { IMcpRegistry } from '../common/mcpRegistryTypes.js' ;
27
- import { LazyCollectionState , IMcpServer , IMcpService , McpConnectionState , McpServerToolsState } from '../common/mcpTypes.js' ;
37
+ import { IMcpServer , IMcpService , LazyCollectionState , McpConnectionState , McpServerToolsState } from '../common/mcpTypes.js' ;
28
38
29
39
// acroynms do not get localized
30
40
const category : ILocalizedString = {
@@ -357,3 +367,168 @@ export class ResetMcpCachedTools extends Action2 {
357
367
mcpService . resetCaches ( ) ;
358
368
}
359
369
}
370
+
371
+ export class AddConfigurationAction extends Action2 {
372
+ static readonly ID = 'workbench.mcp.addConfiguration' ;
373
+
374
+ constructor ( ) {
375
+ super ( {
376
+ id : AddConfigurationAction . ID ,
377
+ title : localize2 ( 'mcp.addConfiguration' , "Add Server..." ) ,
378
+ metadata : {
379
+ description : localize2 ( 'mcp.addConfiguration.description' , "Installs a new Model Context protocol to the mcp.json settings" ) ,
380
+ } ,
381
+ category,
382
+ f1 : true ,
383
+ menu : {
384
+ id : MenuId . EditorContent ,
385
+ when : ContextKeyExpr . and (
386
+ ContextKeyExpr . regex ( ResourceContextKey . Path . key , / \. v s c o d e [ / \\ ] m c p \. j s o n $ / ) ,
387
+ ActiveEditorContext . isEqualTo ( TEXT_FILE_EDITOR_ID )
388
+ )
389
+ }
390
+ } ) ;
391
+ }
392
+
393
+ private async getServerType ( quickInputService : IQuickInputService ) : Promise < { id : 'stdio' | 'sse' } | undefined > {
394
+ return quickInputService . pick ( [
395
+ { id : 'stdio' , label : localize ( 'mcp.serverType.command' , "Command (stdio)" ) , description : localize ( 'mcp.serverType.command.description' , "Run a local command that implements the MCP protocol" ) } ,
396
+ { id : 'sse' , label : localize ( 'mcp.serverType.http' , "HTTP (server-sent events)" ) , description : localize ( 'mcp.serverType.http.description' , "Connect to a remote HTTP server that implements the MCP protocol" ) }
397
+ ] , {
398
+ title : localize ( 'mcp.serverType.title' , "Select Server Type" ) ,
399
+ placeHolder : localize ( 'mcp.serverType.placeholder' , "Choose the type of MCP server to add" )
400
+ } ) as Promise < { id : 'stdio' | 'sse' } | undefined > ;
401
+ }
402
+
403
+ private async getStdioConfig ( quickInputService : IQuickInputService ) : Promise < IMcpConfigurationStdio | undefined > {
404
+ const command = await quickInputService . input ( {
405
+ title : localize ( 'mcp.command.title' , "Enter Command" ) ,
406
+ placeHolder : localize ( 'mcp.command.placeholder' , "Command to run (with optional arguments)" ) ,
407
+ ignoreFocusLost : true ,
408
+ } ) ;
409
+
410
+ if ( ! command ) {
411
+ return undefined ;
412
+ }
413
+
414
+ // Split command into command and args, handling quotes
415
+ const parts = command . match ( / (?: [ ^ \s " ] + | " [ ^ " ] * " ) + / g) ! ;
416
+ return {
417
+ type : 'stdio' ,
418
+ command : parts [ 0 ] . replace ( / " / g, '' ) ,
419
+ args : parts . slice ( 1 ) . map ( arg => arg . replace ( / " / g, '' ) )
420
+ } ;
421
+ }
422
+
423
+ private async getSSEConfig ( quickInputService : IQuickInputService ) : Promise < IMcpConfigurationSSE | undefined > {
424
+ const url = await quickInputService . input ( {
425
+ title : localize ( 'mcp.url.title' , "Enter Server URL" ) ,
426
+ placeHolder : localize ( 'mcp.url.placeholder' , "URL of the MCP server (e.g., http://localhost:3000)" ) ,
427
+ ignoreFocusLost : true ,
428
+ } ) ;
429
+
430
+ if ( ! url ) {
431
+ return undefined ;
432
+ }
433
+
434
+ return {
435
+ type : 'sse' ,
436
+ url
437
+ } ;
438
+ }
439
+
440
+ private async getServerId ( quickInputService : IQuickInputService ) : Promise < string | undefined > {
441
+ const suggestedId = `my-mcp-server-${ generateUuid ( ) . split ( '-' ) [ 0 ] } ` ;
442
+ const id = await quickInputService . input ( {
443
+ title : localize ( 'mcp.serverId.title' , "Enter Server ID" ) ,
444
+ placeHolder : localize ( 'mcp.serverId.placeholder' , "Unique identifier for this server" ) ,
445
+ value : suggestedId ,
446
+ ignoreFocusLost : true ,
447
+ } ) ;
448
+
449
+ return id ;
450
+ }
451
+
452
+ private async getConfigurationTarget ( quickInputService : IQuickInputService , workspace : IWorkspace , isInRemote : boolean ) : Promise < ConfigurationTarget | undefined > {
453
+ const options : ( IQuickPickItem & { target : ConfigurationTarget } ) [ ] = [
454
+ { target : ConfigurationTarget . USER , label : localize ( 'mcp.target.user' , "User Settings" ) , description : localize ( 'mcp.target.user.description' , "Available in all workspaces" ) }
455
+ ] ;
456
+
457
+ if ( isInRemote ) {
458
+ options . push ( { target : ConfigurationTarget . USER_REMOTE , label : localize ( 'mcp.target.remote' , "Remote Settings" ) , description : localize ( 'mcp.target..remote.description' , "Available on this remote machine" ) } ) ;
459
+ }
460
+
461
+ if ( workspace . folders . length > 0 ) {
462
+ options . push ( { target : ConfigurationTarget . WORKSPACE , label : localize ( 'mcp.target.workspace' , "Workspace Settings" ) , description : localize ( 'mcp.target.workspace.description' , "Available in this workspace" ) } ) ;
463
+ }
464
+
465
+ if ( options . length === 1 ) {
466
+ return options [ 0 ] . target ;
467
+ }
468
+
469
+
470
+ const targetPick = await quickInputService . pick ( options , {
471
+ title : localize ( 'mcp.target.title' , "Choose where to save the configuration" ) ,
472
+ } ) ;
473
+
474
+ return targetPick ?. target ;
475
+ }
476
+
477
+ async run ( accessor : ServicesAccessor , configUri ?: string ) : Promise < void > {
478
+ const quickInputService = accessor . get ( IQuickInputService ) ;
479
+ const configurationService = accessor . get ( IConfigurationService ) ;
480
+ const jsonEditingService = accessor . get ( IJSONEditingService ) ;
481
+ const workspaceService = accessor . get ( IWorkspaceContextService ) ;
482
+ const environmentService = accessor . get ( IWorkbenchEnvironmentService ) ;
483
+
484
+ // Step 1: Choose server type
485
+ const serverType = await this . getServerType ( quickInputService ) ;
486
+ if ( ! serverType ) {
487
+ return ;
488
+ }
489
+
490
+ // Step 2: Get server details based on type
491
+ const serverConfig = await ( serverType . id === 'stdio'
492
+ ? this . getStdioConfig ( quickInputService )
493
+ : this . getSSEConfig ( quickInputService ) ) ;
494
+
495
+ if ( ! serverConfig ) {
496
+ return ;
497
+ }
498
+
499
+ // Step 3: Get server ID
500
+ const serverId = await this . getServerId ( quickInputService ) ;
501
+ if ( ! serverId ) {
502
+ return ;
503
+ }
504
+
505
+ // Step 4: Choose configuration target if no configUri provided
506
+ let target : ConfigurationTarget | undefined ;
507
+ const workspace = workspaceService . getWorkspace ( ) ;
508
+ if ( ! configUri ) {
509
+ target = await this . getConfigurationTarget ( quickInputService , workspace , ! ! environmentService . remoteAuthority ) ;
510
+ if ( ! target ) {
511
+ return ;
512
+ }
513
+ }
514
+
515
+ // Step 5: Update configuration
516
+ const writeToUriDirect = configUri
517
+ ? URI . parse ( configUri )
518
+ : target === ConfigurationTarget . WORKSPACE && workspace . folders . length === 1
519
+ ? URI . joinPath ( workspace . folders [ 0 ] . uri , '.vscode' , 'mcp.json' )
520
+ : undefined ;
521
+
522
+ if ( writeToUriDirect ) {
523
+ await jsonEditingService . write ( writeToUriDirect , [ {
524
+ path : [ 'servers' , serverId ] ,
525
+ value : serverConfig
526
+ } ] , true ) ;
527
+ } else {
528
+ const settings : IMcpConfiguration = { ...getConfigValueInTarget ( configurationService . inspect < IMcpConfiguration > ( 'mcp' ) , target ! ) } ;
529
+ settings . servers ??= { } ;
530
+ settings . servers [ serverId ] = serverConfig ;
531
+ await configurationService . updateValue ( 'mcp' , settings , target ! ) ;
532
+ }
533
+ }
534
+ }
0 commit comments