🗝️ 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

@ -5,6 +5,7 @@ import { createRoleMethods, type RoleMethods } from './role';
/* Memories */
import { createMemoryMethods, type MemoryMethods } from './memory';
import { createShareMethods, type ShareMethods } from './share';
import { createPluginAuthMethods, type PluginAuthMethods } from './pluginAuth';
/**
* Creates all database methods for all collections
@ -17,13 +18,15 @@ export function createMethods(mongoose: typeof import('mongoose')) {
...createRoleMethods(mongoose),
...createMemoryMethods(mongoose),
...createShareMethods(mongoose),
...createPluginAuthMethods(mongoose),
};
}
export type { MemoryMethods, ShareMethods, TokenMethods };
export type { MemoryMethods, ShareMethods, TokenMethods, PluginAuthMethods };
export type AllMethods = UserMethods &
SessionMethods &
TokenMethods &
RoleMethods &
MemoryMethods &
ShareMethods;
ShareMethods &
PluginAuthMethods;

View file

@ -0,0 +1,140 @@
import type { DeleteResult, Model } from 'mongoose';
import type { IPluginAuth } from '~/schema/pluginAuth';
import type {
FindPluginAuthsByKeysParams,
UpdatePluginAuthParams,
DeletePluginAuthParams,
FindPluginAuthParams,
} from '~/types';
// Factory function that takes mongoose instance and returns the methods
export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
/**
* Finds a single plugin auth entry by userId and authField
*/
async function findOnePluginAuth({
userId,
authField,
}: FindPluginAuthParams): Promise<IPluginAuth | null> {
try {
return await PluginAuth.findOne({ userId, authField }).lean();
} catch (error) {
throw new Error(
`Failed to find plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
/**
* Finds multiple plugin auth entries by userId and pluginKeys
*/
async function findPluginAuthsByKeys({
userId,
pluginKeys,
}: FindPluginAuthsByKeysParams): Promise<IPluginAuth[]> {
try {
if (!pluginKeys || pluginKeys.length === 0) {
return [];
}
return await PluginAuth.find({
userId,
pluginKey: { $in: pluginKeys },
}).lean();
} catch (error) {
throw new Error(
`Failed to find plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
/**
* Updates or creates a plugin auth entry
*/
async function updatePluginAuth({
userId,
authField,
pluginKey,
value,
}: UpdatePluginAuthParams): Promise<IPluginAuth> {
try {
const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
if (existingAuth) {
return await PluginAuth.findOneAndUpdate(
{ userId, pluginKey, authField },
{ $set: { value } },
{ new: true, upsert: true },
).lean();
} else {
const newPluginAuth = await new PluginAuth({
userId,
authField,
value,
pluginKey,
});
await newPluginAuth.save();
return newPluginAuth.toObject();
}
} catch (error) {
throw new Error(
`Failed to update plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
/**
* Deletes plugin auth entries based on provided parameters
*/
async function deletePluginAuth({
userId,
authField,
pluginKey,
all = false,
}: DeletePluginAuthParams): Promise<DeleteResult> {
try {
if (all) {
const filter: DeletePluginAuthParams = { userId };
if (pluginKey) {
filter.pluginKey = pluginKey;
}
return await PluginAuth.deleteMany(filter);
}
if (!authField) {
throw new Error('authField is required when all is false');
}
return await PluginAuth.deleteOne({ userId, authField });
} catch (error) {
throw new Error(
`Failed to delete plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
/**
* Deletes all plugin auth entries for a user
*/
async function deleteAllUserPluginAuths(userId: string): Promise<DeleteResult> {
try {
return await PluginAuth.deleteMany({ userId });
} catch (error) {
throw new Error(
`Failed to delete all user plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
return {
findOnePluginAuth,
findPluginAuthsByKeys,
updatePluginAuth,
deletePluginAuth,
deleteAllUserPluginAuths,
};
}
export type PluginAuthMethods = ReturnType<typeof createPluginAuthMethods>;

View file

@ -1,13 +1,5 @@
import { Schema, Document } from 'mongoose';
export interface IPluginAuth extends Document {
authField: string;
value: string;
userId: string;
pluginKey?: string;
createdAt?: Date;
updatedAt?: Date;
}
import { Schema } from 'mongoose';
import type { IPluginAuth } from '~/types';
const pluginAuthSchema: Schema<IPluginAuth> = new Schema(
{

View file

@ -14,5 +14,6 @@ export * from './action';
export * from './assistant';
export * from './file';
export * from './share';
export * from './pluginAuth';
/* Memories */
export * from './memory';

View file

@ -0,0 +1,40 @@
import type { Document } from 'mongoose';
export interface IPluginAuth extends Document {
authField: string;
value: string;
userId: string;
pluginKey?: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface PluginAuthQuery {
userId: string;
authField?: string;
pluginKey?: string;
}
export interface FindPluginAuthParams {
userId: string;
authField: string;
}
export interface FindPluginAuthsByKeysParams {
userId: string;
pluginKeys: string[];
}
export interface UpdatePluginAuthParams {
userId: string;
authField: string;
pluginKey: string;
value: string;
}
export interface DeletePluginAuthParams {
userId: string;
authField?: string;
pluginKey?: string;
all?: boolean;
}