import { execSync, execFileSync } from 'child_process';
import { existsSync, readFileSync, writeFileSync, rmSync, chmodSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { resolveCodexCliPath } from './codex-cli.js';
const CONFIG_DIR = join(homedir(), '.ice-puzzle-mcp');
const MCP_SERVER_NAME = 'ice-puzzle-mcp';
function isCommandAvailable(command: string): boolean {
try {
execSync(`${command} --version`, { stdio: 'pipe', timeout: 5000 });
return true;
} catch {
return false;
}
}
function removeFromClaudeCli(): boolean {
if (!isCommandAvailable('claude')) {
return false;
}
try {
execSync(`claude mcp remove --scope user ${MCP_SERVER_NAME}`, { stdio: 'pipe', timeout: 10000 });
console.log(` Removed ${MCP_SERVER_NAME} from Claude Code registry.`);
return true;
} catch {
// May not be registered — that's fine.
return false;
}
}
function removeFromCodexCli(codexCommand: string): boolean {
const namesToRemove = new Set<string>([MCP_SERVER_NAME]);
try {
const listRaw = execFileSync(
codexCommand,
['mcp', 'list', '--json'],
{ stdio: 'pipe', timeout: 15000, encoding: 'utf-8' },
);
const parsed = JSON.parse(listRaw) as unknown;
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (!item || typeof item !== 'object') continue;
const record = item as Record<string, unknown>;
const name = typeof record.name === 'string' ? record.name : null;
const transport = record.transport;
if (!name || !transport || typeof transport !== 'object') continue;
const argsValue = (transport as Record<string, unknown>).args;
if (!Array.isArray(argsValue)) continue;
const args = argsValue.filter((arg): arg is string => typeof arg === 'string');
if (args.includes(MCP_SERVER_NAME)) {
namesToRemove.add(name);
}
}
}
} catch {
// Ignore list failures; we'll still try to remove the canonical name.
}
const removedNames: string[] = [];
for (const serverName of namesToRemove) {
try {
const output = execFileSync(
codexCommand,
['mcp', 'remove', serverName],
{ stdio: 'pipe', timeout: 10000, encoding: 'utf-8' },
);
if (!output.includes('No MCP server named')) {
removedNames.push(serverName);
}
} catch {
// Ignore remove failures per server and continue cleanup.
}
}
if (removedNames.length > 0) {
console.log(` Removed ${removedNames.join(', ')} from Codex MCP registry.`);
return true;
}
return false;
}
function removeFromConfigFile(configPath: string): boolean {
if (!existsSync(configPath)) return false;
let raw: string;
try {
raw = readFileSync(configPath, 'utf-8');
} catch {
return false;
}
let config: any;
try {
config = JSON.parse(raw);
} catch {
return false;
}
if (!config?.mcpServers || typeof config.mcpServers !== 'object') return false;
const parseArgs = (value: any): string[] => (Array.isArray(value) ? value.filter((x: any) => typeof x === 'string') : []);
const isIcePuzzleServer = (entry: any): boolean =>
entry
&& typeof entry === 'object'
&& typeof entry.command === 'string'
&& parseArgs(entry.args).includes(MCP_SERVER_NAME);
const keysToRemove = Object.entries(config.mcpServers)
.filter(([, value]) => isIcePuzzleServer(value))
.map(([key]) => key);
if (keysToRemove.length === 0) return false;
for (const key of keysToRemove) {
delete config.mcpServers[key];
}
// Detect indentation from existing file
const indentMatch = raw.match(/^(\s+)"/m);
const indent: string | number = indentMatch
? (indentMatch[1].includes('\t') ? '\t' : indentMatch[1].length)
: 2;
const trailingNewline = raw.endsWith('\n');
let output = JSON.stringify(config, null, indent);
if (trailingNewline) output += '\n';
writeFileSync(configPath, output, { mode: 0o600 });
chmodSync(configPath, 0o600);
console.log(` Removed ${keysToRemove.join(', ')} from ${configPath}`);
return true;
}
export async function runUninstall(): Promise<void> {
console.log('');
console.log(' ╔═══════════════════════════════════════════╗');
console.log(' ║ ICE PUZZLE MCP UNINSTALL ║');
console.log(' ╚═══════════════════════════════════════════╝');
console.log('');
let anyRemoved = false;
// Step 1: Remove from Claude Code via CLI
console.log(' Step 1/4: Claude Code registry');
if (removeFromClaudeCli()) {
anyRemoved = true;
} else {
console.log(' Claude CLI not available. Checking config files instead.');
}
// Step 2: Remove from config files (fallback / cleanup)
console.log('');
console.log(' Step 2/4: Claude MCP config files');
const configFiles = [
join(homedir(), '.claude', '.mcp.json'),
join(homedir(), '.claude', 'mcp.json'),
];
let removedFromFiles = false;
for (const configFile of configFiles) {
if (removeFromConfigFile(configFile)) {
removedFromFiles = true;
anyRemoved = true;
}
}
if (!removedFromFiles) {
console.log(' No ice-puzzle entries found in config files.');
}
// Step 3: Remove from Codex registry
console.log('');
console.log(' Step 3/4: Codex MCP registry');
const codexCommand = resolveCodexCliPath();
if (!codexCommand) {
console.log(' Codex CLI not found in PATH or standard app locations.');
} else {
if (codexCommand !== 'codex') {
console.log(` Found Codex CLI at ${codexCommand}.`);
}
if (removeFromCodexCli(codexCommand)) {
anyRemoved = true;
} else {
console.log(' No ice-puzzle entries found in Codex MCP config.');
}
}
// Step 4: Remove config directory
console.log('');
console.log(' Step 4/4: Config directory');
if (existsSync(CONFIG_DIR)) {
rmSync(CONFIG_DIR, { recursive: true, force: true });
console.log(` Removed ${CONFIG_DIR}`);
anyRemoved = true;
} else {
console.log(' Config directory not found (already removed).');
}
// Summary
console.log('');
if (anyRemoved) {
console.log(' Uninstall complete. Restart Claude Code and/or Codex to apply changes.');
} else {
console.log(` Nothing to uninstall - ${MCP_SERVER_NAME} was not configured.`);
}
console.log('');
console.log(' To reinstall: npx ice-puzzle-mcp setup');
console.log('');
}