feat: the frontend part of mcp (#22131)

Co-authored-by: jZonG <jzongcode@gmail.com>
Co-authored-by: Novice <novice12185727@gmail.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
This commit is contained in:
Joel
2025-07-10 14:14:02 +08:00
committed by GitHub
parent 535fff62f3
commit 5375d9bb27
152 changed files with 6340 additions and 695 deletions

View File

@@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { memo, useMemo, useRef, useState } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { useTranslation } from 'react-i18next'
import { Group } from '@/app/components/base/icons/src/vender/other'
import AppIcon from '@/app/components/base/app-icon'
type Status = 'not-installed' | 'not-authorized' | undefined
@@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools
const { data: mcpTools } = useAllMCPTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName
return toolWithProvider.name === providerName || toolWithProvider.id === providerName
})
}, [buildInTools, customTools, providerName, workflowTools])
}, [buildInTools, customTools, providerName, workflowTools, mcpTools])
const providerNameParts = providerName.split('/')
const author = providerNameParts[0]
const name = providerNameParts[1]
const icon = useMemo(() => {
if (!isDataReady) return ''
if (currentProvider) return currentProvider.icon as string
if (currentProvider) return currentProvider.icon
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
return iconFromMarketPlace
}, [author, currentProvider, name, isDataReady])
@@ -62,19 +65,32 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
)}
ref={containerRef}
>
{(!iconFetchError && isDataReady)
? <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
: <Group className="h-3 w-3 opacity-35" />
}
{(() => {
if (iconFetchError || !icon)
return <Group className="h-3 w-3 opacity-35" />
if (typeof icon === 'string') {
return <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
}
if (typeof icon === 'object') {
return <AppIcon
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
icon={icon?.content}
background={icon?.background}
/>
}
return <Group className="h-3 w-3 opacity-35" />
})()}
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
</div>
</Tooltip>

View File

@@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
version: '2',
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
@@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
const schemas = toolValue.schemas || []
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
const version = payload.version
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),

View File

@@ -104,7 +104,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
{t('workflow.nodes.agent.toolbox')}
</GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'>
{tools.map(tool => <ToolIcon {...tool} key={tool.id} />)}
{tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)}
</div>
</Group>}
</div>

View File

@@ -38,11 +38,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
readOnly,
outputSchema,
handleMemoryChange,
canChooseMCPTool,
} = useConfig(props.id, props.data)
const { t } = useTranslation()
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return <div className='my-2'>
<Field
required
@@ -56,6 +56,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
meta: inputs.meta,
} : undefined}
onStrategyChange={(strategy) => {
setInputs({
@@ -65,6 +66,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
meta: strategy?.meta,
})
resetEditor(Date.now())
}}
@@ -74,6 +76,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
canChooseMCPTool={canChooseMCPTool}
/>
</Field>
<div className='px-4 py-2'>

View File

@@ -1,14 +1,17 @@
import type { CommonNodeType, Memory } from '@/app/components/workflow/types'
import type { ToolVarInputs } from '../tool/types'
import type { PluginMeta } from '@/app/components/plugins/types'
export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string
agent_strategy_name?: string
agent_strategy_label?: string
agent_parameters?: ToolVarInputs
meta?: PluginMeta
output_schema: Record<string, any>
plugin_unique_identifier?: string
memory?: Memory
version?: string
}
export enum AgentFeature {

View File

@@ -6,13 +6,16 @@ import {
useIsChatMode,
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import type { Memory, Var } from '../../types'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
import produce from 'immer'
import { isSupportMCP } from '@/utils/plugin-version-feature'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
export type StrategyStatus = {
plugin: {
@@ -85,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => {
})
const formData = useMemo(() => {
const paramNameList = (currentStrategy?.parameters || []).map(item => item.name)
return Object.fromEntries(
const res = Object.fromEntries(
Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => {
return [key, value.value]
}),
)
return res
}, [inputs.agent_parameters, currentStrategy?.parameters])
const onFormChange = (value: Record<string, any>) => {
const res: ToolVarInputs = {}
@@ -105,6 +109,42 @@ const useConfig = (id: string, payload: AgentNodeType) => {
})
}
const formattingToolData = (data: any) => {
const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any))
const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true)
const res = produce(data, (draft: any) => {
draft.settings = settingValues
draft.parameters = paramValues
})
return res
}
const formattingLegacyData = () => {
if (inputs.version)
return inputs
const newData = produce(inputs, (draft) => {
const schemas = currentStrategy?.parameters || []
Object.keys(draft.agent_parameters || {}).forEach((key) => {
const targetSchema = schemas.find(schema => schema.name === key)
if (targetSchema?.type === FormTypeEnum.toolSelector)
draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value)
if (targetSchema?.type === FormTypeEnum.multiToolSelector)
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool))
})
draft.version = '2'
})
return newData
}
// formatting legacy data
useEffect(() => {
if (!currentStrategy)
return
const newData = formattingLegacyData()
setInputs(newData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStrategy])
// vars
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
@@ -172,6 +212,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
outputSchema,
handleMemoryChange,
isChatMode,
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
}
}