diff --git a/servers/oeDeploy/README.md b/servers/oeDeploy/README.md deleted file mode 100644 index 2cafcefd7fc2b4997fda8f27fa6f6da36523ef3b..0000000000000000000000000000000000000000 --- a/servers/oeDeploy/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# oeDeploy MCP Server 使用说明 - -oeDeploy 目前提供了两个 MCP Server - -1. `mcp-oedp`:提供 oedp 基本命令的 MCP 调用 -2. `mcp-make-oedp-plugin`:自动完成一个 oeDeploy 插件开发 - -## 1. 环境准备 - -安装 python 依赖。为了更加直观,当前示例使用 `pip` 安装到系统的 python 目录,实际上更加推荐 `uv` 安装到虚拟环境。 - -````bash -pip install pydantic mcp requests --trusted-host mirrors.huaweicloud.com -i https://mirrors.huaweicloud.com/repository/pypi/simple -```` - -部署 oeDeploy MCP Server 所需文件,将 src 目录下的文件拷贝到 ~/.oedp/mcp/ - -```bash -mkdir -p ~/.oedp/mcp/ -cp servers/oeDeploy/src/* ~/.oedp/mcp/ -``` - -## 2. MCP 配置 - -当前示例使用 VScode 开发平台,用 Remote ssh 连接到一个 openEuler24.03sp1 的 linux 环境。 - -在插件 Roo Code中配置了 DeepSeek-V3 的API。 - -打开 MCP 配置页面,编辑 MCP 配置文件 `mcp_settings.json`,在 `mcpServers` 中新增如下内容: - -````json -{ - "mcpServers": { - - // ... - - "mcp-oedp": { - "command": "python3", - "args": [ - "/root/.oedp/mcp/mcp-oedp.py" - ], - "disabled": false, - "alwaysAllow": [] - }, - "mcp-make-oedp-plugin": { - "command": "python3", - "args": [ - "/root/.oedp/mcp/mcp-make-oedp-plugin.py" - ], - "disabled": false, - "alwaysAllow": [] - } - } -} -```` - -配置完成后,可以在 MCP 列表上看看到 `mcp-oedp` 和 `mcp-make-oedp-plugin`,且状态正常。 - -> 如果出现报错,请根据提示信息检查 python 组件依赖是否满足。 - -> 为了提升大模型对 MCP 服务的调用准确度,可以在 Prompt 中补充一句话: -> -> 请仔细阅读我的要求,优先考虑使用MCP解决问题,并选择最匹配的MCP tool函数。如果tool函数返回了一个新的指令字符串,请根据新的指令执行。 - -## 3. 自动编写一个 oeDeploy 插件 - -当前示例中,让 AI 帮我们自动写一个 oeDeploy 插件,用于实现 nginx 的软件安装与服务启动。 - -将如下内容拷贝到 Roo Code 对话框,并发送。为了使结果符合预期,我们应当尽可能准确、详细地描述插件的内容。 - -```` -在~/.oedp/目录下开发一个oeDeploy插件nginx。 -插件功能:在单个节点上安装nginx服务并启动。 -详细说明: -1. oeDeploy插件配置文件中仅配置单个节点,IP为127.0.0.1,用户名root,密码openEuler@123 -2. 当用户执行oedp run install时,在目标节点上,用yum安装nginx,然后启动nginx服务,设置默认启动 -3. nginx的端口号(默认80)在oeDeploy插件配置文件中可以配置 -```` - -> 在执行过程中,Roo Code 会多次调用 MCP 或者执行命令行,部分操作会征求用户的许可。我们也可以将这些行为设置为默认允许。 - -这一过程并不要求我们掌握 oeDeploy 的插件规则,MCP 会让大模型阅读代码仓中的开发文档,并自动完成插件开发。 - -运行结束后,我们会得到一个 nginx 目录,其中有一些 yaml 文件与脚本,这就是一个可以执行的 oeDeploy 插件。 - -## 4. 自动实现一键部署 - -oeDeploy 插件开发完成后,接下来我们让 AI 帮我们完成 nginx 的一键部署。 - -Roo Code 中新建对话框,发送如下指令: - -``` -用oeDeploy,在192.168.0.16节点(用户名root,密码openEuler@123)上一键部署nginx -``` - -> 节点信息请根据实际情况填写,可以是远端地址,也可以是127.0.0.1 - -MCP 会让大模型自动帮我们完成 oeDeploy 插件的初始化、参数配置,以及最终的部署操作。 diff --git a/servers/oeDeploy/mcp_config.json b/servers/oeDeploy/mcp_config.json deleted file mode 100644 index 7421f4d766d29935468c55f6bd6ec52403b16038..0000000000000000000000000000000000000000 --- a/servers/oeDeploy/mcp_config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "mcpServers": { - "mcp-oedp": { - "command": "python3", - "args": [ - "/root/.oedp/mcp/mcp-oedp.py" - ], - "disabled": false, - "alwaysAllow": [] - }, - "mcp-make-oedp-plugin": { - "command": "python3", - "args": [ - "/root/.oedp/mcp/mcp-make-oedp-plugin.py" - ], - "disabled": false, - "alwaysAllow": [] - } - } -} - \ No newline at end of file diff --git a/servers/oeDeploy/src/DevelopGuide.md b/servers/oeDeploy/src/DevelopGuide.md deleted file mode 100644 index e7a7dc8320d9cfa7878f99453d7e66fa7e1e56c0..0000000000000000000000000000000000000000 --- a/servers/oeDeploy/src/DevelopGuide.md +++ /dev/null @@ -1,140 +0,0 @@ -# oeDeploy 插件开发指南 - -## 1 oeDeploy 插件介绍 - -oeDeploy 插件(plugin)是 oedp 工具中提供自动化部署能力的组件,将复杂的部署流程 ansible 化来实现自动化部署。插件中可能会集成多种部署操作(action),例如安装、卸载、环境清理等,每一个部署操作都会对应一个或多个 ansible playbook。插件的所有可配置项都应当集中配置,以降低使用者的学习成本和开发者的维护成本。 - -## 2 插件目录结构 - -oeDeploy 插件目录名称,即插件名称,可以包含版本号(表示软件本身的版本,而非插件的版本) - -插件目录下包含如下内容: - -| 文件或者目录名 | 类型 | 介绍 | -| -------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| config.yaml | yaml | 对用户暴露的唯一配置文件。包含主机相关的配置,如 ip、密码、密钥、端口号等,还包含了软件部署相关的配置项。 | -| main.yaml | yaml | 包含了插件的各种信息,例如名称、版本号、描述,也包含了每个部署操作(action)执行时的具体步骤,每个步骤都对应一个脚本或者playbook(以workspace为根目录)。 | -| doc | 目录 | 承载插件相关的用户文档。该目录非必需。 | -| workspace | 目录 | 承载了软件安装部署所使用的所有文件、源码、二进制、脚本、playbook等等。目录内结构不做限制。 | - -## 3 main.yaml - -`main.yaml`主要用于记录插件的关键信息,包括:名称(`name`)、版本(`version`)、介绍(`description`)、操作(`action`)等,当用户执行 `oedp info` 命令时,`oedp` 工具会读取并解析该文件,然后向用户展示其详细信息。 - -- `action` 字段是一个字典(key-value map),每一个 key 将作为操作的名称。在下方的案例中,用户可以通过 `oedp run install`、`oedp run delete`、`oedp run clean` 命令行来触发对应的执行步骤(`tasks`)。 -- 每一个具体操作中,`description` 项是该操作的说明,用于在执行 `oedp info` 命令时向用户展示;而 `tasks` 项中则记录该操作的具体步骤,为一个列表,执行该操作时,将按顺序执行每一项步骤。 -- 在每个步骤中,开发者应当指定该步骤需要执行的 `playbook` 的路径,也可以在 `vars` 中指定变量文件的路径,`vars` 字段不是必需的。这里所填写的路径都是 `workspace` 目录的相对路径。此外,可以指定 `scope`,即该步骤需要执行的主机组,默认为 all。 -- 在下方的案例中,当用户执行了 `oedp run install` 命令,工具会按顺序执行如下命令: - `ansible-playbook set-env.yml -i config.yaml -e variables.yml --limit all` - `ansible-playbook init-k8s.yml -i config.yaml -e variables.yml --limit all` - 这里的 `config.yaml` 会在下文介绍。 - -```yaml -name: kubernetes -version: 1.31.1 -description: install kubernetes 1.31.1 -action: - install: - description: install kubernetes - tasks: - - name: prepare for install - playbook: set-env.yml - scope: all - - name: install kubernetes - playbook: init-k8s.yml - vars: variables.yml - scope: all - delete: - description: delete kubernetes - tasks: - - name: delete kubernetes - playbook: delete-k8s.yml - scope: all - clean: - description: clean cache files - tasks: - - name: clean cache files - playbook: clean-k8s.yml - scope: all -``` - -## 4 config.yaml - -`config.yaml `是插件中的用户配置文件,主要包含主机组的节点信息,以及部署软件相关的信息,遵循 ansible 的 inventory 文件配置规则,工具在执行 ansible playbook 时直接作为 inventory 传入。 - -需要注意,`config.yaml` 是对普通用户暴露的唯一配置文件,应当注意美观,避免歧义,并保留适当的注释说明。 - -```yaml -all: - children: - masters: - hosts: - 172.27.76.114: # master node IP - ansible_host: 172.27.76.114 # master node IP - ansible_port: 22 - ansible_user: root - ansible_password: PASSWORD - architecture: amd64 # amd64 or arm64 - oeversion: 24.03-LTS # 22.03-LTS or 24.03-LTS - workers: - hosts: - 172.27.70.60: # worker node IP - ansible_host: 172.27.70.60 # worker node IP - ansible_port: 22 - ansible_user: root - ansible_password: PASSWORD - architecture: amd64 - oeversion: 24.03-LTS - 172.27.72.90: # worker node IP - ansible_host: 172.27.72.90 # worker node IP - ansible_port: 22 - ansible_user: root - ansible_password: PASSWORD - architecture: amd64 - oeversion: 24.03-LTS - vars: - init_cluster_force: "true" - service_cidr: 10.96.0.0/16 - pod_cidr: 10.244.0.0/16 - certs_expired: 3650 - has_deployed_containerd: "false" - ansible_ssh_common_args: "-o StrictHostKeyChecking=no" -``` - -## 5 workspace - -`workspace` 目录中承载了部署脚本的主体内容,目录结构不做限制,需要与 `main.yaml `和 `config.yaml `中的各参数对应。 - -`workspace` 目录视为整个插件执行过程中的根目录。 - -当前示例中,`workspace`中的目录结构为: - -``` -workspace -|-- roles -| `-- ... -|-- clean-k8s.yml -|-- delete-k8s.yml -|-- init-k8s.yml -|-- set-env.yml -|-- variables.yml -`-- ... -``` - -## 6 插件打包 - -如果要发布插件供用户使用,需要将插件打包成 `{plugin_name}.tar.gz` 的格式,压缩包的名称和包内目录的名称要对应,且解压后应当仅生成一个目录,例如: - -``` -kubernetes-1.31.1.tar.gz -`-- kubernetes-1.31.1 - |-- main.yaml - |-- config.yaml - `-- workspace/ -``` - -当前示例中,使用如下命令行完成打包: - -````bash -tar zcvf kubernetes-1.31.1.tar.gz kubernetes-1.31.1/ -```` diff --git a/servers/oeDeploy/src/mcp-make-oedp-plugin.py b/servers/oeDeploy/src/mcp-make-oedp-plugin.py deleted file mode 100644 index b504e7d014ca6941a3713b25d40f73ee3a41b612..0000000000000000000000000000000000000000 --- a/servers/oeDeploy/src/mcp-make-oedp-plugin.py +++ /dev/null @@ -1,41 +0,0 @@ -from mcp.server.fastmcp import FastMCP -import requests -import subprocess -import os - -DEFAULT_DIR = "~/.oedp/" -DEVELOP_GUIDE_PATH = "~/.oedp/mcp/DevelopGuide.md" - -# Initialize FastMCP server -mcp = FastMCP("编写一个oeDeploy插件/oedp插件", log_level="ERROR") - -@mcp.tool() -async def make_plugin_auto(plugin: str, parent_dir: str) -> str: - """编写一个插件 - - Args: - plugin: oeDeploy插件名称,表示安装的软件或者需要执行的操作简称 - parent_dir: 插件初始化的路径,如果路径不存在,则创建;如果用户未指定,默认为~/.oedp/ - Returns: - str: 下一步需要执行的指令 - """ - - # 检查父路径是否存在并转换为绝对路径 - if parent_dir and os.path.exists(parent_dir): - parent_dir = os.path.abspath(os.path.expanduser(parent_dir)) - else: - parent_dir = os.path.abspath(os.path.expanduser(DEFAULT_DIR)) - - develop_guide_path = os.path.abspath(os.path.expanduser(DEVELOP_GUIDE_PATH)) - - return f"""编写一个oeDeploy插件分为多个步骤: -1. (重要!)仔细阅读oeDeploy插件的开发文档{develop_guide_path} -2. 根据用户对eDeploy插件功能的详细描述,完成对oeDeploy插件的开发 -3. 开发完成后,用指定方式打包成tar.gz到{parent_dir}目录下(如果有同名文件则强制覆盖) -4. 再调用`oedp info -p {parent_dir}/{plugin}`进行测试 -5. 测试通过后,不用执行oedp部署操作 -""" - -if __name__ == "__main__": - # Initialize and run the server - mcp.run(transport='stdio') \ No newline at end of file diff --git a/servers/oeDeploy_mcp/mcp-rpm.yaml b/servers/oeDeploy_mcp/mcp-rpm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..de080f1c4317daa3eae518bbd26f6babc9762ebc --- /dev/null +++ b/servers/oeDeploy_mcp/mcp-rpm.yaml @@ -0,0 +1,20 @@ +name: "oeDeploy" +summary: "MCP server for oeDeploy" +description: | + Provides MCP tools for oeDeploy. + +dependencies: + system: + - python3 + - uv + - python3-mcp + packages: + - oedp + +files: + required: + - mcp_config.json + - src/mcp-oedp.py + optional: + - src/requirements.txt + - src/pyproject.toml \ No newline at end of file diff --git a/servers/oeDeploy_mcp/mcp_config.json b/servers/oeDeploy_mcp/mcp_config.json new file mode 100644 index 0000000000000000000000000000000000000000..5dfd66fc57bf7cd8641f6bde3faa80ee17218e43 --- /dev/null +++ b/servers/oeDeploy_mcp/mcp_config.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "mcp-oedp": { + "command": "uv", + "args": [ + "--directory", "/opt/mcp-servers/servers/oeDeploy_mcp/src", + "run", "mcp-oedp.py", + "--model_url", "https://api.deepseek.com", + "--api_key", "", + "--model_name", "deepseek-chat" + ], + "disabled": false, + "timeout": 1800 + } + } +} \ No newline at end of file diff --git a/servers/oeDeploy/src/mcp-oedp.py b/servers/oeDeploy_mcp/src/mcp-oedp.py similarity index 34% rename from servers/oeDeploy/src/mcp-oedp.py rename to servers/oeDeploy_mcp/src/mcp-oedp.py index 3010e23ad59372a7c7589659cdfbba6af58945ff..cfe0c4c65a96d14aeb18ab531faf6eb56f276863 100644 --- a/servers/oeDeploy/src/mcp-oedp.py +++ b/servers/oeDeploy_mcp/src/mcp-oedp.py @@ -1,14 +1,111 @@ -from mcp.server.fastmcp import FastMCP -import requests -import subprocess +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2024 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-05-13 +# ====================================================================================================================== + +import argparse +import json import os +import subprocess +import yaml +from openai import OpenAI +from mcp.server.fastmcp import FastMCP +# 全局配置 DEFAULT_DIR = "~/.oedp/" +DOWNLOAD_TIMEOUT = 300 # 默认超时时间(秒) +DOWNLOAD_RETRIES = 12 # 默认重试次数 +LATEST_OEDP_PATH = "https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/.latest_oedp" +SYSTEM_CONTENT = """你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解. +你擅长解决技术问题,并具有优秀的逻辑思维能力.请在这个角色下为我解答以下问题.openEuler是我默认的Linux开发环境.""" + +model_url = "" +model_api_key = "" +model_name = "" # Initialize FastMCP server mcp = FastMCP("安装部署命令行工具oedp调用方法", log_level="ERROR") -def validate_project_structure(project: str) -> str: +def _call_llm(content: str) -> str: + try: + client = OpenAI(api_key=model_api_key, base_url=model_url) + response = client.chat.completions.create( + model=model_name, + messages=[ + {"role": "system", + "content": SYSTEM_CONTENT}, + {"role": "user", + "content": content} + ] + ) + return response.choices[0].message.content + except Exception: + return None + +async def _download_file(url: str, save_path: str, timeout: int = None, max_retries: int = None) -> str: + """下载文件并支持断点续传 + + Args: + url: 下载URL + save_path: 文件保存路径 + timeout: 超时时间(秒),默认使用全局配置 + max_retries: 最大重试次数,默认使用全局配置 + + Returns: + str: 成功返回"[Success]",失败返回错误信息 + """ + timeout = timeout or DOWNLOAD_TIMEOUT + max_retries = max_retries or DOWNLOAD_RETRIES + temp_path = save_path + ".download" + + # 构建curl命令 + curl_cmd = [ + "curl", + "-fL", # 失败时不显示HTML错误页面,跟随重定向 + "-C", "-", # 自动断点续传 + "--max-time", str(timeout), # 设置超时时间 + "--retry", str(max_retries), # 设置重试次数 + "--retry-delay", "2", # 设置重试间隔(秒) + "--output", temp_path, # 输出到临时文件 + url + ] + + for attempt in range(max_retries): + try: + # 执行curl命令 + result = subprocess.run( + curl_cmd, + capture_output=True, + text=True + ) + + if result.returncode == 0: + # 下载完成后重命名临时文件 + os.rename(temp_path, save_path) + return "[Success]" + else: + if attempt == max_retries - 1: + return f"[Fail]Download failed after {max_retries} attempts: {result.stderr}" + + except subprocess.CalledProcessError as e: + if attempt == max_retries - 1: + return f"[Fail]Download failed after {max_retries} attempts: {str(e)}" + except Exception as e: + if attempt == max_retries - 1: + return f"[Fail]Download failed after {max_retries} attempts: {str(e)}" + + return f"[Fail]Download failed after {max_retries} attempts" + +def _validate_project_structure(project: str) -> str: """校验项目目录结构 Args: @@ -24,7 +121,7 @@ def validate_project_structure(project: str) -> str: return f"Missing required file/directory: {f}" return "" -def check_oedp_installed() -> str: +def _check_oedp_installed() -> str: """检查oedp是否安装 Returns: @@ -39,65 +136,30 @@ def check_oedp_installed() -> str: return "oedp is not installed or not in PATH" return "" -async def _extract_plugin(tar_path: str, parent_dir: str) -> str: - """解压插件包到目标目录 - - Args: - tar_path: tar.gz文件路径 - parent_dir: 目标父目录 - Returns: - str: 成功或错误信息 - """ - try: - plugin_name = os.path.basename(tar_path).replace(".tar.gz", "") - plugin_dir = os.path.join(parent_dir, plugin_name) - - # 强制删除已存在的目录 - if os.path.exists(plugin_dir): - import shutil - shutil.rmtree(plugin_dir) - - # 解压到目标路径 - result = subprocess.run( - ["tar", "zxvf", tar_path, "-C", parent_dir], - capture_output=True, - text=True - ) - - # 清理临时文件 - if os.path.abspath(tar_path).startswith("/tmp/"): - os.remove(tar_path) - - if result.returncode == 0: - return "[Success]" - return f"[Fail]Extraction failed: {result.stderr}" - - except subprocess.CalledProcessError as e: - return f"[Fail]Tar command failed: {str(e)}" - except OSError as e: - return f"[Fail]Directory operation failed: {str(e)}" - except Exception as e: - return f"[Fail]Unexpected error: {str(e)}" - @mcp.tool() async def install_oedp() -> str: """下载并安装noarch架构的oedp软件包(oeDeploy的命令行工具) """ try: - # 构建下载URL - url = "https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/noarch/oedp-1.0.2-1.oe2503.noarch.rpm" - package_name = "oedp-1.0.2-1.oe2503.noarch.rpm" + # 下载最新版本信息文件 + latest_info_path = os.path.abspath("/tmp/latest_oedp") + download_result = await _download_file(LATEST_OEDP_PATH, latest_info_path) + if download_result != "[Success]": + return download_result + + # 读取下载URL + with open(latest_info_path, 'r') as f: + url = f.read().strip() + + # 从URL中提取包名 + package_name = os.path.basename(url) + temp_file = os.path.abspath(f"/tmp/{package_name}") # 下载RPM包 - response = requests.get(url, stream=True) - response.raise_for_status() - - # 保存到临时文件 - temp_file = os.path.abspath(f"/tmp/{package_name}") - with open(temp_file, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - + download_result = await _download_file(url, temp_file) + if download_result != "[Success]": + return download_result + # 安装RPM包 result = subprocess.run( ["sudo", "yum", "install", "-y", temp_file], @@ -106,15 +168,14 @@ async def install_oedp() -> str: ) # 清理临时文件 + os.remove(latest_info_path) os.remove(temp_file) if result.returncode == 0: - return "[Success]" + return "[Success] then excute cmd: oedp repo update" else: return f"[Fail]Installation failed: {result.stderr}" - except requests.exceptions.RequestException as e: - return f"[Fail]Download failed: {str(e)}" except subprocess.CalledProcessError as e: return f"[Fail]Yum command failed: {str(e)}" except Exception as e: @@ -144,104 +205,178 @@ async def remove_oedp() -> str: @mcp.tool() async def oedp_init_plugin(plugin: str, parent_dir: str) -> str: - """获取已经存在的oeDeploy插件(又称oedp插件),并初始化到{parent_dir}目录 + """获取的oeDeploy插件(又称oedp插件),并初始化 Args: plugin: oeDeploy插件名称或.tar.gz文件路径/名称 parent_dir: 插件初始化的路径,如果路径不存在,则创建 """ try: - # 检查父路径是否存在并转换为绝对路径 - if parent_dir and os.path.exists(parent_dir): - parent_dir = os.path.abspath(os.path.expanduser(parent_dir)) - else: - parent_dir = os.path.abspath(os.path.expanduser(DEFAULT_DIR)) + # 检查oedp是否安装 + oedp_check_result = _check_oedp_installed() + if oedp_check_result: + return f"[Fail]{oedp_check_result}" + + # 确保父目录存在 + abs_parent_dir = os.path.abspath(os.path.expanduser(parent_dir)) + os.makedirs(abs_parent_dir, exist_ok=True) - # 转换为绝对路径并确保以/结尾 - parent_dir = os.path.abspath(os.path.expanduser(parent_dir)) - parent_dir = parent_dir if parent_dir.endswith("/") else parent_dir + "/" - os.makedirs(parent_dir, exist_ok=True) + # 执行初始化命令 + result = subprocess.run( + ["oedp", "init", plugin, "-d", abs_parent_dir, "-f"], + capture_output=True, + text=True + ) - # 情况1:plugin是.tar.gz文件路径 - if plugin.endswith(".tar.gz"): - abs_path = os.path.abspath(os.path.expanduser(plugin)) - if os.path.exists(abs_path): - return await _extract_plugin(abs_path, parent_dir) - return f"[Fail]tar.gz file not found: {abs_path}" + log_text = result.stdout + "\n" + result.stderr - # 情况2:plugin是.tar.gz文件名 - if plugin.endswith(".tar.gz"): - # 先在parent_dir目录查找 - local_path = os.path.join(parent_dir, plugin) - if os.path.exists(local_path): - return await _extract_plugin(local_path, parent_dir) + if result.returncode == 0: + return "[Success]" + "\n" + log_text + else: + return f"[Fail]Initialization failed" + "\n" + log_text - # 尝试从网络下载 - try: - base_url = "https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins" - url = f"{base_url}/{plugin}" - response = requests.get(url, stream=True) - response.raise_for_status() - - temp_path = os.path.abspath(f"/tmp/{plugin}") - with open(temp_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - return await _extract_plugin(temp_path, parent_dir) - except requests.exceptions.RequestException as e: - return f"[Fail]Download failed: {str(e)}" - - # 情况3:plugin是插件名称 - plugin_dir = os.path.join(parent_dir, plugin) - package_name = f"{plugin}.tar.gz" + except subprocess.CalledProcessError as e: + return f"[Fail]Command execution failed: {str(e)}" + except Exception as e: + return f"[Fail]Unexpected error: {str(e)}" + +@mcp.tool() +async def oedp_info_plugin(project: str) -> str: + """查询oeDeploy插件(又称oedp插件)信息,仅在明确指定project路径时触发 + + Args: + project: oeDeploy插件的项目目录, 其中必定有config.yaml,main.yaml,workspace/ + """ + + # 校验项目目录结构 + abs_project = os.path.abspath(os.path.expanduser(project)) + validation_result = _validate_project_structure(abs_project) + if validation_result: + return f"[Fail]{validation_result}" + + # 检查oedp是否安装 + oedp_check_result = _check_oedp_installed() + if oedp_check_result: + return f"[Fail]{oedp_check_result}" + + # 执行安装命令 + try: + result = subprocess.run( + ["oedp", "info", "-p", abs_project], + capture_output=True, + text=True + ) - # 优先尝试从本地获取插件包 - local_package = os.path.join(parent_dir, package_name) - if os.path.exists(local_package): - return await _extract_plugin(local_package, parent_dir) + log_text = result.stdout + "\n" + result.stderr - # 尝试从网络下载 - try: - base_url = "https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins" - url = f"{base_url}/{package_name}" - response = requests.get(url, stream=True) - response.raise_for_status() - - temp_path = os.path.abspath(f"/tmp/{package_name}") - with open(temp_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) + if result.returncode == 0: + return "[Success]" + "\n" + log_text + else: + return f"[Fail]Installation failed" + "\n" + log_text - return await _extract_plugin(temp_path, parent_dir) - except requests.exceptions.RequestException: - # 所有获取方式都失败,检查现有目录 - if os.path.exists(plugin_dir): - validation_result = validate_project_structure(plugin_dir) - if not validation_result: - return "[Success] (Using existing valid plugin directory)" - return f"[Fail]Existing directory is invalid: {validation_result}" - return f"[Fail]Failed to get plugin package and no existing directory" - + except subprocess.CalledProcessError as e: + return f"[Fail]Command execution failed: {str(e)}" except Exception as e: return f"[Fail]Unexpected error: {str(e)}" @mcp.tool() -async def oedp_setup_plugin(operation: str, project: str) -> str: - """配置oeDeploy插件(又称oedp插件): 根据operation, 修改oeDeploy插件的配置文件{project}/config.yaml +async def oedp_setup_plugin(description: str, project: str) -> str: + """配置oeDeploy插件(又称oedp插件): description,修改oeDeploy插件的配置文件{project}/config.yaml Args: - operation: 用户对oeDeploy插件config.yaml的修改说明(人类描述语言) - project: oeDeploy插件的项目目录, 其中必定有config.yaml,main.yaml,workspace/ + description: 用户对oeDeploy插件config.yaml的修改说明(人类描述语言) + project: oeDeploy插件的项目目录,其中必定有config.yaml,main.yaml,workspace/ """ # 校验项目目录结构 abs_project = os.path.abspath(os.path.expanduser(project)) - validation_result = validate_project_structure(abs_project) + validation_result = _validate_project_structure(abs_project) if validation_result: return f"[Fail]{validation_result}" - return f"请根据用户的指令'{operation}', 修改{abs_project}/config.yaml" + prompt = """ + yaml_content字段中的内容来自一个待修改的yaml文件,请根据description字段中的内容,对yaml_content进行修改. + 要求: 返回的内容中只包含修改后的yaml文本字符串,禁止包含其他任何东西,保留原来yaml文本中的注释信息. + """ + + try: + # 读取config.yaml文件内容 + config_path = os.path.join(abs_project, "config.yaml") + with open(config_path, 'r', encoding='utf-8') as f: + yaml_content = f.read() + + # 构建LLM输入 + input_json = { + "yaml_content": yaml_content, + "description": description, + "prompt": prompt.strip() + } + input_str = json.dumps(input_json, ensure_ascii=False) + + # 调用LLM获取修改后的YAML + output = _call_llm(input_str) + if not output: + return "[Fail]Failed to get response from LLM, make sure LLM api available" + + # 验证YAML格式 + try: + yaml.safe_load(output) + except yaml.YAMLError: + return "[Fail]Invalid YAML format returned by LLM" + + # 备份原配置文件 + backup_path = config_path + ".bak" + if os.path.exists(backup_path): + os.remove(backup_path) + os.rename(config_path, backup_path) + + # 写入修改后的内容 + with open(config_path, 'w', encoding='utf-8') as f: + f.write(output) + + return "[Success]Config updated successfully" + + except Exception as e: + return f"[Fail]Unexpected error: {str(e)}" + +@mcp.tool() +async def oedp_run_action_plugin(action: str, project: str) -> str: + """运行oeDeploy插件(又称oedp插件)的特定操作action,仅在明确指定project路径时触发 + + Args: + action: oeDeploy插件的一个操作名称 + project: oeDeploy插件的项目目录, 其中必定有config.yaml,main.yaml,workspace/ + """ + try: + # 校验项目目录结构 + abs_project = os.path.abspath(os.path.expanduser(project)) + validation_result = _validate_project_structure(abs_project) + if validation_result: + return f"[Fail]{validation_result}" + + # 检查oedp是否安装 + oedp_check_result = _check_oedp_installed() + if oedp_check_result: + return f"[Fail]{oedp_check_result}" + + # 执行命令 + result = subprocess.run( + ["oedp", "run", action, "-p", abs_project], + capture_output=True, + text=True + ) + + log_text = result.stdout + "\n" + result.stderr + + if result.returncode == 0: + return "[Success]" + "\n" + log_text + else: + return f"[Fail]Action execution failed" + "\n" + log_text + + except subprocess.CalledProcessError as e: + return f"[Fail]Command execution failed: {str(e)}" + except Exception as e: + return f"[Fail]Unexpected error: {str(e)}" @mcp.tool() async def oedp_run_install_plugin(project: str) -> str: @@ -253,16 +388,16 @@ async def oedp_run_install_plugin(project: str) -> str: try: # 校验项目目录结构 abs_project = os.path.abspath(os.path.expanduser(project)) - validation_result = validate_project_structure(abs_project) + validation_result = _validate_project_structure(abs_project) if validation_result: return f"[Fail]{validation_result}" # 检查oedp是否安装 - oedp_check_result = check_oedp_installed() + oedp_check_result = _check_oedp_installed() if oedp_check_result: return f"[Fail]{oedp_check_result}" - # 执行安装命令 + # 执行命令 result = subprocess.run( ["oedp", "run", "install", "-p", abs_project], capture_output=True, @@ -283,7 +418,7 @@ async def oedp_run_install_plugin(project: str) -> str: @mcp.tool() async def oedp_run_uninstall_plugin(project: str) -> str: - """运行oeDeploy插件(又称oedp插件)的卸载流程 + """运行oeDeploy插件(又称oedp插件)的卸载流程,仅在明确指定project路径时触发 Args: project: oeDeploy插件的项目目录, 其中必定有config.yaml,main.yaml,workspace/ @@ -291,16 +426,16 @@ async def oedp_run_uninstall_plugin(project: str) -> str: try: # 校验项目目录结构 abs_project = os.path.abspath(os.path.expanduser(project)) - validation_result = validate_project_structure(abs_project) + validation_result = _validate_project_structure(abs_project) if validation_result: return f"[Fail]{validation_result}" # 检查oedp是否安装 - oedp_check_result = check_oedp_installed() + oedp_check_result = _check_oedp_installed() if oedp_check_result: return f"[Fail]{oedp_check_result}" - # 执行安装命令 + # 执行命令 result = subprocess.run( ["oedp", "run", "uninstall", "-p", abs_project], capture_output=True, @@ -320,29 +455,53 @@ async def oedp_run_uninstall_plugin(project: str) -> str: return f"[Fail]Unexpected error: {str(e)}" @mcp.tool() -async def oedp_install_software_one_click(software: str, operation: str) -> str: - """用oeDeploy一键执行特定软件的部署流程(install),仅在用户指定[oeDeploy]与[一键部署]时触发 +async def oedp_install_software_one_click(software: str, description: str) -> str: + """用oeDeploy一键执行特定软件的部署流程install,仅在用户指定'oeDeploy'与'一键部署'时触发 Args: software: 软件名称,可以等价于插件名称 - operation: 用户对部署软件参数的描述,等价于对oeDeploy插件config.yaml的修改说明(人类描述语言) + description: 用户对部署软件参数的描述,等价于对oeDeploy插件config.yaml的修改说明(人类描述语言) Returns: - str: 下一步需要执行的指令 + str: 执行结果与信息 """ # 检查oedp是否安装 - oedp_check_result = check_oedp_installed() + oedp_check_result = _check_oedp_installed() if oedp_check_result: return f"[Fail]{oedp_check_result}" parent_dir = os.path.abspath(os.path.expanduser(DEFAULT_DIR)) + project_path = os.path.join(parent_dir, software) + + # 1. 初始化插件 + init_result = await oedp_init_plugin(software, parent_dir) + if not init_result.startswith("[Success]"): + return f"[Fail]Plugin initialization failed: {init_result}" - return f"""用oeDeploy一键执行特定软件的部署流程(install)分为多个步骤: -1. 调用oedp_init_plugin方法,将指定的oeDeploy插件[{software}]初始化到{parent_dir}目录 -2. 调用oedp_setup_plugin方法,根据用户的指令[{operation}],修改oeDeploy插件的配置文件{parent_dir}/{software}/config.yaml -3. 调用oedp_run_install_plugin方法,运行oeDeploy插件的安装部署流程 项目路径{parent_dir}/{software} -""" + # 2. 配置插件 + setup_result = await oedp_setup_plugin(description, project_path) + if not setup_result.startswith("[Success]"): + return f"[Fail]Plugin setup failed: {setup_result}" + + # 3. 运行安装 + install_result = await oedp_run_install_plugin(project_path) + if not install_result.startswith("[Success]"): + return f"[Fail]Installation failed: {install_result}" + + return "[Success]Software installed successfully" if __name__ == "__main__": + # Parse command line arguments + parser = argparse.ArgumentParser(description='oeDeploy MCP Server') + parser.add_argument('--model_url', required=True, help='Model url') + parser.add_argument('--api_key', required=True, help='API key for the vendor') + parser.add_argument('--model_name', required=True, help='Model name') + args = parser.parse_args() + + # Assign to global variables + model_url = args.model_url + model_api_key = args.api_key + model_name = args.model_name + # Initialize and run the server - mcp.run(transport='stdio') \ No newline at end of file + mcp.run(transport='stdio') diff --git a/servers/oeDeploy_mcp/src/pyproject.toml b/servers/oeDeploy_mcp/src/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..ba8d2ec92f19656d92b3fbcae9c35ba0722f778d --- /dev/null +++ b/servers/oeDeploy_mcp/src/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "mcp-oedp" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "mcp", + "openai", + "pyyaml" +] + +[[tool.uv.index]] +url = "https://mirrors.huaweicloud.com/repository/pypi/simple" +default = true diff --git a/servers/oeDeploy_mcp/src/requirements.txt b/servers/oeDeploy_mcp/src/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..03de40eca8e6ae2a760997ea3ecf69a676ffddcb --- /dev/null +++ b/servers/oeDeploy_mcp/src/requirements.txt @@ -0,0 +1,3 @@ +mcp +openai +pyyaml