/** * MCP 服务管理组件 * 支持 SSE / stdio 两种传输类型的 MCP Server 配置管理 * 功能:列表展示、新建/编辑/删除、启用/禁用切换、测试连接、刷新连接、分页 */ import { ref, computed, onMounted, onUnmounted } from 'vue' import * as api from '../js/api.js' import { toast, formatDate } from '../js/utils.js' // 传输类型选项 const TRANSPORT_OPTIONS = [ { value: 'sse', label: 'SSE(HTTP 服务)' }, { value: 'stdio', label: 'Stdio(本地进程)' } ] export default { template: `

MCP 服务管理

管理 MCP (Model Context Protocol) 服务端配置,支持 SSE 和 Stdio 两种传输方式

共 {{ total }} 条配置
名称 传输类型 URL / 命令 启用状态 描述 创建时间 操作
暂无 MCP 服务配置
{{ s.name || '-' }} {{ s.transport_type === 'sse' ? 'SSE' : 'Stdio' }} {{ getDisplayUrl(s) || '-' }} {{ s.description || '-' }} {{ formatDate(s.create_time) }}
{{ testResult.success ? '连接成功' : '连接失败' }}
{{ testResult.message }}
`, setup() { // ==================== 列表状态 ==================== const servers = ref([]) const currentPage = ref(1) const totalPages = ref(1) const total = ref(0) const pageSize = 10 // ==================== 弹窗状态 ==================== const showModal = ref(false) const modalMode = ref('add') // 'add' | 'edit' const editId = ref(null) const saving = ref(false) // 表单数据 const form = ref(createEmptyForm()) function createEmptyForm() { return { name: '', transportType: 'sse', url: '', command: '', argsText: '', envText: '', description: '' } } // ==================== 测试连接状态 ==================== const testLoading = ref({}) const testResult = ref({ visible: false, success: false, message: '' }) let testResultTimer = null // ==================== 刷新连接状态 ==================== const refreshLoading = ref(false) // ==================== 传输类型选项 ==================== const transportOptions = TRANSPORT_OPTIONS // ==================== 数据加载 ==================== async function load(p = 1) { currentPage.value = p try { const json = await api.listMcpServers(p, pageSize) if (json.success) { servers.value = json.data || [] total.value = json.total || 0 totalPages.value = json.pages || 1 } else { toast(json.message || '查询失败', 'error') } } catch (e) { toast('加载列表失败:' + e.message, 'error') } } // ==================== 弹窗操作 ==================== function openAddModal() { modalMode.value = 'add' editId.value = null form.value = createEmptyForm() showModal.value = true } function openEditModal(server) { modalMode.value = 'edit' editId.value = server.id // 解析 args 数组为换行文本 let argsText = '' if (server.args && Array.isArray(server.args)) { argsText = server.args.join('\n') } else if (typeof server.args === 'string' && server.args) { try { const parsed = JSON.parse(server.args) if (Array.isArray(parsed)) argsText = parsed.join('\n') else argsText = server.args } catch (e) { argsText = server.args } } // 解析环境变量对象为换行文本 let envText = '' if (server.env_vars && typeof server.env_vars === 'object') { envText = Object.entries(server.env_vars).map(([k, v]) => k + '=' + v).join('\n') } else if (typeof server.env_vars === 'string' && server.env_vars) { try { const parsed = JSON.parse(server.env_vars) if (typeof parsed === 'object' && parsed !== null) { envText = Object.entries(parsed).map(([k, v]) => k + '=' + v).join('\n') } else { envText = server.env_vars } } catch (e) { envText = server.env_vars } } form.value = { name: server.name || '', transportType: server.transport_type || 'sse', url: server.server_url || '', command: server.command || '', argsText: argsText, envText: envText, description: server.description || '' } showModal.value = true } function closeModal() { showModal.value = false } // ==================== 保存 ==================== function buildSaveData() { const f = form.value const data = { name: f.name, transportType: f.transportType, description: f.description } if (f.transportType === 'sse') { data.serverUrl = f.url } else { data.command = f.command // 解析 args:按换行拆分,过滤空行 if (f.argsText && f.argsText.trim()) { data.args = f.argsText.split('\n').map(s => s.trim()).filter(Boolean) } else { data.args = [] } } // 解析环境变量:按换行拆分,解析 KEY=value if (f.envText && f.envText.trim()) { const envObj = {} f.envText.split('\n').forEach(line => { const trimmed = line.trim() if (!trimmed) return const eqIdx = trimmed.indexOf('=') if (eqIdx > 0) { envObj[trimmed.substring(0, eqIdx)] = trimmed.substring(eqIdx + 1) } }) data.envVars = envObj } return data } async function save() { const f = form.value // 校验 if (!f.name || !f.name.trim()) { toast('请填写服务名称', 'error') return } if (f.transportType === 'sse') { if (!f.url || !f.url.trim()) { toast('请填写 Server URL', 'error') return } } else { if (!f.command || !f.command.trim()) { toast('请填写 Command', 'error') return } } saving.value = true try { const data = buildSaveData() let json if (modalMode.value === 'add') { json = await api.createMcpServer(data) } else { json = await api.updateMcpServer(editId.value, data) } if (json.success) { toast(modalMode.value === 'add' ? '创建成功' : '更新成功', 'success') closeModal() load(currentPage.value) } else { toast(json.message || '操作失败', 'error') } } catch (e) { toast('保存失败:' + e.message, 'error') } finally { saving.value = false } } // ==================== 删除 ==================== async function remove(id, name) { if (!confirm('确定删除配置「' + (name || id) + '」?删除后不可恢复。')) return try { const json = await api.deleteMcpServer(id) if (json.success) { toast('删除成功', 'success') load(currentPage.value) } else { toast(json.message || '删除失败', 'error') } } catch (e) { toast('删除失败:' + e.message, 'error') } } // ==================== 启用/禁用 ==================== async function toggleActive(server) { const newActive = !server.is_active try { const json = await api.toggleMcpServer(server.id, newActive) if (json.success) { toast(newActive ? '已启用' : '已禁用', 'success') // 乐观更新本地状态,避免刷新列表 server.is_active = newActive } else { toast(json.message || '操作失败', 'error') } } catch (e) { toast('操作失败:' + e.message, 'error') } } // ==================== 测试连接 ==================== async function testConnection(server) { testLoading.value = { ...testLoading.value, [server.id]: true } try { const json = await api.testMcpServer(server.id) showTestResult(json) } catch (e) { showTestResult({ success: false, message: e.message }) } finally { testLoading.value = { ...testLoading.value, [server.id]: false } } } function showTestResult(result) { if (testResultTimer) clearTimeout(testResultTimer) testResult.value = { visible: true, success: result.success, message: result.message || '' } testResultTimer = setTimeout(() => { testResult.value.visible = false }, 5000) } // ==================== 刷新连接 ==================== async function refreshConnections() { refreshLoading.value = true try { const json = await api.refreshMcpServers() if (json.success) { toast('连接已刷新', 'success') load(currentPage.value) } else { toast(json.message || '刷新失败', 'error') } } catch (e) { toast('刷新失败:' + e.message, 'error') } finally { refreshLoading.value = false } } // ==================== 工具函数 ==================== function getDisplayUrl(server) { if (server.transport_type === 'sse') { return server.server_url || '-' } // stdio 模式:显示 command + args let display = server.command || '' if (server.args) { // args 可能是字符串或数组 const argsStr = Array.isArray(server.args) ? server.args.join(' ') : server.args display += ' ' + argsStr } return display || '-' } // ==================== 生命周期 ==================== onMounted(() => { load() }) onUnmounted(() => { if (testResultTimer) clearTimeout(testResultTimer) }) return { // 列表 servers, currentPage, totalPages, total, // 弹窗 showModal, modalMode, form, saving, transportOptions, // 测试 testLoading, testResult, // 刷新 refreshLoading, // 方法 load, openAddModal, openEditModal, closeModal, save, remove, toggleActive, testConnection, refreshConnections, getDisplayUrl, formatDate } } }