feat(mcp): implement mcp server management with CRUD operations
This commit is contained in:
@@ -6,6 +6,8 @@ import userService from "@/services/user_service";
|
||||
import { rsaPsw } from "../utils/encryption";
|
||||
import type { IFactory, IMyLlmModel } from "@/interfaces/database/llm";
|
||||
import type { LLMFactory } from "@/constants/llm";
|
||||
import type { IMcpServer, IMcpServerListResponse } from "@/interfaces/database/mcp";
|
||||
import type { IImportMcpServersRequestBody, ITestMcpRequestBody, ICreateMcpServerRequestBody } from "@/interfaces/request/mcp";
|
||||
|
||||
/**
|
||||
* 个人中心设置
|
||||
@@ -249,3 +251,239 @@ export function useTeamSetting() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 设置
|
||||
*/
|
||||
export function useMcpSetting() {
|
||||
const [mcpServers, setMcpServers] = useState<IMcpServer[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// 获取 MCP 服务器列表
|
||||
const fetchMcpServers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.listMcpServer({
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
if (res.data.code === 0) {
|
||||
const data: IMcpServerListResponse = res.data.data;
|
||||
setMcpServers(data.mcp_servers || []);
|
||||
setTotal(data.total || 0);
|
||||
} else {
|
||||
throw new Error(res.data.message || '获取 MCP 服务器列表失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '获取 MCP 服务器列表失败';
|
||||
setError(errorMessage);
|
||||
logger.error('获取 MCP 服务器列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 导入 MCP 服务器
|
||||
const importMcpServers = useCallback(async (data: IImportMcpServersRequestBody) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.importMcpServer(data);
|
||||
if (res.data.code === 0) {
|
||||
await fetchMcpServers(); // 重新获取列表
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error(res.data.message || '导入 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '导入 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('导入 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
// 导出 MCP 服务器
|
||||
const exportMcpServers = useCallback(async (mcpIds: string[]) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.exportMcpServer(mcpIds);
|
||||
if (res.data.code === 0) {
|
||||
// 处理导出数据,创建下载
|
||||
const exportData = res.data.data;
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'mcp-servers.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error(res.data.message || '导出 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '导出 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('导出 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 测试 MCP 服务器
|
||||
const testMcpServer = useCallback(async (data: ITestMcpRequestBody) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.testMcpServer(data);
|
||||
if (res.data.code === 0) {
|
||||
return { success: true, data: res.data.data };
|
||||
} else {
|
||||
throw new Error(res.data.message || '测试 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '测试 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取 MCP 服务器详情
|
||||
const getMcpServerDetail = useCallback(async (mcpId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.mcpDetail(mcpId);
|
||||
if (res.data.code === 0) {
|
||||
const detail: IMcpServer = res.data.data;
|
||||
return { success: true, data: detail };
|
||||
} else {
|
||||
throw new Error(res.data.message || '获取 MCP 服务器详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '获取 MCP 服务器详情失败';
|
||||
setError(errorMessage);
|
||||
logger.error('获取 MCP 服务器详情失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 创建 MCP 服务器
|
||||
const createMcpServer = useCallback(async (data: ICreateMcpServerRequestBody) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.createMcpServer(data);
|
||||
if (res.data.code === 0) {
|
||||
await fetchMcpServers(); // 重新获取列表
|
||||
return { success: true, data: res.data.data };
|
||||
} else {
|
||||
throw new Error(res.data.message || '创建 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '创建 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('创建 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
// 更新 MCP 服务器
|
||||
const updateMcpServer = useCallback(async (data: ICreateMcpServerRequestBody & { mcp_id: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.updateMcpServer(data);
|
||||
if (res.data.code === 0) {
|
||||
await fetchMcpServers(); // 重新获取列表
|
||||
return { success: true, data: res.data.data };
|
||||
} else {
|
||||
throw new Error(res.data.message || '更新 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '更新 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('更新 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
// 删除 MCP 服务器
|
||||
const deleteMcpServer = useCallback(async (mcpId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.removeMcpServer([mcpId]);
|
||||
if (res.data.code === 0) {
|
||||
await fetchMcpServers(); // 重新获取列表
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error(res.data.message || '删除 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '删除 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('删除 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
// 批量删除 MCP 服务器
|
||||
const deleteMcpServers = useCallback(async (mcpIds: string[]) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await userService.removeMcpServer(mcpIds);
|
||||
if (res.data.code === 0) {
|
||||
await fetchMcpServers(); // 重新获取列表
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error(res.data.message || '批量删除 MCP 服务器失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || '批量删除 MCP 服务器失败';
|
||||
setError(errorMessage);
|
||||
logger.error('批量删除 MCP 服务器失败:', error);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
mcpServers,
|
||||
loading,
|
||||
error,
|
||||
total,
|
||||
|
||||
// 方法
|
||||
fetchMcpServers,
|
||||
importMcpServers,
|
||||
exportMcpServers,
|
||||
testMcpServer,
|
||||
getMcpServerDetail,
|
||||
createMcpServer,
|
||||
updateMcpServer,
|
||||
deleteMcpServer,
|
||||
deleteMcpServers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// export enum McpServerType {
|
||||
// Sse = 'sse',
|
||||
// StreamableHttp = 'streamable-http',
|
||||
// }
|
||||
const MCP_SERVER_TYPE = {
|
||||
Sse: 'sse',
|
||||
StreamableHttp: 'streamable-http',
|
||||
}
|
||||
export type McpServerType = (typeof MCP_SERVER_TYPE)[keyof typeof MCP_SERVER_TYPE];
|
||||
|
||||
export interface IMcpServerVariable {
|
||||
key: string;
|
||||
@@ -12,7 +13,7 @@ export interface IMcpServerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
// server_type: McpServerType;
|
||||
server_type: McpServerType;
|
||||
description?: string;
|
||||
variables?: IMcpServerVariable[];
|
||||
headers: Map<string, string>;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
export interface IMcpServer {
|
||||
create_date: string;
|
||||
description: null;
|
||||
description?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
server_type: string;
|
||||
update_date: string;
|
||||
url: string;
|
||||
variables: Record<string, any> & { tools?: IMCPToolObject };
|
||||
headers: Record<string, any>;
|
||||
}
|
||||
|
||||
export type IMCPToolObject = Record<string, Omit<IMCPTool, 'name'>>;
|
||||
export type IMCPToolObject = Record<string, Partial<IMCPTool>>;
|
||||
|
||||
export type IMCPToolRecord = Record<string, IMCPTool>;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// import { IExportedMcpServer } from '@/interfaces/database/mcp';
|
||||
import { type IExportedMcpServer } from '@/interfaces/database/mcp';
|
||||
|
||||
export interface ITestMcpRequestBody {
|
||||
server_type: string;
|
||||
@@ -6,11 +6,19 @@ export interface ITestMcpRequestBody {
|
||||
headers?: Record<string, any>;
|
||||
variables?: Record<string, any>;
|
||||
timeout?: number;
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface IImportMcpServersRequestBody {
|
||||
// mcpServers: Record<
|
||||
// string,
|
||||
// Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
|
||||
// >;
|
||||
mcpServers: Record<
|
||||
string,
|
||||
Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ICreateMcpServerRequestBody {
|
||||
name?: string;
|
||||
server_type: string;
|
||||
url: string;
|
||||
headers?: Record<string, any>;
|
||||
}
|
||||
|
||||
157
src/pages/setting/components/McpDialog.tsx
Normal file
157
src/pages/setting/components/McpDialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
IconButton,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import McpForm from './McpForm';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const StyledDialogTitle = styled(DialogTitle)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(2, 3),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const StyledDialogContent = styled(DialogContent)(({ theme }) => ({
|
||||
padding: 0,
|
||||
minHeight: '400px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
}));
|
||||
|
||||
interface McpDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (data: ICreateMcpServerRequestBody) => Promise<{ success: boolean; error?: string }>;
|
||||
onTest?: (data: ITestMcpRequestBody) => Promise<{ success: boolean; data?: any; error?: string }>;
|
||||
initialData?: Partial<IMcpServer>;
|
||||
mode?: 'create' | 'edit';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const McpDialog: React.FC<McpDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
onTest,
|
||||
initialData,
|
||||
mode = 'create',
|
||||
loading = false,
|
||||
}) => {
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
logger.debug('McpDialog 组件加载', initialData);
|
||||
|
||||
const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => {
|
||||
setSubmitLoading(true);
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
try {
|
||||
const result = await onSave(data);
|
||||
|
||||
if (result.success) {
|
||||
setSubmitSuccess(true);
|
||||
// 延迟关闭对话框,让用户看到成功消息
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
setSubmitSuccess(false);
|
||||
}, 1500);
|
||||
} else {
|
||||
setSubmitError(result.error || '保存失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSubmitError(error.message || '保存时发生错误');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
|
||||
return { success: submitSuccess, error: submitError || undefined };
|
||||
}, [onSave, onClose, submitSuccess, submitError]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (submitLoading) return; // 防止在提交过程中关闭
|
||||
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getDialogTitle = () => {
|
||||
switch (mode) {
|
||||
case 'edit':
|
||||
return '编辑 MCP 服务器';
|
||||
case 'create':
|
||||
default:
|
||||
return '添加 MCP 服务器';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
disableEscapeKeyDown={submitLoading}
|
||||
>
|
||||
<StyledDialogTitle>
|
||||
<Typography variant="h6" component="div">
|
||||
{getDialogTitle()}
|
||||
</Typography>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
disabled={submitLoading}
|
||||
size="small"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</StyledDialogTitle>
|
||||
|
||||
<StyledDialogContent>
|
||||
{/* 显示提交状态消息 */}
|
||||
{submitError && (
|
||||
<Box p={2}>
|
||||
<Alert severity="error" onClose={() => setSubmitError(null)}>
|
||||
{submitError}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{submitSuccess && (
|
||||
<Box p={2}>
|
||||
<Alert severity="success">
|
||||
{mode === 'create' ? 'MCP 服务器创建成功!' : 'MCP 服务器更新成功!'}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<McpForm
|
||||
initialData={initialData}
|
||||
onSubmit={handleSubmit}
|
||||
onTest={onTest}
|
||||
loading={submitLoading || loading}
|
||||
disabled={submitLoading || loading}
|
||||
/>
|
||||
</StyledDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpDialog;
|
||||
372
src/pages/setting/components/McpForm.tsx
Normal file
372
src/pages/setting/components/McpForm.tsx
Normal file
@@ -0,0 +1,372 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Button,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Chip,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import { Mode } from '@mui/icons-material';
|
||||
|
||||
const FormContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const TestSection = styled(Box)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
const ToolsContainer = styled(Box)(({ theme }) => ({
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
marginTop: theme.spacing(1),
|
||||
}));
|
||||
|
||||
interface McpFormData {
|
||||
name?: string;
|
||||
url: string;
|
||||
server_type: string;
|
||||
authorization_token?: string;
|
||||
}
|
||||
|
||||
interface McpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface McpFormProps {
|
||||
initialData?: Partial<IMcpServer>;
|
||||
onSubmit: (data: ICreateMcpServerRequestBody) => Promise<{ success: boolean; error?: string }>;
|
||||
onTest?: (data: ITestMcpRequestBody) => Promise<{ success: boolean; data?: any; error?: string }>;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const McpForm: React.FC<McpFormProps> = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onTest,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<McpFormData>({
|
||||
name: initialData?.name || '',
|
||||
url: initialData?.url || '',
|
||||
server_type: initialData?.server_type || 'sse',
|
||||
authorization_token: initialData?.variables?.authorization_token || '',
|
||||
});
|
||||
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
|
||||
|
||||
|
||||
const [testResult, setTestResult] = useState<{
|
||||
success: boolean;
|
||||
tools?: McpTool[];
|
||||
error?: string;
|
||||
} | null>(null);
|
||||
|
||||
const [formErrors, setFormErrors] = useState<Partial<McpFormData>>({});
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
// 只在首次初始化时设置默认值
|
||||
setFormData({
|
||||
name: '',
|
||||
url: '',
|
||||
server_type: 'sse',
|
||||
authorization_token: '',
|
||||
});
|
||||
setIsInitialized(true);
|
||||
}
|
||||
if (initialData) {
|
||||
setFormData({
|
||||
name: initialData.name || '',
|
||||
url: initialData.url || '',
|
||||
server_type: initialData.server_type || 'sse',
|
||||
authorization_token: initialData.variables?.authorization_token || '',
|
||||
});
|
||||
if (initialData.variables?.tools) {
|
||||
const tools = Object.entries(initialData.variables.tools).map(([name, tool]) => {
|
||||
const t: McpTool = {
|
||||
name: name,
|
||||
description: tool.description || '',
|
||||
}
|
||||
return t
|
||||
});
|
||||
setTestResult({
|
||||
success: true,
|
||||
tools,
|
||||
});
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [isInitialized]);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const errors: Partial<McpFormData> = {};
|
||||
|
||||
if (!formData.name?.trim()) {
|
||||
errors.name = '名称不能为空';
|
||||
}
|
||||
|
||||
if (!formData.url.trim()) {
|
||||
errors.url = 'URL不能为空';
|
||||
} else if (!/^https?:\/\/.+/.test(formData.url)) {
|
||||
errors.url = 'URL格式不正确';
|
||||
}
|
||||
|
||||
if (!formData.server_type) {
|
||||
errors.server_type = '请选择服务器类型';
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof McpFormData) => (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | { target: { value: string } }
|
||||
) => {
|
||||
const value = event.target.value;
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// 清除对应字段的错误
|
||||
if (formErrors[field]) {
|
||||
setFormErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!onTest) return;
|
||||
|
||||
if (!formData.url.trim()) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: '请先填写 URL',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTestLoading(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const testData: ITestMcpRequestBody = {
|
||||
server_type: formData.server_type,
|
||||
url: formData.url,
|
||||
headers: formData.authorization_token ? { authorization_token: formData.authorization_token } : undefined,
|
||||
name: formData.name?.trim(),
|
||||
};
|
||||
|
||||
const result = await onTest(testData);
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 解析工具数据
|
||||
const tools: McpTool[] = [];
|
||||
if (result.data && Array.isArray(result.data)) {
|
||||
result.data.forEach((tool: any) => {
|
||||
if (tool.name && tool.description) {
|
||||
tools.push({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
tools,
|
||||
});
|
||||
} else {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: result.error || '测试失败',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: error.message || '测试连接时发生错误',
|
||||
});
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
const submitData: ICreateMcpServerRequestBody = {
|
||||
name: formData.name?.trim(),
|
||||
server_type: formData.server_type,
|
||||
url: formData.url.trim(),
|
||||
headers: formData.authorization_token ? { authorization_token: formData.authorization_token } : undefined,
|
||||
};
|
||||
|
||||
await onSubmit(submitData);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormContainer>
|
||||
<TextField
|
||||
label="名称"
|
||||
value={formData.name || ''}
|
||||
onChange={handleInputChange('name')}
|
||||
error={!!formErrors.name}
|
||||
helperText={formErrors.name}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="URL"
|
||||
value={formData.url}
|
||||
onChange={handleInputChange('url')}
|
||||
error={!!formErrors.url}
|
||||
helperText={formErrors.url}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
required
|
||||
placeholder="https://mcp.****.com/sse?key=..."
|
||||
/>
|
||||
|
||||
<FormControl fullWidth required error={!!formErrors.server_type}>
|
||||
<InputLabel>Server Type</InputLabel>
|
||||
<Select
|
||||
value={formData.server_type}
|
||||
onChange={handleInputChange('server_type')}
|
||||
disabled={disabled}
|
||||
label="Server Type"
|
||||
>
|
||||
<MenuItem value="sse">SSE</MenuItem>
|
||||
<MenuItem value="streamable-http">Streamable HTTP</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Authorization Token"
|
||||
value={formData.authorization_token}
|
||||
onChange={handleInputChange('authorization_token')}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
placeholder="e.g. eyJhbGciOiJIUzI1Ni..."
|
||||
helperText="可选:用于身份验证的令牌"
|
||||
/>
|
||||
|
||||
{/* 测试连接部分 */}
|
||||
{onTest && (
|
||||
<TestSection>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h6">测试连接</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleTest}
|
||||
disabled={testLoading || disabled}
|
||||
startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
|
||||
>
|
||||
{testLoading ? '测试中...' : '测试连接'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{testResult && (
|
||||
<Box>
|
||||
{testResult.success ? (
|
||||
<Box>
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
连接成功!发现 {testResult.tools?.length || 0} 个工具
|
||||
</Alert>
|
||||
|
||||
{testResult.tools && testResult.tools.length > 0 && (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="subtitle1">可用工具</Typography>
|
||||
<Chip
|
||||
label={`${testResult.tools.length} tools available`}
|
||||
size="small"
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<ToolsContainer>
|
||||
<List dense>
|
||||
{testResult.tools.map((tool, index) => (
|
||||
<React.Fragment key={tool.name}>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="subtitle2" fontWeight="bold">
|
||||
{tool.name}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{tool.description}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < (testResult.tools?.length || 0) - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</ToolsContainer>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="error">
|
||||
{testResult.error || '连接失败'}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</TestSection>
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Box display="flex" justifyContent="flex-end" mt={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || disabled}
|
||||
startIcon={loading ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
{loading ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</Box>
|
||||
</FormContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpForm;
|
||||
@@ -1,8 +1,470 @@
|
||||
function MCPSetting() {
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
Pagination,
|
||||
CircularProgress,
|
||||
Snackbar,
|
||||
Alert,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
Delete as DeleteIcon,
|
||||
FileDownload as ExportIcon,
|
||||
FileUpload as ImportIcon,
|
||||
Search as SearchIcon,
|
||||
Edit as EditIcon
|
||||
} from '@mui/icons-material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useMcpSetting } from '@/hooks/setting-hooks';
|
||||
import McpDialog from '@/pages/setting/components/McpDialog';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import { useMessage } from '@/hooks/useSnackbar';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const PageContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh'
|
||||
}));
|
||||
|
||||
const PageHeader = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shadows[1],
|
||||
}));
|
||||
|
||||
const SearchContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shadows[1],
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
|
||||
export default function McpSettingPage() {
|
||||
const {
|
||||
mcpServers,
|
||||
loading,
|
||||
total,
|
||||
fetchMcpServers,
|
||||
importMcpServers,
|
||||
exportMcpServers,
|
||||
testMcpServer,
|
||||
getMcpServerDetail,
|
||||
createMcpServer,
|
||||
updateMcpServer,
|
||||
deleteMcpServer,
|
||||
deleteMcpServers,
|
||||
} = useMcpSetting();
|
||||
|
||||
// 本地状态
|
||||
const [searchString, setSearchString] = useState('');
|
||||
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedServerId, setSelectedServerId] = useState<string | null>(null);
|
||||
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
||||
const [editingServer, setEditingServer] = useState<Partial<IMcpServer> | undefined | null>(null);
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(6);
|
||||
const [importJson, setImportJson] = useState('');
|
||||
|
||||
const showMessage = useMessage()
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
fetchMcpServers();
|
||||
}, [fetchMcpServers]);
|
||||
|
||||
// 过滤和分页逻辑
|
||||
const filteredServers = mcpServers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchString.toLowerCase()) ||
|
||||
server.url.toLowerCase().includes(searchString.toLowerCase())
|
||||
);
|
||||
|
||||
const totalPages = Math.ceil(filteredServers.length / pageSize);
|
||||
const paginatedServers = filteredServers.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize
|
||||
);
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchString(event.target.value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleSelectServer = (serverId: string) => {
|
||||
setSelectedServers(prev =>
|
||||
prev.includes(serverId)
|
||||
? prev.filter(id => id !== serverId)
|
||||
: [...prev, serverId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedServers.length === paginatedServers.length) {
|
||||
setSelectedServers([]);
|
||||
} else {
|
||||
setSelectedServers(paginatedServers.map(server => server.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, serverId: string) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedServerId(serverId);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedServerId(null);
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
// const server = mcpServers.find(s => s.id === selectedServerId);
|
||||
const result = await getMcpServerDetail(selectedServerId || '');
|
||||
if (result.success) {
|
||||
const server = result.data;
|
||||
if (server != null && server != undefined) {
|
||||
const headers = server?.headers || {};
|
||||
const authorization_token = headers['authorization_token'] || '';
|
||||
if (authorization_token && authorization_token.length > 0) {
|
||||
server!.variables = {
|
||||
...server?.variables,
|
||||
authorization_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
setEditingServer(server);
|
||||
setMcpDialogOpen(true);
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingServer(null);
|
||||
setMcpDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedServerId) {
|
||||
const result = await deleteMcpServer(selectedServerId);
|
||||
if (result.success) {
|
||||
showMessage.success('删除成功');
|
||||
} else {
|
||||
showMessage.error(result.error || '删除失败');
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
const result = await deleteMcpServers(selectedServers);
|
||||
if (result.success) {
|
||||
showMessage.success('批量删除成功');
|
||||
setSelectedServers([]);
|
||||
} else {
|
||||
showMessage.error(result.error || '批量删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const result = await exportMcpServers(selectedServers);
|
||||
if (result.success) {
|
||||
showMessage.success('导出成功');
|
||||
} else {
|
||||
showMessage.error(result.error || '导出失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMcpSave = async (data: ICreateMcpServerRequestBody) => {
|
||||
try {
|
||||
if (editingServer) {
|
||||
if (!editingServer.id) {
|
||||
showMessage.error('server id 不能为空');
|
||||
return { success: false, error: 'server id 不能为空' };
|
||||
}
|
||||
const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' });
|
||||
if (result.success) {
|
||||
showMessage.success('MCP 服务器更新成功');
|
||||
setMcpDialogOpen(false);
|
||||
setEditingServer(null);
|
||||
return result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = await createMcpServer(data);
|
||||
if (result.success) {
|
||||
showMessage.success('MCP 服务器创建成功');
|
||||
setMcpDialogOpen(false);
|
||||
setEditingServer(null);
|
||||
return result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || '操作失败';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestMcpServer = async (data: ITestMcpRequestBody) => {
|
||||
try {
|
||||
const result = await testMcpServer(data);
|
||||
if (result.success) {
|
||||
showMessage.success('测试成功');
|
||||
return result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || '操作失败';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
const importData = JSON.parse(importJson);
|
||||
if (importData.mcpServers) {
|
||||
const requestData: IImportMcpServersRequestBody = {
|
||||
mcpServers: Object.entries(importData.mcpServers).reduce((acc, [key, value]: [string, any]) => {
|
||||
acc[key] = {
|
||||
type: value.type,
|
||||
url: value.url,
|
||||
authorization_token: value.authorization_token || ''
|
||||
};
|
||||
return acc;
|
||||
}, {} as any)
|
||||
};
|
||||
|
||||
const result = await importMcpServers(requestData);
|
||||
if (result.success) {
|
||||
setImportDialogOpen(false);
|
||||
setImportJson('');
|
||||
}
|
||||
} else {
|
||||
showMessage.error('JSON 格式错误');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage.error('JSON 格式错误');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>MCP Setting</h1>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
MCP Servers
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Customize the list of MCP servers
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" gap={2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ImportIcon />}
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add MCP
|
||||
</Button>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
<SearchContainer>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search MCP servers..."
|
||||
value={searchString}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon sx={{ mr: 1, color: 'text.secondary' }} />
|
||||
}}
|
||||
sx={{ width: 300 }}
|
||||
/>
|
||||
{selectedServers.length > 0 && (
|
||||
<Box display="flex" gap={1}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={handleBulkDelete}
|
||||
color="error"
|
||||
>
|
||||
Delete ({selectedServers.length})
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<ExportIcon />}
|
||||
onClick={handleExport}
|
||||
>
|
||||
Export ({selectedServers.length})
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</SearchContainer>
|
||||
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Grid container spacing={3}>
|
||||
{paginatedServers.map((server) => (
|
||||
<Grid size={{ xs: 6, sm: 4, md: 3 }} key={server.id}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
border: selectedServers.includes(server.id) ? '2px solid #1976d2' : '1px solid #e0e0e0'
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ flexGrow: 1 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Checkbox
|
||||
checked={selectedServers.includes(server.id)}
|
||||
onChange={() => handleSelectServer(server.id)}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuClick(e, server.id)}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="h6" component="h3" gutterBottom>
|
||||
{server.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
类型: {server.server_type}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
更新时间: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* 分页 */}
|
||||
<Box display="flex" justifyContent="center" alignItems="center" mt={3}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
onChange={(_, page) => setCurrentPage(page)}
|
||||
color="primary"
|
||||
/>
|
||||
<Typography variant="body2" sx={{ ml: 2 }}>
|
||||
共 {total} 条
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 操作菜单 */}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
<EditIcon sx={{ mr: 1 }} fontSize="small" />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
||||
<DeleteIcon sx={{ mr: 1 }} fontSize="small" />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* MCP 对话框 */}
|
||||
<McpDialog
|
||||
open={mcpDialogOpen}
|
||||
onClose={() => {
|
||||
setMcpDialogOpen(false);
|
||||
setEditingServer(null);
|
||||
}}
|
||||
onSave={handleMcpSave}
|
||||
onTest={handleTestMcpServer}
|
||||
initialData={editingServer || {}}
|
||||
mode={editingServer ? 'edit' : 'create'}
|
||||
/>
|
||||
|
||||
{/* 导入对话框 */}
|
||||
<Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Import MCP Servers</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
Paste your MCP servers JSON configuration below. The format should match the Mock.json structure.
|
||||
</Alert>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={10}
|
||||
label="JSON Configuration"
|
||||
value={importJson}
|
||||
onChange={(e) => setImportJson(e.target.value)}
|
||||
placeholder={`{
|
||||
"mcpServers": {
|
||||
"Time": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8080",
|
||||
"name": "Time Server",
|
||||
"authorization_token": "",
|
||||
"tool_configuration": {}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setImportDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleImport} variant="contained">
|
||||
Import
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
export default MCPSetting;
|
||||
|
||||
@@ -4,6 +4,9 @@ import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||
import type { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
|
||||
import type { LlmModelType } from '@/constants/knowledge';
|
||||
import type { IAddLlmRequestBody, IDeleteLlmRequestBody, ISetApiKeyRequestBody } from '@/interfaces/request/llm';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { ICreateMcpServerRequestBody, IImportMcpServersRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import type { IPaginationBody, IPaginationRequestBody } from '@/interfaces/request/base';
|
||||
|
||||
// 用户相关API服务
|
||||
const userService = {
|
||||
@@ -121,6 +124,48 @@ const userService = {
|
||||
system_status: () => {
|
||||
return request.get(api.getSystemStatus);
|
||||
},
|
||||
|
||||
/** mcp server 相关接口 */
|
||||
|
||||
// list mcp server
|
||||
listMcpServer: (params: IPaginationBody & { keyword?: string }) => {
|
||||
return request.post(api.listMcpServer, {}, { params });
|
||||
},
|
||||
|
||||
// create mcp server
|
||||
createMcpServer: (data: ICreateMcpServerRequestBody) => {
|
||||
return request.post(api.createMcpServer, data);
|
||||
},
|
||||
|
||||
// update mcp server
|
||||
updateMcpServer: (data: ICreateMcpServerRequestBody & { mcp_id: string }) => {
|
||||
return request.post(api.updateMcpServer, data);
|
||||
},
|
||||
|
||||
// remove mcp server
|
||||
removeMcpServer: (mcp_ids: string[]) => {
|
||||
return request.post(api.deleteMcpServer, { mcp_ids });
|
||||
},
|
||||
|
||||
// import mcp server
|
||||
importMcpServer: (data: IImportMcpServersRequestBody) => {
|
||||
return request.post(api.importMcpServer, data);
|
||||
},
|
||||
|
||||
// export mcp server
|
||||
exportMcpServer: (mcp_ids: string[]) => {
|
||||
return request.post(api.exportMcpServer, { mcp_ids });
|
||||
},
|
||||
|
||||
// mcp detail
|
||||
mcpDetail: (mcpId: string) => {
|
||||
return request.get(api.getMcpServer, { params: { mcp_id: mcpId } });
|
||||
},
|
||||
|
||||
// test mcp server
|
||||
testMcpServer: (data: ITestMcpRequestBody) => {
|
||||
return request.post(api.testMcpServer, data);
|
||||
},
|
||||
};
|
||||
|
||||
export default userService;
|
||||
@@ -92,12 +92,12 @@ class CustomError extends Error {
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 转换数据格式 - 跳过FormData对象
|
||||
if (config.data && !(config.data instanceof FormData)) {
|
||||
config.data = convertTheKeysOfTheObjectToSnake(config.data);
|
||||
}
|
||||
if (config.params) {
|
||||
config.params = convertTheKeysOfTheObjectToSnake(config.params);
|
||||
}
|
||||
// if (config.data && !(config.data instanceof FormData)) {
|
||||
// config.data = convertTheKeysOfTheObjectToSnake(config.data);
|
||||
// }
|
||||
// if (config.params) {
|
||||
// config.params = convertTheKeysOfTheObjectToSnake(config.params);
|
||||
// }
|
||||
|
||||
// 对于FormData,删除默认的Content-Type让浏览器自动设置
|
||||
if (config.data instanceof FormData) {
|
||||
|
||||
Reference in New Issue
Block a user