mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
✨ feat: Implement MCP Server Management API and UI Components
- Added API endpoints for managing MCP servers: create, read, update, and delete functionalities. - Introduced new UI components for MCP server configuration, including MCPFormPanel and MCPConfig. - Updated existing types and data provider to support MCP operations. - Enhanced the side panel to include MCP server management options. - Refactored related components and hooks for better integration with the new MCP features. - Added tests for the new MCP server API functionalities.
This commit is contained in:
parent
20100e120b
commit
351f30254c
26 changed files with 1189 additions and 290 deletions
|
|
@ -5,6 +5,13 @@ const { CacheKeys } = require('librechat-data-provider');
|
||||||
const { requireJwtAuth } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { getFlowStateManager } = require('~/config');
|
const { getFlowStateManager } = require('~/config');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
const {
|
||||||
|
getMCPServers,
|
||||||
|
getMCPServer,
|
||||||
|
createMCPServer,
|
||||||
|
updateMCPServer,
|
||||||
|
deleteMCPServer,
|
||||||
|
} = require('@librechat/api');
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -202,4 +209,44 @@ router.get('/oauth/status/:flowId', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all MCP servers for the authenticated user
|
||||||
|
* @route GET /api/mcp
|
||||||
|
* @returns {Array} Array of MCP servers
|
||||||
|
*/
|
||||||
|
router.get('/', requireJwtAuth, getMCPServers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single MCP server by ID
|
||||||
|
* @route GET /api/mcp/:mcp_id
|
||||||
|
* @param {string} mcp_id - The ID of the MCP server to fetch
|
||||||
|
* @returns {object} MCP server data
|
||||||
|
*/
|
||||||
|
router.get('/:mcp_id', requireJwtAuth, getMCPServer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new MCP server
|
||||||
|
* @route POST /api/mcp/add
|
||||||
|
* @param {object} req.body - MCP server data
|
||||||
|
* @returns {object} Created MCP server with populated tools
|
||||||
|
*/
|
||||||
|
router.post('/add', requireJwtAuth, createMCPServer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing MCP server
|
||||||
|
* @route PUT /api/mcp/:mcp_id
|
||||||
|
* @param {string} mcp_id - The ID of the MCP server to update
|
||||||
|
* @param {object} req.body - Updated MCP server data
|
||||||
|
* @returns {object} Updated MCP server with populated tools
|
||||||
|
*/
|
||||||
|
router.put('/:mcp_id', requireJwtAuth, updateMCPServer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an MCP server
|
||||||
|
* @route DELETE /api/mcp/:mcp_id
|
||||||
|
* @param {string} mcp_id - The ID of the MCP server to delete
|
||||||
|
* @returns {object} Deletion confirmation
|
||||||
|
*/
|
||||||
|
router.delete('/:mcp_id', requireJwtAuth, deleteMCPServer);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
import { FileSources, EModelEndpoint } from 'librechat-data-provider';
|
import { FileSources, EModelEndpoint, TPlugin } from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type * as InputNumberPrimitive from 'rc-input-number';
|
import type * as InputNumberPrimitive from 'rc-input-number';
|
||||||
import type { SetterOrUpdater, RecoilState } from 'recoil';
|
import type { SetterOrUpdater, RecoilState } from 'recoil';
|
||||||
|
|
@ -167,13 +167,27 @@ export type ActionAuthForm = {
|
||||||
token_exchange_method: t.TokenExchangeMethodEnum;
|
token_exchange_method: t.TokenExchangeMethodEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MCPForm = ActionAuthForm & {
|
export type MCPForm = MCPMetadata;
|
||||||
name?: string;
|
|
||||||
|
export type MCP = {
|
||||||
|
mcp_id: string;
|
||||||
|
metadata: MCPMetadata;
|
||||||
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
||||||
|
|
||||||
|
export type MCPMetadata = {
|
||||||
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url: string;
|
||||||
tools?: string[];
|
tools?: TPlugin[];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
trust?: boolean;
|
trust?: boolean;
|
||||||
|
customHeaders?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
requestTimeout?: number;
|
||||||
|
connectionTimeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ 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 { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function AgentPanelSwitch() {
|
export default function AgentPanelSwitch() {
|
||||||
|
|
@ -54,8 +53,5 @@ function AgentPanelSwitchWithContext() {
|
||||||
if (activePanel === Panel.version) {
|
if (activePanel === Panel.version) {
|
||||||
return <VersionPanel />;
|
return <VersionPanel />;
|
||||||
}
|
}
|
||||||
if (activePanel === Panel.mcp) {
|
|
||||||
return <MCPPanel />;
|
|
||||||
}
|
|
||||||
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
222
client/src/components/SidePanel/MCP/MCPConfig.tsx
Normal file
222
client/src/components/SidePanel/MCP/MCPConfig.tsx
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
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 MCPConfig() {
|
||||||
|
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="rounded px-4 py-3 text-sm font-medium hover:no-underline focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||||
|
{localize('com_ui_authentication')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4 pt-2">
|
||||||
|
<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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<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 focus:ring-offset-2"
|
||||||
|
/>
|
||||||
|
<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 focus:ring-offset-2"
|
||||||
|
/>
|
||||||
|
<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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<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="rounded px-4 py-3 text-sm font-medium hover:no-underline focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||||
|
{localize('com_ui_mcp_configuration')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4 pt-2">
|
||||||
|
<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"
|
||||||
|
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 focus:ring-offset-2"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
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 focus:ring-offset-2"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-text-secondary">
|
||||||
|
{localize('com_ui_mcp_connection_timeout_description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,66 +1,103 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
import type { MCPForm } from '~/common';
|
||||||
|
import {
|
||||||
|
useCreateMCPMutation,
|
||||||
|
useUpdateMCPMutation,
|
||||||
|
useDeleteMCPMutation,
|
||||||
|
} from '~/data-provider/MCPs/mutations';
|
||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { defaultMCPFormValues } from '~/common/mcp';
|
import { defaultMCPFormValues } from '~/common/mcp';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { TrashIcon } from '~/components/svg';
|
import { TrashIcon } from '~/components/svg';
|
||||||
import type { MCPForm } from '~/common';
|
|
||||||
import MCPInput from './MCPInput';
|
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({
|
interface MCPFormPanelProps {
|
||||||
onSuccess,
|
// Data
|
||||||
onError,
|
mcp?: MCP;
|
||||||
}: {
|
|
||||||
onSuccess: () => void;
|
// Actions
|
||||||
onError: (error: Error) => void;
|
onBack: () => void;
|
||||||
}) {
|
|
||||||
return {
|
// UI customization
|
||||||
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
title?: string;
|
||||||
try {
|
subtitle?: string;
|
||||||
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
showDeleteButton?: boolean;
|
||||||
onSuccess();
|
deleteConfirmMessage?: string;
|
||||||
} catch (error) {
|
|
||||||
onError(error as Error);
|
// Form customization
|
||||||
}
|
defaultValues?: Partial<MCPForm>;
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MCPPanel() {
|
export default function MCPFormPanel({
|
||||||
|
mcp,
|
||||||
|
onBack,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
showDeleteButton = true,
|
||||||
|
deleteConfirmMessage,
|
||||||
|
defaultValues = defaultMCPFormValues,
|
||||||
|
}: MCPFormPanelProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
|
||||||
const deleteAgentMCP = useDeleteAgentMCP({
|
const create = useCreateMCPMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
onBack();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error creating MCP:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = useUpdateMCPMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
onBack();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error updating MCP:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMCP = useDeleteMCPMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_delete_mcp_success'),
|
message: localize('com_ui_delete_mcp_success'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
});
|
});
|
||||||
setActivePanel(Panel.builder);
|
onBack();
|
||||||
setMcp(undefined);
|
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError: (error) => {
|
||||||
|
console.error('Error deleting MCP:', error);
|
||||||
showToast({
|
showToast({
|
||||||
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
message: localize('com_ui_delete_mcp_error'),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const methods = useForm<MCPForm>({
|
const methods = useForm<MCPForm>({
|
||||||
defaultValues: defaultMCPFormValues,
|
defaultValues: defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reset } = methods;
|
const { reset } = methods;
|
||||||
|
|
@ -74,55 +111,51 @@ export default function MCPPanel() {
|
||||||
url: mcp.metadata.url ?? '',
|
url: mcp.metadata.url ?? '',
|
||||||
tools: mcp.metadata.tools ?? [],
|
tools: mcp.metadata.tools ?? [],
|
||||||
trust: mcp.metadata.trust ?? false,
|
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);
|
reset(formData);
|
||||||
}
|
}
|
||||||
}, [mcp, reset]);
|
}, [mcp, reset]);
|
||||||
|
|
||||||
|
const handleSave = (mcpData: MCP) => {
|
||||||
|
if (mcp) {
|
||||||
|
// Update existing MCP
|
||||||
|
update.mutate({ mcp_id: mcp.mcp_id, data: mcpData });
|
||||||
|
} else {
|
||||||
|
// Create new MCP
|
||||||
|
create.mutate(mcpData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (mcp?.mcp_id) {
|
||||||
|
deleteMCP.mutate({ mcp_id: mcp.mcp_id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className="h-full grow overflow-hidden">
|
<form className="h-full grow overflow-hidden">
|
||||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
<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="relative flex flex-col items-center px-16 py-6 text-center">
|
||||||
<div className="absolute left-0 top-6">
|
<div className="absolute left-0 top-6">
|
||||||
<button
|
<button type="button" className="btn btn-neutral relative" onClick={onBack}>
|
||||||
type="button"
|
|
||||||
className="btn btn-neutral relative"
|
|
||||||
onClick={() => {
|
|
||||||
setActivePanel(Panel.builder);
|
|
||||||
setMcp(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-center gap-2">
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!mcp && (
|
{!!mcp && showDeleteButton && (
|
||||||
<OGDialog>
|
<OGDialog>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<div className="absolute right-0 top-6">
|
<div className="absolute right-0 top-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!agent_id || !mcp.mcp_id}
|
disabled={!mcp.mcp_id}
|
||||||
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
||||||
>
|
>
|
||||||
<TrashIcon className="text-red-500" />
|
<TrashIcon className="text-red-500" />
|
||||||
|
|
@ -135,22 +168,11 @@ export default function MCPPanel() {
|
||||||
className="max-w-[450px]"
|
className="max-w-[450px]"
|
||||||
main={
|
main={
|
||||||
<Label className="text-left text-sm font-medium">
|
<Label className="text-left text-sm font-medium">
|
||||||
{localize('com_ui_delete_mcp_confirm')}
|
{deleteConfirmMessage || localize('com_ui_delete_mcp_confirm')}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
selection={{
|
selection={{
|
||||||
selectHandler: () => {
|
selectHandler: handleDelete,
|
||||||
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:
|
selectClasses:
|
||||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
'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'),
|
selectText: localize('com_ui_delete'),
|
||||||
|
|
@ -160,11 +182,17 @@ export default function MCPPanel() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-xl font-medium">
|
<div className="text-xl font-medium">
|
||||||
{mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server')}
|
{title ||
|
||||||
|
(mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server'))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-text-secondary">{localize('com_agents_mcp_info')}</div>
|
<div className="text-xs text-text-secondary">{subtitle || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<MCPInput mcp={mcp} agent_id={agent_id} setMCP={setMcp} />
|
<MCPInput
|
||||||
|
mcp={mcp}
|
||||||
|
agent_id=""
|
||||||
|
onSave={handleSave}
|
||||||
|
isLoading={create.isLoading || update.isLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
@ -1,58 +1,31 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
import type { MCP } from 'librechat-data-provider';
|
import type { MCP, MCPMetadata } from 'librechat-data-provider';
|
||||||
import MCPAuth from '~/components/SidePanel/Builder/MCPAuth';
|
import { MCPConfig } from '~/components/SidePanel/MCP/MCPConfig';
|
||||||
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,
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
} = 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 +37,20 @@ 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);
|
// Generate MCP ID using server name and delimiter for new MCPs
|
||||||
try {
|
const mcpId =
|
||||||
const response = await updateAgentMCP.mutate({
|
mcp?.mcp_id || `${data.name.replace(/\s+/g, '_').toLowerCase()}${Constants.mcp_delimiter}`;
|
||||||
agent_id: agent_id ?? '',
|
|
||||||
mcp_id: mcp?.mcp_id,
|
const updatedMCP: MCP = {
|
||||||
metadata: {
|
mcp_id: mcpId,
|
||||||
...data,
|
agent_id: agent_id ?? '',
|
||||||
tools: selectedTools,
|
metadata: {
|
||||||
},
|
...data,
|
||||||
});
|
tools: selectedTools,
|
||||||
setMCP(response[1]);
|
} as MCPMetadata, // Type assertion since form validation ensures required fields
|
||||||
showToast({
|
};
|
||||||
message: localize('com_ui_update_mcp_success'),
|
onSave(updatedMCP);
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_update_mcp_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
|
|
@ -140,14 +83,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);
|
||||||
}
|
}
|
||||||
|
|
@ -205,25 +149,48 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<MCPAuth />
|
<MCPConfig />
|
||||||
<div className="my-2 flex items-center gap-2">
|
<div className="my-2 flex items-center">
|
||||||
<Controller
|
<Controller
|
||||||
name="trust"
|
name="trust"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Checkbox id="trust" checked={field.value} onCheckedChange={field.onChange} />
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={field.value ?? false}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||||
|
value={(field.value ?? false).toString()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="trust" className="flex flex-col">
|
<button
|
||||||
{localize('com_ui_trust_app')}
|
type="button"
|
||||||
<span className="text-xs text-text-secondary">
|
className="flex items-center space-x-2"
|
||||||
{localize('com_agents_mcp_trust_subtext')}
|
onClick={() =>
|
||||||
</span>
|
setValue('trust', !getValues('trust'), {
|
||||||
</Label>
|
shouldDirty: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||||
|
htmlFor="trust"
|
||||||
|
>
|
||||||
|
{localize('com_ui_trust_app')}
|
||||||
|
</label>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="-mt-5 ml-6">
|
||||||
|
<span className="text-xs text-text-secondary">
|
||||||
|
{localize('com_agents_mcp_trust_subtext')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{errors.trust && (
|
{errors.trust && (
|
||||||
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
<div className="ml-6">
|
||||||
|
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -231,7 +198,7 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
||||||
<button
|
<button
|
||||||
onClick={saveMCP}
|
onClick={saveMCP}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
|
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white transition-colors duration-200 hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:bg-green-400"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
@ -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,7 +1,7 @@
|
||||||
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 { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
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 { Button, Input, Label } from '~/components/ui';
|
import { Button, Input, Label } from '~/components/ui';
|
||||||
|
|
@ -9,6 +9,7 @@ import { useGetStartupConfig } from '~/data-provider';
|
||||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import MCPFormPanel from './MCPFormPanel';
|
||||||
|
|
||||||
interface ServerConfigWithVars {
|
interface ServerConfigWithVars {
|
||||||
serverName: string;
|
serverName: string;
|
||||||
|
|
@ -24,6 +25,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) {
|
||||||
|
|
@ -89,14 +91,47 @@ export default function MCPPanel() {
|
||||||
setSelectedServerNameForEditing(null);
|
setSelectedServerNameForEditing(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddMCP = () => {
|
||||||
|
setShowMCPForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackFromForm = () => {
|
||||||
|
setShowMCPForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showMCPForm) {
|
||||||
|
return (
|
||||||
|
<MCPFormPanel
|
||||||
|
onBack={handleBackFromForm}
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -144,15 +179,28 @@ export default function MCPPanel() {
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mcpServerDefinitions.map((server) => (
|
{mcpServerDefinitions.map((server) => (
|
||||||
<Button
|
<button
|
||||||
key={server.serverName}
|
key={server.serverName}
|
||||||
variant="outline"
|
type="button"
|
||||||
className="w-full justify-start dark:hover:bg-gray-700"
|
|
||||||
onClick={() => handleServerClickToEdit(server.serverName)}
|
onClick={() => handleServerClickToEdit(server.serverName)}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||||
|
aria-label={`Configure MCP server ${server.serverName}`}
|
||||||
>
|
>
|
||||||
{server.serverName}
|
<div className="flex w-full items-center justify-start gap-2">
|
||||||
</Button>
|
{server.serverName}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
|
|
@ -181,7 +229,7 @@ function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariab
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Always initialize with empty strings based on the schema
|
// Always initialize with empty strings based on the schema
|
||||||
const initialFormValues = Object.keys(server.config.customUserVars).reduce(
|
const initialFormValues = Object.keys(server.config.customUserVars || {}).reduce(
|
||||||
(acc, key) => {
|
(acc, key) => {
|
||||||
acc[key] = '';
|
acc[key] = '';
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -230,7 +278,7 @@ function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariab
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
{Object.keys(server.config.customUserVars).length > 0 && (
|
{Object.keys(server.config.customUserVars || {}).length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleRevokeClick}
|
onClick={handleRevokeClick}
|
||||||
|
|
|
||||||
1
client/src/data-provider/MCPs/index.ts
Normal file
1
client/src/data-provider/MCPs/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './mutations';
|
||||||
73
client/src/data-provider/MCPs/mutations.ts
Normal file
73
client/src/data-provider/MCPs/mutations.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { dataService, QueryKeys } from 'librechat-data-provider';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
|
import type * as t from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export const useCreateMCPMutation = (
|
||||||
|
options?: t.CreateMCPMutationOptions,
|
||||||
|
): UseMutationResult<t.MCP, 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) => {
|
||||||
|
queryClient.setQueryData<t.MCP[]>([QueryKeys.mcpServers], (prev) => {
|
||||||
|
return prev ? [...prev, data] : [data];
|
||||||
|
});
|
||||||
|
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMCPMutation = (
|
||||||
|
options?: t.UpdateMCPMutationOptions,
|
||||||
|
): UseMutationResult<t.MCP, 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) => {
|
||||||
|
queryClient.setQueryData<t.MCP[]>([QueryKeys.mcpServers], (prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return prev.map((mcp) => (mcp.mcp_id === variables.mcp_id ? data : mcp));
|
||||||
|
});
|
||||||
|
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) => {
|
||||||
|
queryClient.setQueryData<t.MCP[]>([QueryKeys.mcpServers], (prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return prev.filter((mcp) => mcp.mcp_id !== variables.mcp_id);
|
||||||
|
});
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -7,6 +7,7 @@ export * from './Memories';
|
||||||
export * from './Messages';
|
export * from './Messages';
|
||||||
export * from './Misc';
|
export * from './Misc';
|
||||||
export * from './Tools';
|
export * from './Tools';
|
||||||
|
export * from './MCPs';
|
||||||
export * from './connection';
|
export * from './connection';
|
||||||
export * from './mutations';
|
export * from './mutations';
|
||||||
export * from './prompts';
|
export * from './prompts';
|
||||||
|
|
|
||||||
|
|
@ -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[] = [];
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -457,7 +459,6 @@
|
||||||
"com_nav_setting_chat": "Chat",
|
"com_nav_setting_chat": "Chat",
|
||||||
"com_nav_setting_data": "Data controls",
|
"com_nav_setting_data": "Data controls",
|
||||||
"com_nav_setting_general": "General",
|
"com_nav_setting_general": "General",
|
||||||
"com_nav_setting_mcp": "MCP Settings",
|
|
||||||
"com_nav_setting_personalization": "Personalization",
|
"com_nav_setting_personalization": "Personalization",
|
||||||
"com_nav_setting_speech": "Speech",
|
"com_nav_setting_speech": "Speech",
|
||||||
"com_nav_settings": "Settings",
|
"com_nav_settings": "Settings",
|
||||||
|
|
@ -827,6 +828,17 @@
|
||||||
"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_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",
|
||||||
|
|
|
||||||
|
|
@ -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/servers';
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
export * from './mcp/utils';
|
export * from './mcp/utils';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
||||||
212
packages/api/src/mcp/servers.spec.ts
Normal file
212
packages/api/src/mcp/servers.spec.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { Response } from 'express';
|
||||||
|
import type { TUser } from 'librechat-data-provider';
|
||||||
|
import { getMCPServers, createMCPServer, updateMCPServer, deleteMCPServer } from './servers';
|
||||||
|
import type { AuthenticatedRequest, MCPRequest, MCPParamsRequest } from '../types';
|
||||||
|
|
||||||
|
describe('MCP Server Functions', () => {
|
||||||
|
let mockReq: Partial<AuthenticatedRequest>;
|
||||||
|
let mockRes: Partial<Response>;
|
||||||
|
let mockUser: TUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUser = { id: 'user123' } as TUser;
|
||||||
|
|
||||||
|
mockReq = { user: mockUser };
|
||||||
|
mockRes = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMCPServers', () => {
|
||||||
|
it('should return mock MCP servers', async () => {
|
||||||
|
await getMCPServers(mockReq as AuthenticatedRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
mcp_id: 'mcp_weather_001',
|
||||||
|
metadata: expect.objectContaining({
|
||||||
|
name: 'Weather Service',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
mcp_id: 'mcp_calendar_002',
|
||||||
|
metadata: expect.objectContaining({
|
||||||
|
name: 'Calendar Manager',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject unauthenticated requests', async () => {
|
||||||
|
mockReq.user = undefined;
|
||||||
|
|
||||||
|
await getMCPServers(mockReq as AuthenticatedRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'User not authenticated' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createMCPServer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq.body = {
|
||||||
|
mcp_id: 'mcp_test_123',
|
||||||
|
metadata: {
|
||||||
|
name: 'Test MCP Server',
|
||||||
|
description: 'A test MCP server',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
tools: ['test_tool'],
|
||||||
|
icon: '🔧',
|
||||||
|
trust: false,
|
||||||
|
customHeaders: [],
|
||||||
|
requestTimeout: 30000,
|
||||||
|
connectionTimeout: 10000,
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new MCP server from form data', async () => {
|
||||||
|
await createMCPServer(mockReq as MCPRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
mcp_id: 'mcp_test_123',
|
||||||
|
metadata: expect.objectContaining({
|
||||||
|
name: 'Test MCP Server',
|
||||||
|
description: 'A test MCP server',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
tools: ['test_tool'],
|
||||||
|
icon: '🔧',
|
||||||
|
trust: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent duplicate server names', async () => {
|
||||||
|
mockReq.body = {
|
||||||
|
metadata: {
|
||||||
|
name: 'Weather Service', // This name already exists in mock data
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
await createMCPServer(mockReq as MCPRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(409);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'MCP server already exists' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate required fields', async () => {
|
||||||
|
mockReq.body = {
|
||||||
|
metadata: {
|
||||||
|
description: 'A test MCP server',
|
||||||
|
// Missing name and url
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
await createMCPServer(mockReq as MCPRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({
|
||||||
|
message: 'Missing required fields: name and url are required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateMCPServer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq.body = {
|
||||||
|
metadata: {
|
||||||
|
name: 'Updated MCP Server',
|
||||||
|
description: 'An updated MCP server',
|
||||||
|
url: 'http://localhost:3001',
|
||||||
|
tools: ['updated_tool'],
|
||||||
|
icon: '⚙️',
|
||||||
|
trust: true,
|
||||||
|
customHeaders: [],
|
||||||
|
requestTimeout: 45000,
|
||||||
|
connectionTimeout: 15000,
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
};
|
||||||
|
mockReq.params = { mcp_id: 'mcp_weather_001' }; // Use existing mock server ID
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing MCP server', async () => {
|
||||||
|
await updateMCPServer(mockReq as MCPParamsRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
mcp_id: 'mcp_weather_001',
|
||||||
|
metadata: expect.objectContaining({
|
||||||
|
name: 'Updated MCP Server',
|
||||||
|
description: 'An updated MCP server',
|
||||||
|
url: 'http://localhost:3001',
|
||||||
|
tools: ['updated_tool'],
|
||||||
|
icon: '⚙️',
|
||||||
|
trust: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject updates to non-existent servers', async () => {
|
||||||
|
mockReq.params = { mcp_id: 'non_existent_id' };
|
||||||
|
|
||||||
|
await updateMCPServer(mockReq as MCPParamsRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'MCP server not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate required fields', async () => {
|
||||||
|
mockReq.body = {
|
||||||
|
metadata: {
|
||||||
|
description: 'An updated MCP server',
|
||||||
|
// Missing name and url
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateMCPServer(mockReq as MCPParamsRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({
|
||||||
|
message: 'Missing required fields: name and url are required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteMCPServer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq.params = { mcp_id: 'mcp_weather_001' }; // Use existing mock server ID
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete existing MCP server', async () => {
|
||||||
|
await deleteMCPServer(mockReq as MCPParamsRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'MCP server deleted successfully' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject deletion of non-existent servers', async () => {
|
||||||
|
mockReq.params = { mcp_id: 'non_existent_id' };
|
||||||
|
|
||||||
|
await deleteMCPServer(mockReq as MCPParamsRequest, mockRes as Response);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'MCP server not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
270
packages/api/src/mcp/servers.ts
Normal file
270
packages/api/src/mcp/servers.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import type { AuthenticatedRequest, MCPRequest, MCPParamsRequest } from '../types';
|
||||||
|
|
||||||
|
// Mock data for demonstration
|
||||||
|
const mockMCPServers: MCP[] = [
|
||||||
|
{
|
||||||
|
mcp_id: 'mcp_weather_001',
|
||||||
|
metadata: {
|
||||||
|
name: 'Weather Service',
|
||||||
|
description: 'Provides weather information and forecasts',
|
||||||
|
url: 'https://weather-mcp.example.com',
|
||||||
|
tools: ['get_current_weather', 'get_forecast', 'get_weather_alerts'],
|
||||||
|
icon: '',
|
||||||
|
trust: true,
|
||||||
|
customHeaders: [],
|
||||||
|
requestTimeout: 30000,
|
||||||
|
connectionTimeout: 10000,
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mcp_id: 'mcp_calendar_002',
|
||||||
|
metadata: {
|
||||||
|
name: 'Calendar Manager',
|
||||||
|
description: 'Manages calendar events and scheduling',
|
||||||
|
url: 'https://calendar-mcp.example.com',
|
||||||
|
tools: ['create_event', 'list_events', 'update_event', 'delete_event'],
|
||||||
|
icon: '',
|
||||||
|
trust: false,
|
||||||
|
customHeaders: [{ id: '1', name: 'Authorization', value: 'Bearer {{api_key}}' }],
|
||||||
|
requestTimeout: 45000,
|
||||||
|
connectionTimeout: 15000,
|
||||||
|
},
|
||||||
|
agent_id: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all MCP servers for the authenticated user
|
||||||
|
*/
|
||||||
|
export const getMCPServers = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('MCP servers fetch without user ID');
|
||||||
|
res.status(401).json({ message: 'User not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return mock MCP servers
|
||||||
|
res.json(mockMCPServers);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching MCP servers:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to fetch MCP servers' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single MCP server by ID
|
||||||
|
*/
|
||||||
|
export const getMCPServer = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { mcp_id } = req.params;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('MCP server fetch without user ID');
|
||||||
|
res.status(401).json({ message: 'User not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mcp_id) {
|
||||||
|
logger.warn('MCP server fetch with missing mcp_id');
|
||||||
|
res.status(400).json({ message: 'Missing required parameter: mcp_id' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the MCP server
|
||||||
|
const server = mockMCPServers.find((s) => s.mcp_id === mcp_id);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
logger.warn(`MCP server ${mcp_id} not found for user ${userId}`);
|
||||||
|
res.status(404).json({ message: 'MCP server not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(server);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching MCP server:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to fetch MCP server' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new MCP server
|
||||||
|
*/
|
||||||
|
export const createMCPServer = async (req: MCPRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { body: formData } = req;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('MCP server creation without user ID');
|
||||||
|
res.status(401).json({ message: 'User not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!formData?.metadata?.name || !formData?.metadata?.url) {
|
||||||
|
logger.warn('MCP server creation with missing required fields');
|
||||||
|
res.status(400).json({
|
||||||
|
message: 'Missing required fields: name and url are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server already exists
|
||||||
|
const serverExists = mockMCPServers.some(
|
||||||
|
(server) => server.metadata.name === formData.metadata.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serverExists) {
|
||||||
|
logger.warn(`MCP server ${formData.metadata.name} already exists for user ${userId}`);
|
||||||
|
res.status(409).json({ message: 'MCP server already exists' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new MCP server from form data
|
||||||
|
const newMCPServer: MCP = {
|
||||||
|
mcp_id: formData.mcp_id || `mcp_${Date.now()}`,
|
||||||
|
metadata: {
|
||||||
|
name: formData.metadata.name,
|
||||||
|
description: formData.metadata.description || '',
|
||||||
|
url: formData.metadata.url,
|
||||||
|
tools: formData.metadata.tools || [],
|
||||||
|
icon: formData.metadata.icon || '🔧',
|
||||||
|
trust: formData.metadata.trust || false,
|
||||||
|
customHeaders: formData.metadata.customHeaders || [],
|
||||||
|
requestTimeout: formData.metadata.requestTimeout || 30000,
|
||||||
|
connectionTimeout: formData.metadata.connectionTimeout || 10000,
|
||||||
|
},
|
||||||
|
agent_id: formData.agent_id || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`Created MCP server: ${newMCPServer.mcp_id} for user ${userId}`);
|
||||||
|
|
||||||
|
res.status(201).json(newMCPServer);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating MCP server:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to create MCP server' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing MCP server
|
||||||
|
*/
|
||||||
|
export const updateMCPServer = async (req: MCPParamsRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
body: formData,
|
||||||
|
params: { mcp_id },
|
||||||
|
} = req;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('MCP server update without user ID');
|
||||||
|
res.status(401).json({ message: 'User not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!formData?.metadata?.name || !formData?.metadata?.url) {
|
||||||
|
logger.warn('MCP server update with missing required fields');
|
||||||
|
res.status(400).json({
|
||||||
|
message: 'Missing required fields: name and url are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mcp_id) {
|
||||||
|
logger.warn('MCP server update with missing mcp_id');
|
||||||
|
res.status(400).json({ message: 'Missing required parameter: mcp_id' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server exists
|
||||||
|
const existingServer = mockMCPServers.find((server) => server.mcp_id === mcp_id);
|
||||||
|
|
||||||
|
if (!existingServer) {
|
||||||
|
logger.warn(`MCP server ${mcp_id} not found for update for user ${userId}`);
|
||||||
|
res.status(404).json({ message: 'MCP server not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create updated MCP server from form data
|
||||||
|
const updatedMCP: MCP = {
|
||||||
|
mcp_id,
|
||||||
|
metadata: {
|
||||||
|
name: formData.metadata.name,
|
||||||
|
description: formData.metadata.description || existingServer.metadata.description || '',
|
||||||
|
url: formData.metadata.url,
|
||||||
|
tools: formData.metadata.tools || existingServer.metadata.tools || [],
|
||||||
|
icon: formData.metadata.icon || existingServer.metadata.icon || '🔧',
|
||||||
|
trust:
|
||||||
|
formData.metadata.trust !== undefined
|
||||||
|
? formData.metadata.trust
|
||||||
|
: existingServer.metadata.trust || false,
|
||||||
|
customHeaders:
|
||||||
|
formData.metadata.customHeaders || existingServer.metadata.customHeaders || [],
|
||||||
|
requestTimeout:
|
||||||
|
formData.metadata.requestTimeout || existingServer.metadata.requestTimeout || 30000,
|
||||||
|
connectionTimeout:
|
||||||
|
formData.metadata.connectionTimeout || existingServer.metadata.connectionTimeout || 10000,
|
||||||
|
},
|
||||||
|
agent_id: formData.agent_id || existingServer.agent_id || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// In a real implementation, you would update this in a database
|
||||||
|
logger.info(`Updated MCP server: ${mcp_id} for user ${userId}`);
|
||||||
|
|
||||||
|
res.json(updatedMCP);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating MCP server:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to update MCP server' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an MCP server
|
||||||
|
*/
|
||||||
|
export const deleteMCPServer = async (req: MCPParamsRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
params: { mcp_id },
|
||||||
|
} = req;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('MCP server deletion without user ID');
|
||||||
|
res.status(401).json({ message: 'User not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mcp_id) {
|
||||||
|
logger.warn('MCP server deletion with missing mcp_id');
|
||||||
|
res.status(400).json({ message: 'Missing required parameter: mcp_id' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server exists
|
||||||
|
const serverExists = mockMCPServers.some((server) => server.mcp_id === mcp_id);
|
||||||
|
|
||||||
|
if (!serverExists) {
|
||||||
|
logger.warn(`MCP server ${mcp_id} not found for deletion for user ${userId}`);
|
||||||
|
res.status(404).json({ message: 'MCP server not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real implementation, you would delete this from a database
|
||||||
|
logger.info(`Deleted MCP server: ${mcp_id} for user ${userId}`);
|
||||||
|
|
||||||
|
res.json({ message: 'MCP server deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting MCP server:', error);
|
||||||
|
res.status(500).json({ message: 'Failed to delete MCP server' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,5 +2,6 @@ export * from './azure';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './google';
|
export * from './google';
|
||||||
export * from './mistral';
|
export * from './mistral';
|
||||||
|
export * from './mcp';
|
||||||
export * from './openai';
|
export * from './openai';
|
||||||
export * from './run';
|
export * from './run';
|
||||||
|
|
|
||||||
17
packages/api/src/types/mcp.ts
Normal file
17
packages/api/src/types/mcp.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { TUser, MCP } from 'librechat-data-provider';
|
||||||
|
import type { Request } from 'express';
|
||||||
|
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user?: TUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPRequest extends AuthenticatedRequest {
|
||||||
|
body: MCP;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPParamsRequest extends AuthenticatedRequest {
|
||||||
|
params: {
|
||||||
|
mcp_id: string;
|
||||||
|
};
|
||||||
|
body: MCP;
|
||||||
|
}
|
||||||
|
|
@ -185,6 +185,21 @@ export const agents = ({ path = '', options }: { path?: string; options?: object
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mcps = ({ path = '', options }: { path?: string; options?: object }) => {
|
||||||
|
let url = '/api/mcp';
|
||||||
|
|
||||||
|
if (path && path !== '') {
|
||||||
|
url += `/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && Object.keys(options).length > 0) {
|
||||||
|
const queryParams = new URLSearchParams(options as Record<string, string>).toString();
|
||||||
|
url += `?${queryParams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${agent_id}/revert` })}`;
|
export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${agent_id}/revert` })}`;
|
||||||
|
|
||||||
export const files = () => '/api/files';
|
export const files = () => '/api/files';
|
||||||
|
|
|
||||||
|
|
@ -832,3 +832,41 @@ 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<ag.MCP> => {
|
||||||
|
return request.post(
|
||||||
|
endpoints.mcps({
|
||||||
|
path: 'add',
|
||||||
|
}),
|
||||||
|
mcp,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMCPServers = (): Promise<ag.MCP[]> => {
|
||||||
|
return request.get(endpoints.mcps({}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMCP = (mcp_id: string): Promise<ag.MCP> => {
|
||||||
|
return request.get(
|
||||||
|
endpoints.mcps({
|
||||||
|
path: mcp_id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMCP = ({ mcp_id, data }: { mcp_id: string; data: ag.MCP }): Promise<ag.MCP> => {
|
||||||
|
return request.put(
|
||||||
|
endpoints.mcps({
|
||||||
|
path: mcp_id,
|
||||||
|
}),
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMCP = ({ mcp_id }: { mcp_id: string }): Promise<Record<string, unknown>> => {
|
||||||
|
return request.delete(
|
||||||
|
endpoints.mcps({
|
||||||
|
path: mcp_id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export enum QueryKeys {
|
||||||
banner = 'banner',
|
banner = 'banner',
|
||||||
/* Memories */
|
/* Memories */
|
||||||
memories = 'memories',
|
memories = 'memories',
|
||||||
|
mcpServers = 'mcpServers',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MutationKeys {
|
export enum MutationKeys {
|
||||||
|
|
|
||||||
|
|
@ -337,18 +337,22 @@ export type MCP = {
|
||||||
metadata: MCPMetadata;
|
metadata: MCPMetadata;
|
||||||
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
||||||
|
|
||||||
export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
|
export type MCPMetadata = {
|
||||||
name?: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url: string;
|
||||||
tools?: string[];
|
tools?: TPlugin[];
|
||||||
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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue