feat(mcp): add enable/disable commands for MCP servers (#11057) (#16299)

Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
Jasmeet Bhatia
2026-01-22 15:38:06 -08:00
committed by GitHub
parent 35feea8868
commit a060e6149a
16 changed files with 1068 additions and 48 deletions
+167
View File
@@ -23,6 +23,12 @@ import {
} from '@google/gemini-cli-core';
import { appEvents, AppEvent } from '../../utils/events.js';
import { MessageType, type HistoryItemMcpStatus } from '../types.js';
import {
McpServerEnablementManager,
normalizeServerId,
canLoadServer,
} from '../../config/mcp/mcpServerEnablement.js';
import { loadSettings } from '../../config/settings.js';
const authCommand: SlashCommand = {
name: 'auth',
@@ -241,6 +247,14 @@ const listAction = async (
}
}
// Get enablement state for all servers
const enablementManager = McpServerEnablementManager.getInstance();
const enablementState: HistoryItemMcpStatus['enablementState'] = {};
for (const serverName of serverNames) {
enablementState[serverName] =
await enablementManager.getDisplayState(serverName);
}
const mcpStatusItem: HistoryItemMcpStatus = {
type: MessageType.MCP_STATUS,
servers: mcpServers,
@@ -263,6 +277,7 @@ const listAction = async (
description: resource.description,
})),
authStatus,
enablementState,
blockedServers: blockedMcpServers,
discoveryInProgress,
connectingServers,
@@ -346,6 +361,156 @@ const refreshCommand: SlashCommand = {
},
};
async function handleEnableDisable(
context: CommandContext,
args: string,
enable: boolean,
): Promise<MessageActionReturn> {
const { config } = context.services;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: 'Config not loaded.',
};
}
const parts = args.trim().split(/\s+/);
const isSession = parts.includes('--session');
const serverName = parts.filter((p) => p !== '--session')[0];
const action = enable ? 'enable' : 'disable';
if (!serverName) {
return {
type: 'message',
messageType: 'error',
content: `Server name required. Usage: /mcp ${action} <server-name> [--session]`,
};
}
const name = normalizeServerId(serverName);
// Validate server exists
const servers = config.getMcpClientManager()?.getMcpServers() || {};
const normalizedServerNames = Object.keys(servers).map(normalizeServerId);
if (!normalizedServerNames.includes(name)) {
return {
type: 'message',
messageType: 'error',
content: `Server '${serverName}' not found. Use /mcp list to see available servers.`,
};
}
// Check if server is from an extension
const serverKey = Object.keys(servers).find(
(key) => normalizeServerId(key) === name,
);
const server = serverKey ? servers[serverKey] : undefined;
if (server?.extension) {
return {
type: 'message',
messageType: 'error',
content: `Server '${serverName}' is provided by extension '${server.extension.name}'.\nUse '/extensions ${action} ${server.extension.name}' to manage this extension.`,
};
}
const manager = McpServerEnablementManager.getInstance();
if (enable) {
const settings = loadSettings();
const result = await canLoadServer(name, {
adminMcpEnabled: settings.merged.admin?.mcp?.enabled ?? true,
allowedList: settings.merged.mcp?.allowed,
excludedList: settings.merged.mcp?.excluded,
});
if (
!result.allowed &&
(result.blockType === 'allowlist' || result.blockType === 'excludelist')
) {
return {
type: 'message',
messageType: 'error',
content: result.reason ?? 'Blocked by settings.',
};
}
if (isSession) {
manager.clearSessionDisable(name);
} else {
await manager.enable(name);
}
if (result.blockType === 'admin') {
context.ui.addItem(
{
type: 'warning',
text: 'MCP disabled by admin. Will load when enabled.',
},
Date.now(),
);
}
} else {
if (isSession) {
manager.disableForSession(name);
} else {
await manager.disable(name);
}
}
const msg = `MCP server '${name}' ${enable ? 'enabled' : 'disabled'}${isSession ? ' for this session' : ''}.`;
const mcpClientManager = config.getMcpClientManager();
if (mcpClientManager) {
context.ui.addItem(
{ type: 'info', text: 'Restarting MCP servers...' },
Date.now(),
);
await mcpClientManager.restart();
}
if (config.getGeminiClient()?.isInitialized())
await config.getGeminiClient().setTools();
context.ui.reloadCommands();
return { type: 'message', messageType: 'info', content: msg };
}
async function getEnablementCompletion(
context: CommandContext,
partialArg: string,
showEnabled: boolean,
): Promise<string[]> {
const { config } = context.services;
if (!config) return [];
const servers = Object.keys(
config.getMcpClientManager()?.getMcpServers() || {},
);
const manager = McpServerEnablementManager.getInstance();
const results: string[] = [];
for (const n of servers) {
const state = await manager.getDisplayState(n);
if (state.enabled === showEnabled && n.startsWith(partialArg)) {
results.push(n);
}
}
return results;
}
const enableCommand: SlashCommand = {
name: 'enable',
description: 'Enable a disabled MCP server',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (ctx, args) => handleEnableDisable(ctx, args, true),
completion: (ctx, arg) => getEnablementCompletion(ctx, arg, false),
};
const disableCommand: SlashCommand = {
name: 'disable',
description: 'Disable an MCP server',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (ctx, args) => handleEnableDisable(ctx, args, false),
completion: (ctx, arg) => getEnablementCompletion(ctx, arg, true),
};
export const mcpCommand: SlashCommand = {
name: 'mcp',
description: 'Manage configured Model Context Protocol (MCP) servers',
@@ -357,6 +522,8 @@ export const mcpCommand: SlashCommand = {
schemaCommand,
authCommand,
refreshCommand,
enableCommand,
disableCommand,
],
action: async (context: CommandContext) => listAction(context),
};
@@ -40,6 +40,13 @@ describe('McpStatus', () => {
blockedServers: [],
serverStatus: () => MCPServerStatus.CONNECTED,
authStatus: {},
enablementState: {
'server-1': {
enabled: true,
isSessionDisabled: false,
isPersistentDisabled: false,
},
},
discoveryInProgress: false,
connectingServers: [],
showDescriptions: true,
@@ -25,6 +25,7 @@ interface McpStatusProps {
blockedServers: Array<{ name: string; extensionName: string }>;
serverStatus: (serverName: string) => MCPServerStatus;
authStatus: HistoryItemMcpStatus['authStatus'];
enablementState: HistoryItemMcpStatus['enablementState'];
discoveryInProgress: boolean;
connectingServers: string[];
showDescriptions: boolean;
@@ -39,6 +40,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
blockedServers,
serverStatus,
authStatus,
enablementState,
discoveryInProgress,
connectingServers,
showDescriptions,
@@ -104,23 +106,35 @@ export const McpStatus: React.FC<McpStatusProps> = ({
let statusText = '';
let statusColor = theme.text.primary;
switch (status) {
case MCPServerStatus.CONNECTED:
statusIndicator = '🟢';
statusText = 'Ready';
statusColor = theme.status.success;
break;
case MCPServerStatus.CONNECTING:
statusIndicator = '🔄';
statusText = 'Starting... (first startup may take longer)';
statusColor = theme.status.warning;
break;
case MCPServerStatus.DISCONNECTED:
default:
statusIndicator = '🔴';
statusText = 'Disconnected';
statusColor = theme.status.error;
break;
// Check enablement state
const serverEnablement = enablementState[serverName];
const isDisabled = serverEnablement && !serverEnablement.enabled;
if (isDisabled) {
statusIndicator = '⏸️';
statusText = serverEnablement.isSessionDisabled
? 'Disabled (session)'
: 'Disabled';
statusColor = theme.text.secondary;
} else {
switch (status) {
case MCPServerStatus.CONNECTED:
statusIndicator = '🟢';
statusText = 'Ready';
statusColor = theme.status.success;
break;
case MCPServerStatus.CONNECTING:
statusIndicator = '🔄';
statusText = 'Starting... (first startup may take longer)';
statusColor = theme.status.warning;
break;
case MCPServerStatus.DISCONNECTED:
default:
statusIndicator = '🔴';
statusText = 'Disconnected';
statusColor = theme.status.error;
break;
}
}
let serverDisplayName = serverName;
+8
View File
@@ -270,6 +270,14 @@ export type HistoryItemMcpStatus = HistoryItemBase & {
string,
'authenticated' | 'expired' | 'unauthenticated' | 'not-configured'
>;
enablementState: Record<
string,
{
enabled: boolean;
isSessionDisabled: boolean;
isPersistentDisabled: boolean;
}
>;
blockedServers: Array<{ name: string; extensionName: string }>;
discoveryInProgress: boolean;
connectingServers: string[];