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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user