feat: add support to sync mcp servers from ModelScope (#2313)

This commit is contained in:
Soulter
2025-08-04 17:24:07 +08:00
committed by GitHub
parent 0c6e526f94
commit 2f8d921adf
5 changed files with 284 additions and 56 deletions
@@ -3,6 +3,7 @@ import json
import os
import asyncio
import logging
import aiohttp
from datetime import timedelta
from deprecated import deprecated
@@ -871,6 +872,100 @@ class FunctionToolManager:
return True
return False
@property
def mcp_config_path(self):
data_dir = get_astrbot_data_path()
return os.path.join(data_dir, "mcp_server.json")
def load_mcp_config(self):
if not os.path.exists(self.mcp_config_path):
# 配置文件不存在,创建默认配置
os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True)
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4)
return DEFAULT_MCP_CONFIG
try:
with open(self.mcp_config_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"加载 MCP 配置失败: {e}")
return DEFAULT_MCP_CONFIG
def save_mcp_config(self, config: dict):
try:
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
return True
except Exception as e:
logger.error(f"保存 MCP 配置失败: {e}")
return False
async def sync_modelscope_mcp_servers(self, access_token: str) -> None:
"""从 ModelScope 平台同步 MCP 服务器配置"""
base_url = "https://www.modelscope.cn/openapi/v1"
url = f"{base_url}/mcp/servers/operational"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
mcp_server_list = data.get("data", {}).get(
"mcp_server_list", []
)
local_mcp_config = self.load_mcp_config()
synced_count = 0
for server in mcp_server_list:
server_name = server["name"]
operational_urls = server.get("operational_urls", [])
if not operational_urls:
continue
url_info = operational_urls[0]
server_url = url_info.get("url")
if not server_url:
continue
# 添加到配置中(同名会覆盖)
local_mcp_config["mcpServers"][server_name] = {
"url": server_url,
"transport": "sse",
"active": True,
"provider": "modelscope",
}
synced_count += 1
if synced_count > 0:
self.save_mcp_config(local_mcp_config)
tasks = []
for server in mcp_server_list:
name = server["name"]
tasks.append(
self.enable_mcp_server(
name=name,
config=local_mcp_config["mcpServers"][name],
)
)
await asyncio.gather(*tasks)
logger.info(
f"从 ModelScope 同步了 {synced_count} 个 MCP 服务器"
)
else:
logger.warning("没有找到可用的 ModelScope MCP 服务器")
else:
raise Exception(
f"ModelScope API 请求失败: HTTP {response.status}"
)
except aiohttp.ClientError as e:
raise Exception(f"网络连接错误: {str(e)}")
except Exception as e:
raise Exception(f"同步 ModelScope MCP 服务器时发生错误: {str(e)}")
def __str__(self):
return str(self.func_list)
+25 -39
View File
@@ -1,5 +1,3 @@
import json
import os
import traceback
import aiohttp
@@ -7,7 +5,6 @@ from quart import request
from astrbot.core import logger
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.star import star_map
from .route import Response, Route, RouteContext
@@ -30,42 +27,14 @@ class ToolsRoute(Route):
"/tools/mcp/test": ("POST", self.test_mcp_connection),
"/tools/list": ("GET", self.get_tool_list),
"/tools/toggle-tool": ("POST", self.toggle_tool),
"/tools/mcp/sync-provider": ("POST", self.sync_provider),
}
self.register_routes()
self.tool_mgr = self.core_lifecycle.provider_manager.llm_tools
@property
def mcp_config_path(self):
data_dir = get_astrbot_data_path()
return os.path.join(data_dir, "mcp_server.json")
def load_mcp_config(self):
if not os.path.exists(self.mcp_config_path):
# 配置文件不存在,创建默认配置
os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True)
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4)
return DEFAULT_MCP_CONFIG
try:
with open(self.mcp_config_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"加载 MCP 配置失败: {e}")
return DEFAULT_MCP_CONFIG
def save_mcp_config(self, config):
try:
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
return True
except Exception as e:
logger.error(f"保存 MCP 配置失败: {e}")
return False
async def get_mcp_servers(self):
try:
config = self.load_mcp_config()
config = self.tool_mgr.load_mcp_config()
servers = []
# 获取所有服务器并添加它们的工具列表
@@ -128,14 +97,14 @@ class ToolsRoute(Route):
if not has_valid_config:
return Response().error("必须提供有效的服务器配置").__dict__
config = self.load_mcp_config()
config = self.tool_mgr.load_mcp_config()
if name in config["mcpServers"]:
return Response().error(f"服务器 {name} 已存在").__dict__
config["mcpServers"][name] = server_config
if self.save_mcp_config(config):
if self.tool_mgr.save_mcp_config(config):
try:
await self.tool_mgr.enable_mcp_server(
name, server_config, timeout=30
@@ -165,7 +134,7 @@ class ToolsRoute(Route):
if not name:
return Response().error("服务器名称不能为空").__dict__
config = self.load_mcp_config()
config = self.tool_mgr.load_mcp_config()
if name not in config["mcpServers"]:
return Response().error(f"服务器 {name} 不存在").__dict__
@@ -201,7 +170,7 @@ class ToolsRoute(Route):
config["mcpServers"][name] = server_config
if self.save_mcp_config(config):
if self.tool_mgr.save_mcp_config(config):
# 处理MCP客户端状态变化
if active:
if name in self.tool_mgr.mcp_client_dict or not only_update_active:
@@ -269,14 +238,14 @@ class ToolsRoute(Route):
if not name:
return Response().error("服务器名称不能为空").__dict__
config = self.load_mcp_config()
config = self.tool_mgr.load_mcp_config()
if name not in config["mcpServers"]:
return Response().error(f"服务器 {name} 不存在").__dict__
del config["mcpServers"][name]
if self.save_mcp_config(config):
if self.tool_mgr.save_mcp_config(config):
if name in self.tool_mgr.mcp_client_dict:
try:
await self.tool_mgr.disable_mcp_server(name, timeout=10)
@@ -376,3 +345,20 @@ class ToolsRoute(Route):
except Exception as e:
logger.error(traceback.format_exc())
return Response().error(f"操作工具失败: {str(e)}").__dict__
async def sync_provider(self):
"""同步 MCP 提供者配置"""
try:
data = await request.json
provider_name = data.get("name") # modelscope, or others
match provider_name:
case "modelscope":
access_token = data.get("access_token", "")
await self.tool_mgr.sync_modelscope_mcp_servers(access_token)
case _:
return Response().error(f"未知: {provider_name}").__dict__
return Response().ok(message="同步成功").__dict__
except Exception as e:
logger.error(traceback.format_exc())
return Response().error(f"同步失败: {str(e)}").__dict__
@@ -17,7 +17,8 @@
"add": "Add Server",
"useTemplateStdio": "Stdio Template",
"useTemplateStreamableHttp": "Streamable HTTP Template",
"useTemplateSse": "SSE Template"
"useTemplateSse": "SSE Template",
"sync": "Sync MCP Servers"
},
"empty": "No MCP servers available, click Add Server to add one",
"status": {
@@ -77,7 +78,8 @@
"buttons": {
"cancel": "Cancel",
"save": "Save",
"testConnection": "Test Connection"
"testConnection": "Test Connection",
"sync": "Sync"
}
},
"serverDetail": {
@@ -17,7 +17,8 @@
"add": "新增服务器",
"useTemplateStdio": "Stdio 模板",
"useTemplateStreamableHttp": "Streamable HTTP 模板",
"useTemplateSse": "SSE 模板"
"useTemplateSse": "SSE 模板",
"sync": "同步服务器"
},
"empty": "暂无 MCP 服务器,点击 新增服务器 添加",
"status": {
@@ -77,7 +78,8 @@
"buttons": {
"cancel": "取消",
"save": "保存",
"testConnection": "测试连接"
"testConnection": "测试连接",
"sync": "同步"
}
},
"serverDetail": {
@@ -89,7 +91,43 @@
"importConfig": "导入配置"
}
},
"confirmDelete": "确定要删除服务器 {name} 吗?"
"confirmDelete": "确定要删除服务器 {name} 吗?",
"syncProvider": {
"title": "同步 MCP 服务器",
"subtitle": "从提供商同步 MCP 服务器配置到本地",
"steps": {
"selectProvider": "步骤 1: 选择提供商",
"configureAuth": "步骤 2: 配置认证",
"syncServers": "步骤 3: 同步服务器"
},
"providers": {
"modelscope": "ModelScope",
"description": "ModelScope 是一个开源的模型社区,提供各种机器学习和AI服务的MCP服务器"
},
"fields": {
"provider": "选择提供商",
"accessToken": "访问令牌",
"tokenRequired": "访问令牌是必填项",
"tokenHint": "请输入您的 ModelScope 访问令牌"
},
"buttons": {
"cancel": "取消",
"previous": "上一步",
"next": "下一步",
"sync": "开始同步",
"getToken": "获取令牌"
},
"status": {
"selectProvider": "请选择一个 MCP 服务器提供商",
"enterToken": "请输入访问令牌以继续",
"readyToSync": "准备同步服务器配置"
},
"messages": {
"syncSuccess": "MCP 服务器同步成功!",
"syncError": "同步失败: {error}",
"tokenHelp": "如何获取 ModelScope 访问令牌?点击右侧按钮查看说明"
}
}
},
"messages": {
"getServersError": "获取 MCP 服务器列表失败: {error}",
+119 -12
View File
@@ -25,10 +25,14 @@
rounded="xl" size="x-large">
{{ tm('functionTools.buttons.view') }}({{ tools.length }})
</v-btn>
<v-btn color="success" prepend-icon="mdi-plus" variant="tonal" @click="showMcpServerDialog = true"
rounded="xl" size="x-large">
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
@click="showMcpServerDialog = true" rounded="xl" size="x-large">
{{ tm('mcpServers.buttons.add') }}
</v-btn>
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
rounded="xl" size="x-large">
{{ tm('mcpServers.buttons.sync') }}
</v-btn>
</div>
</v-row>
@@ -59,8 +63,8 @@
<v-row v-else>
<v-col v-for="(server, index) in mcpServers || []" :key="index" cols="12" md="6" lg="4" xl="3">
<item-card style="background-color: rgb(var(--v-theme-mcpCardBg));" :item="server" title-field="name" enabled-field="active"
@toggle-enabled="updateServerStatus" @delete="deleteServer" @edit="editServer">
<item-card style="background-color: rgb(var(--v-theme-mcpCardBg));" :item="server" title-field="name"
enabled-field="active" @toggle-enabled="updateServerStatus" @delete="deleteServer" @edit="editServer">
<template v-slot:item-details="{ item }">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
@@ -286,6 +290,63 @@
</v-card>
</v-dialog>
<!-- 添加/编辑 MCP 服务器对话框 -->
<v-dialog v-model="showSyncMcpServerDialog" max-width="500px" persistent>
<v-card>
<v-card-title class="bg-primary text-white py-3">
<span>同步外部平台 MCP 服务器</span>
</v-card-title>
<v-card-text class="py-4">
<v-select v-model="selectedMcpServerProvider" :items="mcpServerProviderList"
label="选择平台" variant="outlined" required></v-select>
<div v-if="selectedMcpServerProvider === 'modelscope'">
<v-timeline align="start" side="end">
<v-timeline-item icon="mdi-numeric-1" icon-color="rgb(var(--v-theme-background))">
<div>
<div class="text-h4">发现 MCP 服务器</div>
<p class="mt-2">
访问 <a href="https://www.modelscope.cn/mcp" target="_blank">ModelScope 平台</a> 浏览需要的 MCP 服务器
</p>
</div>
</v-timeline-item>
<v-timeline-item icon="mdi-numeric-2" icon-color="rgb(var(--v-theme-background))">
<div>
<div class="text-h4">获取访问令牌</div>
<p class="mt-2">
<a href="https://modelscope.cn/my/myaccesstoken" target="_blank">账户设置</a>中获取个人访问令牌
</p>
</div>
</v-timeline-item>
<v-timeline-item icon="mdi-numeric-3" icon-color="rgb(var(--v-theme-background))">
<div>
<div class="text-h4">输入您的访问令牌</div>
<p class="mt-2">
输入您的访问令牌以同步 MCP 服务器
</p>
<v-text-field v-model="mcpProviderToken" type="password" variant="outlined"
label="访问令牌" class="mt-2" hide-details/>
</div>
</v-timeline-item>
</v-timeline>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showSyncMcpServerDialog = false" :disabled="loading">
{{ tm('dialogs.addServer.buttons.cancel') }}
</v-btn>
<v-btn color="primary" @click="syncMcpServers" :loading="loading" :disabled="loading">
{{ tm('dialogs.addServer.buttons.sync') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 服务器详情对话框 -->
<v-dialog v-model="showServerDetailDialog" max-width="800px">
<v-card>
@@ -413,14 +474,8 @@
<v-expansion-panel-title>
<v-row no-gutters align="center">
<v-col cols="1">
<v-checkbox
v-model="tool.active"
color="primary"
hide-details
density="compact"
@click.stop
@change="toggleToolStatus(tool)"
></v-checkbox>
<v-checkbox v-model="tool.active" color="primary" hide-details density="compact" @click.stop
@change="toggleToolStatus(tool)"></v-checkbox>
</v-col>
<v-col cols="3">
<div class="d-flex align-center">
@@ -532,6 +587,12 @@ export default {
mcpServers: [],
tools: [],
showMcpServerDialog: false,
selectedMcpServerProvider: "modelscope",
mcpServerProviderList: ["modelscope"],
mcpProviderToken: '',
showSyncMcpServerDialog: false,
showServerDetailDialog: false,
addServerDialogMessage: "",
showToolsDialog: false,
@@ -1010,6 +1071,52 @@ export default {
tool.active = !tool.active;
this.showError(this.tm('messages.toggleToolError', { error: error.response?.data?.message || error.message }));
}
},
// 同步 MCP 服务器
async syncMcpServers() {
if (!this.selectedMcpServerProvider) {
this.showError(this.tm('syncProvider.status.selectProvider'));
return;
}
this.loading = true;
try {
const requestData = {
name: this.selectedMcpServerProvider
};
// 根据不同平台添加相应的参数
if (this.selectedMcpServerProvider === 'modelscope') {
if (!this.mcpProviderToken.trim()) {
this.showError(this.tm('syncProvider.status.enterToken'));
this.loading = false;
return;
}
requestData.access_token = this.mcpProviderToken.trim();
}
const response = await axios.post('/api/tools/mcp/sync-provider', requestData);
if (response.data.status === 'ok') {
this.showSuccess(response.data.message || this.tm('syncProvider.messages.syncSuccess'));
this.showSyncMcpServerDialog = false;
this.mcpProviderToken = '';
// 刷新服务器列表
this.getServers();
this.getTools();
} else {
this.showError(response.data.message || this.tm('syncProvider.messages.syncError', { error: 'Unknown error' }));
}
} catch (error) {
console.error('同步 MCP 服务器失败:', error);
this.showError(this.tm('syncProvider.messages.syncError', {
error: error.response?.data?.message || error.message || '网络连接或访问令牌问题'
}));
} finally {
this.loading = false;
}
}
}
}