summaryrefslogtreecommitdiff
path: root/packages/cli/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/commands')
-rw-r--r--packages/cli/src/commands/mcp.test.ts55
-rw-r--r--packages/cli/src/commands/mcp.ts27
-rw-r--r--packages/cli/src/commands/mcp/add.test.ts88
-rw-r--r--packages/cli/src/commands/mcp/add.ts211
-rw-r--r--packages/cli/src/commands/mcp/list.test.ts154
-rw-r--r--packages/cli/src/commands/mcp/list.ts139
-rw-r--r--packages/cli/src/commands/mcp/remove.test.ts69
-rw-r--r--packages/cli/src/commands/mcp/remove.ts60
8 files changed, 803 insertions, 0 deletions
diff --git a/packages/cli/src/commands/mcp.test.ts b/packages/cli/src/commands/mcp.test.ts
new file mode 100644
index 00000000..b4e9980c
--- /dev/null
+++ b/packages/cli/src/commands/mcp.test.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi } from 'vitest';
+import { mcpCommand } from './mcp.js';
+import { type Argv } from 'yargs';
+import yargs from 'yargs';
+
+describe('mcp command', () => {
+ it('should have correct command definition', () => {
+ expect(mcpCommand.command).toBe('mcp');
+ expect(mcpCommand.describe).toBe('Manage MCP servers');
+ expect(typeof mcpCommand.builder).toBe('function');
+ expect(typeof mcpCommand.handler).toBe('function');
+ });
+
+ it('should have exactly one option (help flag)', () => {
+ // Test to ensure that the global 'gemini' flags are not added to the mcp command
+ const yargsInstance = yargs();
+ const builtYargs = mcpCommand.builder(yargsInstance);
+ const options = builtYargs.getOptions();
+
+ // Should have exactly 1 option (help flag)
+ expect(Object.keys(options.key).length).toBe(1);
+ expect(options.key).toHaveProperty('help');
+ });
+
+ it('should register add, remove, and list subcommands', () => {
+ const mockYargs = {
+ command: vi.fn().mockReturnThis(),
+ demandCommand: vi.fn().mockReturnThis(),
+ version: vi.fn().mockReturnThis(),
+ };
+
+ mcpCommand.builder(mockYargs as unknown as Argv);
+
+ expect(mockYargs.command).toHaveBeenCalledTimes(3);
+
+ // Verify that the specific subcommands are registered
+ const commandCalls = mockYargs.command.mock.calls;
+ const commandNames = commandCalls.map((call) => call[0].command);
+
+ expect(commandNames).toContain('add <name> <commandOrUrl> [args...]');
+ expect(commandNames).toContain('remove <name>');
+ expect(commandNames).toContain('list');
+
+ expect(mockYargs.demandCommand).toHaveBeenCalledWith(
+ 1,
+ 'You need at least one command before continuing.',
+ );
+ });
+});
diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts
new file mode 100644
index 00000000..5e55286c
--- /dev/null
+++ b/packages/cli/src/commands/mcp.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// File for 'gemini mcp' command
+import type { CommandModule, Argv } from 'yargs';
+import { addCommand } from './mcp/add.js';
+import { removeCommand } from './mcp/remove.js';
+import { listCommand } from './mcp/list.js';
+
+export const mcpCommand: CommandModule = {
+ command: 'mcp',
+ describe: 'Manage MCP servers',
+ builder: (yargs: Argv) =>
+ yargs
+ .command(addCommand)
+ .command(removeCommand)
+ .command(listCommand)
+ .demandCommand(1, 'You need at least one command before continuing.')
+ .version(false),
+ handler: () => {
+ // yargs will automatically show help if no subcommand is provided
+ // thanks to demandCommand(1) in the builder.
+ },
+};
diff --git a/packages/cli/src/commands/mcp/add.test.ts b/packages/cli/src/commands/mcp/add.test.ts
new file mode 100644
index 00000000..1d431c48
--- /dev/null
+++ b/packages/cli/src/commands/mcp/add.test.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import yargs from 'yargs';
+import { addCommand } from './add.js';
+import { loadSettings, SettingScope } from '../../config/settings.js';
+
+vi.mock('fs/promises', () => ({
+ readFile: vi.fn(),
+ writeFile: vi.fn(),
+}));
+
+vi.mock('../../config/settings.js', async () => {
+ const actual = await vi.importActual('../../config/settings.js');
+ return {
+ ...actual,
+ loadSettings: vi.fn(),
+ };
+});
+
+const mockedLoadSettings = loadSettings as vi.Mock;
+
+describe('mcp add command', () => {
+ let parser: yargs.Argv;
+ let mockSetValue: vi.Mock;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ const yargsInstance = yargs([]).command(addCommand);
+ parser = yargsInstance;
+ mockSetValue = vi.fn();
+ mockedLoadSettings.mockReturnValue({
+ forScope: () => ({ settings: {} }),
+ setValue: mockSetValue,
+ });
+ });
+
+ it('should add a stdio server to project settings', async () => {
+ await parser.parseAsync(
+ 'add my-server /path/to/server arg1 arg2 -e FOO=bar',
+ );
+
+ expect(mockSetValue).toHaveBeenCalledWith(
+ SettingScope.Workspace,
+ 'mcpServers',
+ {
+ 'my-server': {
+ command: '/path/to/server',
+ args: ['arg1', 'arg2'],
+ env: { FOO: 'bar' },
+ },
+ },
+ );
+ });
+
+ it('should add an sse server to user settings', async () => {
+ await parser.parseAsync(
+ 'add --transport sse sse-server https://example.com/sse-endpoint --scope user -H "X-API-Key: your-key"',
+ );
+
+ expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
+ 'sse-server': {
+ url: 'https://example.com/sse-endpoint',
+ headers: { 'X-API-Key': 'your-key' },
+ },
+ });
+ });
+
+ it('should add an http server to project settings', async () => {
+ await parser.parseAsync(
+ 'add --transport http http-server https://example.com/mcp -H "Authorization: Bearer your-token"',
+ );
+
+ expect(mockSetValue).toHaveBeenCalledWith(
+ SettingScope.Workspace,
+ 'mcpServers',
+ {
+ 'http-server': {
+ httpUrl: 'https://example.com/mcp',
+ headers: { Authorization: 'Bearer your-token' },
+ },
+ },
+ );
+ });
+});
diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts
new file mode 100644
index 00000000..9537e131
--- /dev/null
+++ b/packages/cli/src/commands/mcp/add.ts
@@ -0,0 +1,211 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// File for 'gemini mcp add' command
+import type { CommandModule } from 'yargs';
+import { loadSettings, SettingScope } from '../../config/settings.js';
+import { MCPServerConfig } from '@google/gemini-cli-core';
+
+async function addMcpServer(
+ name: string,
+ commandOrUrl: string,
+ args: Array<string | number> | undefined,
+ options: {
+ scope: string;
+ transport: string;
+ env: string[] | undefined;
+ header: string[] | undefined;
+ timeout?: number;
+ trust?: boolean;
+ description?: string;
+ includeTools?: string[];
+ excludeTools?: string[];
+ },
+) {
+ const {
+ scope,
+ transport,
+ env,
+ header,
+ timeout,
+ trust,
+ description,
+ includeTools,
+ excludeTools,
+ } = options;
+ const settingsScope =
+ scope === 'user' ? SettingScope.User : SettingScope.Workspace;
+ const settings = loadSettings(process.cwd());
+
+ let newServer: Partial<MCPServerConfig> = {};
+
+ const headers = header?.reduce(
+ (acc, curr) => {
+ const [key, ...valueParts] = curr.split(':');
+ const value = valueParts.join(':').trim();
+ if (key.trim() && value) {
+ acc[key.trim()] = value;
+ }
+ return acc;
+ },
+ {} as Record<string, string>,
+ );
+
+ switch (transport) {
+ case 'sse':
+ newServer = {
+ url: commandOrUrl,
+ headers,
+ timeout,
+ trust,
+ description,
+ includeTools,
+ excludeTools,
+ };
+ break;
+ case 'http':
+ newServer = {
+ httpUrl: commandOrUrl,
+ headers,
+ timeout,
+ trust,
+ description,
+ includeTools,
+ excludeTools,
+ };
+ break;
+ case 'stdio':
+ default:
+ newServer = {
+ command: commandOrUrl,
+ args: args?.map(String),
+ env: env?.reduce(
+ (acc, curr) => {
+ const [key, value] = curr.split('=');
+ if (key && value) {
+ acc[key] = value;
+ }
+ return acc;
+ },
+ {} as Record<string, string>,
+ ),
+ timeout,
+ trust,
+ description,
+ includeTools,
+ excludeTools,
+ };
+ break;
+ }
+
+ const existingSettings = settings.forScope(settingsScope).settings;
+ const mcpServers = existingSettings.mcpServers || {};
+
+ const isExistingServer = !!mcpServers[name];
+ if (isExistingServer) {
+ console.log(
+ `MCP server "${name}" is already configured within ${scope} settings.`,
+ );
+ }
+
+ mcpServers[name] = newServer as MCPServerConfig;
+
+ settings.setValue(settingsScope, 'mcpServers', mcpServers);
+
+ if (isExistingServer) {
+ console.log(`MCP server "${name}" updated in ${scope} settings.`);
+ } else {
+ console.log(
+ `MCP server "${name}" added to ${scope} settings. (${transport})`,
+ );
+ }
+}
+
+export const addCommand: CommandModule = {
+ command: 'add <name> <commandOrUrl> [args...]',
+ describe: 'Add a server',
+ builder: (yargs) =>
+ yargs
+ .usage('Usage: gemini mcp add [options] <name> <commandOrUrl> [args...]')
+ .positional('name', {
+ describe: 'Name of the server',
+ type: 'string',
+ demandOption: true,
+ })
+ .positional('commandOrUrl', {
+ describe: 'Command (stdio) or URL (sse, http)',
+ type: 'string',
+ demandOption: true,
+ })
+ .option('scope', {
+ alias: 's',
+ describe: 'Configuration scope (user or project)',
+ type: 'string',
+ default: 'project',
+ choices: ['user', 'project'],
+ })
+ .option('transport', {
+ alias: 't',
+ describe: 'Transport type (stdio, sse, http)',
+ type: 'string',
+ default: 'stdio',
+ choices: ['stdio', 'sse', 'http'],
+ })
+ .option('env', {
+ alias: 'e',
+ describe: 'Set environment variables (e.g. -e KEY=value)',
+ type: 'array',
+ string: true,
+ })
+ .option('header', {
+ alias: 'H',
+ describe:
+ 'Set HTTP headers for SSE and HTTP transports (e.g. -H "X-Api-Key: abc123" -H "Authorization: Bearer abc123")',
+ type: 'array',
+ string: true,
+ })
+ .option('timeout', {
+ describe: 'Set connection timeout in milliseconds',
+ type: 'number',
+ })
+ .option('trust', {
+ describe:
+ 'Trust the server (bypass all tool call confirmation prompts)',
+ type: 'boolean',
+ })
+ .option('description', {
+ describe: 'Set the description for the server',
+ type: 'string',
+ })
+ .option('include-tools', {
+ describe: 'A comma-separated list of tools to include',
+ type: 'array',
+ string: true,
+ })
+ .option('exclude-tools', {
+ describe: 'A comma-separated list of tools to exclude',
+ type: 'array',
+ string: true,
+ }),
+ handler: async (argv) => {
+ await addMcpServer(
+ argv.name as string,
+ argv.commandOrUrl as string,
+ argv.args as Array<string | number>,
+ {
+ scope: argv.scope as string,
+ transport: argv.transport as string,
+ env: argv.env as string[],
+ header: argv.header as string[],
+ timeout: argv.timeout as number | undefined,
+ trust: argv.trust as boolean | undefined,
+ description: argv.description as string | undefined,
+ includeTools: argv.includeTools as string[] | undefined,
+ excludeTools: argv.excludeTools as string[] | undefined,
+ },
+ );
+ },
+};
diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts
new file mode 100644
index 00000000..daf2e3d7
--- /dev/null
+++ b/packages/cli/src/commands/mcp/list.test.ts
@@ -0,0 +1,154 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { listMcpServers } from './list.js';
+import { loadSettings } from '../../config/settings.js';
+import { loadExtensions } from '../../config/extension.js';
+import { createTransport } from '@google/gemini-cli-core';
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+
+vi.mock('../../config/settings.js');
+vi.mock('../../config/extension.js');
+vi.mock('@google/gemini-cli-core');
+vi.mock('@modelcontextprotocol/sdk/client/index.js');
+
+const mockedLoadSettings = loadSettings as vi.Mock;
+const mockedLoadExtensions = loadExtensions as vi.Mock;
+const mockedCreateTransport = createTransport as vi.Mock;
+const MockedClient = Client as vi.Mock;
+
+interface MockClient {
+ connect: vi.Mock;
+ ping: vi.Mock;
+ close: vi.Mock;
+}
+
+interface MockTransport {
+ close: vi.Mock;
+}
+
+describe('mcp list command', () => {
+ let consoleSpy: vi.SpyInstance;
+ let mockClient: MockClient;
+ let mockTransport: MockTransport;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ mockTransport = { close: vi.fn() };
+ mockClient = {
+ connect: vi.fn(),
+ ping: vi.fn(),
+ close: vi.fn(),
+ };
+
+ MockedClient.mockImplementation(() => mockClient);
+ mockedCreateTransport.mockResolvedValue(mockTransport);
+ mockedLoadExtensions.mockReturnValue([]);
+ });
+
+ afterEach(() => {
+ consoleSpy.mockRestore();
+ });
+
+ it('should display message when no servers configured', async () => {
+ mockedLoadSettings.mockReturnValue({ merged: { mcpServers: {} } });
+
+ await listMcpServers();
+
+ expect(consoleSpy).toHaveBeenCalledWith('No MCP servers configured.');
+ });
+
+ it('should display different server types with connected status', async () => {
+ mockedLoadSettings.mockReturnValue({
+ merged: {
+ mcpServers: {
+ 'stdio-server': { command: '/path/to/server', args: ['arg1'] },
+ 'sse-server': { url: 'https://example.com/sse' },
+ 'http-server': { httpUrl: 'https://example.com/http' },
+ },
+ },
+ });
+
+ mockClient.connect.mockResolvedValue(undefined);
+ mockClient.ping.mockResolvedValue(undefined);
+
+ await listMcpServers();
+
+ expect(consoleSpy).toHaveBeenCalledWith('Configured MCP servers:\n');
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'stdio-server: /path/to/server arg1 (stdio) - Connected',
+ ),
+ );
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'sse-server: https://example.com/sse (sse) - Connected',
+ ),
+ );
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'http-server: https://example.com/http (http) - Connected',
+ ),
+ );
+ });
+
+ it('should display disconnected status when connection fails', async () => {
+ mockedLoadSettings.mockReturnValue({
+ merged: {
+ mcpServers: {
+ 'test-server': { command: '/test/server' },
+ },
+ },
+ });
+
+ mockClient.connect.mockRejectedValue(new Error('Connection failed'));
+
+ await listMcpServers();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'test-server: /test/server (stdio) - Disconnected',
+ ),
+ );
+ });
+
+ it('should merge extension servers with config servers', async () => {
+ mockedLoadSettings.mockReturnValue({
+ merged: {
+ mcpServers: { 'config-server': { command: '/config/server' } },
+ },
+ });
+
+ mockedLoadExtensions.mockReturnValue([
+ {
+ config: {
+ name: 'test-extension',
+ mcpServers: { 'extension-server': { command: '/ext/server' } },
+ },
+ },
+ ]);
+
+ mockClient.connect.mockResolvedValue(undefined);
+ mockClient.ping.mockResolvedValue(undefined);
+
+ await listMcpServers();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'config-server: /config/server (stdio) - Connected',
+ ),
+ );
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'extension-server: /ext/server (stdio) - Connected',
+ ),
+ );
+ });
+});
diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts
new file mode 100644
index 00000000..48ea912e
--- /dev/null
+++ b/packages/cli/src/commands/mcp/list.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// File for 'gemini mcp list' command
+import type { CommandModule } from 'yargs';
+import { loadSettings } from '../../config/settings.js';
+import {
+ MCPServerConfig,
+ MCPServerStatus,
+ createTransport,
+} from '@google/gemini-cli-core';
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { loadExtensions } from '../../config/extension.js';
+
+const COLOR_GREEN = '\u001b[32m';
+const COLOR_YELLOW = '\u001b[33m';
+const COLOR_RED = '\u001b[31m';
+const RESET_COLOR = '\u001b[0m';
+
+async function getMcpServersFromConfig(): Promise<
+ Record<string, MCPServerConfig>
+> {
+ const settings = loadSettings(process.cwd());
+ const extensions = loadExtensions(process.cwd());
+ const mcpServers = { ...(settings.merged.mcpServers || {}) };
+ for (const extension of extensions) {
+ Object.entries(extension.config.mcpServers || {}).forEach(
+ ([key, server]) => {
+ if (mcpServers[key]) {
+ return;
+ }
+ mcpServers[key] = {
+ ...server,
+ extensionName: extension.config.name,
+ };
+ },
+ );
+ }
+ return mcpServers;
+}
+
+async function testMCPConnection(
+ serverName: string,
+ config: MCPServerConfig,
+): Promise<MCPServerStatus> {
+ const client = new Client({
+ name: 'mcp-test-client',
+ version: '0.0.1',
+ });
+
+ let transport;
+ try {
+ // Use the same transport creation logic as core
+ transport = await createTransport(serverName, config, false);
+ } 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,
+): Promise<MCPServerStatus> {
+ // Test all server types by attempting actual connection
+ return await testMCPConnection(serverName, server);
+}
+
+export async function listMcpServers(): Promise<void> {
+ const mcpServers = await getMcpServersFromConfig();
+ const serverNames = Object.keys(mcpServers);
+
+ if (serverNames.length === 0) {
+ console.log('No MCP servers configured.');
+ return;
+ }
+
+ console.log('Configured MCP servers:\n');
+
+ for (const serverName of serverNames) {
+ const server = mcpServers[serverName];
+
+ const status = await getServerStatus(serverName, server);
+
+ let statusIndicator = '';
+ let statusText = '';
+ switch (status) {
+ case MCPServerStatus.CONNECTED:
+ statusIndicator = COLOR_GREEN + '✓' + RESET_COLOR;
+ statusText = 'Connected';
+ break;
+ case MCPServerStatus.CONNECTING:
+ statusIndicator = COLOR_YELLOW + '…' + RESET_COLOR;
+ statusText = 'Connecting';
+ break;
+ case MCPServerStatus.DISCONNECTED:
+ default:
+ statusIndicator = COLOR_RED + '✗' + RESET_COLOR;
+ statusText = 'Disconnected';
+ break;
+ }
+
+ let serverInfo = `${serverName}: `;
+ if (server.httpUrl) {
+ serverInfo += `${server.httpUrl} (http)`;
+ } else if (server.url) {
+ serverInfo += `${server.url} (sse)`;
+ } else if (server.command) {
+ serverInfo += `${server.command} ${server.args?.join(' ') || ''} (stdio)`;
+ }
+
+ console.log(`${statusIndicator} ${serverInfo} - ${statusText}`);
+ }
+}
+
+export const listCommand: CommandModule = {
+ command: 'list',
+ describe: 'List all configured MCP servers',
+ handler: async () => {
+ await listMcpServers();
+ },
+};
diff --git a/packages/cli/src/commands/mcp/remove.test.ts b/packages/cli/src/commands/mcp/remove.test.ts
new file mode 100644
index 00000000..eb7dedce
--- /dev/null
+++ b/packages/cli/src/commands/mcp/remove.test.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach } from 'vitest';
+import yargs from 'yargs';
+import { loadSettings, SettingScope } from '../../config/settings.js';
+import { removeCommand } from './remove.js';
+
+vi.mock('fs/promises', () => ({
+ readFile: vi.fn(),
+ writeFile: vi.fn(),
+}));
+
+vi.mock('../../config/settings.js', async () => {
+ const actual = await vi.importActual('../../config/settings.js');
+ return {
+ ...actual,
+ loadSettings: vi.fn(),
+ };
+});
+
+const mockedLoadSettings = loadSettings as vi.Mock;
+
+describe('mcp remove command', () => {
+ let parser: yargs.Argv;
+ let mockSetValue: vi.Mock;
+ let mockSettings: Record<string, unknown>;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ const yargsInstance = yargs([]).command(removeCommand);
+ parser = yargsInstance;
+ mockSetValue = vi.fn();
+ mockSettings = {
+ mcpServers: {
+ 'test-server': {
+ command: 'echo "hello"',
+ },
+ },
+ };
+ mockedLoadSettings.mockReturnValue({
+ forScope: () => ({ settings: mockSettings }),
+ setValue: mockSetValue,
+ });
+ });
+
+ it('should remove a server from project settings', async () => {
+ await parser.parseAsync('remove test-server');
+
+ expect(mockSetValue).toHaveBeenCalledWith(
+ SettingScope.Workspace,
+ 'mcpServers',
+ {},
+ );
+ });
+
+ it('should show a message if server not found', async () => {
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ await parser.parseAsync('remove non-existent-server');
+
+ expect(mockSetValue).not.toHaveBeenCalled();
+ expect(consoleSpy).toHaveBeenCalledWith(
+ 'Server "non-existent-server" not found in project settings.',
+ );
+ });
+});
diff --git a/packages/cli/src/commands/mcp/remove.ts b/packages/cli/src/commands/mcp/remove.ts
new file mode 100644
index 00000000..80d66234
--- /dev/null
+++ b/packages/cli/src/commands/mcp/remove.ts
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// File for 'gemini mcp remove' command
+import type { CommandModule } from 'yargs';
+import { loadSettings, SettingScope } from '../../config/settings.js';
+
+async function removeMcpServer(
+ name: string,
+ options: {
+ scope: string;
+ },
+) {
+ const { scope } = options;
+ const settingsScope =
+ scope === 'user' ? SettingScope.User : SettingScope.Workspace;
+ const settings = loadSettings(process.cwd());
+
+ const existingSettings = settings.forScope(settingsScope).settings;
+ const mcpServers = existingSettings.mcpServers || {};
+
+ if (!mcpServers[name]) {
+ console.log(`Server "${name}" not found in ${scope} settings.`);
+ return;
+ }
+
+ delete mcpServers[name];
+
+ settings.setValue(settingsScope, 'mcpServers', mcpServers);
+
+ console.log(`Server "${name}" removed from ${scope} settings.`);
+}
+
+export const removeCommand: CommandModule = {
+ command: 'remove <name>',
+ describe: 'Remove a server',
+ builder: (yargs) =>
+ yargs
+ .usage('Usage: gemini mcp remove [options] <name>')
+ .positional('name', {
+ describe: 'Name of the server',
+ type: 'string',
+ demandOption: true,
+ })
+ .option('scope', {
+ alias: 's',
+ describe: 'Configuration scope (user or project)',
+ type: 'string',
+ default: 'project',
+ choices: ['user', 'project'],
+ }),
+ handler: async (argv) => {
+ await removeMcpServer(argv.name as string, {
+ scope: argv.scope as string,
+ });
+ },
+};