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
36 changes: 36 additions & 0 deletions docs/developers/tools/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -869,3 +869,39 @@ qwen mcp remove my-server
```

This will find and delete the "my-server" entry from the `mcpServers` object in the appropriate `settings.json` file based on the scope (`-s, --scope`).

### Enabling a Server (`qwen mcp enable`)

To re-enable a server that was previously excluded, use `enable`. This removes the server name from `mcp.excluded` in either user or project settings.

**Command:**

```bash
qwen mcp enable [options] <name>
```

- `-s, --scope`: Configuration scope (user or project). [default: "user"]

**Example:**

```bash
qwen mcp enable github --scope user
```

### Disabling a Server (`qwen mcp disable`)

To disable a configured server without removing its configuration, use `disable`. This adds the server name to `mcp.excluded` in either user or project settings.

**Command:**

```bash
qwen mcp disable [options] <name>
```

- `-s, --scope`: Configuration scope (user or project). [default: "user"]

**Example:**

```bash
qwen mcp disable github --scope user
```
6 changes: 4 additions & 2 deletions packages/cli/src/commands/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('mcp command', () => {
expect(options.key).toHaveProperty('help');
});

it('should register add, remove, and list subcommands', () => {
it('should register add, enable, disable, remove, and list subcommands', () => {
const mockYargs = {
command: vi.fn().mockReturnThis(),
demandCommand: vi.fn().mockReturnThis(),
Expand All @@ -37,13 +37,15 @@ describe('mcp command', () => {

mcpCommand.builder(mockYargs as unknown as Argv);

expect(mockYargs.command).toHaveBeenCalledTimes(3);
expect(mockYargs.command).toHaveBeenCalledTimes(5);

// 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('enable <name>');
expect(commandNames).toContain('disable <name>');
expect(commandNames).toContain('remove <name>');
expect(commandNames).toContain('list');

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// File for 'gemini mcp' command
import type { CommandModule, Argv } from 'yargs';
import { addCommand } from './mcp/add.js';
import { disableCommand } from './mcp/disable.js';
import { enableCommand } from './mcp/enable.js';
import { removeCommand } from './mcp/remove.js';
import { listCommand } from './mcp/list.js';

Expand All @@ -16,6 +18,8 @@ export const mcpCommand: CommandModule = {
builder: (yargs: Argv) =>
yargs
.command(addCommand)
.command(enableCommand)
.command(disableCommand)
.command(removeCommand)
.command(listCommand)
.demandCommand(1, 'You need at least one command before continuing.')
Expand Down
97 changes: 97 additions & 0 deletions packages/cli/src/commands/mcp/disable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import yargs from 'yargs';
import type { Argv } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { disableCommand } from './disable.js';

const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());

vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));

vi.mock('fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
...actual,
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 Mock;

describe('mcp disable command', () => {
let parser: Argv;
let mockSetValue: Mock;
let mockSettings: Record<string, unknown>;

beforeEach(() => {
vi.resetAllMocks();
const yargsInstance = yargs([]).command(disableCommand);
parser = yargsInstance;
mockSetValue = vi.fn();
mockSettings = {
mcp: {
excluded: ['other-server'],
},
};
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: mockSettings }),
setValue: mockSetValue,
});
mockWriteStdoutLine.mockClear();
});

it('should disable a server in user settings by default', async () => {
await parser.parseAsync('disable test-server');

expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcp.excluded',
['other-server', 'test-server'],
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "test-server" disabled in user settings.',
);
});

it('should disable a server in project settings when --scope project is provided', async () => {
await parser.parseAsync('disable test-server --scope project');

expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcp.excluded',
['other-server', 'test-server'],
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "test-server" disabled in project settings.',
);
});

it('should be a no-op if server is already disabled', async () => {
await parser.parseAsync('disable other-server');

expect(mockSetValue).not.toHaveBeenCalled();
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "other-server" is already disabled in user settings.',
);
});
});
60 changes: 60 additions & 0 deletions packages/cli/src/commands/mcp/disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

// File for 'qwen mcp disable' command
import type { CommandModule } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { writeStdoutLine } from '../../utils/stdioHelpers.js';

async function disableMcpServer(
name: string,
options: {
scope: string;
},
) {
const { scope } = options;
const settingsScope =
scope === 'user' ? SettingScope.User : SettingScope.Workspace;
const settings = loadSettings();

const existingSettings = settings.forScope(settingsScope).settings;
const excluded = existingSettings.mcp?.excluded || [];

if (excluded.includes(name)) {
writeStdoutLine(
`Server "${name}" is already disabled in ${scope} settings.`,
);
return;
}

settings.setValue(settingsScope, 'mcp.excluded', [...excluded, name]);
writeStdoutLine(`Server "${name}" disabled in ${scope} settings.`);
}

export const disableCommand: CommandModule = {
command: 'disable <name>',
describe: 'Disable a server by adding it to mcp.excluded',
builder: (yargs) =>
yargs
.usage('Usage: qwen mcp disable [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: 'user',
choices: ['user', 'project'],
}),
handler: async (argv) => {
await disableMcpServer(argv['name'] as string, {
scope: argv['scope'] as string,
});
},
};
97 changes: 97 additions & 0 deletions packages/cli/src/commands/mcp/enable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import yargs from 'yargs';
import type { Argv } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { enableCommand } from './enable.js';

const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());

vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
writeStderrLine: mockWriteStderrLine,
clearScreen: vi.fn(),
}));

vi.mock('fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
...actual,
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 Mock;

describe('mcp enable command', () => {
let parser: Argv;
let mockSetValue: Mock;
let mockSettings: Record<string, unknown>;

beforeEach(() => {
vi.resetAllMocks();
const yargsInstance = yargs([]).command(enableCommand);
parser = yargsInstance;
mockSetValue = vi.fn();
mockSettings = {
mcp: {
excluded: ['test-server', 'other-server'],
},
};
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: mockSettings }),
setValue: mockSetValue,
});
mockWriteStdoutLine.mockClear();
});

it('should enable a server in user settings by default', async () => {
await parser.parseAsync('enable test-server');

expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcp.excluded',
['other-server'],
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "test-server" enabled in user settings.',
);
});

it('should enable a server in project settings when --scope project is provided', async () => {
await parser.parseAsync('enable test-server --scope project');

expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcp.excluded',
['other-server'],
);
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "test-server" enabled in project settings.',
);
});

it('should be a no-op if server is already enabled', async () => {
await parser.parseAsync('enable missing-server');

expect(mockSetValue).not.toHaveBeenCalled();
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "missing-server" is already enabled in user settings.',
);
});
});
Loading