import { join } from "node:path"; import { McpcController } from "./controller.ts"; import { getMcpConfigOverrideFromArgv, resolveAgentDir } from "./discovery.ts"; import { createMcpcPanel, type PanelChange, type PanelResult } from "./panel.ts"; import type { McpcCommandContext, McpcExtensionApi } from "./pi-types.ts"; import type { ActionResult, ServerListItem } from "./types.ts"; const SUBCOMMANDS = ["block", "enable", "cleanup", "list"] as const; function controllerFor(cwd: string): McpcController { const agentDir = resolveAgentDir(); const overrideConfigPath = getMcpConfigOverrideFromArgv(); return new McpcController({ cwd, agentDir, statePath: join(agentDir, "mcpc-state.json"), ...(overrideConfigPath !== undefined ? { overrideConfigPath } : {}), }); } function parseArgs(args: string): { command: string; target: string } { const trimmed = args.trim(); if (!trimmed) return { command: "", target: "" }; const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/); return { command: match?.[1]?.toLowerCase() ?? "", target: match?.[2]?.trim() ?? "" }; } function notify(ctx: McpcCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void { if (ctx.hasUI) ctx.ui.notify(message, level); else console.log(message); } function formatDetails(details: readonly string[]): string { if (details.length === 0) return ""; const shown = details.slice(0, 3).map((detail) => `\n ${detail}`).join(""); return `${shown}${details.length > 3 ? `\n ...and ${details.length - 3} more` : ""}`; } function reportAction(ctx: McpcCommandContext, result: ActionResult): void { switch (result.outcome) { case "blocked": notify(ctx, `MCPC: blocked "${result.name}" in ${result.details.length} config source(s); reloading.`); return; case "enabled": notify(ctx, `MCPC: restored "${result.name}"; reloading.${formatDetails(result.details)}`); return; case "cleaned": notify(ctx, `MCPC: removed stale blocked record for "${result.name}".`); return; case "already-blocked": notify(ctx, `MCPC: "${result.name}" is already blocked.`); return; case "not-blocked": notify(ctx, `MCPC: "${result.name}" has no blocked record.`, "warning"); return; case "not-found": notify(ctx, `MCPC: MCP server "${result.name}" was not found in the effective config.${formatDetails(result.details)}`, "error"); return; case "not-stale": notify(ctx, `MCPC: blocked record "${result.name}" is still authoritative; cleanup is only allowed for conflicts.`, "warning"); return; case "conflict": notify( ctx, `MCPC: cannot restore "${result.name}" because a same-name definition exists. Nothing was overwritten.${formatDetails(result.details)}\nRun /mcpc cleanup ${result.name} to keep the active definition and discard the stale record.`, "error", ); } } function renderStatus(items: readonly ServerListItem[]): string { if (items.length === 0) return "MCPC: no MCP servers found."; const lines = ["MCPC server visibility:", ""]; for (const item of items) { const marker = item.visibility === "enabled" ? "E" : item.visibility === "blocked" ? "B" : "!"; lines.push(`[${marker}] ${item.name}: ${item.visibility}`); } return lines.join("\n"); } async function performToggle( action: "block" | "enable", name: string, ctx: McpcCommandContext, ): Promise { if (!name) { notify(ctx, `Usage: /mcpc ${action} `, "error"); return; } const controller = controllerFor(ctx.cwd); const result = action === "block" ? controller.block(name) : controller.enable(name); reportAction(ctx, result); if (!result.changed) return; await ctx.reload(); } async function performPanelChanges( changes: readonly PanelChange[], ctx: McpcCommandContext, ): Promise { if (changes.length === 0) return; const controller = controllerFor(ctx.cwd); const applied: ActionResult[] = []; for (const change of changes) { try { const result = change.action === "block" ? controller.block(change.name) : controller.enable(change.name); if (result.changed) applied.push(result); else reportAction(ctx, result); } catch (error) { const message = error instanceof Error ? error.message : String(error); notify(ctx, `MCPC: failed to ${change.action} "${change.name}": ${message}`, "error"); } } if (applied.length === 0) return; const blocked = applied.filter((result) => result.outcome === "blocked").length; const enabled = applied.filter((result) => result.outcome === "enabled").length; notify( ctx, `MCPC: saved ${applied.length} change(s) (${blocked} blocked, ${enabled} enabled); reloading.`, ); await ctx.reload(); } async function openPanel(ctx: McpcCommandContext): Promise { const controller = controllerFor(ctx.cwd); const items = controller.list(); if (ctx.mode !== "tui") { notify(ctx, renderStatus(items)); return; } const result = await ctx.ui.custom( (tui, theme, keybindings, done) => createMcpcPanel(items, tui, theme, keybindings, done), { overlay: true, overlayOptions: { anchor: "center", width: 76, minWidth: 40, maxHeight: "80%" }, }, ); if (result.cancelled || result.changes.length === 0) return; await performPanelChanges(result.changes, ctx); } function argumentCompletions(prefix: string): Array<{ value: string; label: string }> | null { const normalized = prefix.trimStart(); const match = normalized.match(/^(\S+)\s+(.*)$/); if (!match) { const items = SUBCOMMANDS .filter((command) => command.startsWith(normalized)) .map((command) => ({ value: command, label: command })); return items.length > 0 ? items : null; } const command = match[1]!.toLowerCase(); const namePrefix = match[2]!.trimStart(); try { const items = controllerFor(process.cwd()).list().filter((item) => { if (!item.name.startsWith(namePrefix)) return false; if (command === "block") return item.visibility === "enabled"; if (command === "enable") return item.visibility !== "enabled"; if (command === "cleanup") return item.visibility === "conflict"; return false; }); const completions = items.map((item) => ({ value: `${command} ${item.name}`, label: item.name })); return completions.length > 0 ? completions : null; } catch { return null; } } export default function mcpcExtension(pi: McpcExtensionApi): void { pi.on("session_start", (_event, ctx) => { if (!ctx.hasUI) return; try { const conflicts = controllerFor(ctx.cwd).getConflicts(); if (conflicts.length === 0) return; const names = conflicts.map((item) => item.name).join(", "); ctx.ui.notify( `MCPC: stale blocked record conflict${conflicts.length === 1 ? "" : "s"}: ${names}. Use /mcpc cleanup after reviewing the active definition.`, "warning", ); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`MCPC: failed to inspect blocked state: ${message}`, "error"); } }); pi.registerCommand("mcpc", { description: "Block or enable MCP servers outside the adapter context", getArgumentCompletions: argumentCompletions, handler: async (args, ctx) => { try { const { command, target } = parseArgs(args ?? ""); if (command === "" || command === "list") { await openPanel(ctx); return; } if (command === "block" || command === "enable") { await performToggle(command, target, ctx); return; } if (command === "cleanup") { const controller = controllerFor(ctx.cwd); if (target === "--all") { const results = controller.cleanupAllConflicts(); if (results.length === 0) { notify(ctx, "MCPC: no stale conflict records found."); return; } notify(ctx, `MCPC: removed ${results.length} stale conflict record(s): ${results.map((result) => result.name).join(", ")}.`); return; } if (!target) { notify(ctx, "Usage: /mcpc cleanup | /mcpc cleanup --all", "error"); return; } reportAction(ctx, controller.cleanup(target)); return; } notify(ctx, "Usage: /mcpc [block | enable | cleanup |--all | list]", "error"); } catch (error) { const message = error instanceof Error ? error.message : String(error); notify(ctx, `MCPC: ${message}`, "error"); } }, }); }