mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 04:31:27 +08:00
✨ 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:
parent
3e2acde9e2
commit
60c85b651f
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -81,6 +81,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"essential": "基础设置",
|
||||
"mcps": "MCP 服务器",
|
||||
"prompt": "提示词设置"
|
||||
},
|
||||
"type": {
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user