Compare commits
15 Commits
dev
...
feat/ui-ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7bf0b35f | ||
|
|
568ec2f7d5 | ||
|
|
234827dc57 | ||
|
|
b055716d36 | ||
|
|
ec8cad3362 | ||
|
|
02c7f744ba | ||
|
|
d18b2c3f1f | ||
|
|
b501afe7a6 | ||
|
|
7a73d2daf3 | ||
|
|
dc03986149 | ||
|
|
c0ddfefd2a | ||
|
|
2717bdc36a | ||
|
|
389ab1db77 | ||
|
|
cf91dc3aad | ||
|
|
c5cd9eb359 |
@@ -453,8 +453,8 @@ OPENID_REUSE_TOKENS=
|
|||||||
OPENID_JWKS_URL_CACHE_ENABLED=
|
OPENID_JWKS_URL_CACHE_ENABLED=
|
||||||
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
|
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
|
||||||
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
|
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
|
||||||
OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED=
|
OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED=
|
||||||
OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API
|
OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for Microsoft Graph API
|
||||||
# Set to true to use the OpenID Connect end session endpoint for logout
|
# Set to true to use the OpenID Connect end session endpoint for logout
|
||||||
OPENID_USE_END_SESSION_ENDPOINT=
|
OPENID_USE_END_SESSION_ENDPOINT=
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
||||||
|
|
||||||
const footer = `Use the context as your learned knowledge to better answer the user.
|
const footer = `Use the context as your learned knowledge to better answer the user.
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ function createContextHandlers(req, userMessageContent) {
|
|||||||
const queryPromises = [];
|
const queryPromises = [];
|
||||||
const processedFiles = [];
|
const processedFiles = [];
|
||||||
const processedIds = new Set();
|
const processedIds = new Set();
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = generateShortLivedToken(req.user.id);
|
||||||
const useFullContext = isEnabled(process.env.RAG_USE_FULL_CONTEXT);
|
const useFullContext = isEnabled(process.env.RAG_USE_FULL_CONTEXT);
|
||||||
|
|
||||||
const query = async (file) => {
|
const query = async (file) => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { tool } = require('@langchain/core/tools');
|
const { tool } = require('@langchain/core/tools');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { Tools, EToolResources } = require('librechat-data-provider');
|
const { Tools, EToolResources } = require('librechat-data-provider');
|
||||||
|
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -59,7 +60,7 @@ const createFileSearchTool = async ({ req, files, entity_id }) => {
|
|||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return 'No files to search. Instruct the user to add files for the search.';
|
return 'No files to search. Instruct the user to add files for the search.';
|
||||||
}
|
}
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = generateShortLivedToken(req.user.id);
|
||||||
if (!jwtToken) {
|
if (!jwtToken) {
|
||||||
return 'There was an error authenticating the file search request.';
|
return 'There was an error authenticating the file search request.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const {
|
|||||||
sendEvent,
|
sendEvent,
|
||||||
createRun,
|
createRun,
|
||||||
Tokenizer,
|
Tokenizer,
|
||||||
|
checkAccess,
|
||||||
memoryInstructions,
|
memoryInstructions,
|
||||||
createMemoryProcessor,
|
createMemoryProcessor,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
@@ -39,8 +40,8 @@ const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
|||||||
const { getFormattedMemories, deleteMemory, setMemory } = require('~/models');
|
const { getFormattedMemories, deleteMemory, setMemory } = require('~/models');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { getProviderConfig } = require('~/server/services/Endpoints');
|
const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||||
const { checkAccess } = require('~/server/middleware/roles/access');
|
|
||||||
const BaseClient = require('~/app/clients/BaseClient');
|
const BaseClient = require('~/app/clients/BaseClient');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { loadAgent } = require('~/models/Agent');
|
const { loadAgent } = require('~/models/Agent');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
|
|
||||||
@@ -401,7 +402,12 @@ class AgentClient extends BaseClient {
|
|||||||
if (user.personalization?.memories === false) {
|
if (user.personalization?.memories === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasAccess = await checkAccess(user, PermissionTypes.MEMORIES, [Permissions.USE]);
|
const hasAccess = await checkAccess({
|
||||||
|
user,
|
||||||
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { EnvVar } = require('@librechat/agents');
|
const { EnvVar } = require('@librechat/agents');
|
||||||
|
const { checkAccess } = require('@librechat/api');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const {
|
const {
|
||||||
Tools,
|
Tools,
|
||||||
AuthType,
|
AuthType,
|
||||||
@@ -13,9 +15,8 @@ const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
|||||||
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
||||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||||
const { loadTools } = require('~/app/clients/tools/util');
|
const { loadTools } = require('~/app/clients/tools/util');
|
||||||
const { checkAccess } = require('~/server/middleware');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { getMessage } = require('~/models/Message');
|
const { getMessage } = require('~/models/Message');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
const fieldsMap = {
|
const fieldsMap = {
|
||||||
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
||||||
@@ -79,6 +80,7 @@ const verifyToolAuth = async (req, res) => {
|
|||||||
throwError: false,
|
throwError: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('Error loading auth values', error);
|
||||||
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -132,7 +134,12 @@ const callTool = async (req, res) => {
|
|||||||
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
||||||
let hasAccess = true;
|
let hasAccess = true;
|
||||||
if (toolAccessPermType[toolId]) {
|
if (toolAccessPermType[toolId]) {
|
||||||
hasAccess = await checkAccess(req.user, toolAccessPermType[toolId], [Permissions.USE]);
|
hasAccess = await checkAccess({
|
||||||
|
user: req.user,
|
||||||
|
permissionType: toolAccessPermType[toolId],
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
const { getRoleByName } = require('~/models/Role');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core function to check if a user has one or more required permissions
|
|
||||||
*
|
|
||||||
* @param {object} user - The user object
|
|
||||||
* @param {PermissionTypes} permissionType - The type of permission to check
|
|
||||||
* @param {Permissions[]} permissions - The list of specific permissions to check
|
|
||||||
* @param {Record<Permissions, string[]>} [bodyProps] - An optional object where keys are permissions and values are arrays of properties to check
|
|
||||||
* @param {object} [checkObject] - The object to check properties against
|
|
||||||
* @returns {Promise<boolean>} Whether the user has the required permissions
|
|
||||||
*/
|
|
||||||
const checkAccess = async (user, permissionType, permissions, bodyProps = {}, checkObject = {}) => {
|
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = await getRoleByName(user.role);
|
|
||||||
if (role && role.permissions && role.permissions[permissionType]) {
|
|
||||||
const hasAnyPermission = permissions.some((permission) => {
|
|
||||||
if (role.permissions[permissionType][permission]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bodyProps[permission] && checkObject) {
|
|
||||||
return bodyProps[permission].some((prop) =>
|
|
||||||
Object.prototype.hasOwnProperty.call(checkObject, prop),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return hasAnyPermission;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
|
|
||||||
*
|
|
||||||
* @param {PermissionTypes} permissionType - The type of permission to check.
|
|
||||||
* @param {Permissions[]} permissions - The list of specific permissions to check.
|
|
||||||
* @param {Record<Permissions, string[]>} [bodyProps] - An optional object where keys are permissions and values are arrays of `req.body` properties to check.
|
|
||||||
* @returns {(req: ServerRequest, res: ServerResponse, next: NextFunction) => Promise<void>} Express middleware function.
|
|
||||||
*/
|
|
||||||
const generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
|
|
||||||
return async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const hasAccess = await checkAccess(
|
|
||||||
req.user,
|
|
||||||
permissionType,
|
|
||||||
permissions,
|
|
||||||
bodyProps,
|
|
||||||
req.body,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasAccess) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn(
|
|
||||||
`[${permissionType}] Forbidden: Insufficient permissions for User ${req.user.id}: ${permissions.join(', ')}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return res.status(500).json({ message: `Server error: ${error.message}` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkAccess,
|
|
||||||
generateCheckAccess,
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
const checkAdmin = require('./admin');
|
const checkAdmin = require('./admin');
|
||||||
const { checkAccess, generateCheckAccess } = require('./access');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkAdmin,
|
checkAdmin,
|
||||||
checkAccess,
|
|
||||||
generateCheckAccess,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { actionDelimiter, SystemRoles, removeNullishValues } = require('librechat-data-provider');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
|
const {
|
||||||
|
SystemRoles,
|
||||||
|
Permissions,
|
||||||
|
PermissionTypes,
|
||||||
|
actionDelimiter,
|
||||||
|
removeNullishValues,
|
||||||
|
} = require('librechat-data-provider');
|
||||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
const { getAgent, updateAgent } = require('~/models/Agent');
|
const { getAgent, updateAgent } = require('~/models/Agent');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const checkAgentCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
// If the user has ADMIN role
|
// If the user has ADMIN role
|
||||||
// then action edition is possible even if not owner of the assistant
|
// then action edition is possible even if not owner of the assistant
|
||||||
const isAdmin = (req) => {
|
const isAdmin = (req) => {
|
||||||
@@ -41,7 +55,7 @@ router.get('/', async (req, res) => {
|
|||||||
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
|
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
|
||||||
* @returns {Object} 200 - success response - application/json
|
* @returns {Object} 200 - success response - application/json
|
||||||
*/
|
*/
|
||||||
router.post('/:agent_id', async (req, res) => {
|
router.post('/:agent_id', checkAgentCreate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { agent_id } = req.params;
|
const { agent_id } = req.params;
|
||||||
|
|
||||||
@@ -149,7 +163,7 @@ router.post('/:agent_id', async (req, res) => {
|
|||||||
* @param {string} req.params.action_id - The ID of the action to delete.
|
* @param {string} req.params.action_id - The ID of the action to delete.
|
||||||
* @returns {Object} 200 - success response - application/json
|
* @returns {Object} 200 - success response - application/json
|
||||||
*/
|
*/
|
||||||
router.delete('/:agent_id/:action_id', async (req, res) => {
|
router.delete('/:agent_id/:action_id', checkAgentCreate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { agent_id, action_id } = req.params;
|
const { agent_id, action_id } = req.params;
|
||||||
const admin = isAdmin(req);
|
const admin = isAdmin(req);
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { generateCheckAccess, skipAgentCheck } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
setHeaders,
|
setHeaders,
|
||||||
moderateText,
|
moderateText,
|
||||||
// validateModel,
|
// validateModel,
|
||||||
generateCheckAccess,
|
|
||||||
validateConvoAccess,
|
validateConvoAccess,
|
||||||
buildEndpointOption,
|
buildEndpointOption,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
||||||
const AgentController = require('~/server/controllers/agents/request');
|
const AgentController = require('~/server/controllers/agents/request');
|
||||||
const addTitle = require('~/server/services/Endpoints/agents/title');
|
const addTitle = require('~/server/services/Endpoints/agents/title');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(moderateText);
|
router.use(moderateText);
|
||||||
|
|
||||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
const checkAgentAccess = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
skipCheck: skipAgentCheck,
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(checkAgentAccess);
|
router.use(checkAgentAccess);
|
||||||
router.use(validateConvoAccess);
|
router.use(validateConvoAccess);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { addTool, updateTool, deleteTool } = require('@librechat/api');
|
||||||
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
||||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||||
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
||||||
@@ -36,4 +37,29 @@ router.get('/:toolId/auth', verifyToolAuth);
|
|||||||
*/
|
*/
|
||||||
router.post('/:toolId/call', toolCallLimiter, callTool);
|
router.post('/:toolId/call', toolCallLimiter, callTool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool/MCP to the system
|
||||||
|
* @route POST /agents/tools/add
|
||||||
|
* @param {object} req.body - Request body containing tool/MCP data
|
||||||
|
* @returns {object} Created tool/MCP object
|
||||||
|
*/
|
||||||
|
router.post('/add', addTool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing tool/MCP in the system
|
||||||
|
* @route PUT /agents/tools/:mcp_id
|
||||||
|
* @param {string} mcp_id - The ID of the MCP to update
|
||||||
|
* @param {object} req.body - Request body containing updated tool/MCP data
|
||||||
|
* @returns {object} Updated tool/MCP object
|
||||||
|
*/
|
||||||
|
router.put('/:mcp_id', updateTool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a tool/MCP from the system
|
||||||
|
* @route DELETE /agents/tools/:mcp_id
|
||||||
|
* @param {string} mcp_id - The ID of the MCP to delete
|
||||||
|
* @returns {object} Deletion confirmation
|
||||||
|
*/
|
||||||
|
router.delete('/:mcp_id', deleteTool);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,29 +1,36 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const v1 = require('~/server/controllers/agents/v1');
|
const v1 = require('~/server/controllers/agents/v1');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const actions = require('./actions');
|
const actions = require('./actions');
|
||||||
const tools = require('./tools');
|
const tools = require('./tools');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const avatar = express.Router();
|
const avatar = express.Router();
|
||||||
|
|
||||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
const checkAgentAccess = generateCheckAccess({
|
||||||
const checkAgentCreate = generateCheckAccess(PermissionTypes.AGENTS, [
|
permissionType: PermissionTypes.AGENTS,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE],
|
||||||
Permissions.CREATE,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkAgentCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
const checkGlobalAgentShare = generateCheckAccess(
|
const checkGlobalAgentShare = generateCheckAccess({
|
||||||
PermissionTypes.AGENTS,
|
permissionType: PermissionTypes.AGENTS,
|
||||||
[Permissions.USE, Permissions.CREATE],
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
{
|
bodyProps: {
|
||||||
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
||||||
},
|
},
|
||||||
);
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkAgentAccess);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent actions route.
|
* Agent actions route.
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { Tokenizer } = require('@librechat/api');
|
const { Tokenizer, generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getAllUserMemories,
|
getAllUserMemories,
|
||||||
toggleUserMemories,
|
toggleUserMemories,
|
||||||
createMemory,
|
createMemory,
|
||||||
setMemory,
|
|
||||||
deleteMemory,
|
deleteMemory,
|
||||||
|
setMemory,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkMemoryRead = generateCheckAccess(PermissionTypes.MEMORIES, [
|
const checkMemoryRead = generateCheckAccess({
|
||||||
Permissions.USE,
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
Permissions.READ,
|
permissions: [Permissions.USE, Permissions.READ],
|
||||||
]);
|
getRoleByName,
|
||||||
const checkMemoryCreate = generateCheckAccess(PermissionTypes.MEMORIES, [
|
});
|
||||||
Permissions.USE,
|
const checkMemoryCreate = generateCheckAccess({
|
||||||
Permissions.CREATE,
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
]);
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
const checkMemoryUpdate = generateCheckAccess(PermissionTypes.MEMORIES, [
|
getRoleByName,
|
||||||
Permissions.USE,
|
});
|
||||||
Permissions.UPDATE,
|
const checkMemoryUpdate = generateCheckAccess({
|
||||||
]);
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
const checkMemoryDelete = generateCheckAccess(PermissionTypes.MEMORIES, [
|
permissions: [Permissions.USE, Permissions.UPDATE],
|
||||||
Permissions.USE,
|
getRoleByName,
|
||||||
Permissions.UPDATE,
|
});
|
||||||
]);
|
const checkMemoryDelete = generateCheckAccess({
|
||||||
const checkMemoryOptOut = generateCheckAccess(PermissionTypes.MEMORIES, [
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE, Permissions.UPDATE],
|
||||||
Permissions.OPT_OUT,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkMemoryOptOut = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
|
permissions: [Permissions.USE, Permissions.OPT_OUT],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { PermissionTypes, Permissions, SystemRoles } = require('librechat-data-provider');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
|
const { Permissions, SystemRoles, PermissionTypes } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getPrompt,
|
getPrompt,
|
||||||
getPrompts,
|
getPrompts,
|
||||||
@@ -14,24 +16,30 @@ const {
|
|||||||
// updatePromptLabels,
|
// updatePromptLabels,
|
||||||
makePromptProduction,
|
makePromptProduction,
|
||||||
} = require('~/models/Prompt');
|
} = require('~/models/Prompt');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkPromptAccess = generateCheckAccess(PermissionTypes.PROMPTS, [Permissions.USE]);
|
const checkPromptAccess = generateCheckAccess({
|
||||||
const checkPromptCreate = generateCheckAccess(PermissionTypes.PROMPTS, [
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE],
|
||||||
Permissions.CREATE,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkPromptCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
const checkGlobalPromptShare = generateCheckAccess(
|
const checkGlobalPromptShare = generateCheckAccess({
|
||||||
PermissionTypes.PROMPTS,
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
[Permissions.USE, Permissions.CREATE],
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
{
|
bodyProps: {
|
||||||
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
||||||
},
|
},
|
||||||
);
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkPromptAccess);
|
router.use(checkPromptAccess);
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getConversationTags,
|
updateTagsForConversation,
|
||||||
updateConversationTag,
|
updateConversationTag,
|
||||||
createConversationTag,
|
createConversationTag,
|
||||||
deleteConversationTag,
|
deleteConversationTag,
|
||||||
updateTagsForConversation,
|
getConversationTags,
|
||||||
} = require('~/models/ConversationTag');
|
} = require('~/models/ConversationTag');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkBookmarkAccess = generateCheckAccess(PermissionTypes.BOOKMARKS, [Permissions.USE]);
|
const checkBookmarkAccess = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.BOOKMARKS,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkBookmarkAccess);
|
router.use(checkBookmarkAccess);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
const { webcrypto } = require('node:crypto');
|
const { webcrypto } = require('node:crypto');
|
||||||
const { isEnabled } = require('@librechat/api');
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
@@ -499,6 +500,18 @@ const resendVerificationEmail = async (req) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Generate a short-lived JWT token
|
||||||
|
* @param {String} userId - The ID of the user
|
||||||
|
* @param {String} [expireIn='5m'] - The expiration time for the token (default is 5 minutes)
|
||||||
|
* @returns {String} - The generated JWT token
|
||||||
|
*/
|
||||||
|
const generateShortLivedToken = (userId, expireIn = '5m') => {
|
||||||
|
return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
|
||||||
|
expiresIn: expireIn,
|
||||||
|
algorithm: 'HS256',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
logoutUser,
|
logoutUser,
|
||||||
@@ -506,7 +519,8 @@ module.exports = {
|
|||||||
registerUser,
|
registerUser,
|
||||||
setAuthTokens,
|
setAuthTokens,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
setOpenIDAuthTokens,
|
||||||
requestPasswordReset,
|
requestPasswordReset,
|
||||||
resendVerificationEmail,
|
resendVerificationEmail,
|
||||||
setOpenIDAuthTokens,
|
generateShortLivedToken,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
|
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
||||||
const { getBufferMetadata } = require('~/server/utils');
|
const { getBufferMetadata } = require('~/server/utils');
|
||||||
const paths = require('~/config/paths');
|
const paths = require('~/config/paths');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a file to a specified output path with a new filename.
|
* Saves a file to a specified output path with a new filename.
|
||||||
@@ -206,7 +207,7 @@ const deleteLocalFile = async (req, file) => {
|
|||||||
const cleanFilepath = file.filepath.split('?')[0];
|
const cleanFilepath = file.filepath.split('?')[0];
|
||||||
|
|
||||||
if (file.embedded && process.env.RAG_API_URL) {
|
if (file.embedded && process.env.RAG_API_URL) {
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = generateShortLivedToken(req.user.id);
|
||||||
axios.delete(`${process.env.RAG_API_URL}/documents`, {
|
axios.delete(`${process.env.RAG_API_URL}/documents`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const FormData = require('form-data');
|
|||||||
const { logAxiosError } = require('@librechat/api');
|
const { logAxiosError } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { FileSources } = require('librechat-data-provider');
|
const { FileSources } = require('librechat-data-provider');
|
||||||
|
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
||||||
@@ -23,7 +24,8 @@ const deleteVectors = async (req, file) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = generateShortLivedToken(req.user.id);
|
||||||
|
|
||||||
return await axios.delete(`${process.env.RAG_API_URL}/documents`, {
|
return await axios.delete(`${process.env.RAG_API_URL}/documents`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
@@ -70,7 +72,7 @@ async function uploadVectors({ req, file, file_id, entity_id }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = generateShortLivedToken(req.user.id);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file_id', file_id);
|
formData.append('file_id', file_id);
|
||||||
formData.append('file', fs.createReadStream(file.path));
|
formData.append('file', fs.createReadStream(file.path));
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ const processFiles = async (files, fileIds) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!fileIds) {
|
if (!fileIds) {
|
||||||
return await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
// Filter out null results from failed updateFileUsage calls
|
||||||
|
return results.filter((result) => result != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let file_id of fileIds) {
|
for (let file_id of fileIds) {
|
||||||
@@ -67,7 +69,9 @@ const processFiles = async (files, fileIds) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: calculate token cost when image is first uploaded
|
// TODO: calculate token cost when image is first uploaded
|
||||||
return await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
// Filter out null results from failed updateFileUsage calls
|
||||||
|
return results.filter((result) => result != null);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
208
api/server/services/Files/processFiles.test.js
Normal file
208
api/server/services/Files/processFiles.test.js
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// Mock the updateFileUsage function before importing the actual processFiles
|
||||||
|
jest.mock('~/models/File', () => ({
|
||||||
|
updateFileUsage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock winston and logger configuration to avoid dependency issues
|
||||||
|
jest.mock('~/config', () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock all other dependencies that might cause issues
|
||||||
|
jest.mock('librechat-data-provider', () => ({
|
||||||
|
isUUID: { parse: jest.fn() },
|
||||||
|
megabyte: 1024 * 1024,
|
||||||
|
FileContext: { message_attachment: 'message_attachment' },
|
||||||
|
FileSources: { local: 'local' },
|
||||||
|
EModelEndpoint: { assistants: 'assistants' },
|
||||||
|
EToolResources: { file_search: 'file_search' },
|
||||||
|
mergeFileConfig: jest.fn(),
|
||||||
|
removeNullishValues: jest.fn((obj) => obj),
|
||||||
|
isAssistantsEndpoint: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Files/images', () => ({
|
||||||
|
convertImage: jest.fn(),
|
||||||
|
resizeAndConvert: jest.fn(),
|
||||||
|
resizeImageBuffer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/controllers/assistants/v2', () => ({
|
||||||
|
addResourceFileId: jest.fn(),
|
||||||
|
deleteResourceFileId: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/models/Agent', () => ({
|
||||||
|
addAgentResourceFile: jest.fn(),
|
||||||
|
removeAgentResourceFiles: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/controllers/assistants/helpers', () => ({
|
||||||
|
getOpenAIClient: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Tools/credentials', () => ({
|
||||||
|
loadAuthValues: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
checkCapability: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/utils/queue', () => ({
|
||||||
|
LB_QueueAsyncCall: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./strategies', () => ({
|
||||||
|
getStrategyFunctions: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/utils', () => ({
|
||||||
|
determineFileType: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the actual processFiles function after all mocks are set up
|
||||||
|
const { processFiles } = require('./process');
|
||||||
|
const { updateFileUsage } = require('~/models/File');
|
||||||
|
|
||||||
|
describe('processFiles', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('null filtering functionality', () => {
|
||||||
|
it('should filter out null results from updateFileUsage when files do not exist', async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
{ file_id: 'existing-file-1' },
|
||||||
|
{ file_id: 'non-existent-file' },
|
||||||
|
{ file_id: 'existing-file-2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock updateFileUsage to return null for non-existent files
|
||||||
|
updateFileUsage.mockImplementation(({ file_id }) => {
|
||||||
|
if (file_id === 'non-existent-file') {
|
||||||
|
return Promise.resolve(null); // Simulate file not found in the database
|
||||||
|
}
|
||||||
|
return Promise.resolve({ file_id, usage: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles);
|
||||||
|
|
||||||
|
expect(updateFileUsage).toHaveBeenCalledTimes(3);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ file_id: 'existing-file-1', usage: 1 },
|
||||||
|
{ file_id: 'existing-file-2', usage: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Critical test - ensure no null values in result
|
||||||
|
expect(result).not.toContain(null);
|
||||||
|
expect(result).not.toContain(undefined);
|
||||||
|
expect(result.length).toBe(2); // Only valid files should be returned
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all updateFileUsage calls return null', async () => {
|
||||||
|
const mockFiles = [{ file_id: 'non-existent-1' }, { file_id: 'non-existent-2' }];
|
||||||
|
|
||||||
|
// All updateFileUsage calls return null
|
||||||
|
updateFileUsage.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles);
|
||||||
|
|
||||||
|
expect(updateFileUsage).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(result).not.toContain(null);
|
||||||
|
expect(result.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work correctly when all files exist', async () => {
|
||||||
|
const mockFiles = [{ file_id: 'file-1' }, { file_id: 'file-2' }];
|
||||||
|
|
||||||
|
updateFileUsage.mockImplementation(({ file_id }) => {
|
||||||
|
return Promise.resolve({ file_id, usage: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ file_id: 'file-1', usage: 1 },
|
||||||
|
{ file_id: 'file-2', usage: 1 },
|
||||||
|
]);
|
||||||
|
expect(result).not.toContain(null);
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle fileIds parameter and filter nulls correctly', async () => {
|
||||||
|
const mockFiles = [{ file_id: 'file-1' }];
|
||||||
|
const mockFileIds = ['file-2', 'non-existent-file'];
|
||||||
|
|
||||||
|
updateFileUsage.mockImplementation(({ file_id }) => {
|
||||||
|
if (file_id === 'non-existent-file') {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return Promise.resolve({ file_id, usage: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles, mockFileIds);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ file_id: 'file-1', usage: 1 },
|
||||||
|
{ file_id: 'file-2', usage: 1 },
|
||||||
|
]);
|
||||||
|
expect(result).not.toContain(null);
|
||||||
|
expect(result).not.toContain(undefined);
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle duplicate file_ids correctly', async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
{ file_id: 'duplicate-file' },
|
||||||
|
{ file_id: 'duplicate-file' }, // Duplicate should be ignored
|
||||||
|
{ file_id: 'unique-file' },
|
||||||
|
];
|
||||||
|
|
||||||
|
updateFileUsage.mockImplementation(({ file_id }) => {
|
||||||
|
return Promise.resolve({ file_id, usage: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles);
|
||||||
|
|
||||||
|
// Should only call updateFileUsage twice (duplicate ignored)
|
||||||
|
expect(updateFileUsage).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ file_id: 'duplicate-file', usage: 1 },
|
||||||
|
{ file_id: 'unique-file', usage: 1 },
|
||||||
|
]);
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty files array', async () => {
|
||||||
|
const result = await processFiles([]);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(updateFileUsage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed null and undefined returns from updateFileUsage', async () => {
|
||||||
|
const mockFiles = [{ file_id: 'file-1' }, { file_id: 'file-2' }, { file_id: 'file-3' }];
|
||||||
|
|
||||||
|
updateFileUsage.mockImplementation(({ file_id }) => {
|
||||||
|
if (file_id === 'file-1') return Promise.resolve(null);
|
||||||
|
if (file_id === 'file-2') return Promise.resolve(undefined);
|
||||||
|
return Promise.resolve({ file_id, usage: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await processFiles(mockFiles);
|
||||||
|
|
||||||
|
expect(result).toEqual([{ file_id: 'file-3', usage: 1 }]);
|
||||||
|
expect(result).not.toContain(null);
|
||||||
|
expect(result).not.toContain(undefined);
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -118,7 +118,7 @@ class CustomOpenIDStrategy extends OpenIDStrategy {
|
|||||||
*/
|
*/
|
||||||
const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = false) => {
|
const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = false) => {
|
||||||
const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);
|
const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);
|
||||||
const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED);
|
const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED);
|
||||||
if (onBehalfFlowRequired) {
|
if (onBehalfFlowRequired) {
|
||||||
if (fromCache) {
|
if (fromCache) {
|
||||||
const cachedToken = await tokensCache.get(sub);
|
const cachedToken = await tokensCache.get(sub);
|
||||||
@@ -130,7 +130,7 @@ const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache =
|
|||||||
config,
|
config,
|
||||||
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||||
{
|
{
|
||||||
scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE || 'user.read',
|
scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE || 'user.read',
|
||||||
assertion: accessToken,
|
assertion: accessToken,
|
||||||
requested_token_use: 'on_behalf_of',
|
requested_token_use: 'on_behalf_of',
|
||||||
},
|
},
|
||||||
|
|||||||
37
client/src/Providers/ActivePanelContext.tsx
Normal file
37
client/src/Providers/ActivePanelContext.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ActivePanelContextType {
|
||||||
|
active: string | undefined;
|
||||||
|
setActive: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivePanelContext = createContext<ActivePanelContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ActivePanelProvider({
|
||||||
|
children,
|
||||||
|
defaultActive,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
defaultActive?: string;
|
||||||
|
}) {
|
||||||
|
const [active, _setActive] = useState<string | undefined>(defaultActive);
|
||||||
|
|
||||||
|
const setActive = (id: string) => {
|
||||||
|
localStorage.setItem('side:active-panel', id);
|
||||||
|
_setActive(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActivePanelContext.Provider value={{ active, setActive }}>
|
||||||
|
{children}
|
||||||
|
</ActivePanelContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActivePanel() {
|
||||||
|
const context = useContext(ActivePanelContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useActivePanel must be used within an ActivePanelProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -40,41 +40,40 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
|||||||
agent_id: agent_id || '',
|
agent_id: agent_id || '',
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const groupedTools =
|
const groupedTools = tools?.reduce(
|
||||||
tools?.reduce(
|
(acc, tool) => {
|
||||||
(acc, tool) => {
|
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
||||||
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
||||||
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
const groupKey = `${serverName.toLowerCase()}`;
|
||||||
const groupKey = `${serverName.toLowerCase()}`;
|
if (!acc[groupKey]) {
|
||||||
if (!acc[groupKey]) {
|
acc[groupKey] = {
|
||||||
acc[groupKey] = {
|
tool_id: groupKey,
|
||||||
tool_id: groupKey,
|
metadata: {
|
||||||
metadata: {
|
name: `${serverName}`,
|
||||||
name: `${serverName}`,
|
pluginKey: groupKey,
|
||||||
pluginKey: groupKey,
|
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
icon: tool.metadata.icon || '',
|
||||||
icon: tool.metadata.icon || '',
|
} as TPlugin,
|
||||||
} as TPlugin,
|
|
||||||
agent_id: agent_id || '',
|
|
||||||
tools: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
acc[groupKey].tools?.push({
|
|
||||||
tool_id: tool.tool_id,
|
|
||||||
metadata: tool.metadata,
|
|
||||||
agent_id: agent_id || '',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
acc[tool.tool_id] = {
|
|
||||||
tool_id: tool.tool_id,
|
|
||||||
metadata: tool.metadata,
|
|
||||||
agent_id: agent_id || '',
|
agent_id: agent_id || '',
|
||||||
|
tools: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return acc;
|
acc[groupKey].tools?.push({
|
||||||
},
|
tool_id: tool.tool_id,
|
||||||
{} as Record<string, AgentToolType & { tools?: AgentToolType[] }>,
|
metadata: tool.metadata,
|
||||||
) || {};
|
agent_id: agent_id || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
acc[tool.tool_id] = {
|
||||||
|
tool_id: tool.tool_id,
|
||||||
|
metadata: tool.metadata,
|
||||||
|
agent_id: agent_id || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, AgentToolType & { tools?: AgentToolType[] }>,
|
||||||
|
);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { default as AssistantsProvider } from './AssistantsContext';
|
export { default as AssistantsProvider } from './AssistantsContext';
|
||||||
export { default as AgentsProvider } from './AgentsContext';
|
export { default as AgentsProvider } from './AgentsContext';
|
||||||
export { default as ToastProvider } from './ToastContext';
|
export { default as ToastProvider } from './ToastContext';
|
||||||
|
export * from './ActivePanelContext';
|
||||||
export * from './AgentPanelContext';
|
export * from './AgentPanelContext';
|
||||||
export * from './ChatContext';
|
export * from './ChatContext';
|
||||||
export * from './ShareContext';
|
export * from './ShareContext';
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
import {
|
|
||||||
AuthorizationTypeEnum,
|
|
||||||
AuthTypeEnum,
|
|
||||||
TokenExchangeMethodEnum,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import { MCPForm } from '~/common/types';
|
import { MCPForm } from '~/common/types';
|
||||||
|
|
||||||
export const defaultMCPFormValues: MCPForm = {
|
export const defaultMCPFormValues: MCPForm = {
|
||||||
type: AuthTypeEnum.None,
|
|
||||||
saved_auth_fields: false,
|
|
||||||
api_key: '',
|
|
||||||
authorization_type: AuthorizationTypeEnum.Basic,
|
|
||||||
custom_auth_header: '',
|
|
||||||
oauth_client_id: '',
|
|
||||||
oauth_client_secret: '',
|
|
||||||
authorization_url: '',
|
|
||||||
client_url: '',
|
|
||||||
scope: '',
|
|
||||||
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
|
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
url: '',
|
url: '',
|
||||||
tools: [],
|
tools: [],
|
||||||
icon: '',
|
icon: '',
|
||||||
trust: false,
|
trust: false,
|
||||||
|
customHeaders: [],
|
||||||
|
requestTimeout: undefined,
|
||||||
|
connectionTimeout: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -167,13 +167,23 @@ export type ActionAuthForm = {
|
|||||||
token_exchange_method: t.TokenExchangeMethodEnum;
|
token_exchange_method: t.TokenExchangeMethodEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MCPForm = ActionAuthForm & {
|
export type MCPAuthForm = {
|
||||||
|
customHeaders?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MCPForm = MCPAuthForm & {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
tools?: string[];
|
tools?: string[];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
trust?: boolean;
|
trust?: boolean;
|
||||||
|
requestTimeout?: number;
|
||||||
|
connectionTimeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
||||||
@@ -219,11 +229,11 @@ export type AgentPanelContextType = {
|
|||||||
mcps?: t.MCP[];
|
mcps?: t.MCP[];
|
||||||
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||||
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
|
||||||
tools: t.AgentToolType[];
|
tools: t.AgentToolType[];
|
||||||
activePanel?: string;
|
activePanel?: string;
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
groupedTools?: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
supportsFiles,
|
supportsFiles,
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
isAgentsEndpoint,
|
isAgentsEndpoint,
|
||||||
EndpointFileConfig,
|
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
|
import type { EndpointFileConfig } from 'librechat-data-provider';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
import AttachFileMenu from './AttachFileMenu';
|
import AttachFileMenu from './AttachFileMenu';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
@@ -14,22 +14,25 @@ import { useChatContext } from '~/Providers';
|
|||||||
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const conversationId = conversation?.conversationId ?? Constants.NEW_CONVO;
|
const conversationId = conversation?.conversationId ?? Constants.NEW_CONVO;
|
||||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
const { endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||||
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
|
const isAgents = useMemo(() => isAgentsEndpoint(endpoint), [endpoint]);
|
||||||
|
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
|
const endpointFileConfig = fileConfig.endpoints[endpoint ?? ''] as EndpointFileConfig | undefined;
|
||||||
| EndpointFileConfig
|
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? endpoint ?? ''] ?? false;
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
|
|
||||||
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
||||||
|
|
||||||
if (isAgents || (endpointSupportsFiles && !isUploadDisabled)) {
|
if (isAgents || (endpointSupportsFiles && !isUploadDisabled)) {
|
||||||
return <AttachFileMenu disabled={disableInputs} conversationId={conversationId} />;
|
return (
|
||||||
|
<AttachFileMenu
|
||||||
|
disabled={disableInputs}
|
||||||
|
conversationId={conversationId}
|
||||||
|
endpointFileConfig={endpointFileConfig}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import React, { useRef, useState, useMemo } from 'react';
|
import React, { useRef, useState, useMemo } from 'react';
|
||||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||||
|
import type { EndpointFileConfig } from 'librechat-data-provider';
|
||||||
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
|
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
|
||||||
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
|
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
@@ -12,9 +13,10 @@ import { cn } from '~/utils';
|
|||||||
interface AttachFileMenuProps {
|
interface AttachFileMenuProps {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
disabled?: boolean | null;
|
disabled?: boolean | null;
|
||||||
|
endpointFileConfig?: EndpointFileConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachFileMenu = ({ disabled, conversationId }: AttachFileMenuProps) => {
|
const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: AttachFileMenuProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const isUploadDisabled = disabled ?? false;
|
const isUploadDisabled = disabled ?? false;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -24,6 +26,7 @@ const AttachFileMenu = ({ disabled, conversationId }: AttachFileMenuProps) => {
|
|||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const { handleFileChange } = useFileHandling({
|
const { handleFileChange } = useFileHandling({
|
||||||
overrideEndpoint: EModelEndpoint.agents,
|
overrideEndpoint: EModelEndpoint.agents,
|
||||||
|
overrideEndpointFileConfig: endpointFileConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** TODO: Ephemeral Agent Capabilities
|
/** TODO: Ephemeral Agent Capabilities
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Instructions from './Instructions';
|
|||||||
import AgentAvatar from './AgentAvatar';
|
import AgentAvatar from './AgentAvatar';
|
||||||
import FileContext from './FileContext';
|
import FileContext from './FileContext';
|
||||||
import SearchForm from './Search/Form';
|
import SearchForm from './Search/Form';
|
||||||
|
import MCPSection from './MCPSection';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import FileSearch from './FileSearch';
|
import FileSearch from './FileSearch';
|
||||||
import Artifacts from './Artifacts';
|
import Artifacts from './Artifacts';
|
||||||
@@ -168,7 +169,7 @@ export default function AgentConfig({
|
|||||||
const visibleToolIds = new Set(selectedToolIds);
|
const visibleToolIds = new Set(selectedToolIds);
|
||||||
|
|
||||||
// Check what group parent tools should be shown if any subtool is present
|
// Check what group parent tools should be shown if any subtool is present
|
||||||
Object.entries(allTools).forEach(([toolId, toolObj]) => {
|
Object.entries(allTools ?? {}).forEach(([toolId, toolObj]) => {
|
||||||
if (toolObj.tools?.length) {
|
if (toolObj.tools?.length) {
|
||||||
// if any subtool of this group is selected, ensure group parent tool rendered
|
// if any subtool of this group is selected, ensure group parent tool rendered
|
||||||
if (toolObj.tools.some((st) => selectedToolIds.includes(st.tool_id))) {
|
if (toolObj.tools.some((st) => selectedToolIds.includes(st.tool_id))) {
|
||||||
@@ -299,6 +300,7 @@ export default function AgentConfig({
|
|||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* // Render all visible IDs (including groups with subtools selected) */}
|
{/* // Render all visible IDs (including groups with subtools selected) */}
|
||||||
{[...visibleToolIds].map((toolId, i) => {
|
{[...visibleToolIds].map((toolId, i) => {
|
||||||
|
if (!allTools) return null;
|
||||||
const tool = allTools[toolId];
|
const tool = allTools[toolId];
|
||||||
if (!tool) return null;
|
if (!tool) return null;
|
||||||
return (
|
return (
|
||||||
@@ -355,7 +357,7 @@ export default function AgentConfig({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* MCP Section */}
|
{/* MCP Section */}
|
||||||
{/* <MCPSection /> */}
|
<MCPSection />
|
||||||
</div>
|
</div>
|
||||||
<ToolSelectDialog
|
<ToolSelectDialog
|
||||||
isOpen={showToolDialog}
|
isOpen={showToolDialog}
|
||||||
|
|||||||
112
client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx
Normal file
112
client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { Panel } from '~/common';
|
||||||
|
import MCPFormPanel from '../MCP/MCPFormPanel';
|
||||||
|
|
||||||
|
// TODO: Add MCP delete (for now mocked for ui)
|
||||||
|
// import { useDeleteAgentMCP } from '~/data-provider';
|
||||||
|
|
||||||
|
function useDeleteAgentMCP({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
||||||
|
try {
|
||||||
|
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
||||||
|
onSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
onError(error as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useUpdateAgentMCP({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: (mcp: MCP) => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
mutate: async (mcp: MCP) => {
|
||||||
|
try {
|
||||||
|
// TODO: Implement MCP endpoint
|
||||||
|
onSuccess(mcp);
|
||||||
|
} catch (error) {
|
||||||
|
onError(error as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentMCPFormPanel() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
||||||
|
|
||||||
|
const updateAgentMCP = useUpdateAgentMCP({
|
||||||
|
onSuccess(mcp) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setMcp(mcp);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast({
|
||||||
|
message: (error as Error).message || localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteAgentMCP = useDeleteAgentMCP({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_delete_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setActivePanel(Panel.builder);
|
||||||
|
setMcp(undefined);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast({
|
||||||
|
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setActivePanel(Panel.builder);
|
||||||
|
setMcp(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (mcp: MCP) => {
|
||||||
|
updateAgentMCP.mutate(mcp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (mcp_id: string, contextId: string) => {
|
||||||
|
deleteAgentMCP.mutate({ mcp_id, agent_id: contextId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MCPFormPanel
|
||||||
|
mcp={mcp}
|
||||||
|
agent_id={agent_id}
|
||||||
|
onBack={handleBack}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onSave={handleSave}
|
||||||
|
showDeleteButton={!!mcp}
|
||||||
|
isDeleteDisabled={!agent_id || !mcp?.mcp_id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import VersionPanel from './Version/VersionPanel';
|
|||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import ActionsPanel from './ActionsPanel';
|
import ActionsPanel from './ActionsPanel';
|
||||||
import AgentPanel from './AgentPanel';
|
import AgentPanel from './AgentPanel';
|
||||||
import MCPPanel from './MCPPanel';
|
import AgentMCPFormPanel from './AgentMCPFormPanel';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function AgentPanelSwitch() {
|
export default function AgentPanelSwitch() {
|
||||||
@@ -55,7 +55,7 @@ function AgentPanelSwitchWithContext() {
|
|||||||
return <VersionPanel />;
|
return <VersionPanel />;
|
||||||
}
|
}
|
||||||
if (activePanel === Panel.mcp) {
|
if (activePanel === Panel.mcp) {
|
||||||
return <MCPPanel />;
|
return <AgentMCPFormPanel />;
|
||||||
}
|
}
|
||||||
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function AgentTool({
|
|||||||
allTools,
|
allTools,
|
||||||
}: {
|
}: {
|
||||||
tool: string;
|
tool: string;
|
||||||
allTools: Record<string, AgentToolType & { tools?: AgentToolType[] }>;
|
allTools?: Record<string, AgentToolType & { tools?: AgentToolType[] }>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
}) {
|
}) {
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
@@ -30,8 +30,10 @@ export default function AgentTool({
|
|||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const updateUserPlugins = useUpdateUserPluginsMutation();
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
if (!allTools) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const currentTool = allTools[tool];
|
const currentTool = allTools[tool];
|
||||||
|
|
||||||
const getSelectedTools = () => {
|
const getSelectedTools = () => {
|
||||||
if (!currentTool?.tools) return [];
|
if (!currentTool?.tools) return [];
|
||||||
const formTools = getValues('tools') || [];
|
const formTools = getValues('tools') || [];
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
|
||||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
|
||||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
|
||||||
import { defaultMCPFormValues } from '~/common/mcp';
|
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
|
||||||
import { useToastContext } from '~/Providers';
|
|
||||||
import { TrashIcon } from '~/components/svg';
|
|
||||||
import type { MCPForm } from '~/common';
|
|
||||||
import MCPInput from './MCPInput';
|
|
||||||
import { Panel } from '~/common';
|
|
||||||
import {
|
|
||||||
AuthTypeEnum,
|
|
||||||
AuthorizationTypeEnum,
|
|
||||||
TokenExchangeMethodEnum,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
// TODO: Add MCP delete (for now mocked for ui)
|
|
||||||
// import { useDeleteAgentMCP } from '~/data-provider';
|
|
||||||
|
|
||||||
function useDeleteAgentMCP({
|
|
||||||
onSuccess,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
onSuccess: () => void;
|
|
||||||
onError: (error: Error) => void;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
|
||||||
try {
|
|
||||||
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
|
||||||
onSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
onError(error as Error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MCPPanel() {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const { showToast } = useToastContext();
|
|
||||||
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
|
||||||
const deleteAgentMCP = useDeleteAgentMCP({
|
|
||||||
onSuccess: () => {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_delete_mcp_success'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
setActivePanel(Panel.builder);
|
|
||||||
setMcp(undefined);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
showToast({
|
|
||||||
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const methods = useForm<MCPForm>({
|
|
||||||
defaultValues: defaultMCPFormValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { reset } = methods;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mcp) {
|
|
||||||
const formData = {
|
|
||||||
icon: mcp.metadata.icon ?? '',
|
|
||||||
name: mcp.metadata.name ?? '',
|
|
||||||
description: mcp.metadata.description ?? '',
|
|
||||||
url: mcp.metadata.url ?? '',
|
|
||||||
tools: mcp.metadata.tools ?? [],
|
|
||||||
trust: mcp.metadata.trust ?? false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mcp.metadata.auth) {
|
|
||||||
Object.assign(formData, {
|
|
||||||
type: mcp.metadata.auth.type || AuthTypeEnum.None,
|
|
||||||
saved_auth_fields: false,
|
|
||||||
api_key: mcp.metadata.api_key ?? '',
|
|
||||||
authorization_type: mcp.metadata.auth.authorization_type || AuthorizationTypeEnum.Basic,
|
|
||||||
oauth_client_id: mcp.metadata.oauth_client_id ?? '',
|
|
||||||
oauth_client_secret: mcp.metadata.oauth_client_secret ?? '',
|
|
||||||
authorization_url: mcp.metadata.auth.authorization_url ?? '',
|
|
||||||
client_url: mcp.metadata.auth.client_url ?? '',
|
|
||||||
scope: mcp.metadata.auth.scope ?? '',
|
|
||||||
token_exchange_method:
|
|
||||||
mcp.metadata.auth.token_exchange_method ?? TokenExchangeMethodEnum.DefaultPost,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(formData);
|
|
||||||
}
|
|
||||||
}, [mcp, reset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form className="h-full grow overflow-hidden">
|
|
||||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
|
||||||
<div className="relative flex flex-col items-center px-16 py-6 text-center">
|
|
||||||
<div className="absolute left-0 top-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-neutral relative"
|
|
||||||
onClick={() => {
|
|
||||||
setActivePanel(Panel.builder);
|
|
||||||
setMcp(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-center gap-2">
|
|
||||||
<ChevronLeft />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!mcp && (
|
|
||||||
<OGDialog>
|
|
||||||
<OGDialogTrigger asChild>
|
|
||||||
<div className="absolute right-0 top-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!agent_id || !mcp.mcp_id}
|
|
||||||
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
|
||||||
>
|
|
||||||
<TrashIcon className="text-red-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</OGDialogTrigger>
|
|
||||||
<OGDialogTemplate
|
|
||||||
showCloseButton={false}
|
|
||||||
title={localize('com_ui_delete_mcp')}
|
|
||||||
className="max-w-[450px]"
|
|
||||||
main={
|
|
||||||
<Label className="text-left text-sm font-medium">
|
|
||||||
{localize('com_ui_delete_mcp_confirm')}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
selection={{
|
|
||||||
selectHandler: () => {
|
|
||||||
if (!agent_id) {
|
|
||||||
return showToast({
|
|
||||||
message: localize('com_agents_no_agent_id_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
deleteAgentMCP.mutate({
|
|
||||||
mcp_id: mcp.mcp_id,
|
|
||||||
agent_id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectClasses:
|
|
||||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
|
||||||
selectText: localize('com_ui_delete'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</OGDialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-xl font-medium">
|
|
||||||
{mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server')}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-text-secondary">{localize('com_agents_mcp_info')}</div>
|
|
||||||
</div>
|
|
||||||
<MCPInput mcp={mcp} agent_id={agent_id} setMCP={setMcp} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
import MCP from '~/components/SidePanel/Builder/MCP';
|
import { MCPItem } from '~/components/SidePanel/MCP/MCPItem';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function MCPSection() {
|
export default function MCPSection() {
|
||||||
@@ -30,7 +30,7 @@ export default function MCPSection() {
|
|||||||
{mcps
|
{mcps
|
||||||
.filter((mcp) => mcp.agent_id === agent_id)
|
.filter((mcp) => mcp.agent_id === agent_id)
|
||||||
.map((mcp, i) => (
|
.map((mcp, i) => (
|
||||||
<MCP
|
<MCPItem
|
||||||
key={i}
|
key={i}
|
||||||
mcp={mcp}
|
mcp={mcp}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import {
|
|||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
|
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||||
import AssistantConversationStarters from './AssistantConversationStarters';
|
import AssistantConversationStarters from './AssistantConversationStarters';
|
||||||
|
import AssistantToolsDialog from '~/components/Tools/AssistantToolsDialog';
|
||||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||||
import { useSelectAssistant, useLocalize } from '~/hooks';
|
import { useSelectAssistant, useLocalize } from '~/hooks';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
|
||||||
import AppendDateCheckbox from './AppendDateCheckbox';
|
import AppendDateCheckbox from './AppendDateCheckbox';
|
||||||
import CapabilitiesForm from './CapabilitiesForm';
|
import CapabilitiesForm from './CapabilitiesForm';
|
||||||
import { SelectDropDown } from '~/components/ui';
|
import { SelectDropDown } from '~/components/ui';
|
||||||
@@ -468,11 +468,10 @@ export default function AssistantPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToolSelectDialog
|
<AssistantToolsDialog
|
||||||
|
endpoint={endpoint}
|
||||||
isOpen={showToolDialog}
|
isOpen={showToolDialog}
|
||||||
setIsOpen={setShowToolDialog}
|
setIsOpen={setShowToolDialog}
|
||||||
toolsFormKey="functions"
|
|
||||||
endpoint={endpoint}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
|
||||||
import {
|
|
||||||
AuthorizationTypeEnum,
|
|
||||||
TokenExchangeMethodEnum,
|
|
||||||
AuthTypeEnum,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
|
|
||||||
export default function MCPAuth() {
|
|
||||||
// Create a separate form for auth
|
|
||||||
const authMethods = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
/* General */
|
|
||||||
type: AuthTypeEnum.None,
|
|
||||||
saved_auth_fields: false,
|
|
||||||
/* API key */
|
|
||||||
api_key: '',
|
|
||||||
authorization_type: AuthorizationTypeEnum.Basic,
|
|
||||||
custom_auth_header: '',
|
|
||||||
/* OAuth */
|
|
||||||
oauth_client_id: '',
|
|
||||||
oauth_client_secret: '',
|
|
||||||
authorization_url: '',
|
|
||||||
client_url: '',
|
|
||||||
scope: '',
|
|
||||||
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { watch, setValue } = authMethods;
|
|
||||||
const type = watch('type');
|
|
||||||
|
|
||||||
// Sync form state when auth type changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (type === 'none') {
|
|
||||||
// Reset auth fields when type is none
|
|
||||||
setValue('api_key', '');
|
|
||||||
setValue('authorization_type', AuthorizationTypeEnum.Basic);
|
|
||||||
setValue('custom_auth_header', '');
|
|
||||||
setValue('oauth_client_id', '');
|
|
||||||
setValue('oauth_client_secret', '');
|
|
||||||
setValue('authorization_url', '');
|
|
||||||
setValue('client_url', '');
|
|
||||||
setValue('scope', '');
|
|
||||||
setValue('token_exchange_method', TokenExchangeMethodEnum.DefaultPost);
|
|
||||||
}
|
|
||||||
}, [type, setValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...authMethods}>
|
|
||||||
<ActionsAuth />
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
226
client/src/components/SidePanel/MCP/MCPAuth.tsx
Normal file
226
client/src/components/SidePanel/MCP/MCPAuth.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { Plus, Trash2, CirclePlus } from 'lucide-react';
|
||||||
|
import * as Menu from '@ariakit/react/menu';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '~/components/ui/Accordion';
|
||||||
|
import { DropdownPopup } from '~/components/ui';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface UserInfoPlaceholder {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfoPlaceholders: UserInfoPlaceholder[] = [
|
||||||
|
{ label: 'user-id', value: '{{LIBRECHAT_USER_ID}}', description: 'Current user ID' },
|
||||||
|
{ label: 'username', value: '{{LIBRECHAT_USER_USERNAME}}', description: 'Current username' },
|
||||||
|
{ label: 'email', value: '{{LIBRECHAT_USER_EMAIL}}', description: 'Current user email' },
|
||||||
|
{ label: 'name', value: '{{LIBRECHAT_USER_NAME}}', description: 'Current user name' },
|
||||||
|
{
|
||||||
|
label: 'provider',
|
||||||
|
value: '{{LIBRECHAT_USER_PROVIDER}}',
|
||||||
|
description: 'Authentication provider',
|
||||||
|
},
|
||||||
|
{ label: 'role', value: '{{LIBRECHAT_USER_ROLE}}', description: 'User role' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function MCPAuth() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { register, watch, setValue } = useFormContext();
|
||||||
|
const [isHeadersMenuOpen, setIsHeadersMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const customHeaders = watch('customHeaders') || [];
|
||||||
|
|
||||||
|
const addCustomHeader = () => {
|
||||||
|
const newHeader = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
setValue('customHeaders', [...customHeaders, newHeader]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCustomHeader = (id: string) => {
|
||||||
|
setValue(
|
||||||
|
'customHeaders',
|
||||||
|
customHeaders.filter((header: any) => header.id !== id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCustomHeader = (id: string, field: 'name' | 'value', value: string) => {
|
||||||
|
setValue(
|
||||||
|
'customHeaders',
|
||||||
|
customHeaders.map((header: any) =>
|
||||||
|
header.id === id ? { ...header, [field]: value } : header,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPlaceholder = (placeholder: UserInfoPlaceholder) => {
|
||||||
|
const newHeader = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: placeholder.label,
|
||||||
|
value: placeholder.value,
|
||||||
|
};
|
||||||
|
setValue('customHeaders', [...customHeaders, newHeader]);
|
||||||
|
setIsHeadersMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerMenuItems = [
|
||||||
|
...userInfoPlaceholders.map((placeholder) => ({
|
||||||
|
label: `${placeholder.label} - ${placeholder.description}`,
|
||||||
|
onClick: () => handleAddPlaceholder(placeholder),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Authentication Accordion */}
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="authentication" className="rounded-lg border border-border-medium">
|
||||||
|
<AccordionTrigger className="px-4 py-3 text-sm font-medium hover:no-underline">
|
||||||
|
{localize('com_ui_authentication')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Custom Headers Section - Individual Inputs Version */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium text-text-primary">
|
||||||
|
{localize('com_ui_mcp_custom_headers')}
|
||||||
|
</label>
|
||||||
|
<DropdownPopup
|
||||||
|
menuId="headers-menu"
|
||||||
|
items={headerMenuItems}
|
||||||
|
isOpen={isHeadersMenuOpen}
|
||||||
|
setIsOpen={setIsHeadersMenuOpen}
|
||||||
|
trigger={
|
||||||
|
<Menu.MenuButton
|
||||||
|
onClick={() => setIsHeadersMenuOpen(!isHeadersMenuOpen)}
|
||||||
|
className="flex h-7 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-xs text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
<CirclePlus className="mr-1 h-3 w-3 text-text-secondary" />
|
||||||
|
{localize('com_ui_mcp_headers')}
|
||||||
|
</Menu.MenuButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{customHeaders.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="min-w-0 flex-1 text-sm text-text-secondary">
|
||||||
|
{localize('com_ui_mcp_no_custom_headers')}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addCustomHeader}
|
||||||
|
className="flex h-7 shrink-0 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-xs text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
{localize('com_ui_mcp_add_header')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{customHeaders.map((header: any) => (
|
||||||
|
<div key={header.id} className="flex min-w-0 gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={localize('com_ui_mcp_header_name')}
|
||||||
|
value={header.name}
|
||||||
|
onChange={(e) => updateCustomHeader(header.id, 'name', e.target.value)}
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-border-medium bg-surface-primary px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={localize('com_ui_mcp_header_value')}
|
||||||
|
value={header.value}
|
||||||
|
onChange={(e) => updateCustomHeader(header.id, 'value', e.target.value)}
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-border-medium bg-surface-primary px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeCustomHeader(header.id)}
|
||||||
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border-medium bg-surface-primary text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Add New Header Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addCustomHeader}
|
||||||
|
className="flex h-7 shrink-0 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-xs text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
{localize('com_ui_mcp_add_header')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* Configuration Accordion */}
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="configuration" className="rounded-lg border border-border-medium">
|
||||||
|
<AccordionTrigger className="px-4 py-3 text-sm font-medium hover:no-underline">
|
||||||
|
{localize('com_ui_mcp_configuration')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Request Timeout */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-text-primary">
|
||||||
|
{localize('com_ui_mcp_request_timeout')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1000"
|
||||||
|
max="300000"
|
||||||
|
placeholder="10000"
|
||||||
|
{...register('requestTimeout')}
|
||||||
|
className="h-9 w-full rounded-md border border-border-medium bg-surface-primary px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-text-secondary">
|
||||||
|
{localize('com_ui_mcp_request_timeout_description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connection Timeout */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-text-primary">
|
||||||
|
{localize('com_ui_mcp_connection_timeout')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1000"
|
||||||
|
max="60000"
|
||||||
|
placeholder="10000"
|
||||||
|
{...register('connectionTimeout')}
|
||||||
|
className="h-9 w-full rounded-md border border-border-medium bg-surface-primary px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-text-secondary">
|
||||||
|
{localize('com_ui_mcp_connection_timeout_description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
137
client/src/components/SidePanel/MCP/MCPFormPanel.tsx
Normal file
137
client/src/components/SidePanel/MCP/MCPFormPanel.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
|
import type { MCPForm } from '~/common';
|
||||||
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
|
import { defaultMCPFormValues } from '~/common/mcp';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import { TrashIcon } from '~/components/svg';
|
||||||
|
import MCPInput from './MCPInput';
|
||||||
|
|
||||||
|
interface MCPFormPanelProps {
|
||||||
|
// Data
|
||||||
|
mcp?: MCP;
|
||||||
|
agent_id?: string;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
onBack: () => void;
|
||||||
|
onDelete?: (mcp_id: string, agent_id: string) => void;
|
||||||
|
onSave: (mcp: MCP) => void;
|
||||||
|
|
||||||
|
// UI customization
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
showDeleteButton?: boolean;
|
||||||
|
isDeleteDisabled?: boolean;
|
||||||
|
deleteConfirmMessage?: string;
|
||||||
|
|
||||||
|
// Form customization
|
||||||
|
defaultValues?: Partial<MCPForm>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MCPFormPanel({
|
||||||
|
mcp,
|
||||||
|
agent_id,
|
||||||
|
onBack,
|
||||||
|
onDelete,
|
||||||
|
onSave,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
showDeleteButton = true,
|
||||||
|
isDeleteDisabled = false,
|
||||||
|
deleteConfirmMessage,
|
||||||
|
defaultValues = defaultMCPFormValues,
|
||||||
|
}: MCPFormPanelProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
const methods = useForm<MCPForm>({
|
||||||
|
defaultValues: defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { reset } = methods;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mcp) {
|
||||||
|
const formData = {
|
||||||
|
icon: mcp.metadata.icon ?? '',
|
||||||
|
name: mcp.metadata.name ?? '',
|
||||||
|
description: mcp.metadata.description ?? '',
|
||||||
|
url: mcp.metadata.url ?? '',
|
||||||
|
tools: mcp.metadata.tools ?? [],
|
||||||
|
trust: mcp.metadata.trust ?? false,
|
||||||
|
customHeaders: mcp.metadata.customHeaders ?? [],
|
||||||
|
requestTimeout: mcp.metadata.requestTimeout,
|
||||||
|
connectionTimeout: mcp.metadata.connectionTimeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
reset(formData);
|
||||||
|
}
|
||||||
|
}, [mcp, reset]);
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (onDelete && mcp?.mcp_id && agent_id) {
|
||||||
|
onDelete(mcp.mcp_id, agent_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form className="h-full grow overflow-hidden">
|
||||||
|
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||||
|
<div className="relative flex flex-col items-center px-16 py-6 text-center">
|
||||||
|
<div className="absolute left-0 top-6">
|
||||||
|
<button type="button" className="btn btn-neutral relative" onClick={onBack}>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
<ChevronLeft />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!mcp && showDeleteButton && onDelete && (
|
||||||
|
<OGDialog>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<div className="absolute right-0 top-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isDeleteDisabled || !mcp.mcp_id || !agent_id}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
<TrashIcon className="text-red-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_mcp')}
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{deleteConfirmMessage || localize('com_ui_delete_mcp_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: handleDelete,
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xl font-medium">
|
||||||
|
{title ||
|
||||||
|
(mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server'))}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-secondary">
|
||||||
|
{subtitle || localize('com_agents_mcp_info')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MCPInput mcp={mcp} agent_id={agent_id} onSave={onSave} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,58 +1,28 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
import type { MCP } from 'librechat-data-provider';
|
import type { MCP } from 'librechat-data-provider';
|
||||||
import MCPAuth from '~/components/SidePanel/Builder/MCPAuth';
|
import { MCPAuth } from '~/components/SidePanel/MCP/MCPAuth';
|
||||||
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||||
import { Label, Checkbox } from '~/components/ui';
|
import { Label, Checkbox } from '~/components/ui';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { useToastContext } from '~/Providers';
|
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import { MCPForm } from '~/common/types';
|
import { MCPForm } from '~/common/types';
|
||||||
|
|
||||||
function useUpdateAgentMCP({
|
|
||||||
onSuccess,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
onSuccess: (data: [string, MCP]) => void;
|
|
||||||
onError: (error: Error) => void;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
mutate: async ({
|
|
||||||
mcp_id,
|
|
||||||
metadata,
|
|
||||||
agent_id,
|
|
||||||
}: {
|
|
||||||
mcp_id?: string;
|
|
||||||
metadata: MCP['metadata'];
|
|
||||||
agent_id: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
// TODO: Implement MCP endpoint
|
|
||||||
onSuccess(['success', { mcp_id, metadata, agent_id } as MCP]);
|
|
||||||
} catch (error) {
|
|
||||||
onError(error as Error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MCPInputProps {
|
interface MCPInputProps {
|
||||||
mcp?: MCP;
|
mcp?: MCP;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
setMCP: React.Dispatch<React.SetStateAction<MCP | undefined>>;
|
onSave: (mcp: MCP) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
export default function MCPInput({ mcp, agent_id, onSave, isLoading = false }: MCPInputProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
control,
|
control,
|
||||||
} = useFormContext<MCPForm>();
|
} = useFormContext<MCPForm>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showTools, setShowTools] = useState(false);
|
const [showTools, setShowTools] = useState(false);
|
||||||
const [selectedTools, setSelectedTools] = useState<string[]>([]);
|
const [selectedTools, setSelectedTools] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -64,50 +34,16 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
|||||||
}
|
}
|
||||||
}, [mcp]);
|
}, [mcp]);
|
||||||
|
|
||||||
const updateAgentMCP = useUpdateAgentMCP({
|
|
||||||
onSuccess(data) {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_update_mcp_success'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
setMCP(data[1]);
|
|
||||||
setShowTools(true);
|
|
||||||
setSelectedTools(data[1].metadata.tools ?? []);
|
|
||||||
setIsLoading(false);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
showToast({
|
|
||||||
message: (error as Error).message || localize('com_ui_update_mcp_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveMCP = handleSubmit(async (data: MCPForm) => {
|
const saveMCP = handleSubmit(async (data: MCPForm) => {
|
||||||
setIsLoading(true);
|
const updatedMCP: MCP = {
|
||||||
try {
|
mcp_id: mcp?.mcp_id ?? '',
|
||||||
const response = await updateAgentMCP.mutate({
|
agent_id: agent_id ?? '',
|
||||||
agent_id: agent_id ?? '',
|
metadata: {
|
||||||
mcp_id: mcp?.mcp_id,
|
...data,
|
||||||
metadata: {
|
tools: selectedTools,
|
||||||
...data,
|
},
|
||||||
tools: selectedTools,
|
};
|
||||||
},
|
onSave(updatedMCP);
|
||||||
});
|
|
||||||
setMCP(response[1]);
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_update_mcp_success'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_update_mcp_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
@@ -140,14 +76,15 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
const base64String = reader.result as string;
|
const base64String = reader.result as string;
|
||||||
setMCP({
|
const updatedMCP: MCP = {
|
||||||
mcp_id: mcp?.mcp_id ?? '',
|
mcp_id: mcp?.mcp_id ?? '',
|
||||||
agent_id: agent_id ?? '',
|
agent_id: agent_id ?? '',
|
||||||
metadata: {
|
metadata: {
|
||||||
...mcp?.metadata,
|
...mcp?.metadata,
|
||||||
icon: base64String,
|
icon: base64String,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
onSave(updatedMCP);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ type MCPProps = {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MCP({ mcp, onClick }: MCPProps) {
|
export function MCPItem({ mcp, onClick }: MCPProps) {
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
|
import { useCreateMCPMutation } from '~/data-provider';
|
||||||
import { Button, Input, Label } from '~/components/ui';
|
import { Button, Input, Label } from '~/components/ui';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
|
import MCPFormPanel from './MCPFormPanel';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
interface ServerConfigWithVars {
|
interface ServerConfigWithVars {
|
||||||
@@ -24,6 +27,7 @@ export default function MCPPanel() {
|
|||||||
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [showMCPForm, setShowMCPForm] = useState(false);
|
||||||
|
|
||||||
const mcpServerDefinitions = useMemo(() => {
|
const mcpServerDefinitions = useMemo(() => {
|
||||||
if (!startupConfig?.mcpServers) {
|
if (!startupConfig?.mcpServers) {
|
||||||
@@ -57,6 +61,23 @@ export default function MCPPanel() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const create = useCreateMCPMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setShowMCPForm(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error creating MCP:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSaveServerVars = useCallback(
|
const handleSaveServerVars = useCallback(
|
||||||
(serverName: string, updatedValues: Record<string, string>) => {
|
(serverName: string, updatedValues: Record<string, string>) => {
|
||||||
const payload: TUpdateUserPlugins = {
|
const payload: TUpdateUserPlugins = {
|
||||||
@@ -89,14 +110,52 @@ export default function MCPPanel() {
|
|||||||
setSelectedServerNameForEditing(null);
|
setSelectedServerNameForEditing(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddMCP = () => {
|
||||||
|
setShowMCPForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackFromForm = () => {
|
||||||
|
setShowMCPForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveMCP = (mcp: MCP) => {
|
||||||
|
create.mutate(mcp);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showMCPForm) {
|
||||||
|
return (
|
||||||
|
<MCPFormPanel
|
||||||
|
onBack={handleBackFromForm}
|
||||||
|
onSave={handleSaveMCP}
|
||||||
|
showDeleteButton={false}
|
||||||
|
title={localize('com_ui_add_mcp_server')}
|
||||||
|
subtitle={localize('com_agents_mcp_info_chat')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (startupConfigLoading) {
|
if (startupConfigLoading) {
|
||||||
return <MCPPanelSkeleton />;
|
return <MCPPanelSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mcpServerDefinitions.length === 0) {
|
if (mcpServerDefinitions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-center text-sm text-gray-500">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
{localize('com_sidepanel_mcp_no_servers_with_vars')}
|
<div className="p-4 text-center text-sm text-gray-500">
|
||||||
|
{localize('com_sidepanel_mcp_no_servers_with_vars')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddMCP}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{localize('com_ui_add_mcp')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,6 +212,16 @@ export default function MCPPanel() {
|
|||||||
{server.serverName}
|
{server.serverName}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddMCP}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{localize('com_ui_add_mcp')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
import type { NavLink, NavProps } from '~/common';
|
import type { NavLink, NavProps } from '~/common';
|
||||||
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
import { AccordionContent, AccordionItem, TooltipAnchor, Accordion, Button } from '~/components/ui';
|
||||||
import { TooltipAnchor, Button } from '~/components';
|
import { ActivePanelProvider, useActivePanel } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
function NavContent({ links, isCollapsed, resize }: Omit<NavProps, 'defaultActive'>) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [active, _setActive] = useState<string | undefined>(defaultActive);
|
const { active, setActive } = useActivePanel();
|
||||||
const getVariant = (link: NavLink) => (link.id === active ? 'default' : 'ghost');
|
const getVariant = (link: NavLink) => (link.id === active ? 'default' : 'ghost');
|
||||||
|
|
||||||
const setActive = (id: string) => {
|
|
||||||
localStorage.setItem('side:active-panel', id + '');
|
|
||||||
_setActive(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-collapsed={isCollapsed}
|
data-collapsed={isCollapsed}
|
||||||
@@ -105,3 +99,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
||||||
|
return (
|
||||||
|
<ActivePanelProvider defaultActive={defaultActive}>
|
||||||
|
<NavContent links={links} isCollapsed={isCollapsed} resize={resize} />
|
||||||
|
</ActivePanelProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
254
client/src/components/Tools/AssistantToolsDialog.tsx
Normal file
254
client/src/components/Tools/AssistantToolsDialog.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { isAgentsEndpoint } from 'librechat-data-provider';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import type {
|
||||||
|
AssistantsEndpoint,
|
||||||
|
EModelEndpoint,
|
||||||
|
TPluginAction,
|
||||||
|
TError,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import type { TPluginStoreDialogProps } from '~/common/types';
|
||||||
|
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
|
||||||
|
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
|
||||||
|
import { useAvailableToolsQuery } from '~/data-provider';
|
||||||
|
import ToolItem from './ToolItem';
|
||||||
|
|
||||||
|
function AssistantToolsDialog({
|
||||||
|
isOpen,
|
||||||
|
endpoint,
|
||||||
|
setIsOpen,
|
||||||
|
}: TPluginStoreDialogProps & {
|
||||||
|
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { getValues, setValue } = useFormContext();
|
||||||
|
const { data: tools } = useAvailableToolsQuery(endpoint);
|
||||||
|
const isAgentTools = isAgentsEndpoint(endpoint);
|
||||||
|
|
||||||
|
const {
|
||||||
|
maxPage,
|
||||||
|
setMaxPage,
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
searchChanged,
|
||||||
|
setSearchChanged,
|
||||||
|
searchValue,
|
||||||
|
setSearchValue,
|
||||||
|
gridRef,
|
||||||
|
handleSearch,
|
||||||
|
handleChangePage,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
errorMessage,
|
||||||
|
setErrorMessage,
|
||||||
|
showPluginAuthForm,
|
||||||
|
setShowPluginAuthForm,
|
||||||
|
selectedPlugin,
|
||||||
|
setSelectedPlugin,
|
||||||
|
} = usePluginDialogHelpers();
|
||||||
|
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
const handleInstallError = (error: TError) => {
|
||||||
|
setError(true);
|
||||||
|
const errorMessage = error.response?.data?.message ?? '';
|
||||||
|
if (errorMessage) {
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setError(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = (pluginAction: TPluginAction) => {
|
||||||
|
const addFunction = () => {
|
||||||
|
const fns = getValues('functions').slice();
|
||||||
|
fns.push(pluginAction.pluginKey);
|
||||||
|
setValue('functions', fns);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pluginAction.auth) {
|
||||||
|
return addFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserPlugins.mutate(pluginAction, {
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
handleInstallError(error as TError);
|
||||||
|
},
|
||||||
|
onSuccess: addFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTool = (tool: string) => {
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{ pluginKey: tool, action: 'uninstall', auth: null, isEntityTool: true },
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
handleInstallError(error as TError);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const fns = getValues('functions').filter((fn: string) => fn !== tool);
|
||||||
|
setValue('functions', fns);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddTool = (pluginKey: string) => {
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey);
|
||||||
|
setSelectedPlugin(getAvailablePluginFromKey);
|
||||||
|
|
||||||
|
const { authConfig, authenticated = false } = getAvailablePluginFromKey ?? {};
|
||||||
|
|
||||||
|
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||||
|
setShowPluginAuthForm(true);
|
||||||
|
} else {
|
||||||
|
handleInstall({ pluginKey, action: 'install', auth: null });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTools = tools?.filter((tool) =>
|
||||||
|
tool.name.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filteredTools) {
|
||||||
|
setMaxPage(Math.ceil(filteredTools.length / itemsPerPage));
|
||||||
|
if (searchChanged) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchChanged(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
tools,
|
||||||
|
itemsPerPage,
|
||||||
|
searchValue,
|
||||||
|
filteredTools,
|
||||||
|
searchChanged,
|
||||||
|
setMaxPage,
|
||||||
|
setCurrentPage,
|
||||||
|
setSearchChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchValue('');
|
||||||
|
}}
|
||||||
|
className="relative z-[102]"
|
||||||
|
>
|
||||||
|
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||||
|
<div className="fixed inset-0 bg-surface-primary opacity-60 transition-opacity dark:opacity-80" />
|
||||||
|
{/* Full-screen container to center the panel */}
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<DialogPanel
|
||||||
|
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-surface-secondary text-left shadow-xl transition-all max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
|
||||||
|
style={{ minHeight: '610px' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b-[1px] border-border-medium px-4 pb-4 pt-5 sm:p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<DialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
|
{isAgentTools
|
||||||
|
? localize('com_nav_tool_dialog_agents')
|
||||||
|
: localize('com_nav_tool_dialog')}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm text-text-secondary">
|
||||||
|
{localize('com_nav_tool_dialog_description')}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="sm:mt-0">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="inline-block rounded-full text-text-secondary transition-colors hover:text-text-primary"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{localize('com_nav_plugin_auth_error')} {errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showPluginAuthForm && (
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<PluginAuthForm
|
||||||
|
plugin={selectedPlugin}
|
||||||
|
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
||||||
|
isEntityTool={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-center space-x-4">
|
||||||
|
<Search className="h-6 w-6 text-text-tertiary" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={localize('com_nav_tool_search')}
|
||||||
|
className="w-64 rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={gridRef}
|
||||||
|
className="grid grid-cols-1 grid-rows-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
style={{ minHeight: '410px' }}
|
||||||
|
>
|
||||||
|
{filteredTools &&
|
||||||
|
filteredTools
|
||||||
|
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
|
||||||
|
.map((tool, index) => (
|
||||||
|
<ToolItem
|
||||||
|
key={index}
|
||||||
|
tool={tool}
|
||||||
|
isInstalled={getValues('functions').includes(tool.pluginKey)}
|
||||||
|
onAddTool={() => onAddTool(tool.pluginKey)}
|
||||||
|
onRemoveTool={() => onRemoveTool(tool.pluginKey)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
{maxPage > 0 ? (
|
||||||
|
<PluginPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
maxPage={maxPage}
|
||||||
|
onChangePage={handleChangePage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ height: '21px' }}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssistantToolsDialog;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
|
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
|
||||||
import { AgentToolType } from 'librechat-data-provider';
|
import type { TPlugin, AgentToolType } from 'librechat-data-provider';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
type ToolItemProps = {
|
type ToolItemProps = {
|
||||||
tool: AgentToolType;
|
tool: TPlugin | AgentToolType;
|
||||||
onAddTool: () => void;
|
onAddTool: () => void;
|
||||||
onRemoveTool: () => void;
|
onRemoveTool: () => void;
|
||||||
isInstalled?: boolean;
|
isInstalled?: boolean;
|
||||||
@@ -19,9 +19,13 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = tool.metadata?.name || tool.tool_id;
|
const name =
|
||||||
const description = tool.metadata?.description || '';
|
(tool as AgentToolType).metadata?.name ||
|
||||||
const icon = tool.metadata?.icon;
|
(tool as AgentToolType).tool_id ||
|
||||||
|
(tool as TPlugin).name;
|
||||||
|
const description =
|
||||||
|
(tool as AgentToolType).metadata?.description || (tool as TPlugin).description || '';
|
||||||
|
const icon = (tool as AgentToolType).metadata?.icon || (tool as TPlugin).icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
|
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
|
||||||
|
|||||||
@@ -67,15 +67,14 @@ function ToolSelectDialog({
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolsFormKey = 'tools';
|
|
||||||
const handleInstall = (pluginAction: TPluginAction) => {
|
const handleInstall = (pluginAction: TPluginAction) => {
|
||||||
const addFunction = () => {
|
const addFunction = () => {
|
||||||
const installedToolIds: string[] = getValues(toolsFormKey) || [];
|
const installedToolIds: string[] = getValues('tools') || [];
|
||||||
// Add the parent
|
// Add the parent
|
||||||
installedToolIds.push(pluginAction.pluginKey);
|
installedToolIds.push(pluginAction.pluginKey);
|
||||||
|
|
||||||
// If this tool is a group, add subtools too
|
// If this tool is a group, add subtools too
|
||||||
const groupObj = groupedTools[pluginAction.pluginKey];
|
const groupObj = groupedTools?.[pluginAction.pluginKey];
|
||||||
if (groupObj?.tools && groupObj.tools.length > 0) {
|
if (groupObj?.tools && groupObj.tools.length > 0) {
|
||||||
for (const sub of groupObj.tools) {
|
for (const sub of groupObj.tools) {
|
||||||
if (!installedToolIds.includes(sub.tool_id)) {
|
if (!installedToolIds.includes(sub.tool_id)) {
|
||||||
@@ -83,7 +82,7 @@ function ToolSelectDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setValue(toolsFormKey, Array.from(new Set(installedToolIds))); // no duplicates just in case
|
setValue('tools', Array.from(new Set(installedToolIds))); // no duplicates just in case
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pluginAction.auth) {
|
if (!pluginAction.auth) {
|
||||||
@@ -101,7 +100,7 @@ function ToolSelectDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveTool = (toolId: string) => {
|
const onRemoveTool = (toolId: string) => {
|
||||||
const groupObj = groupedTools[toolId];
|
const groupObj = groupedTools?.[toolId];
|
||||||
const toolIdsToRemove = [toolId];
|
const toolIdsToRemove = [toolId];
|
||||||
if (groupObj?.tools && groupObj.tools.length > 0) {
|
if (groupObj?.tools && groupObj.tools.length > 0) {
|
||||||
toolIdsToRemove.push(...groupObj.tools.map((sub) => sub.tool_id));
|
toolIdsToRemove.push(...groupObj.tools.map((sub) => sub.tool_id));
|
||||||
@@ -113,8 +112,8 @@ function ToolSelectDialog({
|
|||||||
onError: (error: unknown) => handleInstallError(error as TError),
|
onError: (error: unknown) => handleInstallError(error as TError),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const remainingToolIds =
|
const remainingToolIds =
|
||||||
getValues(toolsFormKey)?.filter((toolId) => !toolIdsToRemove.includes(toolId)) || [];
|
getValues('tools')?.filter((toolId) => !toolIdsToRemove.includes(toolId)) || [];
|
||||||
setValue(toolsFormKey, remainingToolIds);
|
setValue('tools', remainingToolIds);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -268,7 +267,7 @@ function ToolSelectDialog({
|
|||||||
<ToolItem
|
<ToolItem
|
||||||
key={index}
|
key={index}
|
||||||
tool={tool}
|
tool={tool}
|
||||||
isInstalled={getValues(toolsFormKey)?.includes(tool.tool_id) || false}
|
isInstalled={getValues('tools')?.includes(tool.tool_id) || false}
|
||||||
onAddTool={() => onAddTool(tool.tool_id)}
|
onAddTool={() => onAddTool(tool.tool_id)}
|
||||||
onRemoveTool={() => onRemoveTool(tool.tool_id)}
|
onRemoveTool={() => onRemoveTool(tool.tool_id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './Accordion';
|
||||||
export * from './AnimatedTabs';
|
export * from './AnimatedTabs';
|
||||||
export * from './AlertDialog';
|
export * from './AlertDialog';
|
||||||
export * from './Breadcrumb';
|
export * from './Breadcrumb';
|
||||||
|
|||||||
@@ -40,3 +40,67 @@ export const useToolCallMutation = <T extends t.ToolId>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateMCPMutation = (
|
||||||
|
options?: t.CreateMCPMutationOptions,
|
||||||
|
): UseMutationResult<Record<string, unknown>, Error, t.MCP> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(mcp: t.MCP) => {
|
||||||
|
return dataService.createMCP(mcp);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onMutate: (variables) => options?.onMutate?.(variables),
|
||||||
|
onError: (error, variables, context) => options?.onError?.(error, variables, context),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
// Invalidate tools list to trigger refetch
|
||||||
|
queryClient.invalidateQueries([QueryKeys.tools]);
|
||||||
|
// queryClient.invalidateQueries([QueryKeys.mcpTools]);
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMCPMutation = (
|
||||||
|
options?: t.UpdateMCPMutationOptions,
|
||||||
|
): UseMutationResult<Record<string, unknown>, Error, { mcp_id: string; data: t.MCP }> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
({ mcp_id, data }: { mcp_id: string; data: t.MCP }) => {
|
||||||
|
return dataService.updateMCP({ mcp_id, data });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onMutate: (variables) => options?.onMutate?.(variables),
|
||||||
|
onError: (error, variables, context) => options?.onError?.(error, variables, context),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
// Invalidate tools list to trigger refetch
|
||||||
|
queryClient.invalidateQueries([QueryKeys.tools]);
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMCPMutation = (
|
||||||
|
options?: t.DeleteMCPMutationOptions,
|
||||||
|
): UseMutationResult<Record<string, unknown>, Error, { mcp_id: string }> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
({ mcp_id }: { mcp_id: string }) => {
|
||||||
|
return dataService.deleteMCP({ mcp_id });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onMutate: (variables) => options?.onMutate?.(variables),
|
||||||
|
onError: (error, variables, context) => options?.onError?.(error, variables, context),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
// Invalidate tools list to trigger refetch
|
||||||
|
queryClient.invalidateQueries([QueryKeys.tools]);
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
|
||||||
import {
|
|
||||||
defaultAssistantsVersion,
|
|
||||||
fileConfig as defaultFileConfig,
|
|
||||||
EModelEndpoint,
|
|
||||||
isAgentsEndpoint,
|
|
||||||
isAssistantsEndpoint,
|
|
||||||
mergeFileConfig,
|
|
||||||
QueryKeys,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
QueryKeys,
|
||||||
|
EModelEndpoint,
|
||||||
|
mergeFileConfig,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
isAssistantsEndpoint,
|
||||||
|
defaultAssistantsVersion,
|
||||||
|
fileConfig as defaultFileConfig,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import type { EndpointFileConfig, TEndpointsConfig, TError } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile, FileSetter } from '~/common';
|
import type { ExtendedFile, FileSetter } from '~/common';
|
||||||
import { useGetFileConfig, useUploadFileMutation } from '~/data-provider';
|
import { useGetFileConfig, useUploadFileMutation } from '~/data-provider';
|
||||||
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
||||||
|
import { processFileForUpload } from '~/utils/heicConverter';
|
||||||
import { useToastContext } from '~/Providers/ToastContext';
|
import { useToastContext } from '~/Providers/ToastContext';
|
||||||
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import { logger, validateFiles } from '~/utils';
|
import { logger, validateFiles } from '~/utils';
|
||||||
import useClientResize from './useClientResize';
|
import useClientResize from './useClientResize';
|
||||||
import { processFileForUpload } from '~/utils/heicConverter';
|
|
||||||
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
|
||||||
import useUpdateFiles from './useUpdateFiles';
|
import useUpdateFiles from './useUpdateFiles';
|
||||||
|
|
||||||
type UseFileHandling = {
|
type UseFileHandling = {
|
||||||
overrideEndpoint?: EModelEndpoint;
|
|
||||||
fileSetter?: FileSetter;
|
fileSetter?: FileSetter;
|
||||||
fileFilter?: (file: File) => boolean;
|
fileFilter?: (file: File) => boolean;
|
||||||
additionalMetadata?: Record<string, string | undefined>;
|
additionalMetadata?: Record<string, string | undefined>;
|
||||||
|
overrideEndpoint?: EModelEndpoint;
|
||||||
|
overrideEndpointFileConfig?: EndpointFileConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFileHandling = (params?: UseFileHandling) => {
|
const useFileHandling = (params?: UseFileHandling) => {
|
||||||
@@ -246,8 +247,9 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||||||
fileList,
|
fileList,
|
||||||
setError,
|
setError,
|
||||||
endpointFileConfig:
|
endpointFileConfig:
|
||||||
fileConfig?.endpoints[endpoint] ??
|
params?.overrideEndpointFileConfig ??
|
||||||
fileConfig?.endpoints.default ??
|
fileConfig?.endpoints?.[endpoint] ??
|
||||||
|
fileConfig?.endpoints?.default ??
|
||||||
defaultFileConfig.endpoints[endpoint] ??
|
defaultFileConfig.endpoints[endpoint] ??
|
||||||
defaultFileConfig.endpoints.default,
|
defaultFileConfig.endpoints.default,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { Blocks, MCPIcon, AttachmentIcon } from '~/components/svg';
|
|||||||
import Parameters from '~/components/SidePanel/Parameters/Panel';
|
import Parameters from '~/components/SidePanel/Parameters/Panel';
|
||||||
import FilesPanel from '~/components/SidePanel/Files/Panel';
|
import FilesPanel from '~/components/SidePanel/Files/Panel';
|
||||||
import MCPPanel from '~/components/SidePanel/MCP/MCPPanel';
|
import MCPPanel from '~/components/SidePanel/MCP/MCPPanel';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
|
||||||
import { useHasAccess } from '~/hooks';
|
import { useHasAccess } from '~/hooks';
|
||||||
|
|
||||||
export default function useSideNavLinks({
|
export default function useSideNavLinks({
|
||||||
@@ -61,7 +60,6 @@ export default function useSideNavLinks({
|
|||||||
permissionType: PermissionTypes.AGENTS,
|
permissionType: PermissionTypes.AGENTS,
|
||||||
permission: Permissions.CREATE,
|
permission: Permissions.CREATE,
|
||||||
});
|
});
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
|
|
||||||
const Links = useMemo(() => {
|
const Links = useMemo(() => {
|
||||||
const links: NavLink[] = [];
|
const links: NavLink[] = [];
|
||||||
@@ -79,7 +77,7 @@ export default function useSideNavLinks({
|
|||||||
title: 'com_sidepanel_assistant_builder',
|
title: 'com_sidepanel_assistant_builder',
|
||||||
label: '',
|
label: '',
|
||||||
icon: Blocks,
|
icon: Blocks,
|
||||||
id: 'assistants',
|
id: EModelEndpoint.assistants,
|
||||||
Component: PanelSwitch,
|
Component: PanelSwitch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,7 +92,7 @@ export default function useSideNavLinks({
|
|||||||
title: 'com_sidepanel_agent_builder',
|
title: 'com_sidepanel_agent_builder',
|
||||||
label: '',
|
label: '',
|
||||||
icon: Blocks,
|
icon: Blocks,
|
||||||
id: 'agents',
|
id: EModelEndpoint.agents,
|
||||||
Component: AgentPanelSwitch,
|
Component: AgentPanelSwitch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,20 +150,13 @@ export default function useSideNavLinks({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
links.push({
|
||||||
startupConfig?.mcpServers &&
|
title: 'com_nav_mcp_panel',
|
||||||
Object.values(startupConfig.mcpServers).some(
|
label: '',
|
||||||
(server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
|
icon: MCPIcon,
|
||||||
)
|
id: 'mcp-settings',
|
||||||
) {
|
Component: MCPPanel,
|
||||||
links.push({
|
});
|
||||||
title: 'com_nav_setting_mcp',
|
|
||||||
label: '',
|
|
||||||
icon: MCPIcon,
|
|
||||||
id: 'mcp-settings',
|
|
||||||
Component: MCPPanel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'com_sidepanel_hide_panel',
|
title: 'com_sidepanel_hide_panel',
|
||||||
@@ -189,7 +180,6 @@ export default function useSideNavLinks({
|
|||||||
hasAccessToBookmarks,
|
hasAccessToBookmarks,
|
||||||
hasAccessToCreateAgents,
|
hasAccessToCreateAgents,
|
||||||
hidePanel,
|
hidePanel,
|
||||||
startupConfig,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Links;
|
return Links;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export function useMCPSelect({ conversationId }: UseMCPSelectOptions) {
|
|||||||
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||||
const { data: mcpToolDetails, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
const { data: mcpToolDetails, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
||||||
select: (data: TPlugin[]) => {
|
select: (data: TPlugin[]) => {
|
||||||
|
console.log('🔍 Raw tools data received:', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
const mcpToolsMap = new Map<string, TPlugin>();
|
const mcpToolsMap = new Map<string, TPlugin>();
|
||||||
data.forEach((tool) => {
|
data.forEach((tool) => {
|
||||||
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
|
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
|
||||||
@@ -46,7 +48,10 @@ export function useMCPSelect({ conversationId }: UseMCPSelectOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Array.from(mcpToolsMap.values());
|
|
||||||
|
const result = Array.from(mcpToolsMap.values());
|
||||||
|
console.log('🔧 Processed MCP tools:', JSON.stringify(result, null, 2));
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"com_agents_mcp_description_placeholder": "Explain what it does in a few words",
|
"com_agents_mcp_description_placeholder": "Explain what it does in a few words",
|
||||||
"com_agents_mcp_icon_size": "Minimum size 128 x 128 px",
|
"com_agents_mcp_icon_size": "Minimum size 128 x 128 px",
|
||||||
"com_agents_mcp_info": "Add MCP servers to your agent to enable it to perform tasks and interact with external services",
|
"com_agents_mcp_info": "Add MCP servers to your agent to enable it to perform tasks and interact with external services",
|
||||||
|
"com_agents_mcp_info_chat": "Add MCP servers to enable chat to perform tasks and interact with external services",
|
||||||
"com_agents_mcp_name_placeholder": "Custom Tool",
|
"com_agents_mcp_name_placeholder": "Custom Tool",
|
||||||
"com_agents_mcp_trust_subtext": "Custom connectors are not verified by LibreChat",
|
"com_agents_mcp_trust_subtext": "Custom connectors are not verified by LibreChat",
|
||||||
"com_agents_mcps_disabled": "You need to create an agent before adding MCPs.",
|
"com_agents_mcps_disabled": "You need to create an agent before adding MCPs.",
|
||||||
@@ -205,10 +206,10 @@
|
|||||||
"com_endpoint_google_custom_name_placeholder": "Set a custom name for Google",
|
"com_endpoint_google_custom_name_placeholder": "Set a custom name for Google",
|
||||||
"com_endpoint_google_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.",
|
"com_endpoint_google_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.",
|
||||||
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
|
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
|
||||||
"com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
|
|
||||||
"com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.",
|
|
||||||
"com_endpoint_google_thinking": "Enables or disables reasoning. This setting is only supported by certain models (2.5 series). For older models, this setting may have no effect.",
|
"com_endpoint_google_thinking": "Enables or disables reasoning. This setting is only supported by certain models (2.5 series). For older models, this setting may have no effect.",
|
||||||
"com_endpoint_google_thinking_budget": "Guides the number of thinking tokens the model uses. The actual amount may exceed or fall below this value depending on the prompt.\n\nThis setting is only supported by certain models (2.5 series). Gemini 2.5 Pro supports 128-32,768 tokens. Gemini 2.5 Flash supports 0-24,576 tokens. Gemini 2.5 Flash Lite supports 512-24,576 tokens.\n\nLeave blank or set to \"-1\" to let the model automatically decide when and how much to think. By default, Gemini 2.5 Flash Lite does not think.",
|
"com_endpoint_google_thinking_budget": "Guides the number of thinking tokens the model uses. The actual amount may exceed or fall below this value depending on the prompt.\n\nThis setting is only supported by certain models (2.5 series). Gemini 2.5 Pro supports 128-32,768 tokens. Gemini 2.5 Flash supports 0-24,576 tokens. Gemini 2.5 Flash Lite supports 512-24,576 tokens.\n\nLeave blank or set to \"-1\" to let the model automatically decide when and how much to think. By default, Gemini 2.5 Flash Lite does not think.",
|
||||||
|
"com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
|
||||||
|
"com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.",
|
||||||
"com_endpoint_instructions_assistants": "Override Instructions",
|
"com_endpoint_instructions_assistants": "Override Instructions",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.",
|
"com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.",
|
||||||
"com_endpoint_max_output_tokens": "Max Output Tokens",
|
"com_endpoint_max_output_tokens": "Max Output Tokens",
|
||||||
@@ -431,6 +432,7 @@
|
|||||||
"com_nav_log_out": "Log out",
|
"com_nav_log_out": "Log out",
|
||||||
"com_nav_long_audio_warning": "Longer texts will take longer to process.",
|
"com_nav_long_audio_warning": "Longer texts will take longer to process.",
|
||||||
"com_nav_maximize_chat_space": "Maximize chat space",
|
"com_nav_maximize_chat_space": "Maximize chat space",
|
||||||
|
"com_nav_mcp_panel": "MCP Servers",
|
||||||
"com_nav_mcp_vars_update_error": "Error updating MCP custom user variables: {{0}}",
|
"com_nav_mcp_vars_update_error": "Error updating MCP custom user variables: {{0}}",
|
||||||
"com_nav_mcp_vars_updated": "MCP custom user variables updated successfully.",
|
"com_nav_mcp_vars_updated": "MCP custom user variables updated successfully.",
|
||||||
"com_nav_modular_chat": "Enable switching Endpoints mid-conversation",
|
"com_nav_modular_chat": "Enable switching Endpoints mid-conversation",
|
||||||
@@ -827,6 +829,21 @@
|
|||||||
"com_ui_mcp_server_not_found": "Server not found.",
|
"com_ui_mcp_server_not_found": "Server not found.",
|
||||||
"com_ui_mcp_servers": "MCP Servers",
|
"com_ui_mcp_servers": "MCP Servers",
|
||||||
"com_ui_mcp_url": "MCP Server URL",
|
"com_ui_mcp_url": "MCP Server URL",
|
||||||
|
"com_ui_mcp_custom_headers": "Custom Headers",
|
||||||
|
"com_ui_mcp_headers": "Headers",
|
||||||
|
"com_ui_mcp_no_custom_headers": "No custom headers configured",
|
||||||
|
"com_ui_mcp_add_header": "Add Header",
|
||||||
|
"com_ui_mcp_header_name": "Header Name",
|
||||||
|
"com_ui_mcp_header_value": "Header Value",
|
||||||
|
"com_ui_mcp_configuration": "Configuration",
|
||||||
|
"com_ui_mcp_request_timeout": "Request Timeout (ms)",
|
||||||
|
"com_ui_mcp_request_timeout_description": "Maximum time in milliseconds to wait for a response from the MCP server",
|
||||||
|
"com_ui_mcp_connection_timeout": "Connection Timeout (ms)",
|
||||||
|
"com_ui_mcp_connection_timeout_description": "Maximum time in milliseconds to establish connection to the MCP server",
|
||||||
|
"com_ui_mcp_reset_timeout_on_progress": "Reset Timeout on Progress",
|
||||||
|
"com_ui_mcp_reset_timeout_on_progress_description": "Reset the request timeout when progress is received",
|
||||||
|
"com_ui_mcp_max_total_timeout": "Maximum Total Timeout (ms)",
|
||||||
|
"com_ui_mcp_max_total_timeout_description": "Maximum total time in milliseconds allowed for the entire request including retries",
|
||||||
"com_ui_memories": "Memories",
|
"com_ui_memories": "Memories",
|
||||||
"com_ui_memories_allow_create": "Allow creating Memories",
|
"com_ui_memories_allow_create": "Allow creating Memories",
|
||||||
"com_ui_memories_allow_opt_out": "Allow users to opt out of Memories",
|
"com_ui_memories_allow_opt_out": "Allow users to opt out of Memories",
|
||||||
@@ -916,6 +933,7 @@
|
|||||||
"com_ui_rename_prompt": "Rename Prompt",
|
"com_ui_rename_prompt": "Rename Prompt",
|
||||||
"com_ui_requires_auth": "Requires Authentication",
|
"com_ui_requires_auth": "Requires Authentication",
|
||||||
"com_ui_reset_var": "Reset {{0}}",
|
"com_ui_reset_var": "Reset {{0}}",
|
||||||
|
"com_ui_reset_zoom": "Reset Zoom",
|
||||||
"com_ui_result": "Result",
|
"com_ui_result": "Result",
|
||||||
"com_ui_revoke": "Revoke",
|
"com_ui_revoke": "Revoke",
|
||||||
"com_ui_revoke_info": "Revoke all user provided credentials",
|
"com_ui_revoke_info": "Revoke all user provided credentials",
|
||||||
@@ -1057,7 +1075,6 @@
|
|||||||
"com_ui_x_selected": "{{0}} selected",
|
"com_ui_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_ui_reset_zoom": "Reset Zoom",
|
|
||||||
"com_user_message": "You",
|
"com_user_message": "You",
|
||||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||||
}
|
}
|
||||||
80
package-lock.json
generated
80
package-lock.json
generated
@@ -38719,21 +38719,57 @@
|
|||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
"node_modules/pbkdf2": {
|
"node_modules/pbkdf2": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz",
|
||||||
"integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
|
"integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"create-hash": "^1.1.2",
|
"create-hash": "~1.1.3",
|
||||||
"create-hmac": "^1.1.4",
|
"create-hmac": "^1.1.7",
|
||||||
"ripemd160": "^2.0.1",
|
"ripemd160": "=2.0.1",
|
||||||
"safe-buffer": "^5.0.1",
|
"safe-buffer": "^5.2.1",
|
||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.11",
|
||||||
|
"to-buffer": "^1.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pbkdf2/node_modules/create-hash": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cipher-base": "^1.0.1",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"ripemd160": "^2.0.0",
|
||||||
|
"sha.js": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pbkdf2/node_modules/hash-base": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pbkdf2/node_modules/ripemd160": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hash-base": "^2.0.0",
|
||||||
|
"inherits": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/peek-readable": {
|
"node_modules/peek-readable": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
|
||||||
@@ -39919,9 +39955,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-eslint/node_modules/brace-expansion": {
|
"node_modules/prettier-eslint/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -43961,6 +43997,28 @@
|
|||||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/to-buffer": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": "^2.0.5",
|
||||||
|
"safe-buffer": "^5.2.1",
|
||||||
|
"typed-array-buffer": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/to-buffer/node_modules/isarray": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export * from './mcp/manager';
|
export * from './mcp/manager';
|
||||||
export * from './mcp/oauth';
|
export * from './mcp/oauth';
|
||||||
export * from './mcp/auth';
|
export * from './mcp/auth';
|
||||||
|
export * from './mcp/mcpOps';
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
export * from './mcp/utils';
|
export * from './mcp/utils';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
@@ -11,6 +12,8 @@ export * from './oauth';
|
|||||||
export * from './crypto';
|
export * from './crypto';
|
||||||
/* Flow */
|
/* Flow */
|
||||||
export * from './flow/manager';
|
export * from './flow/manager';
|
||||||
|
/* Middleware */
|
||||||
|
export * from './middleware';
|
||||||
/* Agents */
|
/* Agents */
|
||||||
export * from './agents';
|
export * from './agents';
|
||||||
/* Endpoints */
|
/* Endpoints */
|
||||||
|
|||||||
17
packages/api/src/mcp/mcpOps.ts
Normal file
17
packages/api/src/mcp/mcpOps.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { MCP } from 'librechat-data-provider';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
export const addTool = (req: { body: MCP }, res: Response) => {
|
||||||
|
console.log('CREATE MCP:', JSON.stringify(req.body, null, 2));
|
||||||
|
res.send('ok');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTool = (req: { body: MCP; params: { mcp_id: string } }, res: Response) => {
|
||||||
|
console.log('UPDATE MCP:', req.params.mcp_id, JSON.stringify(req.body, null, 2));
|
||||||
|
res.send('ok');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTool = (req: { params: { mcp_id: string } }, res: Response) => {
|
||||||
|
console.log('DELETE MCP:', req.params.mcp_id);
|
||||||
|
res.send('ok');
|
||||||
|
};
|
||||||
141
packages/api/src/middleware/access.ts
Normal file
141
packages/api/src/middleware/access.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import {
|
||||||
|
Permissions,
|
||||||
|
EndpointURLs,
|
||||||
|
EModelEndpoint,
|
||||||
|
PermissionTypes,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express';
|
||||||
|
import type { IRole, IUser } from '@librechat/data-schemas';
|
||||||
|
|
||||||
|
export function skipAgentCheck(req?: ServerRequest): boolean {
|
||||||
|
if (!req || !req?.body?.endpoint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.originalUrl?.includes(EndpointURLs[EModelEndpoint.agents])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isAgentsEndpoint(req.body.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core function to check if a user has one or more required permissions
|
||||||
|
* @param user - The user object
|
||||||
|
* @param permissionType - The type of permission to check
|
||||||
|
* @param permissions - The list of specific permissions to check
|
||||||
|
* @param bodyProps - An optional object where keys are permissions and values are arrays of properties to check
|
||||||
|
* @param checkObject - The object to check properties against
|
||||||
|
* @param skipCheck - An optional function that takes the checkObject and returns true to skip permission checking
|
||||||
|
* @returns Whether the user has the required permissions
|
||||||
|
*/
|
||||||
|
export const checkAccess = async ({
|
||||||
|
req,
|
||||||
|
user,
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
getRoleByName,
|
||||||
|
bodyProps = {} as Record<Permissions, string[]>,
|
||||||
|
checkObject = {},
|
||||||
|
skipCheck,
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
req?: ServerRequest;
|
||||||
|
permissionType: PermissionTypes;
|
||||||
|
permissions: Permissions[];
|
||||||
|
bodyProps?: Record<Permissions, string[]>;
|
||||||
|
checkObject?: object;
|
||||||
|
/** If skipCheck function is provided and returns true, skip permission checking */
|
||||||
|
skipCheck?: (req?: ServerRequest) => boolean;
|
||||||
|
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
if (skipCheck && skipCheck(req)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !user.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await getRoleByName(user.role);
|
||||||
|
if (role && role.permissions && role.permissions[permissionType]) {
|
||||||
|
const hasAnyPermission = permissions.some((permission) => {
|
||||||
|
if (
|
||||||
|
role.permissions?.[permissionType as keyof typeof role.permissions]?.[
|
||||||
|
permission as keyof (typeof role.permissions)[typeof permissionType]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyProps[permission] && checkObject) {
|
||||||
|
return bodyProps[permission].some((prop) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(checkObject, prop),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasAnyPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
|
||||||
|
* @param permissionType - The type of permission to check.
|
||||||
|
* @param permissions - The list of specific permissions to check.
|
||||||
|
* @param bodyProps - An optional object where keys are permissions and values are arrays of `req.body` properties to check.
|
||||||
|
* @param skipCheck - An optional function that takes req.body and returns true to skip permission checking.
|
||||||
|
* @param getRoleByName - A function to get the role by name.
|
||||||
|
* @returns Express middleware function.
|
||||||
|
*/
|
||||||
|
export const generateCheckAccess = ({
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
bodyProps = {} as Record<Permissions, string[]>,
|
||||||
|
skipCheck,
|
||||||
|
getRoleByName,
|
||||||
|
}: {
|
||||||
|
permissionType: PermissionTypes;
|
||||||
|
permissions: Permissions[];
|
||||||
|
bodyProps?: Record<Permissions, string[]>;
|
||||||
|
skipCheck?: (req?: ServerRequest) => boolean;
|
||||||
|
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
|
||||||
|
}): ((req: ServerRequest, res: ServerResponse, next: NextFunction) => Promise<unknown>) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hasAccess = await checkAccess({
|
||||||
|
req,
|
||||||
|
user: req.user as IUser,
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
bodyProps,
|
||||||
|
checkObject: req.body,
|
||||||
|
skipCheck,
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`[${permissionType}] Forbidden: "${req.originalUrl}" - Insufficient permissions for User ${req.user?.id}: ${permissions.join(', ')}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
1
packages/api/src/middleware/index.ts
Normal file
1
packages/api/src/middleware/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './access';
|
||||||
@@ -949,11 +949,11 @@ export const initialModelsConfig: TModelsConfig = {
|
|||||||
[EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock],
|
[EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EndpointURLs: Record<string, string> = {
|
export const EndpointURLs = {
|
||||||
[EModelEndpoint.assistants]: '/api/assistants/v2/chat',
|
[EModelEndpoint.assistants]: '/api/assistants/v2/chat',
|
||||||
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
|
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
|
||||||
[EModelEndpoint.agents]: `/api/${EModelEndpoint.agents}/chat`,
|
[EModelEndpoint.agents]: `/api/${EModelEndpoint.agents}/chat`,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const modularEndpoints = new Set<EModelEndpoint | string>([
|
export const modularEndpoints = new Set<EModelEndpoint | string>([
|
||||||
EModelEndpoint.gptPlugins,
|
EModelEndpoint.gptPlugins,
|
||||||
|
|||||||
@@ -832,3 +832,35 @@ export const createMemory = (data: {
|
|||||||
}): Promise<{ created: boolean; memory: q.TUserMemory }> => {
|
}): Promise<{ created: boolean; memory: q.TUserMemory }> => {
|
||||||
return request.post(endpoints.memories(), data);
|
return request.post(endpoints.memories(), data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMCP = (mcp: ag.MCP): Promise<Record<string, unknown>> => {
|
||||||
|
return request.post(
|
||||||
|
endpoints.agents({
|
||||||
|
path: 'tools/add',
|
||||||
|
}),
|
||||||
|
mcp,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMCP = ({
|
||||||
|
mcp_id,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
mcp_id: string;
|
||||||
|
data: ag.MCP;
|
||||||
|
}): Promise<Record<string, unknown>> => {
|
||||||
|
return request.put(
|
||||||
|
endpoints.agents({
|
||||||
|
path: `tools/${mcp_id}`,
|
||||||
|
}),
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMCP = ({ mcp_id }: { mcp_id: string }): Promise<Record<string, unknown>> => {
|
||||||
|
return request.delete(
|
||||||
|
endpoints.agents({
|
||||||
|
path: `tools/${mcp_id}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export enum QueryKeys {
|
|||||||
banner = 'banner',
|
banner = 'banner',
|
||||||
/* Memories */
|
/* Memories */
|
||||||
memories = 'memories',
|
memories = 'memories',
|
||||||
|
mcpTools = 'mcpTools',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MutationKeys {
|
export enum MutationKeys {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialRe
|
|||||||
export type TPluginAction = {
|
export type TPluginAction = {
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: 'install' | 'uninstall';
|
action: 'install' | 'uninstall';
|
||||||
auth?: Partial<Record<string, string>>;
|
auth?: Partial<Record<string, string>> | null;
|
||||||
isEntityTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ export type TUpdateUserPlugins = {
|
|||||||
isEntityTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: string;
|
action: string;
|
||||||
auth?: Partial<Record<string, string | null>>;
|
auth?: Partial<Record<string, string | null>> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO `label` needs to be changed to the proper `TranslationKeys`
|
// TODO `label` needs to be changed to the proper `TranslationKeys`
|
||||||
|
|||||||
@@ -342,13 +342,17 @@ export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
|
|||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
tools?: string[];
|
tools?: string[];
|
||||||
auth?: MCPAuth;
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
trust?: boolean;
|
trust?: boolean;
|
||||||
|
customHeaders?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
requestTimeout?: number;
|
||||||
|
connectionTimeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MCPAuth = ActionAuth;
|
|
||||||
|
|
||||||
export type AgentToolType = {
|
export type AgentToolType = {
|
||||||
tool_id: string;
|
tool_id: string;
|
||||||
metadata: ToolMetadata;
|
metadata: ToolMetadata;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
AgentCreateParams,
|
AgentCreateParams,
|
||||||
AgentUpdateParams,
|
AgentUpdateParams,
|
||||||
} from './assistants';
|
} from './assistants';
|
||||||
import { Action, ActionMetadata } from './agents';
|
import { Action, ActionMetadata, MCP } from './agents';
|
||||||
|
|
||||||
export type MutationOptions<
|
export type MutationOptions<
|
||||||
Response,
|
Response,
|
||||||
@@ -319,6 +319,15 @@ export type AcceptTermsMutationOptions = MutationOptions<
|
|||||||
/* Tools */
|
/* Tools */
|
||||||
export type UpdatePluginAuthOptions = MutationOptions<types.TUser, types.TUpdateUserPlugins>;
|
export type UpdatePluginAuthOptions = MutationOptions<types.TUser, types.TUpdateUserPlugins>;
|
||||||
|
|
||||||
|
export type CreateMCPMutationOptions = MutationOptions<Record<string, unknown>, MCP>;
|
||||||
|
|
||||||
|
export type UpdateMCPMutationOptions = MutationOptions<
|
||||||
|
Record<string, unknown>,
|
||||||
|
{ mcp_id: string; data: MCP }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DeleteMCPMutationOptions = MutationOptions<Record<string, unknown>, { mcp_id: string }>;
|
||||||
|
|
||||||
export type ToolParamsMap = {
|
export type ToolParamsMap = {
|
||||||
[Tools.execute_code]: {
|
[Tools.execute_code]: {
|
||||||
lang: string;
|
lang: string;
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ const formatDate = (date: Date): string => {
|
|||||||
|
|
||||||
// Factory function that takes mongoose instance and returns the methods
|
// Factory function that takes mongoose instance and returns the methods
|
||||||
export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
||||||
const MemoryEntry = mongoose.models.MemoryEntry;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new memory entry for a user
|
* Creates a new memory entry for a user
|
||||||
* Throws an error if a memory with the same key already exists
|
* Throws an error if a memory with the same key already exists
|
||||||
@@ -28,6 +26,7 @@ export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
|||||||
return { ok: false };
|
return { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MemoryEntry = mongoose.models.MemoryEntry;
|
||||||
const existingMemory = await MemoryEntry.findOne({ userId, key });
|
const existingMemory = await MemoryEntry.findOne({ userId, key });
|
||||||
if (existingMemory) {
|
if (existingMemory) {
|
||||||
throw new Error('Memory with this key already exists');
|
throw new Error('Memory with this key already exists');
|
||||||
@@ -63,6 +62,7 @@ export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
|||||||
return { ok: false };
|
return { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MemoryEntry = mongoose.models.MemoryEntry;
|
||||||
await MemoryEntry.findOneAndUpdate(
|
await MemoryEntry.findOneAndUpdate(
|
||||||
{ userId, key },
|
{ userId, key },
|
||||||
{
|
{
|
||||||
@@ -89,6 +89,7 @@ export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
|||||||
*/
|
*/
|
||||||
async function deleteMemory({ userId, key }: t.DeleteMemoryParams): Promise<t.MemoryResult> {
|
async function deleteMemory({ userId, key }: t.DeleteMemoryParams): Promise<t.MemoryResult> {
|
||||||
try {
|
try {
|
||||||
|
const MemoryEntry = mongoose.models.MemoryEntry;
|
||||||
const result = await MemoryEntry.findOneAndDelete({ userId, key });
|
const result = await MemoryEntry.findOneAndDelete({ userId, key });
|
||||||
return { ok: !!result };
|
return { ok: !!result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -105,6 +106,7 @@ export function createMemoryMethods(mongoose: typeof import('mongoose')) {
|
|||||||
userId: string | Types.ObjectId,
|
userId: string | Types.ObjectId,
|
||||||
): Promise<t.IMemoryEntryLean[]> {
|
): Promise<t.IMemoryEntryLean[]> {
|
||||||
try {
|
try {
|
||||||
|
const MemoryEntry = mongoose.models.MemoryEntry;
|
||||||
return (await MemoryEntry.find({ userId }).lean()) as t.IMemoryEntryLean[];
|
return (await MemoryEntry.find({ userId }).lean()) as t.IMemoryEntryLean[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import type { DeleteResult, Model } from 'mongoose';
|
import type { DeleteResult, Model } from 'mongoose';
|
||||||
import type { IPluginAuth } from '~/schema/pluginAuth';
|
|
||||||
import type {
|
import type {
|
||||||
FindPluginAuthsByKeysParams,
|
FindPluginAuthsByKeysParams,
|
||||||
UpdatePluginAuthParams,
|
UpdatePluginAuthParams,
|
||||||
DeletePluginAuthParams,
|
DeletePluginAuthParams,
|
||||||
FindPluginAuthParams,
|
FindPluginAuthParams,
|
||||||
|
IPluginAuth,
|
||||||
} from '~/types';
|
} from '~/types';
|
||||||
|
|
||||||
// Factory function that takes mongoose instance and returns the methods
|
// Factory function that takes mongoose instance and returns the methods
|
||||||
export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
||||||
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a single plugin auth entry by userId and authField
|
* Finds a single plugin auth entry by userId and authField
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +17,7 @@ export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
|||||||
authField,
|
authField,
|
||||||
}: FindPluginAuthParams): Promise<IPluginAuth | null> {
|
}: FindPluginAuthParams): Promise<IPluginAuth | null> {
|
||||||
try {
|
try {
|
||||||
|
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
||||||
return await PluginAuth.findOne({ userId, authField }).lean();
|
return await PluginAuth.findOne({ userId, authField }).lean();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -39,6 +38,7 @@ export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
||||||
return await PluginAuth.find({
|
return await PluginAuth.find({
|
||||||
userId,
|
userId,
|
||||||
pluginKey: { $in: pluginKeys },
|
pluginKey: { $in: pluginKeys },
|
||||||
@@ -60,6 +60,7 @@ export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
|||||||
value,
|
value,
|
||||||
}: UpdatePluginAuthParams): Promise<IPluginAuth> {
|
}: UpdatePluginAuthParams): Promise<IPluginAuth> {
|
||||||
try {
|
try {
|
||||||
|
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
||||||
const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
|
const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
|
||||||
|
|
||||||
if (existingAuth) {
|
if (existingAuth) {
|
||||||
@@ -95,6 +96,7 @@ export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
|||||||
all = false,
|
all = false,
|
||||||
}: DeletePluginAuthParams): Promise<DeleteResult> {
|
}: DeletePluginAuthParams): Promise<DeleteResult> {
|
||||||
try {
|
try {
|
||||||
|
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
||||||
if (all) {
|
if (all) {
|
||||||
const filter: DeletePluginAuthParams = { userId };
|
const filter: DeletePluginAuthParams = { userId };
|
||||||
if (pluginKey) {
|
if (pluginKey) {
|
||||||
@@ -120,6 +122,7 @@ export function createPluginAuthMethods(mongoose: typeof import('mongoose')) {
|
|||||||
*/
|
*/
|
||||||
async function deleteAllUserPluginAuths(userId: string): Promise<DeleteResult> {
|
async function deleteAllUserPluginAuths(userId: string): Promise<DeleteResult> {
|
||||||
try {
|
try {
|
||||||
|
const PluginAuth: Model<IPluginAuth> = mongoose.models.PluginAuth;
|
||||||
return await PluginAuth.deleteMany({ userId });
|
return await PluginAuth.deleteMany({ userId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
Reference in New Issue
Block a user