diff --git a/src/main/services/urlschema/mcp-install.ts b/src/main/services/urlschema/mcp-install.ts index ceb2e41ece..e35c40d52e 100644 --- a/src/main/services/urlschema/mcp-install.ts +++ b/src/main/services/urlschema/mcp-install.ts @@ -9,13 +9,20 @@ const logger = loggerService.withContext('URLSchema:handleMcpProtocolUrl') function installMCPServer(server: MCPServer) { const mainWindow = windowService.getMainWindow() + const now = Date.now() - if (!server.id) { - server.id = nanoid() + const payload: MCPServer = { + ...server, + id: server.id ?? nanoid(), + installSource: 'protocol', + isTrusted: false, + isActive: false, + trustedAt: undefined, + installedAt: server.installedAt ?? now } if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server) + mainWindow.webContents.send(IpcChannel.Mcp_AddServer, payload) } } diff --git a/src/renderer/src/hooks/useMCPServerTrust.tsx b/src/renderer/src/hooks/useMCPServerTrust.tsx new file mode 100644 index 0000000000..817ae03105 --- /dev/null +++ b/src/renderer/src/hooks/useMCPServerTrust.tsx @@ -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 => { + const commandPreview = getCommandPreview(server) + return modalConfirm({ + title: t('settings.mcp.protocolInstallWarning.title'), + content: ( + + ), + 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 => { + return ensureServerTrustedCore(server, requestConfirm, updateMCPServer) + }, + [requestConfirm, updateMCPServer] + ) + + return { ensureServerTrusted } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index deb9142f65..ff25fb648c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "No prompts available", "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", "providerPlaceholder": "Provider name", "providerUrl": "Provider URL", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index bc5e4b456f..c5071b3b55 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "无可用提示", "requiredField": "必填字段" }, + "protocolInstallWarning": { + "command": "启动命令", + "message": "该 MCP 是通过协议从外部来源安装的,运行来历不明的工具可能对您的计算机造成危害。", + "run": "运行", + "title": "运行外部 MCP?" + }, "provider": "提供者", "providerPlaceholder": "提供者名称", "providerUrl": "提供者网址", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 82228bd17e..0f90670911 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "無可用提示", "requiredField": "必填欄位" }, + "protocolInstallWarning": { + "command": "啟動命令", + "message": "此 MCP 透過協議從外部來源安裝,執行來源不明的工具可能會對您的電腦造成危害。", + "run": "執行", + "title": "執行外部 MCP?" + }, "provider": "提供者", "providerPlaceholder": "提供者名稱", "providerUrl": "提供者網址", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index e4c07b1b07..2f9e8ee828 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -3655,6 +3655,12 @@ "noPromptsAvailable": "Keine Prompts verfügbar", "requiredField": "Pflichtfeld" }, + "protocolInstallWarning": { + "command": "Startbefehl", + "message": "Dieses MCP wurde über ein Protokoll aus einer externen Quelle installiert. Das Ausführen unbekannter Tools kann Ihren Computer schädigen.", + "run": "Laufen", + "title": "Externes MCP ausführen?" + }, "provider": "Anbieter", "providerPlaceholder": "Anbietername", "providerUrl": "Anbieter-Website", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 16708a1bed..938a2f31f1 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "Δεν υπάρχουν διαθέσιμες υποδείξεις", "requiredField": "Υποχρεωτικό πεδίο" }, + "protocolInstallWarning": { + "command": "Εντολή εκκίνησης", + "message": "Αυτό το MCP εγκαταστάθηκε από εξωτερική πηγή μέσω πρωτοκόλλου. Η εκτέλεση άγνωστων εργαλείων ενδέχεται να βλάψει τον υπολογιστή σας.", + "run": "Τρέξε", + "title": "Εκτέλεση εξωτερικού MCP;" + }, "provider": "Πάροχος", "providerPlaceholder": "Όνομα παρόχου", "providerUrl": "URL Παρόχου", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index b651338df9..f78e1c97c3 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "No hay indicaciones disponibles", "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", "providerPlaceholder": "Nombre del proveedor", "providerUrl": "URL del proveedor", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index a47f60a5c0..3470ef1642 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "Aucune invite disponible", "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": "Поставщик", "providerPlaceholder": "Название поставщика", "providerUrl": "Адрес поставщика", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 5298b5ca62..68903b82a8 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "利用可能なプロンプトはありません", "requiredField": "必須フィールド" }, + "protocolInstallWarning": { + "command": "起動コマンド", + "message": "このMCPは外部ソースからプロトコル経由でインストールされました。不明なツールを実行すると、コンピューターに危害を及ぼす可能性があります。", + "run": "走る", + "title": "外部のMCPを実行しますか?" + }, "provider": "プロバイダー", "providerPlaceholder": "プロバイダー名", "providerUrl": "プロバイダーURL", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4ad29fa3e5..80b3c0ba21 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "Nenhuma dica disponível", "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", "providerPlaceholder": "Nome do Fornecedor", "providerUrl": "URL do Fornecedor", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 249fbbbca1..5a5ffe0980 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3445,6 +3445,12 @@ "noPromptsAvailable": "Нет доступных подсказок", "requiredField": "Обязательное поле" }, + "protocolInstallWarning": { + "command": "Команда запуска", + "message": "Этот MCP был установлен из внешнего источника через протокол. Запуск неизвестных инструментов может повредить ваш компьютер.", + "run": "Беги", + "title": "Запускать внешний MCP?" + }, "provider": "Провайдер", "providerPlaceholder": "Имя провайдера", "providerUrl": "URL провайдера", diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx index 46bd8f3afc..73a7993f63 100644 --- a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx @@ -138,6 +138,7 @@ const AddMcpServerModal: FC = ({ // Process DXT file try { + const installTimestamp = Date.now() const result = await window.api.mcp.uploadDxt(dxtFile) if (!result.success) { @@ -188,7 +189,11 @@ const AddMcpServerModal: FC = ({ logoUrl: manifest.icon ? `${extractDir}/${manifest.icon}` : undefined, provider: manifest.author?.name, providerUrl: manifest.homepage || manifest.repository?.url, - tags: manifest.keywords + tags: manifest.keywords, + installSource: 'manual', + isTrusted: true, + installedAt: installTimestamp, + trustedAt: installTimestamp } onSuccess(newServer) @@ -253,12 +258,17 @@ const AddMcpServerModal: FC = ({ } // 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框 + const installTimestamp = Date.now() const newServer: MCPServer = { id: nanoid(), ...serverToAdd, name: serverToAdd.name || t('settings.mcp.newServer'), baseUrl: serverToAdd.baseUrl ?? serverToAdd.url ?? '', - isActive: false // 初始狀態為非啟用 + isActive: false, // 初始狀態為非啟用 + installSource: 'manual', + isTrusted: true, + installedAt: installTimestamp, + trustedAt: installTimestamp } onSuccess(newServer) diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index c1a963e841..ec8de05f9a 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -5,6 +5,7 @@ import { Sortable, useDndReorder } from '@renderer/components/dnd' import { EditIcon, RefreshIcon } from '@renderer/components/Icons' import Scrollbar from '@renderer/components/Scrollbar' import { useMCPServers } from '@renderer/hooks/useMCPServers' +import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust' import { MCPServer } from '@renderer/types' import { formatMcpError } from '@renderer/utils/error' import { matchKeywordsInString } from '@renderer/utils/match' @@ -28,6 +29,7 @@ const logger = loggerService.withContext('McpServersList') const McpServersList: FC = () => { const { mcpServers, addMCPServer, deleteMCPServer, updateMcpServers, updateMCPServer } = useMCPServers() + const { ensureServerTrusted } = useMCPServerTrust() const { t } = useTranslation() const navigate = useNavigate() const [isAddModalVisible, setIsAddModalVisible] = useState(false) @@ -156,30 +158,37 @@ const McpServersList: FC = () => { ) const handleToggleActive = async (server: MCPServer, active: boolean) => { - setLoadingServerIds((prev) => new Set(prev).add(server.id)) - const oldActiveState = server.isActive - logger.silly('toggle activate', { serverId: server.id, active }) + let serverForUpdate = server + if (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 { if (active) { - // Fetch version when server is activated - await fetchServerVersion({ ...server, isActive: active }) + await fetchServerVersion({ ...serverForUpdate, isActive: active }) } else { - await window.api.mcp.stopServer(server) - // Clear version when server is deactivated - setServerVersions((prev) => ({ ...prev, [server.id]: null })) + await window.api.mcp.stopServer(serverForUpdate) + setServerVersions((prev) => ({ ...prev, [serverForUpdate.id]: null })) } - updateMCPServer({ ...server, isActive: active }) + updateMCPServer({ ...serverForUpdate, isActive: active }) } catch (error: any) { window.modal.error({ title: t('settings.mcp.startError'), content: formatMcpError(error), centered: true }) - updateMCPServer({ ...server, isActive: oldActiveState }) + updateMCPServer({ ...serverForUpdate, isActive: oldActiveState }) } finally { setLoadingServerIds((prev) => { const next = new Set(prev) - next.delete(server.id) + next.delete(serverForUpdate.id) return next }) } diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 00a7b00921..55e86d98d2 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -3,6 +3,7 @@ import type { McpError } from '@modelcontextprotocol/sdk/types.js' import { DeleteIcon } from '@renderer/components/Icons' import { useTheme } from '@renderer/context/ThemeProvider' import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers' +import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust' import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription' import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { formatMcpError } from '@renderer/utils/error' @@ -81,6 +82,7 @@ const McpSettings: React.FC = () => { const decodedServerId = serverId ? decodeURIComponent(serverId) : '' const server = useMCPServer(decodedServerId).server as MCPServer const { deleteMCPServer, updateMCPServer } = useMCPServers() + const { ensureServerTrusted } = useMCPServerTrust() const [serverType, setServerType] = useState('stdio') const [form] = Form.useForm() const [loading, setLoading] = useState(false) @@ -266,6 +268,7 @@ const McpSettings: React.FC = () => { // set basic fields const mcpServer: MCPServer = { + ...server, id: server.id, name: values.name, type: values.serverType || server.type, @@ -401,34 +404,43 @@ const McpSettings: React.FC = () => { } await form.validateFields() - setLoadingServer(server.id) - const oldActiveState = server.isActive + let serverForUpdate = server + if (active) { + const trustedServer = await ensureServerTrusted(server) + if (!trustedServer) { + return + } + serverForUpdate = trustedServer + } + + setLoadingServer(serverForUpdate.id) + const oldActiveState = serverForUpdate.isActive try { if (active) { - const localTools = await window.api.mcp.listTools(server) + const localTools = await window.api.mcp.listTools(serverForUpdate) setTools(localTools) - const localPrompts = await window.api.mcp.listPrompts(server) + const localPrompts = await window.api.mcp.listPrompts(serverForUpdate) setPrompts(localPrompts) - const localResources = await window.api.mcp.listResources(server) + const localResources = await window.api.mcp.listResources(serverForUpdate) setResources(localResources) - const version = await window.api.mcp.getServerVersion(server) + const version = await window.api.mcp.getServerVersion(serverForUpdate) setServerVersion(version) } else { - await window.api.mcp.stopServer(server) + await window.api.mcp.stopServer(serverForUpdate) setServerVersion(null) } - updateMCPServer({ ...server, isActive: active }) + updateMCPServer({ ...serverForUpdate, isActive: active }) } catch (error: any) { window.modal.error({ title: t('settings.mcp.startError'), content: formatMcpError(error as McpError), centered: true }) - updateMCPServer({ ...server, isActive: oldActiveState }) + updateMCPServer({ ...serverForUpdate, isActive: oldActiveState }) } finally { setLoadingServer(null) } diff --git a/src/renderer/src/pages/settings/MCPSettings/ProtocolInstallWarning.tsx b/src/renderer/src/pages/settings/MCPSettings/ProtocolInstallWarning.tsx new file mode 100644 index 0000000000..a05391a2e0 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/ProtocolInstallWarning.tsx @@ -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 = ({ + message, + commandLabel, + commandPreview +}) => { + return ( +
+

{message}

+ {commandPreview && ( +
+
{commandLabel}
+
+            {commandPreview}
+          
+
+ )} +
+ ) +} + +export default ProtocolInstallWarningContent diff --git a/src/renderer/src/pages/settings/MCPSettings/utils.ts b/src/renderer/src/pages/settings/MCPSettings/utils.ts new file mode 100644 index 0000000000..d137c669e4 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/utils.ts @@ -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, + updateServer: (server: MCPServer) => void +): Promise { + 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 +} diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index b54db9bbb1..6f06bdd749 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 162, + version: 163, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index eb659399be..aef38bcbe8 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -79,7 +79,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ command: 'npx', args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'], isActive: false, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), @@ -91,14 +93,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ MEMORY_FILE_PATH: 'YOUR_MEMORY_FILE_PATH' }, shouldConfig: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), name: BuiltinMCPServerNames.sequentialThinking, type: 'inMemory', isActive: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), @@ -109,14 +115,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ BRAVE_API_KEY: 'YOUR_API_KEY' }, shouldConfig: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), name: BuiltinMCPServerNames.fetch, type: 'inMemory', isActive: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), @@ -125,7 +135,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ args: ['/Users/username/Desktop', '/path/to/other/allowed/dir'], shouldConfig: true, isActive: false, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), @@ -136,14 +148,18 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ DIFY_KEY: 'YOUR_DIFY_KEY' }, shouldConfig: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), name: BuiltinMCPServerNames.python, type: 'inMemory', isActive: false, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true }, { id: nanoid(), @@ -155,7 +171,9 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ DIDI_API_KEY: 'YOUR_DIDI_API_KEY' }, shouldConfig: true, - provider: 'CherryAI' + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true } ] as const diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 326ea4614d..f682b135de 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -23,6 +23,7 @@ import { DEFAULT_ASSISTANT_SETTINGS } from '@renderer/services/AssistantService' import { Assistant, BuiltinOcrProvider, + isBuiltinMCPServer, isSystemProvider, Model, Provider, @@ -2591,6 +2592,23 @@ const migrateConfig = { logger.error('migrate 162 error', error as Error) 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 + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 91b3f0d564..db07557b15 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -11,7 +11,7 @@ import type { StreamTextParams } from './aiCoreTypes' import type { Chunk } from './chunk' import type { FileMetadata } from './file' import { KnowledgeBase, KnowledgeReference } from './knowledge' -import { MCPConfigSample, McpServerType } from './mcp' +import { MCPConfigSample, MCPServerInstallSource, McpServerType } from './mcp' import type { Message } from './newMessage' import type { BaseTool, MCPTool } from './tool' @@ -830,6 +830,14 @@ export interface MCPServer { shouldConfig?: boolean /** 用于标记服务器是否运行中 */ isActive: boolean + /** 标记 MCP 安装来源,例如 builtin/manual/protocol */ + installSource?: MCPServerInstallSource + /** 指示用户是否已信任该 MCP */ + isTrusted?: boolean + /** 首次标记为信任的时间戳 */ + trustedAt?: number + /** 安装时间戳 */ + installedAt?: number } export type BuiltinMCPServer = MCPServer & { diff --git a/src/renderer/src/types/mcp.ts b/src/renderer/src/types/mcp.ts index c447d697e7..48f48a6167 100644 --- a/src/renderer/src/types/mcp.ts +++ b/src/renderer/src/types/mcp.ts @@ -27,6 +27,9 @@ export const McpServerTypeSchema = z }) .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 + /** * 定义单个 MCP 服务器的配置。 * 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() // 在这里定义额外的校验逻辑