mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
mcp example, mock i/o for client-to-server communications
This commit is contained in:
parent
799f0e5810
commit
7251308244
9 changed files with 343 additions and 22 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { addTool } = require('@librechat/api');
|
||||||
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
||||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||||
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
||||||
|
|
@ -36,4 +37,12 @@ router.get('/:toolId/auth', verifyToolAuth);
|
||||||
*/
|
*/
|
||||||
router.post('/:toolId/call', toolCallLimiter, callTool);
|
router.post('/:toolId/call', toolCallLimiter, callTool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool to the system
|
||||||
|
* @route POST /agents/tools/add
|
||||||
|
* @param {object} req.body - Request body containing tool data
|
||||||
|
* @returns {object} Created tool object
|
||||||
|
*/
|
||||||
|
router.post('/add', addTool);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-quer
|
||||||
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';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
import { useAddToolMutation } from '~/data-provider/Tools';
|
||||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
@ -17,6 +18,12 @@ interface ServerConfigWithVars {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddToolFormData {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: 'function' | 'code_interpreter' | 'file_search';
|
||||||
|
}
|
||||||
|
|
||||||
export default function MCPPanel() {
|
export default function MCPPanel() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
@ -24,6 +31,7 @@ export default function MCPPanel() {
|
||||||
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [showAddToolForm, setShowAddToolForm] = useState(true);
|
||||||
|
|
||||||
const mcpServerDefinitions = useMemo(() => {
|
const mcpServerDefinitions = useMemo(() => {
|
||||||
if (!startupConfig?.mcpServers) {
|
if (!startupConfig?.mcpServers) {
|
||||||
|
|
@ -87,19 +95,25 @@ export default function MCPPanel() {
|
||||||
|
|
||||||
const handleGoBackToList = () => {
|
const handleGoBackToList = () => {
|
||||||
setSelectedServerNameForEditing(null);
|
setSelectedServerNameForEditing(null);
|
||||||
|
setShowAddToolForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowAddToolForm = () => {
|
||||||
|
setShowAddToolForm(true);
|
||||||
|
setSelectedServerNameForEditing(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
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="p-4 text-center text-sm text-gray-500">
|
||||||
{localize('com_sidepanel_mcp_no_servers_with_vars')}
|
// {localize('com_sidepanel_mcp_no_servers_with_vars')}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (selectedServerNameForEditing) {
|
if (selectedServerNameForEditing) {
|
||||||
// Editing View
|
// Editing View
|
||||||
|
|
@ -138,10 +152,32 @@ export default function MCPPanel() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (showAddToolForm) {
|
||||||
|
// Add Tool Form View
|
||||||
|
return (
|
||||||
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleGoBackToList}
|
||||||
|
className="mb-3 flex items-center px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||||
|
{localize('com_ui_back')}
|
||||||
|
</Button>
|
||||||
|
<h3 className="mb-3 text-lg font-medium">{localize('com_ui_add_tool')}</h3>
|
||||||
|
<AddToolForm onCancel={handleGoBackToList} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Server List View
|
// Server List View
|
||||||
return (
|
return (
|
||||||
<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="mb-3 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium">{localize('com_ui_mcp_servers')}</h3>
|
||||||
|
<Button variant="outline" onClick={handleShowAddToolForm} className="text-sm">
|
||||||
|
{localize('com_ui_add_tool')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mcpServerDefinitions.map((server) => (
|
{mcpServerDefinitions.map((server) => (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -251,3 +287,131 @@ function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariab
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddToolFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddToolForm({ onCancel }: AddToolFormProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
|
const addToolMutation = useAddToolMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_tool_added_success', { '0': data.function?.name || 'Unknown' }),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
onCancel();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error adding tool:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_tool_add_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
} = useForm<AddToolFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
type: 'function',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = (data: AddToolFormData) => {
|
||||||
|
addToolMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-4 mt-2 space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tool-name" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_tool_name')}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: localize('com_ui_tool_name_required') }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
id="tool-name"
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
placeholder={localize('com_ui_enter_tool_name')}
|
||||||
|
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="text-xs text-red-500">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tool-description" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_description')}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: localize('com_ui_description_required') }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
id="tool-description"
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
placeholder={localize('com_ui_enter_description')}
|
||||||
|
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.description && <p className="text-xs text-red-500">{errors.description.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tool-type" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_tool_type')}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<select
|
||||||
|
id="tool-type"
|
||||||
|
{...field}
|
||||||
|
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
|
>
|
||||||
|
<option value="function">{localize('com_ui_function')}</option>
|
||||||
|
<option value="code_interpreter">{localize('com_ui_code_interpreter')}</option>
|
||||||
|
<option value="file_search">{localize('com_ui_file_search')}</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.type && <p className="text-xs text-red-500">{errors.type.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={addToolMutation.isLoading}
|
||||||
|
>
|
||||||
|
{localize('com_ui_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-500 text-white hover:bg-green-600"
|
||||||
|
disabled={addToolMutation.isLoading || !isDirty}
|
||||||
|
>
|
||||||
|
{addToolMutation.isLoading ? localize('com_ui_saving') : localize('com_ui_save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,44 @@ export const useToolCallMutation = <T extends t.ToolId>(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for creating a new tool
|
||||||
|
*/
|
||||||
|
interface CreateToolData {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: 'function' | 'code_interpreter' | 'file_search';
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutation hook for adding a new tool to the system
|
||||||
|
* Note: Requires corresponding backend implementation of dataService.createTool
|
||||||
|
*/
|
||||||
|
export const useAddToolMutation = (
|
||||||
|
// options?:
|
||||||
|
// {
|
||||||
|
// onMutate?: (variables: CreateToolData) => void | Promise<unknown>;
|
||||||
|
// onError?: (error: Error, variables: CreateToolData, context: unknown) => void;
|
||||||
|
// onSuccess?: (data: t.Tool, variables: CreateToolData, context: unknown) => void;
|
||||||
|
// }
|
||||||
|
options?: t.MutationOptions<Record<string, unknown>, CreateToolData>,
|
||||||
|
): UseMutationResult<Record<string, unknown>, 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]);
|
||||||
|
return options?.onSuccess?.(data, variables, context);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export const useConversationTagsQuery = (
|
||||||
/**
|
/**
|
||||||
* Hook for getting all available tools for Assistants
|
* Hook for getting all available tools for Assistants
|
||||||
*/
|
*/
|
||||||
export const useAvailableToolsQuery = <TData = t.TPlugin[]>(
|
export const useAvailableToolsQuery = <TData = t.TPlugin[]>( // <-- this one
|
||||||
endpoint: t.AssistantsEndpoint | EModelEndpoint.agents,
|
endpoint: t.AssistantsEndpoint | EModelEndpoint.agents,
|
||||||
config?: UseQueryOptions<t.TPlugin[], unknown, TData>,
|
config?: UseQueryOptions<t.TPlugin[], unknown, TData>,
|
||||||
): QueryObserverResult<TData> => {
|
): QueryObserverResult<TData> => {
|
||||||
|
|
|
||||||
|
|
@ -152,20 +152,20 @@ export default function useSideNavLinks({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// if (
|
||||||
startupConfig?.mcpServers &&
|
// startupConfig?.mcpServers &&
|
||||||
Object.values(startupConfig.mcpServers).some(
|
// Object.values(startupConfig.mcpServers).some(
|
||||||
(server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
|
// (server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
|
||||||
)
|
// )
|
||||||
) {
|
// ) {
|
||||||
links.push({
|
links.push({
|
||||||
title: 'com_nav_setting_mcp',
|
title: 'com_nav_setting_mcp',
|
||||||
label: '',
|
label: '',
|
||||||
icon: MCPIcon,
|
icon: MCPIcon,
|
||||||
id: 'mcp-settings',
|
id: 'mcp-settings',
|
||||||
Component: MCPPanel,
|
Component: MCPPanel,
|
||||||
});
|
});
|
||||||
}
|
// }
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'com_sidepanel_hide_panel',
|
title: 'com_sidepanel_hide_panel',
|
||||||
|
|
|
||||||
|
|
@ -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/add';
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
export * from './mcp/utils';
|
export * from './mcp/utils';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
||||||
81
packages/api/src/mcp/add.ts
Normal file
81
packages/api/src/mcp/add.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
|
||||||
|
interface CreateToolRequest extends Request {
|
||||||
|
body: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: 'function' | 'code_interpreter' | 'file_search';
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool to the system
|
||||||
|
* @route POST /agents/tools/add
|
||||||
|
* @param {object} req.body - Request body containing tool data
|
||||||
|
* @param {string} req.body.name - Tool name
|
||||||
|
* @param {string} req.body.description - Tool description
|
||||||
|
* @param {string} req.body.type - Tool type (function, code_interpreter, file_search)
|
||||||
|
* @param {object} [req.body.metadata] - Optional metadata
|
||||||
|
* @returns {object} Created tool object
|
||||||
|
*/
|
||||||
|
export const addTool = async (req: CreateToolRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { name, description, type, metadata } = req.body;
|
||||||
|
|
||||||
|
// Log the incoming request for development
|
||||||
|
logger.info(
|
||||||
|
'Add Tool Request:' +
|
||||||
|
JSON.stringify({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
metadata,
|
||||||
|
userId: req.user?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!name || !description || !type) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields: name, description, and type are required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tool type
|
||||||
|
const validTypes = ['function', 'code_interpreter', 'file_search'];
|
||||||
|
if (!validTypes.includes(type)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Invalid tool type. Must be one of: ${validTypes.join(', ')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return a mock successful response
|
||||||
|
// TODO: Implement actual tool creation logic
|
||||||
|
const mockTool = {
|
||||||
|
id: `tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
type,
|
||||||
|
function: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
metadata: metadata || {},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Tool created successfully:' + JSON.stringify(mockTool));
|
||||||
|
|
||||||
|
res.status(201).json(mockTool);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error adding tool:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal server error while adding tool',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -312,6 +312,30 @@ export const getToolCalls = (params: q.GetToolCallParams): Promise<q.ToolCallRes
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createTool = (toolData: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: 'function' | 'code_interpreter' | 'file_search';
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}): Promise<{
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}> => {
|
||||||
|
return request.post(
|
||||||
|
endpoints.agents({
|
||||||
|
path: 'tools/add',
|
||||||
|
}),
|
||||||
|
toolData,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/* Files */
|
/* Files */
|
||||||
|
|
||||||
export const getFiles = (): Promise<f.TFile[]> => {
|
export const getFiles = (): Promise<f.TFile[]> => {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export enum QueryKeys {
|
||||||
banner = 'banner',
|
banner = 'banner',
|
||||||
/* Memories */
|
/* Memories */
|
||||||
memories = 'memories',
|
memories = 'memories',
|
||||||
|
mcpTools = 'mcpTools',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MutationKeys {
|
export enum MutationKeys {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue