@@ -33,12 +33,29 @@ import { fileExistsAtPath } from "../../utils/fs"
33
33
import { arePathsEqual } from "../../utils/path"
34
34
import { injectVariables } from "../../utils/config"
35
35
36
- export type McpConnection = {
36
+ // Discriminated union for connection states
37
+ export type ConnectedMcpConnection = {
38
+ type : "connected"
37
39
server : McpServer
38
40
client : Client
39
41
transport : StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
40
42
}
41
43
44
+ export type DisconnectedMcpConnection = {
45
+ type : "disconnected"
46
+ server : McpServer
47
+ client : null
48
+ transport : null
49
+ }
50
+
51
+ export type McpConnection = ConnectedMcpConnection | DisconnectedMcpConnection
52
+
53
+ // Enum for disable reasons
54
+ export enum DisableReason {
55
+ MCP_DISABLED = "mcpDisabled" ,
56
+ SERVER_DISABLED = "serverDisabled" ,
57
+ }
58
+
42
59
// Base configuration schema for common settings
43
60
const BaseConfigSchema = z . object ( {
44
61
disabled : z . boolean ( ) . optional ( ) ,
@@ -497,6 +514,7 @@ export class McpHub {
497
514
const result = McpSettingsSchema . safeParse ( config )
498
515
499
516
if ( result . success ) {
517
+ // Pass all servers including disabled ones - they'll be handled in updateServerConnections
500
518
await this . updateServerConnections ( result . data . mcpServers || { } , source , false )
501
519
} else {
502
520
const errorMessages = result . error . errors
@@ -552,6 +570,49 @@ export class McpHub {
552
570
await this . initializeMcpServers ( "project" )
553
571
}
554
572
573
+ /**
574
+ * Creates a placeholder connection for disabled servers or when MCP is globally disabled
575
+ * @param name The server name
576
+ * @param config The server configuration
577
+ * @param source The source of the server (global or project)
578
+ * @param reason The reason for creating a placeholder (mcpDisabled or serverDisabled)
579
+ * @returns A placeholder DisconnectedMcpConnection object
580
+ */
581
+ private createPlaceholderConnection (
582
+ name : string ,
583
+ config : z . infer < typeof ServerConfigSchema > ,
584
+ source : "global" | "project" ,
585
+ reason : DisableReason ,
586
+ ) : DisconnectedMcpConnection {
587
+ return {
588
+ type : "disconnected" ,
589
+ server : {
590
+ name,
591
+ config : JSON . stringify ( config ) ,
592
+ status : "disconnected" ,
593
+ disabled : reason === DisableReason . SERVER_DISABLED ? true : config . disabled ,
594
+ source,
595
+ projectPath : source === "project" ? vscode . workspace . workspaceFolders ?. [ 0 ] ?. uri . fsPath : undefined ,
596
+ errorHistory : [ ] ,
597
+ } ,
598
+ client : null ,
599
+ transport : null ,
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Checks if MCP is globally enabled
605
+ * @returns Promise<boolean> indicating if MCP is enabled
606
+ */
607
+ private async isMcpEnabled ( ) : Promise < boolean > {
608
+ const provider = this . providerRef . deref ( )
609
+ if ( ! provider ) {
610
+ return true // Default to enabled if provider is not available
611
+ }
612
+ const state = await provider . getState ( )
613
+ return state . mcpEnabled ?? true
614
+ }
615
+
555
616
private async connectToServer (
556
617
name : string ,
557
618
config : z . infer < typeof ServerConfigSchema > ,
@@ -560,6 +621,26 @@ export class McpHub {
560
621
// Remove existing connection if it exists with the same source
561
622
await this . deleteConnection ( name , source )
562
623
624
+ // Check if MCP is globally enabled
625
+ const mcpEnabled = await this . isMcpEnabled ( )
626
+ if ( ! mcpEnabled ) {
627
+ // Still create a connection object to track the server, but don't actually connect
628
+ const connection = this . createPlaceholderConnection ( name , config , source , DisableReason . MCP_DISABLED )
629
+ this . connections . push ( connection )
630
+ return
631
+ }
632
+
633
+ // Skip connecting to disabled servers
634
+ if ( config . disabled ) {
635
+ // Still create a connection object to track the server, but don't actually connect
636
+ const connection = this . createPlaceholderConnection ( name , config , source , DisableReason . SERVER_DISABLED )
637
+ this . connections . push ( connection )
638
+ return
639
+ }
640
+
641
+ // Set up file watchers for enabled servers
642
+ this . setupFileWatcher ( name , config , source )
643
+
563
644
try {
564
645
const client = new Client (
565
646
{
@@ -733,7 +814,9 @@ export class McpHub {
733
814
transport . start = async ( ) => { }
734
815
}
735
816
736
- const connection : McpConnection = {
817
+ // Create a connected connection
818
+ const connection : ConnectedMcpConnection = {
819
+ type : "connected" ,
737
820
server : {
738
821
name,
739
822
config : JSON . stringify ( configInjected ) ,
@@ -826,8 +909,8 @@ export class McpHub {
826
909
// Use the helper method to find the connection
827
910
const connection = this . findConnection ( serverName , source )
828
911
829
- if ( ! connection ) {
830
- throw new Error ( `Server ${ serverName } not found` )
912
+ if ( ! connection || connection . type !== "connected" ) {
913
+ return [ ]
831
914
}
832
915
833
916
const response = await connection . client . request ( { method : "tools/list" } , ListToolsResultSchema )
@@ -881,7 +964,7 @@ export class McpHub {
881
964
private async fetchResourcesList ( serverName : string , source ?: "global" | "project" ) : Promise < McpResource [ ] > {
882
965
try {
883
966
const connection = this . findConnection ( serverName , source )
884
- if ( ! connection ) {
967
+ if ( ! connection || connection . type !== "connected" ) {
885
968
return [ ]
886
969
}
887
970
const response = await connection . client . request ( { method : "resources/list" } , ListResourcesResultSchema )
@@ -898,7 +981,7 @@ export class McpHub {
898
981
) : Promise < McpResourceTemplate [ ] > {
899
982
try {
900
983
const connection = this . findConnection ( serverName , source )
901
- if ( ! connection ) {
984
+ if ( ! connection || connection . type !== "connected" ) {
902
985
return [ ]
903
986
}
904
987
const response = await connection . client . request (
@@ -913,15 +996,20 @@ export class McpHub {
913
996
}
914
997
915
998
async deleteConnection ( name : string , source ?: "global" | "project" ) : Promise < void > {
999
+ // Clean up file watchers for this server
1000
+ this . removeFileWatchersForServer ( name )
1001
+
916
1002
// If source is provided, only delete connections from that source
917
1003
const connections = source
918
1004
? this . connections . filter ( ( conn ) => conn . server . name === name && conn . server . source === source )
919
1005
: this . connections . filter ( ( conn ) => conn . server . name === name )
920
1006
921
1007
for ( const connection of connections ) {
922
1008
try {
923
- await connection . transport . close ( )
924
- await connection . client . close ( )
1009
+ if ( connection . type === "connected" ) {
1010
+ await connection . transport . close ( )
1011
+ await connection . client . close ( )
1012
+ }
925
1013
} catch ( error ) {
926
1014
console . error ( `Failed to close transport for ${ name } :` , error )
927
1015
}
@@ -975,15 +1063,21 @@ export class McpHub {
975
1063
if ( ! currentConnection ) {
976
1064
// New server
977
1065
try {
978
- this . setupFileWatcher ( name , validatedConfig , source )
1066
+ // Only setup file watcher for enabled servers
1067
+ if ( ! validatedConfig . disabled ) {
1068
+ this . setupFileWatcher ( name , validatedConfig , source )
1069
+ }
979
1070
await this . connectToServer ( name , validatedConfig , source )
980
1071
} catch ( error ) {
981
1072
this . showErrorMessage ( `Failed to connect to new MCP server ${ name } ` , error )
982
1073
}
983
1074
} else if ( ! deepEqual ( JSON . parse ( currentConnection . server . config ) , config ) ) {
984
1075
// Existing server with changed config
985
1076
try {
986
- this . setupFileWatcher ( name , validatedConfig , source )
1077
+ // Only setup file watcher for enabled servers
1078
+ if ( ! validatedConfig . disabled ) {
1079
+ this . setupFileWatcher ( name , validatedConfig , source )
1080
+ }
987
1081
await this . deleteConnection ( name , source )
988
1082
await this . connectToServer ( name , validatedConfig , source )
989
1083
} catch ( error ) {
@@ -1066,10 +1160,21 @@ export class McpHub {
1066
1160
this . fileWatchers . clear ( )
1067
1161
}
1068
1162
1163
+ private removeFileWatchersForServer ( serverName : string ) {
1164
+ const watchers = this . fileWatchers . get ( serverName )
1165
+ if ( watchers ) {
1166
+ watchers . forEach ( ( watcher ) => watcher . close ( ) )
1167
+ this . fileWatchers . delete ( serverName )
1168
+ }
1169
+ }
1170
+
1069
1171
async restartConnection ( serverName : string , source ?: "global" | "project" ) : Promise < void > {
1070
1172
this . isConnecting = true
1071
- const provider = this . providerRef . deref ( )
1072
- if ( ! provider ) {
1173
+
1174
+ // Check if MCP is globally enabled
1175
+ const mcpEnabled = await this . isMcpEnabled ( )
1176
+ if ( ! mcpEnabled ) {
1177
+ this . isConnecting = false
1073
1178
return
1074
1179
}
1075
1180
@@ -1111,6 +1216,23 @@ export class McpHub {
1111
1216
return
1112
1217
}
1113
1218
1219
+ // Check if MCP is globally enabled
1220
+ const mcpEnabled = await this . isMcpEnabled ( )
1221
+ if ( ! mcpEnabled ) {
1222
+ // Clear all existing connections
1223
+ const existingConnections = [ ...this . connections ]
1224
+ for ( const conn of existingConnections ) {
1225
+ await this . deleteConnection ( conn . server . name , conn . server . source )
1226
+ }
1227
+
1228
+ // Still initialize servers to track them, but they won't connect
1229
+ await this . initializeMcpServers ( "global" )
1230
+ await this . initializeMcpServers ( "project" )
1231
+
1232
+ await this . notifyWebviewOfServerChanges ( )
1233
+ return
1234
+ }
1235
+
1114
1236
this . isConnecting = true
1115
1237
vscode . window . showInformationMessage ( t ( "mcp:info.refreshing_all" ) )
1116
1238
@@ -1257,8 +1379,21 @@ export class McpHub {
1257
1379
try {
1258
1380
connection . server . disabled = disabled
1259
1381
1260
- // Only refresh capabilities if connected
1261
- if ( connection . server . status === "connected" ) {
1382
+ // If disabling a connected server, disconnect it
1383
+ if ( disabled && connection . server . status === "connected" ) {
1384
+ // Clean up file watchers when disabling
1385
+ this . removeFileWatchersForServer ( serverName )
1386
+ await this . deleteConnection ( serverName , serverSource )
1387
+ // Re-add as a disabled connection
1388
+ await this . connectToServer ( serverName , JSON . parse ( connection . server . config ) , serverSource )
1389
+ } else if ( ! disabled && connection . server . status === "disconnected" ) {
1390
+ // If enabling a disabled server, connect it
1391
+ const config = JSON . parse ( connection . server . config )
1392
+ await this . deleteConnection ( serverName , serverSource )
1393
+ // When re-enabling, file watchers will be set up in connectToServer
1394
+ await this . connectToServer ( serverName , config , serverSource )
1395
+ } else if ( connection . server . status === "connected" ) {
1396
+ // Only refresh capabilities if connected
1262
1397
connection . server . tools = await this . fetchToolsList ( serverName , serverSource )
1263
1398
connection . server . resources = await this . fetchResourcesList ( serverName , serverSource )
1264
1399
connection . server . resourceTemplates = await this . fetchResourceTemplatesList (
@@ -1439,7 +1574,7 @@ export class McpHub {
1439
1574
1440
1575
async readResource ( serverName : string , uri : string , source ?: "global" | "project" ) : Promise < McpResourceResponse > {
1441
1576
const connection = this . findConnection ( serverName , source )
1442
- if ( ! connection ) {
1577
+ if ( ! connection || connection . type !== "connected" ) {
1443
1578
throw new Error ( `No connection found for server: ${ serverName } ${ source ? ` with source ${ source } ` : "" } ` )
1444
1579
}
1445
1580
if ( connection . server . disabled ) {
@@ -1463,7 +1598,7 @@ export class McpHub {
1463
1598
source ?: "global" | "project" ,
1464
1599
) : Promise < McpToolCallResponse > {
1465
1600
const connection = this . findConnection ( serverName , source )
1466
- if ( ! connection ) {
1601
+ if ( ! connection || connection . type !== "connected" ) {
1467
1602
throw new Error (
1468
1603
`No connection found for server: ${ serverName } ${ source ? ` with source ${ source } ` : "" } . Please make sure to use MCP servers available under 'Connected MCP Servers'.` ,
1469
1604
)
@@ -1609,6 +1744,64 @@ export class McpHub {
1609
1744
}
1610
1745
}
1611
1746
1747
+ /**
1748
+ * Handles enabling/disabling MCP globally
1749
+ * @param enabled Whether MCP should be enabled or disabled
1750
+ * @returns Promise<void>
1751
+ */
1752
+ async handleMcpEnabledChange ( enabled : boolean ) : Promise < void > {
1753
+ if ( ! enabled ) {
1754
+ // If MCP is being disabled, disconnect all servers with error handling
1755
+ const existingConnections = [ ...this . connections ]
1756
+ const disconnectionErrors : Array < { serverName : string ; error : string } > = [ ]
1757
+
1758
+ for ( const conn of existingConnections ) {
1759
+ try {
1760
+ await this . deleteConnection ( conn . server . name , conn . server . source )
1761
+ } catch ( error ) {
1762
+ const errorMessage = error instanceof Error ? error . message : String ( error )
1763
+ disconnectionErrors . push ( {
1764
+ serverName : conn . server . name ,
1765
+ error : errorMessage ,
1766
+ } )
1767
+ console . error ( `Failed to disconnect MCP server ${ conn . server . name } : ${ errorMessage } ` )
1768
+ }
1769
+ }
1770
+
1771
+ // If there were errors, notify the user
1772
+ if ( disconnectionErrors . length > 0 ) {
1773
+ const errorSummary = disconnectionErrors . map ( ( e ) => `${ e . serverName } : ${ e . error } ` ) . join ( "\n" )
1774
+ vscode . window . showWarningMessage (
1775
+ t ( "mcp:errors.disconnect_servers_partial" , {
1776
+ count : disconnectionErrors . length ,
1777
+ errors : errorSummary ,
1778
+ } ) ||
1779
+ `Failed to disconnect ${ disconnectionErrors . length } MCP server(s). Check the output for details.` ,
1780
+ )
1781
+ }
1782
+
1783
+ // Re-initialize servers to track them in disconnected state
1784
+ try {
1785
+ await this . refreshAllConnections ( )
1786
+ } catch ( error ) {
1787
+ console . error ( `Failed to refresh MCP connections after disabling: ${ error } ` )
1788
+ vscode . window . showErrorMessage (
1789
+ t ( "mcp:errors.refresh_after_disable" ) || "Failed to refresh MCP connections after disabling" ,
1790
+ )
1791
+ }
1792
+ } else {
1793
+ // If MCP is being enabled, reconnect all servers
1794
+ try {
1795
+ await this . refreshAllConnections ( )
1796
+ } catch ( error ) {
1797
+ console . error ( `Failed to refresh MCP connections after enabling: ${ error } ` )
1798
+ vscode . window . showErrorMessage (
1799
+ t ( "mcp:errors.refresh_after_enable" ) || "Failed to refresh MCP connections after enabling" ,
1800
+ )
1801
+ }
1802
+ }
1803
+ }
1804
+
1612
1805
async dispose ( ) : Promise < void > {
1613
1806
// Prevent multiple disposals
1614
1807
if ( this . isDisposed ) {
0 commit comments