diff --git a/backend/artifacts/serializers.py b/backend/artifacts/serializers.py index a25d41d0cabc4719160b302740f200a1d402a848..d6ed309cff410bbeb01903fe03f8de80f7c9c348 100644 --- a/backend/artifacts/serializers.py +++ b/backend/artifacts/serializers.py @@ -14,6 +14,7 @@ import os import re +import json from rest_framework import serializers @@ -92,6 +93,7 @@ class MCPDetailSerializer(serializers.ModelSerializer): tag = serializers.SerializerMethodField() installed_status = serializers.SerializerMethodField() cmd_list = serializers.SerializerMethodField() + app_list = serializers.SerializerMethodField() updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M") class Meta: @@ -112,6 +114,7 @@ class MCPDetailSerializer(serializers.ModelSerializer): 'cmd_list', 'mcp_config', 'installed_status', + 'app_list', ) @staticmethod @@ -129,6 +132,18 @@ class MCPDetailSerializer(serializers.ModelSerializer): return Task.Status.NOT_YET return Task.Status.SUCCESS + @staticmethod + def get_app_list(obj): + cmd = ["/var/lib/dev-store/src/mcp_manage.sh", "mcp-status", obj.package_name] + executor = CommandExecutor(cmd, timeout=30) + stdout, _, returncode = executor.run() + if returncode == 0: # 成功执行 + stdout = stdout.strip() if stdout else "" + app_status_list = json.loads(stdout) + return app_status_list + else: + return [] + @staticmethod def get_cmd_list(obj): cmd_list = [ diff --git a/backend/artifacts/views.py b/backend/artifacts/views.py index 6c38b3b0a18560b3e550915949766c7213fe2eff..07ab7c6c7e447d8000907a3cace6ddcc7c80efde 100644 --- a/backend/artifacts/views.py +++ b/backend/artifacts/views.py @@ -29,6 +29,7 @@ from artifacts.serializers import ( ) from artifacts.tasks.install_mcp_task import InstallMCPTask from artifacts.utils import get_devstore_log +from utils.mcp_tools import get_mcp_status_in_apps, manage_mcp_config from constants.choices import ArtifactTag from tasks.models import Task from tasks.scheduler import scheduler, check_scheduler_load @@ -132,6 +133,72 @@ class ArtifactViewSet(viewsets.GenericViewSet): return Response(result, status=status_code) + @action(methods=['GET'], detail=False) + def mcp_apps_status(self, request): + """获取MCP在所有智能体应用中的配置状态""" + logger.info(f"==== API: [GET] /v1.0/artifacts/mcp_apps_status/ ====") + + # 获取必需参数 + package_name = request.query_params.get('package_name') + + # 参数验证 + if not package_name: + return Response( + {'is_success': False, 'message': f"Missing required parameter: package_name"}, + status=status.HTTP_400_BAD_REQUEST + ) + + logger.info(f"Querying MCP status in apps for: {package_name}") + + # 调用MCP状态查询方法 + result = get_mcp_status_in_apps(package_name) + + # 根据结果设置HTTP状态码 + status_code = result.get('status_code', status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 创建响应并添加防缓存头 + response = Response(result, status=status_code) + response['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response['Pragma'] = 'no-cache' + response['Expires'] = '0' + return Response(result, status=status_code) + + @action(methods=['POST'], detail=False) + def mcp_config_manage(self, request): + """管理MCP配置(添加或删除)""" + logger.info(f"==== API: [POST] /v1.0/artifacts/mcp_config_manage/ ====") + + # 获取必需参数 + action = request.query_params.get('action') + package_name = request.query_params.get('package_name') + app_name = request.query_params.get('app_name') + + # 参数验证 + if not action: + return Response( + {'is_success': False, 'message': f"Missing required parameter: action"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not package_name: + return Response( + {'is_success': False, 'message': f"Missing required parameter: package_name"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not app_name: + return Response( + {'is_success': False, 'message': f"Missing required parameter: app_name"}, + status=status.HTTP_400_BAD_REQUEST + ) + + logger.info(f"Managing MCP config - action: {action}, mcp: {package_name}, app: {app_name}") + + # 调用MCP配置管理方法 + result = manage_mcp_config(action, package_name, app_name) + + # 根据结果设置HTTP状态码 + status_code = result.get('status_code', status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response(result, status=status_code) def list(self, request): diff --git a/backend/mcp_manage.sh b/backend/mcp_manage.sh new file mode 100755 index 0000000000000000000000000000000000000000..a79a2536c4f3805cd30fca771d09ede41ac0c0a6 --- /dev/null +++ b/backend/mcp_manage.sh @@ -0,0 +1,325 @@ +#!/bin/bash +# mcp_manage.sh - MCP配置管理模块化版本,适合接口调用 + +# 获取实际调用者用户名(支持 sudo / su 场景) +CALL_USER=$(logname 2>/dev/null || echo $SUDO_USER || echo $USER) +# 获取调用者 home 目录 +CALL_USER_HOME=$(getent passwd "$CALL_USER" | cut -d: -f6) +# == 预定义路径 == +declare -A APP_CONFIG_PATHS=( + ["DeepChat"]="$CALL_USER_HOME/.config/DeepChat/mcp_settings.json" + ["roo-code"]="$CALL_USER_HOME/.config/VSCodium/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" +) + +declare -A APP_DISPLAY_NAMES=( + ["roo-code"]="roo-code" + ["DeepChat"]="DeepChat" +) + +MCP_SERVERS_DIR="/opt/mcp-servers/servers" + +# == 公用函数 == +json_error() { + local message="$1" + echo "{\"error\":\"$message\"}" +} + +fail() { + local msg="$1" + json_error "$msg" + exit 1 +} + +get_app_config_path() { + local app="$1" + if [[ -n "${APP_CONFIG_PATHS[$app]}" ]]; then + echo "${APP_CONFIG_PATHS[$app]}" + else + echo "$CALL_USER_HOME/.config/${app}/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" + fi +} + +get_mcp_server_name_from_config() { + local config_file="$1" + + # 检查文件是否存在 + if [[ ! -f "$config_file" ]]; then + return 1 + fi + + # 从 mcp_config.json 中读取服务器配置的第一个键名 + local server_name + server_name=$(jq -r '.mcpServers | keys[0]' "$config_file" 2>/dev/null) + + if [[ -n "$server_name" && "$server_name" != "null" ]]; then + echo "$server_name" + return 0 + fi + + return 1 +} + +# == 从配置文件中查找真实的服务器名称 == +find_real_server_name() { + local mcp_name="$1" + local app="$2" + local user_config + user_config=$(get_app_config_path "$app") + + # 检查用户配置文件是否存在 + if [[ ! -f "$user_config" ]]; then + return 1 + fi + + # 通过 find_mcp_config_file 找到配置文件 + local config_file + config_file=$(find_mcp_config_file "$mcp_name") + + if [[ $? -eq 0 && -f "$config_file" ]]; then + # 从MCP配置文件中获取真实的服务器名称 + local real_server_name + real_server_name=$(get_mcp_server_name_from_config "$config_file") + + if [[ -n "$real_server_name" ]]; then + # 检查用户配置中是否存在这个服务器名称 + if jq -e ".mcpServers.\"$real_server_name\"" "$user_config" >/dev/null 2>&1; then + echo "$real_server_name" + return 0 + fi + fi + fi + + return 1 +} + +# == 查找 MCP 配置文件 == +find_mcp_config_file() { + local package_name="$1" + + # 检查是否是完整路径 + if [[ -f "$package_name" ]]; then + echo "$package_name" + return 0 + fi + + # 直接使用rpm -ql查找包的mcp_config.json文件 + local config_file + config_file=$(rpm -ql "$package_name" 2>/dev/null | grep "mcp_config.json$" | head -1) + + if [[ -n "$config_file" && -f "$config_file" ]]; then + echo "$config_file" + return 0 + fi + + return 1 +} + + +is_app_installed() { + local app="$1" + + case "$app" in + "roo code"|"roo-code") + if ! command -v "vscodium" >/dev/null 2>&1 && [[ ! -d "$CALL_USER_HOME/.config/VSCodium" ]]; then + return 1 + fi + if [[ -d "$CALL_USER_HOME/.config/VSCodium/User/globalStorage/rooveterinaryinc.roo-cline" ]]; then + return 0 + fi + return 1 + ;; + "DeepChat") + if command -v "deepchat" >/dev/null 2>&1; then + return 0 + fi + if [[ -d "$CALL_USER_HOME/.config/DeepChat" ]]; then + return 0 + fi + return 1 + ;; + *) + if command -v "$app" >/dev/null 2>&1; then + return 0 + fi + local config_dir="$CALL_USER_HOME/.config/$app" + if [[ -d "$config_dir" ]]; then + return 0 + fi + return 1 + ;; + esac +} + +normalize_mcp_config() { + local config_file="$1" + local app="$2" + + case "$app" in + "DeepChat") + jq ' + .mcpServers |= with_entries(.value |= . + { + descriptions: (.descriptions // .description // ""), + icons: (.icons // "🔧"), + autoApprove: (.autoApprove // []), + type: (.type // "stdio"), + env: (.env // {}), + disable: (.disable // .disabled // false) + }) + ' "$config_file" + ;; + *) + cat "$config_file" + ;; + esac +} + +write_config() { + local mcp_name="$1" + local app="$2" + + local config_file + config_file=$(find_mcp_config_file "$mcp_name") + if [[ $? -ne 0 ]]; then + fail "找不到MCP配置文件: $mcp_name" + fi + + if ! is_app_installed "$app"; then + fail "$app 未安装或未使用过" + fi + + local user_config + user_config=$(get_app_config_path "$app") + if [[ ! -f "$user_config" ]]; then + fail "MCP配置文件不存在: $user_config,请先在 $app 中安装MCP扩展" + fi + + local normalized_config + normalized_config=$(normalize_mcp_config "$config_file" "$app") + + if echo "$normalized_config" | jq -s '.[1] * .[0]' "$user_config" - > "$user_config.tmp" 2>/dev/null; then + mv -f "$user_config.tmp" "$user_config" + echo "true" + else + rm -f "$user_config.tmp" + fail "配置合并失败" + fi +} + +delete_config() { + local server_name="$1" + local app="$2" + local user_config + user_config=$(get_app_config_path "$app") + + if [[ ! -f "$user_config" ]]; then + fail "配置文件不存在" + fi + + # 使用find_real_server_name查找真实的服务器名称 + local real_server_name + real_server_name=$(find_real_server_name "$server_name" "$app") + + if [[ -z "$real_server_name" ]]; then + fail "服务器 '$server_name' 不存在" + fi + + if jq "del(.mcpServers.\"$real_server_name\")" "$user_config" > "$user_config.tmp" 2>/dev/null; then + mv -f "$user_config.tmp" "$user_config" + echo "true" + else + rm -f "$user_config.tmp" + fail "删除操作失败" + fi +} + +query_mcp_in_all_apps() { + local mcp_name="$1" + + # 查找配置文件 + local config_file + config_file=$(find_mcp_config_file "$mcp_name") + if [[ $? -ne 0 ]]; then + json_error "本地找不到MCP: $mcp_name" + return 1 + fi + + local app_list='[' + local first=true + + for app in "${!APP_CONFIG_PATHS[@]}"; do + local display_name="${APP_DISPLAY_NAMES[$app]:-$app}" + local mcp_status="removed" + + if is_app_installed "$app"; then + local real_server_name + real_server_name=$(find_real_server_name "$mcp_name" "$app" 2>/dev/null) + + if [[ $? -eq 0 ]] && [[ -n "$real_server_name" ]]; then + mcp_status="added" + fi + fi + + if [[ "$first" == true ]]; then + first=false + else + app_list+=',' + fi + app_list+="{\"name\":\"$display_name\",\"status\":\"$mcp_status\"}" + done + + app_list+=']' + echo "$app_list" + return 0 +} + +# == 命令行参数解析 == +usage() { + cat < [参数] + +命令: + status - 查询指定服务器在特定应用中的状态 + add - 添加MCP配置到指定应用 + del - 从指定应用删除MCP配置 + mcp-status - 查询指定MCP在所有应用中的配置状态 + help - 显示帮助 + +支持的应用: + roo-code - Roo Code (基于VSCodium) + DeepChat - DeepChat独立应用 + +示例: + $0 status oeDeploy roo-code # 查询oeDeploy在Roo Code中的状态 + $0 add oeDeploy roo-code # 将oeDeploy添加到Roo Code + $0 del mcp-oedp roo-code # 从Roo Code删除mcp-oedp + $0 mcp-status oeDeploy # 查看oeDeploy在所有应用中的配置状态 +EOF +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +cmd=$1 +shift + +case "$cmd" in + add) + [[ $# -eq 2 ]] || fail "add 需要2个参数 " + write_config "$1" "$2" + ;; + del) + [[ $# -eq 2 ]] || fail "del 需要2个参数 " + delete_config "$1" "$2" + ;; + mcp-status) + [[ $# -eq 1 ]] || fail "mcp-status 需要1个参数 " + query_mcp_in_all_apps "$1" + ;; + help|*) + usage + ;; +esac + diff --git a/backend/sync_files.sh b/backend/sync_files.sh index 23f5aae01131cd87a1b508ee062e6ba1a6bfe588..a905ee4bdc065e55613dfbec32af273d26fae007 100644 --- a/backend/sync_files.sh +++ b/backend/sync_files.sh @@ -31,6 +31,6 @@ cp -rf configs/* /etc/dev-store rm -rf /var/lib/dev-store/* [ ! -d /var/lib/dev-store/src ] && mkdir -p /var/lib/dev-store/src rm -rf /var/lib/dev-store/src/* -cp -rf artifacts tasks constants dev_store utils manage.py /var/lib/dev-store/src +cp -rf artifacts tasks constants dev_store utils manage.py mcp_manage.sh /var/lib/dev-store/src cp -rf services /var/lib/dev-store echo "success $(date "+%Y-%m-%d %H:%M:%S")" diff --git a/backend/utils/mcp_tools.py b/backend/utils/mcp_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..88aac5e8b02f6f2c0405986c6a179255b0127bd2 --- /dev/null +++ b/backend/utils/mcp_tools.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2025 Huawei Technologies Co., Ltd. +# oeDeploy is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# Create: 2025-08-14 +# ====================================================================================================================== +import json +from typing import Dict, Any +from rest_framework import status +from utils.cmd_executor import CommandExecutor + + +def get_mcp_status_in_apps(package_name: str) -> Dict[str, Any]: + """获取指定MCP在所有智能体应用中的配置状态""" + try: + cmd = ["/var/lib/dev-store/src/mcp_manage.sh", "mcp-status", package_name] + + executor = CommandExecutor(cmd, timeout=30) + stdout, stderr, returncode = executor.run() + if returncode == 0: # 成功执行 + stdout = stdout.strip() if stdout else "" + app_status_list = json.loads(stdout) + return { + "is_success": True, + "data": app_status_list, + "message": "Query successful", + "status_code": status.HTTP_200_OK + } + else: # 执行失败 + error_msg = stderr or stdout or "Query failed" + return { + "is_success": False, + "data": [], + "message": error_msg, + "status_code": status.HTTP_400_BAD_REQUEST + } + + except Exception as e: + return { + "is_success": False, + "data": [], + "message": f"Query error: {str(e)}", + "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR + } + + +def manage_mcp_config(action: str, package_name: str, app_name: str) -> Dict[str, Any]: + """管理MCP配置(添加或删除)""" + if action not in ["add", "delete"]: + return { + "is_success": False, + "message": "Invalid action type", + "status_code": status.HTTP_400_BAD_REQUEST + } + + try: + script_cmd = "add" if action == "add" else "del" + cmd = ["/var/lib/dev-store/src/mcp_manage.sh", script_cmd, package_name, app_name] + executor = CommandExecutor(cmd, timeout=30) + stdout, stderr, returncode = executor.run() + + if returncode == 0: # 成功执行 + action_msg = "added" if action == "add" else "deleted" + return { + "is_success": True, + "message": f"MCP config {action_msg} successfully", + "status_code": status.HTTP_200_OK + } + else: # 执行失败 + error_msg = stderr or stdout or "Operation failed" + return { + "is_success": False, + "message": error_msg, + "status_code": status.HTTP_400_BAD_REQUEST + } + + except Exception as e: + return { + "is_success": False, + "message": f"Operation error: {str(e)}", + "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR + }