Skip to content

Commit 78b784b

Browse files
committed
fix: address PR #6084 review feedback
- Extract duplicate placeholder connection creation logic into createPlaceholderConnection helper - Fix type safety by allowing null client/transport in McpConnection type - Add proper null checks throughout the codebase - Add error handling to server disconnection loop in webviewMessageHandler - Centralize MCP enabled state checking with isMcpEnabled helper - Extract repeated UI condition into isExpandable computed property - Add comprehensive test coverage for edge cases including: - Toggling global MCP enabled state while servers are active - Handling refreshAllConnections when MCP is disabled - Skipping connection restart when MCP is disabled
1 parent 59e9f87 commit 78b784b

File tree

4 files changed

+298
-88
lines changed

4 files changed

+298
-88
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -884,17 +884,54 @@ export const webviewMessageHandler = async (
884884
// If MCP is being disabled, disconnect all servers
885885
const mcpHubInstance = provider.getMcpHub()
886886
if (!mcpEnabled && mcpHubInstance) {
887-
// Disconnect all existing connections
887+
// Disconnect all existing connections with error handling
888888
const existingConnections = [...mcpHubInstance.connections]
889+
const disconnectionErrors: Array<{ serverName: string; error: string }> = []
890+
889891
for (const conn of existingConnections) {
890-
await mcpHubInstance.deleteConnection(conn.server.name, conn.server.source)
892+
try {
893+
await mcpHubInstance.deleteConnection(conn.server.name, conn.server.source)
894+
} catch (error) {
895+
const errorMessage = error instanceof Error ? error.message : String(error)
896+
disconnectionErrors.push({
897+
serverName: conn.server.name,
898+
error: errorMessage,
899+
})
900+
provider.log(`Failed to disconnect MCP server ${conn.server.name}: ${errorMessage}`)
901+
}
902+
}
903+
904+
// If there were errors, notify the user
905+
if (disconnectionErrors.length > 0) {
906+
const errorSummary = disconnectionErrors.map((e) => `${e.serverName}: ${e.error}`).join("\n")
907+
vscode.window.showWarningMessage(
908+
t("mcp:errors.disconnect_servers_partial", {
909+
count: disconnectionErrors.length,
910+
errors: errorSummary,
911+
}) ||
912+
`Failed to disconnect ${disconnectionErrors.length} MCP server(s). Check the output for details.`,
913+
)
891914
}
892915

893916
// Re-initialize servers to track them in disconnected state
894-
await mcpHubInstance.refreshAllConnections()
917+
try {
918+
await mcpHubInstance.refreshAllConnections()
919+
} catch (error) {
920+
provider.log(`Failed to refresh MCP connections after disabling: ${error}`)
921+
vscode.window.showErrorMessage(
922+
t("mcp:errors.refresh_after_disable") || "Failed to refresh MCP connections after disabling",
923+
)
924+
}
895925
} else if (mcpEnabled && mcpHubInstance) {
896926
// If MCP is being enabled, reconnect all servers
897-
await mcpHubInstance.refreshAllConnections()
927+
try {
928+
await mcpHubInstance.refreshAllConnections()
929+
} catch (error) {
930+
provider.log(`Failed to refresh MCP connections after enabling: ${error}`)
931+
vscode.window.showErrorMessage(
932+
t("mcp:errors.refresh_after_enable") || "Failed to refresh MCP connections after enabling",
933+
)
934+
}
898935
}
899936

900937
await provider.postStateToWebview()

src/services/mcp/McpHub.ts

Lines changed: 76 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import { injectVariables } from "../../utils/config"
3535

3636
export type McpConnection = {
3737
server: McpServer
38-
client: Client
39-
transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
38+
client: Client | null
39+
transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport | null
4040
}
4141

4242
// Base configuration schema for common settings
@@ -553,6 +553,48 @@ export class McpHub {
553553
await this.initializeMcpServers("project")
554554
}
555555

556+
/**
557+
* Creates a placeholder connection for disabled servers or when MCP is globally disabled
558+
* @param name The server name
559+
* @param config The server configuration
560+
* @param source The source of the server (global or project)
561+
* @param reason The reason for creating a placeholder (mcpDisabled or serverDisabled)
562+
* @returns A placeholder McpConnection object
563+
*/
564+
private createPlaceholderConnection(
565+
name: string,
566+
config: z.infer<typeof ServerConfigSchema>,
567+
source: "global" | "project",
568+
reason: "mcpDisabled" | "serverDisabled",
569+
): McpConnection {
570+
return {
571+
server: {
572+
name,
573+
config: JSON.stringify(config),
574+
status: "disconnected",
575+
disabled: reason === "serverDisabled" ? true : config.disabled,
576+
source,
577+
projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
578+
errorHistory: [],
579+
},
580+
client: null,
581+
transport: null,
582+
}
583+
}
584+
585+
/**
586+
* Checks if MCP is globally enabled
587+
* @returns Promise<boolean> indicating if MCP is enabled
588+
*/
589+
private async isMcpEnabled(): Promise<boolean> {
590+
const provider = this.providerRef.deref()
591+
if (!provider) {
592+
return true // Default to enabled if provider is not available
593+
}
594+
const state = await provider.getState()
595+
return state.mcpEnabled ?? true
596+
}
597+
556598
private async connectToServer(
557599
name: string,
558600
config: z.infer<typeof ServerConfigSchema>,
@@ -562,49 +604,18 @@ export class McpHub {
562604
await this.deleteConnection(name, source)
563605

564606
// Check if MCP is globally enabled
565-
const provider = this.providerRef.deref()
566-
if (provider) {
567-
const state = await provider.getState()
568-
const mcpEnabled = state.mcpEnabled ?? true
569-
570-
// Skip connecting if MCP is globally disabled
571-
if (!mcpEnabled) {
572-
// Still create a connection object to track the server, but don't actually connect
573-
const connection: McpConnection = {
574-
server: {
575-
name,
576-
config: JSON.stringify(config),
577-
status: "disconnected",
578-
disabled: config.disabled,
579-
source,
580-
projectPath:
581-
source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
582-
errorHistory: [],
583-
},
584-
client: null as any, // We won't actually create a client when MCP is disabled
585-
transport: null as any, // We won't actually create a transport when MCP is disabled
586-
}
587-
this.connections.push(connection)
588-
return
589-
}
607+
const mcpEnabled = await this.isMcpEnabled()
608+
if (!mcpEnabled) {
609+
// Still create a connection object to track the server, but don't actually connect
610+
const connection = this.createPlaceholderConnection(name, config, source, "mcpDisabled")
611+
this.connections.push(connection)
612+
return
590613
}
591614

592615
// Skip connecting to disabled servers
593616
if (config.disabled) {
594617
// Still create a connection object to track the server, but don't actually connect
595-
const connection: McpConnection = {
596-
server: {
597-
name,
598-
config: JSON.stringify(config),
599-
status: "disconnected",
600-
disabled: true,
601-
source,
602-
projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
603-
errorHistory: [],
604-
},
605-
client: null as any, // We won't actually create a client for disabled servers
606-
transport: null as any, // We won't actually create a transport for disabled servers
607-
}
618+
const connection = this.createPlaceholderConnection(name, config, source, "serverDisabled")
608619
this.connections.push(connection)
609620
return
610621
}
@@ -875,8 +886,8 @@ export class McpHub {
875886
// Use the helper method to find the connection
876887
const connection = this.findConnection(serverName, source)
877888

878-
if (!connection) {
879-
throw new Error(`Server ${serverName} not found`)
889+
if (!connection || !connection.client) {
890+
throw new Error(`Server ${serverName} not found or not connected`)
880891
}
881892

882893
const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema)
@@ -930,7 +941,7 @@ export class McpHub {
930941
private async fetchResourcesList(serverName: string, source?: "global" | "project"): Promise<McpResource[]> {
931942
try {
932943
const connection = this.findConnection(serverName, source)
933-
if (!connection) {
944+
if (!connection || !connection.client) {
934945
return []
935946
}
936947
const response = await connection.client.request({ method: "resources/list" }, ListResourcesResultSchema)
@@ -947,7 +958,7 @@ export class McpHub {
947958
): Promise<McpResourceTemplate[]> {
948959
try {
949960
const connection = this.findConnection(serverName, source)
950-
if (!connection) {
961+
if (!connection || !connection.client) {
951962
return []
952963
}
953964
const response = await connection.client.request(
@@ -969,8 +980,12 @@ export class McpHub {
969980

970981
for (const connection of connections) {
971982
try {
972-
await connection.transport.close()
973-
await connection.client.close()
983+
if (connection.transport) {
984+
await connection.transport.close()
985+
}
986+
if (connection.client) {
987+
await connection.client.close()
988+
}
974989
} catch (error) {
975990
console.error(`Failed to close transport for ${name}:`, error)
976991
}
@@ -1123,16 +1138,9 @@ export class McpHub {
11231138

11241139
async restartConnection(serverName: string, source?: "global" | "project"): Promise<void> {
11251140
this.isConnecting = true
1126-
const provider = this.providerRef.deref()
1127-
if (!provider) {
1128-
return
1129-
}
11301141

11311142
// Check if MCP is globally enabled
1132-
const state = await provider.getState()
1133-
const mcpEnabled = state.mcpEnabled ?? true
1134-
1135-
// Skip restarting if MCP is globally disabled
1143+
const mcpEnabled = await this.isMcpEnabled()
11361144
if (!mcpEnabled) {
11371145
this.isConnecting = false
11381146
return
@@ -1177,26 +1185,20 @@ export class McpHub {
11771185
}
11781186

11791187
// Check if MCP is globally enabled
1180-
const provider = this.providerRef.deref()
1181-
if (provider) {
1182-
const state = await provider.getState()
1183-
const mcpEnabled = state.mcpEnabled ?? true
1184-
1185-
// Skip refreshing if MCP is globally disabled
1186-
if (!mcpEnabled) {
1187-
// Clear all existing connections
1188-
const existingConnections = [...this.connections]
1189-
for (const conn of existingConnections) {
1190-
await this.deleteConnection(conn.server.name, conn.server.source)
1191-
}
1188+
const mcpEnabled = await this.isMcpEnabled()
1189+
if (!mcpEnabled) {
1190+
// Clear all existing connections
1191+
const existingConnections = [...this.connections]
1192+
for (const conn of existingConnections) {
1193+
await this.deleteConnection(conn.server.name, conn.server.source)
1194+
}
11921195

1193-
// Still initialize servers to track them, but they won't connect
1194-
await this.initializeMcpServers("global")
1195-
await this.initializeMcpServers("project")
1196+
// Still initialize servers to track them, but they won't connect
1197+
await this.initializeMcpServers("global")
1198+
await this.initializeMcpServers("project")
11961199

1197-
await this.notifyWebviewOfServerChanges()
1198-
return
1199-
}
1200+
await this.notifyWebviewOfServerChanges()
1201+
return
12001202
}
12011203

12021204
this.isConnecting = true
@@ -1537,7 +1539,7 @@ export class McpHub {
15371539

15381540
async readResource(serverName: string, uri: string, source?: "global" | "project"): Promise<McpResourceResponse> {
15391541
const connection = this.findConnection(serverName, source)
1540-
if (!connection) {
1542+
if (!connection || !connection.client) {
15411543
throw new Error(`No connection found for server: ${serverName}${source ? ` with source ${source}` : ""}`)
15421544
}
15431545
if (connection.server.disabled) {
@@ -1561,7 +1563,7 @@ export class McpHub {
15611563
source?: "global" | "project",
15621564
): Promise<McpToolCallResponse> {
15631565
const connection = this.findConnection(serverName, source)
1564-
if (!connection) {
1566+
if (!connection || !connection.client) {
15651567
throw new Error(
15661568
`No connection found for server: ${serverName}${source ? ` with source ${source}` : ""}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
15671569
)

0 commit comments

Comments
 (0)