feat: add MCP server support for agents

- Add MCP server configuration UI for agent settings
- Update agent and session forms to include MCP server selection
- Fix MCP API service logging and tools handling
- Add Chinese localization for MCP settings
- Update type definitions to support MCP server arrays

This enables agents to use MCP (Model Control Protocol) servers
as additional tools and capabilities in their execution context.
This commit is contained in:
Vaayne 2025-09-23 09:51:01 +08:00
parent 3e2acde9e2
commit 60c85b651f
7 changed files with 150 additions and 9 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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<Props> = ({
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<Props> = ({
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<Props> = ({
form.instructions,
form.accessible_paths,
form.allowed_tools,
form.mcps,
session,
onClose,
onSessionCreated,

View File

@ -81,6 +81,7 @@
},
"settings": {
"essential": "基础设置",
"mcps": "MCP 服务器",
"prompt": "提示词设置"
},
"type": {

View File

@ -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> | void
}
export const AgentMCPSettings: React.FC<AgentMCPSettingsProps> = ({ agent, updateAgent }) => {
const { t } = useTranslation()
const { mcpServers: allMcpServers } = useMCPServers()
const [selectedIds, setSelectedIds] = useState<string[]>([])
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 <span className="text-foreground-400 text-xs">{meta}</span>
}, [])
if (!agent) {
return null
}
return (
<SettingsContainer>
<SettingsItem divider={false} className="flex-1">
<div className="flex h-full flex-col gap-4">
<div className="flex items-center justify-between">
<SettingsTitle>
{t('assistants.settings.mcp.title')}
<Tooltip
placement="right"
content={t('assistants.settings.mcp.description', 'Select MCP servers to use with this agent')}>
<Info size={16} className="text-foreground-400" />
</Tooltip>
</SettingsTitle>
{availableServers.length > 0 ? (
<span className="text-foreground-500 text-xs">
{enabledCount} / {availableServers.length} {t('settings.mcp.active')}
</span>
) : null}
</div>
{availableServers.length > 0 ? (
<div className="flex flex-1 flex-col gap-3 overflow-auto pr-1">
{availableServers.map((server) => {
const isSelected = selectedIds.includes(server.id)
return (
<Card key={server.id} shadow="none" className="border border-default-200">
<CardHeader className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate font-medium text-sm">{server.name}</span>
{server.description ? (
<span className="line-clamp-2 text-foreground-500 text-xs">{server.description}</span>
) : null}
</div>
<Switch
aria-label={t('assistants.settings.mcp.toggle', {
defaultValue: `Toggle ${server.name}`
})}
isSelected={isSelected}
isDisabled={!server.isActive}
size="sm"
onValueChange={(value) => handleToggle(server.id, value)}
/>
</CardHeader>
<CardBody className="gap-1 py-0 pb-3">
{renderServerMeta(server.baseUrl)}
{renderServerMeta(server.provider)}
</CardBody>
</Card>
)
})}
</div>
) : (
<div className="flex flex-1 items-center justify-center rounded-medium border border-default-200 border-dashed px-4 py-10 text-foreground-500 text-sm">
{t('assistants.settings.mcp.noServersAvailable', 'No MCP servers available')}
</div>
)}
</div>
</SettingsItem>
</SettingsContainer>
)
}
export default AgentMCPSettings

View File

@ -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<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
const [open, setOpen] = useState(true)
@ -51,6 +52,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ 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<AgentSettingPopupParams> = ({ tab, ag
<Settings>
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
{menu === 'mcps' && <AgentMCPSettings agent={agent} updateAgent={updateAgent} />}
</Settings>
</div>
)

View File

@ -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<BaseAgentForm, 'id'> & { id?: never }