feat: MCP 服务器支持开机自启

- 新增 autostart 字段与后端增删改接口
- 启动时后台自动连接所有 autostart=true 的服务器
- 卡片列表展示「开机启动」徽标,添加/编辑弹窗支持勾选
This commit is contained in:
guo zebin
2026-04-17 16:39:31 +08:00
parent 3a44d03bbe
commit bf910fd3f0
3 changed files with 96 additions and 16 deletions
+14 -8
View File
@@ -3,7 +3,7 @@
"id": 1,
"name": "tools",
"ip": "",
"connection_time": "2026-03-27 10:58:07",
"connection_time": "2026-04-15 21:58:59",
"key": "",
"transport": "stdio",
"command": "python",
@@ -11,7 +11,8 @@
"test/mcp_stdio_example.py"
],
"cwd": "",
"env": {}
"env": {},
"autostart": false
},
{
"id": 2,
@@ -25,7 +26,8 @@
"server.py"
],
"cwd": "mcp_servers/schedule_manager",
"env": {}
"env": {},
"autostart": false
},
{
"id": 3,
@@ -41,7 +43,8 @@
"cwd": "mcp_servers/logseq",
"env": {
"LOGSEQ_GRAPH_DIR": "D:/iCloudDrive/iCloud~com~logseq~logseq/第二大脑"
}
},
"autostart": false
},
{
"id": 4,
@@ -55,7 +58,8 @@
"mcp_servers/yueshen_rag/server.py"
],
"cwd": "",
"env": {}
"env": {},
"autostart": false
},
{
"id": 5,
@@ -69,13 +73,14 @@
"mcp_servers/window_capture/server.py"
],
"cwd": "",
"env": {}
"env": {},
"autostart": false
},
{
"id": 6,
"name": "课程知识库",
"ip": "",
"connection_time": "2026-04-10 10:19:46",
"connection_time": "2026-04-17 16:28:31",
"key": "",
"transport": "stdio",
"command": "python",
@@ -85,6 +90,7 @@
"./fay_player_knowledge"
],
"cwd": "",
"env": {}
"env": {},
"autostart": true
}
]
+43 -4
View File
@@ -60,6 +60,8 @@ def load_mcp_servers():
for server in servers:
server['status'] = 'offline'
server['latency'] = '0ms'
if 'autostart' not in server:
server['autostart'] = False
return servers
else:
# 如果文件不存在,使用默认数据并保存
@@ -122,7 +124,8 @@ def save_mcp_servers(servers):
"command": server.get('command', ''),
"args": server.get('args', []),
"cwd": server.get('cwd', ''),
"env": server.get('env', {})
"env": server.get('env', {}),
"autostart": bool(server.get('autostart', False))
}
servers_to_save.append(server_copy)
@@ -449,7 +452,8 @@ def add_mcp_server():
"command": data.get('command', ''),
"args": data.get('args', []),
"cwd": data.get('cwd', ''),
"env": data.get('env', {})
"env": data.get('env', {}),
"autostart": bool(data.get('autostart', False))
}
# 如果请求中包含 auto_connect 字段并且为 True,则尝试连接
@@ -651,6 +655,8 @@ def update_mcp_server(server_id):
server['name'] = data.get('name', server['name'])
transport = data.get('transport', server.get('transport', 'sse'))
server['transport'] = transport
if 'autostart' in data:
server['autostart'] = bool(data.get('autostart'))
# 根据传输类型更新配置
if transport == 'stdio':
@@ -1660,18 +1666,51 @@ def run():
)
server.serve_forever()
# 启动时自动连接标记为 autostart 的服务器
def _autostart_connect_servers():
"""在 Flask 启动后,后台连接所有配置了开机启动的服务器。"""
global mcp_servers
# 稍等片刻让 Flask 完成启动,避免与其它初始化抢占日志
time.sleep(2)
targets = [s for s in mcp_servers if s.get('autostart')]
if not targets:
return
util.log(1, f"开机启动:准备连接 {len(targets)} 个MCP服务器")
for server in targets:
try:
server_id = server['id']
success, updated_server, _ = connect_to_real_mcp(server)
for i, s in enumerate(mcp_servers):
if s['id'] == server_id:
mcp_servers[i] = updated_server
break
if success:
util.log(1, f"开机启动:{updated_server.get('name')} 已连接")
else:
util.log(1, f"开机启动:{updated_server.get('name')} 连接失败")
except Exception as e:
util.log(1, f"开机启动连接异常: {e}")
try:
save_mcp_servers(mcp_servers)
except Exception as e:
util.log(1, f"开机启动后保存状态失败: {e}")
# 启动MCP服务器
def start():
# 启动连接检查定时任务
# start_connection_check() TODO 暂时取消定时检查任务
# 输出启动信息
util.log(1, "MCP服务已启动在端口5010")
# 启动服务器
from scheduler.thread_manager import MyThread
MyThread(target=run).start()
# 后台触发 autostart 连接
MyThread(target=_autostart_connect_servers).start()
if __name__ == '__main__':
import logging
logging.basicConfig(level=logging.DEBUG)
+39 -4
View File
@@ -58,6 +58,20 @@
font-weight: bold;
font-size: 16px;
}
.autostart-badge {
margin-left: 8px;
padding: 1px 8px;
border-radius: 10px;
font-size: 12px;
line-height: 1.4;
background-color: #409eff;
color: #fff;
white-space: nowrap;
display: inline-block;
}
.autostart-badge.hidden {
display: none;
}
.status-online {
color: #67c23a;
display: flex;
@@ -511,9 +525,12 @@
<div class="mcp-container">
<!-- 使用Jinja2模板循环渲染MCP服务器卡片 -->
{% for server in mcp_servers %}
<div class="mcp-card tooltip" data-server-id="{{ server.id }}" data-transport="{{ server.transport or 'sse' }}" data-command="{{ server.command or '' }}" data-args="{{ (server.args | join(' ')) if server.args else '' }}" data-cwd="{{ server.cwd or '' }}" data-key="{{ server.key or '' }}" data-env='{{ server.env | tojson }}'>
<div class="mcp-card tooltip" data-server-id="{{ server.id }}" data-transport="{{ server.transport or 'sse' }}" data-command="{{ server.command or '' }}" data-args="{{ (server.args | join(' ')) if server.args else '' }}" data-cwd="{{ server.cwd or '' }}" data-key="{{ server.key or '' }}" data-env='{{ server.env | tojson }}' data-autostart="{{ '1' if server.autostart else '0' }}">
<div class="mcp-header">
<div class="mcp-title">{{ server.name }}</div>
<div style="display:flex; align-items:center;">
<div class="mcp-title">{{ server.name }}</div>
<span class="autostart-badge{% if not server.autostart %} hidden{% endif %}">开机启动</span>
</div>
<div class="status-{{ server.status }}">
<div class="status-dot {{ server.status }}-dot"></div>
{{ '在线' if server.status == 'online' else '离线' }}
@@ -600,6 +617,10 @@
<input type="text" class="mcp-input" id="mcpEnv" placeholder='例如: {"MODE":"local"}'>
</div>
</div>
<div class="mcp-form-item">
<label class="mcp-label">开机启动:</label>
<input type="checkbox" id="mcpAutostart">
</div>
</div>
<div class="mcp-dialog-footer">
<button class="add-server-btn" id="addServerBtn">添加</button>
@@ -656,6 +677,10 @@
<input type="text" class="mcp-input" id="editMcpEnv" placeholder='例如: {"MODE":"local"}'>
</div>
</div>
<div class="mcp-form-item">
<label class="mcp-label">开机启动:</label>
<input type="checkbox" id="editAutostart">
</div>
<div class="mcp-form-item">
<label class="mcp-label">保存后重连:</label>
<input type="checkbox" id="editAutoReconnect" checked>
@@ -773,6 +798,7 @@
const args = card.getAttribute('data-args') || '';
const cwd = card.getAttribute('data-cwd') || '';
const env = card.getAttribute('data-env') || '';
const autostart = card.getAttribute('data-autostart') === '1';
// 填充表单
document.getElementById('editMcpName').value = name;
@@ -783,6 +809,7 @@
document.getElementById('editMcpArgs').value = args;
document.getElementById('editMcpCwd').value = cwd;
document.getElementById('editMcpEnv').value = env && env !== 'null' ? env : '';
document.getElementById('editAutostart').checked = autostart;
// 切换显示
const editTransportSel = document.getElementById('editMcpTransport');
@@ -817,6 +844,7 @@
const cwd = document.getElementById('editMcpCwd').value.trim();
const envText = document.getElementById('editMcpEnv').value.trim();
const autoReconnect = document.getElementById('editAutoReconnect').checked;
const autostart = document.getElementById('editAutostart').checked;
// 获取保存按钮并设置加载状态
const saveBtn = document.getElementById('editSaveBtn');
@@ -824,7 +852,7 @@
saveBtn.textContent = '保存中...';
saveBtn.disabled = true;
let payload = { name, transport, auto_reconnect: autoReconnect };
let payload = { name, transport, auto_reconnect: autoReconnect, autostart };
if (transport === 'sse') {
payload.ip = ip;
payload.key = key;
@@ -857,6 +885,12 @@
// 传输类型与属性
card.setAttribute('data-transport', transport);
card.setAttribute('data-autostart', autostart ? '1' : '0');
const badgeEl = card.querySelector('.autostart-badge');
if (badgeEl) {
if (autostart) badgeEl.classList.remove('hidden');
else badgeEl.classList.add('hidden');
}
if (transport === 'sse') {
const ipEl = card.querySelector('.mcp-info .mcp-value');
if (ipEl) {
@@ -913,8 +947,9 @@
// 获取输入值
const name = document.getElementById('mcpName').value.trim();
const transport = document.getElementById('mcpTransport').value;
const autostart = document.getElementById('mcpAutostart').checked;
let data = { name, transport };
let data = { name, transport, autostart };
if (transport === 'stdio') {
const command = document.getElementById('mcpCommand').value.trim();
const argsStr = document.getElementById('mcpArgs').value.trim();