From 349886a3d6be78ab89e329ee213ef8f1cfc3f6e2 Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 28 Jul 2026 16:30:43 +0800 Subject: [PATCH] fix(uninstall): list managed MCP servers in the removal summary Uninstall already removed teamai-managed MCP servers, but the preview never mentioned them because they lived outside RemovalPlan. Read managed-mcp.json during plan discovery and print the entries alongside hooks/skills so dry-run and confirm prompts show what will go. --- src/__tests__/uninstall.test.ts | 39 +++++++++++++++++++++++++++++++++ src/uninstall.ts | 27 +++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/__tests__/uninstall.test.ts b/src/__tests__/uninstall.test.ts index 1591fc5..c2b2b13 100644 --- a/src/__tests__/uninstall.test.ts +++ b/src/__tests__/uninstall.test.ts @@ -390,6 +390,45 @@ describe('uninstall', () => { expect(claudeMd).toContain(TEAMAI_RULES_START); }); + it('uninstall summary lists teamai-managed MCP servers', async () => { + const { homeDir, repoPath, teamaiHome } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + + await fse.writeJson(path.join(teamaiHome, 'managed-mcp.json'), { + claude: [{ name: 'gpu-analysis', hash: 'abc' }], + cursor: [{ name: 'context7', hash: 'def' }], + }); + + mockAutoDetectInit.mockResolvedValue({ + localConfig: makeLocalConfig(homeDir, repoPath), + teamConfig: makeTeamConfig({ + toolPaths: { + claude: { + skills: '.claude/skills', + rules: '.claude/rules', + settings: '.claude/settings.json', + claudemd: '.claude/CLAUDE.md', + mcp: '.claude.json', + }, + }, + }), + }); + + const lines: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }); + + await uninstall({ dryRun: true, force: true }); + spy.mockRestore(); + + const summary = lines.join('\n'); + expect(summary).toContain('MCP servers (2):'); + expect(summary).toContain('claude/gpu-analysis'); + expect(summary).toContain('cursor/context7'); + }); + it('什么都不存在时正常退出', async () => { const homeDir = path.join(tmpDir, 'empty-home'); const repoPath = path.join(tmpDir, 'empty-repo'); diff --git a/src/uninstall.ts b/src/uninstall.ts index 4378d81..e3f1188 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -15,11 +15,13 @@ import { TEAMAI_ENV_END, getTeamaiHome, getManagedHooksPath, + managedMcpManifestPath, resolveBaseDir, type GlobalOptions, type TeamaiConfig, type LocalConfig, type Scope, + type ManagedMcpManifest, } from './types.js'; import { BUILTIN_RULE_NAMES } from './builtin-rules.js'; import { BUILTIN_AGENT_NAMES } from './builtin-agents.js'; @@ -27,6 +29,7 @@ import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; import { pathExists, readFileSafe, + readJson, writeFile, remove, listDirs, @@ -55,6 +58,8 @@ interface RemovalPlan { ruleFiles: string[]; /** Built-in agent .md files deployed by the CLI (e.g. teamai-recall). */ agentFiles: string[]; + /** teamai-managed MCP servers from managed-mcp.json (`tool/server` or `tool:project/server`). */ + mcpServers: string[]; /** Shell profile path containing env block (null if none). */ shellProfile: string | null; /** Docs directory (null if doesn't exist). */ @@ -148,6 +153,7 @@ async function buildRemovalPlan( skillDirs: [], ruleFiles: [], agentFiles: [], + mcpServers: [], shellProfile: null, docsDir: null, teamaiHome, @@ -156,6 +162,18 @@ async function buildRemovalPlan( scope: localConfig.scope, }; + // MCP servers are tracked in managed-mcp.json (same ownership model as hooks). + const mcpManifestPath = expandHome( + managedMcpManifestPath(localConfig.scope, localConfig.projectRoot), + ); + const mcpManifest = (await readJson(mcpManifestPath)) ?? {}; + for (const [toolKey, records] of Object.entries(mcpManifest)) { + for (const rec of records ?? []) { + if (rec?.name) plan.mcpServers.push(`${toolKey}/${rec.name}`); + } + } + plan.mcpServers.sort(); + // Discover team repo resource names for targeted removal. CLI built-in // resources (recall agent/rule, share-learnings skill, …) are deployed by // the CLI itself rather than synced from the team repo, so fold their names @@ -291,6 +309,7 @@ function isPlanEmpty(plan: RemovalPlan): boolean { plan.skillDirs.length === 0 && plan.ruleFiles.length === 0 && plan.agentFiles.length === 0 && + plan.mcpServers.length === 0 && plan.shellProfile === null && plan.docsDir === null && !plan.teamaiHomeExists @@ -343,6 +362,14 @@ function printSummary(plan: RemovalPlan): void { console.log(''); } + if (plan.mcpServers.length > 0) { + console.log(` MCP servers (${plan.mcpServers.length}):`); + for (const entry of plan.mcpServers) { + console.log(` ${entry}`); + } + console.log(''); + } + if (plan.shellProfile) { console.log(' Shell profile 环境变量块:'); console.log(` ${plan.shellProfile}`);