fix(mcp): update OAuth client names and improve MCP commands

- Rename MCP OAuth client names from 'Gemini CLI' to 'Qwen Code'
- Update MCP add/remove/list commands with improved error handling
- Add comprehensive tests for OAuth provider
- Fix token storage test assertions
- Clean up unused i18n translation keys
- Update gemini-converter and window title references

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-08 10:46:48 +08:00
parent 36931e1eab
commit 21e711469d
27 changed files with 244 additions and 124 deletions

View file

@ -65,22 +65,18 @@ describe('mcp add command', () => {
});
});
it('should add a stdio server to project settings', async () => {
it('should add a stdio server to user settings by default', 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' },
},
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'my-server': {
command: '/path/to/server',
args: ['arg1', 'arg2'],
env: { FOO: 'bar' },
},
);
});
});
it('should add an sse server to user settings', async () => {
@ -96,21 +92,17 @@ describe('mcp add command', () => {
});
});
it('should add an http server to project settings', async () => {
it('should add an http server to user settings by default', 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' },
},
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'http-server': {
httpUrl: 'https://example.com/mcp',
headers: { Authorization: 'Bearer your-token' },
},
);
});
});
it('should handle MCP server args with -- separator', async () => {
@ -118,16 +110,12 @@ describe('mcp add command', () => {
'add my-server npx -- -y http://example.com/some-package',
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
{
'my-server': {
command: 'npx',
args: ['-y', 'http://example.com/some-package'],
},
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'my-server': {
command: 'npx',
args: ['-y', 'http://example.com/some-package'],
},
);
});
});
it('should handle unknown options as MCP server args', async () => {
@ -135,16 +123,12 @@ describe('mcp add command', () => {
'add test-server npx -y http://example.com/some-package',
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
{
'test-server': {
command: 'npx',
args: ['-y', 'http://example.com/some-package'],
},
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'test-server': {
command: 'npx',
args: ['-y', 'http://example.com/some-package'],
},
);
});
});
describe('when handling scope and directory', () => {
@ -166,10 +150,10 @@ describe('mcp add command', () => {
setupMocks('/path/to/project', '/path/to/project');
});
it('should use project scope by default', async () => {
it('should use user scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
expect.any(Object),
);
@ -199,10 +183,10 @@ describe('mcp add command', () => {
setupMocks('/path/to/project/subdir', '/path/to/project');
});
it('should use project scope by default', async () => {
it('should use user scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
expect.any(Object),
);
@ -214,22 +198,14 @@ describe('mcp add command', () => {
setupMocks('/home/user', '/home/user');
});
it('should show an error by default', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {
throw new Error('process.exit called');
}) as (code?: number) => never);
await expect(
parser.parseAsync(`add ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockWriteStderrLine).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
it('should use user scope by default without error', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcpServers',
expect.any(Object),
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(mockSetValue).not.toHaveBeenCalled();
expect(mockWriteStderrLine).not.toHaveBeenCalled();
});
it('should show an error when --scope=project is used explicitly', async () => {
@ -266,16 +242,16 @@ describe('mcp add command', () => {
setupMocks('/home/user/some/dir', '/home/user/some/dir');
});
it('should use project scope by default', async () => {
it('should use user scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
expect.any(Object),
);
});
it('should write to the WORKSPACE scope, not the USER scope', async () => {
it('should write to the USER scope by default', async () => {
await parser.parseAsync(`add my-new-server echo`);
// We expect setValue to be called once.
@ -284,8 +260,8 @@ describe('mcp add command', () => {
// We get the scope that setValue was called with.
const calledScope = mockSetValue.mock.calls[0][0];
// We assert that the scope was Workspace, not User.
expect(calledScope).toBe(SettingScope.Workspace);
// We assert that the scope was User by default.
expect(calledScope).toBe(SettingScope.User);
});
});
@ -294,10 +270,10 @@ describe('mcp add command', () => {
setupMocks('/tmp/foo', '/tmp/foo');
});
it('should use project scope by default', async () => {
it('should use user scope by default', async () => {
await parser.parseAsync(`add ${serverName} ${command}`);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
expect.any(Object),
);
@ -328,12 +304,12 @@ describe('mcp add command', () => {
});
});
it('should update the existing server in the project scope', async () => {
it('should update the existing server in the user scope by default', async () => {
await parser.parseAsync(
`add ${serverName} ${updatedCommand} ${updatedArgs.join(' ')}`,
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
expect.objectContaining({
[serverName]: expect.objectContaining({

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
// File for 'gemini mcp add' command
// File for 'qwen mcp add' command
import type { CommandModule } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
@ -159,7 +159,7 @@ export const addCommand: CommandModule = {
alias: 's',
describe: 'Configuration scope (user or project)',
type: 'string',
default: 'project',
default: 'user',
choices: ['user', 'project'],
})
.option('transport', {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
// File for 'gemini mcp list' command
// File for 'qwen mcp list' command
import type { CommandModule } from 'yargs';
import { loadSettings } from '../../config/settings.js';
import { writeStdoutLine } from '../../utils/stdioHelpers.js';

View file

@ -11,6 +11,7 @@ import { removeCommand } from './remove.js';
const mockWriteStdoutLine = vi.hoisted(() => vi.fn());
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
const mockDeleteCredentials = vi.hoisted(() => vi.fn());
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: mockWriteStdoutLine,
@ -35,6 +36,17 @@ vi.mock('../../config/settings.js', async () => {
};
});
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
MCPOAuthTokenStorage: vi.fn(() => ({
deleteCredentials: mockDeleteCredentials,
})),
};
});
const mockedLoadSettings = loadSettings as vi.Mock;
describe('mcp remove command', () => {
@ -59,24 +71,45 @@ describe('mcp remove command', () => {
setValue: mockSetValue,
});
mockWriteStdoutLine.mockClear();
mockDeleteCredentials.mockClear();
});
it('should remove a server from project settings', async () => {
it('should remove a server from user settings by default', async () => {
await parser.parseAsync('remove test-server');
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
SettingScope.User,
'mcpServers',
{},
);
});
it('should show a message if server not found', async () => {
it('should clean up OAuth tokens when removing a server', async () => {
await parser.parseAsync('remove test-server');
expect(mockDeleteCredentials).toHaveBeenCalledWith('test-server');
});
it('should not fail if OAuth token cleanup fails', async () => {
mockDeleteCredentials.mockRejectedValue(new Error('cleanup failed'));
await parser.parseAsync('remove test-server');
// Server should still be removed from settings despite token cleanup failure
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.User,
'mcpServers',
{},
);
});
it('should not clean up OAuth tokens if server not found', async () => {
await parser.parseAsync('remove non-existent-server');
expect(mockSetValue).not.toHaveBeenCalled();
expect(mockDeleteCredentials).not.toHaveBeenCalled();
expect(mockWriteStdoutLine).toHaveBeenCalledWith(
'Server "non-existent-server" not found in project settings.',
'Server "non-existent-server" not found in user settings.',
);
});
});

View file

@ -4,10 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
// File for 'gemini mcp remove' command
// File for 'qwen mcp remove' command
import type { CommandModule } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
import { MCPOAuthTokenStorage } from '@qwen-code/qwen-code-core';
async function removeMcpServer(
name: string,
@ -32,6 +33,14 @@ async function removeMcpServer(
settings.setValue(settingsScope, 'mcpServers', mcpServers);
// Clean up any stored OAuth tokens for this server
try {
const tokenStorage = new MCPOAuthTokenStorage();
await tokenStorage.deleteCredentials(name);
} catch {
// Token cleanup is best-effort; don't fail the remove operation
}
writeStdoutLine(`Server "${name}" removed from ${scope} settings.`);
}
@ -40,7 +49,7 @@ export const removeCommand: CommandModule = {
describe: 'Remove a server',
builder: (yargs) =>
yargs
.usage('Usage: gemini mcp remove [options] <name>')
.usage('Usage: qwen mcp remove [options] <name>')
.positional('name', {
describe: 'Name of the server',
type: 'string',
@ -50,7 +59,7 @@ export const removeCommand: CommandModule = {
alias: 's',
describe: 'Configuration scope (user or project)',
type: 'string',
default: 'project',
default: 'user',
choices: ['user', 'project'],
}),
handler: async (argv) => {