2025-04-09 16:11:16 -04:00
|
|
|
import React, { memo, useRef, useMemo, useEffect, useCallback } from 'react';
|
2025-04-07 19:16:56 -04:00
|
|
|
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';
|
|
|
|
|
|
2025-04-09 18:38:48 -04:00
|
|
|
const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
|
|
|
|
|
if (rawCurrentValue) {
|
|
|
|
|
try {
|
|
|
|
|
const currentValue = rawCurrentValue?.trim() ?? '';
|
|
|
|
|
if (currentValue.length > 2) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Array.isArray(value) && value.length > 0;
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-07 19:16:56 -04:00
|
|
|
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
|
|
|
|
const localize = useLocalize();
|
|
|
|
|
const key = conversationId ?? Constants.NEW_CONVO;
|
2025-04-09 18:38:48 -04:00
|
|
|
const hasSetFetched = useRef<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const { data: mcpServerSet, isFetched } = 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;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-07 19:16:56 -04:00
|
|
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
2025-04-09 16:11:16 -04:00
|
|
|
const mcpState = useMemo(() => {
|
|
|
|
|
return ephemeralAgent?.mcp ?? [];
|
|
|
|
|
}, [ephemeralAgent?.mcp]);
|
2025-04-09 18:38:48 -04:00
|
|
|
|
2025-04-07 19:16:56 -04:00
|
|
|
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}`,
|
2025-04-09 16:11:16 -04:00
|
|
|
mcpState,
|
2025-04-07 19:16:56 -04:00
|
|
|
setSelectedValues,
|
2025-04-09 18:38:48 -04:00
|
|
|
storageCondition,
|
2025-04-07 19:16:56 -04:00
|
|
|
);
|
|
|
|
|
|
2025-04-09 16:11:16 -04:00
|
|
|
useEffect(() => {
|
2025-04-09 18:38:48 -04:00
|
|
|
if (hasSetFetched.current === key) {
|
2025-04-09 16:11:16 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!isFetched) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-09 18:38:48 -04:00
|
|
|
hasSetFetched.current = key;
|
|
|
|
|
if ((mcpServerSet?.size ?? 0) > 0) {
|
|
|
|
|
setMCPValues(mcpValues.filter((mcp) => mcpServerSet?.has(mcp)));
|
2025-04-09 16:11:16 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setMCPValues([]);
|
2025-04-09 18:38:48 -04:00
|
|
|
}, [isFetched, setMCPValues, mcpServerSet, key, mcpValues]);
|
2025-04-09 16:11:16 -04:00
|
|
|
|
2025-04-07 19:16:56 -04:00
|
|
|
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],
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-09 18:38:48 -04:00
|
|
|
const mcpServers = useMemo(() => {
|
|
|
|
|
return Array.from(mcpServerSet ?? []);
|
|
|
|
|
}, [mcpServerSet]);
|
|
|
|
|
|
|
|
|
|
if (!mcpServerSet || mcpServerSet.size === 0) {
|
2025-04-07 19:16:56 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<MultiSelect
|
|
|
|
|
items={mcpServers ?? []}
|
|
|
|
|
selectedValues={mcpValues ?? []}
|
|
|
|
|
setSelectedValues={setMCPValues}
|
|
|
|
|
defaultSelectedValues={mcpValues ?? []}
|
|
|
|
|
renderSelectedValues={renderSelectedValues}
|
|
|
|
|
placeholder={localize('com_ui_mcp_servers')}
|
2025-04-09 16:11:16 -04:00
|
|
|
popoverClassName="min-w-fit"
|
|
|
|
|
className="badge-icon min-w-fit"
|
2025-04-07 19:16:56 -04:00
|
|
|
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);
|