mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 07:10:55 +00:00
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:
parent
36931e1eab
commit
21e711469d
27 changed files with 244 additions and 124 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue