Skip to content

feat(cli): import Claude MCP servers#5095

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
tt-a1i:feat/import-claude-mcp-config
Jun 14, 2026
Merged

feat(cli): import Claude MCP servers#5095
wenshao merged 2 commits into
QwenLM:mainfrom
tt-a1i:feat/import-claude-mcp-config

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add /import-config for Phase 1 Claude config migration: MCP servers only
  • import Claude Code user/project MCP settings and Claude Desktop MCP settings into Qwen settings
  • preserve existing effective MCP server names, skip unsupported reserved names, and report unreadable/malformed config warnings

Refs #4845

Testing

  • npm exec -- vitest run packages/cli/src/config/claudeMcpImport.test.ts packages/cli/src/ui/commands/importConfigCommand.test.ts packages/cli/src/services/BuiltinCommandLoader.test.ts
  • npm run typecheck --workspace packages/cli
  • npm run lint --workspace packages/cli
  • npm run check-i18n --workspace packages/cli
  • git diff --check

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.


describe('importConfigCommand', () => {
it('is a built-in command available in all execution modes', () => {
expect(importConfigCommand.name).toBe('import-config');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The action() method is never invoked in tests. The helper functions (parseImportConfigArgs, resolveImportSourceForScope, formatClaudeMcpImportResult) are tested in isolation, but the integration wiring — including the try/catch error path when getSettingScope throws — is uncovered.

Suggested change
expect(importConfigCommand.name).toBe('import-config');
describe('importConfigCommand', () => {
it('is a built-in command available in all execution modes', () => {
expect(importConfigCommand.name).toBe('import-config');
expect(importConfigCommand.kind).toBe(CommandKind.BUILT_IN);
expect(importConfigCommand.supportedModes).toEqual([
'interactive',
'non_interactive',
'acp',
]);
});
describe('action integration', () => {
it('runs the import and formats the result', async () => {
// mock context with settings that returns empty sources
// and verify the returned MessageActionReturn
});
it('catches thrown errors and returns an error message', async () => {
// mock settings where workspace.path === user.path
// verify getSettingScope throw is caught, not propagated
});
});

— qwen3.7-max via Qwen Code /review

});

it('imports Claude Desktop MCP servers from the platform config path', () => {
const desktopPath = getClaudeDesktopConfigPath(homeDir, 'darwin');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Only the darwin platform is tested for getClaudeDesktopConfigPath. The win32 branch (with and without APPDATA env var) and the Linux/default branch (~/.config/Claude/...) have no test coverage. Consider adding:

Suggested change
const desktopPath = getClaudeDesktopConfigPath(homeDir, 'darwin');
const desktopPath = getClaudeDesktopConfigPath(homeDir, 'darwin');
// Also test other platforms:
// expect(getClaudeDesktopConfigPath(homeDir, 'win32', { APPDATA: 'C:\\Users\\u\\AppData\\Roaming' }))
// .toBe(path.win32.join('C:\\Users\\u\\AppData\\Roaming', 'Claude', 'claude_desktop_config.json'));
// expect(getClaudeDesktopConfigPath(homeDir, 'linux'))
// .toBe(path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json'));

— qwen3.7-max via Qwen Code /review

});
}

function loadClaudeDesktopMcpServers(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] loadClaudeDesktopMcpServers is a ~45-line copy of loadMcpServersFromSettingsFile. The only difference is how filePath is computed. Consider replacing the body with a one-liner:

Suggested change
function loadClaudeDesktopMcpServers(
function loadClaudeDesktopMcpServers(
homeDir: string,
platform: NodeJS.Platform,
env: NodeJS.ProcessEnv,
): ClaudeMcpSourceResult {
return loadMcpServersFromSettingsFile(
getClaudeDesktopConfigPath(homeDir, platform, env),
'Claude Desktop',
'claude-desktop',
);
}

— qwen3.7-max via Qwen Code /review

continue;
}

if (!isRecord(serverConfig)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This isRecord(serverConfig) check is unreachable — copyMcpServers already filters non-record values with an error before storing them. The 'invalid' skip reason can never be produced. Consider removing the dead branch, or add a comment explaining it guards against a future code path.

Suggested change
if (!isRecord(serverConfig)) {
if (existingNames.has(name)) {
skipped.push({ name, source: source.label, reason: 'already-exists' });
continue;
}

— qwen3.7-max via Qwen Code /review

},
completion: async (_context, partialArg) => {
const current = partialArg.trimStart();
const candidates = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The completion function matches candidate.startsWith(current) against the full partialArg. Once the user types "claude-code ", no candidate starts with that string, so --scope user/--scope project become unreachable. Consider completing on the last token only:

Suggested change
const candidates = [
completion: async (_context, partialArg) => {
const parts = partialArg.trimStart().split(/\s+/);
const current = parts[parts.length - 1] ?? '';
const candidates = [
'all',
'claude-code',
'claude-desktop',
'--scope user',
'--scope project',
];
return candidates.filter((candidate) => candidate.startsWith(current));
},

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Local end-to-end verification (merge reference)

I built this PR and exercised /import-config in the real qwen binary — the interactive TUI driven via tmux plus headless -p runs — against a seeded isolated $HOME containing Claude Code (~/.claude.json, ~/.claude/settings.json) and Claude Desktop (~/.config/Claude/claude_desktop_config.json) MCP configs with valid entries and edge cases.

Verdict: the feature works end-to-end exactly as designed — reads the right Claude paths, writes the right Qwen scope, preserves existing names, skips reserved names, handles malformed configs gracefully, and does not pollute the prototype. LGTM for this Phase-1 (MCP-only) scope. Two non-blocking polish notes below.

Environment: Linux, Node 22.22.2, head 326fe26d, isolated $HOME, --auth-type openai.

1. PR test plan — all green

Command Result
vitest run claudeMcpImport / importConfigCommand / BuiltinCommandLoader 31 passed (12 + 8 + 11)
typecheck --workspace packages/cli exit 0
lint --workspace packages/cli exit 0
check-i18n --workspace packages/cli exit 0
git diff --check clean

2. Real-binary end-to-end matrix

Seed: ~/.claude.json mcpServers = cc-everything, cc-fetch, cc-http, __proto__ (reserved), cc-broken (a string, not an object) + projects["<ws>"].mcpServers = cc-project-db; ~/.claude/settings.json = cc-global-git; Claude Desktop = desktop-weather. Qwen settings start with no mcpServers.

# Scenario How Result
1 import all → user TUI /import-config Imported 5 … cc-everything, cc-fetch, cc-http, cc-global-git, desktop-weather · Skipped unsupported server name(s): __proto__ · warning …server "cc-broken" is not an object - skipped
2 persistence read ~/.qwen/settings.json ✅ all 5 written with command/args/type/url intact
3 prototype-pollution safety inspect imported object __proto__ produced no own key and a clean prototype (Object.create(null) + reserved-name skip)
4 idempotent re-run TUI /import-config again No new … imported · Skipped existing server(s): cc-everything … desktop-weather — no duplication
5 malformed config headless, broken Desktop JSON Warnings: Failed to parse …claude_desktop_config.json: … and nothing written
6 project scope headless --scope project Imported 2 … to project settings: proj-settings-srv, cc-project-db written to <ws>/.qwen/settings.json; user settings left untouched (Desktop correctly not scanned for project scope)
7 arg handling --help / bogus-source ✅ usage text / Unknown argument: bogus-source + usage

The TUI rendered case 1 as a warning message and case 4 as a message; settings were persisted to disk in both interactive and headless runs.

3. Non-blocking notes

  1. The 'invalid' skip path is unreachable. copyMcpServers() already drops any non-object server entry into errors[] (a warning) and continues, so source.servers only ever holds objects. The later if (!isRecord(serverConfig)) skipped.push({reason:'invalid'}) in importClaudeMcpServers() can therefore never fire, and the Skipped invalid server(s): … line in formatClaudeMcpImportResult() is dead output. In my run, the non-object cc-broken surfaced as a warning, not an invalid-skip (and no test asserts reason:'invalid'). Harmless, but the redundant check / output line could be removed.
  2. Error-icon on a benign no-op re-run. messageType is 'error' whenever errors.length > 0 && imported.length === 0. So re-running after a successful import, while a malformed/odd entry still produces a parse/shape warning, renders the whole result as a red even though nothing went wrong (everything was already imported). 'warning' would read better for the "0 imported but only warnings" case.

Neither affects correctness. Reads/writes, scope resolution (project+all → claude-code only), name preservation, reserved-name skipping, malformed handling, and persistence all behave correctly in the real binary.

Good to merge as Phase 1 of #4845.

中文版(Chinese version)

本地端到端验证(合并参考)

我构建了此 PR,并在真实 qwen 二进制中验证了 /import-config——交互式 TUI 用 tmux 驱动,外加 headless -p 运行——针对一个预置的隔离 $HOME,其中包含 Claude Code(~/.claude.json~/.claude/settings.json)与 Claude Desktop(~/.config/Claude/claude_desktop_config.json)的 MCP 配置,含合法条目与边界情况。

结论:该功能端到端完全按设计工作——读取正确的 Claude 路径、写入正确的 Qwen scope、保留已有名称、跳过保留名、优雅处理损坏配置、且不污染原型。在 Phase 1(仅 MCP)范围内建议合并。下面是两条不阻断的打磨建议。

环境:Linux、Node 22.22.2、head 326fe26d、隔离 $HOME--auth-type openai

1. PR 测试计划 —— 全绿

命令 结果
vitest run claudeMcpImport / importConfigCommand / BuiltinCommandLoader 31 通过(12 + 8 + 11)
typecheck --workspace packages/cli exit 0
lint --workspace packages/cli exit 0
check-i18n --workspace packages/cli exit 0
git diff --check 干净

2. 真实二进制端到端矩阵

预置:~/.claude.jsonmcpServers = cc-everythingcc-fetchcc-http__proto__(保留名)、cc-broken(是字符串、非对象)+ projects["<ws>"].mcpServers = cc-project-db~/.claude/settings.json = cc-global-git;Claude Desktop = desktop-weather。Qwen 设置初始无 mcpServers

# 场景 方式 结果
1 导入 all → user TUI /import-config Imported 5 … cc-everything, cc-fetch, cc-http, cc-global-git, desktop-weather · Skipped unsupported server name(s): __proto__ · 警告 …server "cc-broken" is not an object - skipped
2 持久化 ~/.qwen/settings.json ✅ 5 个全部写入,command/args/type/url 完整
3 原型污染安全性 检查导入对象 __proto__ 产生自有键、原型干净(Object.create(null) + 保留名跳过)
4 幂等重跑 TUI 再次 /import-config No new … imported · Skipped existing server(s): … —— 无重复
5 损坏配置 headless,损坏的 Desktop JSON Warnings: Failed to parse …,且不写入
6 project scope headless --scope project Imported 2 … to project settings: proj-settings-srv, cc-project-db,写入 <ws>/.qwen/settings.json;user 设置不动(project scope 正确地不扫描 Desktop)
7 参数处理 --help / bogus-source ✅ 用法文本 / Unknown argument: bogus-source + 用法

TUI 中场景 1 渲染为 警告消息,场景 4 渲染为 消息;交互与 headless 两种运行都把设置持久化到了磁盘。

3. 不阻断的建议

  1. 'invalid' 跳过分支不可达。 copyMcpServers() 已经把任何非对象的 server 条目推入 errors[](一条警告)并 continue,因此 source.servers 里永远只有对象。后面 importClaudeMcpServers() 中的 if (!isRecord(serverConfig)) skipped.push({reason:'invalid'}) 永远不会触发,formatClaudeMcpImportResult() 里的 Skipped invalid server(s): … 是死输出。我的运行中,非对象的 cc-broken 表现为一条警告,而非 invalid 跳过(且没有测试断言 reason:'invalid')。无害,但这个冗余检查/输出行可以删除。

  2. 良性 no-op 重跑显示错误图标。 只要 errors.length > 0 && imported.length === 0messageType 就是 'error'。因此在成功导入后再次运行时,只要还有某个损坏/异常条目产生解析/结构警告,整个结果就会渲染成红色 ,尽管什么都没出错(已全部导入过)。对"导入 0 但仅有警告"的情况,'warning' 会更合适。

两者都不影响正确性。读写、scope 解析(project+all → 仅 claude-code)、名称保留、保留名跳过、损坏处理、持久化,在真实二进制中均行为正确。

作为 #4845 的 Phase 1,可以合并。

@tt-a1i
tt-a1i force-pushed the feat/import-claude-mcp-config branch from 326fe26 to e1e3f0b Compare June 14, 2026 08:22
@tt-a1i

tt-a1i commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

thanks, both points make sense. pushed a follow-up that removes the dead invalid-skip branch/output and makes the no-op-with-warnings case render as a warning.

also rebased onto latest main and added the missing zh/zh-TW command description translations that were tripping CI. focused tests, typecheck, lint, check-i18n, and diff check are green locally; CI is rerunning.

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Follow-up verification of the polish commit (merge reference)

This supersedes my earlier report, which tested 326fe26d. The branch was rebased and e1e3f0b4 ("polish Claude MCP import feedback") landed. I rebuilt at the new head on Linux (Node 22.22.2) and re-ran everything.

Both of my earlier notes are addressed, verified live, and the feature still works end-to-end. LGTM.

What the polish commit changed (diff-confirmed at e1e3f0b4)

  • Note 1 — dead invalid path removed: dropped 'invalid' from the SkippedClaudeMcpServer union, the unreachable if (!isRecord(serverConfig)) … reason:'invalid' block in importClaudeMcpServers(), and the skippedInvalid filter + Skipped invalid server(s) output in the formatter.
  • Note 2 — no-op-with-warnings is now a warning: messageType simplified to errors>0 || skipped>0 ? 'warning' : 'info' — the 'error' branch is gone, so a result with skips/warnings but imported===0 renders as a warning. Genuine failures still surface as errors via the command action's try/catch. A new unit test (keeps no-op imports with source warnings as warnings) pins it.
  • CI: added the Import MCP servers from Claude configs command-description translation to en/zh/zh-TW, fixing check-i18n.

1. PR test plan — green at e1e3f0b4

Command Result
vitest run (3 files) 32 passed (12 + 9 + 11; the importConfig file gained the new no-op-warning test)
check-i18n --workspace packages/cli exit 0 (was the CI failure; now fixed)
typecheck --workspace packages/cli exit 0
lint --workspace packages/cli exit 0
git diff --check clean

Two process notes, neither a PR defect: the combined 3-file vitest run hit a one-off file-level failure with 0 assertion failures, green on immediate retry — the known coverage temp-file/worker race. And typecheck fails with TS6305 if run before packages/core finishes rebuilding after the rebase (stale core/dist); it passes once the core build completes, matching your own note about rebuilding core first.

2. Real-binary re-test (TUI via tmux) — the two fixes, observed

Same seeded isolated $HOME (Claude Code + Desktop configs; valid + __proto__ + a non-object cc-broken).

Check Prior head 326fe26d New head e1e3f0b4
/import-config (run 1) △ Imported 5 …, skips __proto__, warns cc-broken same△ Imported 5 … cc-everything, cc-fetch, cc-http, cc-global-git, desktop-weather
re-run (0 imported + already-exists + warning) error icon warningNo new Claude MCP servers imported … (Note 2 fixed)
non-object cc-broken warning + a dead Skipped invalid code path ✅ warning only; dead path/output gone (Note 1)
settings persisted 5 servers written ✅ 5 servers written, config intact
__proto__ prototype pollution none ✅ none (no own key, clean prototype)

Reads/writes, scope resolution, name preservation, reserved-name skipping, malformed handling, and persistence all still behave correctly in the real binary; the only user-visible change is the re-run now reading as a warning instead of an error, which is the intended fix.

Verdict: the polish cleanly resolves both notes with a new test and the i18n fix; nothing regressed. Good to merge as Phase 1 of #4845.

中文版(Chinese version)

polish 提交的跟进验证(合并参考)

本评论取代我此前的报告(当时测的是 326fe26d)。分支已 rebase,并合入了 e1e3f0b4("polish Claude MCP import feedback")。我在新 head 上于 Linux(Node 22.22.2)重新构建并重跑了全部验证。

我此前提的两点都已处理、已实测确认,且功能端到端仍正常。建议合并。

polish 提交改了什么(已在 e1e3f0b4 的 diff 中确认)

  • 建议 1 —— 移除死的 invalid 分支:从 SkippedClaudeMcpServer 联合类型去掉 'invalid',删除 importClaudeMcpServers() 中不可达的 if (!isRecord(serverConfig)) … reason:'invalid' 块,以及 formatter 里的 skippedInvalid 过滤与 Skipped invalid server(s) 输出。
  • 建议 2 —— "0 导入 + 警告"现在是 warningmessageType 简化为 errors>0 || skipped>0 ? 'warning' : 'info'——去掉了 'error' 分支,因此"有跳过/警告但 imported===0"的结果渲染为 warning。真正的失败仍通过命令 action 的 try/catch 报为 error。新增单元测试(keeps no-op imports with source warnings as warnings)锁定该行为。
  • CI:为 en/zh/zh-TW 补上 Import MCP servers from Claude configs 命令描述翻译,修复 check-i18n

1. PR 测试计划 —— 新 head 上全绿

命令 结果
vitest run(3 文件) 32 通过(12 + 9 + 11;importConfig 文件新增了 no-op-warning 测试)
check-i18n --workspace packages/cli exit 0(此前的 CI 失败点,现已修复)
typecheck --workspace packages/cli exit 0
lint --workspace packages/cli exit 0
git diff --check 干净

两点流程说明,均非 PR 缺陷:3 文件合并 vitest 运行出现过一次文件级失败但0 个断言失败,立即重跑即绿——即已知的 coverage 临时文件/worker 竞态。另外,rebase 后若在 packages/core 重建完成前跑 typecheck,会因 core/dist 过期报 TS6305;core 构建完成后即通过,与你自己提到的"先重建 core"一致。

2. 真实二进制复测(tmux 驱动 TUI)—— 两处修复实测

同一套预置隔离 $HOME(Claude Code + Desktop 配置;合法项 + __proto__ + 非对象的 cc-broken)。

检查项 旧 head 326fe26d 新 head e1e3f0b4
/import-config(第 1 次) △ Imported 5 …,跳过 __proto__,警告 cc-broken 相同 —— △ Imported 5 … cc-everything, cc-fetch, cc-http, cc-global-git, desktop-weather
重跑(0 导入 + already-exists + 警告) 错误图标 警告 —— No new Claude MCP servers imported …(建议 2 已修)
非对象 cc-broken 警告 + 一段死的 Skipped invalid 代码路径 ✅ 仅警告;死路径/输出已移除(建议 1)
设置持久化 写入 5 个 server ✅ 写入 5 个 server,配置完整
__proto__ 原型污染 ✅ 无(无自有键、原型干净)

读写、scope 解析、名称保留、保留名跳过、损坏处理、持久化在真实二进制中均仍然正确;唯一用户可见变化就是重跑现在显示为 warning 而非 error,这正是本次预期的修复。

结论:polish 干净地解决了两点,并配套新增测试与 i18n 修复,无回归。作为 #4845 的 Phase 1,可以合并。

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@tt-a1i
tt-a1i marked this pull request as ready for review June 14, 2026 08:46
@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template doesn't strictly match .github/pull_request_template.md (uses "Summary" / "Testing" instead of "What this PR does" / "Why it's needed" / "Reviewer Test Plan"), but all the essential information is there — what it does, how to test it, and the linked issue. Not blocking on this.

On direction: This is Phase 1 of #4845 (Claude config migration), scoped to MCP servers only — exactly the right starting point. Helping users bring their existing MCP configs over from Claude Code and Claude Desktop is a real friction-reducer. The phased approach in the parent issue makes sense, and this PR delivers the lowest-risk, highest-value slice. No direct Claude Code CHANGELOG entry for /import-config, but MCP config portability is clearly in the same problem space.

On approach: The scope feels right — two new modules (claudeMcpImport.ts for core logic, importConfigCommand.ts for the slash command), ~500 lines each for implementation and tests, plus i18n and registration. No over-engineering, no speculative features. The code reads Claude Code (.claude.json + .claude/settings.json) and Claude Desktop config paths, deduplicates by name, guards against prototype pollution, and reports errors clearly. Straightforward and contained.

Moving on to code review. 🔍

中文说明

感谢贡献,@tt-a1i

模板与 .github/pull_request_template.md 不完全一致(用了 "Summary" / "Testing" 而非规定的标题),但关键信息都有——功能说明、测试方法、关联 issue。不因此阻塞。

方向: 这是 #4845(Claude 配置迁移)的第一阶段,仅涉及 MCP 服务器——正确的起点。帮助用户从 Claude Code 和 Claude Desktop 迁移已有的 MCP 配置是真实存在的摩擦点。父 issue 的分阶段方案合理,本 PR 交付了风险最低、价值最高的部分。Claude Code CHANGELOG 中没有直接对应的 /import-config 功能,但 MCP 配置可移植性显然属于同一领域。

方案: 范围合适——两个新模块(claudeMcpImport.ts 核心逻辑 + importConfigCommand.ts 斜杠命令),实现和测试各约 500 行,加上 i18n 和注册。没有过度工程化,没有投机性功能。代码读取 Claude Code(.claude.json + .claude/settings.json)和 Claude Desktop 配置路径,按名称去重,防范 prototype pollution,错误报告清晰。简洁且可控。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal before reading the diff: I'd create a core import module in packages/cli/src/config/ that reads Claude Code (.claude.json, .claude/settings.json) and Claude Desktop platform config paths, parses JSON with comment stripping, deduplicates against existing Qwen MCP servers, guards against prototype pollution in server names, and writes via the existing LoadedSettings.setValue API. A separate slash command module handles arg parsing and formatting. Tests cover the main flows plus edge cases.

The PR's approach matches this closely. Two clean modules — claudeMcpImport.ts (core logic, ~500 lines) and importConfigCommand.ts (slash command, ~330 lines) — with comprehensive tests. No correctness bugs, no security holes, no regressions spotted. The code follows project conventions: proper type imports, ESM throughout, i18n keys for all three locales, strip-json-comments for resilient JSON parsing, Object.create(null) for safe server records, and defensive isRecord() guards.

No critical blockers. No AGENTS.md violations. The implementation is straightforward and maintainable.

Real-scenario testing

Set up a fake ~/.claude.json with two MCP servers (filesystem, github), then exercised the command via headless -p mode.

Test 1: /import-config --help

$ HOME=/tmp/triage-claude-home-4535 npm run dev -- -p '/import-config --help'

Import MCP servers from Claude configs.

Usage: /import-config [all|claude-code|claude-desktop] [--scope user|project]

Examples:
  /import-config
  /import-config claude-code
  /import-config claude-desktop --scope user
  /import-config --scope project

Test 2: /import-config (initial import)

$ HOME=/tmp/triage-claude-home-4535 npm run dev -- -p '/import-config'

Imported 2 MCP server(s) to user settings: filesystem, github

Test 3: /import-config (idempotent re-run)

$ HOME=/tmp/triage-claude-home-4535 npm run dev -- -p '/import-config'

No new Claude MCP servers imported into user settings.
Skipped existing server(s): filesystem, github

Test 4: /import-config claude-desktop (no Desktop config present)

$ HOME=/tmp/triage-claude-home-4535 npm run dev -- -p '/import-config claude-desktop'

No new Claude MCP servers imported into user settings.

Checked:
  - /tmp/triage-claude-home-4535/.config/Claude/claude_desktop_config.json

Test 5: Verify settings file written correctly

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {"GITHUB_TOKEN": "test-token"}
    }
  },
  "$version": 4
}

Test 6: Unit tests + typecheck + lint

Tests: 32 passed (3 files)
Typecheck: clean
Lint: clean

All scenarios behave as expected. The command is idempotent, reports clearly when nothing is found, and writes valid settings.

中文说明

代码审查

独立提案:在 packages/cli/src/config/ 中创建核心导入模块,读取 Claude Code(.claude.json.claude/settings.json)和 Claude Desktop 平台配置路径,解析 JSON(支持注释),与现有 Qwen MCP 服务器去重,防范 server 名称的 prototype pollution,通过已有的 LoadedSettings.setValue API 写入。单独的斜杠命令模块处理参数解析和格式化。测试覆盖主流程和边界情况。

PR 的方案与此一致。两个干净的模块——claudeMcpImport.ts(核心逻辑,~500 行)和 importConfigCommand.ts(斜杠命令,~330 行)——配合全面的测试。未发现正确性 bug、安全漏洞或回归。代码遵循项目规范:正确的类型导入、全 ESM、三种语言的 i18n、strip-json-comments 容错解析、Object.create(null) 安全记录、防御性 isRecord() 检查。

无关键阻塞。无 AGENTS.md 违规。 实现简洁可维护。

真实场景测试

创建了包含两个 MCP 服务器(filesystemgithub)的假 ~/.claude.json,通过 headless -p 模式测试。所有场景均符合预期:导入成功、幂等重复运行、无配置时清晰报告、设置文件正确写入。

单元测试 32 项通过,typecheck 和 lint 均干净。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This PR does exactly what it says, and it does it well. The implementation is clean — read Claude configs, merge into Qwen settings, skip duplicates, report clearly — with no unnecessary abstraction or speculative scope. The test suite is thorough (32 tests covering happy paths, edge cases like __proto__ poisoning, malformed JSON, and scope conflicts). Real-scenario testing confirms the command works end-to-end: import, idempotent re-run, source filtering, and correct settings file output all behave as expected.

My independent proposal matched the PR's approach almost exactly, which tells me the design space is natural and the author found the right shape. The code is the kind you'd want to find when debugging at 2 AM — each function does one thing, error messages are actionable, and the data flow is easy to trace.

Ship it. ✅

中文说明

本 PR 完全兑现了承诺。实现简洁——读取 Claude 配置,合并到 Qwen 设置,去重,清晰报告——没有不必要的抽象或投机性扩展。测试套件全面(32 项测试覆盖正常路径、__proto__ 注入、畸形 JSON、scope 冲突等边界情况)。真实场景测试确认端到端功能正常:导入、幂等重复运行、来源过滤、设置文件输出均符合预期。

我的独立提案与 PR 方案几乎一致,说明设计空间自然,作者找到了正确的形态。代码简洁易懂——每个函数职责单一,错误信息可操作,数据流易追踪。

可以合并。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 77acfd8 into QwenLM:main Jun 14, 2026
29 checks passed
doudouOUC pushed a commit that referenced this pull request Jun 15, 2026
* feat(cli): import Claude MCP servers

* fix(cli): polish Claude MCP import feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants