import React, { useState, useEffect, useCallback } from 'react'; import { adminAPI } from '@/api'; const styles: Record = { container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }, title: { fontSize: 24, fontWeight: 700, color: '#1A1F36' }, addBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 }, statsRow: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 24 }, statCard: { backgroundColor: '#fff', padding: 20, borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }, statLabel: { fontSize: 13, color: '#6b7280', marginBottom: 8 }, statValue: { fontSize: 28, fontWeight: 700, color: '#1A1F36' }, toolbar: { display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' as const }, searchInput: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, width: 250 }, filterSelect: { padding: '8px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, backgroundColor: '#fff' }, table: { width: '100%', borderCollapse: 'collapse' as const, backgroundColor: '#fff', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }, th: { padding: '12px 16px', textAlign: 'left' as const, fontSize: 13, fontWeight: 600, color: '#6b7280', backgroundColor: '#f9fafb', borderBottom: '1px solid #e5e7eb' }, td: { padding: '12px 16px', fontSize: 14, color: '#374151', borderBottom: '1px solid #f3f4f6' }, badge: { display: 'inline-block', padding: '2px 10px', borderRadius: 20, fontSize: 12, fontWeight: 600 }, actionBtn: { padding: '4px 12px', border: '1px solid #e5e7eb', borderRadius: 6, fontSize: 13, cursor: 'pointer', marginRight: 6, backgroundColor: '#fff' }, pagination: { display: 'flex', justifyContent: 'center', gap: 8, marginTop: 20 }, pageBtn: { padding: '8px 16px', border: '1px solid #e5e7eb', borderRadius: 6, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 }, modalOverlay: { position: 'fixed' as const, top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000 }, modal: { backgroundColor: '#fff', borderRadius: 16, padding: 32, width: 560, maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)' }, modalTitle: { fontSize: 18, fontWeight: 700, marginBottom: 24, color: '#1A1F36' }, formGroup: { marginBottom: 16 }, label: { display: 'block', fontSize: 13, fontWeight: 600, color: '#374151', marginBottom: 6 }, input: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, boxSizing: 'border-box' as const }, textarea: { width: '100%', padding: '10px 12px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 14, minHeight: 80, resize: 'vertical' as const, boxSizing: 'border-box' as const }, modalActions: { display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 24 }, cancelBtn: { padding: '10px 24px', border: '1px solid #e5e7eb', borderRadius: 8, cursor: 'pointer', backgroundColor: '#fff', fontSize: 14 }, submitBtn: { padding: '10px 24px', backgroundColor: '#4f46e5', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 600 }, toast: { position: 'fixed' as const, top: 20, right: 20, padding: '12px 24px', borderRadius: 8, fontSize: 14, fontWeight: 600, zIndex: 2000, color: '#fff' }, detailsGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 16 }, detailsItem: { fontSize: 13 }, detailsLabel: { color: '#6b7280', marginBottom: 4 }, }; const TRANSPORT_TYPES = [ { value: '', label: '全部类型' }, { value: 'stdio', label: 'STDIO' }, { value: 'sse', label: 'SSE' }, { value: 'streamable_http', label: 'HTTP 流式' }, ]; const STATUS_TYPES = [ { value: '', label: '全部状态' }, { value: 'connected', label: '已连接' }, { value: 'disconnected', label: '未连接' }, { value: 'error', label: '错误' }, ]; interface MCPServer { id: string; name: string; description: string; transport_type: string; transport_type_label: string; command: string; args: Record; env_vars: Record; url: string; headers: Record; is_enabled: boolean; status: string; last_connected_at: string; tools_count: number; resources_count: number; created_at: string; updated_at: string; } export default function AdminMcp() { const [servers, setServers] = useState([]); const [stats, setStats] = useState({}); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [search, setSearch] = useState(''); const [filterTransport, setFilterTransport] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [showModal, setShowModal] = useState(false); const [editingServer, setEditingServer] = useState(null); const [toast, setToast] = useState<{ message: string; type: string } | null>(null); const pageSize = 20; const showToast = (message: string, type: string = 'success') => { setToast({ message, type }); setTimeout(() => setToast(null), 3000); }; const fetchStats = useCallback(async () => { try { const result = await adminAPI.getAdminMcpStats() as any; if (result.code === 0) setStats(result.data); } catch (e) { /* ignore */ } }, []); const fetchServers = useCallback(async () => { setLoading(true); try { const params: any = { page, page_size: pageSize }; if (search) params.search = search; if (filterTransport) params.transport_type = filterTransport; if (filterStatus) params.status = filterStatus; const result = await adminAPI.getAdminMcpServers(params) as any; if (result.code === 0) { setServers(result.data.items || []); setTotal(result.data.total || 0); } } catch (e) { showToast('加载服务失败', 'error'); } finally { setLoading(false); } }, [page, search, filterTransport, filterStatus]); useEffect(() => { fetchStats(); fetchServers(); }, [fetchStats, fetchServers]); const handleCreate = () => { setEditingServer(null); setShowModal(true); }; const handleEdit = (server: MCPServer) => { setEditingServer(server); setShowModal(true); }; const handleDelete = async (server: MCPServer) => { if (!confirm(`确认删除 MCP 服务 "${server.name}"?`)) return; try { await adminAPI.deleteAdminMcpServer(server.id); showToast('服务已删除'); fetchServers(); fetchStats(); } catch (e) { showToast('删除失败', 'error'); } }; const handleToggle = async (server: MCPServer) => { try { await adminAPI.toggleAdminMcpServer(server.id); showToast(server.is_enabled ? '已禁用' : '已启用'); fetchServers(); fetchStats(); } catch (e) { showToast('切换失败', 'error'); } }; const handleTest = async (server: MCPServer) => { try { const result = await adminAPI.testAdminMcpServer(server.id) as any; if (result.code === 0) { showToast(result.data.success ? '连接成功' : `连接失败:${result.data.message}`, result.data.success ? 'success' : 'error'); fetchServers(); fetchStats(); } } catch (e) { showToast('测试失败', 'error'); } }; const handleSubmit = async (formData: any) => { try { if (editingServer) { await adminAPI.updateAdminMcpServer(editingServer.id, formData); showToast('服务已更新'); } else { await adminAPI.createAdminMcpServer(formData); showToast('服务已创建'); } setShowModal(false); fetchServers(); fetchStats(); } catch (e: any) { showToast(e.message || '操作失败', 'error'); } }; const getStatusBadge = (status: string) => { const labels: Record = { connected: '已连接', disconnected: '未连接', error: '错误' }; const colors: Record = { connected: { bg: '#dcfce7', color: '#166534' }, disconnected: { bg: '#f3f4f6', color: '#6b7280' }, error: { bg: '#fee2e2', color: '#991b1b' }, }; const c = colors[status] || colors.disconnected; return {labels[status] || status}; }; const getTransportBadge = (type: string) => { const labels: Record = { sse: 'SSE', stdio: '标准IO', streamable_http: 'HTTP 流式' }; const c = type === 'sse' ? { bg: '#e0e7ff', color: '#3730a3' } : type === 'streamable_http' ? { bg: '#d1fae5', color: '#065f46' } : { bg: '#fef3c7', color: '#92400e' }; return {labels[type] || type}; }; const totalPages = Math.ceil(total / pageSize); return (
{toast && (
{toast.message}
)}

MCP 服务管理

{/* 手动添加服务入口已移除:MCP 仅支持通过 URL 导入(见应用市场「导入」)或由 AI 聊天自动部署安装 */}
服务总数
{stats.total_servers || 0}
已启用
{stats.enabled_servers || 0}
已连接
{stats.connected_servers || 0}
传输方式分布
{Object.entries(stats.by_transport || {}).map(([k, v]) => ( {k}: {v as number} ))}
{ setSearch(e.target.value); setPage(1); }} />
{loading ? ( ) : servers.length === 0 ? ( ) : servers.map(s => ( ))}
名称 传输方式 状态 工具数 最后连接 启用 操作
加载中...
未找到 MCP 服务
{s.name}
{s.description?.substring(0, 50)}{s.description?.length > 50 ? '...' : ''}
{getTransportBadge(s.transport_type)} {getStatusBadge(s.status)} {s.tools_count || '-'} {s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : '从未'} {s.is_enabled ? '是' : '否'}
{totalPages > 1 && (
第 {page} / {totalPages} 页
)} {showModal && setShowModal(false)} />}
); } function McpModal({ server, onSubmit, onClose }: { server: MCPServer | null; onSubmit: (data: any) => void; onClose: () => void }) { const [form, setForm] = useState({ name: server?.name || '', description: server?.description || '', transport_type: server?.transport_type || 'stdio', command: server?.command || '', args: server?.args ? JSON.stringify(server.args, null, 2) : '', env_vars: server?.env_vars ? JSON.stringify(server.env_vars, null, 2) : '', url: server?.url || '', headers: server?.headers ? JSON.stringify(server.headers, null, 2) : '', }); const handleChange = (field: string, value: string) => { setForm(prev => ({ ...prev, [field]: value })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data: any = { name: form.name, description: form.description, transport_type: form.transport_type, }; if (form.transport_type === 'stdio') { data.command = form.command; try { data.args = form.args ? JSON.parse(form.args) : {}; } catch { data.args = {}; } try { data.env_vars = form.env_vars ? JSON.parse(form.env_vars) : {}; } catch { data.env_vars = {}; } } else { data.url = form.url; try { data.headers = form.headers ? JSON.parse(form.headers) : {}; } catch { data.headers = {}; } } onSubmit(data); }; return (
e.stopPropagation()}>
{server ? '编辑 MCP 服务' : '添加 MCP 服务'}
handleChange('name', e.target.value)} required placeholder="例如:文件系统服务" />