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

@ -0,0 +1,93 @@
import { logger } from '@librechat/data-schemas';
import type { IPluginAuth, PluginAuthMethods } from '@librechat/data-schemas';
import { decrypt } from '../crypto/encryption';
export interface GetPluginAuthMapParams {
userId: string;
pluginKeys: string[];
throwError?: boolean;
findPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'];
}
export type PluginAuthMap = Record<string, Record<string, string>>;
/**
* Retrieves and decrypts authentication values for multiple plugins
* @returns A map where keys are pluginKeys and values are objects of authField:decryptedValue pairs
*/
export async function getPluginAuthMap({
userId,
pluginKeys,
throwError = true,
findPluginAuthsByKeys,
}: GetPluginAuthMapParams): Promise<PluginAuthMap> {
try {
/** Early return for empty plugin keys */
if (!pluginKeys?.length) {
return {};
}
/** All plugin auths for current user query */
const pluginAuths = await findPluginAuthsByKeys({ userId, pluginKeys });
/** Group auth records by pluginKey for efficient lookup */
const authsByPlugin = new Map<string, IPluginAuth[]>();
for (const auth of pluginAuths) {
if (!auth.pluginKey) {
logger.warn(`[getPluginAuthMap] Missing pluginKey for userId ${userId}`);
continue;
}
const existing = authsByPlugin.get(auth.pluginKey) || [];
existing.push(auth);
authsByPlugin.set(auth.pluginKey, existing);
}
const authMap: PluginAuthMap = {};
const decryptionPromises: Promise<void>[] = [];
/** Single loop through requested pluginKeys */
for (const pluginKey of pluginKeys) {
authMap[pluginKey] = {};
const auths = authsByPlugin.get(pluginKey) || [];
for (const auth of auths) {
decryptionPromises.push(
(async () => {
try {
const decryptedValue = await decrypt(auth.value);
authMap[pluginKey][auth.authField] = decryptedValue;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error(
`[getPluginAuthMap] Decryption failed for userId ${userId}, plugin ${pluginKey}, field ${auth.authField}: ${message}`,
);
if (throwError) {
throw new Error(
`Decryption failed for plugin ${pluginKey}, field ${auth.authField}: ${message}`,
);
}
}
})(),
);
}
}
await Promise.all(decryptionPromises);
return authMap;
} catch (error) {
if (!throwError) {
/** Empty objects for each plugin key on error */
return pluginKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {} as PluginAuthMap);
}
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error(
`[getPluginAuthMap] Failed to fetch auth values for userId ${userId}, plugins: ${pluginKeys.join(', ')}: ${message}`,
);
throw error;
}
}

View file

@ -1,6 +1,12 @@
import { Run, Providers } from '@librechat/agents';
import { providerEndpointMap, KnownEndpoints } from 'librechat-data-provider';
import type { StandardGraphConfig, EventHandler, GraphEvents, IState } from '@librechat/agents';
import type {
StandardGraphConfig,
EventHandler,
GenericTool,
GraphEvents,
IState,
} from '@librechat/agents';
import type { Agent } from 'librechat-data-provider';
import type * as t from '~/types';
@ -32,7 +38,7 @@ export async function createRun({
streaming = true,
streamUsage = true,
}: {
agent: Agent;
agent: Omit<Agent, 'tools'> & { tools?: GenericTool[] };
signal: AbortSignal;
runId?: string;
streaming?: boolean;