mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
264 lines
7.3 KiB
TypeScript
264 lines
7.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// File for 'gemini mcp list' command
|
|
import type { CommandModule } from 'yargs';
|
|
import {
|
|
type MergedSettings,
|
|
loadSettings,
|
|
type LoadedSettings,
|
|
} from '../../config/settings.js';
|
|
import {
|
|
MCPServerStatus,
|
|
createTransport,
|
|
debugLogger,
|
|
applyAdminAllowlist,
|
|
getAdminBlockedMcpServersMessage,
|
|
} from '@google/gemini-cli-core';
|
|
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { ExtensionManager } from '../../config/extension-manager.js';
|
|
import {
|
|
canLoadServer,
|
|
McpServerEnablementManager,
|
|
} from '../../config/mcp/index.js';
|
|
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
|
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
|
import { exitCli } from '../utils.js';
|
|
import chalk from 'chalk';
|
|
|
|
export async function getMcpServersFromConfig(
|
|
settings?: MergedSettings,
|
|
): Promise<{
|
|
mcpServers: Record<string, MCPServerConfig>;
|
|
blockedServerNames: string[];
|
|
}> {
|
|
if (!settings) {
|
|
settings = loadSettings().merged;
|
|
}
|
|
|
|
const extensionManager = new ExtensionManager({
|
|
settings,
|
|
workspaceDir: process.cwd(),
|
|
requestConsent: requestConsentNonInteractive,
|
|
requestSetting: promptForSetting,
|
|
});
|
|
const extensions = await extensionManager.loadExtensions();
|
|
const mcpServers = { ...settings.mcpServers };
|
|
for (const extension of extensions) {
|
|
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
|
if (mcpServers[key]) {
|
|
return;
|
|
}
|
|
mcpServers[key] = {
|
|
...server,
|
|
extension,
|
|
};
|
|
});
|
|
}
|
|
|
|
const adminAllowlist = settings.admin?.mcp?.config;
|
|
const filteredResult = applyAdminAllowlist(mcpServers, adminAllowlist);
|
|
|
|
return filteredResult;
|
|
}
|
|
|
|
async function testMCPConnection(
|
|
serverName: string,
|
|
config: MCPServerConfig,
|
|
isTrusted: boolean,
|
|
activeSettings: MergedSettings,
|
|
): Promise<MCPServerStatus> {
|
|
// SECURITY: Only test connection if workspace is trusted or if it's a remote server.
|
|
// stdio servers execute local commands and must never run in untrusted workspaces.
|
|
const isStdio = !!config.command;
|
|
if (isStdio && !isTrusted) {
|
|
return MCPServerStatus.DISCONNECTED;
|
|
}
|
|
|
|
const client = new Client({
|
|
name: 'mcp-test-client',
|
|
version: '0.0.1',
|
|
});
|
|
|
|
const mcpContext = {
|
|
sanitizationConfig: {
|
|
enableEnvironmentVariableRedaction: true,
|
|
allowedEnvironmentVariables: [],
|
|
blockedEnvironmentVariables: activeSettings.advanced.excludedEnvVars,
|
|
},
|
|
emitMcpDiagnostic: (
|
|
severity: 'info' | 'warning' | 'error',
|
|
message: string,
|
|
error?: unknown,
|
|
serverName?: string,
|
|
) => {
|
|
// In non-interactive list, we log everything through debugLogger for consistency
|
|
if (severity === 'error') {
|
|
debugLogger.error(
|
|
chalk.red(`Error${serverName ? ` (${serverName})` : ''}: ${message}`),
|
|
error,
|
|
);
|
|
} else if (severity === 'warning') {
|
|
debugLogger.warn(
|
|
chalk.yellow(
|
|
`Warning${serverName ? ` (${serverName})` : ''}: ${message}`,
|
|
),
|
|
error,
|
|
);
|
|
} else {
|
|
debugLogger.log(message, error);
|
|
}
|
|
},
|
|
isTrustedFolder: () => isTrusted,
|
|
};
|
|
|
|
let transport;
|
|
try {
|
|
// Use the same transport creation logic as core
|
|
transport = await createTransport(serverName, config, false, mcpContext);
|
|
} catch (_error) {
|
|
await client.close();
|
|
return MCPServerStatus.DISCONNECTED;
|
|
}
|
|
|
|
try {
|
|
// Attempt actual MCP connection with short timeout
|
|
await client.connect(transport, { timeout: 5000 }); // 5s timeout
|
|
|
|
// Test basic MCP protocol by pinging the server
|
|
await client.ping();
|
|
|
|
await client.close();
|
|
return MCPServerStatus.CONNECTED;
|
|
} catch (_error) {
|
|
await transport.close();
|
|
return MCPServerStatus.DISCONNECTED;
|
|
}
|
|
}
|
|
|
|
async function getServerStatus(
|
|
serverName: string,
|
|
server: MCPServerConfig,
|
|
isTrusted: boolean,
|
|
activeSettings: MergedSettings,
|
|
): Promise<MCPServerStatus> {
|
|
const mcpEnablementManager = McpServerEnablementManager.getInstance();
|
|
const loadResult = await canLoadServer(serverName, {
|
|
adminMcpEnabled: activeSettings.admin?.mcp?.enabled ?? true,
|
|
allowedList: activeSettings.mcp?.allowed,
|
|
excludedList: activeSettings.mcp?.excluded,
|
|
enablement: mcpEnablementManager.getEnablementCallbacks(),
|
|
});
|
|
|
|
if (!loadResult.allowed) {
|
|
if (
|
|
loadResult.blockType === 'admin' ||
|
|
loadResult.blockType === 'allowlist' ||
|
|
loadResult.blockType === 'excludelist'
|
|
) {
|
|
return MCPServerStatus.BLOCKED;
|
|
}
|
|
return MCPServerStatus.DISABLED;
|
|
}
|
|
|
|
// Test all server types by attempting actual connection
|
|
return testMCPConnection(serverName, server, isTrusted, activeSettings);
|
|
}
|
|
|
|
export async function listMcpServers(
|
|
loadedSettingsArg?: LoadedSettings,
|
|
): Promise<void> {
|
|
const loadedSettings = loadedSettingsArg ?? loadSettings();
|
|
const activeSettings = loadedSettings.merged;
|
|
|
|
const { mcpServers, blockedServerNames } =
|
|
await getMcpServersFromConfig(activeSettings);
|
|
const serverNames = Object.keys(mcpServers);
|
|
|
|
if (blockedServerNames.length > 0) {
|
|
const message = getAdminBlockedMcpServersMessage(
|
|
blockedServerNames,
|
|
undefined,
|
|
);
|
|
debugLogger.log(chalk.yellow(message + '\n'));
|
|
}
|
|
|
|
if (serverNames.length === 0) {
|
|
if (blockedServerNames.length === 0) {
|
|
debugLogger.log('No MCP servers configured.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
debugLogger.log('Configured MCP servers:\n');
|
|
|
|
for (const serverName of serverNames) {
|
|
const server = mcpServers[serverName];
|
|
|
|
const status = await getServerStatus(
|
|
serverName,
|
|
server,
|
|
loadedSettings.isTrusted,
|
|
activeSettings,
|
|
);
|
|
|
|
let statusIndicator = '';
|
|
let statusText = '';
|
|
switch (status) {
|
|
case MCPServerStatus.CONNECTED:
|
|
statusIndicator = chalk.green('✓');
|
|
statusText = 'Connected';
|
|
break;
|
|
case MCPServerStatus.CONNECTING:
|
|
statusIndicator = chalk.yellow('…');
|
|
statusText = 'Connecting';
|
|
break;
|
|
case MCPServerStatus.BLOCKED:
|
|
statusIndicator = chalk.red('⛔');
|
|
statusText = 'Blocked';
|
|
break;
|
|
case MCPServerStatus.DISABLED:
|
|
statusIndicator = chalk.gray('○');
|
|
statusText = 'Disabled';
|
|
break;
|
|
case MCPServerStatus.DISCONNECTED:
|
|
default:
|
|
statusIndicator = chalk.red('✗');
|
|
statusText = 'Disconnected';
|
|
break;
|
|
}
|
|
|
|
let serverInfo =
|
|
serverName +
|
|
(server.extension?.name ? ` (from ${server.extension.name})` : '') +
|
|
': ';
|
|
if (server.httpUrl) {
|
|
serverInfo += `${server.httpUrl} (http)`;
|
|
} else if (server.url) {
|
|
const type = server.type || 'http';
|
|
serverInfo += `${server.url} (${type})`;
|
|
} else if (server.command) {
|
|
serverInfo += `${server.command} ${server.args?.join(' ') || ''} (stdio)`;
|
|
}
|
|
|
|
debugLogger.log(`${statusIndicator} ${serverInfo} - ${statusText}`);
|
|
}
|
|
}
|
|
|
|
interface ListArgs {
|
|
loadedSettings?: LoadedSettings;
|
|
}
|
|
|
|
export const listCommand: CommandModule<object, ListArgs> = {
|
|
command: 'list',
|
|
describe: 'List all configured MCP servers',
|
|
handler: async (argv) => {
|
|
await listMcpServers(argv.loadedSettings);
|
|
await exitCli();
|
|
},
|
|
};
|