diff --git a/src/main/apiServer/services/mcp.ts b/src/main/apiServer/services/mcp.ts index d6acae745e..4cb19477b8 100644 --- a/src/main/apiServer/services/mcp.ts +++ b/src/main/apiServer/services/mcp.ts @@ -113,14 +113,14 @@ class MCPApiService extends EventEmitter { const client = await mcpService.initClient(server) const tools = await client.listTools() - logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) }) + logger.silly(`Server with id ${id} info:`, { tools: JSON.stringify(tools.tools) }) return { id: server.id, name: server.name, type: server.type, description: server.description, - tools + tools: tools.tools } } catch (error: any) { logger.error(`Failed to get server info with id ${id}:`, error) diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 6536c6103e..df70742312 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -57,7 +57,8 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({ instructions: existing?.instructions, model: existing?.model ?? 'claude-4-sonnet', accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [], - allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [] + allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [], + mcps: existing?.mcps ? [...existing.mcps] : [] }) interface BaseProps { diff --git a/src/renderer/src/components/Popups/agent/SessionModal.tsx b/src/renderer/src/components/Popups/agent/SessionModal.tsx index df77792d71..9bddab7848 100644 --- a/src/renderer/src/components/Popups/agent/SessionModal.tsx +++ b/src/renderer/src/components/Popups/agent/SessionModal.tsx @@ -58,7 +58,8 @@ const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): ? [...existing.allowed_tools] : agent?.allowed_tools ? [...agent.allowed_tools] - : [] + : [], + mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : [] }) interface BaseProps { @@ -266,7 +267,8 @@ export const SessionModal: React.FC = ({ instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...(form.allowed_tools ?? [])] + allowed_tools: [...(form.allowed_tools ?? [])], + mcps: [...(form.mcps ?? [])] } satisfies UpdateSessionForm updateSession(updatePayload) @@ -278,7 +280,8 @@ export const SessionModal: React.FC = ({ instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...(form.allowed_tools ?? [])] + allowed_tools: [...(form.allowed_tools ?? [])], + mcps: [...(form.mcps ?? [])] } satisfies CreateSessionForm const createdSession = await createSession(newSession) if (createdSession) { @@ -300,6 +303,7 @@ export const SessionModal: React.FC = ({ form.instructions, form.accessible_paths, form.allowed_tools, + form.mcps, session, onClose, onSessionCreated, diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e6ec135a0e..75897d6d46 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -81,6 +81,7 @@ }, "settings": { "essential": "基础设置", + "mcps": "MCP 服务器", "prompt": "提示词设置" }, "type": { diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentMCPSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentMCPSettings.tsx new file mode 100644 index 0000000000..f9ca0566fd --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/AgentMCPSettings.tsx @@ -0,0 +1,130 @@ +import { Card, CardBody, CardHeader, Switch, Tooltip } from '@heroui/react' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import { GetAgentResponse, UpdateAgentForm } from '@renderer/types' +import { Info } from 'lucide-react' +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingsContainer, SettingsItem, SettingsTitle } from './shared' + +interface AgentMCPSettingsProps { + agent: GetAgentResponse | undefined | null + updateAgent: (form: UpdateAgentForm) => Promise | void +} + +export const AgentMCPSettings: React.FC = ({ agent, updateAgent }) => { + const { t } = useTranslation() + const { mcpServers: allMcpServers } = useMCPServers() + const [selectedIds, setSelectedIds] = useState([]) + + const availableServers = useMemo(() => allMcpServers ?? [], [allMcpServers]) + + useEffect(() => { + if (!agent) { + setSelectedIds([]) + return + } + const mcps = agent.mcps ?? [] + const validIds = mcps.filter((id) => availableServers.some((server) => server.id === id)) + setSelectedIds((prev) => { + if (prev.length === validIds.length && prev.every((id) => validIds.includes(id))) { + return prev + } + return validIds + }) + }, [agent, availableServers]) + + const handleToggle = useCallback( + (serverId: string, isEnabled: boolean) => { + if (!agent) return + + setSelectedIds((prev) => { + const exists = prev.includes(serverId) + if (isEnabled === exists) { + return prev + } + const next = isEnabled ? [...prev, serverId] : prev.filter((id) => id !== serverId) + updateAgent({ id: agent.id, mcps: next }) + return next + }) + }, + [agent, updateAgent] + ) + + const enabledCount = useMemo(() => { + const validSelected = selectedIds.filter((id) => availableServers.some((server) => server.id === id)) + return validSelected.length + }, [selectedIds, availableServers]) + + const renderServerMeta = useCallback((meta?: ReactNode) => { + if (!meta) return null + return {meta} + }, []) + + if (!agent) { + return null + } + + return ( + + +
+
+ + {t('assistants.settings.mcp.title')} + + + + + {availableServers.length > 0 ? ( + + {enabledCount} / {availableServers.length} {t('settings.mcp.active')} + + ) : null} +
+ + {availableServers.length > 0 ? ( +
+ {availableServers.map((server) => { + const isSelected = selectedIds.includes(server.id) + return ( + + +
+ {server.name} + {server.description ? ( + {server.description} + ) : null} +
+ handleToggle(server.id, value)} + /> +
+ + {renderServerMeta(server.baseUrl)} + {renderServerMeta(server.provider)} + +
+ ) + })} +
+ ) : ( +
+ {t('assistants.settings.mcp.noServersAvailable', 'No MCP servers available')} +
+ )} +
+
+
+ ) +} + +export default AgentMCPSettings diff --git a/src/renderer/src/pages/settings/AgentSettings/index.tsx b/src/renderer/src/pages/settings/AgentSettings/index.tsx index 525eb94675..da6cb5355c 100644 --- a/src/renderer/src/pages/settings/AgentSettings/index.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/index.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' import AgentEssentialSettings from './AgentEssentialSettings' +import AgentMCPSettings from './AgentMCPSettings' import AgentPromptSettings from './AgentPromptSettings' import { AgentLabel } from './shared' @@ -20,7 +21,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams { resolve: () => void } -type AgentSettingPopupTab = 'essential' | 'prompt' +type AgentSettingPopupTab = 'essential' | 'prompt' | 'mcps' | 'session-mcps' const AgentSettingPopupContainer: React.FC = ({ tab, agentId, resolve }) => { const [open, setOpen] = useState(true) @@ -51,6 +52,10 @@ const AgentSettingPopupContainer: React.FC = ({ tab, ag { key: 'prompt', label: t('agent.settings.prompt') + }, + { + key: 'mcps', + label: t('agent.settings.mcps', 'MCP Servers') } ] as const satisfies { key: AgentSettingPopupTab; label: string }[] ).filter(Boolean) @@ -81,6 +86,7 @@ const AgentSettingPopupContainer: React.FC = ({ tab, ag {menu === 'essential' && } {menu === 'prompt' && } + {menu === 'mcps' && } ) diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index bf414c785e..9ba470b20d 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -156,8 +156,6 @@ export interface AgentMessagePersistExchangeResult { // Not implemented fields: // - plan_model: Optional model for planning/thinking tasks // - small_model: Optional lightweight model for quick responses -// - mcps: Optional array of MCP (Model Control Protocol) tool IDs -// - allowed_tools: Optional array of permitted tool IDs // - configuration: Optional agent settings (temperature, top_p, etc.) // ------------------ Form models ------------------ export type BaseAgentForm = { @@ -170,6 +168,7 @@ export type BaseAgentForm = { model: string accessible_paths: string[] allowed_tools: string[] + mcps?: string[] } export type AddAgentForm = Omit & { id?: never }