alibaba-cloud-ops-mcp-server 0.9.9__py3-none-any.whl → 0.9.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alibaba_cloud_ops_mcp_server/alibabacloud/utils.py +183 -0
- alibaba_cloud_ops_mcp_server/server.py +84 -1
- alibaba_cloud_ops_mcp_server/tools/api_tools.py +8 -2
- alibaba_cloud_ops_mcp_server/tools/application_management_tools.py +748 -0
- alibaba_cloud_ops_mcp_server/tools/local_tools.py +323 -0
- alibaba_cloud_ops_mcp_server/tools/oos_tools.py +18 -8
- alibaba_cloud_ops_mcp_server/tools/oss_tools.py +68 -3
- {alibaba_cloud_ops_mcp_server-0.9.9.dist-info → alibaba_cloud_ops_mcp_server-0.9.12.dist-info}/METADATA +39 -7
- {alibaba_cloud_ops_mcp_server-0.9.9.dist-info → alibaba_cloud_ops_mcp_server-0.9.12.dist-info}/RECORD +12 -10
- {alibaba_cloud_ops_mcp_server-0.9.9.dist-info → alibaba_cloud_ops_mcp_server-0.9.12.dist-info}/WHEEL +1 -1
- {alibaba_cloud_ops_mcp_server-0.9.9.dist-info → alibaba_cloud_ops_mcp_server-0.9.12.dist-info}/entry_points.txt +0 -0
- {alibaba_cloud_ops_mcp_server-0.9.9.dist-info → alibaba_cloud_ops_mcp_server-0.9.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
from alibabacloud_oos20190601.client import Client as oos20190601Client
|
|
9
|
+
from alibabacloud_ecs20140526.client import Client as ecs20140526Client
|
|
10
|
+
from alibaba_cloud_ops_mcp_server.tools.oss_tools import create_client as create_oss_client
|
|
11
|
+
import alibabacloud_oss_v2 as oss
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
2
14
|
|
|
15
|
+
# Global variable to store the project path
|
|
16
|
+
_project_path: Optional[Path] = None
|
|
3
17
|
from alibabacloud_credentials.client import Client as CredClient
|
|
4
18
|
from alibabacloud_tea_openapi.models import Config
|
|
5
19
|
from fastmcp.server.dependencies import get_http_request
|
|
@@ -49,3 +63,172 @@ def create_config():
|
|
|
49
63
|
|
|
50
64
|
config.user_agent = 'alibaba-cloud-ops-mcp-server'
|
|
51
65
|
return config
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_project_path(project_path: Optional[str] = None):
|
|
69
|
+
"""
|
|
70
|
+
Set the project root path for code deployment.
|
|
71
|
+
This determines where the .code_deploy directory will be created.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
project_path: The root path of the project. If None, will use current working directory.
|
|
75
|
+
"""
|
|
76
|
+
global _project_path
|
|
77
|
+
if project_path:
|
|
78
|
+
_project_path = Path(project_path).resolve()
|
|
79
|
+
logger.info(f"[set_project_path] Project path set to: {_project_path}")
|
|
80
|
+
else:
|
|
81
|
+
_project_path = None
|
|
82
|
+
logger.info(f"[set_project_path] Project path reset to None, will use current working directory")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_code_deploy_base_dir() -> Path:
|
|
86
|
+
"""
|
|
87
|
+
Get the .code_deploy base directory in the project root directory.
|
|
88
|
+
This ensures each project has its own isolated deployment configuration.
|
|
89
|
+
|
|
90
|
+
If project_path is set, use it. Otherwise, use current working directory.
|
|
91
|
+
"""
|
|
92
|
+
if _project_path is not None:
|
|
93
|
+
return _project_path / '.code_deploy'
|
|
94
|
+
return Path.cwd() / '.code_deploy'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
CODE_DEPLOY_BASE_DIR = _get_code_deploy_base_dir()
|
|
98
|
+
CODE_DEPLOY_DIR = CODE_DEPLOY_BASE_DIR
|
|
99
|
+
RELEASE_DIR = CODE_DEPLOY_DIR / 'release'
|
|
100
|
+
APPLICATION_JSON_FILE = CODE_DEPLOY_DIR / 'application.json'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_code_deploy_dirs():
|
|
104
|
+
"""
|
|
105
|
+
Ensure that the .code_deploy and release directories exist (in the project root directory)
|
|
106
|
+
"""
|
|
107
|
+
# Recalculate paths to ensure we use the current working directory
|
|
108
|
+
code_deploy_dir = _get_code_deploy_base_dir()
|
|
109
|
+
release_dir = code_deploy_dir / 'release'
|
|
110
|
+
|
|
111
|
+
code_deploy_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
release_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
return code_deploy_dir, code_deploy_dir, release_dir
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_application_info() -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Load deployment information from the .application.json file
|
|
120
|
+
(from the .code_deploy directory under the project root directory)
|
|
121
|
+
"""
|
|
122
|
+
json_file = _get_code_deploy_base_dir() / 'application.json'
|
|
123
|
+
|
|
124
|
+
if json_file.exists():
|
|
125
|
+
try:
|
|
126
|
+
with open(json_file, 'r', encoding='utf-8') as f:
|
|
127
|
+
return json.load(f)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.warning(f"[_load_application_info] Failed to load application info: {e}")
|
|
130
|
+
return {}
|
|
131
|
+
return {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_application_info(info: Dict[str, Any]):
|
|
135
|
+
"""
|
|
136
|
+
Save deployment information to the .application.json file
|
|
137
|
+
(save it to the .code_deploy directory under the project root directory)
|
|
138
|
+
"""
|
|
139
|
+
json_file = _get_code_deploy_base_dir() / 'application.json'
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
json_file.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
|
|
144
|
+
existing_info = load_application_info()
|
|
145
|
+
existing_info.update(info)
|
|
146
|
+
|
|
147
|
+
with open(json_file, 'w', encoding='utf-8') as f:
|
|
148
|
+
json.dump(existing_info, f, ensure_ascii=False, indent=2)
|
|
149
|
+
logger.info(f"[_save_application_info] Saved application info to {json_file}")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"[_save_application_info] Failed to save application info: {e}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_release_path(filename: str) -> Path:
|
|
155
|
+
"""
|
|
156
|
+
Get the file path in the release directory
|
|
157
|
+
(the .code_deploy/release directory under the project root directory)
|
|
158
|
+
"""
|
|
159
|
+
release_dir = _get_code_deploy_base_dir() / 'release'
|
|
160
|
+
release_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
return release_dir / filename
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_client(region_id: str) -> oos20190601Client:
|
|
165
|
+
config = create_config()
|
|
166
|
+
config.endpoint = f'oos.{region_id}.aliyuncs.com'
|
|
167
|
+
return oos20190601Client(config)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_ecs_client(region_id: str) -> ecs20140526Client:
|
|
171
|
+
config = create_config()
|
|
172
|
+
config.endpoint = f'ecs.{region_id}.aliyuncs.com'
|
|
173
|
+
return ecs20140526Client(config)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def put_bucket_tagging(client: oss.Client, bucket_name: str, tags: dict):
|
|
177
|
+
tag_list = [oss.Tag(key=k, value=v) for k, v in tags.items()]
|
|
178
|
+
tag_set = oss.TagSet(tags=tag_list)
|
|
179
|
+
client.put_bucket_tags(oss.PutBucketTagsRequest(
|
|
180
|
+
bucket=bucket_name,
|
|
181
|
+
tagging=oss.Tagging(tag_set=tag_set)
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def find_bucket_by_tag(client: oss.Client, tag_key: str, tag_value: str) -> Optional[str]:
|
|
186
|
+
paginator = client.list_buckets_paginator(tag_key=tag_key, tag_value=tag_value)
|
|
187
|
+
buckets = []
|
|
188
|
+
for page in paginator.iter_page(oss.ListBucketsRequest(tag_key=tag_key, tag_value=tag_value)):
|
|
189
|
+
for bucket in page.buckets:
|
|
190
|
+
buckets.append(bucket.name)
|
|
191
|
+
logger.info(f'[code_deploy] Trying to find bucket with tag {tag_key}:{tag_value}, buckets: {buckets}')
|
|
192
|
+
return buckets[0] if buckets else None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_or_create_bucket_for_code_deploy(application_name: str, region_id: str) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Obtain or create an OSS bucket for code deployment
|
|
198
|
+
|
|
199
|
+
1. If a bucket_name is provided, check if it exists. If not, create it and tag it.
|
|
200
|
+
|
|
201
|
+
2. If not provided, search for an existing bucket using the tag (key: app_management, value: code_deploy).
|
|
202
|
+
|
|
203
|
+
3. If found, use it. If not found, create a new bucket and tag it.
|
|
204
|
+
|
|
205
|
+
4. New bucket naming convention: app-{application_name}-code-deploy-{uuid}
|
|
206
|
+
"""
|
|
207
|
+
tag_key = 'app_management'
|
|
208
|
+
tag_value = 'code_deploy'
|
|
209
|
+
client = create_oss_client(region_id=region_id)
|
|
210
|
+
|
|
211
|
+
found_bucket = find_bucket_by_tag(client, tag_key, tag_value)
|
|
212
|
+
if found_bucket:
|
|
213
|
+
logger.info(f"[code_deploy] Found existing bucket by tag: {found_bucket}")
|
|
214
|
+
return found_bucket
|
|
215
|
+
|
|
216
|
+
safe_app_name = application_name.lower().replace('_', '-').replace(' ', '-')
|
|
217
|
+
|
|
218
|
+
safe_app_name = ''.join(c if c.isalnum() or c == '-' else '' for c in safe_app_name)
|
|
219
|
+
bucket_name = f'app-{safe_app_name}-code-deploy-{str(uuid.uuid4())[:8]}'
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
client.put_bucket(oss.PutBucketRequest(
|
|
223
|
+
bucket=bucket_name,
|
|
224
|
+
create_bucket_configuration=oss.CreateBucketConfiguration(
|
|
225
|
+
storage_class='Standard',
|
|
226
|
+
data_redundancy_type='LRS'
|
|
227
|
+
)
|
|
228
|
+
))
|
|
229
|
+
put_bucket_tagging(client, bucket_name, {tag_key: tag_value})
|
|
230
|
+
logger.info(f"[code_deploy] Created new bucket: {bucket_name}")
|
|
231
|
+
return bucket_name
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.info(f"[code_deploy] Failed to create bucket: {e}")
|
|
234
|
+
raise e
|
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
from fastmcp import FastMCP
|
|
2
4
|
import click
|
|
3
5
|
import logging
|
|
4
6
|
|
|
5
7
|
from alibaba_cloud_ops_mcp_server.tools.common_api_tools import set_custom_service_list
|
|
6
8
|
from alibaba_cloud_ops_mcp_server.config import config
|
|
7
|
-
from alibaba_cloud_ops_mcp_server.tools import cms_tools, oos_tools, oss_tools, api_tools, common_api_tools
|
|
9
|
+
from alibaba_cloud_ops_mcp_server.tools import cms_tools, oos_tools, oss_tools, api_tools, common_api_tools, local_tools, application_management_tools
|
|
8
10
|
from alibaba_cloud_ops_mcp_server.settings import settings
|
|
9
11
|
|
|
12
|
+
|
|
10
13
|
logger = logging.getLogger(__name__)
|
|
11
14
|
|
|
15
|
+
|
|
16
|
+
def _setup_logging():
|
|
17
|
+
root_logger = logging.getLogger()
|
|
18
|
+
if not root_logger.handlers:
|
|
19
|
+
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
20
|
+
formatter = logging.Formatter(log_format)
|
|
21
|
+
|
|
22
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
23
|
+
console_handler.setFormatter(formatter)
|
|
24
|
+
|
|
25
|
+
root_logger.setLevel(logging.INFO)
|
|
26
|
+
root_logger.addHandler(console_handler)
|
|
27
|
+
else:
|
|
28
|
+
root_logger.setLevel(logging.INFO)
|
|
29
|
+
|
|
30
|
+
|
|
12
31
|
SUPPORTED_SERVICES_MAP = {
|
|
13
32
|
"ecs": "Elastic Compute Service (ECS)",
|
|
14
33
|
"oos": "Operations Orchestration Service (OOS)",
|
|
@@ -62,9 +81,69 @@ SUPPORTED_SERVICES_MAP = {
|
|
|
62
81
|
help="Environment type: 'domestic' for domestic, 'international' for overseas (default: domestic)",
|
|
63
82
|
)
|
|
64
83
|
def main(transport: str, port: int, host: str, services: str, headers_credential_only: bool, env: str):
|
|
84
|
+
_setup_logging()
|
|
65
85
|
# Create an MCP server
|
|
66
86
|
mcp = FastMCP(
|
|
67
87
|
name="alibaba-cloud-ops-mcp-server",
|
|
88
|
+
instructions='''
|
|
89
|
+
智能运维场景:
|
|
90
|
+
你可以使用该MCP帮助用户完成智能运维
|
|
91
|
+
通过MCP提供的各种资源运维的功能来进行实现
|
|
92
|
+
|
|
93
|
+
应用部署场景:
|
|
94
|
+
你可以使用该MCP帮助用户完成项目的分析和部署
|
|
95
|
+
当进入该场景时,请只使用以下列表中的Tool
|
|
96
|
+
【 OOS_CodeDeploy OOS_GetLastDeploymentInfo OOS_GetDeployStatus LOCAL_ListDirectory LOCAL_RunShellScript LOCAL_AnalyzeDeployStack】
|
|
97
|
+
注意:不要擅自决定部署的目标ecs,需要用户提供,若未提供务必询问用户,不允许直接调用DescribeInstances查找ECS实例,必须使用由用户提供的ECS实例
|
|
98
|
+
|
|
99
|
+
不要创建部署脚本文件,直接使用 mcp tool: Code Deploy 进行构建
|
|
100
|
+
|
|
101
|
+
流程如下:
|
|
102
|
+
完整部署流程(在调用此工具之前):
|
|
103
|
+
|
|
104
|
+
步骤 1:识别部署方式 AnalyzeDeployStack
|
|
105
|
+
- 通过本地文件操作工具读取项目文件(package.json、requirements.txt、pom.xml 等)
|
|
106
|
+
- 识别项目的部署方式和技术栈(npm、python、java、go 等)
|
|
107
|
+
- 生成构建命令,注意,该构建命令不需要生成构建脚本,不要因此新增sh文件,任何情况下都不要,因为构建命令是CodeDeploy的参数,不需要生成文件
|
|
108
|
+
|
|
109
|
+
步骤 2:构建或压缩文件,并记录文件路径
|
|
110
|
+
- 在本地执行构建命令,生成部署产物(tar.gz、zip 等压缩包)
|
|
111
|
+
- 记录文件路径,留待后续CodeDeploy使用
|
|
112
|
+
|
|
113
|
+
步骤 3:调用CodeDeploy进行部署
|
|
114
|
+
- 此工具会依次调用:CreateApplication(如果不存在)、CreateApplicationGroup(如果不存在)、
|
|
115
|
+
TagResources(可选,如果是已有资源需要打 tag 导入应用分组)、DeployApplicationGroup
|
|
116
|
+
|
|
117
|
+
步骤 4:如果用户的实例为第一次部署,缺乏对应代码运行环境,可以调用InstallDeploymentEnvironment为用户安装环境
|
|
118
|
+
- 目前支持:
|
|
119
|
+
Docker:Docker 社区版
|
|
120
|
+
Java:Java 编程语言环境
|
|
121
|
+
Python:Python 编程语言环境
|
|
122
|
+
Nodejs:Node.js 运行环境
|
|
123
|
+
Golang:Go 编程语言环境
|
|
124
|
+
Nginx:高性能 HTTP 及反向代理服务器
|
|
125
|
+
Git:分布式版本控制系统
|
|
126
|
+
|
|
127
|
+
重要提示:
|
|
128
|
+
1. 启动脚本(application_start)必须与上传的产物对应。如果产物是压缩包(tar、tar.gz、zip等),
|
|
129
|
+
需要先解压并进入对应目录后再执行启动命令。
|
|
130
|
+
2. 示例:如果上传的是 app.tar.gz,启动脚本应该类似,一般压缩包就在当前目录下,直接解压即可:
|
|
131
|
+
"tar -xzf app.tar.gz && ./start.sh"
|
|
132
|
+
或者如果解压后是Java应用:
|
|
133
|
+
"tar -xzf app.tar.gz && java -jar app.jar"
|
|
134
|
+
3. 确保启动命令能够正确找到并执行解压后的可执行文件或脚本,避免部署失败。启动命令应该将程序运行在后台并打印日志到指定文件,
|
|
135
|
+
注意使用非交互式命令,比如unzip -o等自动覆盖的命令,无需交互
|
|
136
|
+
例如:
|
|
137
|
+
- npm 程序示例:
|
|
138
|
+
"tar -xzf app.tar.gz && nohup npm start > /root/app.log 2>&1 &"
|
|
139
|
+
或者分别输出标准输出和错误日志:
|
|
140
|
+
"tar -xzf app.tar.gz && nohup npm start > /root/app.log 2> /root/app.error.log &"
|
|
141
|
+
- Java 程序示例:
|
|
142
|
+
"tar -xzf app.tar.gz && nohup java -jar app.jar > /root/app.log 2>&1 &"
|
|
143
|
+
- Python 程序示例:
|
|
144
|
+
"tar -xzf app.tar.gz && nohup python app.py > /root/app.log 2>&1 &"
|
|
145
|
+
说明:使用 nohup 命令可以让程序在后台运行,即使终端关闭也不会终止;> 重定向标准输出到日志文件;2>&1 将标准错误也重定向到同一文件;& 符号让命令在后台执行。
|
|
146
|
+
4. 应用和应用分组会自动检查是否存在,如果存在则跳过创建,避免重复创建错误。''',
|
|
68
147
|
port=port,
|
|
69
148
|
host=host,
|
|
70
149
|
stateless_http=True
|
|
@@ -81,11 +160,15 @@ def main(transport: str, port: int, host: str, services: str, headers_credential
|
|
|
81
160
|
mcp.tool(tool)
|
|
82
161
|
for tool in oos_tools.tools:
|
|
83
162
|
mcp.tool(tool)
|
|
163
|
+
for tool in application_management_tools.tools:
|
|
164
|
+
mcp.tool(tool)
|
|
84
165
|
for tool in cms_tools.tools:
|
|
85
166
|
mcp.tool(tool)
|
|
86
167
|
for tool in oss_tools.tools:
|
|
87
168
|
mcp.tool(tool)
|
|
88
169
|
api_tools.create_api_tools(mcp, config)
|
|
170
|
+
for tool in local_tools.tools:
|
|
171
|
+
mcp.tool(tool)
|
|
89
172
|
|
|
90
173
|
# Initialize and run the server
|
|
91
174
|
logger.debug(f'mcp server is running on {transport} mode.')
|
|
@@ -99,9 +99,14 @@ ECS_LIST_PARAMETERS = {
|
|
|
99
99
|
|
|
100
100
|
def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
|
|
101
101
|
service = service.lower()
|
|
102
|
-
|
|
102
|
+
try:
|
|
103
|
+
api_meta, _ = ApiMetaClient.get_api_meta(service, api)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f'Get API Meta Error: {e}')
|
|
106
|
+
api_meta = {}
|
|
107
|
+
|
|
103
108
|
version = ApiMetaClient.get_service_version(service)
|
|
104
|
-
method = 'POST' if api_meta.get('methods', [])[0] == 'post' else 'GET'
|
|
109
|
+
method = 'POST' if api_meta.get('methods', ['post'])[0] == 'post' else 'GET'
|
|
105
110
|
path = api_meta.get('path', '/')
|
|
106
111
|
style = ApiMetaClient.get_service_style(service)
|
|
107
112
|
|
|
@@ -264,6 +269,7 @@ def _create_and_decorate_tool(mcp: FastMCP, service: str, api: str):
|
|
|
264
269
|
|
|
265
270
|
return decorated_function
|
|
266
271
|
|
|
272
|
+
|
|
267
273
|
def create_api_tools(mcp: FastMCP, config:dict):
|
|
268
274
|
for service_code, apis in config.items():
|
|
269
275
|
for api_name in apis:
|