feat: support disabling bundled mcp servers

This commit is contained in:
Affaan Mustafa
2026-04-05 14:37:28 -07:00
parent 1346f83b08
commit 786f46dad5
7 changed files with 283 additions and 4 deletions

View File

@@ -19,6 +19,7 @@
const fs = require('fs');
const path = require('path');
const { parseDisabledMcpServers } = require('../lib/mcp-config');
let TOML;
try {
@@ -210,6 +211,7 @@ function main() {
const configPath = args.find(a => !a.startsWith('-'));
const dryRun = args.includes('--dry-run');
const updateMcp = args.includes('--update-mcp');
const disabledServers = new Set(parseDisabledMcpServers(process.env.ECC_DISABLED_MCPS));
if (!configPath) {
console.error('Usage: merge-mcp-config.js <config.toml> [--dry-run] [--update-mcp]');
@@ -222,6 +224,9 @@ function main() {
}
log(`Package manager: ${PM_NAME} (exec: ${PM_EXEC})`);
if (disabledServers.size > 0) {
log(`Disabled via ECC_DISABLED_MCPS: ${[...disabledServers].join(', ')}`);
}
let raw = fs.readFileSync(configPath, 'utf8');
let parsed;
@@ -249,6 +254,18 @@ function main() {
const finalEntry = resolvedEntry || urlEntry;
const resolvedLabel = hasCanonical ? name : legacyName || name;
if (disabledServers.has(name)) {
if (finalEntry) {
toRemoveLog.push(`mcp_servers.${resolvedLabel} (disabled)`);
raw = removeServerFromText(raw, resolvedLabel, existing);
if (resolvedLabel !== name) {
raw = removeServerFromText(raw, name, existing);
}
}
log(` [skip] mcp_servers.${name} (disabled)`);
continue;
}
if (finalEntry) {
if (updateMcp) {
// --update-mcp: remove existing section (and legacy alias), will re-add below
@@ -278,7 +295,9 @@ function main() {
}
}
if (toAppend.length === 0) {
const hasRemovals = toRemoveLog.length > 0;
if (toAppend.length === 0 && !hasRemovals) {
log('All ECC MCP servers already present. Nothing to do.');
return;
}
@@ -297,14 +316,19 @@ function main() {
// Write: for add-only, append to preserve existing content byte-for-byte.
// For --update-mcp, we modified `raw` above, so write the full file + appended sections.
if (updateMcp) {
if (updateMcp || hasRemovals) {
for (const label of toRemoveLog) log(` [update] ${label}`);
const cleaned = raw.replace(/\n+$/, '\n');
fs.writeFileSync(configPath, cleaned + appendText, 'utf8');
fs.writeFileSync(configPath, cleaned + (toAppend.length > 0 ? appendText : ''), 'utf8');
} else {
fs.appendFileSync(configPath, appendText, 'utf8');
}
if (hasRemovals && toAppend.length === 0) {
log(`Done. Removed ${toRemoveLog.length} disabled server(s).`);
return;
}
log(`Done. ${toAppend.length} server(s) ${updateMcp ? 'updated' : 'added'}.`);
}

View File

@@ -4,6 +4,7 @@ const fs = require('fs');
const path = require('path');
const { writeInstallState } = require('../install-state');
const { filterMcpConfig, parseDisabledMcpServers } = require('../mcp-config');
function readJsonObject(filePath, label) {
let parsed;
@@ -124,6 +125,49 @@ function findHooksSourcePath(plan, hooksDestinationPath) {
return operation ? operation.sourcePath : null;
}
function isMcpConfigPath(filePath) {
const basename = path.basename(String(filePath || ''));
return basename === '.mcp.json' || basename === 'mcp.json';
}
function buildFilteredMcpWrites(plan) {
const disabledServers = parseDisabledMcpServers(process.env.ECC_DISABLED_MCPS);
if (disabledServers.length === 0) {
return [];
}
const writes = [];
for (const operation of plan.operations) {
if (!isMcpConfigPath(operation.destinationPath) || !operation.sourcePath || !fs.existsSync(operation.sourcePath)) {
continue;
}
let sourceConfig;
try {
sourceConfig = readJsonObject(operation.sourcePath, 'MCP config');
} catch {
continue;
}
if (!sourceConfig.mcpServers || typeof sourceConfig.mcpServers !== 'object' || Array.isArray(sourceConfig.mcpServers)) {
continue;
}
const filtered = filterMcpConfig(sourceConfig, disabledServers);
if (filtered.removed.length === 0) {
continue;
}
writes.push({
destinationPath: operation.destinationPath,
filteredConfig: filtered.config,
});
}
return writes;
}
function buildMergedSettings(plan) {
if (!plan.adapter || plan.adapter.target !== 'claude') {
return null;
@@ -177,6 +221,7 @@ function buildMergedSettings(plan) {
function applyInstallPlan(plan) {
const mergedSettingsPlan = buildMergedSettings(plan);
const filteredMcpWrites = buildFilteredMcpWrites(plan);
for (const operation of plan.operations) {
fs.mkdirSync(path.dirname(operation.destinationPath), { recursive: true });
@@ -198,6 +243,15 @@ function applyInstallPlan(plan) {
);
}
for (const writePlan of filteredMcpWrites) {
fs.mkdirSync(path.dirname(writePlan.destinationPath), { recursive: true });
fs.writeFileSync(
writePlan.destinationPath,
JSON.stringify(writePlan.filteredConfig, null, 2) + '\n',
'utf8'
);
}
writeInstallState(plan.installStatePath, plan.statePreview);
return {

56
scripts/lib/mcp-config.js Normal file
View File

@@ -0,0 +1,56 @@
'use strict';
function parseDisabledMcpServers(value) {
return [...new Set(
String(value || '')
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
)];
}
function filterMcpConfig(config, disabledServerNames = []) {
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error('MCP config must be a JSON object');
}
const servers = config.mcpServers;
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
throw new Error('MCP config must include an mcpServers object');
}
const disabled = new Set(parseDisabledMcpServers(disabledServerNames));
if (disabled.size === 0) {
return {
config: {
...config,
mcpServers: { ...servers },
},
removed: [],
};
}
const nextServers = {};
const removed = [];
for (const [name, serverConfig] of Object.entries(servers)) {
if (disabled.has(name)) {
removed.push(name);
continue;
}
nextServers[name] = serverConfig;
}
return {
config: {
...config,
mcpServers: nextServers,
},
removed,
};
}
module.exports = {
filterMcpConfig,
parseDisabledMcpServers,
};