@@ -33,12 +33,29 @@ import { fileExistsAtPath } from "../../utils/fs"
3333import { arePathsEqual } from "../../utils/path"
3434import { injectVariables } from "../../utils/config"
3535
36- export type McpConnection = {
36+ // Discriminated union for connection states
37+ export type ConnectedMcpConnection = {
38+ type : "connected"
3739 server : McpServer
3840 client : Client
3941 transport : StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
4042}
4143
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+
4259// Base configuration schema for common settings
4360const BaseConfigSchema = z . object ( {
4461 disabled : z . boolean ( ) . optional ( ) ,
@@ -497,6 +514,7 @@ export class McpHub {
497514 const result = McpSettingsSchema . safeParse ( config )
498515
499516 if ( result . success ) {
517+ // Pass all servers including disabled ones - they'll be handled in updateServerConnections
500518 await this . updateServerConnections ( result . data . mcpServers || { } , source , false )
501519 } else {
502520 const errorMessages = result . error . errors
@@ -552,6 +570,49 @@ export class McpHub {
552570 await this . initializeMcpServers ( "project" )
553571 }
554572
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+
555616 private async connectToServer (
556617 name : string ,
557618 config : z . infer < typeof ServerConfigSchema > ,
@@ -560,6 +621,26 @@ export class McpHub {
560621 // Remove existing connection if it exists with the same source
561622 await this . deleteConnection ( name , source )
562623
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+
563644 try {
564645 const client = new Client (
565646 {
@@ -733,7 +814,9 @@ export class McpHub {
733814 transport . start = async ( ) => { }
734815 }
735816
736- const connection : McpConnection = {
817+ // Create a connected connection
818+ const connection : ConnectedMcpConnection = {
819+ type : "connected" ,
737820 server : {
738821 name,
739822 config : JSON . stringify ( configInjected ) ,
@@ -826,8 +909,8 @@ export class McpHub {
826909 // Use the helper method to find the connection
827910 const connection = this . findConnection ( serverName , source )
828911
829- if ( ! connection ) {
830- throw new Error ( `Server ${ serverName } not found` )
912+ if ( ! connection || connection . type !== "connected" ) {
913+ return [ ]
831914 }
832915
833916 const response = await connection . client . request ( { method : "tools/list" } , ListToolsResultSchema )
@@ -881,7 +964,7 @@ export class McpHub {
881964 private async fetchResourcesList ( serverName : string , source ?: "global" | "project" ) : Promise < McpResource [ ] > {
882965 try {
883966 const connection = this . findConnection ( serverName , source )
884- if ( ! connection ) {
967+ if ( ! connection || connection . type !== "connected" ) {
885968 return [ ]
886969 }
887970 const response = await connection . client . request ( { method : "resources/list" } , ListResourcesResultSchema )
@@ -898,7 +981,7 @@ export class McpHub {
898981 ) : Promise < McpResourceTemplate [ ] > {
899982 try {
900983 const connection = this . findConnection ( serverName , source )
901- if ( ! connection ) {
984+ if ( ! connection || connection . type !== "connected" ) {
902985 return [ ]
903986 }
904987 const response = await connection . client . request (
@@ -913,15 +996,20 @@ export class McpHub {
913996 }
914997
915998 async deleteConnection ( name : string , source ?: "global" | "project" ) : Promise < void > {
999+ // Clean up file watchers for this server
1000+ this . removeFileWatchersForServer ( name )
1001+
9161002 // If source is provided, only delete connections from that source
9171003 const connections = source
9181004 ? this . connections . filter ( ( conn ) => conn . server . name === name && conn . server . source === source )
9191005 : this . connections . filter ( ( conn ) => conn . server . name === name )
9201006
9211007 for ( const connection of connections ) {
9221008 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+ }
9251013 } catch ( error ) {
9261014 console . error ( `Failed to close transport for ${ name } :` , error )
9271015 }
@@ -975,15 +1063,21 @@ export class McpHub {
9751063 if ( ! currentConnection ) {
9761064 // New server
9771065 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+ }
9791070 await this . connectToServer ( name , validatedConfig , source )
9801071 } catch ( error ) {
9811072 this . showErrorMessage ( `Failed to connect to new MCP server ${ name } ` , error )
9821073 }
9831074 } else if ( ! deepEqual ( JSON . parse ( currentConnection . server . config ) , config ) ) {
9841075 // Existing server with changed config
9851076 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+ }
9871081 await this . deleteConnection ( name , source )
9881082 await this . connectToServer ( name , validatedConfig , source )
9891083 } catch ( error ) {
@@ -1066,10 +1160,21 @@ export class McpHub {
10661160 this . fileWatchers . clear ( )
10671161 }
10681162
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+
10691171 async restartConnection ( serverName : string , source ?: "global" | "project" ) : Promise < void > {
10701172 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
10731178 return
10741179 }
10751180
@@ -1111,6 +1216,23 @@ export class McpHub {
11111216 return
11121217 }
11131218
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+
11141236 this . isConnecting = true
11151237 vscode . window . showInformationMessage ( t ( "mcp:info.refreshing_all" ) )
11161238
@@ -1257,8 +1379,21 @@ export class McpHub {
12571379 try {
12581380 connection . server . disabled = disabled
12591381
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
12621397 connection . server . tools = await this . fetchToolsList ( serverName , serverSource )
12631398 connection . server . resources = await this . fetchResourcesList ( serverName , serverSource )
12641399 connection . server . resourceTemplates = await this . fetchResourceTemplatesList (
@@ -1439,7 +1574,7 @@ export class McpHub {
14391574
14401575 async readResource ( serverName : string , uri : string , source ?: "global" | "project" ) : Promise < McpResourceResponse > {
14411576 const connection = this . findConnection ( serverName , source )
1442- if ( ! connection ) {
1577+ if ( ! connection || connection . type !== "connected" ) {
14431578 throw new Error ( `No connection found for server: ${ serverName } ${ source ? ` with source ${ source } ` : "" } ` )
14441579 }
14451580 if ( connection . server . disabled ) {
@@ -1463,7 +1598,7 @@ export class McpHub {
14631598 source ?: "global" | "project" ,
14641599 ) : Promise < McpToolCallResponse > {
14651600 const connection = this . findConnection ( serverName , source )
1466- if ( ! connection ) {
1601+ if ( ! connection || connection . type !== "connected" ) {
14671602 throw new Error (
14681603 `No connection found for server: ${ serverName } ${ source ? ` with source ${ source } ` : "" } . Please make sure to use MCP servers available under 'Connected MCP Servers'.` ,
14691604 )
@@ -1609,6 +1744,64 @@ export class McpHub {
16091744 }
16101745 }
16111746
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+
16121805 async dispose ( ) : Promise < void > {
16131806 // Prevent multiple disposals
16141807 if ( this . isDisposed ) {
0 commit comments