mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +08:00
feat: add confirmation modal for activating protocol-installed MCP (#11070)
* feat: add confirmation modal for activating protocol-installed MCP * fix: sync i18n * fix(i18n): Auto update translations for PR #11070 * chore: verify ci is working * Revert "chore: verify ci is working" This reverts commita2434a397d. --------- Co-authored-by: GitHub Action <action@github.com> (cherry picked from commit68e0d8b0f1)
This commit is contained in:
parent
ce5829c24d
commit
8d5d76cc00
@ -9,13 +9,20 @@ const logger = loggerService.withContext('URLSchema:handleMcpProtocolUrl')
|
|||||||
|
|
||||||
function installMCPServer(server: MCPServer) {
|
function installMCPServer(server: MCPServer) {
|
||||||
const mainWindow = windowService.getMainWindow()
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
if (!server.id) {
|
const payload: MCPServer = {
|
||||||
server.id = nanoid()
|
...server,
|
||||||
|
id: server.id ?? nanoid(),
|
||||||
|
installSource: 'protocol',
|
||||||
|
isTrusted: false,
|
||||||
|
isActive: false,
|
||||||
|
trustedAt: undefined,
|
||||||
|
installedAt: server.installedAt ?? now
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
|
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
src/renderer/src/hooks/useMCPServerTrust.tsx
Normal file
57
src/renderer/src/hooks/useMCPServerTrust.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import ProtocolInstallWarningContent from '@renderer/pages/settings/MCPSettings/ProtocolInstallWarning'
|
||||||
|
import {
|
||||||
|
ensureServerTrusted as ensureServerTrustedCore,
|
||||||
|
getCommandPreview
|
||||||
|
} from '@renderer/pages/settings/MCPSettings/utils'
|
||||||
|
import { MCPServer } from '@renderer/types'
|
||||||
|
import { modalConfirm } from '@renderer/utils'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { useMCPServers } from './useMCPServers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for handling MCP server trust verification
|
||||||
|
* Binds UI (modal dialog) to the core trust verification logic
|
||||||
|
*/
|
||||||
|
export const useMCPServerTrust = () => {
|
||||||
|
const { updateMCPServer } = useMCPServers()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request user confirmation to trust a server
|
||||||
|
* Shows a warning modal with server command preview
|
||||||
|
*/
|
||||||
|
const requestConfirm = useCallback(
|
||||||
|
async (server: MCPServer): Promise<boolean> => {
|
||||||
|
const commandPreview = getCommandPreview(server)
|
||||||
|
return modalConfirm({
|
||||||
|
title: t('settings.mcp.protocolInstallWarning.title'),
|
||||||
|
content: (
|
||||||
|
<ProtocolInstallWarningContent
|
||||||
|
message={t('settings.mcp.protocolInstallWarning.message')}
|
||||||
|
commandLabel={t('settings.mcp.protocolInstallWarning.command')}
|
||||||
|
commandPreview={commandPreview}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
okText: t('settings.mcp.protocolInstallWarning.run'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okButtonProps: { danger: true }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a server is trusted before proceeding
|
||||||
|
* Combines core logic with UI confirmation
|
||||||
|
*/
|
||||||
|
const ensureServerTrusted = useCallback(
|
||||||
|
async (server: MCPServer): Promise<MCPServer | null> => {
|
||||||
|
return ensureServerTrustedCore(server, requestConfirm, updateMCPServer)
|
||||||
|
},
|
||||||
|
[requestConfirm, updateMCPServer]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ensureServerTrusted }
|
||||||
|
}
|
||||||
@ -3444,6 +3444,12 @@
|
|||||||
"noPromptsAvailable": "No prompts available",
|
"noPromptsAvailable": "No prompts available",
|
||||||
"requiredField": "Required Field"
|
"requiredField": "Required Field"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Startup command",
|
||||||
|
"message": "This MCP was installed from an external source via protocol. Running unknown tools may harm your computer.",
|
||||||
|
"run": "Run",
|
||||||
|
"title": "Run external MCP?"
|
||||||
|
},
|
||||||
"provider": "Provider",
|
"provider": "Provider",
|
||||||
"providerPlaceholder": "Provider name",
|
"providerPlaceholder": "Provider name",
|
||||||
"providerUrl": "Provider URL",
|
"providerUrl": "Provider URL",
|
||||||
|
|||||||
@ -3444,6 +3444,12 @@
|
|||||||
"noPromptsAvailable": "无可用提示",
|
"noPromptsAvailable": "无可用提示",
|
||||||
"requiredField": "必填字段"
|
"requiredField": "必填字段"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "启动命令",
|
||||||
|
"message": "该 MCP 是通过协议从外部来源安装的,运行来历不明的工具可能对您的计算机造成危害。",
|
||||||
|
"run": "运行",
|
||||||
|
"title": "运行外部 MCP?"
|
||||||
|
},
|
||||||
"provider": "提供者",
|
"provider": "提供者",
|
||||||
"providerPlaceholder": "提供者名称",
|
"providerPlaceholder": "提供者名称",
|
||||||
"providerUrl": "提供者网址",
|
"providerUrl": "提供者网址",
|
||||||
|
|||||||
@ -3444,6 +3444,12 @@
|
|||||||
"noPromptsAvailable": "無可用提示",
|
"noPromptsAvailable": "無可用提示",
|
||||||
"requiredField": "必填欄位"
|
"requiredField": "必填欄位"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "啟動命令",
|
||||||
|
"message": "此 MCP 透過協議從外部來源安裝,執行來源不明的工具可能會對您的電腦造成危害。",
|
||||||
|
"run": "執行",
|
||||||
|
"title": "執行外部 MCP?"
|
||||||
|
},
|
||||||
"provider": "提供者",
|
"provider": "提供者",
|
||||||
"providerPlaceholder": "提供者名稱",
|
"providerPlaceholder": "提供者名稱",
|
||||||
"providerUrl": "提供者網址",
|
"providerUrl": "提供者網址",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "Δεν υπάρχουν διαθέσιμες υποδείξεις",
|
"noPromptsAvailable": "Δεν υπάρχουν διαθέσιμες υποδείξεις",
|
||||||
"requiredField": "Υποχρεωτικό πεδίο"
|
"requiredField": "Υποχρεωτικό πεδίο"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Εντολή εκκίνησης",
|
||||||
|
"message": "Αυτό το MCP εγκαταστάθηκε από εξωτερική πηγή μέσω πρωτοκόλλου. Η εκτέλεση άγνωστων εργαλείων ενδέχεται να βλάψει τον υπολογιστή σας.",
|
||||||
|
"run": "Τρέξε",
|
||||||
|
"title": "Εκτέλεση εξωτερικού MCP;"
|
||||||
|
},
|
||||||
"provider": "Πάροχος",
|
"provider": "Πάροχος",
|
||||||
"providerPlaceholder": "Όνομα παρόχου",
|
"providerPlaceholder": "Όνομα παρόχου",
|
||||||
"providerUrl": "URL Παρόχου",
|
"providerUrl": "URL Παρόχου",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "No hay indicaciones disponibles",
|
"noPromptsAvailable": "No hay indicaciones disponibles",
|
||||||
"requiredField": "Campo obligatorio"
|
"requiredField": "Campo obligatorio"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Comando de inicio",
|
||||||
|
"message": "Este MCP fue instalado desde una fuente externa a través del protocolo. Ejecutar herramientas desconocidas puede dañar tu computadora.",
|
||||||
|
"run": "Correr",
|
||||||
|
"title": "¿Ejecutar MCP externo?"
|
||||||
|
},
|
||||||
"provider": "Proveedor",
|
"provider": "Proveedor",
|
||||||
"providerPlaceholder": "Nombre del proveedor",
|
"providerPlaceholder": "Nombre del proveedor",
|
||||||
"providerUrl": "URL del proveedor",
|
"providerUrl": "URL del proveedor",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "Aucune invite disponible",
|
"noPromptsAvailable": "Aucune invite disponible",
|
||||||
"requiredField": "Champ obligatoire"
|
"requiredField": "Champ obligatoire"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Commande de démarrage",
|
||||||
|
"message": "Ce MCP a été installé depuis une source externe via le protocole. L'exécution d'outils inconnus peut endommager votre ordinateur.",
|
||||||
|
"run": "Courir",
|
||||||
|
"title": "Exécuter un MCP externe ?"
|
||||||
|
},
|
||||||
"provider": "Поставщик",
|
"provider": "Поставщик",
|
||||||
"providerPlaceholder": "Название поставщика",
|
"providerPlaceholder": "Название поставщика",
|
||||||
"providerUrl": "Адрес поставщика",
|
"providerUrl": "Адрес поставщика",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||||
"requiredField": "必須フィールド"
|
"requiredField": "必須フィールド"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "起動コマンド",
|
||||||
|
"message": "このMCPは外部ソースからプロトコル経由でインストールされました。不明なツールを実行すると、コンピューターに危害を及ぼす可能性があります。",
|
||||||
|
"run": "走る",
|
||||||
|
"title": "外部のMCPを実行しますか?"
|
||||||
|
},
|
||||||
"provider": "プロバイダー",
|
"provider": "プロバイダー",
|
||||||
"providerPlaceholder": "プロバイダー名",
|
"providerPlaceholder": "プロバイダー名",
|
||||||
"providerUrl": "プロバイダーURL",
|
"providerUrl": "プロバイダーURL",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "Nenhuma dica disponível",
|
"noPromptsAvailable": "Nenhuma dica disponível",
|
||||||
"requiredField": "Campo obrigatório"
|
"requiredField": "Campo obrigatório"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Comando de inicialização",
|
||||||
|
"message": "Este MCP foi instalado a partir de uma fonte externa via protocolo. Executar ferramentas desconhecidas pode prejudicar seu computador.",
|
||||||
|
"run": "Correr",
|
||||||
|
"title": "Executar MCP externo?"
|
||||||
|
},
|
||||||
"provider": "Fornecedor",
|
"provider": "Fornecedor",
|
||||||
"providerPlaceholder": "Nome do Fornecedor",
|
"providerPlaceholder": "Nome do Fornecedor",
|
||||||
"providerUrl": "URL do Fornecedor",
|
"providerUrl": "URL do Fornecedor",
|
||||||
|
|||||||
@ -3434,6 +3434,12 @@
|
|||||||
"noPromptsAvailable": "Нет доступных подсказок",
|
"noPromptsAvailable": "Нет доступных подсказок",
|
||||||
"requiredField": "Обязательное поле"
|
"requiredField": "Обязательное поле"
|
||||||
},
|
},
|
||||||
|
"protocolInstallWarning": {
|
||||||
|
"command": "Команда запуска",
|
||||||
|
"message": "Этот MCP был установлен из внешнего источника через протокол. Запуск неизвестных инструментов может повредить ваш компьютер.",
|
||||||
|
"run": "Беги",
|
||||||
|
"title": "Запускать внешний MCP?"
|
||||||
|
},
|
||||||
"provider": "Провайдер",
|
"provider": "Провайдер",
|
||||||
"providerPlaceholder": "Имя провайдера",
|
"providerPlaceholder": "Имя провайдера",
|
||||||
"providerUrl": "URL провайдера",
|
"providerUrl": "URL провайдера",
|
||||||
|
|||||||
@ -138,6 +138,7 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({
|
|||||||
|
|
||||||
// Process DXT file
|
// Process DXT file
|
||||||
try {
|
try {
|
||||||
|
const installTimestamp = Date.now()
|
||||||
const result = await window.api.mcp.uploadDxt(dxtFile)
|
const result = await window.api.mcp.uploadDxt(dxtFile)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@ -188,7 +189,11 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({
|
|||||||
logoUrl: manifest.icon ? `${extractDir}/${manifest.icon}` : undefined,
|
logoUrl: manifest.icon ? `${extractDir}/${manifest.icon}` : undefined,
|
||||||
provider: manifest.author?.name,
|
provider: manifest.author?.name,
|
||||||
providerUrl: manifest.homepage || manifest.repository?.url,
|
providerUrl: manifest.homepage || manifest.repository?.url,
|
||||||
tags: manifest.keywords
|
tags: manifest.keywords,
|
||||||
|
installSource: 'manual',
|
||||||
|
isTrusted: true,
|
||||||
|
installedAt: installTimestamp,
|
||||||
|
trustedAt: installTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess(newServer)
|
onSuccess(newServer)
|
||||||
@ -253,12 +258,17 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框
|
// 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框
|
||||||
|
const installTimestamp = Date.now()
|
||||||
const newServer: MCPServer = {
|
const newServer: MCPServer = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
...serverToAdd,
|
...serverToAdd,
|
||||||
name: serverToAdd.name || t('settings.mcp.newServer'),
|
name: serverToAdd.name || t('settings.mcp.newServer'),
|
||||||
baseUrl: serverToAdd.baseUrl ?? serverToAdd.url ?? '',
|
baseUrl: serverToAdd.baseUrl ?? serverToAdd.url ?? '',
|
||||||
isActive: false // 初始狀態為非啟用
|
isActive: false, // 初始狀態為非啟用
|
||||||
|
installSource: 'manual',
|
||||||
|
isTrusted: true,
|
||||||
|
installedAt: installTimestamp,
|
||||||
|
trustedAt: installTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess(newServer)
|
onSuccess(newServer)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
|||||||
import { EditIcon, RefreshIcon } from '@renderer/components/Icons'
|
import { EditIcon, RefreshIcon } from '@renderer/components/Icons'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
|
import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { formatMcpError } from '@renderer/utils/error'
|
import { formatMcpError } from '@renderer/utils/error'
|
||||||
import { matchKeywordsInString } from '@renderer/utils/match'
|
import { matchKeywordsInString } from '@renderer/utils/match'
|
||||||
@ -28,6 +29,7 @@ const logger = loggerService.withContext('McpServersList')
|
|||||||
|
|
||||||
const McpServersList: FC = () => {
|
const McpServersList: FC = () => {
|
||||||
const { mcpServers, addMCPServer, deleteMCPServer, updateMcpServers, updateMCPServer } = useMCPServers()
|
const { mcpServers, addMCPServer, deleteMCPServer, updateMcpServers, updateMCPServer } = useMCPServers()
|
||||||
|
const { ensureServerTrusted } = useMCPServerTrust()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false)
|
const [isAddModalVisible, setIsAddModalVisible] = useState(false)
|
||||||
@ -156,30 +158,37 @@ const McpServersList: FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleToggleActive = async (server: MCPServer, active: boolean) => {
|
const handleToggleActive = async (server: MCPServer, active: boolean) => {
|
||||||
setLoadingServerIds((prev) => new Set(prev).add(server.id))
|
let serverForUpdate = server
|
||||||
const oldActiveState = server.isActive
|
if (active) {
|
||||||
logger.silly('toggle activate', { serverId: server.id, active })
|
const trustedServer = await ensureServerTrusted(server)
|
||||||
|
if (!trustedServer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverForUpdate = trustedServer
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingServerIds((prev) => new Set(prev).add(serverForUpdate.id))
|
||||||
|
const oldActiveState = serverForUpdate.isActive
|
||||||
|
logger.silly('toggle activate', { serverId: serverForUpdate.id, active })
|
||||||
try {
|
try {
|
||||||
if (active) {
|
if (active) {
|
||||||
// Fetch version when server is activated
|
await fetchServerVersion({ ...serverForUpdate, isActive: active })
|
||||||
await fetchServerVersion({ ...server, isActive: active })
|
|
||||||
} else {
|
} else {
|
||||||
await window.api.mcp.stopServer(server)
|
await window.api.mcp.stopServer(serverForUpdate)
|
||||||
// Clear version when server is deactivated
|
setServerVersions((prev) => ({ ...prev, [serverForUpdate.id]: null }))
|
||||||
setServerVersions((prev) => ({ ...prev, [server.id]: null }))
|
|
||||||
}
|
}
|
||||||
updateMCPServer({ ...server, isActive: active })
|
updateMCPServer({ ...serverForUpdate, isActive: active })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
window.modal.error({
|
window.modal.error({
|
||||||
title: t('settings.mcp.startError'),
|
title: t('settings.mcp.startError'),
|
||||||
content: formatMcpError(error),
|
content: formatMcpError(error),
|
||||||
centered: true
|
centered: true
|
||||||
})
|
})
|
||||||
updateMCPServer({ ...server, isActive: oldActiveState })
|
updateMCPServer({ ...serverForUpdate, isActive: oldActiveState })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingServerIds((prev) => {
|
setLoadingServerIds((prev) => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
next.delete(server.id)
|
next.delete(serverForUpdate.id)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { McpError } from '@modelcontextprotocol/sdk/types.js'
|
|||||||
import { DeleteIcon } from '@renderer/components/Icons'
|
import { DeleteIcon } from '@renderer/components/Icons'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
|
import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust'
|
||||||
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
|
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
|
||||||
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
||||||
import { formatMcpError } from '@renderer/utils/error'
|
import { formatMcpError } from '@renderer/utils/error'
|
||||||
@ -81,6 +82,7 @@ const McpSettings: React.FC = () => {
|
|||||||
const decodedServerId = serverId ? decodeURIComponent(serverId) : ''
|
const decodedServerId = serverId ? decodeURIComponent(serverId) : ''
|
||||||
const server = useMCPServer(decodedServerId).server as MCPServer
|
const server = useMCPServer(decodedServerId).server as MCPServer
|
||||||
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
||||||
|
const { ensureServerTrusted } = useMCPServerTrust()
|
||||||
const [serverType, setServerType] = useState<MCPServer['type']>('stdio')
|
const [serverType, setServerType] = useState<MCPServer['type']>('stdio')
|
||||||
const [form] = Form.useForm<MCPFormValues>()
|
const [form] = Form.useForm<MCPFormValues>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@ -266,6 +268,7 @@ const McpSettings: React.FC = () => {
|
|||||||
|
|
||||||
// set basic fields
|
// set basic fields
|
||||||
const mcpServer: MCPServer = {
|
const mcpServer: MCPServer = {
|
||||||
|
...server,
|
||||||
id: server.id,
|
id: server.id,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
type: values.serverType || server.type,
|
type: values.serverType || server.type,
|
||||||
@ -401,34 +404,43 @@ const McpSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await form.validateFields()
|
await form.validateFields()
|
||||||
setLoadingServer(server.id)
|
let serverForUpdate = server
|
||||||
const oldActiveState = server.isActive
|
if (active) {
|
||||||
|
const trustedServer = await ensureServerTrusted(server)
|
||||||
|
if (!trustedServer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverForUpdate = trustedServer
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingServer(serverForUpdate.id)
|
||||||
|
const oldActiveState = serverForUpdate.isActive
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (active) {
|
if (active) {
|
||||||
const localTools = await window.api.mcp.listTools(server)
|
const localTools = await window.api.mcp.listTools(serverForUpdate)
|
||||||
setTools(localTools)
|
setTools(localTools)
|
||||||
|
|
||||||
const localPrompts = await window.api.mcp.listPrompts(server)
|
const localPrompts = await window.api.mcp.listPrompts(serverForUpdate)
|
||||||
setPrompts(localPrompts)
|
setPrompts(localPrompts)
|
||||||
|
|
||||||
const localResources = await window.api.mcp.listResources(server)
|
const localResources = await window.api.mcp.listResources(serverForUpdate)
|
||||||
setResources(localResources)
|
setResources(localResources)
|
||||||
|
|
||||||
const version = await window.api.mcp.getServerVersion(server)
|
const version = await window.api.mcp.getServerVersion(serverForUpdate)
|
||||||
setServerVersion(version)
|
setServerVersion(version)
|
||||||
} else {
|
} else {
|
||||||
await window.api.mcp.stopServer(server)
|
await window.api.mcp.stopServer(serverForUpdate)
|
||||||
setServerVersion(null)
|
setServerVersion(null)
|
||||||
}
|
}
|
||||||
updateMCPServer({ ...server, isActive: active })
|
updateMCPServer({ ...serverForUpdate, isActive: active })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
window.modal.error({
|
window.modal.error({
|
||||||
title: t('settings.mcp.startError'),
|
title: t('settings.mcp.startError'),
|
||||||
content: formatMcpError(error as McpError),
|
content: formatMcpError(error as McpError),
|
||||||
centered: true
|
centered: true
|
||||||
})
|
})
|
||||||
updateMCPServer({ ...server, isActive: oldActiveState })
|
updateMCPServer({ ...serverForUpdate, isActive: oldActiveState })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingServer(null)
|
setLoadingServer(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface ProtocolInstallWarningContentProps {
|
||||||
|
message: string
|
||||||
|
commandLabel: string
|
||||||
|
commandPreview: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warning content component for protocol-installed MCP servers
|
||||||
|
* Displays a security warning and the command that will be executed
|
||||||
|
*/
|
||||||
|
const ProtocolInstallWarningContent: React.FC<ProtocolInstallWarningContentProps> = ({
|
||||||
|
message,
|
||||||
|
commandLabel,
|
||||||
|
commandPreview
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 text-left">
|
||||||
|
<p>{message}</p>
|
||||||
|
{commandPreview && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-semibold">{commandLabel}</div>
|
||||||
|
<pre className="whitespace-pre-wrap break-all rounded-md bg-[var(--color-fill-secondary)] p-2">
|
||||||
|
{commandPreview}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolInstallWarningContent
|
||||||
58
src/renderer/src/pages/settings/MCPSettings/utils.ts
Normal file
58
src/renderer/src/pages/settings/MCPSettings/utils.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { MCPServer } from '@renderer/types'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MCPSettings/utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get command preview string from MCP server configuration
|
||||||
|
* @param server - The MCP server to extract command from
|
||||||
|
* @returns Formatted command string with arguments
|
||||||
|
*/
|
||||||
|
export const getCommandPreview = (server: MCPServer): string => {
|
||||||
|
return [server.command, ...(server.args ?? [])]
|
||||||
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a server is trusted before proceeding (pure logic, no UI)
|
||||||
|
* @param currentServer - The server to verify trust for
|
||||||
|
* @param requestConfirm - Callback to request user confirmation
|
||||||
|
* @param updateServer - Callback to update server state
|
||||||
|
* @returns The trusted server if confirmed, or null if user declined
|
||||||
|
*/
|
||||||
|
export async function ensureServerTrusted(
|
||||||
|
currentServer: MCPServer,
|
||||||
|
requestConfirm: (server: MCPServer) => Promise<boolean>,
|
||||||
|
updateServer: (server: MCPServer) => void
|
||||||
|
): Promise<MCPServer | null> {
|
||||||
|
const isProtocolInstall = currentServer.installSource === 'protocol'
|
||||||
|
|
||||||
|
logger.silly('ensureServerTrusted', {
|
||||||
|
serverId: currentServer.id,
|
||||||
|
installSource: currentServer.installSource,
|
||||||
|
isTrusted: currentServer.isTrusted
|
||||||
|
})
|
||||||
|
|
||||||
|
// Early return if no trust verification needed
|
||||||
|
if (!isProtocolInstall || currentServer.isTrusted) {
|
||||||
|
return currentServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request user confirmation via callback
|
||||||
|
const confirmed = await requestConfirm(currentServer)
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server with trust information
|
||||||
|
const trustedServer = {
|
||||||
|
...currentServer,
|
||||||
|
installSource: 'protocol' as const,
|
||||||
|
isTrusted: true,
|
||||||
|
trustedAt: Date.now()
|
||||||
|
}
|
||||||
|
updateServer(trustedServer)
|
||||||
|
return trustedServer
|
||||||
|
}
|
||||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 162,
|
version: 163,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -79,7 +79,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
command: 'npx',
|
command: 'npx',
|
||||||
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
||||||
isActive: false,
|
isActive: false,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -91,14 +93,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
MEMORY_FILE_PATH: 'YOUR_MEMORY_FILE_PATH'
|
MEMORY_FILE_PATH: 'YOUR_MEMORY_FILE_PATH'
|
||||||
},
|
},
|
||||||
shouldConfig: true,
|
shouldConfig: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: BuiltinMCPServerNames.sequentialThinking,
|
name: BuiltinMCPServerNames.sequentialThinking,
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -109,14 +115,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
BRAVE_API_KEY: 'YOUR_API_KEY'
|
BRAVE_API_KEY: 'YOUR_API_KEY'
|
||||||
},
|
},
|
||||||
shouldConfig: true,
|
shouldConfig: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: BuiltinMCPServerNames.fetch,
|
name: BuiltinMCPServerNames.fetch,
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -125,7 +135,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
args: ['/Users/username/Desktop', '/path/to/other/allowed/dir'],
|
args: ['/Users/username/Desktop', '/path/to/other/allowed/dir'],
|
||||||
shouldConfig: true,
|
shouldConfig: true,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -136,14 +148,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
DIFY_KEY: 'YOUR_DIFY_KEY'
|
DIFY_KEY: 'YOUR_DIFY_KEY'
|
||||||
},
|
},
|
||||||
shouldConfig: true,
|
shouldConfig: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: BuiltinMCPServerNames.python,
|
name: BuiltinMCPServerNames.python,
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -155,7 +171,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
|
|||||||
DIDI_API_KEY: 'YOUR_DIDI_API_KEY'
|
DIDI_API_KEY: 'YOUR_DIDI_API_KEY'
|
||||||
},
|
},
|
||||||
shouldConfig: true,
|
shouldConfig: true,
|
||||||
provider: 'CherryAI'
|
provider: 'CherryAI',
|
||||||
|
installSource: 'builtin',
|
||||||
|
isTrusted: true
|
||||||
}
|
}
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { DEFAULT_ASSISTANT_SETTINGS } from '@renderer/services/AssistantService'
|
|||||||
import {
|
import {
|
||||||
Assistant,
|
Assistant,
|
||||||
BuiltinOcrProvider,
|
BuiltinOcrProvider,
|
||||||
|
isBuiltinMCPServer,
|
||||||
isSystemProvider,
|
isSystemProvider,
|
||||||
Model,
|
Model,
|
||||||
Provider,
|
Provider,
|
||||||
@ -2591,6 +2592,23 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 162 error', error as Error)
|
logger.error('migrate 162 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'163': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
if (state?.mcp?.servers) {
|
||||||
|
state.mcp.servers = state.mcp.servers.map((server) => {
|
||||||
|
const inferredSource = isBuiltinMCPServer(server) ? 'builtin' : 'unknown'
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
installSource: inferredSource
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 169 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import type { StreamTextParams } from './aiCoreTypes'
|
|||||||
import type { Chunk } from './chunk'
|
import type { Chunk } from './chunk'
|
||||||
import type { FileMetadata } from './file'
|
import type { FileMetadata } from './file'
|
||||||
import { KnowledgeBase, KnowledgeReference } from './knowledge'
|
import { KnowledgeBase, KnowledgeReference } from './knowledge'
|
||||||
import { MCPConfigSample, McpServerType } from './mcp'
|
import { MCPConfigSample, MCPServerInstallSource, McpServerType } from './mcp'
|
||||||
import type { Message } from './newMessage'
|
import type { Message } from './newMessage'
|
||||||
import type { BaseTool, MCPTool } from './tool'
|
import type { BaseTool, MCPTool } from './tool'
|
||||||
|
|
||||||
@ -819,6 +819,14 @@ export interface MCPServer {
|
|||||||
shouldConfig?: boolean
|
shouldConfig?: boolean
|
||||||
/** 用于标记服务器是否运行中 */
|
/** 用于标记服务器是否运行中 */
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
/** 标记 MCP 安装来源,例如 builtin/manual/protocol */
|
||||||
|
installSource?: MCPServerInstallSource
|
||||||
|
/** 指示用户是否已信任该 MCP */
|
||||||
|
isTrusted?: boolean
|
||||||
|
/** 首次标记为信任的时间戳 */
|
||||||
|
trustedAt?: number
|
||||||
|
/** 安装时间戳 */
|
||||||
|
installedAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BuiltinMCPServer = MCPServer & {
|
export type BuiltinMCPServer = MCPServer & {
|
||||||
|
|||||||
@ -27,6 +27,9 @@ export const McpServerTypeSchema = z
|
|||||||
})
|
})
|
||||||
.pipe(z.union([z.literal('stdio'), z.literal('sse'), z.literal('streamableHttp'), z.literal('inMemory')])) // 大多数情况下默认使用 stdio
|
.pipe(z.union([z.literal('stdio'), z.literal('sse'), z.literal('streamableHttp'), z.literal('inMemory')])) // 大多数情况下默认使用 stdio
|
||||||
|
|
||||||
|
export const MCPServerInstallSourceSchema = z.enum(['builtin', 'manual', 'protocol', 'unknown']).default('unknown')
|
||||||
|
export type MCPServerInstallSource = z.infer<typeof MCPServerInstallSourceSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义单个 MCP 服务器的配置。
|
* 定义单个 MCP 服务器的配置。
|
||||||
* FIXME: 为了兼容性,暂时允许用户编辑任意字段,这可能会导致问题。
|
* FIXME: 为了兼容性,暂时允许用户编辑任意字段,这可能会导致问题。
|
||||||
@ -168,7 +171,11 @@ export const McpServerConfigSchema = z
|
|||||||
* 是否激活
|
* 是否激活
|
||||||
* 可选。用于标识服务器是否处于激活状态。
|
* 可选。用于标识服务器是否处于激活状态。
|
||||||
*/
|
*/
|
||||||
isActive: z.boolean().optional().describe('Whether the server is active')
|
isActive: z.boolean().optional().describe('Whether the server is active'),
|
||||||
|
installSource: MCPServerInstallSourceSchema.optional().describe('Where the MCP server was installed from'),
|
||||||
|
isTrusted: z.boolean().optional().describe('Whether the MCP server has been trusted by user'),
|
||||||
|
trustedAt: z.number().optional().describe('Timestamp when the server was trusted'),
|
||||||
|
installedAt: z.number().optional().describe('Timestamp when the server was installed')
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
// 在这里定义额外的校验逻辑
|
// 在这里定义额外的校验逻辑
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user