From 66093b1eb38462b74d187a89a8f4d0075f40d2c4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 8 May 2025 12:12:36 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20refactor:=20MCP=20Chat=20Visibil?= =?UTF-8?q?ity=20Option,=20Google=20Rates,=20Remove=20OpenAPI=20Plugins=20?= =?UTF-8?q?(#7286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Update Gemini 2.5 Pro Preview Model Name in Token Values * refactor: Update DeleteButton to close menu when deletion is successful * refactor: Add unmountOnHide prop to DropdownPopup in multiple components * chore: linting * chore: linting * feat: Add `chatMenu` option for MCP Servers to control visibility in MCPSelect dropdown * refactor: Update loadManifestTools to return combined tool manifest with MCP tools first * chore: remove deprecated openapi plugins * chore: linting * chore(AgentClient): linting, remove unnecessary `checkVisionRequest` logger * refactor(AuthService): change logoutUser logging from error to debug level * chore: new Gemini models token values and rates * chore(AskController): linting --- api/app/clients/BaseClient.js | 16 +-- api/app/clients/tools/util/addOpenAPISpecs.js | 30 ----- .../tools/util/addOpenAPISpecs.spec.js | 76 ------------ api/app/clients/tools/util/handleTools.js | 26 ---- api/app/clients/tools/util/loadSpecs.js | 117 ------------------ api/app/clients/tools/util/loadSpecs.spec.js | 101 --------------- api/models/tx.js | 3 +- api/models/tx.spec.js | 6 + api/server/controllers/AskController.js | 2 +- api/server/controllers/PluginController.js | 7 +- api/server/controllers/agents/client.js | 10 -- api/server/services/AuthService.js | 2 +- api/utils/tokens.js | 2 + .../components/Chat/ExportAndShareMenu.tsx | 1 + .../Chat/Input/Files/AttachFileMenu.tsx | 1 + .../src/components/Chat/Input/MCPSelect.tsx | 3 +- .../components/Chat/Menus/BookmarkMenu.tsx | 1 + .../ConvoOptions/ConvoOptions.tsx | 7 +- .../ConvoOptions/DeleteButton.tsx | 15 ++- .../src/components/Prompts/AdminSettings.tsx | 13 +- .../SidePanel/Agents/AdminSettings.tsx | 1 + client/src/utils/map.ts | 17 +-- packages/data-provider/src/mcp.ts | 2 + packages/data-provider/src/schemas.ts | 1 + packages/mcp/src/manager.ts | 15 ++- packages/mcp/src/types/mcp.ts | 7 +- 26 files changed, 80 insertions(+), 402 deletions(-) delete mode 100644 api/app/clients/tools/util/addOpenAPISpecs.js delete mode 100644 api/app/clients/tools/util/addOpenAPISpecs.spec.js delete mode 100644 api/app/clients/tools/util/loadSpecs.js delete mode 100644 api/app/clients/tools/util/loadSpecs.spec.js diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 0c9a6e4d7..55b878018 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -63,15 +63,15 @@ class BaseClient { } setOptions() { - throw new Error('Method \'setOptions\' must be implemented.'); + throw new Error("Method 'setOptions' must be implemented."); } async getCompletion() { - throw new Error('Method \'getCompletion\' must be implemented.'); + throw new Error("Method 'getCompletion' must be implemented."); } async sendCompletion() { - throw new Error('Method \'sendCompletion\' must be implemented.'); + throw new Error("Method 'sendCompletion' must be implemented."); } getSaveOptions() { @@ -237,11 +237,11 @@ class BaseClient { const userMessage = opts.isEdited ? this.currentMessages[this.currentMessages.length - 2] : this.createUserMessage({ - messageId: userMessageId, - parentMessageId, - conversationId, - text: message, - }); + messageId: userMessageId, + parentMessageId, + conversationId, + text: message, + }); if (typeof opts?.getReqData === 'function') { opts.getReqData({ diff --git a/api/app/clients/tools/util/addOpenAPISpecs.js b/api/app/clients/tools/util/addOpenAPISpecs.js deleted file mode 100644 index 8b87be994..000000000 --- a/api/app/clients/tools/util/addOpenAPISpecs.js +++ /dev/null @@ -1,30 +0,0 @@ -const { loadSpecs } = require('./loadSpecs'); - -function transformSpec(input) { - return { - name: input.name_for_human, - pluginKey: input.name_for_model, - description: input.description_for_human, - icon: input?.logo_url ?? 'https://placehold.co/70x70.png', - // TODO: add support for authentication - isAuthRequired: 'false', - authConfig: [], - }; -} - -async function addOpenAPISpecs(availableTools) { - try { - const specs = (await loadSpecs({})).map(transformSpec); - if (specs.length > 0) { - return [...specs, ...availableTools]; - } - return availableTools; - } catch (error) { - return availableTools; - } -} - -module.exports = { - transformSpec, - addOpenAPISpecs, -}; diff --git a/api/app/clients/tools/util/addOpenAPISpecs.spec.js b/api/app/clients/tools/util/addOpenAPISpecs.spec.js deleted file mode 100644 index 21ff4eb8c..000000000 --- a/api/app/clients/tools/util/addOpenAPISpecs.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs'); -const { loadSpecs } = require('./loadSpecs'); -const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); - -jest.mock('./loadSpecs'); -jest.mock('../dynamic/OpenAPIPlugin'); - -describe('transformSpec', () => { - it('should transform input spec to a desired format', () => { - const input = { - name_for_human: 'Human Name', - name_for_model: 'Model Name', - description_for_human: 'Human Description', - logo_url: 'https://example.com/logo.png', - }; - - const expectedOutput = { - name: 'Human Name', - pluginKey: 'Model Name', - description: 'Human Description', - icon: 'https://example.com/logo.png', - isAuthRequired: 'false', - authConfig: [], - }; - - expect(transformSpec(input)).toEqual(expectedOutput); - }); - - it('should use default icon if logo_url is not provided', () => { - const input = { - name_for_human: 'Human Name', - name_for_model: 'Model Name', - description_for_human: 'Human Description', - }; - - const expectedOutput = { - name: 'Human Name', - pluginKey: 'Model Name', - description: 'Human Description', - icon: 'https://placehold.co/70x70.png', - isAuthRequired: 'false', - authConfig: [], - }; - - expect(transformSpec(input)).toEqual(expectedOutput); - }); -}); - -describe('addOpenAPISpecs', () => { - it('should add specs to available tools', async () => { - const availableTools = ['Tool1', 'Tool2']; - const specs = [ - { - name_for_human: 'Human Name', - name_for_model: 'Model Name', - description_for_human: 'Human Description', - logo_url: 'https://example.com/logo.png', - }, - ]; - - loadSpecs.mockResolvedValue(specs); - createOpenAPIPlugin.mockReturnValue('Plugin'); - - const result = await addOpenAPISpecs(availableTools); - expect(result).toEqual([...specs.map(transformSpec), ...availableTools]); - }); - - it('should return available tools if specs loading fails', async () => { - const availableTools = ['Tool1', 'Tool2']; - - loadSpecs.mockRejectedValue(new Error('Failed to load specs')); - - const result = await addOpenAPISpecs(availableTools); - expect(result).toEqual(availableTools); - }); -}); diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 201009513..e480dd492 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -24,7 +24,6 @@ const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/pro const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { createMCPTool } = require('~/server/services/MCP'); -const { loadSpecs } = require('./loadSpecs'); const { logger } = require('~/config'); const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`); @@ -232,7 +231,6 @@ const loadTools = async ({ /** @type {Record} */ const toolContextMap = {}; - const remainingTools = []; const appTools = options.req?.app?.locals?.availableTools ?? {}; for (const tool of tools) { @@ -292,30 +290,6 @@ const loadTools = async ({ requestedTools[tool] = toolInstance; continue; } - - if (functions === true) { - remainingTools.push(tool); - } - } - - let specs = null; - if (useSpecs === true && functions === true && remainingTools.length > 0) { - specs = await loadSpecs({ - llm: model, - user, - message: options.message, - memory: options.memory, - signal: options.signal, - tools: remainingTools, - map: true, - verbose: false, - }); - } - - for (const tool of remainingTools) { - if (specs && specs[tool]) { - requestedTools[tool] = specs[tool]; - } } if (returnMap) { diff --git a/api/app/clients/tools/util/loadSpecs.js b/api/app/clients/tools/util/loadSpecs.js deleted file mode 100644 index e5b543132..000000000 --- a/api/app/clients/tools/util/loadSpecs.js +++ /dev/null @@ -1,117 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { z } = require('zod'); -const { logger } = require('~/config'); -const { createOpenAPIPlugin } = require('~/app/clients/tools/dynamic/OpenAPIPlugin'); - -// The minimum Manifest definition -const ManifestDefinition = z.object({ - schema_version: z.string().optional(), - name_for_human: z.string(), - name_for_model: z.string(), - description_for_human: z.string(), - description_for_model: z.string(), - auth: z.object({}).optional(), - api: z.object({ - // Spec URL or can be the filename of the OpenAPI spec yaml file, - // located in api\app\clients\tools\.well-known\openapi - url: z.string(), - type: z.string().optional(), - is_user_authenticated: z.boolean().nullable().optional(), - has_user_authentication: z.boolean().nullable().optional(), - }), - // use to override any params that the LLM will consistently get wrong - params: z.object({}).optional(), - logo_url: z.string().optional(), - contact_email: z.string().optional(), - legal_info_url: z.string().optional(), -}); - -function validateJson(json) { - try { - return ManifestDefinition.parse(json); - } catch (error) { - logger.debug('[validateJson] manifest parsing error', error); - return false; - } -} - -// omit the LLM to return the well known jsons as objects -async function loadSpecs({ llm, user, message, tools = [], map = false, memory, signal }) { - const directoryPath = path.join(__dirname, '..', '.well-known'); - let files = []; - - for (let i = 0; i < tools.length; i++) { - const filePath = path.join(directoryPath, tools[i] + '.json'); - - try { - // If the access Promise is resolved, it means that the file exists - // Then we can add it to the files array - await fs.promises.access(filePath, fs.constants.F_OK); - files.push(tools[i] + '.json'); - } catch (err) { - logger.error(`[loadSpecs] File ${tools[i] + '.json'} does not exist`, err); - } - } - - if (files.length === 0) { - files = (await fs.promises.readdir(directoryPath)).filter( - (file) => path.extname(file) === '.json', - ); - } - - const validJsons = []; - const constructorMap = {}; - - logger.debug('[validateJson] files', files); - - for (const file of files) { - if (path.extname(file) === '.json') { - const filePath = path.join(directoryPath, file); - const fileContent = await fs.promises.readFile(filePath, 'utf8'); - const json = JSON.parse(fileContent); - - if (!validateJson(json)) { - logger.debug('[validateJson] Invalid json', json); - continue; - } - - if (llm && map) { - constructorMap[json.name_for_model] = async () => - await createOpenAPIPlugin({ - data: json, - llm, - message, - memory, - signal, - user, - }); - continue; - } - - if (llm) { - validJsons.push(createOpenAPIPlugin({ data: json, llm })); - continue; - } - - validJsons.push(json); - } - } - - if (map) { - return constructorMap; - } - - const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin); - - // logger.debug('[validateJson] plugins', plugins); - // logger.debug(plugins[0].name); - - return plugins; -} - -module.exports = { - loadSpecs, - validateJson, - ManifestDefinition, -}; diff --git a/api/app/clients/tools/util/loadSpecs.spec.js b/api/app/clients/tools/util/loadSpecs.spec.js deleted file mode 100644 index 7b906d86f..000000000 --- a/api/app/clients/tools/util/loadSpecs.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -const fs = require('fs'); -const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs'); -const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); - -jest.mock('../dynamic/OpenAPIPlugin'); - -describe('ManifestDefinition', () => { - it('should validate correct json', () => { - const json = { - name_for_human: 'Test', - name_for_model: 'Test', - description_for_human: 'Test', - description_for_model: 'Test', - api: { - url: 'http://test.com', - }, - }; - - expect(() => ManifestDefinition.parse(json)).not.toThrow(); - }); - - it('should not validate incorrect json', () => { - const json = { - name_for_human: 'Test', - name_for_model: 'Test', - description_for_human: 'Test', - description_for_model: 'Test', - api: { - url: 123, // incorrect type - }, - }; - - expect(() => ManifestDefinition.parse(json)).toThrow(); - }); -}); - -describe('validateJson', () => { - it('should return parsed json if valid', () => { - const json = { - name_for_human: 'Test', - name_for_model: 'Test', - description_for_human: 'Test', - description_for_model: 'Test', - api: { - url: 'http://test.com', - }, - }; - - expect(validateJson(json)).toEqual(json); - }); - - it('should return false if json is not valid', () => { - const json = { - name_for_human: 'Test', - name_for_model: 'Test', - description_for_human: 'Test', - description_for_model: 'Test', - api: { - url: 123, // incorrect type - }, - }; - - expect(validateJson(json)).toEqual(false); - }); -}); - -describe('loadSpecs', () => { - beforeEach(() => { - jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']); - jest.spyOn(fs.promises, 'readFile').mockResolvedValue( - JSON.stringify({ - name_for_human: 'Test', - name_for_model: 'Test', - description_for_human: 'Test', - description_for_model: 'Test', - api: { - url: 'http://test.com', - }, - }), - ); - createOpenAPIPlugin.mockResolvedValue({}); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should return plugins', async () => { - const plugins = await loadSpecs({ llm: true, verbose: false }); - - expect(plugins).toHaveLength(1); - expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1); - }); - - it('should return constructorMap if map is true', async () => { - const plugins = await loadSpecs({ llm: {}, map: true, verbose: false }); - - expect(plugins).toHaveProperty('Test'); - expect(createOpenAPIPlugin).not.toHaveBeenCalled(); - }); -}); diff --git a/api/models/tx.js b/api/models/tx.js index 95f0b47db..df88390b1 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -118,7 +118,8 @@ const tokenValues = Object.assign( 'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 }, 'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 }, 'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing - 'gemini-2.5-pro-preview-03-25': { prompt: 1.25, completion: 10 }, + 'gemini-2.5-pro': { prompt: 1.25, completion: 10 }, + 'gemini-2.5-flash': { prompt: 0.15, completion: 3.5 }, 'gemini-2.5': { prompt: 0, completion: 0 }, // Free for a period of time 'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 }, 'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 }, diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 5e1681072..97a730232 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -488,6 +488,9 @@ describe('getCacheMultiplier', () => { describe('Google Model Tests', () => { const googleModels = [ + 'gemini-2.5-pro-preview-05-06', + 'gemini-2.5-flash-preview-04-17', + 'gemini-2.5-exp', 'gemini-2.0-flash-lite-preview-02-05', 'gemini-2.0-flash-001', 'gemini-2.0-flash-exp', @@ -525,6 +528,9 @@ describe('Google Model Tests', () => { it('should map to the correct model keys', () => { const expected = { + 'gemini-2.5-pro-preview-05-06': 'gemini-2.5-pro', + 'gemini-2.5-flash-preview-04-17': 'gemini-2.5-flash', + 'gemini-2.5-exp': 'gemini-2.5', 'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite', 'gemini-2.0-flash-001': 'gemini-2.0-flash', 'gemini-2.0-flash-exp': 'gemini-2.0-flash', diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index 7676b7882..40b209ef3 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -228,7 +228,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { if (!client?.skipSaveUserMessage && latestUserMessage) { await saveMessage(req, latestUserMessage, { - context: 'api/server/controllers/AskController.js - don\'t skip saving user message', + context: "api/server/controllers/AskController.js - don't skip saving user message", }); } diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 8bb1df59e..674e36002 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,5 +1,4 @@ const { CacheKeys, AuthType } = require('librechat-data-provider'); -const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); const { getToolkitKey } = require('~/server/services/ToolService'); const { getCustomConfig } = require('~/server/services/Config'); const { availableTools } = require('~/app/clients/tools'); @@ -70,7 +69,7 @@ const getAvailablePluginsController = async (req, res) => { ); } - let plugins = await addOpenAPISpecs(authenticatedPlugins); + let plugins = authenticatedPlugins; if (includedTools.length > 0) { plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey)); @@ -106,11 +105,11 @@ const getAvailableTools = async (req, res) => { return; } - const pluginManifest = availableTools; + let pluginManifest = availableTools; const customConfig = await getCustomConfig(); if (customConfig?.mcpServers != null) { const mcpManager = getMCPManager(); - await mcpManager.loadManifestTools(pluginManifest); + pluginManifest = await mcpManager.loadManifestTools(pluginManifest); } /** @type {TPlugin[]} */ diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 210224d89..a3484f650 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -148,19 +148,13 @@ class AgentClient extends BaseClient { * @param {MongoFile[]} attachments */ checkVisionRequest(attachments) { - logger.info( - '[api/server/controllers/agents/client.js #checkVisionRequest] not implemented', - attachments, - ); // if (!attachments) { // return; // } - // const availableModels = this.options.modelsConfig?.[this.options.endpoint]; // if (!availableModels) { // return; // } - // let visionRequestDetected = false; // for (const file of attachments) { // if (file?.type?.includes('image')) { @@ -171,13 +165,11 @@ class AgentClient extends BaseClient { // if (!visionRequestDetected) { // return; // } - // this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels }); // if (this.isVisionModel) { // delete this.modelOptions.stop; // return; // } - // for (const model of availableModels) { // if (!validateVisionModel({ model, availableModels })) { // continue; @@ -187,14 +179,12 @@ class AgentClient extends BaseClient { // delete this.modelOptions.stop; // return; // } - // if (!availableModels.includes(this.defaultVisionModel)) { // return; // } // if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) { // return; // } - // this.modelOptions.model = this.defaultVisionModel; // this.isVisionModel = true; // delete this.modelOptions.stop; diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 6ad4a3acf..0bb1e22cf 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -56,7 +56,7 @@ const logoutUser = async (req, refreshToken) => { try { req.session.destroy(); } catch (destroyErr) { - logger.error('[logoutUser] Failed to destroy session.', destroyErr); + logger.debug('[logoutUser] Failed to destroy session.', destroyErr); } return { status: 200, message: 'Logout successful' }; diff --git a/api/utils/tokens.js b/api/utils/tokens.js index e2c311692..7ff59acfd 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -68,6 +68,8 @@ const googleModels = { 'gemini-pro-vision': 12288, 'gemini-exp': 2000000, 'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens + 'gemini-2.5-pro': 1000000, + 'gemini-2.5-flash': 1000000, 'gemini-2.0': 2000000, 'gemini-2.0-flash': 1000000, 'gemini-2.0-flash-lite': 1000000, diff --git a/client/src/components/Chat/ExportAndShareMenu.tsx b/client/src/components/Chat/ExportAndShareMenu.tsx index 4d67967cc..0ac0144da 100644 --- a/client/src/components/Chat/ExportAndShareMenu.tsx +++ b/client/src/components/Chat/ExportAndShareMenu.tsx @@ -70,6 +70,7 @@ export default function ExportAndShareMenu({ { isOpen={isPopoverActive} setIsOpen={setIsPopoverActive} modal={true} + unmountOnHide={true} trigger={menuTrigger} items={dropdownItems} iconClassName="mr-0" diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 4080e8e2b..0cb0206bc 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -31,7 +31,8 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) { select: (data) => { const serverNames = new Set(); data.forEach((tool) => { - if (tool.pluginKey.includes(Constants.mcp_delimiter)) { + const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter); + if (isMCP && tool.chatMenu !== false) { const parts = tool.pluginKey.split(Constants.mcp_delimiter); serverNames.add(parts[parts.length - 1]); } diff --git a/client/src/components/Chat/Menus/BookmarkMenu.tsx b/client/src/components/Chat/Menus/BookmarkMenu.tsx index 48945a577..58fcbfdd8 100644 --- a/client/src/components/Chat/Menus/BookmarkMenu.tsx +++ b/client/src/components/Chat/Menus/BookmarkMenu.tsx @@ -160,6 +160,7 @@ const BookmarkMenu: FC = () => { focusLoop={true} menuId={menuId} isOpen={isMenuOpen} + unmountOnHide={true} setIsOpen={setIsMenuOpen} keyPrefix={`${conversationId}-bookmark-`} trigger={ diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 1fd74dee3..87211c1a6 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -235,10 +235,11 @@ function ConvoOptions({ )} diff --git a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx index 26810f40b..c40ca5295 100644 --- a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx @@ -4,13 +4,12 @@ import { useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import type { TMessage } from 'librechat-data-provider'; import { - Label, - OGDialog, - OGDialogTitle, - OGDialogContent, - OGDialogHeader, Button, Spinner, + OGDialog, + OGDialogTitle, + OGDialogHeader, + OGDialogContent, } from '~/components'; import { useDeleteConversationMutation } from '~/data-provider'; import { useLocalize, useNewConvo } from '~/hooks'; @@ -24,14 +23,17 @@ type DeleteButtonProps = { showDeleteDialog?: boolean; setShowDeleteDialog?: (value: boolean) => void; triggerRef?: React.RefObject; + setMenuOpen?: React.Dispatch>; }; export function DeleteConversationDialog({ setShowDeleteDialog, conversationId, + setMenuOpen, retainView, title, }: { + setMenuOpen?: React.Dispatch>; setShowDeleteDialog: (value: boolean) => void; conversationId: string; retainView: () => void; @@ -51,6 +53,7 @@ export function DeleteConversationDialog({ newConversation(); navigate('/c/new', { replace: true }); } + setMenuOpen?.(false); retainView(); }, onError: () => { @@ -98,6 +101,7 @@ export default function DeleteButton({ conversationId, retainView, title, + setMenuOpen, showDeleteDialog, setShowDeleteDialog, triggerRef, @@ -115,6 +119,7 @@ export default function DeleteButton({ diff --git a/client/src/components/Prompts/AdminSettings.tsx b/client/src/components/Prompts/AdminSettings.tsx index 5311e2b37..8324c29e5 100644 --- a/client/src/components/Prompts/AdminSettings.tsx +++ b/client/src/components/Prompts/AdminSettings.tsx @@ -66,7 +66,7 @@ const AdminSettings = () => { const [confirmAdminUseChange, setConfirmAdminUseChange] = useState<{ newValue: boolean; callback: (value: boolean) => void; - } | null>(null); + } | null>(null); const { mutate, isLoading } = useUpdatePromptPermissionsMutation({ onSuccess: () => { showToast({ status: 'success', message: localize('com_ui_saved') }); @@ -166,6 +166,7 @@ const AdminSettings = () => {
{localize('com_ui_role_select')}: { setValue={setValue} {...(selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE ? { - confirmChange: ( - newValue: boolean, - onChange: (value: boolean) => void, - ) => setConfirmAdminUseChange({ newValue, callback: onChange }), - } + confirmChange: ( + newValue: boolean, + onChange: (value: boolean) => void, + ) => setConfirmAdminUseChange({ newValue, callback: onChange }), + } : {})} /> {selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && ( diff --git a/client/src/components/SidePanel/Agents/AdminSettings.tsx b/client/src/components/SidePanel/Agents/AdminSettings.tsx index cd5c0679f..fe2c62cb5 100644 --- a/client/src/components/SidePanel/Agents/AdminSettings.tsx +++ b/client/src/components/SidePanel/Agents/AdminSettings.tsx @@ -157,6 +157,7 @@ const AdminSettings = () => {
{localize('com_ui_role_select')}: { - const key = `${call.messageId}_${call.partIndex ?? 0}_${call.blockIndex ?? 0}_${call.toolId}`; - const array = acc[key] ?? []; - array.push(call); - acc[key] = array; + return toolCalls.reduce( + (acc, call) => { + const key = `${call.messageId}_${call.partIndex ?? 0}_${call.blockIndex ?? 0}_${call.toolId}`; + const array = acc[key] ?? []; + array.push(call); + acc[key] = array; - return acc; - }, {} as { [key: string]: t.ToolCallResult[] | undefined }); + return acc; + }, + {} as { [key: string]: t.ToolCallResult[] | undefined }, + ); } diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index a222f2e7e..cdfc516b8 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -5,6 +5,8 @@ const BaseOptionsSchema = z.object({ iconPath: z.string().optional(), timeout: z.number().optional(), initTimeout: z.number().optional(), + /** Controls visibility in chat dropdown menu (MCPSelect) */ + chatMenu: z.boolean().optional(), }); export const StdioOptionsSchema = BaseOptionsSchema.extend({ diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 65187afbe..d4f43785c 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -417,6 +417,7 @@ export const tPluginSchema = z.object({ icon: z.string().optional(), authConfig: z.array(tPluginAuthConfigSchema).optional(), authenticated: z.boolean().optional(), + chatMenu: z.boolean().optional(), isButton: z.boolean().optional(), toolkit: z.boolean().optional(), }); diff --git a/packages/mcp/src/manager.ts b/packages/mcp/src/manager.ts index 0ffe18136..df7458099 100644 --- a/packages/mcp/src/manager.ts +++ b/packages/mcp/src/manager.ts @@ -370,7 +370,9 @@ export class MCPManager { /** * Loads tools from all app-level connections into the manifest. */ - public async loadManifestTools(manifestTools: t.LCToolManifest): Promise { + public async loadManifestTools(manifestTools: t.LCToolManifest): Promise { + const mcpTools: t.LCManifestTool[] = []; + for (const [serverName, connection] of this.connections.entries()) { try { if (connection.isConnected() !== true) { @@ -383,17 +385,24 @@ export class MCPManager { const tools = await connection.fetchTools(); for (const tool of tools) { const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`; - manifestTools.push({ + const manifestTool: t.LCManifestTool = { name: tool.name, pluginKey, description: tool.description ?? '', icon: connection.iconPath, - }); + }; + const config = this.mcpConfigs[serverName]; + if (config?.chatMenu === false) { + manifestTool.chatMenu = false; + } + mcpTools.push(manifestTool); } } catch (error) { this.logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error); } } + + return [...mcpTools, ...manifestTools]; } /** diff --git a/packages/mcp/src/types/mcp.ts b/packages/mcp/src/types/mcp.ts index a0938ac92..7840e0e16 100644 --- a/packages/mcp/src/types/mcp.ts +++ b/packages/mcp/src/types/mcp.ts @@ -33,7 +33,7 @@ export interface LCFunctionTool { } export type LCAvailableTools = Record; - +export type LCManifestTool = TPlugin; export type LCToolManifest = TPlugin[]; export interface MCPPrompt { name: string; @@ -84,7 +84,10 @@ export type FormattedContent = }; }; -export type FormattedContentResult = [string | FormattedContent[], undefined | { content: FormattedContent[] }]; +export type FormattedContentResult = [ + string | FormattedContent[], + undefined | { content: FormattedContent[] }, +]; export type ImageFormatter = (item: ImageContent) => FormattedContent;