mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
✂️ refactor: MCP UI Separation for Agents (#9237)
* refactor: MCP UI Separation for Agents (Dustin WIP)
feat: separate MCPs into their own lists away from tools + actions and add the status indicator functionality from chat to their dropdown ui
fix: spotify mcp was not persisting on agent creation
feat: show disconnected saved servers and their tools in agent mcp list in created agents
fix: select-all regression fixed (caused by deleting tools we were drawing from for rendering list)
fix: dont show all mcps, only those installed in agent in list
feat: separate ToolSelectDialog for MCPServerTools
fix: uninitialized mcp servers not showing as added in toolselectdialog
refactor: reduce looping in AgentPanelContext for categorizing groups and mcps
refactor: split ToolSelectDialog and MCPToolSelectDialog functionality (still needs customization for custom user vars)
chore: address ESLint comments
chore: address ESLint comments
feat: one-click initialization on MCP servers in agent builder
fix: stop propagation triggering reinit on caret click
refactor: split uninitialized MCPs component from initialized MCPs
feat: new mcp tool select dialog ui with custom user vars
feat: show initialization state for CUV configurable MCPs too
chore: remove unused localization string
fix: deselecting all tools caused a re-render
fix: remove subtools so removal from MCPToolSelectDialog works more consistently
feat: added servers have all tools enabled by default
feat: mcp server list now alphabetical to prevent annoying ui behavior of servers jumping around depending on tool selection
fix: filter out placeholder group mcp tools from any actual tool calls / definitions
feat: indicator now takes you to config dialog for uninitialized servers
feat: show previously configured mcp servers that are now missing from the yaml
feat: select all enabled by default on first add to mcp server list
chore: address ESLint comments
* refactor: MCP UI Separation for Agents (Danny WIP)
chore: remove use of `{serverName}_mcp_{serverName}`
chore: import order
WIP: separate component concerns
refactor: streamline agent mcp tools
refactor: unify MCP server handling and improve tool visibility logic, remove unnecessary normalization or sorting, remove nesting button, make variable names clear
refactor: rename mcpServerIds to mcpServerNames for clarity and consistency across components
refactor: remove groupedMCPTools and toolToServerMap, streamline MCP server handling in context and components to effectively utilize mcpServersMap
refactor: optimize tool selection logic by replacing array includes with Set for improved performance
chore: add error logging for failed auth URL parsing in ToolCall component
refactor: enhance MCP tool handling by improving server name management and updating UI elements for better clarity
* refactor: decouple connection status from useMCPServerManager with useMCPConnectionStatus
* fix: improve MCP tool validation logic to handle unconfigured servers
* chore: enhance log message clarity for MCP server disconnection in updateUserPluginsController
* refactor: simplify connection status extraction in useMCPConnectionStatus hook
* refactor: improve initializing UX
* chore: replace string literal with ResourceType constant in useResourcePermissions
* refactor: cleanup code, remove redundancies, rename variables for clarity
* chore: add back filtering and sorting for mcp tools dialog
* refactor: initializeServer to return response and early return
* refactor: enhance server initialization logic and improve UI for OAuth interaction
* chore: clarify warning message for unconfigured MCP server in handleTools
* refactor: prevent CustomUserVarsSection from submitting tools dialog form
* fix: nested button of button issue in UninitializedMCPTool
* feat: add functionality to revoke custom user variables in MCPToolSelectDialog
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
d16f93b5f7
commit
49e8443ec5
30 changed files with 1589 additions and 180 deletions
|
|
@ -312,6 +312,16 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
|
||||||
continue;
|
continue;
|
||||||
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
|
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
|
||||||
const [toolName, serverName] = tool.split(Constants.mcp_delimiter);
|
const [toolName, serverName] = tool.split(Constants.mcp_delimiter);
|
||||||
|
if (toolName === Constants.mcp_server) {
|
||||||
|
/** Placeholder used for UI purposes */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (serverName && options.req?.config?.mcpConfig?.[serverName] == null) {
|
||||||
|
logger.warn(
|
||||||
|
`MCP server "${serverName}" for "${toolName}" tool is not configured${agent?.id != null && agent.id ? ` but attached to "${agent.id}"` : ''}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (toolName === Constants.mcp_all) {
|
if (toolName === Constants.mcp_all) {
|
||||||
const currentMCPGenerator = async (index) =>
|
const currentMCPGenerator = async (index) =>
|
||||||
createMCPTools({
|
createMCPTools({
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ const updateUserPluginsController = async (req, res) => {
|
||||||
// Extract server name from pluginKey (format: "mcp_<serverName>")
|
// Extract server name from pluginKey (format: "mcp_<serverName>")
|
||||||
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
|
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
|
||||||
logger.info(
|
logger.info(
|
||||||
`[updateUserPluginsController] Disconnecting MCP server ${serverName} for user ${user.id} after plugin auth update for ${pluginKey}.`,
|
`[updateUserPluginsController] Attempting disconnect of MCP server "${serverName}" for user ${user.id} after plugin auth update.`,
|
||||||
);
|
);
|
||||||
await mcpManager.disconnectUserConnection(user.id, serverName);
|
await mcpManager.disconnectUserConnection(user.id, serverName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const { logger } = require('@librechat/data-schemas');
|
||||||
const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api');
|
const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Tools,
|
Tools,
|
||||||
|
Constants,
|
||||||
SystemRoles,
|
SystemRoles,
|
||||||
FileSources,
|
FileSources,
|
||||||
ResourceType,
|
ResourceType,
|
||||||
|
|
@ -69,9 +70,9 @@ const createAgentHandler = async (req, res) => {
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
if (availableTools[tool]) {
|
if (availableTools[tool]) {
|
||||||
agentData.tools.push(tool);
|
agentData.tools.push(tool);
|
||||||
}
|
} else if (systemTools[tool]) {
|
||||||
|
agentData.tools.push(tool);
|
||||||
if (systemTools[tool]) {
|
} else if (tool.includes(Constants.mcp_delimiter)) {
|
||||||
agentData.tools.push(tool);
|
agentData.tools.push(tool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,7 @@ async function createMCPTool({
|
||||||
availableTools: tools,
|
availableTools: tools,
|
||||||
}) {
|
}) {
|
||||||
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
|
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
|
||||||
|
|
||||||
const availableTools =
|
const availableTools =
|
||||||
tools ?? (await getCachedTools({ userId: req.user?.id, includeGlobal: true }));
|
tools ?? (await getCachedTools({ userId: req.user?.id, includeGlobal: true }));
|
||||||
/** @type {LCTool | undefined} */
|
/** @type {LCTool | undefined} */
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import React, { createContext, useContext, useState } from 'react';
|
import React, { createContext, useContext, useState, useMemo } from 'react';
|
||||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { MCP, Action, TPlugin, AgentToolType } from 'librechat-data-provider';
|
import type { MCP, Action, TPlugin, AgentToolType } from 'librechat-data-provider';
|
||||||
import type { AgentPanelContextType } from '~/common';
|
import type { AgentPanelContextType, MCPServerInfo } from '~/common';
|
||||||
import { useAvailableToolsQuery, useGetActionsQuery } from '~/data-provider';
|
import { useAvailableToolsQuery, useGetActionsQuery, useGetStartupConfig } from '~/data-provider';
|
||||||
import { useLocalize, useGetAgentsConfig } from '~/hooks';
|
import { useLocalize, useGetAgentsConfig, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
|
type GroupedToolType = AgentToolType & { tools?: AgentToolType[] };
|
||||||
|
type GroupedToolsRecord = Record<string, GroupedToolType>;
|
||||||
|
|
||||||
const AgentPanelContext = createContext<AgentPanelContextType | undefined>(undefined);
|
const AgentPanelContext = createContext<AgentPanelContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function useAgentPanelContext() {
|
export function useAgentPanelContext() {
|
||||||
|
|
@ -33,67 +36,116 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
enabled: !!agent_id,
|
enabled: !!agent_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tools =
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
pluginTools?.map((tool) => ({
|
const mcpServerNames = useMemo(
|
||||||
tool_id: tool.pluginKey,
|
() => Object.keys(startupConfig?.mcpServers ?? {}),
|
||||||
metadata: tool as TPlugin,
|
[startupConfig],
|
||||||
agent_id: agent_id || '',
|
);
|
||||||
})) || [];
|
|
||||||
|
const { connectionStatus } = useMCPConnectionStatus({
|
||||||
|
enabled: !!agent_id && mcpServerNames.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedData = useMemo(() => {
|
||||||
|
if (!pluginTools) {
|
||||||
|
return {
|
||||||
|
tools: [],
|
||||||
|
groupedTools: {},
|
||||||
|
mcpServersMap: new Map<string, MCPServerInfo>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools: AgentToolType[] = [];
|
||||||
|
const groupedTools: GroupedToolsRecord = {};
|
||||||
|
|
||||||
|
const configuredServers = new Set(mcpServerNames);
|
||||||
|
const mcpServersMap = new Map<string, MCPServerInfo>();
|
||||||
|
|
||||||
|
for (const pluginTool of pluginTools) {
|
||||||
|
const tool: AgentToolType = {
|
||||||
|
tool_id: pluginTool.pluginKey,
|
||||||
|
metadata: pluginTool as TPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
tools.push(tool);
|
||||||
|
|
||||||
const groupedTools = tools?.reduce(
|
|
||||||
(acc, tool) => {
|
|
||||||
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
||||||
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
||||||
const groupKey = `${serverName.toLowerCase()}`;
|
|
||||||
if (!acc[groupKey]) {
|
if (!mcpServersMap.has(serverName)) {
|
||||||
acc[groupKey] = {
|
const metadata = {
|
||||||
tool_id: groupKey,
|
name: serverName,
|
||||||
metadata: {
|
pluginKey: serverName,
|
||||||
name: `${serverName}`,
|
|
||||||
pluginKey: groupKey,
|
|
||||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||||
icon: tool.metadata.icon || '',
|
icon: pluginTool.icon || '',
|
||||||
} as TPlugin,
|
} as TPlugin;
|
||||||
agent_id: agent_id || '',
|
|
||||||
|
mcpServersMap.set(serverName, {
|
||||||
|
serverName,
|
||||||
tools: [],
|
tools: [],
|
||||||
};
|
isConfigured: configuredServers.has(serverName),
|
||||||
}
|
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
||||||
acc[groupKey].tools?.push({
|
metadata,
|
||||||
tool_id: tool.tool_id,
|
|
||||||
metadata: tool.metadata,
|
|
||||||
agent_id: agent_id || '',
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpServersMap.get(serverName)!.tools.push(tool);
|
||||||
} else {
|
} else {
|
||||||
acc[tool.tool_id] = {
|
// Non-MCP tool
|
||||||
|
groupedTools[tool.tool_id] = {
|
||||||
tool_id: tool.tool_id,
|
tool_id: tool.tool_id,
|
||||||
metadata: tool.metadata,
|
metadata: tool.metadata,
|
||||||
agent_id: agent_id || '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return acc;
|
}
|
||||||
},
|
|
||||||
{} as Record<string, AgentToolType & { tools?: AgentToolType[] }>,
|
for (const mcpServerName of mcpServerNames) {
|
||||||
);
|
if (mcpServersMap.has(mcpServerName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const metadata = {
|
||||||
|
icon: '',
|
||||||
|
name: mcpServerName,
|
||||||
|
pluginKey: mcpServerName,
|
||||||
|
description: `${localize('com_ui_tool_collection_prefix')} ${mcpServerName}`,
|
||||||
|
} as TPlugin;
|
||||||
|
|
||||||
|
mcpServersMap.set(mcpServerName, {
|
||||||
|
tools: [],
|
||||||
|
metadata,
|
||||||
|
isConfigured: true,
|
||||||
|
serverName: mcpServerName,
|
||||||
|
isConnected: connectionStatus?.[mcpServerName]?.connectionState === 'connected',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
groupedTools,
|
||||||
|
mcpServersMap,
|
||||||
|
};
|
||||||
|
}, [pluginTools, localize, mcpServerNames, connectionStatus]);
|
||||||
|
|
||||||
const { agentsConfig, endpointsConfig } = useGetAgentsConfig();
|
const { agentsConfig, endpointsConfig } = useGetAgentsConfig();
|
||||||
|
|
||||||
const value: AgentPanelContextType = {
|
const value: AgentPanelContextType = {
|
||||||
mcp,
|
mcp,
|
||||||
mcps,
|
mcps,
|
||||||
/** Query data for actions and tools */
|
|
||||||
tools,
|
|
||||||
action,
|
action,
|
||||||
setMcp,
|
setMcp,
|
||||||
actions,
|
actions,
|
||||||
setMcps,
|
setMcps,
|
||||||
agent_id,
|
agent_id,
|
||||||
setAction,
|
setAction,
|
||||||
|
pluginTools,
|
||||||
activePanel,
|
activePanel,
|
||||||
groupedTools,
|
|
||||||
agentsConfig,
|
agentsConfig,
|
||||||
setActivePanel,
|
setActivePanel,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
setCurrentAgentId,
|
setCurrentAgentId,
|
||||||
|
tools: processedData.tools,
|
||||||
|
groupedTools: processedData.groupedTools,
|
||||||
|
mcpServersMap: processedData.mcpServersMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AgentPanelContext.Provider value={value}>{children}</AgentPanelContext.Provider>;
|
return <AgentPanelContext.Provider value={value}>{children}</AgentPanelContext.Provider>;
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,14 @@ export type AgentPanelProps = {
|
||||||
agentsConfig?: t.TAgentsEndpoint | null;
|
agentsConfig?: t.TAgentsEndpoint | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MCPServerInfo {
|
||||||
|
serverName: string;
|
||||||
|
tools: t.AgentToolType[];
|
||||||
|
isConfigured: boolean;
|
||||||
|
isConnected: boolean;
|
||||||
|
metadata: t.TPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
export type AgentPanelContextType = {
|
export type AgentPanelContextType = {
|
||||||
action?: t.Action;
|
action?: t.Action;
|
||||||
actions?: t.Action[];
|
actions?: t.Action[];
|
||||||
|
|
@ -225,13 +233,16 @@ export type AgentPanelContextType = {
|
||||||
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||||
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
||||||
tools: t.AgentToolType[];
|
|
||||||
activePanel?: string;
|
activePanel?: string;
|
||||||
|
tools: t.AgentToolType[];
|
||||||
|
pluginTools?: t.TPlugin[];
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
agentsConfig?: t.TAgentsEndpoint | null;
|
agentsConfig?: t.TAgentsEndpoint | null;
|
||||||
endpointsConfig?: t.TEndpointsConfig | null;
|
endpointsConfig?: t.TEndpointsConfig | null;
|
||||||
|
/** Pre-computed MCP server information indexed by server key */
|
||||||
|
mcpServersMap: Map<string, MCPServerInfo>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentModelPanelProps = {
|
export type AgentModelPanelProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useCallback } from 'react';
|
||||||
import { MultiSelect, MCPIcon } from '@librechat/client';
|
import { MultiSelect, MCPIcon } from '@librechat/client';
|
||||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
|
||||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||||
import { useBadgeRowContext } from '~/Providers';
|
import { useBadgeRowContext } from '~/Providers';
|
||||||
|
import { useMCPServerManager } from '~/hooks';
|
||||||
|
|
||||||
type MCPSelectProps = { conversationId?: string | null };
|
type MCPSelectProps = { conversationId?: string | null };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import * as Ariakit from '@ariakit/react';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { PinIcon, MCPIcon } from '@librechat/client';
|
import { PinIcon, MCPIcon } from '@librechat/client';
|
||||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
|
||||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||||
|
import { useMCPServerManager } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface MCPSubMenuProps {
|
interface MCPSubMenuProps {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@ export default function ToolCall({
|
||||||
const url = new URL(authURL);
|
const url = new URL(authURL);
|
||||||
return url.hostname;
|
return url.hostname;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
'client/src/components/Chat/Messages/Content/ToolCall.tsx - Failed to parse auth URL',
|
||||||
|
e,
|
||||||
|
);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}, [auth]);
|
}, [auth]);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ interface CustomUserVarsSectionProps {
|
||||||
onRevoke: () => void;
|
onRevoke: () => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthFieldProps {
|
interface AuthFieldProps {
|
||||||
name: string;
|
name: string;
|
||||||
config: CustomUserVarConfig;
|
config: CustomUserVarConfig;
|
||||||
|
|
@ -69,7 +68,7 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
|
||||||
? localize('com_ui_mcp_update_var', { 0: config.title })
|
? localize('com_ui_mcp_update_var', { 0: config.title })
|
||||||
: localize('com_ui_mcp_enter_var', { 0: config.title })
|
: localize('com_ui_mcp_enter_var', { 0: config.title })
|
||||||
}
|
}
|
||||||
className="w-full shadow-sm sm:text-sm"
|
className="w-full rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary placeholder:text-text-secondary focus:outline-none sm:text-sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -79,23 +78,22 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomUserVarsSection({
|
export default function CustomUserVarsSection({
|
||||||
serverName,
|
|
||||||
fields,
|
fields,
|
||||||
onSave,
|
onSave,
|
||||||
onRevoke,
|
onRevoke,
|
||||||
|
serverName,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
}: CustomUserVarsSectionProps) {
|
}: CustomUserVarsSectionProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
// Fetch auth value flags for the server
|
|
||||||
const { data: authValuesData } = useMCPAuthValuesQuery(serverName, {
|
const { data: authValuesData } = useMCPAuthValuesQuery(serverName, {
|
||||||
enabled: !!serverName,
|
enabled: !!serverName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
reset,
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<Record<string, string>>({
|
} = useForm<Record<string, string>>({
|
||||||
defaultValues: useMemo(() => {
|
defaultValues: useMemo(() => {
|
||||||
|
|
@ -140,10 +138,20 @@ export default function CustomUserVarsSection({
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button onClick={handleRevokeClick} variant="destructive" disabled={isSubmitting}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={handleRevokeClick}
|
||||||
|
>
|
||||||
{localize('com_ui_revoke')}
|
{localize('com_ui_revoke')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit(onFormSubmit)} variant="submit" disabled={isSubmitting}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={handleSubmit(onFormSubmit)}
|
||||||
|
>
|
||||||
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
|
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { Button, Spinner } from '@librechat/client';
|
import { Button, Spinner } from '@librechat/client';
|
||||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
import { useLocalize, useMCPServerManager, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
|
||||||
interface ServerInitializationSectionProps {
|
interface ServerInitializationSectionProps {
|
||||||
sidePanel?: boolean;
|
sidePanel?: boolean;
|
||||||
|
|
@ -21,16 +21,15 @@ export default function ServerInitializationSection({
|
||||||
}: ServerInitializationSectionProps) {
|
}: ServerInitializationSectionProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const {
|
const { initializeServer, cancelOAuthFlow, isInitializing, isCancellable, getOAuthUrl } =
|
||||||
initializeServer,
|
useMCPServerManager({ conversationId });
|
||||||
connectionStatus,
|
|
||||||
cancelOAuthFlow,
|
|
||||||
isInitializing,
|
|
||||||
isCancellable,
|
|
||||||
getOAuthUrl,
|
|
||||||
} = useMCPServerManager({ conversationId });
|
|
||||||
|
|
||||||
const serverStatus = connectionStatus[serverName];
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const { connectionStatus } = useMCPConnectionStatus({
|
||||||
|
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverStatus = connectionStatus?.[serverName];
|
||||||
const isConnected = serverStatus?.connectionState === 'connected';
|
const isConnected = serverStatus?.connectionState === 'connected';
|
||||||
const canCancel = isCancellable(serverName);
|
const canCancel = isCancellable(serverName);
|
||||||
const isServerInitializing = isInitializing(serverName);
|
const isServerInitializing = isInitializing(serverName);
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,23 @@ import {
|
||||||
getIconKey,
|
getIconKey,
|
||||||
cn,
|
cn,
|
||||||
} from '~/utils';
|
} from '~/utils';
|
||||||
import { useFileMapContext, useAgentPanelContext } from '~/Providers';
|
import { ToolSelectDialog, MCPToolSelectDialog } from '~/components/Tools';
|
||||||
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
|
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
|
||||||
|
import { useFileMapContext, useAgentPanelContext } from '~/Providers';
|
||||||
import AgentCategorySelector from './AgentCategorySelector';
|
import AgentCategorySelector from './AgentCategorySelector';
|
||||||
import Action from '~/components/SidePanel/Builder/Action';
|
import Action from '~/components/SidePanel/Builder/Action';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
import { useLocalize, useVisibleTools } from '~/hooks';
|
||||||
import { useGetAgentFiles } from '~/data-provider';
|
import { useGetAgentFiles } from '~/data-provider';
|
||||||
import { icons } from '~/hooks/Endpoint/Icons';
|
import { icons } from '~/hooks/Endpoint/Icons';
|
||||||
import Instructions from './Instructions';
|
import Instructions from './Instructions';
|
||||||
import AgentAvatar from './AgentAvatar';
|
import AgentAvatar from './AgentAvatar';
|
||||||
import FileContext from './FileContext';
|
import FileContext from './FileContext';
|
||||||
import SearchForm from './Search/Form';
|
import SearchForm from './Search/Form';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import FileSearch from './FileSearch';
|
import FileSearch from './FileSearch';
|
||||||
import Artifacts from './Artifacts';
|
import Artifacts from './Artifacts';
|
||||||
import AgentTool from './AgentTool';
|
import AgentTool from './AgentTool';
|
||||||
import CodeForm from './Code/Form';
|
import CodeForm from './Code/Form';
|
||||||
|
import MCPTools from './MCPTools';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||||
|
|
@ -43,10 +44,12 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const methods = useFormContext<AgentForm>();
|
const methods = useFormContext<AgentForm>();
|
||||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||||
|
const [showMCPToolDialog, setShowMCPToolDialog] = useState(false);
|
||||||
const {
|
const {
|
||||||
actions,
|
actions,
|
||||||
setAction,
|
setAction,
|
||||||
agentsConfig,
|
agentsConfig,
|
||||||
|
mcpServersMap,
|
||||||
setActivePanel,
|
setActivePanel,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
groupedTools: allTools,
|
groupedTools: allTools,
|
||||||
|
|
@ -173,19 +176,7 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
Icon = icons[iconKey];
|
Icon = icons[iconKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine what to show
|
const { toolIds, mcpServerNames } = useVisibleTools(tools, allTools, mcpServersMap);
|
||||||
const selectedToolIds = tools ?? [];
|
|
||||||
const visibleToolIds = new Set(selectedToolIds);
|
|
||||||
|
|
||||||
// Check what group parent tools should be shown if any subtool is present
|
|
||||||
Object.entries(allTools ?? {}).forEach(([toolId, toolObj]) => {
|
|
||||||
if (toolObj.tools?.length) {
|
|
||||||
// if any subtool of this group is selected, ensure group parent tool rendered
|
|
||||||
if (toolObj.tools.some((st) => selectedToolIds.includes(st.tool_id))) {
|
|
||||||
visibleToolIds.add(toolId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -326,8 +317,8 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* // Render all visible IDs (including groups with subtools selected) */}
|
{/* Render all visible IDs (including groups with subtools selected) */}
|
||||||
{[...visibleToolIds].map((toolId, i) => {
|
{toolIds.map((toolId, i) => {
|
||||||
if (!allTools) return null;
|
if (!allTools) return null;
|
||||||
const tool = allTools[toolId];
|
const tool = allTools[toolId];
|
||||||
if (!tool) return null;
|
if (!tool) return null;
|
||||||
|
|
@ -385,8 +376,11 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* MCP Section */}
|
{/* MCP Section */}
|
||||||
{/* <MCPSection /> */}
|
<MCPTools
|
||||||
|
agentId={agent_id}
|
||||||
|
mcpServerNames={mcpServerNames}
|
||||||
|
setShowMCPToolDialog={setShowMCPToolDialog}
|
||||||
|
/>
|
||||||
{/* Support Contact (Optional) */}
|
{/* Support Contact (Optional) */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="mb-1.5 flex items-center gap-2">
|
<div className="mb-1.5 flex items-center gap-2">
|
||||||
|
|
@ -477,6 +471,13 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
setIsOpen={setShowToolDialog}
|
setIsOpen={setShowToolDialog}
|
||||||
endpoint={EModelEndpoint.agents}
|
endpoint={EModelEndpoint.agents}
|
||||||
/>
|
/>
|
||||||
|
<MCPToolSelectDialog
|
||||||
|
agentId={agent_id}
|
||||||
|
isOpen={showMCPToolDialog}
|
||||||
|
mcpServerNames={mcpServerNames}
|
||||||
|
setIsOpen={setShowMCPToolDialog}
|
||||||
|
endpoint={EModelEndpoint.agents}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Tools,
|
Tools,
|
||||||
Constants,
|
Constants,
|
||||||
SystemRoles,
|
SystemRoles,
|
||||||
|
ResourceType,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
PermissionBits,
|
PermissionBits,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
|
|
@ -53,7 +54,7 @@ export default function AgentPanel() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
|
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
|
||||||
'agent',
|
ResourceType.AGENT,
|
||||||
basicAgentQuery.data?._id || '',
|
basicAgentQuery.data?._id || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
368
client/src/components/SidePanel/Agents/MCPTool.tsx
Normal file
368
client/src/components/SidePanel/Agents/MCPTool.tsx
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import * as Ariakit from '@ariakit/react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import {
|
||||||
|
Label,
|
||||||
|
Checkbox,
|
||||||
|
OGDialog,
|
||||||
|
Accordion,
|
||||||
|
TrashIcon,
|
||||||
|
AccordionItem,
|
||||||
|
CircleHelpIcon,
|
||||||
|
OGDialogTrigger,
|
||||||
|
useToastContext,
|
||||||
|
AccordionContent,
|
||||||
|
OGDialogTemplate,
|
||||||
|
} from '@librechat/client';
|
||||||
|
import type { AgentForm, MCPServerInfo } from '~/common';
|
||||||
|
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||||
|
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||||
|
import { useLocalize, useMCPServerManager } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager();
|
||||||
|
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
const [accordionValue, setAccordionValue] = useState<string>('');
|
||||||
|
const [hoveredToolId, setHoveredToolId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!serverInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentServerName = serverInfo.serverName;
|
||||||
|
|
||||||
|
const getSelectedTools = () => {
|
||||||
|
if (!serverInfo?.tools) return [];
|
||||||
|
const formTools = getValues('tools') || [];
|
||||||
|
return serverInfo.tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFormTools = (newSelectedTools: string[]) => {
|
||||||
|
const currentTools = getValues('tools') || [];
|
||||||
|
const otherTools = currentTools.filter(
|
||||||
|
(t: string) => !serverInfo?.tools?.some((st) => st.tool_id === t),
|
||||||
|
);
|
||||||
|
setValue('tools', [...otherTools, ...newSelectedTools]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTool = (serverName: string) => {
|
||||||
|
if (!serverName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const currentTools = getValues('tools');
|
||||||
|
const remainingToolIds =
|
||||||
|
currentTools?.filter(
|
||||||
|
(currentToolId) =>
|
||||||
|
currentToolId !== serverName &&
|
||||||
|
!currentToolId.endsWith(`${Constants.mcp_delimiter}${serverName}`),
|
||||||
|
) || [];
|
||||||
|
setValue('tools', remainingToolIds);
|
||||||
|
showToast({ message: 'Tool deleted successfully', status: 'success' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedTools = getSelectedTools();
|
||||||
|
const isExpanded = accordionValue === currentServerName;
|
||||||
|
|
||||||
|
const statusIconProps = getServerStatusIconProps(currentServerName);
|
||||||
|
const configDialogProps = getConfigDialogProps();
|
||||||
|
|
||||||
|
const statusIcon = statusIconProps && (
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="cursor-pointer rounded p-0.5 hover:bg-surface-secondary"
|
||||||
|
>
|
||||||
|
<MCPServerStatusIcon {...statusIconProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog>
|
||||||
|
<Accordion type="single" value={accordionValue} onValueChange={setAccordionValue} collapsible>
|
||||||
|
<AccordionItem value={currentServerName} className="group relative w-full border-none">
|
||||||
|
<div
|
||||||
|
className="relative flex w-full items-center gap-1 rounded-lg p-1 hover:bg-surface-primary-alt"
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccordionPrimitive.Header asChild>
|
||||||
|
<div
|
||||||
|
className="flex grow cursor-pointer select-none items-center gap-1 rounded bg-transparent p-0 text-left transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1"
|
||||||
|
onClick={() =>
|
||||||
|
setAccordionValue((prev) => {
|
||||||
|
if (prev) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return currentServerName;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{statusIcon && <div className="flex items-center">{statusIcon}</div>}
|
||||||
|
|
||||||
|
{serverInfo.metadata.icon && (
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full">
|
||||||
|
<div
|
||||||
|
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${serverInfo.metadata.icon})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="grow px-2 py-1.5"
|
||||||
|
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{currentServerName}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute right-0 transition-all duration-300',
|
||||||
|
isHovering || isFocused
|
||||||
|
? 'translate-x-0 opacity-100'
|
||||||
|
: 'translate-x-8 opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
data-checkbox-container
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="mt-1"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`select-all-${currentServerName}`}
|
||||||
|
checked={
|
||||||
|
selectedTools.length === serverInfo.tools?.length &&
|
||||||
|
selectedTools.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (serverInfo.tools) {
|
||||||
|
const newSelectedTools = checked
|
||||||
|
? serverInfo.tools.map((t) => t.tool_id)
|
||||||
|
: [
|
||||||
|
`${Constants.mcp_server}${Constants.mcp_delimiter}${currentServerName}`,
|
||||||
|
];
|
||||||
|
updateFormTools(newSelectedTools);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 rounded border border-border-medium transition-all duration-200 hover:border-border-heavy',
|
||||||
|
isExpanded ? 'visible' : 'pointer-events-none invisible',
|
||||||
|
)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const checkbox = e.currentTarget as HTMLButtonElement;
|
||||||
|
checkbox.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={isExpanded ? 0 : -1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* Caret button for accordion */}
|
||||||
|
<AccordionPrimitive.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-7 items-center justify-center rounded transition-colors duration-200 hover:bg-surface-active-alt focus:translate-x-0 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||||
|
isExpanded && 'bg-surface-active-alt',
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 transition-transform duration-200',
|
||||||
|
isExpanded && 'rotate-180',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-7 items-center justify-center rounded transition-colors duration-200',
|
||||||
|
'hover:bg-surface-active-alt focus:translate-x-0 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||||
|
)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
aria-label={`Delete ${currentServerName}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccordionContent className="relative ml-1 pt-1 before:absolute before:bottom-2 before:left-0 before:top-0 before:w-0.5 before:bg-border-medium">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{serverInfo.tools?.map((subTool) => (
|
||||||
|
<label
|
||||||
|
key={subTool.tool_id}
|
||||||
|
htmlFor={subTool.tool_id}
|
||||||
|
className={cn(
|
||||||
|
'border-token-border-light hover:bg-token-surface-secondary flex cursor-pointer items-center rounded-lg border p-2',
|
||||||
|
'ml-2 mr-1 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background',
|
||||||
|
)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHoveredToolId(subTool.tool_id)}
|
||||||
|
onMouseLeave={() => setHoveredToolId(null)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={subTool.tool_id}
|
||||||
|
checked={selectedTools.includes(subTool.tool_id)}
|
||||||
|
onCheckedChange={(_checked) => {
|
||||||
|
const newSelectedTools = selectedTools.includes(subTool.tool_id)
|
||||||
|
? selectedTools.filter((t) => t !== subTool.tool_id)
|
||||||
|
: [...selectedTools, subTool.tool_id];
|
||||||
|
updateFormTools(newSelectedTools);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
const checkbox = e.currentTarget as HTMLButtonElement;
|
||||||
|
checkbox.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className={cn(
|
||||||
|
'relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer rounded border border-border-medium transition-[border-color] duration-200 hover:border-border-heavy focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-token-text-primary select-none">
|
||||||
|
{subTool.metadata.name}
|
||||||
|
</span>
|
||||||
|
{subTool.metadata.description && (
|
||||||
|
<Ariakit.HovercardProvider placement="left-start">
|
||||||
|
<div className="ml-auto flex h-6 w-6 items-center justify-center">
|
||||||
|
<Ariakit.HovercardAnchor
|
||||||
|
render={
|
||||||
|
<Ariakit.Button
|
||||||
|
className={cn(
|
||||||
|
'flex h-5 w-5 cursor-help items-center rounded-full text-text-secondary transition-opacity duration-200',
|
||||||
|
hoveredToolId === subTool.tool_id ? 'opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
|
aria-label={localize('com_ui_tool_info')}
|
||||||
|
>
|
||||||
|
<CircleHelpIcon className="h-4 w-4" />
|
||||||
|
<Ariakit.VisuallyHidden>
|
||||||
|
{localize('com_ui_tool_info')}
|
||||||
|
</Ariakit.VisuallyHidden>
|
||||||
|
</Ariakit.Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Ariakit.HovercardDisclosure
|
||||||
|
className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
aria-label={localize('com_ui_tool_more_info')}
|
||||||
|
aria-expanded={hoveredToolId === subTool.tool_id}
|
||||||
|
aria-controls={`tool-description-${subTool.tool_id}`}
|
||||||
|
>
|
||||||
|
<Ariakit.VisuallyHidden>
|
||||||
|
{localize('com_ui_tool_more_info')}
|
||||||
|
</Ariakit.VisuallyHidden>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Ariakit.HovercardDisclosure>
|
||||||
|
</div>
|
||||||
|
<Ariakit.Hovercard
|
||||||
|
id={`tool-description-${subTool.tool_id}`}
|
||||||
|
gutter={14}
|
||||||
|
shift={40}
|
||||||
|
flip={false}
|
||||||
|
className="z-[999] w-80 scale-95 rounded-2xl border border-border-medium bg-surface-secondary p-4 text-text-primary opacity-0 shadow-md transition-all duration-200 data-[enter]:scale-100 data-[leave]:scale-95 data-[enter]:opacity-100 data-[leave]:opacity-0"
|
||||||
|
portal={true}
|
||||||
|
unmountOnHide={true}
|
||||||
|
role="tooltip"
|
||||||
|
aria-label={subTool.metadata.description}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
{subTool.metadata.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Ariakit.Hovercard>
|
||||||
|
</Ariakit.HovercardProvider>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_tool')}
|
||||||
|
mainClassName="px-0"
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_tool_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: () => removeTool(currentServerName),
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{configDialogProps && <MCPConfigDialog {...configDialogProps} />}
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
client/src/components/SidePanel/Agents/MCPTools.tsx
Normal file
71
client/src/components/SidePanel/Agents/MCPTools.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import UninitializedMCPTool from './UninitializedMCPTool';
|
||||||
|
import UnconfiguredMCPTool from './UnconfiguredMCPTool';
|
||||||
|
import { useAgentPanelContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import MCPTool from './MCPTool';
|
||||||
|
|
||||||
|
export default function MCPTools({
|
||||||
|
agentId,
|
||||||
|
mcpServerNames,
|
||||||
|
setShowMCPToolDialog,
|
||||||
|
}: {
|
||||||
|
agentId: string;
|
||||||
|
mcpServerNames?: string[];
|
||||||
|
setShowMCPToolDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { mcpServersMap } = useAgentPanelContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="text-token-text-primary mb-2 block font-medium">
|
||||||
|
{localize('com_ui_mcp_servers')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<div className="mb-1">
|
||||||
|
{/* Render servers with selected tools */}
|
||||||
|
{mcpServerNames?.map((mcpServerName) => {
|
||||||
|
const serverInfo = mcpServersMap.get(mcpServerName);
|
||||||
|
if (!serverInfo?.isConfigured) {
|
||||||
|
return (
|
||||||
|
<UnconfiguredMCPTool
|
||||||
|
key={`${mcpServerName}-${agentId}`}
|
||||||
|
serverName={mcpServerName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!serverInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverInfo.isConnected) {
|
||||||
|
return (
|
||||||
|
<MCPTool key={`${serverInfo.serverName}-${agentId}`} serverInfo={serverInfo} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UninitializedMCPTool
|
||||||
|
key={`${serverInfo.serverName}-${agentId}`}
|
||||||
|
serverInfo={serverInfo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMCPToolDialog(true)}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{localize('com_assistants_add_mcp_server_tools')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
client/src/components/SidePanel/Agents/UnconfiguredMCPTool.tsx
Normal file
127
client/src/components/SidePanel/Agents/UnconfiguredMCPTool.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { CircleX } from 'lucide-react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import {
|
||||||
|
Label,
|
||||||
|
OGDialog,
|
||||||
|
TrashIcon,
|
||||||
|
useToastContext,
|
||||||
|
OGDialogTrigger,
|
||||||
|
OGDialogTemplate,
|
||||||
|
} from '@librechat/client';
|
||||||
|
import type { AgentForm } from '~/common';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
export default function UnconfiguredMCPTool({ serverName }: { serverName?: string }) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
if (!serverName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTool = () => {
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_delete_tool_error', { error: String(error) }),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const currentTools = getValues('tools');
|
||||||
|
const remainingToolIds =
|
||||||
|
currentTools?.filter(
|
||||||
|
(currentToolId) =>
|
||||||
|
currentToolId !== serverName &&
|
||||||
|
!currentToolId.endsWith(`${Constants.mcp_delimiter}${serverName}`),
|
||||||
|
) || [];
|
||||||
|
setValue('tools', remainingToolIds);
|
||||||
|
showToast({ message: localize('com_ui_delete_tool_success'), status: 'success' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog>
|
||||||
|
<div
|
||||||
|
className="group relative flex w-full items-center gap-1 rounded-lg p-1 text-sm hover:bg-surface-primary-alt"
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded p-1">
|
||||||
|
<CircleX className="h-4 w-4 text-red-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex grow cursor-not-allowed items-center gap-1 rounded bg-transparent p-0 text-left transition-colors">
|
||||||
|
<div
|
||||||
|
className="grow select-none px-2 py-1.5"
|
||||||
|
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{serverName}
|
||||||
|
<span className="ml-2 text-xs text-text-secondary">
|
||||||
|
{' - '}
|
||||||
|
{localize('com_ui_unavailable')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-7 items-center justify-center rounded transition-all duration-200 hover:bg-surface-active-alt focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||||
|
isHovering || isFocused ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||||
|
)}
|
||||||
|
aria-label={`Delete ${serverName}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
</div>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_tool')}
|
||||||
|
mainClassName="px-0"
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_tool_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: () => removeTool(),
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
client/src/components/SidePanel/Agents/UninitializedMCPTool.tsx
Normal file
183
client/src/components/SidePanel/Agents/UninitializedMCPTool.tsx
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import {
|
||||||
|
Label,
|
||||||
|
OGDialog,
|
||||||
|
TrashIcon,
|
||||||
|
OGDialogTrigger,
|
||||||
|
useToastContext,
|
||||||
|
OGDialogTemplate,
|
||||||
|
} from '@librechat/client';
|
||||||
|
import type { AgentForm, MCPServerInfo } from '~/common';
|
||||||
|
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||||
|
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||||
|
import { useLocalize, useMCPServerManager } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
export default function UninitializedMCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
const { initializeServer, isInitializing, getServerStatusIconProps, getConfigDialogProps } =
|
||||||
|
useMCPServerManager();
|
||||||
|
|
||||||
|
if (!serverInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTool = (serverName: string) => {
|
||||||
|
if (!serverName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_delete_tool_error', { error: String(error) }),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const currentTools = getValues('tools');
|
||||||
|
const remainingToolIds =
|
||||||
|
currentTools?.filter(
|
||||||
|
(currentToolId) =>
|
||||||
|
currentToolId !== serverName &&
|
||||||
|
!currentToolId.endsWith(`${Constants.mcp_delimiter}${serverName}`),
|
||||||
|
) || [];
|
||||||
|
setValue('tools', remainingToolIds);
|
||||||
|
showToast({ message: localize('com_ui_delete_tool_success'), status: 'success' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverName = serverInfo.serverName;
|
||||||
|
const isServerInitializing = isInitializing(serverName);
|
||||||
|
const statusIconProps = getServerStatusIconProps(serverName);
|
||||||
|
const configDialogProps = getConfigDialogProps();
|
||||||
|
|
||||||
|
const statusIcon = statusIconProps && (
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="cursor-pointer rounded p-0.5 hover:bg-surface-secondary"
|
||||||
|
>
|
||||||
|
<MCPServerStatusIcon {...statusIconProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog>
|
||||||
|
<div
|
||||||
|
className="group relative flex w-full items-center gap-1 rounded-lg p-1 text-sm hover:bg-surface-primary-alt"
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex grow cursor-pointer items-center gap-1 rounded bg-transparent p-0 text-left transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest('[data-status-icon]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isServerInitializing) {
|
||||||
|
initializeServer(serverName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isServerInitializing) {
|
||||||
|
initializeServer(serverName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-disabled={isServerInitializing}
|
||||||
|
>
|
||||||
|
{statusIcon && (
|
||||||
|
<div className="flex items-center" data-status-icon>
|
||||||
|
{statusIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{serverInfo.metadata.icon && (
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full">
|
||||||
|
<div
|
||||||
|
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${serverInfo.metadata.icon})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="grow px-2 py-1.5"
|
||||||
|
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{serverName}
|
||||||
|
{isServerInitializing && (
|
||||||
|
<span className="ml-2 text-xs text-text-secondary">
|
||||||
|
{localize('com_ui_initializing')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-7 items-center justify-center rounded transition-all duration-200 hover:bg-surface-active-alt focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||||
|
isHovering || isFocused ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||||
|
)}
|
||||||
|
aria-label={`Delete ${serverName}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
</div>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_tool')}
|
||||||
|
mainClassName="px-0"
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_tool_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: () => removeTool(serverName),
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{configDialogProps && <MCPConfigDialog {...configDialogProps} />}
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,11 @@ import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
||||||
import ServerInitializationSection from '~/components/MCP/ServerInitializationSection';
|
import ServerInitializationSection from '~/components/MCP/ServerInitializationSection';
|
||||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
|
||||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||||
import { MCPPanelProvider, useMCPPanelContext } from '~/Providers';
|
import { MCPPanelProvider, useMCPPanelContext } from '~/Providers';
|
||||||
|
import { useLocalize, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
function MCPPanelContent() {
|
function MCPPanelContent() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -19,7 +18,10 @@ function MCPPanelContent() {
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { conversationId } = useMCPPanelContext();
|
const { conversationId } = useMCPPanelContext();
|
||||||
const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig();
|
const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig();
|
||||||
const { data: connectionStatusData } = useMCPConnectionStatusQuery();
|
const { connectionStatus } = useMCPConnectionStatus({
|
||||||
|
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
@ -57,11 +59,6 @@ function MCPPanelContent() {
|
||||||
}));
|
}));
|
||||||
}, [startupConfig?.mcpServers]);
|
}, [startupConfig?.mcpServers]);
|
||||||
|
|
||||||
const connectionStatus = useMemo(
|
|
||||||
() => connectionStatusData?.connectionStatus || {},
|
|
||||||
[connectionStatusData?.connectionStatus],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleServerClickToEdit = (serverName: string) => {
|
const handleServerClickToEdit = (serverName: string) => {
|
||||||
setSelectedServerNameForEditing(serverName);
|
setSelectedServerNameForEditing(serverName);
|
||||||
};
|
};
|
||||||
|
|
@ -125,7 +122,7 @@ function MCPPanelContent() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverStatus = connectionStatus[selectedServerNameForEditing];
|
const serverStatus = connectionStatus?.[selectedServerNameForEditing];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto max-w-full space-y-4 overflow-x-hidden py-2">
|
<div className="h-auto max-w-full space-y-4 overflow-x-hidden py-2">
|
||||||
|
|
@ -170,7 +167,7 @@ function MCPPanelContent() {
|
||||||
<div className="h-auto max-w-full overflow-x-hidden py-2">
|
<div className="h-auto max-w-full overflow-x-hidden py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mcpServerDefinitions.map((server) => {
|
{mcpServerDefinitions.map((server) => {
|
||||||
const serverStatus = connectionStatus[server.serverName];
|
const serverStatus = connectionStatus?.[server.serverName];
|
||||||
const isConnected = serverStatus?.connectionState === 'connected';
|
const isConnected = serverStatus?.connectionState === 'connected';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
116
client/src/components/Tools/MCPToolItem.tsx
Normal file
116
client/src/components/Tools/MCPToolItem.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
|
||||||
|
import type { AgentToolType } from 'librechat-data-provider';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
type MCPToolItemProps = {
|
||||||
|
tool: AgentToolType;
|
||||||
|
onAddTool: () => void;
|
||||||
|
onRemoveTool: () => void;
|
||||||
|
isInstalled?: boolean;
|
||||||
|
isConfiguring?: boolean;
|
||||||
|
isInitializing?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function MCPToolItem({
|
||||||
|
tool,
|
||||||
|
onAddTool,
|
||||||
|
onRemoveTool,
|
||||||
|
isInstalled = false,
|
||||||
|
isConfiguring = false,
|
||||||
|
isInitializing = false,
|
||||||
|
}: MCPToolItemProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isInstalled) {
|
||||||
|
onRemoveTool();
|
||||||
|
} else {
|
||||||
|
onAddTool();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = tool.metadata?.name || tool.tool_id;
|
||||||
|
const description = tool.metadata?.description || '';
|
||||||
|
const icon = tool.metadata?.icon;
|
||||||
|
|
||||||
|
// Determine button state and text
|
||||||
|
const getButtonState = () => {
|
||||||
|
if (isInstalled) {
|
||||||
|
return {
|
||||||
|
text: localize('com_nav_tool_remove'),
|
||||||
|
icon: <XCircle className="flex h-4 w-4 items-center stroke-2" />,
|
||||||
|
className:
|
||||||
|
'btn relative bg-gray-300 hover:bg-gray-400 dark:bg-gray-50 dark:hover:bg-gray-200',
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfiguring) {
|
||||||
|
return {
|
||||||
|
text: localize('com_ui_confirm'),
|
||||||
|
icon: <PlusCircleIcon className="flex h-4 w-4 items-center stroke-2" />,
|
||||||
|
className: 'btn btn-primary relative',
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitializing) {
|
||||||
|
return {
|
||||||
|
text: localize('com_ui_initializing'),
|
||||||
|
icon: <Wrench className="flex h-4 w-4 items-center stroke-2" />,
|
||||||
|
className: 'btn btn-primary relative opacity-75 cursor-not-allowed',
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: localize('com_ui_add'),
|
||||||
|
icon: <PlusCircleIcon className="flex h-4 w-4 items-center stroke-2" />,
|
||||||
|
className: 'btn btn-primary relative',
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonState = getButtonState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="h-[70px] w-[70px] shrink-0">
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt={localize('com_ui_logo', { 0: name })}
|
||||||
|
className="h-full w-full rounded-[5px] bg-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center rounded-[5px] border border-border-medium bg-transparent">
|
||||||
|
<Wrench className="h-8 w-8 text-text-secondary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 rounded-[5px] ring-1 ring-inset ring-black/10"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-col items-start justify-between">
|
||||||
|
<div className="mb-2 line-clamp-1 max-w-full text-lg leading-5 text-text-primary">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={buttonState.className}
|
||||||
|
aria-label={`${buttonState.text} ${name}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={buttonState.disabled}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{buttonState.text}
|
||||||
|
{buttonState.icon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="line-clamp-3 h-[60px] text-sm text-text-secondary">{description}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MCPToolItem;
|
||||||
370
client/src/components/Tools/MCPToolSelectDialog.tsx
Normal file
370
client/src/components/Tools/MCPToolSelectDialog.tsx
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import type { TError, AgentToolType } from 'librechat-data-provider';
|
||||||
|
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
||||||
|
import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks';
|
||||||
|
import { useGetStartupConfig, useAvailableToolsQuery } from '~/data-provider';
|
||||||
|
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||||
|
import { PluginPagination } from '~/components/Plugins/Store';
|
||||||
|
import { useAgentPanelContext } from '~/Providers';
|
||||||
|
import MCPToolItem from './MCPToolItem';
|
||||||
|
|
||||||
|
function MCPToolSelectDialog({
|
||||||
|
isOpen,
|
||||||
|
agentId,
|
||||||
|
setIsOpen,
|
||||||
|
mcpServerNames,
|
||||||
|
}: TPluginStoreDialogProps & {
|
||||||
|
agentId: string;
|
||||||
|
mcpServerNames?: string[];
|
||||||
|
endpoint: EModelEndpoint.agents;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { mcpServersMap } = useAgentPanelContext();
|
||||||
|
const { initializeServer } = useMCPServerManager();
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
const { refetch: refetchAvailableTools } = useAvailableToolsQuery(EModelEndpoint.agents);
|
||||||
|
|
||||||
|
const [isInitializing, setIsInitializing] = useState<string | null>(null);
|
||||||
|
const [configuringServer, setConfiguringServer] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
maxPage,
|
||||||
|
setMaxPage,
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
searchChanged,
|
||||||
|
setSearchChanged,
|
||||||
|
searchValue,
|
||||||
|
setSearchValue,
|
||||||
|
gridRef,
|
||||||
|
handleSearch,
|
||||||
|
handleChangePage,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
errorMessage,
|
||||||
|
setErrorMessage,
|
||||||
|
} = usePluginDialogHelpers();
|
||||||
|
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
|
||||||
|
const handleInstallError = (error: TError) => {
|
||||||
|
setError(true);
|
||||||
|
const errorMessage = error.response?.data?.message ?? '';
|
||||||
|
if (errorMessage) {
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setError(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDirectAdd = async (serverName: string) => {
|
||||||
|
try {
|
||||||
|
setIsInitializing(serverName);
|
||||||
|
const serverInfo = mcpServersMap.get(serverName);
|
||||||
|
if (!serverInfo?.isConnected) {
|
||||||
|
const result = await initializeServer(serverName);
|
||||||
|
if (result?.success && result.oauthRequired && result.oauthUrl) {
|
||||||
|
setIsInitializing(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'install',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
handleInstallError(error as TError);
|
||||||
|
setIsInitializing(null);
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
const { data: updatedAvailableTools } = await refetchAvailableTools();
|
||||||
|
|
||||||
|
const currentTools = getValues('tools') || [];
|
||||||
|
const toolsToAdd: string[] = [
|
||||||
|
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (updatedAvailableTools) {
|
||||||
|
updatedAvailableTools.forEach((tool) => {
|
||||||
|
if (tool.pluginKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
|
||||||
|
toolsToAdd.push(tool.pluginKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTools = toolsToAdd.filter((tool) => !currentTools.includes(tool));
|
||||||
|
if (newTools.length > 0) {
|
||||||
|
setValue('tools', [...currentTools, ...newTools]);
|
||||||
|
}
|
||||||
|
setIsInitializing(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding MCP server:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCustomVars = async (serverName: string, authData: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
await updateUserPlugins.mutateAsync({
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'install',
|
||||||
|
auth: authData,
|
||||||
|
isEntityTool: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleDirectAdd(serverName);
|
||||||
|
|
||||||
|
setConfiguringServer(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving custom vars:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevokeCustomVars = (serverName: string) => {
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => handleInstallError(error as TError),
|
||||||
|
onSuccess: () => {
|
||||||
|
setConfiguringServer(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddTool = async (serverName: string) => {
|
||||||
|
if (configuringServer === serverName) {
|
||||||
|
setConfiguringServer(null);
|
||||||
|
await handleDirectAdd(serverName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
||||||
|
const hasCustomUserVars =
|
||||||
|
serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0;
|
||||||
|
|
||||||
|
if (hasCustomUserVars) {
|
||||||
|
setConfiguringServer(serverName);
|
||||||
|
} else {
|
||||||
|
await handleDirectAdd(serverName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTool = (serverName: string) => {
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
isEntityTool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => handleInstallError(error as TError),
|
||||||
|
onSuccess: () => {
|
||||||
|
const currentTools = getValues('tools') || [];
|
||||||
|
const remainingTools = currentTools.filter(
|
||||||
|
(tool) =>
|
||||||
|
tool !== serverName && !tool.endsWith(`${Constants.mcp_delimiter}${serverName}`),
|
||||||
|
);
|
||||||
|
setValue('tools', remainingTools);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const installedToolsSet = useMemo(() => {
|
||||||
|
return new Set(mcpServerNames);
|
||||||
|
}, [mcpServerNames]);
|
||||||
|
|
||||||
|
const mcpServers = useMemo(() => {
|
||||||
|
const servers = Array.from(mcpServersMap.values());
|
||||||
|
return servers.sort((a, b) => a.serverName.localeCompare(b.serverName));
|
||||||
|
}, [mcpServersMap]);
|
||||||
|
|
||||||
|
const filteredServers = useMemo(() => {
|
||||||
|
if (!searchValue) {
|
||||||
|
return mcpServers;
|
||||||
|
}
|
||||||
|
return mcpServers.filter((serverInfo) =>
|
||||||
|
serverInfo.serverName.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
);
|
||||||
|
}, [mcpServers, searchValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMaxPage(Math.ceil(filteredServers.length / itemsPerPage));
|
||||||
|
if (searchChanged) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchChanged(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
setMaxPage,
|
||||||
|
itemsPerPage,
|
||||||
|
searchChanged,
|
||||||
|
setCurrentPage,
|
||||||
|
setSearchChanged,
|
||||||
|
filteredServers.length,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchValue('');
|
||||||
|
setConfiguringServer(null);
|
||||||
|
setIsInitializing(null);
|
||||||
|
}}
|
||||||
|
className="relative z-[102]"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-surface-primary opacity-60 transition-opacity dark:opacity-80" />
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<DialogPanel
|
||||||
|
className="relative max-h-[90vh] w-full transform overflow-hidden overflow-y-auto rounded-lg bg-surface-secondary text-left shadow-xl transition-all max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
|
||||||
|
style={{ minHeight: '610px' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b-[1px] border-border-medium px-4 pb-4 pt-5 sm:p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<DialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
|
{localize('com_nav_tool_dialog_mcp_server_tools')}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm text-text-secondary">
|
||||||
|
{localize('com_nav_tool_dialog_description')}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setConfiguringServer(null);
|
||||||
|
setIsInitializing(null);
|
||||||
|
}}
|
||||||
|
className="inline-block rounded-full text-text-secondary transition-colors hover:text-text-primary"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{localize('com_nav_plugin_auth_error')} {errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{configuringServer && (
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
{localize('com_ui_mcp_configure_server_description', { 0: configuringServer })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CustomUserVarsSection
|
||||||
|
serverName={configuringServer}
|
||||||
|
fields={startupConfig?.mcpServers?.[configuringServer]?.customUserVars || {}}
|
||||||
|
onSave={(authData) => handleSaveCustomVars(configuringServer, authData)}
|
||||||
|
onRevoke={() => handleRevokeCustomVars(configuringServer)}
|
||||||
|
isSubmitting={updateUserPlugins.isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center space-x-4"
|
||||||
|
onClick={() => setConfiguringServer(null)}
|
||||||
|
>
|
||||||
|
<Search className="h-6 w-6 text-text-tertiary" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={localize('com_nav_tool_search')}
|
||||||
|
className="w-64 rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={gridRef}
|
||||||
|
className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
style={{ minHeight: '410px' }}
|
||||||
|
>
|
||||||
|
{filteredServers
|
||||||
|
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
|
||||||
|
.map((serverInfo) => {
|
||||||
|
const isInstalled = installedToolsSet.has(serverInfo.serverName);
|
||||||
|
const isConfiguring = configuringServer === serverInfo.serverName;
|
||||||
|
const isServerInitializing = isInitializing === serverInfo.serverName;
|
||||||
|
|
||||||
|
const tool: AgentToolType = {
|
||||||
|
agent_id: agentId,
|
||||||
|
tool_id: serverInfo.serverName,
|
||||||
|
metadata: {
|
||||||
|
...serverInfo.metadata,
|
||||||
|
description: `${localize('com_ui_tool_collection_prefix')} ${serverInfo.serverName}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MCPToolItem
|
||||||
|
tool={tool}
|
||||||
|
isInstalled={isInstalled}
|
||||||
|
key={serverInfo.serverName}
|
||||||
|
isConfiguring={isConfiguring}
|
||||||
|
isInitializing={isServerInitializing}
|
||||||
|
onAddTool={() => onAddTool(serverInfo.serverName)}
|
||||||
|
onRemoveTool={() => onRemoveTool(serverInfo.serverName)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
{maxPage > 0 ? (
|
||||||
|
<PluginPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
maxPage={maxPage}
|
||||||
|
onChangePage={handleChangePage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ height: '21px' }}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MCPToolSelectDialog;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { Constants, isAgentsEndpoint } from 'librechat-data-provider';
|
import { isAgentsEndpoint } from 'librechat-data-provider';
|
||||||
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
|
||||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -15,7 +15,6 @@ import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
||||||
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
|
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
|
||||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
|
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
|
||||||
import { useAvailableToolsQuery } from '~/data-provider';
|
|
||||||
import ToolItem from './ToolItem';
|
import ToolItem from './ToolItem';
|
||||||
|
|
||||||
function ToolSelectDialog({
|
function ToolSelectDialog({
|
||||||
|
|
@ -26,10 +25,9 @@ function ToolSelectDialog({
|
||||||
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
|
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
|
||||||
const { data: tools } = useAvailableToolsQuery(endpoint);
|
|
||||||
const { groupedTools } = useAgentPanelContext();
|
|
||||||
const isAgentTools = isAgentsEndpoint(endpoint);
|
const isAgentTools = isAgentsEndpoint(endpoint);
|
||||||
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
const { groupedTools, pluginTools } = useAgentPanelContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
maxPage,
|
maxPage,
|
||||||
|
|
@ -121,17 +119,10 @@ function ToolSelectDialog({
|
||||||
|
|
||||||
const onAddTool = (pluginKey: string) => {
|
const onAddTool = (pluginKey: string) => {
|
||||||
setShowPluginAuthForm(false);
|
setShowPluginAuthForm(false);
|
||||||
const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey);
|
const availablePluginFromKey = pluginTools?.find((p) => p.pluginKey === pluginKey);
|
||||||
setSelectedPlugin(getAvailablePluginFromKey);
|
setSelectedPlugin(availablePluginFromKey);
|
||||||
|
|
||||||
const isMCPTool = pluginKey.includes(Constants.mcp_delimiter);
|
const { authConfig, authenticated = false } = availablePluginFromKey ?? {};
|
||||||
|
|
||||||
if (isMCPTool) {
|
|
||||||
// MCP tools have their variables configured elsewhere (e.g., MCPPanel or MCPSelect),
|
|
||||||
// so we directly proceed to install without showing the auth form.
|
|
||||||
handleInstall({ pluginKey, action: 'install', auth: {} });
|
|
||||||
} else {
|
|
||||||
const { authConfig, authenticated = false } = getAvailablePluginFromKey ?? {};
|
|
||||||
if (authConfig && authConfig.length > 0 && !authenticated) {
|
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||||
setShowPluginAuthForm(true);
|
setShowPluginAuthForm(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -141,18 +132,15 @@ function ToolSelectDialog({
|
||||||
auth: {},
|
auth: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTools = Object.values(groupedTools || {}).filter(
|
const filteredTools = Object.values(groupedTools || {}).filter(
|
||||||
(tool: AgentToolType & { tools?: AgentToolType[] }) => {
|
(currentTool: AgentToolType & { tools?: AgentToolType[] }) => {
|
||||||
// Check if the parent tool matches
|
if (currentTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||||
if (tool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase())) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check if any child tools match
|
if (currentTool.tools) {
|
||||||
if (tool.tools) {
|
return currentTool.tools.some((childTool) =>
|
||||||
return tool.tools.some((childTool) =>
|
|
||||||
childTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()),
|
childTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +157,9 @@ function ToolSelectDialog({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
tools,
|
pluginTools,
|
||||||
itemsPerPage,
|
|
||||||
searchValue,
|
searchValue,
|
||||||
|
itemsPerPage,
|
||||||
filteredTools,
|
filteredTools,
|
||||||
searchChanged,
|
searchChanged,
|
||||||
setMaxPage,
|
setMaxPage,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
export { default as MCPToolSelectDialog } from './MCPToolSelectDialog';
|
||||||
export { default as ToolSelectDialog } from './ToolSelectDialog';
|
export { default as ToolSelectDialog } from './ToolSelectDialog';
|
||||||
export { default as ToolItem } from './ToolItem';
|
export { default as ToolItem } from './ToolItem';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
export * from './useMCPSelect';
|
|
||||||
export * from './useGetMCPTools';
|
export * from './useGetMCPTools';
|
||||||
|
export * from './useMCPConnectionStatus';
|
||||||
|
export * from './useMCPSelect';
|
||||||
|
export * from './useVisibleTools';
|
||||||
export { useMCPServerManager } from './useMCPServerManager';
|
export { useMCPServerManager } from './useMCPServerManager';
|
||||||
|
|
|
||||||
11
client/src/hooks/MCP/useMCPConnectionStatus.ts
Normal file
11
client/src/hooks/MCP/useMCPConnectionStatus.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||||
|
|
||||||
|
export function useMCPConnectionStatus({ enabled }: { enabled?: boolean } = {}) {
|
||||||
|
const { data } = useMCPConnectionStatusQuery({
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionStatus: data?.connectionStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,7 @@ import {
|
||||||
} from 'librechat-data-provider/react-query';
|
} from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||||
import type { ConfigFieldDetail } from '~/common';
|
import type { ConfigFieldDetail } from '~/common';
|
||||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
import { useLocalize, useMCPSelect, useGetMCPTools, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { useLocalize, useMCPSelect, useGetMCPTools } from '~/hooks';
|
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
|
||||||
interface ServerState {
|
interface ServerState {
|
||||||
|
|
@ -21,7 +20,7 @@ interface ServerState {
|
||||||
pollInterval: NodeJS.Timeout | null;
|
pollInterval: NodeJS.Timeout | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMCPServerManager({ conversationId }: { conversationId?: string | null }) {
|
export function useMCPServerManager({ conversationId }: { conversationId?: string | null } = {}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
@ -83,13 +82,9 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
return initialStates;
|
return initialStates;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: connectionStatusData } = useMCPConnectionStatusQuery({
|
const { connectionStatus } = useMCPConnectionStatus({
|
||||||
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
|
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
|
||||||
});
|
});
|
||||||
const connectionStatus = useMemo(
|
|
||||||
() => connectionStatusData?.connectionStatus || {},
|
|
||||||
[connectionStatusData?.connectionStatus],
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Filter disconnected servers when values change, but only after initial load
|
/** Filter disconnected servers when values change, but only after initial load
|
||||||
This prevents clearing selections on page refresh when servers haven't connected yet
|
This prevents clearing selections on page refresh when servers haven't connected yet
|
||||||
|
|
@ -97,7 +92,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
const hasInitialLoadCompleted = useRef(false);
|
const hasInitialLoadCompleted = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connectionStatusData || Object.keys(connectionStatus).length === 0) {
|
if (!connectionStatus || Object.keys(connectionStatus).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +110,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
if (connectedSelected.length !== mcpValues.length) {
|
if (connectedSelected.length !== mcpValues.length) {
|
||||||
setMCPValues(connectedSelected);
|
setMCPValues(connectedSelected);
|
||||||
}
|
}
|
||||||
}, [connectionStatus, connectionStatusData, mcpValues, setMCPValues]);
|
}, [connectionStatus, mcpValues, setMCPValues]);
|
||||||
|
|
||||||
const updateServerState = useCallback((serverName: string, updates: Partial<ServerState>) => {
|
const updateServerState = useCallback((serverName: string, updates: Partial<ServerState>) => {
|
||||||
setServerStates((prev) => {
|
setServerStates((prev) => {
|
||||||
|
|
@ -229,11 +224,17 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
const initializeServer = useCallback(
|
const initializeServer = useCallback(
|
||||||
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
||||||
updateServerState(serverName, { isInitializing: true });
|
updateServerState(serverName, { isInitializing: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await reinitializeMutation.mutateAsync(serverName);
|
const response = await reinitializeMutation.mutateAsync(serverName);
|
||||||
|
if (!response.success) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
cleanupServerState(serverName);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
if (response.oauthRequired && response.oauthUrl) {
|
if (response.oauthRequired && response.oauthUrl) {
|
||||||
updateServerState(serverName, {
|
updateServerState(serverName, {
|
||||||
oauthUrl: response.oauthUrl,
|
oauthUrl: response.oauthUrl,
|
||||||
|
|
@ -248,7 +249,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
startServerPolling(serverName);
|
startServerPolling(serverName);
|
||||||
} else {
|
} else {
|
||||||
await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
|
await queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
|
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
|
||||||
|
|
@ -262,13 +263,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
cleanupServerState(serverName);
|
cleanupServerState(serverName);
|
||||||
}
|
}
|
||||||
} else {
|
return response;
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
cleanupServerState(serverName);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -351,7 +346,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverStatus = connectionStatus[serverName];
|
const serverStatus = connectionStatus?.[serverName];
|
||||||
if (serverStatus?.connectionState === 'connected') {
|
if (serverStatus?.connectionState === 'connected') {
|
||||||
connectedServers.push(serverName);
|
connectedServers.push(serverName);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -381,7 +376,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
const filteredValues = currentValues.filter((name) => name !== serverName);
|
const filteredValues = currentValues.filter((name) => name !== serverName);
|
||||||
setMCPValues(filteredValues);
|
setMCPValues(filteredValues);
|
||||||
} else {
|
} else {
|
||||||
const serverStatus = connectionStatus[serverName];
|
const serverStatus = connectionStatus?.[serverName];
|
||||||
if (serverStatus?.connectionState === 'connected') {
|
if (serverStatus?.connectionState === 'connected') {
|
||||||
setMCPValues([...currentValues, serverName]);
|
setMCPValues([...currentValues, serverName]);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -455,7 +450,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
const getServerStatusIconProps = useCallback(
|
const getServerStatusIconProps = useCallback(
|
||||||
(serverName: string) => {
|
(serverName: string) => {
|
||||||
const tool = mcpToolDetails?.find((t) => t.name === serverName);
|
const tool = mcpToolDetails?.find((t) => t.name === serverName);
|
||||||
const serverStatus = connectionStatus[serverName];
|
const serverStatus = connectionStatus?.[serverName];
|
||||||
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
||||||
|
|
||||||
const handleConfigClick = (e: React.MouseEvent) => {
|
const handleConfigClick = (e: React.MouseEvent) => {
|
||||||
|
|
@ -532,7 +527,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverName: selectedToolForConfig.name,
|
serverName: selectedToolForConfig.name,
|
||||||
serverStatus: connectionStatus[selectedToolForConfig.name],
|
serverStatus: connectionStatus?.[selectedToolForConfig.name],
|
||||||
isOpen: isConfigModalOpen,
|
isOpen: isConfigModalOpen,
|
||||||
onOpenChange: handleDialogOpenChange,
|
onOpenChange: handleDialogOpenChange,
|
||||||
fieldsSchema,
|
fieldsSchema,
|
||||||
|
|
@ -553,7 +548,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
return {
|
return {
|
||||||
configuredServers,
|
configuredServers,
|
||||||
connectionStatus,
|
|
||||||
initializeServer,
|
initializeServer,
|
||||||
cancelOAuthFlow,
|
cancelOAuthFlow,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
|
|
|
||||||
79
client/src/hooks/MCP/useVisibleTools.ts
Normal file
79
client/src/hooks/MCP/useVisibleTools.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import type { AgentToolType } from 'librechat-data-provider';
|
||||||
|
import type { MCPServerInfo } from '~/common';
|
||||||
|
|
||||||
|
type GroupedToolType = AgentToolType & { tools?: AgentToolType[] };
|
||||||
|
type GroupedToolsRecord = Record<string, GroupedToolType>;
|
||||||
|
|
||||||
|
interface VisibleToolsResult {
|
||||||
|
toolIds: string[];
|
||||||
|
mcpServerNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to calculate visible tool IDs based on selected tools and their parent groups.
|
||||||
|
* If any subtool of a group is selected, the parent group tool is also made visible.
|
||||||
|
*
|
||||||
|
* @param selectedToolIds - Array of selected tool IDs
|
||||||
|
* @param allTools - Record of all available tools
|
||||||
|
* @param mcpServersMap - Map of all MCP servers
|
||||||
|
* @returns Object containing separate arrays of visible tool IDs for regular and MCP tools
|
||||||
|
*/
|
||||||
|
export function useVisibleTools(
|
||||||
|
selectedToolIds: string[] | undefined,
|
||||||
|
allTools: GroupedToolsRecord | undefined,
|
||||||
|
mcpServersMap: Map<string, MCPServerInfo>,
|
||||||
|
): VisibleToolsResult {
|
||||||
|
return useMemo(() => {
|
||||||
|
const mcpServers = new Set<string>();
|
||||||
|
const selectedSet = new Set<string>();
|
||||||
|
const regularToolIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const toolId of selectedToolIds ?? []) {
|
||||||
|
if (!toolId.includes(Constants.mcp_delimiter)) {
|
||||||
|
selectedSet.add(toolId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const serverName = toolId.split(Constants.mcp_delimiter)[1];
|
||||||
|
if (!serverName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mcpServers.add(serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTools) {
|
||||||
|
for (const [toolId, toolObj] of Object.entries(allTools)) {
|
||||||
|
if (selectedSet.has(toolId)) {
|
||||||
|
regularToolIds.add(toolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolObj.tools?.length) {
|
||||||
|
for (const subtool of toolObj.tools) {
|
||||||
|
if (selectedSet.has(subtool.tool_id)) {
|
||||||
|
regularToolIds.add(toolId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mcpServersMap) {
|
||||||
|
for (const [mcpServerName] of mcpServersMap) {
|
||||||
|
if (mcpServers.has(mcpServerName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/** Legacy check */
|
||||||
|
if (selectedSet.has(mcpServerName)) {
|
||||||
|
mcpServers.add(mcpServerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolIds: Array.from(regularToolIds).sort((a, b) => a.localeCompare(b)),
|
||||||
|
mcpServerNames: Array.from(mcpServers).sort((a, b) => a.localeCompare(b)),
|
||||||
|
};
|
||||||
|
}, [allTools, mcpServersMap, selectedToolIds]);
|
||||||
|
}
|
||||||
|
|
@ -104,6 +104,7 @@
|
||||||
"com_assistants_actions_info": "Let your Assistant retrieve information or take actions via API's",
|
"com_assistants_actions_info": "Let your Assistant retrieve information or take actions via API's",
|
||||||
"com_assistants_add_actions": "Add Actions",
|
"com_assistants_add_actions": "Add Actions",
|
||||||
"com_assistants_add_tools": "Add Tools",
|
"com_assistants_add_tools": "Add Tools",
|
||||||
|
"com_assistants_add_mcp_server_tools": "Add MCP Server Tools",
|
||||||
"com_assistants_allow_sites_you_trust": "Only allow sites you trust.",
|
"com_assistants_allow_sites_you_trust": "Only allow sites you trust.",
|
||||||
"com_assistants_append_date": "Append Current Date & Time",
|
"com_assistants_append_date": "Append Current Date & Time",
|
||||||
"com_assistants_append_date_tooltip": "When enabled, the current client date and time will be appended to the assistant system instructions.",
|
"com_assistants_append_date_tooltip": "When enabled, the current client date and time will be appended to the assistant system instructions.",
|
||||||
|
|
@ -579,7 +580,8 @@
|
||||||
"com_nav_theme_system": "System",
|
"com_nav_theme_system": "System",
|
||||||
"com_nav_tool_dialog": "Assistant Tools",
|
"com_nav_tool_dialog": "Assistant Tools",
|
||||||
"com_nav_tool_dialog_agents": "Agent Tools",
|
"com_nav_tool_dialog_agents": "Agent Tools",
|
||||||
"com_nav_tool_dialog_description": "Assistant must be saved to persist tool selections.",
|
"com_nav_tool_dialog_mcp_server_tools": "MCP Server Tools",
|
||||||
|
"com_nav_tool_dialog_description": "Agent must be saved to persist tool selections.",
|
||||||
"com_nav_tool_remove": "Remove",
|
"com_nav_tool_remove": "Remove",
|
||||||
"com_nav_tool_search": "Search tools",
|
"com_nav_tool_search": "Search tools",
|
||||||
"com_nav_user": "USER",
|
"com_nav_user": "USER",
|
||||||
|
|
@ -769,6 +771,7 @@
|
||||||
"com_ui_confirm_action": "Confirm Action",
|
"com_ui_confirm_action": "Confirm Action",
|
||||||
"com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?",
|
"com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?",
|
||||||
"com_ui_confirm_change": "Confirm Change",
|
"com_ui_confirm_change": "Confirm Change",
|
||||||
|
"com_ui_confirm": "Confirm",
|
||||||
"com_ui_connecting": "Connecting",
|
"com_ui_connecting": "Connecting",
|
||||||
"com_ui_context": "Context",
|
"com_ui_context": "Context",
|
||||||
"com_ui_continue": "Continue",
|
"com_ui_continue": "Continue",
|
||||||
|
|
@ -830,6 +833,8 @@
|
||||||
"com_ui_delete_success": "Successfully deleted",
|
"com_ui_delete_success": "Successfully deleted",
|
||||||
"com_ui_delete_tool": "Delete Tool",
|
"com_ui_delete_tool": "Delete Tool",
|
||||||
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
||||||
|
"com_ui_delete_tool_error": "Error while deleting the tool: {{error}}",
|
||||||
|
"com_ui_delete_tool_success": "Tool deleted successfully",
|
||||||
"com_ui_deleted": "Deleted",
|
"com_ui_deleted": "Deleted",
|
||||||
"com_ui_deleting_file": "Deleting file...",
|
"com_ui_deleting_file": "Deleting file...",
|
||||||
"com_ui_descending": "Desc",
|
"com_ui_descending": "Desc",
|
||||||
|
|
@ -947,6 +952,7 @@
|
||||||
"com_ui_image_gen": "Image Gen",
|
"com_ui_image_gen": "Image Gen",
|
||||||
"com_ui_import": "Import",
|
"com_ui_import": "Import",
|
||||||
"com_ui_import_conversation_error": "There was an error importing your conversations",
|
"com_ui_import_conversation_error": "There was an error importing your conversations",
|
||||||
|
"com_ui_initializing": "Initializing...",
|
||||||
"com_ui_import_conversation_file_type_error": "Unsupported import type",
|
"com_ui_import_conversation_file_type_error": "Unsupported import type",
|
||||||
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
||||||
"com_ui_import_conversation_success": "Conversations imported successfully",
|
"com_ui_import_conversation_success": "Conversations imported successfully",
|
||||||
|
|
@ -1202,6 +1208,7 @@
|
||||||
"com_ui_unarchive": "Unarchive",
|
"com_ui_unarchive": "Unarchive",
|
||||||
"com_ui_unarchive_error": "Failed to unarchive conversation",
|
"com_ui_unarchive_error": "Failed to unarchive conversation",
|
||||||
"com_ui_unknown": "Unknown",
|
"com_ui_unknown": "Unknown",
|
||||||
|
"com_ui_unavailable": "Unavailable",
|
||||||
"com_ui_unset": "Unset",
|
"com_ui_unset": "Unset",
|
||||||
"com_ui_untitled": "Untitled",
|
"com_ui_untitled": "Untitled",
|
||||||
"com_ui_update": "Update",
|
"com_ui_update": "Update",
|
||||||
|
|
@ -1265,5 +1272,7 @@
|
||||||
"com_ui_x_selected": "{{0}} selected",
|
"com_ui_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
|
"com_ui_mcp_configure_server": "Configure {{0}}",
|
||||||
|
"com_ui_mcp_configure_server_description": "Configure custom variables for {{0}}",
|
||||||
"com_user_message": "You"
|
"com_user_message": "You"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import { cn } from '~/utils';
|
||||||
import './Tooltip.css';
|
import './Tooltip.css';
|
||||||
|
|
||||||
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
||||||
description: string;
|
|
||||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
||||||
className?: string;
|
|
||||||
role?: string;
|
role?: string;
|
||||||
|
className?: string;
|
||||||
|
description: string;
|
||||||
enableHTML?: boolean;
|
enableHTML?: boolean;
|
||||||
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||||
|
|
|
||||||
|
|
@ -1559,8 +1559,13 @@ export enum Constants {
|
||||||
mcp_delimiter = '_mcp_',
|
mcp_delimiter = '_mcp_',
|
||||||
/** Prefix for MCP plugins */
|
/** Prefix for MCP plugins */
|
||||||
mcp_prefix = 'mcp_',
|
mcp_prefix = 'mcp_',
|
||||||
/** Unique value to indicate all MCP servers */
|
/** Unique value to indicate all MCP servers. For backend use only. */
|
||||||
mcp_all = 'sys__all__sys',
|
mcp_all = 'sys__all__sys',
|
||||||
|
/**
|
||||||
|
* Unique value to indicate the MCP tool was added to an agent.
|
||||||
|
* This helps inform the UI if the mcp server was previously added.
|
||||||
|
* */
|
||||||
|
mcp_server = 'sys__server__sys',
|
||||||
/** Placeholder Agent ID for Ephemeral Agents */
|
/** Placeholder Agent ID for Ephemeral Agents */
|
||||||
EPHEMERAL_AGENT_ID = 'ephemeral',
|
EPHEMERAL_AGENT_ID = 'ephemeral',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ export type ActionMetadataRuntime = ActionMetadata & {
|
||||||
export type MCP = {
|
export type MCP = {
|
||||||
mcp_id: string;
|
mcp_id: string;
|
||||||
metadata: MCPMetadata;
|
metadata: MCPMetadata;
|
||||||
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id?: string });
|
||||||
|
|
||||||
export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
|
export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -352,6 +352,6 @@ export type MCPAuth = ActionAuth;
|
||||||
export type AgentToolType = {
|
export type AgentToolType = {
|
||||||
tool_id: string;
|
tool_id: string;
|
||||||
metadata: ToolMetadata;
|
metadata: ToolMetadata;
|
||||||
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id?: string });
|
||||||
|
|
||||||
export type ToolMetadata = TPlugin;
|
export type ToolMetadata = TPlugin;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue