feat(mcp): implement mcp server management with CRUD operations
This commit is contained in:
13
mcp/Filesystem.json
Normal file
13
mcp/Filesystem.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"Filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-filesystem",
|
||||||
|
"/path/to/allowed/directory"
|
||||||
|
],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
mcp/Mock.json
Normal file
53
mcp/Mock.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import userService from "@/services/user_service";
|
|||||||
import { rsaPsw } from "../utils/encryption";
|
import { rsaPsw } from "../utils/encryption";
|
||||||
import type { IFactory, IMyLlmModel } from "@/interfaces/database/llm";
|
import type { IFactory, IMyLlmModel } from "@/interfaces/database/llm";
|
||||||
import type { LLMFactory } from "@/constants/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 {
|
const MCP_SERVER_TYPE = {
|
||||||
// Sse = 'sse',
|
Sse: 'sse',
|
||||||
// StreamableHttp = 'streamable-http',
|
StreamableHttp: 'streamable-http',
|
||||||
// }
|
}
|
||||||
|
export type McpServerType = (typeof MCP_SERVER_TYPE)[keyof typeof MCP_SERVER_TYPE];
|
||||||
|
|
||||||
export interface IMcpServerVariable {
|
export interface IMcpServerVariable {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -12,7 +13,7 @@ export interface IMcpServerInfo {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
// server_type: McpServerType;
|
server_type: McpServerType;
|
||||||
description?: string;
|
description?: string;
|
||||||
variables?: IMcpServerVariable[];
|
variables?: IMcpServerVariable[];
|
||||||
headers: Map<string, string>;
|
headers: Map<string, string>;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
export interface IMcpServer {
|
export interface IMcpServer {
|
||||||
create_date: string;
|
create_date: string;
|
||||||
description: null;
|
description?: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
server_type: string;
|
server_type: string;
|
||||||
update_date: string;
|
update_date: string;
|
||||||
url: string;
|
url: string;
|
||||||
variables: Record<string, any> & { tools?: IMCPToolObject };
|
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>;
|
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 {
|
export interface ITestMcpRequestBody {
|
||||||
server_type: string;
|
server_type: string;
|
||||||
@@ -6,11 +6,19 @@ export interface ITestMcpRequestBody {
|
|||||||
headers?: Record<string, any>;
|
headers?: Record<string, any>;
|
||||||
variables?: Record<string, any>;
|
variables?: Record<string, any>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IImportMcpServersRequestBody {
|
export interface IImportMcpServersRequestBody {
|
||||||
// mcpServers: Record<
|
mcpServers: Record<
|
||||||
// string,
|
string,
|
||||||
// Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
|
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 (
|
return (
|
||||||
<div>
|
<PageContainer>
|
||||||
<h1>MCP Setting</h1>
|
<PageHeader>
|
||||||
</div>
|
<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 { IUserInfo, ITenant } from '@/interfaces/database/user-setting';
|
||||||
import type { LlmModelType } from '@/constants/knowledge';
|
import type { LlmModelType } from '@/constants/knowledge';
|
||||||
import type { IAddLlmRequestBody, IDeleteLlmRequestBody, ISetApiKeyRequestBody } from '@/interfaces/request/llm';
|
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服务
|
// 用户相关API服务
|
||||||
const userService = {
|
const userService = {
|
||||||
@@ -121,6 +124,48 @@ const userService = {
|
|||||||
system_status: () => {
|
system_status: () => {
|
||||||
return request.get(api.getSystemStatus);
|
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;
|
export default userService;
|
||||||
@@ -92,12 +92,12 @@ class CustomError extends Error {
|
|||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
// 转换数据格式 - 跳过FormData对象
|
// 转换数据格式 - 跳过FormData对象
|
||||||
if (config.data && !(config.data instanceof FormData)) {
|
// if (config.data && !(config.data instanceof FormData)) {
|
||||||
config.data = convertTheKeysOfTheObjectToSnake(config.data);
|
// config.data = convertTheKeysOfTheObjectToSnake(config.data);
|
||||||
}
|
// }
|
||||||
if (config.params) {
|
// if (config.params) {
|
||||||
config.params = convertTheKeysOfTheObjectToSnake(config.params);
|
// config.params = convertTheKeysOfTheObjectToSnake(config.params);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 对于FormData,删除默认的Content-Type让浏览器自动设置
|
// 对于FormData,删除默认的Content-Type让浏览器自动设置
|
||||||
if (config.data instanceof FormData) {
|
if (config.data instanceof FormData) {
|
||||||
|
|||||||
Reference in New Issue
Block a user