Jelajahi Sumber

feat: add wildcard support for MCP alwaysAllow configuration (#10948)

Co-authored-by: Roo Code <[email protected]>
roomote[bot] 2 minggu lalu
induk
melakukan
c28478eda7
2 mengubah file dengan 144 tambahan dan 1 penghapusan
  1. 4 1
      src/services/mcp/McpHub.ts
  2. 140 0
      src/services/mcp/__tests__/McpHub.spec.ts

+ 4 - 1
src/services/mcp/McpHub.ts

@@ -1009,10 +1009,13 @@ export class McpHub {
 				// Continue with empty configs
 			}
 
+			// Check if wildcard "*" is in the alwaysAllow config
+			const hasWildcard = alwaysAllowConfig.includes("*")
+
 			// Mark tools as always allowed and enabled for prompt based on settings
 			const tools = (response?.tools || []).map((tool) => ({
 				...tool,
-				alwaysAllow: alwaysAllowConfig.includes(tool.name),
+				alwaysAllow: hasWildcard || alwaysAllowConfig.includes(tool.name),
 				enabledForPrompt: !disabledToolsList.includes(tool.name),
 			}))
 

+ 140 - 0
src/services/mcp/__tests__/McpHub.spec.ts

@@ -911,6 +911,146 @@ describe("McpHub", () => {
 			expect(writtenConfig.mcpServers["test-server"].alwaysAllow).toBeDefined()
 			expect(writtenConfig.mcpServers["test-server"].alwaysAllow).toContain("new-tool")
 		})
+
+		it("should mark all tools as always allowed when wildcard is present", async () => {
+			const mockConfig = {
+				mcpServers: {
+					"test-server": {
+						type: "stdio",
+						command: "node",
+						args: ["test.js"],
+						alwaysAllow: ["*"],
+					},
+				},
+			}
+
+			// Mock reading config - needs to return for every read
+			vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig))
+
+			// Set up mock connection with tools
+			const mockConnection: ConnectedMcpConnection = {
+				type: "connected",
+				server: {
+					name: "test-server",
+					type: "stdio",
+					command: "node",
+					args: ["test.js"],
+					source: "global",
+				} as any,
+				client: {
+					request: vi.fn().mockResolvedValue({
+						tools: [
+							{ name: "tool1", description: "Tool 1" },
+							{ name: "tool2", description: "Tool 2" },
+							{ name: "tool3", description: "Tool 3" },
+						],
+					}),
+				} as any,
+				transport: {} as any,
+			}
+			mcpHub.connections = [mockConnection]
+
+			// Fetch tools list to test wildcard matching
+			const tools = await mcpHub["fetchToolsList"]("test-server", "global")
+
+			// All tools should be marked as always allowed
+			expect(tools.length).toBe(3)
+			expect(tools[0].alwaysAllow).toBe(true)
+			expect(tools[1].alwaysAllow).toBe(true)
+			expect(tools[2].alwaysAllow).toBe(true)
+		})
+
+		it("should support both wildcard and specific tool names in alwaysAllow", async () => {
+			const mockConfig = {
+				mcpServers: {
+					"test-server": {
+						type: "stdio",
+						command: "node",
+						args: ["test.js"],
+						alwaysAllow: ["*", "specific-tool"],
+					},
+				},
+			}
+
+			// Mock reading config - needs to return for every read
+			vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig))
+
+			// Set up mock connection with tools
+			const mockConnection: ConnectedMcpConnection = {
+				type: "connected",
+				server: {
+					name: "test-server",
+					type: "stdio",
+					command: "node",
+					args: ["test.js"],
+					source: "global",
+				} as any,
+				client: {
+					request: vi.fn().mockResolvedValue({
+						tools: [
+							{ name: "tool1", description: "Tool 1" },
+							{ name: "specific-tool", description: "Specific Tool" },
+						],
+					}),
+				} as any,
+				transport: {} as any,
+			}
+			mcpHub.connections = [mockConnection]
+
+			// Fetch tools list
+			const tools = await mcpHub["fetchToolsList"]("test-server", "global")
+
+			// All tools should be marked as always allowed due to wildcard
+			expect(tools.length).toBe(2)
+			expect(tools[0].alwaysAllow).toBe(true)
+			expect(tools[1].alwaysAllow).toBe(true)
+		})
+
+		it("should only allow specific tools when no wildcard is present", async () => {
+			const mockConfig = {
+				mcpServers: {
+					"test-server": {
+						type: "stdio",
+						command: "node",
+						args: ["test.js"],
+						alwaysAllow: ["allowed-tool"],
+					},
+				},
+			}
+
+			// Mock reading config - needs to return for every read
+			vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig))
+
+			// Set up mock connection with tools
+			const mockConnection: ConnectedMcpConnection = {
+				type: "connected",
+				server: {
+					name: "test-server",
+					type: "stdio",
+					command: "node",
+					args: ["test.js"],
+					source: "global",
+				} as any,
+				client: {
+					request: vi.fn().mockResolvedValue({
+						tools: [
+							{ name: "allowed-tool", description: "Allowed Tool" },
+							{ name: "not-allowed-tool", description: "Not Allowed Tool" },
+						],
+					}),
+				} as any,
+				transport: {} as any,
+			}
+			mcpHub.connections = [mockConnection]
+
+			// Fetch tools list
+			const tools = await mcpHub["fetchToolsList"]("test-server", "global")
+
+			// Only the specifically allowed tool should be marked as always allowed
+			expect(tools.length).toBe(2)
+			expect(tools[0].alwaysAllow).toBe(true) // allowed-tool
+			expect(tools[1].alwaysAllow).toBe(false) // not-allowed-tool
+		})
 	})
 
 	describe("toggleToolEnabledForPrompt", () => {