diff --git a/packages/coding-agent/src/mcp/config-writer.ts b/packages/coding-agent/src/mcp/config-writer.ts index 87fe459a0..94bada1ab 100644 --- a/packages/coding-agent/src/mcp/config-writer.ts +++ b/packages/coding-agent/src/mcp/config-writer.ts @@ -180,3 +180,36 @@ export async function listMCPServers(filePath: string): Promise { const config = await readMCPConfigFile(filePath); return Object.keys(config.mcpServers ?? {}); } + +/** + * Read the disabled servers list from a config file. + */ +export async function readDisabledServers(filePath: string): Promise { + const config = await readMCPConfigFile(filePath); + return config.disabledServers ?? []; +} + +/** + * Add or remove a server name from the disabled servers list. + */ +export async function setServerDisabled(filePath: string, name: string, disabled: boolean): Promise { + const config = await readMCPConfigFile(filePath); + const current = new Set(config.disabledServers ?? []); + + if (disabled) { + current.add(name); + } else { + current.delete(name); + } + + const updated: MCPConfigFile = { + ...config, + disabledServers: current.size > 0 ? Array.from(current).sort() : undefined, + }; + + if (!updated.disabledServers) { + delete updated.disabledServers; + } + + await writeMCPConfigFile(filePath, updated); +} diff --git a/packages/coding-agent/src/mcp/config.ts b/packages/coding-agent/src/mcp/config.ts index b1f8695a6..3ffb42df7 100644 --- a/packages/coding-agent/src/mcp/config.ts +++ b/packages/coding-agent/src/mcp/config.ts @@ -3,10 +3,13 @@ * * Uses the capability system to load MCP servers from multiple sources. */ + +import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs"; import { mcpCapability } from "../capability/mcp"; import type { SourceMeta } from "../capability/types"; import type { MCPServer } from "../discovery"; import { loadCapability } from "../discovery"; +import { readDisabledServers } from "./config-writer"; import type { MCPServerConfig } from "./types"; /** Options for loading MCP configs */ @@ -97,12 +100,14 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt ? result.items : result.items.filter(server => server._source.level !== "project"); + // Load user-level disabled servers list + const disabledServers = new Set(await readDisabledServers(getMCPConfigPath("user", cwd))); // Convert to legacy format and preserve source metadata const configs: Record = {}; const sources: Record = {}; for (const server of servers) { const config = convertToLegacyConfig(server); - if (config.enabled === false) { + if (config.enabled === false || disabledServers.has(server.name)) { continue; } configs[server.name] = config; diff --git a/packages/coding-agent/src/mcp/types.ts b/packages/coding-agent/src/mcp/types.ts index f5eb77df2..583b6d817 100644 --- a/packages/coding-agent/src/mcp/types.ts +++ b/packages/coding-agent/src/mcp/types.ts @@ -89,6 +89,7 @@ export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSs /** Root .mcp.json file structure */ export interface MCPConfigFile { mcpServers?: Record; + disabledServers?: string[]; } // ============================================================================= diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index 78103555d..50504025b 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -8,7 +8,14 @@ import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { SourceMeta } from "../../capability/types"; import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp"; import { connectToServer, disconnectServer, listTools } from "../../mcp/client"; -import { addMCPServer, readMCPConfigFile, removeMCPServer, updateMCPServer } from "../../mcp/config-writer"; +import { + addMCPServer, + readDisabledServers, + readMCPConfigFile, + removeMCPServer, + setServerDisabled, + updateMCPServer, +} from "../../mcp/config-writer"; import { MCPOAuthFlow } from "../../mcp/oauth-flow"; import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types"; import type { OAuthCredential } from "../../session/auth-storage"; @@ -774,7 +781,12 @@ export class MCPCommandController { } } - if (userServers.length === 0 && projectServers.length === 0 && discoveredServers.length === 0) { + if ( + userServers.length === 0 && + projectServers.length === 0 && + discoveredServers.length === 0 && + (userConfig.disabledServers ?? []).length === 0 + ) { this.#showMessage( [ "", @@ -868,6 +880,17 @@ export class MCPCommandController { lines.push(""); } } + + // Show servers disabled via /mcp disable (from third-party configs) + const disabledServers = await readDisabledServers(userPath); + const relevantDisabled = disabledServers.filter(n => !configServerNames.has(n)); + if (relevantDisabled.length > 0) { + lines.push(theme.fg("accent", "Disabled") + theme.fg("muted", " (discovered servers):")); + for (const name of relevantDisabled) { + lines.push(` ${theme.fg("accent", name)}${theme.fg("warning", " ◌ disabled")}`); + } + lines.push(""); + } this.#showMessage(lines.join("\n")); } catch (error) { this.ctx.showError(`Failed to list servers: ${error instanceof Error ? error.message : String(error)}`); @@ -1061,7 +1084,41 @@ export class MCPCommandController { try { const found = await this.#findConfiguredServer(name); if (!found) { - this.ctx.showError(`Server "${name}" not found.`); + // Check if this is a discovered server from a third-party config + const userConfigPath = getMCPConfigPath("user", getProjectDir()); + const disabledServers = new Set(await readDisabledServers(userConfigPath)); + const isDiscovered = this.ctx.mcpManager?.getSource(name); + const isCurrentlyDisabled = disabledServers.has(name); + if (!isDiscovered && !isCurrentlyDisabled) { + this.ctx.showError(`Server "${name}" not found.`); + return; + } + if (isCurrentlyDisabled === !enabled) { + this.#showMessage( + ["", theme.fg("muted", `Server "${name}" is already ${enabled ? "enabled" : "disabled"}.`), ""].join( + "\n", + ), + ); + return; + } + await setServerDisabled(userConfigPath, name, !enabled); + if (enabled) { + await this.#reloadMCP(); + const state = await this.#waitForServerConnectionWithAnimation(name); + const status = + state === "connected" + ? theme.fg("success", "Connected") + : state === "connecting" + ? theme.fg("muted", "Connecting") + : theme.fg("warning", "Not connected yet"); + this.#showMessage( + ["", theme.fg("success", `\u2713 Enabled "${name}"`), "", ` Status: ${status}`, ""].join("\n"), + ); + } else { + await this.ctx.mcpManager?.disconnectServer(name); + await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []); + this.#showMessage(["", theme.fg("success", `\u2713 Disabled "${name}"`), ""].join("\n")); + } return; }