Skip to content

Commit 5dd1ba0

Browse files
danny-avilaMichielMAnalytics
authored andcommitted
🪐 feat: MCP OAuth 2.0 Discovery Support (danny-avila#7924)
* 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
1 parent 986e17d commit 5dd1ba0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3281
-948
lines changed

api/cache/getLogStores.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const roles = isRedisEnabled
2929
? new Keyv({ store: keyvRedis })
3030
: new Keyv({ namespace: CacheKeys.ROLES });
3131

32+
const mcpTools = isRedisEnabled
33+
? new Keyv({ store: keyvRedis })
34+
: new Keyv({ namespace: CacheKeys.MCP_TOOLS });
35+
3236
const audioRuns = isRedisEnabled
3337
? new Keyv({ store: keyvRedis, ttl: Time.TEN_MINUTES })
3438
: new Keyv({ namespace: CacheKeys.AUDIO_RUNS, ttl: Time.TEN_MINUTES });
@@ -67,6 +71,7 @@ const openIdExchangedTokensCache = isRedisEnabled
6771

6872
const namespaces = {
6973
[CacheKeys.ROLES]: roles,
74+
[CacheKeys.MCP_TOOLS]: mcpTools,
7075
[CacheKeys.CONFIG_STORE]: config,
7176
[CacheKeys.PENDING_REQ]: pending_req,
7277
[ViolationTypes.BAN]: new Keyv({ store: keyvMongo, namespace: CacheKeys.BANS, ttl: duration }),

api/config/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ let flowManager = null;
1616
*/
1717
function getMCPManager(userId) {
1818
if (!mcpManager) {
19-
mcpManager = MCPManager.getInstance(logger);
19+
mcpManager = MCPManager.getInstance();
2020
} else {
2121
mcpManager.checkIdleConnections(userId);
2222
}
@@ -31,7 +31,6 @@ function getFlowStateManager(flowsCache) {
3131
if (!flowManager) {
3232
flowManager = new FlowStateManager(flowsCache, {
3333
ttl: Time.ONE_MINUTE * 3,
34-
logger,
3534
});
3635
}
3736
return flowManager;

api/models/Agent.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
removeAgentIdsFromProject,
1212
removeAgentFromAllProjects,
1313
} = require('./Project');
14+
const { getCachedTools } = require('~/server/services/Config');
1415
const getLogStores = require('~/cache/getLogStores');
1516
const { getActions } = require('./Action');
1617
const { Agent } = require('~/db/models');
@@ -55,12 +56,12 @@ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter)
5556
* @param {string} params.agent_id
5657
* @param {string} params.endpoint
5758
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
58-
* @returns {Agent|null} The agent document as a plain object, or null if not found.
59+
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
5960
*/
60-
const loadEphemeralAgent = ({ req, agent_id, endpoint, model_parameters: _m }) => {
61+
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
6162
const { model, ...model_parameters } = _m;
6263
/** @type {Record<string, FunctionTool>} */
63-
const availableTools = req.app.locals.availableTools;
64+
const availableTools = await getCachedTools({ includeGlobal: true });
6465
/** @type {TEphemeralAgent | null} */
6566
const ephemeralAgent = req.body.ephemeralAgent;
6667
const mcpServers = new Set(ephemeralAgent?.mcp);
@@ -117,7 +118,7 @@ const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
117118
return null;
118119
}
119120
if (agent_id === EPHEMERAL_AGENT_ID) {
120-
return loadEphemeralAgent({ req, agent_id, endpoint, model_parameters });
121+
return await loadEphemeralAgent({ req, agent_id, endpoint, model_parameters });
121122
}
122123
const agent = await getAgent({
123124
id: agent_id,

api/models/Token.js

Lines changed: 0 additions & 44 deletions
This file was deleted.

api/models/inviteUser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const mongoose = require('mongoose');
2+
const { getRandomValues } = require('@librechat/api');
23
const { logger, hashToken } = require('@librechat/data-schemas');
3-
const { getRandomValues } = require('~/server/utils/crypto');
44
const { createToken, findToken } = require('~/models');
55

66
/**

api/server/controllers/TwoFactorController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const { encryptV3 } = require('@librechat/api');
12
const { logger } = require('@librechat/data-schemas');
23
const {
34
verifyTOTP,
@@ -7,7 +8,6 @@ const {
78
generateBackupCodes,
89
} = require('~/server/services/twoFactorService');
910
const { getUserById, updateUser } = require('~/models');
10-
const { encryptV3 } = require('~/server/utils/crypto');
1111

1212
const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, '');
1313

api/server/controllers/assistants/v1.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
const fs = require('fs').promises;
2+
const { logger } = require('@librechat/data-schemas');
23
const { FileContext } = require('librechat-data-provider');
34
const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process');
45
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
56
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
67
const { deleteAssistantActions } = require('~/server/services/ActionService');
78
const { updateAssistantDoc, getAssistants } = require('~/models/Assistant');
89
const { getOpenAIClient, fetchAssistants } = require('./helpers');
10+
const { getCachedTools } = require('~/server/services/Config');
911
const { manifestToolMap } = require('~/app/clients/tools');
1012
const { deleteFileByFilter } = require('~/models/File');
11-
const { logger } = require('~/config');
1213

1314
/**
1415
* Create an assistant.
@@ -30,21 +31,20 @@ const createAssistant = async (req, res) => {
3031
delete assistantData.conversation_starters;
3132
delete assistantData.append_current_datetime;
3233

34+
const toolDefinitions = await getCachedTools({ includeGlobal: true });
35+
3336
assistantData.tools = tools
3437
.map((tool) => {
3538
if (typeof tool !== 'string') {
3639
return tool;
3740
}
3841

39-
const toolDefinitions = req.app.locals.availableTools;
4042
const toolDef = toolDefinitions[tool];
4143
if (!toolDef && manifestToolMap[tool] && manifestToolMap[tool].toolkit === true) {
42-
return (
43-
Object.entries(toolDefinitions)
44-
.filter(([key]) => key.startsWith(`${tool}_`))
45-
// eslint-disable-next-line no-unused-vars
46-
.map(([_, val]) => val)
47-
);
44+
return Object.entries(toolDefinitions)
45+
.filter(([key]) => key.startsWith(`${tool}_`))
46+
47+
.map(([_, val]) => val);
4848
}
4949

5050
return toolDef;
@@ -135,21 +135,21 @@ const patchAssistant = async (req, res) => {
135135
append_current_datetime,
136136
...updateData
137137
} = req.body;
138+
139+
const toolDefinitions = await getCachedTools({ includeGlobal: true });
140+
138141
updateData.tools = (updateData.tools ?? [])
139142
.map((tool) => {
140143
if (typeof tool !== 'string') {
141144
return tool;
142145
}
143146

144-
const toolDefinitions = req.app.locals.availableTools;
145147
const toolDef = toolDefinitions[tool];
146148
if (!toolDef && manifestToolMap[tool] && manifestToolMap[tool].toolkit === true) {
147-
return (
148-
Object.entries(toolDefinitions)
149-
.filter(([key]) => key.startsWith(`${tool}_`))
150-
// eslint-disable-next-line no-unused-vars
151-
.map(([_, val]) => val)
152-
);
149+
return Object.entries(toolDefinitions)
150+
.filter(([key]) => key.startsWith(`${tool}_`))
151+
152+
.map(([_, val]) => val);
153153
}
154154

155155
return toolDef;

api/server/controllers/assistants/v2.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
const { logger } = require('@librechat/data-schemas');
12
const { ToolCallTypes } = require('librechat-data-provider');
23
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
34
const { validateAndUpdateTool } = require('~/server/services/ActionService');
5+
const { getCachedTools } = require('~/server/services/Config');
46
const { updateAssistantDoc } = require('~/models/Assistant');
57
const { manifestToolMap } = require('~/app/clients/tools');
68
const { getOpenAIClient } = require('./helpers');
7-
const { logger } = require('~/config');
89

910
/**
1011
* Create an assistant.
@@ -27,21 +28,20 @@ const createAssistant = async (req, res) => {
2728
delete assistantData.conversation_starters;
2829
delete assistantData.append_current_datetime;
2930

31+
const toolDefinitions = await getCachedTools({ includeGlobal: true });
32+
3033
assistantData.tools = tools
3134
.map((tool) => {
3235
if (typeof tool !== 'string') {
3336
return tool;
3437
}
3538

36-
const toolDefinitions = req.app.locals.availableTools;
3739
const toolDef = toolDefinitions[tool];
3840
if (!toolDef && manifestToolMap[tool] && manifestToolMap[tool].toolkit === true) {
39-
return (
40-
Object.entries(toolDefinitions)
41-
.filter(([key]) => key.startsWith(`${tool}_`))
42-
// eslint-disable-next-line no-unused-vars
43-
.map(([_, val]) => val)
44-
);
41+
return Object.entries(toolDefinitions)
42+
.filter(([key]) => key.startsWith(`${tool}_`))
43+
44+
.map(([_, val]) => val);
4545
}
4646

4747
return toolDef;
@@ -125,13 +125,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
125125

126126
let hasFileSearch = false;
127127
for (const tool of updateData.tools ?? []) {
128-
const toolDefinitions = req.app.locals.availableTools;
128+
const toolDefinitions = await getCachedTools({ includeGlobal: true });
129129
let actualTool = typeof tool === 'string' ? toolDefinitions[tool] : tool;
130130

131131
if (!actualTool && manifestToolMap[tool] && manifestToolMap[tool].toolkit === true) {
132132
actualTool = Object.entries(toolDefinitions)
133133
.filter(([key]) => key.startsWith(`${tool}_`))
134-
// eslint-disable-next-line no-unused-vars
134+
135135
.map(([_, val]) => val);
136136
} else if (!actualTool) {
137137
continue;

0 commit comments

Comments
 (0)