Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 27b4b30

Browse files
committed
feat: add per-mode MCP server allowlist (allowedMcpServers)
Add an optional allowedMcpServers field to ModeConfig that acts as a whitelist for MCP servers on a per-mode basis. When defined, only the listed MCP servers tools are injected for that mode. When omitted or empty, all enabled MCP servers are included (preserving current behavior). This addresses context bloat and tool limit issues when running multiple MCP servers with models that have strict tool limits (e.g. 128-tool limit). Changes: - packages/types/src/mode.ts: Add allowedMcpServers to modeConfigSchema - src/core/prompts/tools/native-tools/mcp_server.ts: Accept allowedMcpServers filter - src/core/task/build-tools.ts: Pass allowedMcpServers from mode config - src/core/prompts/system.ts: Filter MCP capabilities in system prompt Closes #12004
1 parent 137d3f4 commit 27b4b30

6 files changed

Lines changed: 149 additions & 5 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { modeConfigSchema } from "../mode.js"
2+
3+
describe("modeConfigSchema", () => {
4+
const validBase = {
5+
slug: "test-mode",
6+
name: "Test Mode",
7+
roleDefinition: "A test mode",
8+
groups: ["read", "edit"],
9+
}
10+
11+
it("should accept a mode config without allowedMcpServers", () => {
12+
const result = modeConfigSchema.safeParse(validBase)
13+
expect(result.success).toBe(true)
14+
})
15+
16+
it("should accept a mode config with allowedMcpServers as an array of strings", () => {
17+
const result = modeConfigSchema.safeParse({
18+
...validBase,
19+
groups: ["read", "edit", "mcp"],
20+
allowedMcpServers: ["postgres-mcp", "redis-mcp"],
21+
})
22+
expect(result.success).toBe(true)
23+
if (result.success) {
24+
expect(result.data.allowedMcpServers).toEqual(["postgres-mcp", "redis-mcp"])
25+
}
26+
})
27+
28+
it("should accept a mode config with empty allowedMcpServers array", () => {
29+
const result = modeConfigSchema.safeParse({
30+
...validBase,
31+
allowedMcpServers: [],
32+
})
33+
expect(result.success).toBe(true)
34+
if (result.success) {
35+
expect(result.data.allowedMcpServers).toEqual([])
36+
}
37+
})
38+
39+
it("should reject allowedMcpServers with non-string values", () => {
40+
const result = modeConfigSchema.safeParse({
41+
...validBase,
42+
allowedMcpServers: [123, true],
43+
})
44+
expect(result.success).toBe(false)
45+
})
46+
47+
it("should accept allowedMcpServers as undefined (backward compatible)", () => {
48+
const result = modeConfigSchema.safeParse({
49+
...validBase,
50+
allowedMcpServers: undefined,
51+
})
52+
expect(result.success).toBe(true)
53+
if (result.success) {
54+
expect(result.data.allowedMcpServers).toBeUndefined()
55+
}
56+
})
57+
})

packages/types/src/mode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ export const modeConfigSchema = z.object({
102102
customInstructions: z.string().optional(),
103103
groups: groupEntryArraySchema,
104104
source: z.enum(["global", "project"]).optional(),
105+
/**
106+
* Optional allowlist of MCP server names for this mode.
107+
* When defined, only the listed MCP servers' tools are injected for the mode.
108+
* When omitted or empty, all enabled MCP servers are included (default behavior).
109+
*/
110+
allowedMcpServers: z.array(z.string()).optional(),
105111
})
106112

107113
export type ModeConfig = z.infer<typeof modeConfigSchema>

src/core/prompts/system.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,14 @@ async function generatePrompt(
6666

6767
// Check if MCP functionality should be included
6868
const hasMcpGroup = modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp")
69-
const hasMcpServers = mcpHub && mcpHub.getServers().length > 0
69+
let hasMcpServers = mcpHub && mcpHub.getServers().length > 0
70+
71+
// If this mode has an allowedMcpServers allowlist, check that at least one allowed server exists
72+
if (hasMcpServers && modeConfig.allowedMcpServers && modeConfig.allowedMcpServers.length > 0) {
73+
const allowedSet = new Set(modeConfig.allowedMcpServers)
74+
hasMcpServers = mcpHub!.getServers().some((server) => allowedSet.has(server.name))
75+
}
76+
7077
const shouldIncludeMcp = hasMcpGroup && hasMcpServers
7178

7279
const codeIndexManager = CodeIndexManager.getInstance(context, cwd)

src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,66 @@ describe("getMcpServerTools", () => {
170170
})
171171
})
172172

