mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-07 01:33:31 +08:00
feat: support disabling bundled mcp servers
This commit is contained in:
@@ -708,6 +708,14 @@ Copy the hooks from `hooks/hooks.json` to your `~/.claude/settings.json`.
|
|||||||
|
|
||||||
Copy desired MCP server definitions from `mcp-configs/mcp-servers.json` into your official Claude Code config in `~/.claude/settings.json`, or into a project-scoped `.mcp.json` if you want repo-local MCP access.
|
Copy desired MCP server definitions from `mcp-configs/mcp-servers.json` into your official Claude Code config in `~/.claude/settings.json`, or into a project-scoped `.mcp.json` if you want repo-local MCP access.
|
||||||
|
|
||||||
|
If you already run your own copies of ECC-bundled MCPs, set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ECC_DISABLED_MCPS="github,context7,exa,playwright,sequential-thinking,memory"
|
||||||
|
```
|
||||||
|
|
||||||
|
ECC-managed install and Codex sync flows will skip or remove those bundled servers instead of re-adding duplicates.
|
||||||
|
|
||||||
**Important:** Replace `YOUR_*_HERE` placeholders with your actual API keys.
|
**Important:** Replace `YOUR_*_HERE` placeholders with your actual API keys.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
"_comments": {
|
"_comments": {
|
||||||
"usage": "Copy the servers you need to your ~/.claude.json mcpServers section",
|
"usage": "Copy the servers you need to your ~/.claude.json mcpServers section",
|
||||||
"env_vars": "Replace YOUR_*_HERE placeholders with actual values",
|
"env_vars": "Replace YOUR_*_HERE placeholders with actual values",
|
||||||
"disabling": "Use disabledMcpServers array in project config to disable per-project",
|
"disabling": "Use ECC_DISABLED_MCPS=github,context7,... to disable bundled ECC MCPs during install/sync, or use disabledMcpServers in project config for per-project overrides",
|
||||||
"context_warning": "Keep under 10 MCPs enabled to preserve context window"
|
"context_warning": "Keep under 10 MCPs enabled to preserve context window"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { parseDisabledMcpServers } = require('../lib/mcp-config');
|
||||||
|
|
||||||
let TOML;
|
let TOML;
|
||||||
try {
|
try {
|
||||||
@@ -210,6 +211,7 @@ function main() {
|
|||||||
const configPath = args.find(a => !a.startsWith('-'));
|
const configPath = args.find(a => !a.startsWith('-'));
|
||||||
const dryRun = args.includes('--dry-run');
|
const dryRun = args.includes('--dry-run');
|
||||||
const updateMcp = args.includes('--update-mcp');
|
const updateMcp = args.includes('--update-mcp');
|
||||||
|
const disabledServers = new Set(parseDisabledMcpServers(process.env.ECC_DISABLED_MCPS));
|
||||||
|
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
console.error('Usage: merge-mcp-config.js <config.toml> [--dry-run] [--update-mcp]');
|
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})`);
|
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 raw = fs.readFileSync(configPath, 'utf8');
|
||||||
let parsed;
|
let parsed;
|
||||||
@@ -249,6 +254,18 @@ function main() {
|
|||||||
const finalEntry = resolvedEntry || urlEntry;
|
const finalEntry = resolvedEntry || urlEntry;
|
||||||
const resolvedLabel = hasCanonical ? name : legacyName || name;
|
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 (finalEntry) {
|
||||||
if (updateMcp) {
|
if (updateMcp) {
|
||||||
// --update-mcp: remove existing section (and legacy alias), will re-add below
|
// --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.');
|
log('All ECC MCP servers already present. Nothing to do.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -297,14 +316,19 @@ function main() {
|
|||||||
|
|
||||||
// Write: for add-only, append to preserve existing content byte-for-byte.
|
// 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.
|
// 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}`);
|
for (const label of toRemoveLog) log(` [update] ${label}`);
|
||||||
const cleaned = raw.replace(/\n+$/, '\n');
|
const cleaned = raw.replace(/\n+$/, '\n');
|
||||||
fs.writeFileSync(configPath, cleaned + appendText, 'utf8');
|
fs.writeFileSync(configPath, cleaned + (toAppend.length > 0 ? appendText : ''), 'utf8');
|
||||||
} else {
|
} else {
|
||||||
fs.appendFileSync(configPath, appendText, 'utf8');
|
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'}.`);
|
log(`Done. ${toAppend.length} server(s) ${updateMcp ? 'updated' : 'added'}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const { writeInstallState } = require('../install-state');
|
const { writeInstallState } = require('../install-state');
|
||||||
|
const { filterMcpConfig, parseDisabledMcpServers } = require('../mcp-config');
|
||||||
|
|
||||||
function readJsonObject(filePath, label) {
|
function readJsonObject(filePath, label) {
|
||||||
let parsed;
|
let parsed;
|
||||||
@@ -124,6 +125,49 @@ function findHooksSourcePath(plan, hooksDestinationPath) {
|
|||||||
return operation ? operation.sourcePath : null;
|
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) {
|
function buildMergedSettings(plan) {
|
||||||
if (!plan.adapter || plan.adapter.target !== 'claude') {
|
if (!plan.adapter || plan.adapter.target !== 'claude') {
|
||||||
return null;
|
return null;
|
||||||
@@ -177,6 +221,7 @@ function buildMergedSettings(plan) {
|
|||||||
|
|
||||||
function applyInstallPlan(plan) {
|
function applyInstallPlan(plan) {
|
||||||
const mergedSettingsPlan = buildMergedSettings(plan);
|
const mergedSettingsPlan = buildMergedSettings(plan);
|
||||||
|
const filteredMcpWrites = buildFilteredMcpWrites(plan);
|
||||||
|
|
||||||
for (const operation of plan.operations) {
|
for (const operation of plan.operations) {
|
||||||
fs.mkdirSync(path.dirname(operation.destinationPath), { recursive: true });
|
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);
|
writeInstallState(plan.installStatePath, plan.statePreview);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
56
scripts/lib/mcp-config.js
Normal file
56
scripts/lib/mcp-config.js
Normal 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,
|
||||||
|
};
|
||||||
62
tests/lib/mcp-config.test.js
Normal file
62
tests/lib/mcp-config.test.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const { filterMcpConfig, parseDisabledMcpServers } = require('../../scripts/lib/mcp-config');
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` \u2713 ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` \u2717 ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing mcp-config.js ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('parseDisabledMcpServers dedupes and trims values', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
parseDisabledMcpServers(' github,exa ,github,,playwright '),
|
||||||
|
['github', 'exa', 'playwright']
|
||||||
|
);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('filterMcpConfig removes disabled servers and preserves others', () => {
|
||||||
|
const result = filterMcpConfig({
|
||||||
|
mcpServers: {
|
||||||
|
github: { command: 'npx' },
|
||||||
|
exa: { url: 'https://mcp.exa.ai/mcp' },
|
||||||
|
memory: { command: 'npx' },
|
||||||
|
},
|
||||||
|
_comments: { usage: 'demo' },
|
||||||
|
}, ['github', 'memory']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.removed, ['github', 'memory']);
|
||||||
|
assert.deepStrictEqual(Object.keys(result.config.mcpServers), ['exa']);
|
||||||
|
assert.deepStrictEqual(result.config._comments, { usage: 'demo' });
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('filterMcpConfig leaves config unchanged when no disabled servers are provided', () => {
|
||||||
|
const result = filterMcpConfig({
|
||||||
|
mcpServers: {
|
||||||
|
github: { command: 'npx' },
|
||||||
|
},
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.removed, []);
|
||||||
|
assert.deepStrictEqual(Object.keys(result.config.mcpServers), ['github']);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
@@ -7,6 +7,7 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { execFileSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
|
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
|
||||||
|
|
||||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
|
||||||
|
|
||||||
@@ -440,6 +441,80 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('filters copied mcp config files when ECC_DISABLED_MCPS is set', () => {
|
||||||
|
const tempDir = createTempDir('install-apply-mcp-');
|
||||||
|
const sourcePath = path.join(tempDir, '.mcp.json');
|
||||||
|
const destinationPath = path.join(tempDir, 'installed', '.mcp.json');
|
||||||
|
const installStatePath = path.join(tempDir, 'installed', 'ecc-install-state.json');
|
||||||
|
const previousValue = process.env.ECC_DISABLED_MCPS;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
||||||
|
fs.writeFileSync(sourcePath, JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
github: { command: 'npx' },
|
||||||
|
exa: { url: 'https://mcp.exa.ai/mcp' },
|
||||||
|
memory: { command: 'npx' },
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
process.env.ECC_DISABLED_MCPS = 'github,memory';
|
||||||
|
|
||||||
|
applyInstallPlan({
|
||||||
|
targetRoot: path.join(tempDir, 'installed'),
|
||||||
|
installStatePath,
|
||||||
|
statePreview: {
|
||||||
|
schemaVersion: 'ecc.install.v1',
|
||||||
|
installedAt: new Date().toISOString(),
|
||||||
|
target: {
|
||||||
|
id: 'test-install',
|
||||||
|
kind: 'project',
|
||||||
|
root: path.join(tempDir, 'installed'),
|
||||||
|
installStatePath,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
profile: null,
|
||||||
|
modules: ['test-mcp'],
|
||||||
|
includeComponents: [],
|
||||||
|
excludeComponents: [],
|
||||||
|
legacyLanguages: [],
|
||||||
|
legacyMode: false,
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
selectedModules: ['test-mcp'],
|
||||||
|
skippedModules: [],
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
repoVersion: null,
|
||||||
|
repoCommit: null,
|
||||||
|
manifestVersion: 1,
|
||||||
|
},
|
||||||
|
operations: [],
|
||||||
|
},
|
||||||
|
operations: [{
|
||||||
|
kind: 'copy-file',
|
||||||
|
moduleId: 'test-mcp',
|
||||||
|
sourcePath,
|
||||||
|
sourceRelativePath: '.mcp.json',
|
||||||
|
destinationPath,
|
||||||
|
strategy: 'preserve-relative-path',
|
||||||
|
ownership: 'managed',
|
||||||
|
scaffoldOnly: false,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
const installed = readJson(destinationPath);
|
||||||
|
assert.deepStrictEqual(Object.keys(installed.mcpServers), ['exa']);
|
||||||
|
} finally {
|
||||||
|
if (previousValue === undefined) {
|
||||||
|
delete process.env.ECC_DISABLED_MCPS;
|
||||||
|
} else {
|
||||||
|
process.env.ECC_DISABLED_MCPS = previousValue;
|
||||||
|
}
|
||||||
|
cleanup(tempDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('reinstall does not duplicate managed hook entries', () => {
|
if (test('reinstall does not duplicate managed hook entries', () => {
|
||||||
const homeDir = createTempDir('install-apply-home-');
|
const homeDir = createTempDir('install-apply-home-');
|
||||||
const projectDir = createTempDir('install-apply-project-');
|
const projectDir = createTempDir('install-apply-project-');
|
||||||
|
|||||||
Reference in New Issue
Block a user