diff --git a/api/server/routes/agents/tools.js b/api/server/routes/agents/tools.js index 8e498b1db8..fbff6f6e43 100644 --- a/api/server/routes/agents/tools.js +++ b/api/server/routes/agents/tools.js @@ -1,4 +1,5 @@ const express = require('express'); +const { addTool } = require('@librechat/api'); const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools'); const { getAvailableTools } = require('~/server/controllers/PluginController'); const { toolCallLimiter } = require('~/server/middleware/limiters'); @@ -36,4 +37,12 @@ router.get('/:toolId/auth', verifyToolAuth); */ 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); + module.exports = router; diff --git a/client/src/common/mcp.ts b/client/src/common/mcp.ts index b4f44a1f94..d6739e168c 100644 --- a/client/src/common/mcp.ts +++ b/client/src/common/mcp.ts @@ -1,26 +1,13 @@ -import { - AuthorizationTypeEnum, - AuthTypeEnum, - TokenExchangeMethodEnum, -} from 'librechat-data-provider'; import { MCPForm } from '~/common/types'; 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: '', description: '', url: '', tools: [], icon: '', trust: false, + customHeaders: [], + requestTimeout: undefined, + connectionTimeout: undefined, }; diff --git a/client/src/components/SidePanel/MCP/MCPFormPanel.tsx b/client/src/components/SidePanel/MCP/MCPFormPanel.tsx index c6bcc7fb88..58f763533b 100644 --- a/client/src/components/SidePanel/MCP/MCPFormPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPFormPanel.tsx @@ -9,11 +9,6 @@ import { defaultMCPFormValues } from '~/common/mcp'; import useLocalize from '~/hooks/useLocalize'; import { TrashIcon } from '~/components/svg'; import MCPInput from './MCPInput'; -import { - AuthTypeEnum, - AuthorizationTypeEnum, - TokenExchangeMethodEnum, -} from 'librechat-data-provider'; interface MCPFormPanelProps { // Data @@ -66,24 +61,11 @@ export default function MCPFormPanel({ 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, }; - 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]); diff --git a/client/src/components/SidePanel/MCP/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPPanel.tsx index 850d555bfc..88a3aad54a 100644 --- a/client/src/components/SidePanel/MCP/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPPanel.tsx @@ -7,6 +7,7 @@ import type { TUpdateUserPlugins } from 'librechat-data-provider'; import type { MCP } from 'librechat-data-provider'; import { Button, Input, Label } from '~/components/ui'; import { useGetStartupConfig } from '~/data-provider'; +import { useAddToolMutation } from '~/data-provider/Tools/mutations'; import MCPPanelSkeleton from './MCPPanelSkeleton'; import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; @@ -60,6 +61,23 @@ export default function MCPPanel() { }, }); + const createMCPMutation = useAddToolMutation({ + 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( (serverName: string, updatedValues: Record) => { const payload: TUpdateUserPlugins = { @@ -101,13 +119,20 @@ export default function MCPPanel() { }; const handleSaveMCP = (mcp: MCP) => { - // TODO: Implement MCP save logic for conversation context - console.log('Saving MCP:', mcp); - showToast({ - message: localize('com_ui_update_mcp_success'), - status: 'success', - }); - setShowMCPForm(false); + // Transform MCP data to match the expected format + const mcpData = { + name: mcp.metadata.name || '', + description: mcp.metadata.description || '', + url: mcp.metadata.url || '', + icon: mcp.metadata.icon || '', + tools: mcp.metadata.tools || [], + trust: mcp.metadata.trust ?? false, + customHeaders: mcp.metadata.customHeaders || [], + requestTimeout: mcp.metadata.requestTimeout, + connectionTimeout: mcp.metadata.connectionTimeout, + }; + + createMCPMutation.mutate(mcpData); }; if (showMCPForm) { diff --git a/client/src/data-provider/Tools/mutations.ts b/client/src/data-provider/Tools/mutations.ts index 0c87c6e5eb..2501eecbb0 100644 --- a/client/src/data-provider/Tools/mutations.ts +++ b/client/src/data-provider/Tools/mutations.ts @@ -40,3 +40,43 @@ export const useToolCallMutation = ( }, ); }; + +interface CreateToolData { + name: string; + description: string; + metadata?: Record; + // MCP-specific fields + url?: string; + icon?: string; + tools?: string[]; + trust?: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; +} + +export const useAddToolMutation = ( + options?: t.MutationOptions, CreateToolData>, +): UseMutationResult, Error, CreateToolData> => { + const queryClient = useQueryClient(); + + return useMutation( + (toolData: CreateToolData) => { + return dataService.createTool(toolData); + }, + { + 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); + }, + }, + ); +}; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 0341de44b0..b5266343d0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,6 +2,7 @@ export * from './mcp/manager'; export * from './mcp/oauth'; export * from './mcp/auth'; +export * from './mcp/add'; /* Utilities */ export * from './mcp/utils'; export * from './utils'; diff --git a/packages/api/src/mcp/add.ts b/packages/api/src/mcp/add.ts new file mode 100644 index 0000000000..ed17c59991 --- /dev/null +++ b/packages/api/src/mcp/add.ts @@ -0,0 +1,154 @@ +import { Request, Response } from 'express'; +import { logger } from '@librechat/data-schemas'; + +interface MCPCreateRequest extends Request { + body: { + name: string; + description: string; + url: string; + icon?: string; + tools?: string[]; + trust: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; + }; + user?: { + id: string; + }; +} + +export interface MCPCreateResponse { + mcp_id: string; + metadata: { + name: string; + description: string; + url: string; + icon?: string; + tools?: string[]; + trust: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; + }; + created_at: string; + updated_at: string; +} + +export interface MCPCreateError { + error: string; + message?: string; +} + +/** + * Add a new tool/MCP to the system + * @route POST /agents/tools/add + * @param {object} req.body - Request body containing tool/MCP data + * @param {string} req.body.name - Tool/MCP name + * @param {string} req.body.description - Tool/MCP description + * @param {string} req.body.url - Tool/MCP server URL + * @param {string} [req.body.icon] - Tool/MCP icon (base64) + * @param {string[]} [req.body.tools] - Available tools + * @param {boolean} req.body.trust - Trust flag + * @param {string[]} [req.body.customHeaders] - Custom headers + * @param {string} [req.body.requestTimeout] - Request timeout + * @param {string} [req.body.connectionTimeout] - Connection timeout + * @returns {object} Created tool/MCP object + */ +export const addTool = async (req: MCPCreateRequest, res: Response) => { + try { + const { + name, + description, + url, + icon, + tools, + trust, + customHeaders, + requestTimeout, + connectionTimeout, + } = req.body; + + // Log the raw request body for debugging + logger.info('Raw request body: ' + JSON.stringify(req.body, null, 2)); + + // Log the incoming tool/MCP request for development + const logData = { + name, + description, + url, + icon: icon ? 'base64_icon_provided' : 'no_icon', + tools: tools || [], + trust, + customHeaders: customHeaders || [], + timeouts: { + requestTimeout: requestTimeout || 'default', + connectionTimeout: connectionTimeout || 'default', + }, + userId: req.user?.id, + timestamp: new Date().toISOString(), + }; + + logger.info('Add Tool/MCP Request: ' + JSON.stringify(logData, null, 2)); + + // Validate required fields + if (!name || !description || !url) { + return res.status(400).json({ + error: 'Missing required fields: name, description, and url are required', + }); + } + + // Validate URL format + try { + new URL(url); + } catch { + return res.status(400).json({ + error: 'Invalid URL format', + }); + } + + // Validate trust flag + if (typeof trust !== 'boolean') { + return res.status(400).json({ + error: 'Trust flag must be a boolean value', + }); + } + + // For now, return a mock successful response + // TODO: Implement actual tool/MCP creation logic + const mockTool = { + mcp_id: `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + metadata: { + name, + description, + url, + icon: icon || undefined, + tools: tools || [], + trust, + customHeaders: customHeaders || [], + requestTimeout, + connectionTimeout, + }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + logger.info('Tool/MCP created successfully:', JSON.stringify(mockTool, null, 2)); + + res.status(201).json(mockTool); + } catch (error) { + logger.error('Error adding tool/MCP:', error); + res.status(500).json({ + error: 'Internal server error while adding tool/MCP', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +}; diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index c76efbac87..49ce568021 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -832,3 +832,55 @@ export const createMemory = (data: { }): Promise<{ created: boolean; memory: q.TUserMemory }> => { return request.post(endpoints.memories(), data); }; + +export const createTool = (toolData: { + name: string; + description: string; + metadata?: Record; + // MCP-specific fields + url?: string; + icon?: string; + tools?: string[]; + trust?: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; +}): Promise<{ + id?: string; + mcp_id?: string; + type?: string; + function?: { + name: string; + description: string; + }; + metadata: + | Record + | { + name: string; + description: string; + url?: string; + icon?: string; + tools?: string[]; + trust?: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; + }; + created_at: string; + updated_at: string; +}> => { + return request.post( + endpoints.agents({ + path: 'tools/add', + }), + toolData, + ); +}; diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 93e21c7e45..786c29295f 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -48,6 +48,7 @@ export enum QueryKeys { banner = 'banner', /* Memories */ memories = 'memories', + mcpTools = 'mcpTools', } export enum MutationKeys { diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts index ff286c21f4..57c40f059a 100644 --- a/packages/data-provider/src/types/agents.ts +++ b/packages/data-provider/src/types/agents.ts @@ -342,13 +342,17 @@ export type MCPMetadata = Omit & { description?: string; url?: string; tools?: string[]; - auth?: MCPAuth; icon?: string; trust?: boolean; + customHeaders?: Array<{ + id: string; + name: string; + value: string; + }>; + requestTimeout?: number; + connectionTimeout?: number; }; -export type MCPAuth = ActionAuth; - export type AgentToolType = { tool_id: string; metadata: ToolMetadata;