feat(mcp): support disabling discovered servers from third-party configs

Previously, /mcp enable|disable only worked for servers defined directly
in user or project config files. Discovered servers from third-party
configs (e.g. capability-provided) could not be toggled off without
removing them at the source.

This adds a disabledServers list to the user-level .mcp.json config that
acts as an overlay. When loading MCP configs, servers whose names appear
in this list are excluded alongside those with enabled: false.

Changes:
- Add disabledServers field to MCPConfigFile type
- Add readDisabledServers/setServerDisabled helpers in config-writer
- Filter discovered servers against the disabled list during config load
- Handle enable/disable toggle for discovered servers in the MCP
  command controller, including reconnection on re-enable
- Show disabled discovered servers in /mcp list output
This commit is contained in:
Roman Alexander
2026-02-16 00:49:35 +07:00
parent e7d90ff703
commit 0800cfa2d3
4 changed files with 100 additions and 4 deletions
@@ -180,3 +180,36 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
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<string[]> {
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<void> {
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);
}
+6 -1
View File
@@ -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<string, MCPServerConfig> = {};
const sources: Record<string, SourceMeta> = {};
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;
+1
View File
@@ -89,6 +89,7 @@ export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSs
/** Root .mcp.json file structure */
export interface MCPConfigFile {
mcpServers?: Record<string, MCPServerConfig>;
disabledServers?: string[];
}
// =============================================================================
@@ -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;
}