173+
describe("allowedMcpServers filtering", () => {
174+
it("should return all server tools when allowedMcpServers is undefined", () => {
175+
const server1 = createMockServer("postgres-mcp", [createMockTool("query")])
176+
const server2 = createMockServer("redis-mcp", [createMockTool("get")])
177+
const server3 = createMockServer("filesystem-mcp", [createMockTool("read")])
178+
const mockHub = createMockMcpHub([server1, server2, server3])
179+
180+
const result = getMcpServerTools(mockHub as McpHub, undefined)
181+
182+
expect(result).toHaveLength(3)
183+
})
184+
185+
it("should return all server tools when allowedMcpServers is empty array", () => {
186+
const server1 = createMockServer("postgres-mcp", [createMockTool("query")])
187+
const server2 = createMockServer("redis-mcp", [createMockTool("get")])
188+
const mockHub = createMockMcpHub([server1, server2])
189+
190+
const result = getMcpServerTools(mockHub as McpHub, [])
191+
192+
expect(result).toHaveLength(2)
193+
})
194+
195+
it("should filter to only allowed servers", () => {
196+
const server1 = createMockServer("postgres-mcp", [createMockTool("query")])
197+
const server2 = createMockServer("redis-mcp", [createMockTool("get")])
198+
const server3 = createMockServer("filesystem-mcp", [createMockTool("read")])
199+
const mockHub = createMockMcpHub([server1, server2, server3])
200+
201+
const result = getMcpServerTools(mockHub as McpHub, ["postgres-mcp", "redis-mcp"])
202+
203+
expect(result).toHaveLength(2)
204+
const toolNames = result.map((t) => getFunction(t).name)
205+
expect(toolNames).toContain("mcp--postgres-mcp--query")
206+
expect(toolNames).toContain("mcp--redis-mcp--get")
207+
expect(toolNames).not.toContain("mcp--filesystem-mcp--read")
208+
})
209+
210+
it("should return empty array when no servers match the allowlist", () => {
211+
const server1 = createMockServer("postgres-mcp", [createMockTool("query")])
212+
const mockHub = createMockMcpHub([server1])
213+
214+
const result = getMcpServerTools(mockHub as McpHub, ["nonexistent-server"])
215+
216+
expect(result).toEqual([])
217+
})
218+
219+
it("should handle allowedMcpServers with a single server", () => {
220+
const server1 = createMockServer("postgres-mcp", [createMockTool("query"), createMockTool("execute")])
221+
const server2 = createMockServer("redis-mcp", [createMockTool("get")])
222+
const mockHub = createMockMcpHub([server1, server2])
223+
224+
const result = getMcpServerTools(mockHub as McpHub, ["postgres-mcp"])
225+
226+
expect(result).toHaveLength(2)
227+
const toolNames = result.map((t) => getFunction(t).name)
228+
expect(toolNames).toContain("mcp--postgres-mcp--query")
229+
expect(toolNames).toContain("mcp--postgres-mcp--execute")
230+
})
231+
})
232+
173233
it("should not include required field when schema has no required fields", () => {
174234
const toolWithoutRequired: McpTool = {
175235
name: "toolWithoutRequired",

src/core/prompts/tools/native-tools/mcp_server.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import { normalizeToolSchema, type JsonSchema } from "../../../../utils/json-sch
99
* global and project configs, project servers take priority (handled by McpHub.getServers()).
1010
*
1111
* @param mcpHub The McpHub instance containing connected servers.
12+
* @param allowedMcpServers Optional allowlist of server names. When provided, only servers
13+
* whose name is in the list will have their tools included. When omitted, all servers are included.
1214
* @returns An array of OpenAI.Chat.ChatCompletionTool definitions.
1315
*/
14-
export function getMcpServerTools(mcpHub?: McpHub): OpenAI.Chat.ChatCompletionTool[] {
16+
export function getMcpServerTools(mcpHub?: McpHub, allowedMcpServers?: string[]): OpenAI.Chat.ChatCompletionTool[] {
1517
if (!mcpHub) {
1618
return []
1719
}
1820

19-
const servers = mcpHub.getServers()
21+
let servers = mcpHub.getServers()
22+
23+
// If an allowlist is provided and non-empty, filter to only allowed servers
24+
if (allowedMcpServers && allowedMcpServers.length > 0) {
25+
const allowedSet = new Set(allowedMcpServers)
26+
servers = servers.filter((server) => allowedSet.has(server.name))
27+
}
2028
const tools: OpenAI.Chat.ChatCompletionTool[] = []
2129
// Track seen tool names to prevent duplicates (e.g., when same server exists in both global and project configs)
2230
const seenToolNames = new Set<string>()

src/core/task/build-tools.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { customToolRegistry, formatNative } from "@roo-code/core"
77

88
import type { ClineProvider } from "../webview/ClineProvider"
99
import { getRooDirectoriesForCwd } from "../../services/roo-config/index.js"
10+
import { getModeBySlug } from "../../shared/modes"
11+
import { defaultModeSlug } from "../../shared/modes"
1012

1113
import { getNativeTools, getMcpServerTools } from "../prompts/tools/native-tools"
1214
import {
@@ -124,8 +126,12 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
124126
mcpHub,
125127
)
126128

127-
// Filter MCP tools based on mode restrictions.
128-
const mcpTools = getMcpServerTools(mcpHub)
129+
// Resolve the current mode config to get per-mode settings like allowedMcpServers.
130+
const modeSlug = mode ?? defaultModeSlug
131+
const modeConfig = getModeBySlug(modeSlug, customModes)
132+
133+
// Filter MCP tools based on mode restrictions and per-mode allowedMcpServers.
134+
const mcpTools = getMcpServerTools(mcpHub, modeConfig?.allowedMcpServers)
129135
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments)
130136

131137
// Add custom tools if they are available and the experiment is enabled.

0 commit comments

Comments
 (0)