LibreChat/api/server/controllers/PluginController.js
Danny Avila bcf469fe6b
feat(mcp): Implement dedicated MCP tools controller and routes
- Added a new controller for handling MCP-specific tool endpoints, decoupled from regular LibreChat tools.
- Implemented a route to fetch all available MCP tools for the user.
- Updated caching mechanisms to support MCP server-specific tools.
- Refactored existing code to replace user tools caching with server tools caching.
- Introduced new hooks and queries for fetching MCP tools in the client application.
- Updated the AgentPanelProvider to integrate MCP tools alongside regular tools.
- Modified various components to utilize the new MCP tools query and ensure proper refetching of data.
- Enhanced error handling and logging for MCP tools operations.
2025-09-20 12:01:52 -04:00

129 lines
4.8 KiB
JavaScript

const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { getToolkitKey, checkPluginAuth, filterUniquePlugins } = require('@librechat/api');
const { getCachedTools, setCachedTools } = require('~/server/services/Config');
const { availableTools, toolkits } = require('~/app/clients/tools');
const { getAppConfig } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
const getAvailablePluginsController = async (req, res) => {
try {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedPlugins = await cache.get(CacheKeys.PLUGINS);
if (cachedPlugins) {
res.status(200).json(cachedPlugins);
return;
}
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {{ filteredTools: string[], includedTools: string[] }} */
const { filteredTools = [], includedTools = [] } = appConfig;
/** @type {import('@librechat/api').LCManifestTool[]} */
const pluginManifest = availableTools;
const uniquePlugins = filterUniquePlugins(pluginManifest);
let authenticatedPlugins = [];
for (const plugin of uniquePlugins) {
authenticatedPlugins.push(
checkPluginAuth(plugin) ? { ...plugin, authenticated: true } : plugin,
);
}
let plugins = authenticatedPlugins;
if (includedTools.length > 0) {
plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
} else {
plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));
}
await cache.set(CacheKeys.PLUGINS, plugins);
res.status(200).json(plugins);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
/**
* Retrieves and returns a list of available tools, either from a cache or by reading a plugin manifest file.
*
* This function first attempts to retrieve the list of tools from a cache. If the tools are not found in the cache,
* it reads a plugin manifest file, filters for unique plugins, and determines if each plugin is authenticated.
* Only plugins that are marked as available in the application's local state are included in the final list.
* The resulting list of tools is then cached and sent to the client.
*
* @param {object} req - The request object, containing information about the HTTP request.
* @param {object} res - The response object, used to send back the desired HTTP response.
* @returns {Promise<void>} A promise that resolves when the function has completed.
*/
const getAvailableTools = async (req, res) => {
try {
const userId = req.user?.id;
if (!userId) {
logger.warn('[getAvailableTools] User ID not found in request');
return res.status(401).json({ message: 'Unauthorized' });
}
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
// Return early if we have cached tools
if (cachedToolsArray != null) {
res.status(200).json(cachedToolsArray);
return;
}
/** @type {Record<string, FunctionTool> | null} Get tool definitions to filter which tools are actually available */
let toolDefinitions = await getCachedTools();
if (toolDefinitions == null && appConfig?.availableTools != null) {
logger.warn('[getAvailableTools] Tool cache was empty, re-initializing from app config');
await setCachedTools(appConfig.availableTools);
toolDefinitions = appConfig.availableTools;
}
/** @type {import('@librechat/api').LCManifestTool[]} */
let pluginManifest = availableTools;
/** @type {TPlugin[]} Deduplicate and authenticate plugins */
const uniquePlugins = filterUniquePlugins(pluginManifest);
const authenticatedPlugins = uniquePlugins.map((plugin) => {
if (checkPluginAuth(plugin)) {
return { ...plugin, authenticated: true };
} else {
return plugin;
}
});
/** Filter plugins based on availability */
const toolsOutput = [];
for (const plugin of authenticatedPlugins) {
const isToolDefined = toolDefinitions?.[plugin.pluginKey] !== undefined;
const isToolkit =
plugin.toolkit === true &&
Object.keys(toolDefinitions ?? {}).some(
(key) => getToolkitKey({ toolkits, toolName: key }) === plugin.pluginKey,
);
if (!isToolDefined && !isToolkit) {
continue;
}
toolsOutput.push(plugin);
}
const finalTools = filterUniquePlugins(toolsOutput);
await cache.set(CacheKeys.TOOLS, finalTools);
res.status(200).json(finalTools);
} catch (error) {
logger.error('[getAvailableTools]', error);
res.status(500).json({ message: error.message });
}
};
module.exports = {
getAvailableTools,
getAvailablePluginsController,
};