From b513565f301bbd7c55e9523e8dd876751bc9c613 Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Thu, 23 Oct 2025 16:28:23 +0800 Subject: [PATCH] feat(mcp): implement mcp server management with CRUD operations --- mcp/Filesystem.json | 13 + mcp/Mock.json | 53 +++ src/hooks/setting-hooks.ts | 238 +++++++++++ src/interfaces/database/mcp-server.ts | 11 +- src/interfaces/database/mcp.ts | 5 +- src/interfaces/request/mcp.ts | 18 +- src/pages/setting/components/McpDialog.tsx | 157 +++++++ src/pages/setting/components/McpForm.tsx | 372 ++++++++++++++++ src/pages/setting/mcp.tsx | 472 ++++++++++++++++++++- src/services/user_service.ts | 45 ++ src/utils/request.ts | 12 +- 11 files changed, 1373 insertions(+), 23 deletions(-) create mode 100644 mcp/Filesystem.json create mode 100644 mcp/Mock.json create mode 100644 src/pages/setting/components/McpDialog.tsx create mode 100644 src/pages/setting/components/McpForm.tsx diff --git a/mcp/Filesystem.json b/mcp/Filesystem.json new file mode 100644 index 0000000..38e5ad1 --- /dev/null +++ b/mcp/Filesystem.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "Filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/allowed/directory" + ], + "env": {} + } + } +} \ No newline at end of file diff --git a/mcp/Mock.json b/mcp/Mock.json new file mode 100644 index 0000000..80136de --- /dev/null +++ b/mcp/Mock.json @@ -0,0 +1,53 @@ +{ + "mcpServers": { + "Time": { + "type": "mcp_server_time", + "url": "http://localhost:8080", + "name": "Time Server", + "authorization_token": "", + "tool_configuration": {} + }, + "Mock": { + "type": "mcp_server_mock", + "url": "http://localhost:8080", + "name": "Mock Server", + "authorization_token": "", + "tool_configuration": {} + }, + "Filesystem": { + "type": "mcp_server_filesystem", + "url": "http://localhost:8080", + "name": "Filesystem Server", + "authorization_token": "", + "tool_configuration": {} + }, + "MockFilesystem": { + "type": "mcp_server_mock_filesystem", + "url": "http://localhost:8080", + "name": "Mock Filesystem Server", + "authorization_token": "", + "tool_configuration": {} + }, + "MockTime": { + "type": "mcp_server_mock_time", + "url": "http://localhost:8080", + "name": "Mock Time Server", + "authorization_token": "", + "tool_configuration": {} + }, + "MockMock": { + "type": "mcp_server_mock_mock", + "url": "http://localhost:8080", + "name": "Mock Mock Server", + "authorization_token": "", + "tool_configuration": {} + }, + "MockMockFilesystem": { + "type": "mcp_server_mock_mock_filesystem", + "url": "http://localhost:8080", + "name": "Mock Mock Filesystem Server", + "authorization_token": "", + "tool_configuration": {} + } + } +} \ No newline at end of file diff --git a/src/hooks/setting-hooks.ts b/src/hooks/setting-hooks.ts index 1cb05ca..3ed67ad 100644 --- a/src/hooks/setting-hooks.ts +++ b/src/hooks/setting-hooks.ts @@ -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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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, + }; +} + diff --git a/src/interfaces/database/mcp-server.ts b/src/interfaces/database/mcp-server.ts index 4a5bb04..9413812 100644 --- a/src/interfaces/database/mcp-server.ts +++ b/src/interfaces/database/mcp-server.ts @@ -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; diff --git a/src/interfaces/database/mcp.ts b/src/interfaces/database/mcp.ts index 143cf8c..0d3a104 100644 --- a/src/interfaces/database/mcp.ts +++ b/src/interfaces/database/mcp.ts @@ -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 & { tools?: IMCPToolObject }; + headers: Record; } -export type IMCPToolObject = Record>; +export type IMCPToolObject = Record>; export type IMCPToolRecord = Record; diff --git a/src/interfaces/request/mcp.ts b/src/interfaces/request/mcp.ts index b9c7471..c9c9905 100644 --- a/src/interfaces/request/mcp.ts +++ b/src/interfaces/request/mcp.ts @@ -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; variables?: Record; timeout?: number; + name?: string } export interface IImportMcpServersRequestBody { - // mcpServers: Record< - // string, - // Pick - // >; + mcpServers: Record< + string, + Pick + >; +} + +export interface ICreateMcpServerRequestBody { + name?: string; + server_type: string; + url: string; + headers?: Record; } diff --git a/src/pages/setting/components/McpDialog.tsx b/src/pages/setting/components/McpDialog.tsx new file mode 100644 index 0000000..b8fad37 --- /dev/null +++ b/src/pages/setting/components/McpDialog.tsx @@ -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; + mode?: 'create' | 'edit'; + loading?: boolean; +} + +const McpDialog: React.FC = ({ + open, + onClose, + onSave, + onTest, + initialData, + mode = 'create', + loading = false, +}) => { + const [submitLoading, setSubmitLoading] = useState(false); + const [submitError, setSubmitError] = useState(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 ( + + + + {getDialogTitle()} + + + + + + + + {/* 显示提交状态消息 */} + {submitError && ( + + setSubmitError(null)}> + {submitError} + + + )} + + {submitSuccess && ( + + + {mode === 'create' ? 'MCP 服务器创建成功!' : 'MCP 服务器更新成功!'} + + + )} + + + + + ); +}; + +export default McpDialog; \ No newline at end of file diff --git a/src/pages/setting/components/McpForm.tsx b/src/pages/setting/components/McpForm.tsx new file mode 100644 index 0000000..e8e9e98 --- /dev/null +++ b/src/pages/setting/components/McpForm.tsx @@ -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; + 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 = ({ + initialData, + onSubmit, + onTest, + loading = false, + disabled = false, +}) => { + const [formData, setFormData] = useState({ + 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>({}); + 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 = {}; + + 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 | { 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 ( + + + + + + + Server Type + + + + + + {/* 测试连接部分 */} + {onTest && ( + + + 测试连接 + + + + {testResult && ( + + {testResult.success ? ( + + + 连接成功!发现 {testResult.tools?.length || 0} 个工具 + + + {testResult.tools && testResult.tools.length > 0 && ( + + }> + + 可用工具 + + + + + + + {testResult.tools.map((tool, index) => ( + + + + {tool.name} + + } + secondary={ + + {tool.description} + + } + /> + + {index < (testResult.tools?.length || 0) - 1 && } + + ))} + + + + + )} + + ) : ( + + {testResult.error || '连接失败'} + + )} + + )} + + )} + + {/* 提交按钮 */} + + + + + ); +}; + +export default McpForm; \ No newline at end of file diff --git a/src/pages/setting/mcp.tsx b/src/pages/setting/mcp.tsx index e848a81..2535587 100644 --- a/src/pages/setting/mcp.tsx +++ b/src/pages/setting/mcp.tsx @@ -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([]); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedServerId, setSelectedServerId] = useState(null); + const [mcpDialogOpen, setMcpDialogOpen] = useState(false); + const [editingServer, setEditingServer] = useState | 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) => { + 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, 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 ( -
-

MCP Setting

-
+ + + + + MCP Servers + + + Customize the list of MCP servers + + + + + + + + + + + }} + sx={{ width: 300 }} + /> + {selectedServers.length > 0 && ( + + + + + )} + + + {loading ? ( + + + + ) : ( + <> + + {paginatedServers.map((server) => ( + + + + + handleSelectServer(server.id)} + size="small" + /> + handleMenuClick(e, server.id)} + > + + + + + {server.name} + + + 类型: {server.server_type} + + + 更新时间: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')} + + + + + ))} + + + {/* 分页 */} + + setCurrentPage(page)} + color="primary" + /> + + 共 {total} 条 + + + + )} + + {/* 操作菜单 */} + + + + Edit + + + + Delete + + + + {/* MCP 对话框 */} + { + setMcpDialogOpen(false); + setEditingServer(null); + }} + onSave={handleMcpSave} + onTest={handleTestMcpServer} + initialData={editingServer || {}} + mode={editingServer ? 'edit' : 'create'} + /> + + {/* 导入对话框 */} + setImportDialogOpen(false)} maxWidth="md" fullWidth> + Import MCP Servers + + + Paste your MCP servers JSON configuration below. The format should match the Mock.json structure. + + setImportJson(e.target.value)} + placeholder={`{ + "mcpServers": { + "Time": { + "type": "sse", + "url": "http://localhost:8080", + "name": "Time Server", + "authorization_token": "", + "tool_configuration": {} + } + } + }`} + /> + + + + + + + ); } -export default MCPSetting; diff --git a/src/services/user_service.ts b/src/services/user_service.ts index 57b37f9..1cf9084 100644 --- a/src/services/user_service.ts +++ b/src/services/user_service.ts @@ -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; \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts index 3c3c419..89d9c5a 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -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) {