feat: add support to sync mcp servers from ModelScope (#2313)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user