feat: mcp tool permissions (#4348)

* feat(MCPSettings): add tool toggle functionality and update server configuration

* fix(McpSettings): improve server type handling and tool fetching logic
This commit is contained in:
LiuVaayne 2025-04-04 09:57:54 +08:00 committed by GitHub
parent 4d5cfe06f5
commit 10848f7a45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 83 additions and 35 deletions

View File

@ -93,22 +93,17 @@ const McpSettings: React.FC<Props> = ({ server }) => {
.join('\n') .join('\n')
: '' : ''
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps }, [server, form])
}, [server])
// Watch the serverType field to update the form layout dynamically
useEffect(() => { useEffect(() => {
const type = form.getFieldValue('serverType') const currentServerType = form.getFieldValue('serverType')
type && setServerType(type) if (currentServerType) {
}, [form]) setServerType(currentServerType)
}
// Load tools on initial mount if server is active
useEffect(() => {
fetchTools()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [form.getFieldValue('serverType')])
const fetchTools = async () => { const fetchTools = useCallback(async () => {
if (server.isActive) { if (server.isActive) {
try { try {
setLoadingServer(server.id) setLoadingServer(server.id)
@ -124,7 +119,15 @@ const McpSettings: React.FC<Props> = ({ server }) => {
setLoadingServer(null) setLoadingServer(null)
} }
} }
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [server.id])
useEffect(() => {
console.log('Loading tools for server:', server.id, 'Active:', server.isActive)
if (server.isActive) {
fetchTools()
}
}, [server.id, server.isActive, fetchTools])
// Save the form data // Save the form data
const onSave = async () => { const onSave = async () => {
@ -241,10 +244,6 @@ const McpSettings: React.FC<Props> = ({ server }) => {
[server, t] [server, t]
) )
const onFormValuesChange = () => {
setIsFormChanged(true)
}
const formatError = (error: any) => { const formatError = (error: any) => {
if (error.message.includes('32000')) { if (error.message.includes('32000')) {
return t('settings.mcp.errors.32000') return t('settings.mcp.errors.32000')
@ -278,6 +277,35 @@ const McpSettings: React.FC<Props> = ({ server }) => {
} }
} }
// Handle toggling a tool on/off
const handleToggleTool = useCallback(
async (tool: MCPTool, enabled: boolean) => {
// Create a new disabledTools array or use the existing one
let disabledTools = [...(server.disabledTools || [])]
if (enabled) {
// Remove tool from disabledTools if it's being enabled
disabledTools = disabledTools.filter((name) => name !== tool.name)
} else {
// Add tool to disabledTools if it's being disabled
if (!disabledTools.includes(tool.name)) {
disabledTools.push(tool.name)
}
}
// Update the server with new disabledTools
const updatedServer = {
...server,
disabledTools
}
// Save the updated server configuration
// await window.api.mcp.updateServer(updatedServer)
updateMCPServer(updatedServer)
},
[server, updateMCPServer]
)
return ( return (
<SettingContainer> <SettingContainer>
<SettingGroup style={{ marginBottom: 0 }}> <SettingGroup style={{ marginBottom: 0 }}>
@ -302,7 +330,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
onValuesChange={onFormValuesChange} onValuesChange={() => setIsFormChanged(true)}
style={{ style={{
// height: 'calc(100vh - var(--navbar-height) - 315px)', // height: 'calc(100vh - var(--navbar-height) - 315px)',
overflowY: 'auto', overflowY: 'auto',
@ -380,7 +408,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
</> </>
)} )}
</Form> </Form>
{server.isActive && <MCPToolsSection tools={tools} />} {server.isActive && <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />}
</SettingGroup> </SettingGroup>
</SettingContainer> </SettingContainer>
) )

View File

@ -1,11 +1,27 @@
import { MCPTool } from '@renderer/types' import { MCPServer, MCPTool } from '@renderer/types'
import { Badge, Collapse, Descriptions, Empty, Flex, Tag, Tooltip, Typography } from 'antd' import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
const MCPToolsSection = ({ tools }: { tools: MCPTool[] }) => { interface MCPToolsSectionProps {
tools: MCPTool[]
server: MCPServer
onToggleTool: (tool: MCPTool, enabled: boolean) => void
}
const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
// Check if a tool is enabled (not in the disabledTools array)
const isToolEnabled = (tool: MCPTool) => {
return !server.disabledTools?.includes(tool.name)
}
// Handle tool toggle
const handleToggle = (tool: MCPTool, checked: boolean) => {
onToggleTool(tool, checked)
}
// Render tool properties from the input schema // Render tool properties from the input schema
const renderToolProperties = (tool: MCPTool) => { const renderToolProperties = (tool: MCPTool) => {
if (!tool.inputSchema?.properties) return null if (!tool.inputSchema?.properties) return null
@ -86,18 +102,21 @@ const MCPToolsSection = ({ tools }: { tools: MCPTool[] }) => {
<Collapse.Panel <Collapse.Panel
key={tool.id} key={tool.id}
header={ header={
<Flex vertical align="flex-start" style={{ width: '100%' }}> <Flex justify="space-between" align="center" style={{ width: '100%' }}>
<Flex align="center" style={{ width: '100%' }}> <Flex vertical align="flex-start">
<Typography.Text strong>{tool.name}</Typography.Text> <Flex align="center" style={{ width: '100%' }}>
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}> <Typography.Text strong>{tool.name}</Typography.Text>
{tool.id} <Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
</Typography.Text> {tool.id}
</Typography.Text>
</Flex>
{tool.description && (
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
{tool.description}
</Typography.Text>
)}
</Flex> </Flex>
{tool.description && ( <Switch checked={isToolEnabled(tool)} onChange={(checked) => handleToggle(tool, checked)} />
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
{tool.description}
</Typography.Text>
)}
</Flex> </Flex>
}> }>
<SelectableContent>{renderToolProperties(tool)}</SelectableContent> <SelectableContent>{renderToolProperties(tool)}</SelectableContent>

View File

@ -106,8 +106,8 @@ export async function fetchChatCompletion({
if (enabledMCPs && enabledMCPs.length > 0) { if (enabledMCPs && enabledMCPs.length > 0) {
for (const mcpServer of enabledMCPs) { for (const mcpServer of enabledMCPs) {
const tools = await window.api.mcp.listTools(mcpServer) const tools = await window.api.mcp.listTools(mcpServer)
console.debug('tools', tools) const availableTools = tools.filter((tool: any) => !mcpServer.disabledTools?.includes(tool.name))
mcpTools.push(...tools) mcpTools.push(...availableTools)
} }
} }

View File

@ -372,6 +372,7 @@ export interface MCPServer {
args?: string[] args?: string[]
env?: Record<string, string> env?: Record<string, string>
isActive: boolean isActive: boolean
disabledTools?: string[] // List of tool names that are disabled for this server
} }
export interface MCPToolInputSchema { export interface MCPToolInputSchema {