Enable/disable MCP servers

This commit is contained in:
Saoud Rizwan 2025-01-19 14:19:27 -08:00
parent 89b9b56b74
commit fc82e95beb
6 changed files with 153 additions and 1 deletions

View file

@ -717,6 +717,8 @@ npm run build
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and alwaysAllow=[].
\`\`\`json
{
"mcpServers": {

View file

@ -608,6 +608,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "toggleMcpServer": {
try {
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
} catch (error) {
console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
}
break
}
case "toggleToolAlwaysAllow": {
try {
await this.mcpHub?.toggleToolAlwaysAllow(message.serverName!, message.toolName!, message.alwaysAllow!)

View file

@ -33,6 +33,7 @@ const StdioConfigSchema = z.object({
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
alwaysAllow: AlwaysAllowSchema.optional(),
disabled: z.boolean().optional(),
})
const McpSettingsSchema = z.object({
@ -54,7 +55,8 @@ export class McpHub {
}
getServers(): McpServer[] {
return this.connections.map((conn) => conn.server)
// Only return enabled servers
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
}
async getMcpServersPath(): Promise<string> {
@ -192,11 +194,13 @@ export class McpHub {
}
// valid schema
const parsedConfig = StdioConfigSchema.parse(config)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "connecting",
disabled: parsedConfig.disabled,
},
client,
transport,
@ -458,11 +462,91 @@ export class McpHub {
// Using server
// Public methods for server management
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
let settingsPath: string
try {
settingsPath = await this.getMcpSettingsFilePath()
// Ensure the settings file exists and is accessible
try {
await fs.access(settingsPath)
} catch (error) {
console.error("Settings file not accessible:", error)
throw new Error("Settings file not accessible")
}
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
// Validate the config structure
if (!config || typeof config !== "object") {
throw new Error("Invalid config structure")
}
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
if (config.mcpServers[serverName]) {
// Create a new server config object to ensure clean structure
const serverConfig = {
...config.mcpServers[serverName],
disabled,
}
// Ensure required fields exist
if (!serverConfig.alwaysAllow) {
serverConfig.alwaysAllow = []
}
config.mcpServers[serverName] = serverConfig
// Write the entire config back
const updatedConfig = {
mcpServers: config.mcpServers,
}
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection) {
try {
connection.server.disabled = disabled
// Only refresh capabilities if connected
if (connection.server.status === "connected") {
connection.server.tools = await this.fetchToolsList(serverName)
connection.server.resources = await this.fetchResourcesList(serverName)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
}
} catch (error) {
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
}
}
await this.notifyWebviewOfServerChanges()
}
} catch (error) {
console.error("Failed to update server disabled state:", error)
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
vscode.window.showErrorMessage(
`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
)
throw error
}
}
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
if (connection.server.disabled) {
throw new Error(`Server "${serverName}" is disabled`)
}
return await connection.client.request(
{
method: "resources/read",
@ -481,6 +565,11 @@ export class McpHub {
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
)
}
if (connection.server.disabled) {
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
}
return await connection.client.request(
{
method: "tools/call",

View file

@ -36,8 +36,10 @@ export interface WebviewMessage {
| "openAdvisorModelSettings"
| "requestVsCodeLmModels"
| "toggleToolAlwaysAllow"
| "toggleMcpServer"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]

View file

@ -6,6 +6,7 @@ export type McpServer = {
tools?: McpTool[]
resources?: McpResource[]
resourceTemplates?: McpResourceTemplate[]
disabled?: boolean
}
export type McpTool = {

View file

@ -190,12 +190,62 @@ const ServerRow = ({ server }: { server: McpServer }) => {
background: "var(--vscode-textCodeBlock-background)",
cursor: server.error ? "default" : "pointer",
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
opacity: server.disabled ? 0.6 : 1,
}}
onClick={handleRowClick}>
{!server.error && (
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
)}
<span style={{ flex: 1 }}>{server.name}</span>
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
<div
role="switch"
aria-checked={!server.disabled}
tabIndex={0}
style={{
width: "20px",
height: "10px",
backgroundColor: server.disabled
? "var(--vscode-titleBar-inactiveForeground)"
: "var(--vscode-testing-iconPassed)",
borderRadius: "5px",
position: "relative",
cursor: "pointer",
transition: "background-color 0.2s",
opacity: server.disabled ? 0.5 : 0.9,
}}
onClick={() => {
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled,
})
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled,
})
}
}}>
<div
style={{
width: "6px",
height: "6px",
backgroundColor: "white",
border: "1px solid #666666",
borderRadius: "50%",
position: "absolute",
top: "1px",
left: server.disabled ? "2px" : "12px",
transition: "left 0.2s",
}}
/>
</div>
</div>
<div
style={{
width: "8px",