Skip to content

Commit fab0f46

Browse files
fix: make disabled_mcps work for custom and plugin MCPs
Addresses cubic-dev-ai feedback on PR #513 where disabled_mcps configuration was only filtering built-in MCPs but not custom MCPs from .mcp.json or plugin MCPs. Changes: - Custom .mcp.json MCPs: loadMcpConfigs now accepts disabledMcps parameter and filters accordingly - Plugin MCPs: loadPluginMcpServers now filters both namespaced (plugin:server) and non-namespaced (server) names - Added comprehensive tests for custom MCP filtering (4 new test cases) - All 625 tests pass, typecheck clean, build successful Tested: - disabled_mcps: ["playwright", "custom-server"] filters correctly across all scopes - Backward compatibility: disabled: true in .mcp.json still works - Both custom and plugin MCPs respect the disabled_mcps configuration Note: Skill-embedded MCP support can be added in a follow-up PR if needed. This PR addresses the core issue identified in the feedback.
1 parent 7a10b24 commit fab0f46

File tree

4 files changed

+184
-9
lines changed

4 files changed

+184
-9
lines changed

src/features/claude-code-mcp-loader/loader.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,158 @@ describe("getSystemMcpServerNames", () => {
160160
}
161161
})
162162
})
163+
164+
describe("loadMcpConfigs with disabled_mcps", () => {
165+
beforeEach(() => {
166+
mkdirSync(TEST_DIR, { recursive: true })
167+
})
168+
169+
afterEach(() => {
170+
rmSync(TEST_DIR, { recursive: true, force: true })
171+
})
172+
173+
it("should filter out custom MCPs in disabled_mcps list", async () => {
174+
// #given
175+
const mcpConfig = {
176+
mcpServers: {
177+
playwright: {
178+
command: "npx",
179+
args: ["@playwright/mcp@latest"],
180+
},
181+
sqlite: {
182+
command: "uvx",
183+
args: ["mcp-server-sqlite"],
184+
},
185+
"custom-server": {
186+
command: "node",
187+
args: ["custom-mcp"],
188+
},
189+
},
190+
}
191+
writeFileSync(join(TEST_DIR, ".mcp.json"), JSON.stringify(mcpConfig))
192+
193+
const originalCwd = process.cwd()
194+
process.chdir(TEST_DIR)
195+
196+
try {
197+
// #when
198+
const { loadMcpConfigs } = await import("./loader")
199+
const result = await loadMcpConfigs(["playwright", "custom-server"])
200+
201+
// #then
202+
expect(result.servers).toBeDefined()
203+
expect(result.servers["sqlite"]).toBeDefined()
204+
expect(result.servers["playwright"]).toBeUndefined()
205+
expect(result.servers["custom-server"]).toBeUndefined()
206+
expect(Object.keys(result.servers).length).toBe(1)
207+
} finally {
208+
process.chdir(originalCwd)
209+
}
210+
})
211+
212+
it("should respect both disabled_mcps and disabled:true", async () => {
213+
// #given
214+
const mcpConfig = {
215+
mcpServers: {
216+
playwright: {
217+
command: "npx",
218+
args: ["@playwright/mcp@latest"],
219+
},
220+
sqlite: {
221+
command: "uvx",
222+
args: ["mcp-server-sqlite"],
223+
disabled: true,
224+
},
225+
},
226+
}
227+
writeFileSync(join(TEST_DIR, ".mcp.json"), JSON.stringify(mcpConfig))
228+
229+
const originalCwd = process.cwd()
230+
process.chdir(TEST_DIR)
231+
232+
try {
233+
// #when
234+
const { loadMcpConfigs } = await import("./loader")
235+
const result = await loadMcpConfigs(["playwright"])
236+
237+
// #then
238+
expect(result.servers).toBeDefined()
239+
expect(result.servers["playwright"]).toBeUndefined()
240+
expect(result.servers["sqlite"]).toBeUndefined()
241+
expect(Object.keys(result.servers).length).toBe(0)
242+
} finally {
243+
process.chdir(originalCwd)
244+
}
245+
})
246+
247+
it("should filter across all scopes (user, project, local)", async () => {
248+
// #given
249+
const claudeDir = join(TEST_DIR, ".claude")
250+
mkdirSync(claudeDir, { recursive: true })
251+
252+
const projectMcp = {
253+
mcpServers: {
254+
playwright: { command: "npx", args: ["@playwright/mcp@latest"] },
255+
},
256+
}
257+
const localMcp = {
258+
mcpServers: {
259+
sqlite: { command: "uvx", args: ["mcp-server-sqlite"] },
260+
},
261+
}
262+
263+
writeFileSync(join(TEST_DIR, ".mcp.json"), JSON.stringify(projectMcp))
264+
writeFileSync(join(claudeDir, ".mcp.json"), JSON.stringify(localMcp))
265+
266+
const originalCwd = process.cwd()
267+
process.chdir(TEST_DIR)
268+
269+
try {
270+
// #when
271+
const { loadMcpConfigs } = await import("./loader")
272+
const result = await loadMcpConfigs(["playwright"])
273+
274+
// #then
275+
expect(result.servers).toBeDefined()
276+
expect(result.servers["playwright"]).toBeUndefined()
277+
expect(result.servers["sqlite"]).toBeDefined()
278+
expect(Object.keys(result.servers).length).toBe(1)
279+
} finally {
280+
process.chdir(originalCwd)
281+
}
282+
})
283+
284+
it("should not filter when disabled_mcps is empty", async () => {
285+
// #given
286+
const mcpConfig = {
287+
mcpServers: {
288+
playwright: {
289+
command: "npx",
290+
args: ["@playwright/mcp@latest"],
291+
},
292+
sqlite: {
293+
command: "uvx",
294+
args: ["mcp-server-sqlite"],
295+
},
296+
},
297+
}
298+
writeFileSync(join(TEST_DIR, ".mcp.json"), JSON.stringify(mcpConfig))
299+
300+
const originalCwd = process.cwd()
301+
process.chdir(TEST_DIR)
302+
303+
try {
304+
// #when
305+
const { loadMcpConfigs } = await import("./loader")
306+
const result = await loadMcpConfigs([])
307+
308+
// #then
309+
expect(result.servers).toBeDefined()
310+
expect(result.servers["playwright"]).toBeDefined()
311+
expect(result.servers["sqlite"]).toBeDefined()
312+
expect(Object.keys(result.servers).length).toBe(2)
313+
} finally {
314+
process.chdir(originalCwd)
315+
}
316+
})
317+
})

