Merge pull request #97 from RooVetGit/mcp_global_checkbox

More safety around always allowing MCP
This commit is contained in:
Matt Rubens
2024-12-13 17:50:25 -05:00
committed by GitHub
9 changed files with 51 additions and 8 deletions

View File

@@ -633,6 +633,8 @@ npm run build
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object. 5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
IMPORTANT: Regardless of what else you see in the settings file, you must not set any defaults for the \`alwaysAllow\` array in the newly added MCP server.
\`\`\`json \`\`\`json
{ {
"mcpServers": { "mcpServers": {

View File

@@ -67,6 +67,7 @@ type GlobalStateKey =
| "allowedCommands" | "allowedCommands"
| "soundEnabled" | "soundEnabled"
| "diffEnabled" | "diffEnabled"
| "alwaysAllowMcp"
export const GlobalFileNames = { export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json", apiConversationHistory: "api_conversation_history.json",
@@ -456,6 +457,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined)
await this.postStateToWebview() await this.postStateToWebview()
break break
case "alwaysAllowMcp":
await this.updateGlobalState("alwaysAllowMcp", message.bool)
await this.postStateToWebview()
break
case "askResponse": case "askResponse":
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break break
@@ -904,6 +909,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite, alwaysAllowWrite,
alwaysAllowExecute, alwaysAllowExecute,
alwaysAllowBrowser, alwaysAllowBrowser,
alwaysAllowMcp,
soundEnabled, soundEnabled,
diffEnabled, diffEnabled,
taskHistory, taskHistory,
@@ -921,6 +927,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false,
alwaysAllowBrowser: alwaysAllowBrowser ?? false, alwaysAllowBrowser: alwaysAllowBrowser ?? false,
alwaysAllowMcp: alwaysAllowMcp ?? false,
uriScheme: vscode.env.uriScheme, uriScheme: vscode.env.uriScheme,
clineMessages: this.cline?.clineMessages || [], clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || []) taskHistory: (taskHistory || [])
@@ -1017,6 +1024,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite, alwaysAllowWrite,
alwaysAllowExecute, alwaysAllowExecute,
alwaysAllowBrowser, alwaysAllowBrowser,
alwaysAllowMcp,
taskHistory, taskHistory,
allowedCommands, allowedCommands,
soundEnabled, soundEnabled,
@@ -1053,6 +1061,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>, this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>, this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>, this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowMcp") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>, this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>, this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>, this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
@@ -1107,6 +1116,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false,
alwaysAllowBrowser: alwaysAllowBrowser ?? false, alwaysAllowBrowser: alwaysAllowBrowser ?? false,
alwaysAllowMcp: alwaysAllowMcp ?? false,
taskHistory, taskHistory,
allowedCommands, allowedCommands,
soundEnabled, soundEnabled,

View File

@@ -47,6 +47,7 @@ export interface ExtensionState {
alwaysAllowWrite?: boolean alwaysAllowWrite?: boolean
alwaysAllowExecute?: boolean alwaysAllowExecute?: boolean
alwaysAllowBrowser?: boolean alwaysAllowBrowser?: boolean
alwaysAllowMcp?: boolean
uriScheme?: string uriScheme?: string
allowedCommands?: string[] allowedCommands?: string[]
soundEnabled?: boolean soundEnabled?: boolean

View File

@@ -29,6 +29,7 @@ export interface WebviewMessage {
| "cancelTask" | "cancelTask"
| "refreshOpenRouterModels" | "refreshOpenRouterModels"
| "alwaysAllowBrowser" | "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "playSound" | "playSound"
| "soundEnabled" | "soundEnabled"
| "diffEnabled" | "diffEnabled"

View File

@@ -37,7 +37,7 @@ interface ChatViewProps {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, allowedCommands } = useExtensionState() const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands } = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined //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) 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)
@@ -803,11 +803,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
(alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) || (alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) ||
(alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) || (alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) ||
(alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) || (alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) ||
(clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed()) (alwaysAllowMcp && clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed())
) { ) {
handlePrimaryButtonClick() handlePrimaryButtonClick()
} }
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands, mcpServers]) }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])
return ( return (
<div <div

View File

@@ -5,9 +5,10 @@ import { vscode } from "../../utils/vscode"
type McpToolRowProps = { type McpToolRowProps = {
tool: McpTool tool: McpTool
serverName?: string serverName?: string
alwaysAllowMcp?: boolean
} }
const McpToolRow = ({ tool, serverName }: McpToolRowProps) => { const McpToolRow = ({ tool, serverName, alwaysAllowMcp }: McpToolRowProps) => {
const handleAlwaysAllowChange = () => { const handleAlwaysAllowChange = () => {
if (!serverName) return; if (!serverName) return;
@@ -33,7 +34,7 @@ const McpToolRow = ({ tool, serverName }: McpToolRowProps) => {
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span> <span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span> <span style={{ fontWeight: 500 }}>{tool.name}</span>
</div> </div>
{serverName && ( {serverName && alwaysAllowMcp && (
<VSCodeCheckbox <VSCodeCheckbox
checked={tool.alwaysAllow} checked={tool.alwaysAllow}
onChange={handleAlwaysAllowChange} onChange={handleAlwaysAllowChange}

View File

@@ -17,7 +17,7 @@ type McpViewProps = {
} }
const McpView = ({ onDone }: McpViewProps) => { const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers } = useExtensionState() const { mcpServers: servers, alwaysAllowMcp } = useExtensionState()
// const [servers, setServers] = useState<McpServer[]>([ // const [servers, setServers] = useState<McpServer[]>([
// // Add some mock servers for testing // // Add some mock servers for testing
// { // {
@@ -126,7 +126,7 @@ const McpView = ({ onDone }: McpViewProps) => {
{servers.length > 0 && ( {servers.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{servers.map((server) => ( {servers.map((server) => (
<ServerRow key={server.name} server={server} /> <ServerRow key={server.name} server={server} alwaysAllowMcp={alwaysAllowMcp} />
))} ))}
</div> </div>
)} )}
@@ -152,7 +152,7 @@ const McpView = ({ onDone }: McpViewProps) => {
} }
// Server Row Component // Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => { const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer, alwaysAllowMcp?: boolean }) => {
const [isExpanded, setIsExpanded] = useState(false) const [isExpanded, setIsExpanded] = useState(false)
const getStatusColor = () => { const getStatusColor = () => {
@@ -260,6 +260,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
key={tool.name} key={tool.name}
tool={tool} tool={tool}
serverName={server.name} serverName={server.name}
alwaysAllowMcp={alwaysAllowMcp}
/> />
))} ))}
</div> </div>

View File

@@ -25,6 +25,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setAlwaysAllowExecute, setAlwaysAllowExecute,
alwaysAllowBrowser, alwaysAllowBrowser,
setAlwaysAllowBrowser, setAlwaysAllowBrowser,
alwaysAllowMcp,
setAlwaysAllowMcp,
soundEnabled, soundEnabled,
setSoundEnabled, setSoundEnabled,
diffEnabled, diffEnabled,
@@ -50,6 +52,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser }) vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
@@ -195,7 +198,29 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
color: "var(--vscode-errorForeground)", color: "var(--vscode-errorForeground)",
}}> }}>
WARNING: When enabled, Cline will automatically perform browser actions without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.<br/><br/>NOTE: The checkbox only applies when the model supports computer use. WARNING: When enabled, Cline will automatically perform browser actions without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.<br/><br/>NOTE: The checkbox only applies when the model supports computer use.
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysAllowMcp}
onChange={(e: any) => {
setAlwaysAllowMcp(e.target.checked)
vscode.postMessage({ type: "alwaysAllowMcp", bool: e.target.checked })
}}>
<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
padding: "8px",
backgroundColor: "var(--vscode-errorBackground)",
border: "1px solid var(--vscode-errorBorder)",
borderRadius: "4px",
color: "var(--vscode-errorForeground)",
}}>
WARNING: When enabled, you can set individual MCP tools to auto-approve in the MCP Servers view. A tool will only be auto-approved if both this setting and the tool's individual "Always allow" checkbox are enabled. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.
</p> </p>
</div> </div>

View File

@@ -25,6 +25,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysAllowWrite: (value: boolean) => void setAlwaysAllowWrite: (value: boolean) => void
setAlwaysAllowExecute: (value: boolean) => void setAlwaysAllowExecute: (value: boolean) => void
setAlwaysAllowBrowser: (value: boolean) => void setAlwaysAllowBrowser: (value: boolean) => void
setAlwaysAllowMcp: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void setShowAnnouncement: (value: boolean) => void
setAllowedCommands: (value: string[]) => void setAllowedCommands: (value: string[]) => void
setSoundEnabled: (value: boolean) => void setSoundEnabled: (value: boolean) => void
@@ -134,6 +135,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })), setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })), setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })), setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),