Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 139 additions & 5 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,32 @@ export class McpHub {
// New server
try {
this.setupFileWatcher(name, validatedConfig, source)
await this.connectToServer(name, validatedConfig, source)
if (!validatedConfig.disabled) {
await this.connectToServer(name, validatedConfig, source)
} else {
// Create a disconnected server entry for disabled servers
const disconnectedServer: McpConnection = {
server: {
name,
config: JSON.stringify(validatedConfig),
status: "disconnected",
disabled: true,
source,
projectPath:
source === "project"
? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
: undefined,
errorHistory: [],
error: "Server disabled",
tools: [],
resources: [],
resourceTemplates: [],
},
client: {} as any,
transport: {} as any,
}
this.connections.push(disconnectedServer)
}
} catch (error) {
this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error)
}
Expand All @@ -985,9 +1010,85 @@ export class McpHub {
try {
this.setupFileWatcher(name, validatedConfig, source)
await this.deleteConnection(name, source)
await this.connectToServer(name, validatedConfig, source)
if (!validatedConfig.disabled) {
await this.connectToServer(name, validatedConfig, source)
} else {
// Create a disconnected server entry for disabled servers
const disconnectedServer: McpConnection = {
server: {
name,
config: JSON.stringify(validatedConfig),
status: "disconnected",
disabled: true,
source,
projectPath:
source === "project"
? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
: undefined,
errorHistory: currentConnection.server.errorHistory || [],
error: "Server disabled",
tools: [],
resources: [],
resourceTemplates: [],
},
client: {} as any,
transport: {} as any,
}
this.connections.push(disconnectedServer)
}
} catch (error) {
this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error)
// If this was a disabled server, still create the disconnected entry
if (validatedConfig.disabled) {
const disconnectedServer: McpConnection = {
server: {
name,
config: JSON.stringify(validatedConfig),
status: "disconnected",
disabled: true,
source,
projectPath:
source === "project"
? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
: undefined,
errorHistory: currentConnection.server.errorHistory || [],
error: "Server disabled",
tools: [],
resources: [],
resourceTemplates: [],
},
client: {} as any,
transport: {} as any,
}
this.connections.push(disconnectedServer)
}
}
} else if (validatedConfig.disabled && currentConnection.server.status === "connected") {
// Existing server that became disabled - disconnect it
try {
await this.deleteConnection(name, source)
// Create a disconnected server entry to maintain the server in the list
const disconnectedServer: McpConnection = {
server: {
name,
config: JSON.stringify(validatedConfig),
status: "disconnected",
disabled: true,
source,
projectPath:
source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
errorHistory: currentConnection.server.errorHistory || [],
error: "Server disabled",
tools: [],
resources: [],
resourceTemplates: [],
},
client: currentConnection.client,
transport: currentConnection.transport,
}
this.connections.push(disconnectedServer)
} catch (error) {
this.showErrorMessage(`Failed to disconnect disabled MCP server ${name}`, error)
}
}
// If server exists with same config, do nothing
Expand Down Expand Up @@ -1257,8 +1358,41 @@ export class McpHub {
try {
connection.server.disabled = disabled

// Only refresh capabilities if connected
if (connection.server.status === "connected") {
if (disabled && connection.server.status === "connected") {
// If disabling a connected server, disconnect it
await this.deleteConnection(serverName, serverSource)
// Create a disconnected server entry to maintain the server in the list
const disconnectedServer: McpConnection = {
server: {
name: serverName,
config: connection.server.config,
status: "disconnected",
disabled: true,
source: serverSource,
projectPath: connection.server.projectPath,
errorHistory: connection.server.errorHistory || [],
error: "Server disabled",
tools: [],
resources: [],
resourceTemplates: [],
},
client: connection.client,
transport: connection.transport,
}
this.connections.push(disconnectedServer)
} else if (!disabled && connection.server.status === "disconnected") {
// If enabling a disconnected server, try to reconnect it
try {
const parsedConfig = JSON.parse(connection.server.config)
const validatedConfig = this.validateServerConfig(parsedConfig, serverName)
await this.deleteConnection(serverName, serverSource)
await this.connectToServer(serverName, validatedConfig, serverSource)
} catch (reconnectError) {
console.error(`Failed to reconnect server ${serverName}:`, reconnectError)
connection.server.error = `Failed to reconnect: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`
}
} else if (connection.server.status === "connected") {
// Only refresh capabilities if still connected and not disabled
connection.server.tools = await this.fetchToolsList(serverName, serverSource)
connection.server.resources = await this.fetchResourcesList(serverName, serverSource)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(
Expand All @@ -1267,7 +1401,7 @@ export class McpHub {
)
}
} catch (error) {
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
console.error(`Failed to handle server state change for ${serverName}:`, error)
}
}

Expand Down
186 changes: 185 additions & 1 deletion src/services/mcp/__tests__/McpHub.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ vi.mock("vscode", () => ({
Disposable: {
from: vi.fn(),
},
Uri: {
file: vi.fn().mockImplementation((path: string) => ({
scheme: "file",
authority: "",
path: path,
query: "",
fragment: "",
fsPath: path,
with: vi.fn(),
toJSON: vi.fn(),
})),
},
RelativePattern: vi.fn().mockImplementation((base: string, pattern: string) => ({
base,
pattern,
})),
}))
vi.mock("fs/promises")
vi.mock("../../../core/webview/ClineProvider")
Expand Down Expand Up @@ -93,7 +109,6 @@ describe("McpHub", () => {
// Mock console.error to suppress error messages during tests
console.error = vi.fn()


const mockUri: Uri = {
scheme: "file",
authority: "",
Expand Down Expand Up @@ -570,6 +585,175 @@ describe("McpHub", () => {
'Server "disabled-server" is disabled',
)
})

it("should disconnect server when disabled via toggleServerDisabled", async () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabled: false,
},
},
}

// Mock reading initial config
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockConfig))

