mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02: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;
|
||||
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
|
||||
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) {
|
||||
const currentMCPGenerator = async (index) =>
|
||||
createMCPTools({
|
||||
|
|
|
@ -187,7 +187,7 @@ const updateUserPluginsController = async (req, res) => {
|
|||
// Extract server name from pluginKey (format: "mcp_<serverName>")
|
||||
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ const { logger } = require('@librechat/data-schemas');
|
|||
const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api');
|
||||
const {
|
||||
Tools,
|
||||
Constants,
|
||||
SystemRoles,
|
||||
FileSources,
|
||||
ResourceType,
|
||||
|
@ -69,9 +70,9 @@ const createAgentHandler = async (req, res) => {
|
|||
for (const tool of tools) {
|
||||
if (availableTools[tool]) {
|
||||
agentData.tools.push(tool);
|
||||
}
|
||||
|
||||
if (systemTools[tool]) {
|
||||
} else if (systemTools[tool]) {
|
||||
agentData.tools.push(tool);
|
||||
} else if (tool.includes(Constants.mcp_delimiter)) {
|
||||
agentData.tools.push(tool);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -271,6 +271,7 @@ async function createMCPTool({
|
|||
availableTools: tools,
|
||||
}) {
|
||||
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
|
||||
|
||||
const availableTools =
|
||||
tools ?? (await getCachedTools({ userId: req.user?.id, includeGlobal: true }));
|
||||
/** @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 type { MCP, Action, TPlugin, AgentToolType } from 'librechat-data-provider';
|
||||
import type { AgentPanelContextType } from '~/common';
|
||||
import { useAvailableToolsQuery, useGetActionsQuery } from '~/data-provider';
|
||||
import { useLocalize, useGetAgentsConfig } from '~/hooks';
|
||||
import type { AgentPanelContextType, MCPServerInfo } from '~/common';
|
||||
import { useAvailableToolsQuery, useGetActionsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import { useLocalize, useGetAgentsConfig, useMCPConnectionStatus } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
type GroupedToolType = AgentToolType & { tools?: AgentToolType[] };
|
||||
type GroupedToolsRecord = Record<string, GroupedToolType>;
|
||||
|
||||
const AgentPanelContext = createContext<AgentPanelContextType | undefined>(undefined);
|
||||
|
||||
export function useAgentPanelContext() {
|
||||
|
@ -33,67 +36,116 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||
enabled: !!agent_id,
|
||||
});
|
||||
|
||||
const tools =
|
||||
pluginTools?.map((tool) => ({
|
||||
tool_id: tool.pluginKey,
|
||||
metadata: tool as TPlugin,
|
||||
agent_id: agent_id || '',
|
||||
})) || [];
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const mcpServerNames = useMemo(
|
||||
() => Object.keys(startupConfig?.mcpServers ?? {}),
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
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)) {
|
||||
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
||||
const groupKey = `${serverName.toLowerCase()}`;
|
||||
if (!acc[groupKey]) {
|
||||
acc[groupKey] = {
|
||||
tool_id: groupKey,
|
||||
metadata: {
|
||||
name: `${serverName}`,
|
||||
pluginKey: groupKey,
|
||||
|
||||
if (!mcpServersMap.has(serverName)) {
|
||||
const metadata = {
|
||||
name: serverName,
|
||||
pluginKey: serverName,
|
||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||
icon: tool.metadata.icon || '',
|
||||
} as TPlugin,
|
||||
agent_id: agent_id || '',
|
||||
icon: pluginTool.icon || '',
|
||||
} as TPlugin;
|
||||
|
||||
mcpServersMap.set(serverName, {
|
||||
serverName,
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
acc[groupKey].tools?.push({
|
||||
tool_id: tool.tool_id,
|
||||
metadata: tool.metadata,
|
||||
agent_id: agent_id || '',
|
||||
isConfigured: configuredServers.has(serverName),
|
||||
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
mcpServersMap.get(serverName)!.tools.push(tool);
|
||||
} else {
|
||||
acc[tool.tool_id] = {
|
||||
// Non-MCP tool
|
||||
groupedTools[tool.tool_id] = {
|
||||
tool_id: tool.tool_id,
|
||||
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 value: AgentPanelContextType = {
|
||||
mcp,
|
||||
mcps,
|
||||
/** Query data for actions and tools */
|
||||
tools,
|
||||
action,
|
||||
setMcp,
|
||||
actions,
|
||||
setMcps,
|
||||
agent_id,
|
||||
setAction,
|
||||
pluginTools,
|
||||
activePanel,
|
||||
groupedTools,
|
||||
agentsConfig,
|
||||
setActivePanel,
|
||||
endpointsConfig,
|
||||
setCurrentAgentId,
|
||||
tools: processedData.tools,
|
||||
groupedTools: processedData.groupedTools,
|
||||
mcpServersMap: processedData.mcpServersMap,
|
||||
};
|
||||
|
||||
return <AgentPanelContext.Provider value={value}>{children}</AgentPanelContext.Provider>;
|
||||
|
|
|
@ -216,6 +216,14 @@ export type AgentPanelProps = {
|
|||
agentsConfig?: t.TAgentsEndpoint | null;
|
||||
};
|
||||
|
||||
export interface MCPServerInfo {
|
||||
serverName: string;
|
||||
tools: t.AgentToolType[];
|
||||
isConfigured: boolean;
|
||||
isConnected: boolean;
|
||||
metadata: t.TPlugin;
|
||||
}
|
||||
|
||||
export type AgentPanelContextType = {
|
||||
action?: t.Action;
|
||||
actions?: t.Action[];
|
||||
|
@ -225,13 +233,16 @@ export type AgentPanelContextType = {
|
|||
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
||||
tools: t.AgentToolType[];
|
||||
activePanel?: string;
|
||||
tools: t.AgentToolType[];
|
||||
pluginTools?: t.TPlugin[];
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
agent_id?: string;
|
||||
agentsConfig?: t.TAgentsEndpoint | null;
|
||||
endpointsConfig?: t.TEndpointsConfig | null;
|
||||
/** Pre-computed MCP server information indexed by server key */
|
||||
mcpServersMap: Map<string, MCPServerInfo>;
|
||||
};
|
||||
|
||||
export type AgentModelPanelProps = {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { MultiSelect, MCPIcon } from '@librechat/client';
|
||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||
import { useBadgeRowContext } from '~/Providers';
|
||||
import { useMCPServerManager } from '~/hooks';
|
||||
|
||||
type MCPSelectProps = { conversationId?: string | null };
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import * as Ariakit from '@ariakit/react';
|
|||
import { ChevronRight } from 'lucide-react';
|
||||
import { PinIcon, MCPIcon } from '@librechat/client';
|
||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||
import { useMCPServerManager } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface MCPSubMenuProps {
|
||||
|
|
|
@ -88,6 +88,10 @@ export default function ToolCall({
|
|||
const url = new URL(authURL);
|
||||
return url.hostname;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'client/src/components/Chat/Messages/Content/ToolCall.tsx - Failed to parse auth URL',
|
||||
e,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}, [auth]);
|
||||
|
|
|
@ -16,7 +16,6 @@ interface CustomUserVarsSectionProps {
|
|||
onRevoke: () => void;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
interface AuthFieldProps {
|
||||
name: string;
|
||||
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_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({
|
||||
serverName,
|
||||
fields,
|
||||
onSave,
|
||||
onRevoke,
|
||||
serverName,
|
||||
isSubmitting = false,
|
||||
}: CustomUserVarsSectionProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
// Fetch auth value flags for the server
|
||||
const { data: authValuesData } = useMCPAuthValuesQuery(serverName, {
|
||||
enabled: !!serverName,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<Record<string, string>>({
|
||||
defaultValues: useMemo(() => {
|
||||
|
@ -140,10 +138,20 @@ export default function CustomUserVarsSection({
|
|||
</form>
|
||||
|
||||
<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')}
|
||||
</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')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { Button, Spinner } from '@librechat/client';
|
||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useLocalize, useMCPServerManager, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
interface ServerInitializationSectionProps {
|
||||
sidePanel?: boolean;
|
||||
|
@ -21,16 +21,15 @@ export default function ServerInitializationSection({
|
|||
}: ServerInitializationSectionProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const {
|
||||
initializeServer,
|
||||
connectionStatus,
|
||||
cancelOAuthFlow,
|
||||
isInitializing,
|
||||
isCancellable,
|
||||
getOAuthUrl,
|
||||
} = useMCPServerManager({ conversationId });
|
||||
const { initializeServer, 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 canCancel = isCancellable(serverName);
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
|
|
|
@ -12,22 +12,23 @@ import {
|
|||
getIconKey,
|
||||
cn,
|
||||
} from '~/utils';
|
||||
import { useFileMapContext, useAgentPanelContext } from '~/Providers';
|
||||
import { ToolSelectDialog, MCPToolSelectDialog } from '~/components/Tools';
|
||||
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
|
||||
import { useFileMapContext, useAgentPanelContext } from '~/Providers';
|
||||
import AgentCategorySelector from './AgentCategorySelector';
|
||||
import Action from '~/components/SidePanel/Builder/Action';
|
||||
import { ToolSelectDialog } from '~/components/Tools';
|
||||
import { useLocalize, useVisibleTools } from '~/hooks';
|
||||
import { useGetAgentFiles } from '~/data-provider';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import Instructions from './Instructions';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import FileContext from './FileContext';
|
||||
import SearchForm from './Search/Form';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import FileSearch from './FileSearch';
|
||||
import Artifacts from './Artifacts';
|
||||
import AgentTool from './AgentTool';
|
||||
import CodeForm from './Code/Form';
|
||||
import MCPTools from './MCPTools';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
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 methods = useFormContext<AgentForm>();
|
||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||
const [showMCPToolDialog, setShowMCPToolDialog] = useState(false);
|
||||
const {
|
||||
actions,
|
||||
setAction,
|
||||
agentsConfig,
|
||||
mcpServersMap,
|
||||
setActivePanel,
|
||||
endpointsConfig,
|
||||
groupedTools: allTools,
|
||||
|
@ -173,19 +176,7 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
|||
Icon = icons[iconKey];
|
||||
}
|
||||
|
||||
// Determine what to show
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
const { toolIds, mcpServerNames } = useVisibleTools(tools, allTools, mcpServersMap);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -326,8 +317,8 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
|||
</label>
|
||||
<div>
|
||||
<div className="mb-1">
|
||||
{/* // Render all visible IDs (including groups with subtools selected) */}
|
||||
{[...visibleToolIds].map((toolId, i) => {
|
||||
{/* Render all visible IDs (including groups with subtools selected) */}
|
||||
{toolIds.map((toolId, i) => {
|
||||
if (!allTools) return null;
|
||||
const tool = allTools[toolId];
|
||||
if (!tool) return null;
|
||||
|
@ -385,8 +376,11 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
|||
</div>
|
||||
</div>
|
||||
{/* MCP Section */}
|
||||
{/* <MCPSection /> */}
|
||||
|
||||
<MCPTools
|
||||
agentId={agent_id}
|
||||
mcpServerNames={mcpServerNames}
|
||||
setShowMCPToolDialog={setShowMCPToolDialog}
|
||||
/>
|
||||
{/* Support Contact (Optional) */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-1.5 flex items-center gap-2">
|
||||
|
@ -477,6 +471,13 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
|||
setIsOpen={setShowToolDialog}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
<MCPToolSelectDialog
|
||||
agentId={agent_id}
|
||||
isOpen={showMCPToolDialog}
|
||||
mcpServerNames={mcpServerNames}
|
||||
setIsOpen={setShowMCPToolDialog}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Tools,
|
||||
Constants,
|
||||
SystemRoles,
|
||||
ResourceType,
|
||||
EModelEndpoint,
|
||||
PermissionBits,
|
||||
isAssistantsEndpoint,
|
||||
|
@ -53,7 +54,7 @@ export default function AgentPanel() {
|
|||
});
|
||||
|
||||
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
|
||||
'agent',
|
||||
ResourceType.AGENT,
|
||||
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 type { TUpdateUserPlugins } from 'librechat-data-provider';
|
||||
import ServerInitializationSection from '~/components/MCP/ServerInitializationSection';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||
import { MCPPanelProvider, useMCPPanelContext } from '~/Providers';
|
||||
import { useLocalize, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
function MCPPanelContent() {
|
||||
const localize = useLocalize();
|
||||
|
@ -19,7 +18,10 @@ function MCPPanelContent() {
|
|||
const { showToast } = useToastContext();
|
||||
const { conversationId } = useMCPPanelContext();
|
||||
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>(
|
||||
null,
|
||||
);
|
||||
|
@ -57,11 +59,6 @@ function MCPPanelContent() {
|
|||
}));
|
||||
}, [startupConfig?.mcpServers]);
|
||||
|
||||
const connectionStatus = useMemo(
|
||||
() => connectionStatusData?.connectionStatus || {},
|
||||
[connectionStatusData?.connectionStatus],
|
||||
);
|
||||
|
||||
const handleServerClickToEdit = (serverName: string) => {
|
||||
setSelectedServerNameForEditing(serverName);
|
||||
};
|
||||
|
@ -125,7 +122,7 @@ function MCPPanelContent() {
|
|||
);
|
||||
}
|
||||
|
||||
const serverStatus = connectionStatus[selectedServerNameForEditing];
|
||||
const serverStatus = connectionStatus?.[selectedServerNameForEditing];
|
||||
|
||||
return (
|
||||
<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="space-y-2">
|
||||
{mcpServerDefinitions.map((server) => {
|
||||
const serverStatus = connectionStatus[server.serverName];
|
||||
const serverStatus = connectionStatus?.[server.serverName];
|
||||
const isConnected = serverStatus?.connectionState === 'connected';
|
||||
|
||||
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 { Search, X } from 'lucide-react';
|
||||
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 { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
|
@ -15,7 +15,6 @@ import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
|||
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
|
||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
|
||||
import { useAvailableToolsQuery } from '~/data-provider';
|
||||
import ToolItem from './ToolItem';
|
||||
|
||||
function ToolSelectDialog({
|
||||
|
@ -26,10 +25,9 @@ function ToolSelectDialog({
|
|||
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||
const { data: tools } = useAvailableToolsQuery(endpoint);
|
||||
const { groupedTools } = useAgentPanelContext();
|
||||
const isAgentTools = isAgentsEndpoint(endpoint);
|
||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||
const { groupedTools, pluginTools } = useAgentPanelContext();
|
||||
|
||||
const {
|
||||
maxPage,
|
||||
|
@ -121,17 +119,10 @@ function ToolSelectDialog({
|
|||
|
||||
const onAddTool = (pluginKey: string) => {
|
||||
setShowPluginAuthForm(false);
|
||||
const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey);
|
||||
setSelectedPlugin(getAvailablePluginFromKey);
|
||||
const availablePluginFromKey = pluginTools?.find((p) => p.pluginKey === pluginKey);
|
||||
setSelectedPlugin(availablePluginFromKey);
|
||||
|
||||
const isMCPTool = pluginKey.includes(Constants.mcp_delimiter);
|
||||
|
||||
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 ?? {};
|
||||
const { authConfig, authenticated = false } = availablePluginFromKey ?? {};
|
||||
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||
setShowPluginAuthForm(true);
|
||||
} else {
|
||||
|
@ -141,18 +132,15 @@ function ToolSelectDialog({
|
|||
auth: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTools = Object.values(groupedTools || {}).filter(
|
||||
(tool: AgentToolType & { tools?: AgentToolType[] }) => {
|
||||
// Check if the parent tool matches
|
||||
if (tool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
(currentTool: AgentToolType & { tools?: AgentToolType[] }) => {
|
||||
if (currentTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
// Check if any child tools match
|
||||
if (tool.tools) {
|
||||
return tool.tools.some((childTool) =>
|
||||
if (currentTool.tools) {
|
||||
return currentTool.tools.some((childTool) =>
|
||||
childTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
@ -169,9 +157,9 @@ function ToolSelectDialog({
|
|||
}
|
||||
}
|
||||
}, [
|
||||
tools,
|
||||
itemsPerPage,
|
||||
pluginTools,
|
||||
searchValue,
|
||||
itemsPerPage,
|
||||
filteredTools,
|
||||
searchChanged,
|
||||
setMaxPage,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default as MCPToolSelectDialog } from './MCPToolSelectDialog';
|
||||
export { default as ToolSelectDialog } from './ToolSelectDialog';
|
||||
export { default as ToolItem } from './ToolItem';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export * from './useMCPSelect';
|
||||
export * from './useGetMCPTools';
|
||||
export * from './useMCPConnectionStatus';
|
||||
export * from './useMCPSelect';
|
||||
export * from './useVisibleTools';
|
||||
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';
|
||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||
import type { ConfigFieldDetail } from '~/common';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import { useLocalize, useMCPSelect, useGetMCPTools } from '~/hooks';
|
||||
import { useLocalize, useMCPSelect, useGetMCPTools, useMCPConnectionStatus } from '~/hooks';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
interface ServerState {
|
||||
|
@ -21,7 +20,7 @@ interface ServerState {
|
|||
pollInterval: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
export function useMCPServerManager({ conversationId }: { conversationId?: string | null }) {
|
||||
export function useMCPServerManager({ conversationId }: { conversationId?: string | null } = {}) {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToastContext();
|
||||
|
@ -83,13 +82,9 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
return initialStates;
|
||||
});
|
||||
|
||||
const { data: connectionStatusData } = useMCPConnectionStatusQuery({
|
||||
const { connectionStatus } = useMCPConnectionStatus({
|
||||
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
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectionStatusData || Object.keys(connectionStatus).length === 0) {
|
||||
if (!connectionStatus || Object.keys(connectionStatus).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -115,7 +110,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
if (connectedSelected.length !== mcpValues.length) {
|
||||
setMCPValues(connectedSelected);
|
||||
}
|
||||
}, [connectionStatus, connectionStatusData, mcpValues, setMCPValues]);
|
||||
}, [connectionStatus, mcpValues, setMCPValues]);
|
||||
|
||||
const updateServerState = useCallback((serverName: string, updates: Partial<ServerState>) => {
|
||||
setServerStates((prev) => {
|
||||
|
@ -229,11 +224,17 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
const initializeServer = useCallback(
|
||||
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
||||
updateServerState(serverName, { isInitializing: true });
|
||||
|
||||
try {
|
||||
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) {
|
||||
updateServerState(serverName, {
|
||||
oauthUrl: response.oauthUrl,
|
||||
|
@ -248,7 +249,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
startServerPolling(serverName);
|
||||
} else {
|
||||
await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
|
||||
await queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
|
||||
|
@ -262,13 +263,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
} else {
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
||||
showToast({
|
||||
|
@ -351,7 +346,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
return;
|
||||
}
|
||||
|
||||
const serverStatus = connectionStatus[serverName];
|
||||
const serverStatus = connectionStatus?.[serverName];
|
||||
if (serverStatus?.connectionState === 'connected') {
|
||||
connectedServers.push(serverName);
|
||||
} else {
|
||||
|
@ -381,7 +376,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
const filteredValues = currentValues.filter((name) => name !== serverName);
|
||||
setMCPValues(filteredValues);
|
||||
} else {
|
||||
const serverStatus = connectionStatus[serverName];
|
||||
const serverStatus = connectionStatus?.[serverName];
|
||||
if (serverStatus?.connectionState === 'connected') {
|
||||
setMCPValues([...currentValues, serverName]);
|
||||
} else {
|
||||
|
@ -455,7 +450,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
const getServerStatusIconProps = useCallback(
|
||||
(serverName: string) => {
|
||||
const tool = mcpToolDetails?.find((t) => t.name === serverName);
|
||||
const serverStatus = connectionStatus[serverName];
|
||||
const serverStatus = connectionStatus?.[serverName];
|
||||
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
||||
|
||||
const handleConfigClick = (e: React.MouseEvent) => {
|
||||
|
@ -532,7 +527,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
return {
|
||||
serverName: selectedToolForConfig.name,
|
||||
serverStatus: connectionStatus[selectedToolForConfig.name],
|
||||
serverStatus: connectionStatus?.[selectedToolForConfig.name],
|
||||
isOpen: isConfigModalOpen,
|
||||
onOpenChange: handleDialogOpenChange,
|
||||
fieldsSchema,
|
||||
|
@ -553,7 +548,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
|||
|
||||
return {
|
||||
configuredServers,
|
||||
connectionStatus,
|
||||
initializeServer,
|
||||
cancelOAuthFlow,
|
||||
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_add_actions": "Add Actions",
|
||||
"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_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.",
|
||||
|
@ -579,7 +580,8 @@
|
|||
"com_nav_theme_system": "System",
|
||||
"com_nav_tool_dialog": "Assistant 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_search": "Search tools",
|
||||
"com_nav_user": "USER",
|
||||
|
@ -769,6 +771,7 @@
|
|||
"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_change": "Confirm Change",
|
||||
"com_ui_confirm": "Confirm",
|
||||
"com_ui_connecting": "Connecting",
|
||||
"com_ui_context": "Context",
|
||||
"com_ui_continue": "Continue",
|
||||
|
@ -830,6 +833,8 @@
|
|||
"com_ui_delete_success": "Successfully deleted",
|
||||
"com_ui_delete_tool": "Delete 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_deleting_file": "Deleting file...",
|
||||
"com_ui_descending": "Desc",
|
||||
|
@ -947,6 +952,7 @@
|
|||
"com_ui_image_gen": "Image Gen",
|
||||
"com_ui_import": "Import",
|
||||
"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_info": "Import conversations from a JSON file",
|
||||
"com_ui_import_conversation_success": "Conversations imported successfully",
|
||||
|
@ -1202,6 +1208,7 @@
|
|||
"com_ui_unarchive": "Unarchive",
|
||||
"com_ui_unarchive_error": "Failed to unarchive conversation",
|
||||
"com_ui_unknown": "Unknown",
|
||||
"com_ui_unavailable": "Unavailable",
|
||||
"com_ui_unset": "Unset",
|
||||
"com_ui_untitled": "Untitled",
|
||||
"com_ui_update": "Update",
|
||||
|
@ -1265,5 +1272,7 @@
|
|||
"com_ui_x_selected": "{{0}} selected",
|
||||
"com_ui_yes": "Yes",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@ import { cn } from '~/utils';
|
|||
import './Tooltip.css';
|
||||
|
||||
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
||||
description: string;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
role?: string;
|
||||
className?: string;
|
||||
description: string;
|
||||
enableHTML?: boolean;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||
|
|
|
@ -1559,8 +1559,13 @@ export enum Constants {
|
|||
mcp_delimiter = '_mcp_',
|
||||
/** Prefix for MCP plugins */
|
||||
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',
|
||||
/**
|
||||
* 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 */
|
||||
EPHEMERAL_AGENT_ID = 'ephemeral',
|
||||
}
|
||||
|
|
|
@ -335,7 +335,7 @@ export type ActionMetadataRuntime = ActionMetadata & {
|
|||
export type MCP = {
|
||||
mcp_id: string;
|
||||
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'> & {
|
||||
name?: string;
|
||||
|
@ -352,6 +352,6 @@ export type MCPAuth = ActionAuth;
|
|||
export type AgentToolType = {
|
||||
tool_id: string;
|
||||
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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue