mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
MCP checkbox for always allow
This commit is contained in:
@@ -813,14 +813,19 @@ export const ChatRowContent = ({
|
||||
|
||||
{useMcpServer.type === "use_mcp_tool" && (
|
||||
<>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.description || "",
|
||||
}}
|
||||
/>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.description || "",
|
||||
alwaysAllow: server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.alwaysAllow || false,
|
||||
}}
|
||||
serverName={useMcpServer.serverName}
|
||||
/>
|
||||
</div>
|
||||
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
<div
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ClineSayTool,
|
||||
ExtensionMessage,
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
import { McpServer, McpTool } from "../../../../src/shared/mcp"
|
||||
import { findLast } from "../../../../src/shared/array"
|
||||
import { combineApiRequests } from "../../../../src/shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "../../../../src/shared/combineCommandSequences"
|
||||
@@ -36,7 +37,7 @@ interface ChatViewProps {
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, allowedCommands } = useExtensionState()
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, allowedCommands } = useExtensionState()
|
||||
|
||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
||||
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
|
||||
@@ -767,6 +768,19 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return false
|
||||
}
|
||||
|
||||
const isMcpToolAlwaysAllowed = () => {
|
||||
const lastMessage = messages.at(-1)
|
||||
if (lastMessage?.type === "ask" && lastMessage.ask === "use_mcp_server" && lastMessage.text) {
|
||||
const mcpServerUse = JSON.parse(lastMessage.text) as { type: string; serverName: string; toolName: string }
|
||||
if (mcpServerUse.type === "use_mcp_tool") {
|
||||
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
||||
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
||||
return tool?.alwaysAllow || false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isAllowedCommand = () => {
|
||||
const lastMessage = messages.at(-1)
|
||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||
@@ -788,11 +802,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
(alwaysAllowBrowser && clineAsk === "browser_action_launch") ||
|
||||
(alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) ||
|
||||
(alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) ||
|
||||
(alwaysAllowExecute && clineAsk === "command" && isAllowedCommand())
|
||||
(alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) ||
|
||||
(clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed())
|
||||
) {
|
||||
handlePrimaryButtonClick()
|
||||
}
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands])
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands, mcpServers])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { McpTool } from "../../../../src/shared/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
type McpToolRowProps = {
|
||||
tool: McpTool
|
||||
serverName?: string
|
||||
}
|
||||
|
||||
const McpToolRow = ({ tool }: McpToolRowProps) => {
|
||||
const McpToolRow = ({ tool, serverName }: McpToolRowProps) => {
|
||||
const handleAlwaysAllowChange = () => {
|
||||
if (!serverName) return;
|
||||
|
||||
vscode.postMessage({
|
||||
type: "toggleToolAlwaysAllow",
|
||||
serverName,
|
||||
toolName: tool.name,
|
||||
alwaysAllow: !tool.alwaysAllow
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: "3px 0",
|
||||
}}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
|
||||
<span style={{ fontWeight: 500 }}>{tool.name}</span>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
|
||||
<span style={{ fontWeight: 500 }}>{tool.name}</span>
|
||||
</div>
|
||||
{serverName && (
|
||||
<VSCodeCheckbox
|
||||
checked={tool.alwaysAllow}
|
||||
onChange={handleAlwaysAllowChange}
|
||||
data-tool={tool.name}>
|
||||
Always allow
|
||||
</VSCodeCheckbox>
|
||||
)}
|
||||
</div>
|
||||
{tool.description && (
|
||||
<div
|
||||
|
||||
@@ -256,7 +256,11 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
|
||||
{server.tools.map((tool) => (
|
||||
<McpToolRow key={tool.name} tool={tool} />
|
||||
<McpToolRow
|
||||
key={tool.name}
|
||||
tool={tool}
|
||||
serverName={server.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
107
webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx
Normal file
107
webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, screen } from '@testing-library/react'
|
||||
import McpToolRow from '../McpToolRow'
|
||||
import { vscode } from '../../../utils/vscode'
|
||||
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('McpToolRow', () => {
|
||||
const mockTool = {
|
||||
name: 'test-tool',
|
||||
description: 'A test tool',
|
||||
alwaysAllow: false
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders tool name and description', () => {
|
||||
render(<McpToolRow tool={mockTool} />)
|
||||
|
||||
expect(screen.getByText('test-tool')).toBeInTheDocument()
|
||||
expect(screen.getByText('A test tool')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show always allow checkbox when serverName is not provided', () => {
|
||||
render(<McpToolRow tool={mockTool} />)
|
||||
|
||||
expect(screen.queryByText('Always allow')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows always allow checkbox when serverName is provided', () => {
|
||||
render(<McpToolRow tool={mockTool} serverName="test-server" />)
|
||||
|
||||
expect(screen.getByText('Always allow')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('sends message to toggle always allow when checkbox is clicked', () => {
|
||||
render(<McpToolRow tool={mockTool} serverName="test-server" />)
|
||||
|
||||
const checkbox = screen.getByRole('checkbox')
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'toggleToolAlwaysAllow',
|
||||
serverName: 'test-server',
|
||||
toolName: 'test-tool',
|
||||
alwaysAllow: true
|
||||
})
|
||||
})
|
||||
|
||||
it('reflects always allow state in checkbox', () => {
|
||||
const alwaysAllowedTool = {
|
||||
...mockTool,
|
||||
alwaysAllow: true
|
||||
}
|
||||
|
||||
render(<McpToolRow tool={alwaysAllowedTool} serverName="test-server" />)
|
||||
|
||||
const checkbox = screen.getByRole('checkbox')
|
||||
expect(checkbox).toBeChecked()
|
||||
})
|
||||
|
||||
it('prevents event propagation when clicking the checkbox', () => {
|
||||
const mockStopPropagation = jest.fn()
|
||||
render(<McpToolRow tool={mockTool} serverName="test-server" />)
|
||||
|
||||
const container = screen.getByTestId('tool-row-container')
|
||||
fireEvent.click(container, {
|
||||
stopPropagation: mockStopPropagation
|
||||
})
|
||||
|
||||
expect(mockStopPropagation).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('displays input schema parameters when provided', () => {
|
||||
const toolWithSchema = {
|
||||
...mockTool,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'string',
|
||||
description: 'First parameter'
|
||||
},
|
||||
param2: {
|
||||
type: 'number',
|
||||
description: 'Second parameter'
|
||||
}
|
||||
},
|
||||
required: ['param1']
|
||||
}
|
||||
}
|
||||
|
||||
render(<McpToolRow tool={toolWithSchema} serverName="test-server" />)
|
||||
|
||||
expect(screen.getByText('Parameters')).toBeInTheDocument()
|
||||
expect(screen.getByText('param1')).toBeInTheDocument()
|
||||
expect(screen.getByText('param2')).toBeInTheDocument()
|
||||
expect(screen.getByText('First parameter')).toBeInTheDocument()
|
||||
expect(screen.getByText('Second parameter')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user