diff --git a/docs/usage-guide.md b/docs/usage-guide.md index b0e365070..491962468 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -1267,7 +1267,7 @@ Claude Code may show project `.mcp.json` servers as pending approval until you a ```bash teamai mcp list # servers, the file each comes from, secret status, and where they are installed teamai mcp inject # apply now; --dry-run to preview, --force to override collisions -teamai mcp remove # remove every teamai-managed server +teamai mcp remove # remove every teamai-managed server; --dry-run to preview ``` diff --git a/docs/usage-guide.zh-CN.md b/docs/usage-guide.zh-CN.md index 1e33ebf80..cb6a1b351 100644 --- a/docs/usage-guide.zh-CN.md +++ b/docs/usage-guide.zh-CN.md @@ -1147,7 +1147,7 @@ Claude Code 可能把来自仓库的 `.mcp.json` 标为待批准,需在交互 ```bash teamai mcp list # 查看 server、各自来自哪个文件、密钥状态与安装位置 teamai mcp inject # 立即注入;--dry-run 预览,--force 覆盖同名 -teamai mcp remove # 移除所有 teamai 管理的 server +teamai mcp remove # 移除所有 teamai 管理的 server;--dry-run 预览 ``` diff --git a/skill-data/setup/references/manage-admin.md b/skill-data/setup/references/manage-admin.md index dd3490816..904a7385d 100644 --- a/skill-data/setup/references/manage-admin.md +++ b/skill-data/setup/references/manage-admin.md @@ -31,6 +31,7 @@ run `teamai pull`). ```bash teamai mcp list # team MCP servers + per-tool install status teamai mcp inject # push team MCP servers into every AI tool's config +teamai mcp remove --dry-run # preview removal without changing tool configs or managed records teamai mcp remove # remove teamai-managed MCP servers ``` diff --git a/src/__tests__/e2e/mcp-uninstall.test.ts b/src/__tests__/e2e/mcp-uninstall.test.ts index 79a8316c9..9096ee9e2 100644 --- a/src/__tests__/e2e/mcp-uninstall.test.ts +++ b/src/__tests__/e2e/mcp-uninstall.test.ts @@ -8,8 +8,9 @@ import { fileURLToPath } from 'node:url'; // ─── MCP uninstall end-to-end ───────────────────────────────── // // Offline fixture (no TEAMAI_TEST_TOKEN). Drives the real CLI binary: -// mcp inject → servers land in ~/.claude.json -// uninstall → teamai-managed servers are removed; hand-added ones stay +// mcp inject → servers land in ~/.claude.json +// mcp remove --dry-run → previews removal without changing config or manifest +// uninstall → teamai-managed servers are removed; hand-added ones stay // // Catches the order bug where removeAll ran after ~/.teamai/ (and thus // managed-mcp.json) had already been deleted. @@ -121,7 +122,17 @@ describe('MCP uninstall (e2e)', () => { url: 'https://example.com/api/mcp', }); expect(afterInject.mcpServers['my-own']).toEqual({ command: 'my-server' }); - expect(fs.existsSync(path.join(teamaiHome, 'managed-mcp.json'))).toBe(true); + const manifestPath = path.join(teamaiHome, 'managed-mcp.json'); + expect(fs.existsSync(manifestPath)).toBe(true); + + const configBeforePreview = fs.readFileSync(claudeJson); + const manifestBeforePreview = fs.readFileSync(manifestPath); + const preview = await runCLI(['mcp', 'remove', '--dry-run'], env, homeDir); + expect(preview.code).toBe(0); + expect(preview.output).toContain('MCP remove (dry run):'); + expect(preview.output).toMatch(/removed\s+claude\/team-mcp/); + expect(fs.readFileSync(claudeJson)).toEqual(configBeforePreview); + expect(fs.readFileSync(manifestPath)).toEqual(manifestBeforePreview); const uninstall = await runCLI(['uninstall', '--force'], env, homeDir); expect(uninstall.code).toBe(0); diff --git a/src/__tests__/mcp-cmd.test.ts b/src/__tests__/mcp-cmd.test.ts index 97c9fd533..49343e79b 100644 --- a/src/__tests__/mcp-cmd.test.ts +++ b/src/__tests__/mcp-cmd.test.ts @@ -272,8 +272,28 @@ describe('mcpRemove', () => { spy.mockRestore(); } - expect(reconcileMcpForConfig).toHaveBeenCalledWith(init.teamConfig, init.localConfig, { removeAll: true }); + expect(reconcileMcpForConfig).toHaveBeenCalledWith(init.teamConfig, init.localConfig, { removeAll: true, dryRun: undefined }); expect(releaseCleanMcpGitExcludes).toHaveBeenCalledWith(init.teamConfig, init.localConfig); expect(order).toEqual(['reconcile', 'release']); }); + + it('previews removal without changing MCP configs, migration state, or git exclusions', async () => { + const init = { localConfig: { repo: { localPath: '/repo' }, scope: 'project', projectRoot: '/work/app' }, teamConfig: { toolPaths: {} } }; + mockedAutoDetectInit.mockResolvedValue(init); + vi.mocked(releaseCleanMcpGitExcludes).mockClear(); + (reconcileMcpForConfig as Mock).mockResolvedValueOnce({ changes: [{ action: 'removed', tool: 'claude', server: 'team-mcp' }], wrote: false }); + const output: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((line?: unknown) => { output.push(String(line)); }); + try { + await mcpRemove({ dryRun: true }); + } finally { + spy.mockRestore(); + } + + expect(mockedAutoDetectInit).toHaveBeenCalledWith(undefined, { dryRun: true }); + expect(reconcileMcpForConfig).toHaveBeenCalledWith(init.teamConfig, init.localConfig, { removeAll: true, dryRun: true }); + expect(releaseCleanMcpGitExcludes).not.toHaveBeenCalled(); + expect(output.join('\n')).toContain('MCP remove (dry run):'); + expect(output.join('\n')).toContain('removed'); + }); }); diff --git a/src/mcp-cmd.ts b/src/mcp-cmd.ts index 75aa2ae09..f86922a63 100644 --- a/src/mcp-cmd.ts +++ b/src/mcp-cmd.ts @@ -148,15 +148,18 @@ export async function mcpInject( else if (!options.dryRun) log.info('Already up to date.'); } -export async function mcpRemove(_options: GlobalOptions): Promise { - const { localConfig, teamConfig } = await autoDetectInit(); - const { changes, wrote } = await reconcileMcpForConfig(teamConfig, localConfig, { removeAll: true }); +export async function mcpRemove(options: GlobalOptions): Promise { + const { localConfig, teamConfig } = await autoDetectInit(undefined, { dryRun: options.dryRun }); + const { changes, wrote } = await reconcileMcpForConfig(teamConfig, localConfig, { + removeAll: true, + dryRun: options.dryRun, + }); // Nothing of teamai's is left for .git/info/exclude to protect (#882). - await releaseCleanMcpGitExcludes(teamConfig, localConfig); + if (!options.dryRun) await releaseCleanMcpGitExcludes(teamConfig, localConfig); - console.log('MCP remove:'); + console.log(options.dryRun ? 'MCP remove (dry run):' : 'MCP remove:'); reportChanges(changes); if (wrote) log.success('teamai-managed MCP servers removed.'); - else log.info('Nothing to remove.'); + else if (!options.dryRun) log.info('Nothing to remove.'); }