mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 03:10:15 +01:00
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json - Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates. - Update dependencies for ajv and cross-spawn to their latest versions. - Add ajv as a new dependency in the sdk module. - Include json-schema-traverse as a new dependency in the sdk module. * feat: @librechat/auth * feat: Add crypto module exports to auth package - Introduced a new crypto module by creating index.ts in the crypto directory. - Updated the main index.ts of the auth package to export from the new crypto module. * feat: Update package dependencies and build scripts for auth package - Added @librechat/auth as a dependency in package.json and package-lock.json. - Updated build scripts to include the auth package in both frontend and bun build processes. - Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management. * refactor: Migrate crypto utility functions to @librechat/auth - Replaced local crypto utility imports with the new @librechat/auth package across multiple files. - Removed the obsolete crypto.js file and its exports. - Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth. * feat: Enhance OAuth token handling and update dependencies in auth package * chore: Remove Token model and TokenService due to restructuring of OAuth handling - Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens. - This change is part of a broader refactor to streamline OAuth token management and improve code organization. * refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality * refactor: Simplify logger usage in MCP and FlowStateManager classes * chore: fix imports * feat: Add OAuth configuration schema to MCP with token exchange method support * feat: FIRST PASS Implement MCP OAuth flow with token management and error handling - Added a new route for handling OAuth callbacks and token retrieval. - Integrated OAuth token storage and retrieval mechanisms. - Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors. - Implemented dynamic client registration and metadata discovery for OAuth. - Updated MCPManager to manage OAuth tokens and handle authentication requirements. - Introduced comprehensive logging for OAuth processes and error handling. * refactor: Update MCPConnection and MCPManager to utilize new URL handling - Added a `url` property to MCPConnection for better URL management. - Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling. - Changed logging from info to debug level for flow manager and token methods initialization. - Improved comments for clarity on existing tokens and OAuth event listener setup. * refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection - Updated the connection timeout error messages to include the duration of the timeout. - Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility. * chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs * refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management - Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction. - Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability. - Improved logging messages related to token persistence and retrieval processes. * refactor: Update MCP OAuth handling to use static methods and improve flow management - Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies. - Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management. - Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval. * refactor: Integrate token methods into createMCPTool for enhanced token management * refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management * chore: clean up logging * feat: first pass, auth URL from MCP OAuth flow * chore: Improve logging format for OAuth authentication URL display * chore: cleanup mcp manager comments * feat: add connection reconnection logic in MCPManager * refactor: reorganize token storage handling in MCP - Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns. - Updated imports to reflect the new token storage structure. - Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management. * chore: update comment for SYSTEM_USER_ID in MCPManager for clarity * feat: implement refresh token functionality in MCP - Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections. - Introduced a refreshTokens function to facilitate token refresh logic. - Enhanced MCPTokenStorage to manage client information and refresh token processes. - Updated logging for better traceability during token operations. * chore: cleanup @librechat/auth * feat: implement MCP server initialization in a separate service - Added a new service to handle the initialization of MCP servers, improving code organization and readability. - Refactored the server startup logic to utilize the new initializeMCP function. - Removed redundant MCP initialization code from the main server file. * fix: don't log auth url for user connections * feat: enhance OAuth flow with success and error handling components - Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages. - Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication. - Added localization support for success and error messages in the translation files. - Implemented countdown functionality in the success component for a better user experience. * fix: refresh token handling for user connections, add missing URL and methods - add standard enum for system user id and helper for determining app-lvel vs. user-level connections * refactor: update token handling in MCPManager and MCPTokenStorage * fix: improve error logging in OAuth authentication handler * fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server) * fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens * chore: remove unused auth package directory from update configuration * ci: fix mocks in samlStrategy tests * ci: add mcpConfig to AppService test setup * chore: remove obsolete MCP OAuth implementation documentation * fix: update build script for API to use correct command * chore: bump version of @librechat/api to 1.2.4 * fix: update abort signal handling in createMCPTool function * fix: add optional clientInfo parameter to refreshTokensFunction metadata * refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management * fix: concurrent refresh token handling issue * refactor: add signal parameter to getUserConnection method for improved abort handling * chore: JSDoc typing for `loadEphemeralAgent` * refactor: update isConnectionActive method to use destructured parameters for improved readability * feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools * ci: fix agent test
354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const { SerpAPI } = require('@langchain/community/tools/serpapi');
|
|
const { Calculator } = require('@langchain/community/tools/calculator');
|
|
const { EnvVar, createCodeExecutionTool, createSearchTool } = require('@librechat/agents');
|
|
const {
|
|
Tools,
|
|
Constants,
|
|
EToolResources,
|
|
loadWebSearchAuth,
|
|
replaceSpecialVars,
|
|
} = require('librechat-data-provider');
|
|
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
|
const {
|
|
availableTools,
|
|
manifestToolMap,
|
|
// Basic Tools
|
|
GoogleSearchAPI,
|
|
// Structured Tools
|
|
DALLE3,
|
|
FluxAPI,
|
|
OpenWeather,
|
|
StructuredSD,
|
|
StructuredACS,
|
|
TraversaalSearch,
|
|
StructuredWolfram,
|
|
createYouTubeTools,
|
|
TavilySearchResults,
|
|
createOpenAIImageTools,
|
|
} = require('../');
|
|
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
|
|
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
|
const { getCachedTools } = require('~/server/services/Config');
|
|
const { createMCPTool } = require('~/server/services/MCP');
|
|
|
|
const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
|
|
|
|
/**
|
|
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
|
|
* Tools without required authentication or with valid authentication are considered valid.
|
|
*
|
|
* @param {Object} user The user object for whom to validate tool access.
|
|
* @param {Array<string>} tools An array of tool identifiers to validate. Defaults to an empty array.
|
|
* @returns {Promise<Array<string>>} A promise that resolves to an array of valid tool identifiers.
|
|
*/
|
|
const validateTools = async (user, tools = []) => {
|
|
try {
|
|
const validToolsSet = new Set(tools);
|
|
const availableToolsToValidate = availableTools.filter((tool) =>
|
|
validToolsSet.has(tool.pluginKey),
|
|
);
|
|
|
|
/**
|
|
* Validates the credentials for a given auth field or set of alternate auth fields for a tool.
|
|
* If valid admin or user authentication is found, the function returns early. Otherwise, it removes the tool from the set of valid tools.
|
|
*
|
|
* @param {string} authField The authentication field or fields (separated by "||" for alternates) to validate.
|
|
* @param {string} toolName The identifier of the tool being validated.
|
|
*/
|
|
const validateCredentials = async (authField, toolName) => {
|
|
const fields = authField.split('||');
|
|
for (const field of fields) {
|
|
const adminAuth = process.env[field];
|
|
if (adminAuth && adminAuth.length > 0) {
|
|
return;
|
|
}
|
|
|
|
let userAuth = null;
|
|
try {
|
|
userAuth = await getUserPluginAuthValue(user, field);
|
|
} catch (err) {
|
|
if (field === fields[fields.length - 1] && !userAuth) {
|
|
throw err;
|
|
}
|
|
}
|
|
if (userAuth && userAuth.length > 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
validToolsSet.delete(toolName);
|
|
};
|
|
|
|
for (const tool of availableToolsToValidate) {
|
|
if (!tool.authConfig || tool.authConfig.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
for (const auth of tool.authConfig) {
|
|
await validateCredentials(auth.authField, tool.pluginKey);
|
|
}
|
|
}
|
|
|
|
return Array.from(validToolsSet.values());
|
|
} catch (err) {
|
|
logger.error('[validateTools] There was a problem validating tools', err);
|
|
throw new Error('There was a problem validating tools');
|
|
}
|
|
};
|
|
|
|
/** @typedef {typeof import('@langchain/core/tools').Tool} ToolConstructor */
|
|
/** @typedef {import('@langchain/core/tools').Tool} Tool */
|
|
|
|
/**
|
|
* Initializes a tool with authentication values for the given user, supporting alternate authentication fields.
|
|
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
|
|
*
|
|
* @param {string} userId The user ID for which the tool is being loaded.
|
|
* @param {Array<string>} authFields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
|
* @param {ToolConstructor} ToolConstructor The constructor function for the tool to be initialized.
|
|
* @param {Object} options Optional parameters to be passed to the tool constructor alongside authentication values.
|
|
* @returns {() => Promise<Tool>} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
|
|
*/
|
|
const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => {
|
|
return async function () {
|
|
const authValues = await loadAuthValues({ userId, authFields });
|
|
return new ToolConstructor({ ...options, ...authValues, userId });
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {string} toolKey
|
|
* @returns {Array<string>}
|
|
*/
|
|
const getAuthFields = (toolKey) => {
|
|
return manifestToolMap[toolKey]?.authConfig.map((auth) => auth.authField) ?? [];
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {object} object
|
|
* @param {string} object.user
|
|
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [object.agent]
|
|
* @param {string} [object.model]
|
|
* @param {EModelEndpoint} [object.endpoint]
|
|
* @param {LoadToolOptions} [object.options]
|
|
* @param {boolean} [object.useSpecs]
|
|
* @param {Array<string>} object.tools
|
|
* @param {boolean} [object.functions]
|
|
* @param {boolean} [object.returnMap]
|
|
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
|
|
*/
|
|
const loadTools = async ({
|
|
user,
|
|
agent,
|
|
model,
|
|
endpoint,
|
|
tools = [],
|
|
options = {},
|
|
functions = true,
|
|
returnMap = false,
|
|
}) => {
|
|
const toolConstructors = {
|
|
flux: FluxAPI,
|
|
calculator: Calculator,
|
|
google: GoogleSearchAPI,
|
|
open_weather: OpenWeather,
|
|
wolfram: StructuredWolfram,
|
|
'stable-diffusion': StructuredSD,
|
|
'azure-ai-search': StructuredACS,
|
|
traversaal_search: TraversaalSearch,
|
|
tavily_search_results_json: TavilySearchResults,
|
|
};
|
|
|
|
const customConstructors = {
|
|
serpapi: async (_toolContextMap) => {
|
|
const authFields = getAuthFields('serpapi');
|
|
let envVar = authFields[0] ?? '';
|
|
let apiKey = process.env[envVar];
|
|
if (!apiKey) {
|
|
apiKey = await getUserPluginAuthValue(user, envVar);
|
|
}
|
|
return new SerpAPI(apiKey, {
|
|
location: 'Austin,Texas,United States',
|
|
hl: 'en',
|
|
gl: 'us',
|
|
});
|
|
},
|
|
youtube: async (_toolContextMap) => {
|
|
const authFields = getAuthFields('youtube');
|
|
const authValues = await loadAuthValues({ userId: user, authFields });
|
|
return createYouTubeTools(authValues);
|
|
},
|
|
image_gen_oai: async (toolContextMap) => {
|
|
const authFields = getAuthFields('image_gen_oai');
|
|
const authValues = await loadAuthValues({ userId: user, authFields });
|
|
const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? [];
|
|
let toolContext = '';
|
|
for (let i = 0; i < imageFiles.length; i++) {
|
|
const file = imageFiles[i];
|
|
if (!file) {
|
|
continue;
|
|
}
|
|
if (i === 0) {
|
|
toolContext =
|
|
'Image files provided in this request (their image IDs listed in order of appearance) available for image editing:';
|
|
}
|
|
toolContext += `\n\t- ${file.file_id}`;
|
|
if (i === imageFiles.length - 1) {
|
|
toolContext += `\n\nInclude any you need in the \`image_ids\` array when calling \`${EToolResources.image_edit}_oai\`. You may also include previously referenced or generated image IDs.`;
|
|
}
|
|
}
|
|
if (toolContext) {
|
|
toolContextMap.image_edit_oai = toolContext;
|
|
}
|
|
return createOpenAIImageTools({
|
|
...authValues,
|
|
isAgent: !!agent,
|
|
req: options.req,
|
|
imageFiles,
|
|
});
|
|
},
|
|
};
|
|
|
|
const requestedTools = {};
|
|
|
|
if (functions === true) {
|
|
toolConstructors.dalle = DALLE3;
|
|
}
|
|
|
|
/** @type {ImageGenOptions} */
|
|
const imageGenOptions = {
|
|
isAgent: !!agent,
|
|
req: options.req,
|
|
fileStrategy: options.fileStrategy,
|
|
processFileURL: options.processFileURL,
|
|
returnMetadata: options.returnMetadata,
|
|
uploadImageBuffer: options.uploadImageBuffer,
|
|
};
|
|
|
|
const toolOptions = {
|
|
flux: imageGenOptions,
|
|
dalle: imageGenOptions,
|
|
'stable-diffusion': imageGenOptions,
|
|
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' },
|
|
};
|
|
|
|
/** @type {Record<string, string>} */
|
|
const toolContextMap = {};
|
|
const appTools = (await getCachedTools({ includeGlobal: true })) ?? {};
|
|
|
|
for (const tool of tools) {
|
|
if (tool === Tools.execute_code) {
|
|
requestedTools[tool] = async () => {
|
|
const authValues = await loadAuthValues({
|
|
userId: user,
|
|
authFields: [EnvVar.CODE_API_KEY],
|
|
});
|
|
const codeApiKey = authValues[EnvVar.CODE_API_KEY];
|
|
const { files, toolContext } = await primeCodeFiles(options, codeApiKey);
|
|
if (toolContext) {
|
|
toolContextMap[tool] = toolContext;
|
|
}
|
|
const CodeExecutionTool = createCodeExecutionTool({
|
|
user_id: user,
|
|
files,
|
|
...authValues,
|
|
});
|
|
CodeExecutionTool.apiKey = codeApiKey;
|
|
return CodeExecutionTool;
|
|
};
|
|
continue;
|
|
} else if (tool === Tools.file_search) {
|
|
requestedTools[tool] = async () => {
|
|
const { files, toolContext } = await primeSearchFiles(options);
|
|
if (toolContext) {
|
|
toolContextMap[tool] = toolContext;
|
|
}
|
|
return createFileSearchTool({ req: options.req, files, entity_id: agent?.id });
|
|
};
|
|
continue;
|
|
} else if (tool === Tools.web_search) {
|
|
const webSearchConfig = options?.req?.app?.locals?.webSearch;
|
|
const result = await loadWebSearchAuth({
|
|
userId: user,
|
|
loadAuthValues,
|
|
webSearchConfig,
|
|
});
|
|
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
|
|
requestedTools[tool] = async () => {
|
|
toolContextMap[tool] = `# \`${tool}\`:
|
|
Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
|
|
1. **Execute immediately without preface** when using \`${tool}\`.
|
|
2. **After the search, begin with a brief summary** that directly addresses the query without headers or explaining your process.
|
|
3. **Structure your response clearly** using Markdown formatting (Level 2 headers for sections, lists for multiple points, tables for comparisons).
|
|
4. **Cite sources properly** according to the citation anchor format, utilizing group anchors when appropriate.
|
|
5. **Tailor your approach to the query type** (academic, news, coding, etc.) while maintaining an expert, journalistic, unbiased tone.
|
|
6. **Provide comprehensive information** with specific details, examples, and as much relevant context as possible from search results.
|
|
7. **Avoid moralizing language.**
|
|
`.trim();
|
|
return createSearchTool({
|
|
...result.authResult,
|
|
onSearchResults,
|
|
onGetHighlights,
|
|
logger,
|
|
});
|
|
};
|
|
continue;
|
|
} else if (tool && appTools[tool] && mcpToolPattern.test(tool)) {
|
|
requestedTools[tool] = async () =>
|
|
createMCPTool({
|
|
req: options.req,
|
|
res: options.res,
|
|
toolKey: tool,
|
|
model: agent?.model ?? model,
|
|
provider: agent?.provider ?? endpoint,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (customConstructors[tool]) {
|
|
requestedTools[tool] = async () => customConstructors[tool](toolContextMap);
|
|
continue;
|
|
}
|
|
|
|
if (toolConstructors[tool]) {
|
|
const options = toolOptions[tool] || {};
|
|
const toolInstance = loadToolWithAuth(
|
|
user,
|
|
getAuthFields(tool),
|
|
toolConstructors[tool],
|
|
options,
|
|
);
|
|
requestedTools[tool] = toolInstance;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (returnMap) {
|
|
return requestedTools;
|
|
}
|
|
|
|
const toolPromises = [];
|
|
for (const tool of tools) {
|
|
const validTool = requestedTools[tool];
|
|
if (validTool) {
|
|
toolPromises.push(
|
|
validTool().catch((error) => {
|
|
logger.error(`Error loading tool ${tool}:`, error);
|
|
return null;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []);
|
|
return { loadedTools, toolContextMap };
|
|
};
|
|
|
|
module.exports = {
|
|
loadToolWithAuth,
|
|
validateTools,
|
|
loadTools,
|
|
};
|