🗝️ feat: User Provided Credentials for MCP Servers (#7980)

* 🗝️ feat: Per-User Credentials for MCP Servers

chore: add aider to gitignore

feat: fill custom variables to MCP server

feat: replace placeholders with custom user MCP variables

feat: handle MCP install/uninstall (uses pluginauths)

feat: add MCP custom variables dialog to MCPSelect

feat: add MCP custom variables dialog to the side panel

feat: do not require to fill MCP credentials for in tools dialog

feat: add translations keys (en+cs) for custom MCP variables

fix: handle LIBRECHAT_USER_ID correctly during MCP var replacement

style: remove unused MCP translation keys

style: fix eslint for MCP custom vars

chore: move aider gitignore to AI section

* feat: Add Plugin Authentication Methods to data-schemas

* refactor: Replace PluginAuth model methods with new utility functions for improved code organization and maintainability

* refactor: Move IPluginAuth interface to types directory for better organization and update pluginAuth schema to use the new import

* refactor: Remove unused getUsersPluginsAuthValuesMap function and streamline PluginService.js; add new getPluginAuthMap function for improved plugin authentication handling

* chore: fix typing for optional tools property with GenericTool[] type

* chore: update librechat-data-provider version to 0.7.88

* refactor: optimize getUserMCPAuthMap function by reducing variable usage and improving server key collection logic

* refactor: streamline MCP tool creation by removing customUserVars parameter and enhancing user-specific authentication handling to avoid closure encapsulation

* refactor: extract processSingleValue function to streamline MCP environment variable processing and enhance readability

* refactor: enhance MCP tool processing logic by simplifying conditions and improving authentication handling for custom user variables

* ci: fix action tests

* chore: fix imports, remove comments

* chore: remove non-english translations

* fix: remove newline at end of translation.json file

---------

Co-authored-by: Aleš Kůtek <kutekales@gmail.com>
This commit is contained in:
Danny Avila 2025-06-19 18:27:55 -04:00 committed by GitHub
parent 8b15bb2ed6
commit 3e4b01de82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1536 additions and 166 deletions

View file

@ -1,6 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { encrypt, decrypt } = require('@librechat/api');
const { PluginAuth } = require('~/db/models');
const { findOnePluginAuth, updatePluginAuth, deletePluginAuth } = require('~/models');
/**
* Asynchronously retrieves and decrypts the authentication value for a user's plugin, based on a specified authentication field.
@ -25,7 +25,7 @@ const { PluginAuth } = require('~/db/models');
*/
const getUserPluginAuthValue = async (userId, authField, throwError = true) => {
try {
const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean();
const pluginAuth = await findOnePluginAuth({ userId, authField });
if (!pluginAuth) {
throw new Error(`No plugin auth ${authField} found for user ${userId}`);
}
@ -79,23 +79,12 @@ const getUserPluginAuthValue = async (userId, authField, throwError = true) => {
const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
try {
const encryptedValue = await encrypt(value);
const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean();
if (pluginAuth) {
return await PluginAuth.findOneAndUpdate(
{ userId, authField },
{ $set: { value: encryptedValue } },
{ new: true, upsert: true },
).lean();
} else {
const newPluginAuth = await new PluginAuth({
userId,
authField,
value: encryptedValue,
pluginKey,
});
await newPluginAuth.save();
return newPluginAuth.toObject();
}
return await updatePluginAuth({
userId,
authField,
pluginKey,
value: encryptedValue,
});
} catch (err) {
logger.error('[updateUserPluginAuth]', err);
return err;
@ -105,26 +94,25 @@ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
/**
* @async
* @param {string} userId
* @param {string} authField
* @param {boolean} [all]
* @param {string | null} authField - The specific authField to delete, or null if `all` is true.
* @param {boolean} [all=false] - Whether to delete all auths for the user (or for a specific pluginKey if provided).
* @param {string} [pluginKey] - Optional. If `all` is true and `pluginKey` is provided, delete all auths for this user and pluginKey.
* @returns {Promise<import('mongoose').DeleteResult>}
* @throws {Error}
*/
const deleteUserPluginAuth = async (userId, authField, all = false) => {
if (all) {
try {
const response = await PluginAuth.deleteMany({ userId });
return response;
} catch (err) {
logger.error('[deleteUserPluginAuth]', err);
return err;
}
}
const deleteUserPluginAuth = async (userId, authField, all = false, pluginKey) => {
try {
return await PluginAuth.deleteOne({ userId, authField });
return await deletePluginAuth({
userId,
authField,
pluginKey,
all,
});
} catch (err) {
logger.error('[deleteUserPluginAuth]', err);
logger.error(
`[deleteUserPluginAuth] Error deleting ${all ? 'all' : 'single'} auth(s) for userId: ${userId}${pluginKey ? ` and pluginKey: ${pluginKey}` : ''}`,
err,
);
return err;
}
};