import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { backendClient } from '@/app/infra/http'; import { KnowledgeBase, MCPResource, MCPServer, PluginTool, } from '@/app/infra/entities/api'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { CircleHelp, Database, FileText, Plus, Server, Wrench, X, } from 'lucide-react'; type ToolSource = NonNullable; type BoundMCPResource = { server_uuid?: string; server_name?: string; uri: string; mode?: string; enabled?: boolean; max_bytes?: number; max_tokens?: number; }; type PipelineExtensions = Awaited< ReturnType >; type AvailablePlugin = PipelineExtensions['available_plugins'][number]; type ToolProviderGroup = { key: string; name: string; tools: PluginTool[]; }; type ToolSourceGroup = { key: ToolSource | string; label: string; groups: ToolProviderGroup[]; total: number; }; type MCPToolOwner = { serverId?: string; serverName: string; }; type PluginToolOwner = { pluginId: string; }; type BoundPlugin = PipelineExtensions['bound_plugins'][number]; const BUILTIN_TOOL_NAMES = new Set([ 'exec', 'read', 'write', 'edit', 'glob', 'grep', ]); const TOOL_SOURCE_ORDER = ['plugin', 'mcp', 'skill', 'builtin']; function getMCPResourceKey(server: MCPServer, uri: string) { return `${server.uuid || server.name}:${uri}`; } function isSameMCPResource( resource: BoundMCPResource, server: MCPServer, uri: string, ) { return ( (resource.server_uuid === server.uuid || (!resource.server_uuid && resource.server_name === server.name)) && resource.uri === uri ); } function getBoundPluginId(plugin: BoundPlugin) { return `${plugin.author || ''}/${plugin.name}`; } function InfoTooltip({ label }: { label: string }) { return ( {label} ); } function normalizeToolSource(tool: PluginTool): ToolSource | string { if (tool.source) { return tool.source; } if (BUILTIN_TOOL_NAMES.has(tool.name)) { return 'builtin'; } if (tool.name.startsWith('langbot_mcp_')) { return 'mcp'; } return 'plugin'; } function hasReliableSourceMetadata(tool: PluginTool) { if (!tool.source) { return false; } if (tool.source === 'plugin' || tool.source === 'mcp') { return Boolean(tool.source_id || tool.source_name); } return true; } function buildToolSourceGroups( tools: PluginTool[], sourceLabels: Record, ) { const sourceGroups = new Map>(); for (const tool of tools) { const source = normalizeToolSource(tool); const providerName = tool.source_name || (source === 'builtin' ? 'LangBot' : sourceLabels[source] || source); const providerKey = `${source}:${tool.source_id || providerName}`; if (!sourceGroups.has(source)) { sourceGroups.set(source, new Map()); } const providerGroups = sourceGroups.get(source)!; if (!providerGroups.has(providerKey)) { providerGroups.set(providerKey, { key: providerKey, name: providerName, tools: [], }); } providerGroups.get(providerKey)!.tools.push(tool); } return Array.from(sourceGroups.entries()) .sort(([left], [right]) => { const leftIndex = TOOL_SOURCE_ORDER.indexOf(left); const rightIndex = TOOL_SOURCE_ORDER.indexOf(right); return ( (leftIndex === -1 ? TOOL_SOURCE_ORDER.length : leftIndex) - (rightIndex === -1 ? TOOL_SOURCE_ORDER.length : rightIndex) ); }) .map( ([source, providerGroups]): ToolSourceGroup => ({ key: source, label: sourceLabels[source] || source, groups: Array.from(providerGroups.values()).sort((left, right) => left.name.localeCompare(right.name), ), total: Array.from(providerGroups.values()).reduce( (count, group) => count + group.tools.length, 0, ), }), ); } function buildMCPToolOwners(servers: MCPServer[]) { const owners = new Map(); const ambiguousNames = new Set(); for (const server of servers) { for (const tool of server.runtime_info?.tools || []) { if (!tool.name) { continue; } const owner = { serverId: server.uuid, serverName: server.name, }; if (owners.has(tool.name)) { ambiguousNames.add(tool.name); continue; } owners.set(tool.name, owner); } } for (const name of ambiguousNames) { owners.delete(name); } return owners; } function buildPluginToolOwners(plugins: AvailablePlugin[]) { const owners = new Map(); const ambiguousNames = new Set(); for (const plugin of plugins) { const metadata = plugin.manifest?.manifest?.metadata; const pluginName = metadata?.name; if (!pluginName) { continue; } const pluginId = metadata.author ? `${metadata.author}/${pluginName}` : pluginName; for (const component of plugin.components || []) { const manifest = component.manifest?.manifest; if (manifest?.kind !== 'Tool') { continue; } const toolName = manifest.metadata?.name; if (!toolName) { continue; } if (owners.has(toolName)) { ambiguousNames.add(toolName); continue; } owners.set(toolName, { pluginId }); } } for (const name of ambiguousNames) { owners.delete(name); } return owners; } function annotateMCPToolSource( tool: PluginTool, owner?: MCPToolOwner, ): PluginTool { if (!owner || hasReliableSourceMetadata(tool)) { return tool; } return { ...tool, source: 'mcp', source_name: owner.serverName, source_id: owner.serverId, }; } function annotatePluginToolSource( tool: PluginTool, owner?: PluginToolOwner, ): PluginTool { if (!owner || hasReliableSourceMetadata(tool)) { return tool; } return { ...tool, source: 'plugin', source_name: owner.pluginId, source_id: owner.pluginId, }; } function annotateToolSource( tool: PluginTool, pluginOwner?: PluginToolOwner, mcpOwner?: MCPToolOwner, ): PluginTool { if (hasReliableSourceMetadata(tool)) { return tool; } if (tool.source === 'plugin') { return annotatePluginToolSource(tool, pluginOwner); } if (tool.source === 'mcp') { return annotateMCPToolSource(tool, mcpOwner); } if (mcpOwner && !pluginOwner) { return annotateMCPToolSource(tool, mcpOwner); } if (pluginOwner && !mcpOwner) { return annotatePluginToolSource(tool, pluginOwner); } return tool; } export default function ToolResourceSelectors({ pipelineId, value, onChange, mode = 'all', }: { pipelineId?: string; value: Record; onChange: (patch: Record) => void; mode?: 'tools' | 'resources' | 'all'; }) { const { t } = useTranslation(); const [tools, setTools] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [extensions, setExtensions] = useState(null); const [toolsDialogOpen, setToolsDialogOpen] = useState(false); const [kbDialogOpen, setKbDialogOpen] = useState(false); const [tempSelectedToolNames, setTempSelectedToolNames] = useState( [], ); const [tempSelectedKBIds, setTempSelectedKBIds] = useState([]); useEffect(() => { if (mode !== 'resources') { backendClient.getTools(pipelineId).then((resp) => setTools(resp.tools)); } if (mode !== 'tools') { backendClient .getKnowledgeBases() .then((resp) => setKnowledgeBases(resp.bases)); } if (pipelineId) { backendClient .getPipelineExtensions(pipelineId) .then((resp) => setExtensions(resp)); } }, [mode, pipelineId]); const enableAllTools = value['enable-all-tools'] !== false; const selectedToolNames = Array.isArray(value.tools) ? value.tools : []; const selectedKBIds = Array.isArray(value['knowledge-bases']) ? value['knowledge-bases'] : []; const selectedMCPResources: BoundMCPResource[] = Array.isArray( value['mcp-resources'], ) ? value['mcp-resources'] : extensions?.bound_mcp_resources || []; const mcpResourceReadEnabled = typeof value['mcp-resource-agent-read-enabled'] === 'boolean' ? value['mcp-resource-agent-read-enabled'] : (extensions?.mcp_resource_agent_read_enabled ?? true); const scopedMCPServers = useMemo(() => { if (!extensions) return []; const boundServerIds = new Set(extensions.bound_mcp_servers || []); return extensions.enable_all_mcp_servers ? extensions.available_mcp_servers : extensions.available_mcp_servers.filter((server) => boundServerIds.has(server.uuid || ''), ); }, [extensions]); const scopedMCPServerIds = useMemo( () => new Set( scopedMCPServers .map((server) => server.uuid) .filter((uuid): uuid is string => !!uuid), ), [scopedMCPServers], ); const scopedMCPServerNames = useMemo( () => new Set(scopedMCPServers.map((server) => server.name)), [scopedMCPServers], ); const mcpToolOwners = useMemo( () => buildMCPToolOwners(scopedMCPServers), [scopedMCPServers], ); const pluginToolOwners = useMemo( () => buildPluginToolOwners(extensions?.available_plugins || []), [extensions], ); const scopedPluginIds = useMemo( () => new Set( extensions?.enable_all_plugins ? (extensions.available_plugins || []).map((plugin) => { const metadata = plugin.manifest?.manifest?.metadata; return metadata?.name ? `${metadata.author || ''}/${metadata.name}` : ''; }) : (extensions?.bound_plugins || []).map(getBoundPluginId), ), [extensions], ); const availableTools = useMemo( () => tools .map((tool) => annotateToolSource( tool, pluginToolOwners.get(tool.name), mcpToolOwners.get(tool.name), ), ) .filter((tool) => { const source = normalizeToolSource(tool); if (source === 'plugin') { if (!extensions) { return false; } if (extensions.enable_all_plugins) { return true; } return ( (tool.source_id && scopedPluginIds.has(tool.source_id)) || (tool.source_name && scopedPluginIds.has(tool.source_name)) ); } if (source === 'mcp') { if (!extensions) { return false; } if (extensions.enable_all_mcp_servers) { return true; } return ( (tool.source_id && scopedMCPServerIds.has(tool.source_id)) || (tool.source_name && scopedMCPServerNames.has(tool.source_name)) ); } return true; }), [ extensions, mcpToolOwners, pluginToolOwners, scopedMCPServerIds, scopedMCPServerNames, scopedPluginIds, tools, ], ); const availableToolNames = useMemo( () => new Set(availableTools.map((tool) => tool.name)), [availableTools], ); const resourceServers = useMemo(() => { return scopedMCPServers.filter( (server) => server.runtime_info?.status === 'connected' && (server.runtime_info.resources || []).length > 0, ); }, [scopedMCPServers]); const availableMCPResourceKeys = useMemo(() => { const keys = new Set(); for (const server of resourceServers) { for (const resource of server.runtime_info?.resources || []) { keys.add(getMCPResourceKey(server, resource.uri)); } } return keys; }, [resourceServers]); const scopedSelectedMCPResources = selectedMCPResources.filter((resource) => { const server = scopedMCPServers.find( (item) => item.uuid === resource.server_uuid || (!resource.server_uuid && item.name === resource.server_name), ); return server ? availableMCPResourceKeys.has(getMCPResourceKey(server, resource.uri)) : false; }); const selectedTools = selectedToolNames .map((name: string) => availableTools.find((tool) => tool.name === name)) .filter((tool): tool is PluginTool => !!tool); const selectedKnowledgeBases = selectedKBIds .map((kbId: string) => knowledgeBases.find((base) => base.uuid === kbId)) .filter((base): base is KnowledgeBase => !!base); const sourceLabels = useMemo>( () => ({ builtin: t('pipelines.localAgent.builtinTools'), plugin: t('pipelines.localAgent.pluginTools'), mcp: t('pipelines.localAgent.mcpTools'), skill: t('pipelines.localAgent.skillTools'), }), [t], ); const availableToolGroups = useMemo( () => buildToolSourceGroups(availableTools, sourceLabels), [availableTools, sourceLabels], ); const selectedToolGroups = useMemo( () => buildToolSourceGroups(selectedTools, sourceLabels), [selectedTools, sourceLabels], ); const handleToggleToolMode = (checked: boolean) => { onChange({ 'enable-all-tools': checked }); }; const handleConfirmTools = () => { onChange({ tools: tempSelectedToolNames.filter((name) => availableToolNames.has(name), ), }); setToolsDialogOpen(false); }; const handleConfirmKnowledgeBases = () => { onChange({ 'knowledge-bases': tempSelectedKBIds }); setKbDialogOpen(false); }; const handleToggleMCPResource = ( server: MCPServer, resource: MCPResource, checked: boolean, ) => { const next = checked ? [ ...scopedSelectedMCPResources.filter( (item) => !isSameMCPResource(item, server, resource.uri), ), { server_uuid: server.uuid, server_name: server.name, uri: resource.uri, mode: 'pinned', enabled: true, }, ] : scopedSelectedMCPResources.filter( (item) => !isSameMCPResource(item, server, resource.uri), ); onChange({ 'mcp-resources': next }); }; const isMCPResourceSelected = (server: MCPServer, uri: string) => scopedSelectedMCPResources.some( (resource) => isSameMCPResource(resource, server, uri) && resource.enabled !== false, ); return (
{mode !== 'resources' && (

{t('pipelines.localAgent.toolsTitle')}

{t('pipelines.localAgent.toolsDescription')}

{enableAllTools ? (

{t('pipelines.localAgent.allToolsEnabled')}

) : selectedTools.length === 0 ? (

{t('pipelines.localAgent.noToolsSelected')}

) : (
{selectedToolGroups.map((sourceGroup) => (
{sourceGroup.label} {sourceGroup.total}
{sourceGroup.groups.map((providerGroup) => (
{providerGroup.name} {providerGroup.tools.length}
{providerGroup.tools.map((tool) => (
{tool.name}
{tool.human_desc && (
{tool.human_desc}
)}
))}
))}
))}
)}
)} {mode !== 'tools' && (

{t('pipelines.localAgent.resourcesTitle')}

{t('pipelines.localAgent.resourcesDescription')}

{t('pipelines.localAgent.knowledgeBases')}
{selectedKnowledgeBases.length === 0 ? (

{t('knowledge.noKnowledgeBaseSelected')}

) : (
{selectedKnowledgeBases.map((base) => (
{base.emoji && {base.emoji}} {base.name} {base.knowledge_engine?.name && ( {extractI18nObject(base.knowledge_engine.name)} )}
{base.description && (
{base.description}
)}
))}
)}
{t('pipelines.localAgent.mcpResources')}
onChange({ 'mcp-resource-agent-read-enabled': checked }) } />
{resourceServers.length === 0 ? (

{t('pipelines.localAgent.noMCPResourcesAvailable')}

) : (
{resourceServers.map((server) => (
{server.name} {server.runtime_info?.resources?.length || 0}
{(server.runtime_info?.resources || []).map( (resource) => ( ), )}
))}
)}
)} {mode !== 'resources' && ( {t('pipelines.localAgent.selectTools')}
{availableToolGroups.map((sourceGroup) => { return (
{sourceGroup.label} {sourceGroup.key === 'mcp' && ( )} {sourceGroup.key === 'skill' && ( )} {sourceGroup.total}
{sourceGroup.groups.map((providerGroup) => (
{providerGroup.name} {providerGroup.tools.length}
{providerGroup.tools.map((tool) => { const selected = tempSelectedToolNames.includes( tool.name, ); return (
setTempSelectedToolNames((prev) => prev.includes(tool.name) ? prev.filter( (name) => name !== tool.name, ) : [...prev, tool.name], ) } >
{tool.name}
{tool.human_desc && (
{tool.human_desc}
)}
); })}
))}
); })} {availableToolGroups.length === 0 && (

{t('pipelines.localAgent.noToolsSelected')}

)}
)} {mode !== 'tools' && ( {t('pipelines.localAgent.selectKnowledgeBases')}
{knowledgeBases.map((base) => { const kbId = base.uuid || ''; const selected = tempSelectedKBIds.includes(kbId); return (
setTempSelectedKBIds((prev) => prev.includes(kbId) ? prev.filter((id) => id !== kbId) : [...prev, kbId], ) } >
{base.emoji && {base.emoji}} {base.name}
{base.description && (
{base.description}
)}
); })} {knowledgeBases.length === 0 && (

{t('knowledge.noKnowledgeBaseSelected')}

)}
)}
); }