// Set up mock connection with connected status
const mockConnection: McpConnection = {
server: {
name: "test-server",
config: JSON.stringify({
type: "stdio",
command: "node",
args: ["test.js"],
disabled: false,
}),
status: "connected",
disabled: false,
source: "global",
errorHistory: [],
},
client: {
close: vi.fn().mockResolvedValue(undefined),
} as any,
transport: {
close: vi.fn().mockResolvedValue(undefined),
} as any,
}
mcpHub.connections = [mockConnection]

await mcpHub.toggleServerDisabled("test-server", true)

// Verify the server was disconnected and marked as disabled
const updatedConnection = mcpHub.connections.find((conn) => conn.server.name === "test-server")
expect(updatedConnection).toBeDefined()
expect(updatedConnection?.server.status).toBe("disconnected")
expect(updatedConnection?.server.disabled).toBe(true)
expect(updatedConnection?.server.error).toBe("Server disabled")

// Verify transport and client were closed
expect(mockConnection.transport.close).toHaveBeenCalled()
expect(mockConnection.client.close).toHaveBeenCalled()
})

it("should reconnect server when enabled via toggleServerDisabled", async () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabled: true,
},
},
}

// Mock reading initial config
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockConfig))

// Set up mock connection with disconnected status
const mockConnection: McpConnection = {
server: {
name: "test-server",
config: JSON.stringify({
type: "stdio",
command: "node",
args: ["test.js"],
disabled: true,
}),
status: "disconnected",
disabled: true,
source: "global",
errorHistory: [],
},
client: {} as any,
transport: {} as any,
}
mcpHub.connections = [mockConnection]

// Mock the connectToServer method to avoid actual connection
const connectToServerSpy = vi.spyOn(mcpHub as any, "connectToServer").mockResolvedValue(undefined)

await mcpHub.toggleServerDisabled("test-server", false)

// Verify connectToServer was called to reconnect
expect(connectToServerSpy).toHaveBeenCalledWith("test-server", expect.any(Object), "global")
})

it("should disconnect server when disabled via updateServerConnections", async () => {
// Set up mock connection with connected status
const mockConnection: McpConnection = {
server: {
name: "test-server",
config: JSON.stringify({
type: "stdio",
command: "node",
args: ["test.js"],
disabled: false,
}),
status: "connected",
disabled: false,
source: "global",
errorHistory: [],
},
client: {
close: vi.fn().mockResolvedValue(undefined),
} as any,
transport: {
close: vi.fn().mockResolvedValue(undefined),
} as any,
}
mcpHub.connections = [mockConnection]

// Update with disabled config
const newServers = {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabled: true,
},
}

await mcpHub.updateServerConnections(newServers, "global", false)

// Debug: Log the connections after update
console.log(
"Connections after update:",
mcpHub.connections.map((c) => ({
name: c.server.name,
status: c.server.status,
disabled: c.server.disabled,
})),
)
console.log("Total connections:", mcpHub.connections.length)
console.log("Looking for server:", "test-server")

// Verify the server was disconnected and marked as disabled
const updatedConnection = mcpHub.connections.find((conn) => conn.server.name === "test-server")
console.log(
"Found connection:",
updatedConnection
? {
name: updatedConnection.server.name,
status: updatedConnection.server.status,
disabled: updatedConnection.server.disabled,
}
: "undefined",
)
expect(updatedConnection).toBeDefined()
expect(updatedConnection?.server.status).toBe("disconnected")
expect(updatedConnection?.server.disabled).toBe(true)
// The error message should indicate the server is disabled (could be "Server disabled" or contain error details)
expect(updatedConnection?.server.error).toBeTruthy()

// Verify transport and client were closed
expect(mockConnection.transport.close).toHaveBeenCalled()
expect(mockConnection.client.close).toHaveBeenCalled()
})
})

describe("callTool", () => {
Expand Down