/**
* 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 }} 条配置
{{ testResult.success ? '连接成功' : '连接失败' }}
{{ testResult.message }}
{{ modalMode === 'add' ? '新建 MCP 服务配置' : '编辑 MCP 服务配置' }}
每行一个参数,留空则不传参
格式:KEY=value,每行一组,留空则不设置
`,
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
}
}
}