Skip to content

Commit 7a865e2

Browse files
roomote[bot]roomote-agenthannesrudolphdaniel-lxs
authored
fix: prevent disabled MCP servers from starting processes and show correct status (#6084)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: hannesrudolph <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent 263e317 commit 7a865e2

File tree

4 files changed

+1396
-250
lines changed

4 files changed

+1396
-250
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,13 @@ export const webviewMessageHandler = async (
901901
case "mcpEnabled":
902902
const mcpEnabled = message.bool ?? true
903903
await updateGlobalState("mcpEnabled", mcpEnabled)
904+
905+
// Delegate MCP enable/disable logic to McpHub
906+
const mcpHubInstance = provider.getMcpHub()
907+
if (mcpHubInstance) {
908+
await mcpHubInstance.handleMcpEnabledChange(mcpEnabled)
909+
}
910+
904911
await provider.postStateToWebview()
905912
break
906913
case "enableMcpServerCreation":

src/services/mcp/McpHub.ts

Lines changed: 209 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,29 @@ import { fileExistsAtPath } from "../../utils/fs"
3333
import { arePathsEqual } from "../../utils/path"
3434
import { 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
4360
const 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

Comments
 (0)