🔦 feat: MCP Support for Non-Agent Endpoints (#6775)

* wip: mcp select

* refactor: Update useAvailableToolsQuery to support generic data types

* feat: Enhance MCPSelect to dynamically load server options and improve MultiSelect component styling

* WIP: ephemeral agents

* wip: Add null check for MCPSelect and improve MultiSelect focus handling

* feat: Pass conversationId prop to MCPSelect in BadgeRow to optimize badge rendering

* feat: useApplyNewAgentTemplate hook to manage ephemeral agent upon conversation creation

* WIP: eph. agent payload

* refactor(OpenAIClient): streamline message processing by replacing content handling with parseTextParts function

* feat: enhance applyAgentTemplate function to accept source conversation ID for improved template application

* feat(parsers): add skipReasoning parameter to parseTextParts for conditional reasoning handling

* WIP: first pass, ephemeral agent backend processing

* chore: import order

* feat: update loadEphemeralAgent and loadAgent functions to accept model_parameters for enhanced agent configuration

* feat: add showMCPServers prop to BadgeRow for conditional rendering of MCPSelect, fix react rule violation

* feat: enhance MCPSelect with localized placeholder and custom icon, add renderSelectedValues callback

* feat: simplify message processing in AnthropicClient by replacing content handling with parseTextParts function

* feat: implement useLocalStorage hook for managing MCP values and update MCPSelect to utilize it

* chore: remove chatGPTBrowserSchema from endpoint schemas and update types for improved schema management

* chore: remove compactChatGPTSchema from endpoint schemas and update types for better schema management

* refactor: rename schemas for clarity and improve schema management

* feat: extend model detection to include 'codestral' alongside 'mistral'

* feat: add endpointType parameter to buildOptions and initializeClient functions

* fix: update condition for handling completion in BaseClient to include agents client

* refactor: simplify payload parsing logic in AgentClient and remove unused providerParsers

* refactor: change useSetRecoilState to useRecoilState for better state management in MCPSelect component

* refactor: streamline chat route handlers by consolidating middleware and improving endpoint structure

* style: update MCPSelect and MultiSelect components for improved layout in mobile view

* v0.7.790

* feat: add getMessageMapMethod to process message text and content in GoogleClient

* chore: include LAST_MCP_ key prefix in clearLocalStorage function for proper teardown on logout
This commit is contained in:
Danny Avila 2025-04-07 19:16:56 -04:00 committed by GitHub
parent 018143b5cc
commit 910c73359b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 741 additions and 285 deletions

View file

@ -1,4 +1,5 @@
import React, {
memo,
useState,
useRef,
useEffect,
@ -12,11 +13,14 @@ import type { LucideIcon } from 'lucide-react';
import type { BadgeItem } from '~/common';
import { useChatBadges } from '~/hooks';
import { Badge } from '~/components/ui';
import MCPSelect from './MCPSelect';
import store from '~/store';
interface BadgeRowProps {
showMCPServers?: boolean;
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
onToggle?: (badgeId: string, currentActive: boolean) => void;
conversationId?: string | null;
isInChat: boolean;
}
@ -33,7 +37,8 @@ interface BadgeWrapperProps {
const BadgeWrapper = React.memo(
forwardRef<HTMLDivElement, BadgeWrapperProps>(
({ badge, isEditing, isInChat, onToggle, onDelete, onMouseDown, badgeRefs }, ref) => {
const isActive = badge.atom ? useRecoilValue(badge.atom) : false;
const atomBadge = useRecoilValue(badge.atom);
const isActive = badge.atom ? atomBadge : false;
return (
<div
@ -126,7 +131,7 @@ const dragReducer = (state: DragState, action: DragAction): DragState => {
}
};
export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat }: BadgeRowProps) {
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
const [dragState, dispatch] = useReducer(dragReducer, {
draggedBadge: null,
@ -340,6 +345,7 @@ export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
/>
</div>
)}
{showMCPServers === true && <MCPSelect conversationId={conversationId} />}
{ghostBadge && (
<div
className="ghost-badge h-full"
@ -367,3 +373,5 @@ export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
</div>
);
}
export default memo(BadgeRow);

View file

@ -1,7 +1,7 @@
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
import { useWatch } from 'react-hook-form';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import {
useChatContext,
useChatFormContext,
@ -28,8 +28,8 @@ import CollapseChat from './CollapseChat';
import StreamAudio from './StreamAudio';
import StopButton from './StopButton';
import SendButton from './SendButton';
import { BadgeRow } from './BadgeRow';
import EditBadges from './EditBadges';
import BadgeRow from './BadgeRow';
import Mention from './Mention';
import store from '~/store';
@ -289,7 +289,9 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
<AttachFileChat disableInputs={disableInputs} />
</div>
<BadgeRow
onChange={(newBadges) => setBadges(newBadges)}
showMCPServers={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
conversationId={conversation?.conversationId ?? Constants.NEW_CONVO}
onChange={setBadges}
isInChat={
Array.isArray(conversation?.messages) && conversation.messages.length >= 1
}

View file

@ -0,0 +1,82 @@
import React, { memo, useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import { useAvailableToolsQuery } from '~/data-provider';
import useLocalStorage from '~/hooks/useLocalStorageAlt';
import MultiSelect from '~/components/ui/MultiSelect';
import { ephemeralAgentByConvoId } from '~/store';
import MCPIcon from '~/components/ui/MCPIcon';
import { useLocalize } from '~/hooks';
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
const localize = useLocalize();
const key = conversationId ?? Constants.NEW_CONVO;
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
const setSelectedValues = useCallback(
(values: string[] | null | undefined) => {
if (!values) {
return;
}
if (!Array.isArray(values)) {
return;
}
setEphemeralAgent((prev) => ({
...prev,
mcp: values,
}));
},
[setEphemeralAgent],
);
const [mcpValues, setMCPValues] = useLocalStorage<string[]>(
`${LocalStorageKeys.LAST_MCP_}${key}`,
ephemeralAgent?.mcp ?? [],
setSelectedValues,
);
const { data: mcpServers } = useAvailableToolsQuery(EModelEndpoint.agents, {
select: (data) => {
const serverNames = new Set<string>();
data.forEach((tool) => {
if (tool.pluginKey.includes(Constants.mcp_delimiter)) {
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
serverNames.add(parts[parts.length - 1]);
}
});
return [...serverNames];
},
});
const renderSelectedValues = useCallback(
(values: string[], placeholder?: string) => {
if (values.length === 0) {
return placeholder || localize('com_ui_select') + '...';
}
if (values.length === 1) {
return values[0];
}
return localize('com_ui_x_selected', { 0: values.length });
},
[localize],
);
if (!mcpServers || mcpServers.length === 0) {
return null;
}
return (
<MultiSelect
items={mcpServers ?? []}
selectedValues={mcpValues ?? []}
setSelectedValues={setMCPValues}
defaultSelectedValues={mcpValues ?? []}
renderSelectedValues={renderSelectedValues}
placeholder={localize('com_ui_mcp_servers')}
popoverClassName="min-w-[200px]"
className="badge-icon h-full min-w-[150px]"
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-shadow md:w-full size-9 p-2 md:p-3 bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
/>
);
}
export default memo(MCPSelect);