src/features/claude-code-mcp-loader/loader.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export function getSystemMcpServerNames(): Set<string> {
6666
return names
6767
}
6868

69-
export async function loadMcpConfigs(): Promise<McpLoadResult> {
69+
export async function loadMcpConfigs(
70+
disabledMcps: string[] = []
71+
): Promise<McpLoadResult> {
7072
const servers: McpLoadResult["servers"] = {}
7173
const loadedServers: LoadedMcpServer[] = []
7274
const paths = getMcpConfigPaths()
@@ -76,6 +78,11 @@ export async function loadMcpConfigs(): Promise<McpLoadResult> {
7678
if (!config?.mcpServers) continue
7779

7880
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
81+
if (disabledMcps.includes(name)) {
82+
log(`Skipping MCP "${name}" (in disabled_mcps)`, { path })
83+
continue
84+
}
85+
7986
if (serverConfig.disabled) {
8087
log(`Skipping disabled MCP server "${name}"`, { path })
8188
continue

src/features/claude-code-plugin-loader/loader.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,8 @@ export function loadPluginAgents(
388388
}
389389

390390
export async function loadPluginMcpServers(
391-
plugins: LoadedPlugin[]
391+
plugins: LoadedPlugin[],
392+
disabledMcps: string[] = []
392393
): Promise<Record<string, McpServerConfig>> {
393394
const servers: Record<string, McpServerConfig> = {}
394395

@@ -405,14 +406,20 @@ export async function loadPluginMcpServers(
405406
if (!config.mcpServers) continue
406407

407408
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
409+
const namespacedName = `${plugin.name}:${name}`
410+
411+
if (disabledMcps.includes(namespacedName) || disabledMcps.includes(name)) {
412+
log(`Skipping plugin MCP "${namespacedName}" (in disabled_mcps)`, { path: plugin.mcpPath })
413+
continue
414+
}
415+
408416
if (serverConfig.disabled) {
409417
log(`Skipping disabled MCP server "${name}" from plugin ${plugin.name}`)
410418
continue
411419
}
412420

413421
try {
414422
const transformed = transformMcpServer(name, serverConfig)
415-
const namespacedName = `${plugin.name}:${name}`
416423
servers[namespacedName] = transformed
417424
log(`Loaded plugin MCP server: ${namespacedName}`, { path: plugin.mcpPath })
418425
} catch (error) {
@@ -461,14 +468,17 @@ export interface PluginComponentsResult {
461468
errors: PluginLoadError[]
462469
}
463470

464-
export async function loadAllPluginComponents(options?: PluginLoaderOptions): Promise<PluginComponentsResult> {
471+
export async function loadAllPluginComponents(
472+
options?: PluginLoaderOptions,
473+
disabledMcps: string[] = []
474+
): Promise<PluginComponentsResult> {
465475
const { plugins, errors } = discoverInstalledPlugins(options)
466476

467477
const [commands, skills, agents, mcpServers, hooksConfigs] = await Promise.all([
468478
Promise.resolve(loadPluginCommands(plugins)),
469479
Promise.resolve(loadPluginSkillsAsCommands(plugins)),
470480
Promise.resolve(loadPluginAgents(plugins)),
471-
loadPluginMcpServers(plugins),
481+
loadPluginMcpServers(plugins, disabledMcps),
472482
Promise.resolve(loadPluginHooksConfigs(plugins)),
473483
])
474484

src/plugin-handlers/config-handler.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
6666
}
6767

6868
const pluginComponents = (pluginConfig.claude_code?.plugins ?? true)
69-
? await loadAllPluginComponents({
70-
enabledPluginsOverride: pluginConfig.claude_code?.plugins_override,
71-
})
69+
? await loadAllPluginComponents(
70+
{
71+
enabledPluginsOverride: pluginConfig.claude_code?.plugins_override,
72+
},
73+
pluginConfig.disabled_mcps
74+
)
7275
: {
7376
commands: {},
7477
skills: {},
@@ -271,7 +274,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
271274
};
272275

273276
const mcpResult = (pluginConfig.claude_code?.mcp ?? true)
274-
? await loadMcpConfigs()
277+
? await loadMcpConfigs(pluginConfig.disabled_mcps)
275278
: { servers: {} };
276279

277280
config.mcp = {

0 commit comments

Comments
 (0)