alibaba-cloud-ops-mcp-server 0.8.7__tar.gz → 0.8.9__tar.gz
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-0.8.9/.github/workflows/python-ci.yml +28 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/.gitignore +1 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/PKG-INFO +6 -3
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/README.md +5 -2
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/README_zh.md +3 -0
- alibaba_cloud_ops_mcp_server-0.8.9/examples/openapi_mcp_quickstart/server.py +17 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/pyproject.toml +7 -1
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/alibabacloud/api_meta_client.py +2 -1
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/server.py +9 -2
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/tools/api_tools.py +28 -4
- alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_api_meta_client.py +292 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_exception.py +36 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_utils.py +15 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/test_init.py +8 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/test_server.py +30 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_api_tools.py +324 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_cms_tools.py +179 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_oos_tools.py +221 -0
- alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_oss_tools.py +84 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/uv.lock +179 -1
- alibaba_cloud_ops_mcp_server-0.8.7/examples/openapi_mcp_quickstart/server.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/LICENSE +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/image/Alibaba-Cloud-Ops-MCP-User-Group-en.png +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/image/Alibaba-Cloud-Ops-MCP-User-Group-zh.png +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/image/alibaba-cloud.png +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/__init__.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/__init__.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/alibabacloud/__init__.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/alibabacloud/exception.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/alibabacloud/utils.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/config.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/tools/__init__.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/tools/cms_tools.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/tools/oos_tools.py +0 -0
- {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/alibaba_cloud_ops_mcp_server/tools/oss_tools.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: unit test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install uv
|
|
22
|
+
run: |
|
|
23
|
+
pip install uv
|
|
24
|
+
- name: Run tests with coverage
|
|
25
|
+
env:
|
|
26
|
+
PYTHONPATH: src
|
|
27
|
+
run: |
|
|
28
|
+
uv run pytest --cov=src --cov-report=term-missing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alibaba-cloud-ops-mcp-server
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.9
|
|
4
4
|
Summary: A MCP server for Alibaba Cloud
|
|
5
5
|
Author-email: Zheng Dayu <dayu.zdy@alibaba-inc.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -58,16 +58,19 @@ To use `alibaba-cloud-ops-mcp-server` MCP Server with any other MCP Client, you
|
|
|
58
58
|
## MCP Maketplace Integration
|
|
59
59
|
|
|
60
60
|
* [Cline](https://cline.bot/mcp-marketplace)
|
|
61
|
+
* [Cursor](https://docs.cursor.com/tools) [](https://cursor.com/install-mcp?name=alibaba-cloud-ops-mcp-server&config=eyJ0aW1lb3V0Ijo2MDAsImNvbW1hbmQiOiJ1dnggYWxpYmFiYS1jbG91ZC1vcHMtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEIjoiWW91ciBBY2Nlc3MgS2V5IElEIiwiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCI6IllvdXIgQWNjZXNzIEtleSBTRUNSRVQifX0%3D)
|
|
61
62
|
* [ModelScope](https://www.modelscope.cn/mcp/servers/@aliyun/alibaba-cloud-ops-mcp-server?lang=en_US)
|
|
62
63
|
* [Lingma](https://lingma.aliyun.com/)
|
|
63
64
|
* [Smithery AI](https://smithery.ai/server/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
64
65
|
* [FC-Function AI](https://cap.console.aliyun.com/template-detail?template=237)
|
|
66
|
+
* [Alibaba Cloud Model Studio](https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/alibaba-cloud-ops)
|
|
65
67
|
|
|
66
68
|
## Know More
|
|
67
69
|
|
|
68
|
-
* [Alibaba Cloud MCP Server is ready to use out of the box!!](https://developer.aliyun.com/article/1661348)
|
|
69
|
-
* [Setup Alibaba Cloud MCP Server on Bailian](https://developer.aliyun.com/article/1662120)
|
|
70
|
+
* [Alibaba Cloud Ops MCP Server is ready to use out of the box!!](https://developer.aliyun.com/article/1661348)
|
|
71
|
+
* [Setup Alibaba Cloud Ops MCP Server on Bailian](https://developer.aliyun.com/article/1662120)
|
|
70
72
|
* [Build your own Alibaba Cloud OpenAPI MCP Server with 10 lines of code](https://developer.aliyun.com/article/1662202)
|
|
73
|
+
* [Alibaba Cloud Ops MCP Server is officially available on the Alibaba Cloud Model Studio Platform MCP Marketplace](https://developer.aliyun.com/article/1665019)
|
|
71
74
|
|
|
72
75
|
## Tools
|
|
73
76
|
|
|
@@ -42,16 +42,19 @@ To use `alibaba-cloud-ops-mcp-server` MCP Server with any other MCP Client, you
|
|
|
42
42
|
## MCP Maketplace Integration
|
|
43
43
|
|
|
44
44
|
* [Cline](https://cline.bot/mcp-marketplace)
|
|
45
|
+
* [Cursor](https://docs.cursor.com/tools) [](https://cursor.com/install-mcp?name=alibaba-cloud-ops-mcp-server&config=eyJ0aW1lb3V0Ijo2MDAsImNvbW1hbmQiOiJ1dnggYWxpYmFiYS1jbG91ZC1vcHMtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEIjoiWW91ciBBY2Nlc3MgS2V5IElEIiwiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCI6IllvdXIgQWNjZXNzIEtleSBTRUNSRVQifX0%3D)
|
|
45
46
|
* [ModelScope](https://www.modelscope.cn/mcp/servers/@aliyun/alibaba-cloud-ops-mcp-server?lang=en_US)
|
|
46
47
|
* [Lingma](https://lingma.aliyun.com/)
|
|
47
48
|
* [Smithery AI](https://smithery.ai/server/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
48
49
|
* [FC-Function AI](https://cap.console.aliyun.com/template-detail?template=237)
|
|
50
|
+
* [Alibaba Cloud Model Studio](https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/alibaba-cloud-ops)
|
|
49
51
|
|
|
50
52
|
## Know More
|
|
51
53
|
|
|
52
|
-
* [Alibaba Cloud MCP Server is ready to use out of the box!!](https://developer.aliyun.com/article/1661348)
|
|
53
|
-
* [Setup Alibaba Cloud MCP Server on Bailian](https://developer.aliyun.com/article/1662120)
|
|
54
|
+
* [Alibaba Cloud Ops MCP Server is ready to use out of the box!!](https://developer.aliyun.com/article/1661348)
|
|
55
|
+
* [Setup Alibaba Cloud Ops MCP Server on Bailian](https://developer.aliyun.com/article/1662120)
|
|
54
56
|
* [Build your own Alibaba Cloud OpenAPI MCP Server with 10 lines of code](https://developer.aliyun.com/article/1662202)
|
|
57
|
+
* [Alibaba Cloud Ops MCP Server is officially available on the Alibaba Cloud Model Studio Platform MCP Marketplace](https://developer.aliyun.com/article/1665019)
|
|
55
58
|
|
|
56
59
|
## Tools
|
|
57
60
|
|
|
@@ -42,16 +42,19 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
|
42
42
|
## MCP市场集成
|
|
43
43
|
|
|
44
44
|
* [Cline](https://cline.bot/mcp-marketplace)
|
|
45
|
+
* [Cursor](https://docs.cursor.com/tools) [](https://cursor.com/install-mcp?name=alibaba-cloud-ops-mcp-server&config=eyJ0aW1lb3V0Ijo2MDAsImNvbW1hbmQiOiJ1dnggYWxpYmFiYS1jbG91ZC1vcHMtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEIjoiWW91ciBBY2Nlc3MgS2V5IElEIiwiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCI6IllvdXIgQWNjZXNzIEtleSBTRUNSRVQifX0%3D)
|
|
45
46
|
* [魔搭](https://www.modelscope.cn/mcp/servers/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
46
47
|
* [通义灵码](https://lingma.aliyun.com/)
|
|
47
48
|
* [Smithery AI](https://smithery.ai/server/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
48
49
|
* [FC-Function AI](https://cap.console.aliyun.com/template-detail?template=237)
|
|
50
|
+
* [阿里云百炼平台](https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/alibaba-cloud-ops)
|
|
49
51
|
|
|
50
52
|
## 了解更多
|
|
51
53
|
|
|
52
54
|
* [阿里云 MCP Server 开箱即用!](https://developer.aliyun.com/article/1661348)
|
|
53
55
|
* [在百炼平台配置您的自定义阿里云MCP Server](https://developer.aliyun.com/article/1662120)
|
|
54
56
|
* [10行代码,实现你的专属阿里云OpenAPI MCP Server](https://developer.aliyun.com/article/1662202)
|
|
57
|
+
* [阿里云CloudOps MCP正式上架百炼平台MCP市场](https://developer.aliyun.com/article/1665019)
|
|
55
58
|
|
|
56
59
|
## 功能点(Tool)
|
|
57
60
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Build your own Alibaba Cloud OpenAPI MCP Server with 10 lines of code
|
|
2
|
+
# https://developer.aliyun.com/article/1662202
|
|
3
|
+
# example codes
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from alibaba_cloud_ops_mcp_server.tools import api_tools
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
mcp = FastMCP("Example MCP server")
|
|
9
|
+
config = {
|
|
10
|
+
'ecs': ['DescribeInstances', 'DescribeRegions'],
|
|
11
|
+
'vpc': ['DescribeVpcs', 'DescribeVSwitches']
|
|
12
|
+
}
|
|
13
|
+
api_tools.create_api_tools(mcp, config)
|
|
14
|
+
mcp.run(transport='sse')
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "alibaba-cloud-ops-mcp-server"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.9"
|
|
4
4
|
description = "A MCP server for Alibaba Cloud"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -27,5 +27,11 @@ dependencies = [
|
|
|
27
27
|
[tool.hatch.build.targets.wheel]
|
|
28
28
|
packages = ["src/alibaba_cloud_ops_mcp_server"]
|
|
29
29
|
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.4.0",
|
|
33
|
+
"pytest-cov>=6.1.1",
|
|
34
|
+
]
|
|
35
|
+
|
|
30
36
|
[project.scripts]
|
|
31
37
|
alibaba-cloud-ops-mcp-server = "alibaba_cloud_ops_mcp_server:main"
|
|
@@ -30,12 +30,13 @@ class ApiMetaClient:
|
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def get_response_from_pop_api(cls, pop_api_name, service=None, api=None, version=None):
|
|
33
|
+
url = None # 提前定义,防止 except 中引用未定义变量
|
|
33
34
|
try:
|
|
34
35
|
api_config = cls.config.get(pop_api_name)
|
|
35
36
|
try:
|
|
36
37
|
formatted_path = api_config[cls.PATH].format(service=service, api=api, version=version)
|
|
37
38
|
except KeyError as e:
|
|
38
|
-
raise Exception(f'Failed to format path, path: {api_config
|
|
39
|
+
raise Exception(f'Failed to format path, path: {api_config.get(cls.PATH)}, error: {e}')
|
|
39
40
|
|
|
40
41
|
url = f'{cls.BASE_URL}/{formatted_path}'
|
|
41
42
|
response = requests.get(url)
|
|
@@ -21,11 +21,18 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
default=8000,
|
|
22
22
|
help="Port number",
|
|
23
23
|
)
|
|
24
|
-
|
|
24
|
+
@click.option(
|
|
25
|
+
"--host",
|
|
26
|
+
type=str,
|
|
27
|
+
default="127.0.0.1",
|
|
28
|
+
help="Host",
|
|
29
|
+
)
|
|
30
|
+
def main(transport: str, port: int, host: str):
|
|
25
31
|
# Create an MCP server
|
|
26
32
|
mcp = FastMCP(
|
|
27
33
|
name="alibaba-cloud-ops-mcp-server",
|
|
28
|
-
port=port
|
|
34
|
+
port=port,
|
|
35
|
+
host=host
|
|
29
36
|
)
|
|
30
37
|
for tool in oos_tools.tools:
|
|
31
38
|
mcp.add_tool(tool)
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
from mcp.server.fastmcp import FastMCP, Context
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
import logging
|
|
5
|
+
import json
|
|
5
6
|
|
|
6
7
|
import inspect
|
|
7
8
|
import types
|
|
@@ -32,6 +33,15 @@ def create_client(service: str, region_id: str) -> OpenApiClient:
|
|
|
32
33
|
return OpenApiClient(config)
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
# 类型为String的JSON数组参数
|
|
37
|
+
ECS_LIST_PARAMETERS = {
|
|
38
|
+
'HpcClusterIds', 'DedicatedHostClusterIds', 'DedicatedHostIds',
|
|
39
|
+
'InstanceIds', 'DeploymentSetIds', 'KeyPairNames', 'SecurityGroupIds',
|
|
40
|
+
'diskIds', 'repeatWeekdays', 'timePoints', 'DiskIds', 'SnapshotLinkIds',
|
|
41
|
+
'EipAddresses', 'PublicIpAddresses', 'PrivateIpAddresses'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
|
|
36
46
|
service = service.lower()
|
|
37
47
|
api_meta, _ = ApiMetaClient.get_api_meta(service, api)
|
|
@@ -39,8 +49,16 @@ def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
|
|
|
39
49
|
method = 'POST' if api_meta.get('methods', [])[0] == 'post' else 'GET'
|
|
40
50
|
path = api_meta.get('path', '/')
|
|
41
51
|
style = ApiMetaClient.get_service_style(service)
|
|
52
|
+
|
|
53
|
+
# 处理特殊参数格式
|
|
54
|
+
processed_parameters = parameters.copy()
|
|
55
|
+
if service == 'ecs':
|
|
56
|
+
for param_name, param_value in parameters.items():
|
|
57
|
+
if param_name in ECS_LIST_PARAMETERS and isinstance(param_value, list):
|
|
58
|
+
processed_parameters[param_name] = json.dumps(param_value)
|
|
59
|
+
|
|
42
60
|
req = open_api_models.OpenApiRequest(
|
|
43
|
-
query=OpenApiUtilClient.query(
|
|
61
|
+
query=OpenApiUtilClient.query(processed_parameters)
|
|
44
62
|
)
|
|
45
63
|
params = open_api_models.Params(
|
|
46
64
|
action=api,
|
|
@@ -53,7 +71,7 @@ def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
|
|
|
53
71
|
req_body_type='formData',
|
|
54
72
|
body_type='json'
|
|
55
73
|
)
|
|
56
|
-
client = create_client(service,
|
|
74
|
+
client = create_client(service, processed_parameters.get('RegionId', 'cn-hangzhou'))
|
|
57
75
|
runtime = util_models.RuntimeOptions()
|
|
58
76
|
return client.call_api(params, req, runtime)
|
|
59
77
|
|
|
@@ -75,9 +93,15 @@ def _create_function_schemas(service, api, api_meta):
|
|
|
75
93
|
description = schema.get('description', '')
|
|
76
94
|
example = schema.get('example', '')
|
|
77
95
|
type_ = schema.get('type', '')
|
|
78
|
-
description = f'{description}
|
|
96
|
+
description = f'{description} 参数类型: {type_},参数示例:{example}'
|
|
79
97
|
required = schema.get('required', False)
|
|
80
|
-
|
|
98
|
+
|
|
99
|
+
# 只有在service为ecs时,才对特定参数进行特殊处理
|
|
100
|
+
if service.lower() == 'ecs' and name in ECS_LIST_PARAMETERS and type_ == 'string':
|
|
101
|
+
python_type = list
|
|
102
|
+
else:
|
|
103
|
+
python_type = type_map.get(type_, str)
|
|
104
|
+
|
|
81
105
|
field_info = (
|
|
82
106
|
python_type,
|
|
83
107
|
field(
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from alibaba_cloud_ops_mcp_server.alibabacloud import api_meta_client
|
|
4
|
+
|
|
5
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
6
|
+
def test_get_response_from_pop_api_success(mock_get):
|
|
7
|
+
mock_get.return_value.json.return_value = [{"code": "ecs", "defaultVersion": "2014-05-26", "style": "RPC"}]
|
|
8
|
+
data = api_meta_client.ApiMetaClient.get_response_from_pop_api('GetProductList')
|
|
9
|
+
assert isinstance(data, list)
|
|
10
|
+
assert data[0]["code"] == "ecs"
|
|
11
|
+
|
|
12
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
13
|
+
def test_get_response_from_pop_api_exception(mock_get):
|
|
14
|
+
mock_get.side_effect = Exception('fail')
|
|
15
|
+
with pytest.raises(Exception) as e:
|
|
16
|
+
api_meta_client.ApiMetaClient.get_response_from_pop_api('GetProductList')
|
|
17
|
+
assert 'Failed to get response' in str(e.value)
|
|
18
|
+
|
|
19
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
20
|
+
def test_get_service_version_and_style(mock_get):
|
|
21
|
+
mock_get.return_value.json.return_value = [{"code": "ecs", "defaultVersion": "2014-05-26", "style": "RPC"}]
|
|
22
|
+
v = api_meta_client.ApiMetaClient.get_service_version('ecs')
|
|
23
|
+
s = api_meta_client.ApiMetaClient.get_service_style('ecs')
|
|
24
|
+
assert v == "2014-05-26"
|
|
25
|
+
assert s == "RPC"
|
|
26
|
+
|
|
27
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
28
|
+
def test_get_standard_service_and_api(mock_get):
|
|
29
|
+
# 1st call: GetProductList, 2nd call: GetApiOverview
|
|
30
|
+
mock_get.return_value.json.side_effect = [
|
|
31
|
+
[{"code": "ecs", "defaultVersion": "2014-05-26"}],
|
|
32
|
+
{"apis": {"DescribeInstances": {}}}
|
|
33
|
+
]
|
|
34
|
+
service, api = api_meta_client.ApiMetaClient.get_standard_service_and_api('ecs', 'DescribeInstances', '2014-05-26')
|
|
35
|
+
assert service == 'ecs'
|
|
36
|
+
assert api == 'DescribeInstances'
|
|
37
|
+
|
|
38
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
39
|
+
def test_get_api_meta_invalid(mock_get):
|
|
40
|
+
# 1st call: GetProductList returns empty list
|
|
41
|
+
mock_get.return_value.json.return_value = []
|
|
42
|
+
with pytest.raises(Exception) as e:
|
|
43
|
+
api_meta_client.ApiMetaClient.get_api_meta('notexist', 'api')
|
|
44
|
+
assert 'InvalidServiceName' in str(e.value) or 'object has no attribute' in str(e.value)
|
|
45
|
+
|
|
46
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
|
|
47
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=(None, 'api'))
|
|
48
|
+
def test_get_api_meta_service_none(mock_get_std, mock_get_ver):
|
|
49
|
+
with pytest.raises(Exception) as e:
|
|
50
|
+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
|
|
51
|
+
assert 'InvalidServiceName' in str(e.value)
|
|
52
|
+
|
|
53
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
|
|
54
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', None))
|
|
55
|
+
def test_get_api_meta_api_none(mock_get_std, mock_get_ver):
|
|
56
|
+
with pytest.raises(Exception) as e:
|
|
57
|
+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
|
|
58
|
+
assert 'InvalidAPIName' in str(e.value)
|
|
59
|
+
|
|
60
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta', return_value=({}, '2014-05-26'))
|
|
61
|
+
def test_get_response_from_api_meta_empty(mock_get_meta):
|
|
62
|
+
prop, ver = api_meta_client.ApiMetaClient.get_response_from_api_meta('ecs', 'DescribeInstances')
|
|
63
|
+
assert prop == {}
|
|
64
|
+
assert ver == '2014-05-26'
|
|
65
|
+
|
|
66
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
|
|
67
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
68
|
+
def test_get_ref_api_meta_keyerror(mock_pop_api, mock_std):
|
|
69
|
+
# ref_path指向不存在的key
|
|
70
|
+
mock_pop_api.return_value = {'apis': {}}
|
|
71
|
+
with pytest.raises(KeyError):
|
|
72
|
+
api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/notfound'}, 'ecs', '2014-05-26')
|
|
73
|
+
|
|
74
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
|
|
75
|
+
def test_get_api_parameters_params_in_and_ref(mock_get_meta):
|
|
76
|
+
# 测试params_in过滤和递归ref
|
|
77
|
+
api_meta = {
|
|
78
|
+
'parameters': [
|
|
79
|
+
{'name': 'foo', 'in': 'query', 'schema': {'type': 'string'}},
|
|
80
|
+
{'name': 'bar', 'in': 'body', 'schema': {'type': 'string', '$ref': '#/defs/bar'}}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
# get_ref_api_meta返回递归结构
|
|
84
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_ref_api_meta', return_value={'properties': {'baz': {}}}):
|
|
85
|
+
mock_get_meta.return_value = (api_meta, '2014-05-26')
|
|
86
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances', params_in='query')
|
|
87
|
+
assert params == ['foo']
|
|
88
|
+
# 测试递归ref
|
|
89
|
+
params2 = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
90
|
+
assert 'baz' in params2
|
|
91
|
+
|
|
92
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
|
|
93
|
+
def test_get_api_parameters_circular_ref(mock_get_meta):
|
|
94
|
+
# 测试循环引用
|
|
95
|
+
api_meta = {
|
|
96
|
+
'parameters': [
|
|
97
|
+
{'name': 'foo', 'in': 'query', 'schema': {'type': 'string', '$ref': '#/defs/foo'}}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
# get_ref_api_meta返回带$ref的结构,模拟循环
|
|
101
|
+
def fake_get_ref(data, service, version):
|
|
102
|
+
return {'$ref': '#/defs/foo'}
|
|
103
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_ref_api_meta', side_effect=fake_get_ref):
|
|
104
|
+
mock_get_meta.return_value = (api_meta, '2014-05-26')
|
|
105
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
106
|
+
assert 'foo' in params
|
|
107
|
+
|
|
108
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta', side_effect=Exception('fail'))
|
|
109
|
+
def test_get_api_field_exception(mock_get_meta):
|
|
110
|
+
val = api_meta_client.ApiMetaClient.get_api_field('parameters', 'ecs', 'DescribeInstances', default='d')
|
|
111
|
+
assert val == 'd'
|
|
112
|
+
|
|
113
|
+
def test_get_api_body_style_none():
|
|
114
|
+
# get_api_field返回None
|
|
115
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_api_field', return_value=None):
|
|
116
|
+
val = api_meta_client.ApiMetaClient.get_api_body_style('ecs', 'DescribeInstances')
|
|
117
|
+
assert val is None
|
|
118
|
+
# get_api_field返回无STYLE参数
|
|
119
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_api_field', return_value=[{'in': 'body'}]):
|
|
120
|
+
val = api_meta_client.ApiMetaClient.get_api_body_style('ecs', 'DescribeInstances')
|
|
121
|
+
assert val is None
|
|
122
|
+
|
|
123
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
124
|
+
def test_get_apis_in_service(mock_get):
|
|
125
|
+
mock_get.return_value.json.return_value = {"apis": {"A": {}, "B": {}}}
|
|
126
|
+
apis = api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
|
|
127
|
+
assert set(apis) == {"A", "B"}
|
|
128
|
+
|
|
129
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
130
|
+
def test_get_response_from_pop_api_keyerror(mock_get):
|
|
131
|
+
# config 缺 key
|
|
132
|
+
with patch.object(api_meta_client.ApiMetaClient, 'config', {'GetProductList': {}}):
|
|
133
|
+
with pytest.raises(Exception) as e:
|
|
134
|
+
api_meta_client.ApiMetaClient.get_response_from_pop_api('GetProductList')
|
|
135
|
+
assert 'Failed to format path' in str(e.value)
|
|
136
|
+
|
|
137
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta', return_value=({}, '2014-05-26'))
|
|
138
|
+
def test_get_response_from_api_meta_no_properties(mock_get_meta):
|
|
139
|
+
# property_values 取不到属性
|
|
140
|
+
with patch.dict('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.__dict__', {'RESPONSES': 'responses', 'HTTP_SUCCESS_CODE': '200', 'SCHEMA': 'schema', 'PROPERTIES': 'properties'}):
|
|
141
|
+
prop, ver = api_meta_client.ApiMetaClient.get_response_from_api_meta('ecs', 'DescribeInstances')
|
|
142
|
+
assert prop == {}
|
|
143
|
+
assert ver == '2014-05-26'
|
|
144
|
+
|
|
145
|
+
def test_get_api_parameters_empty():
|
|
146
|
+
# parameters 为空
|
|
147
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_api_meta', return_value=({'parameters': []}, '2014-05-26')):
|
|
148
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
149
|
+
assert params == []
|
|
150
|
+
|
|
151
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
152
|
+
def test_get_apis_in_service_no_apis(mock_get):
|
|
153
|
+
mock_get.return_value.json.return_value = {}
|
|
154
|
+
with pytest.raises(KeyError):
|
|
155
|
+
api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
|
|
156
|
+
|
|
157
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
158
|
+
def test_get_api_parameters_schema_not_dict(mock_get):
|
|
159
|
+
# get_api_meta返回的schema不是dict
|
|
160
|
+
api_meta = {
|
|
161
|
+
'parameters': [
|
|
162
|
+
{'name': 'foo', 'in': 'query', 'schema': None},
|
|
163
|
+
{'name': 'bar', 'in': 'query', 'schema': 'notadict'}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_api_meta', return_value=(api_meta, '2014-05-26')):
|
|
167
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
168
|
+
# 两个参数都应该被返回
|
|
169
|
+
assert 'foo' in params
|
|
170
|
+
assert 'bar' in params
|
|
171
|
+
|
|
172
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
173
|
+
def test_get_apis_in_service_normal(mock_get):
|
|
174
|
+
"""测试get_apis_in_service方法正常返回API列表"""
|
|
175
|
+
mock_get.return_value.json.return_value = {"apis": {"DescribeInstances": {}, "StartInstance": {}}}
|
|
176
|
+
apis = api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
|
|
177
|
+
assert set(apis) == {"DescribeInstances", "StartInstance"}
|
|
178
|
+
assert len(apis) == 2
|
|
179
|
+
|
|
180
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
|
|
181
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=(None, None))
|
|
182
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
183
|
+
def test_get_api_meta_service_none_exception(mock_pop_api, mock_get_std, mock_get_ver):
|
|
184
|
+
"""测试get_api_meta方法中service_standard为None时抛出异常"""
|
|
185
|
+
with pytest.raises(Exception) as e:
|
|
186
|
+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
|
|
187
|
+
assert 'InvalidServiceName' in str(e.value)
|
|
188
|
+
|
|
189
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
|
|
190
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', None))
|
|
191
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
192
|
+
def test_get_api_meta_api_none_exception(mock_pop_api, mock_get_std, mock_get_ver):
|
|
193
|
+
"""测试get_api_meta方法中api_standard为None时抛出异常"""
|
|
194
|
+
with pytest.raises(Exception) as e:
|
|
195
|
+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
|
|
196
|
+
assert 'InvalidAPIName' in str(e.value)
|
|
197
|
+
|
|
198
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
|
|
199
|
+
def test_get_api_parameters_schema_not_dict_more_cases(mock_get_meta):
|
|
200
|
+
"""测试get_api_parameters中更多非dict类型的schema"""
|
|
201
|
+
api_meta = {
|
|
202
|
+
'parameters': [
|
|
203
|
+
{'name': 'foo', 'in': 'query', 'schema': 'string'}, # 字符串
|
|
204
|
+
{'name': 'bar', 'in': 'query', 'schema': 123}, # 数字
|
|
205
|
+
{'name': 'baz', 'in': 'query', 'schema': []}, # 列表
|
|
206
|
+
{'name': 'qux', 'in': 'query', 'schema': None}, # None
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
mock_get_meta.return_value = (api_meta, '2014-05-26')
|
|
210
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
211
|
+
assert 'foo' in params
|
|
212
|
+
assert 'bar' in params
|
|
213
|
+
assert 'baz' in params
|
|
214
|
+
assert 'qux' in params
|
|
215
|
+
|
|
216
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
|
|
217
|
+
def test_get_apis_in_service_normal(mock_get):
|
|
218
|
+
"""测试get_apis_in_service方法正常返回API列表"""
|
|
219
|
+
mock_get.return_value.json.return_value = {"apis": {"DescribeInstances": {}, "StartInstance": {}}}
|
|
220
|
+
apis = api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
|
|
221
|
+
assert set(apis) == {"DescribeInstances", "StartInstance"}
|
|
222
|
+
assert len(apis) == 2
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
|
|
226
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
227
|
+
def test_get_ref_api_meta_invalid_path(mock_pop_api, mock_std):
|
|
228
|
+
# 模拟 ref_path 指向不存在的 key
|
|
229
|
+
mock_pop_api.return_value = {'apis': {'DescribeInstances': {}}}
|
|
230
|
+
with pytest.raises(KeyError):
|
|
231
|
+
api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/notfound'}, 'ecs', '2014-05-26')
|
|
232
|
+
|
|
233
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
|
|
234
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
235
|
+
def test_get_ref_api_meta_invalid_path(mock_pop_api, mock_std):
|
|
236
|
+
# 模拟 ref_path 指向不存在的 key
|
|
237
|
+
mock_pop_api.return_value = {'apis': {'DescribeInstances': {}}}
|
|
238
|
+
with pytest.raises(KeyError):
|
|
239
|
+
api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/notfound'}, 'ecs', '2014-05-26')
|
|
240
|
+
|
|
241
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
|
|
242
|
+
def test_get_api_field_default_value(mock_get_meta):
|
|
243
|
+
# 模拟 get_api_meta 返回无 field_type 的数据
|
|
244
|
+
mock_get_meta.return_value = ({}, '2014-05-26')
|
|
245
|
+
val = api_meta_client.ApiMetaClient.get_api_field('parameters', 'ecs', 'DescribeInstances', default='default_val')
|
|
246
|
+
assert val == 'default_val'
|
|
247
|
+
|
|
248
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
|
|
249
|
+
def test_get_api_parameters_nested_ref(mock_get_meta):
|
|
250
|
+
# 模拟嵌套 $ref
|
|
251
|
+
api_meta = {
|
|
252
|
+
'parameters': [
|
|
253
|
+
{'name': 'foo', 'in': 'query', 'schema': {'$ref': '#/defs/A'}}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
def fake_get_ref(data, service, version):
|
|
257
|
+
if '#/defs/A' in data.get('$ref', ''):
|
|
258
|
+
return {'properties': {'a': {'$ref': '#/defs/B'}}}
|
|
259
|
+
elif '#/defs/B' in data.get('$ref', ''):
|
|
260
|
+
return {'properties': {'b': {}}}
|
|
261
|
+
return {}
|
|
262
|
+
with patch.object(api_meta_client.ApiMetaClient, 'get_ref_api_meta', side_effect=fake_get_ref):
|
|
263
|
+
mock_get_meta.return_value = (api_meta, '2014-05-26')
|
|
264
|
+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
|
|
265
|
+
assert 'a' in params and 'b' in params # 深层嵌套属性应被提取
|
|
266
|
+
|
|
267
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
|
|
268
|
+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
|
|
269
|
+
def test_get_ref_api_meta_valid_path(mock_pop_api, mock_std):
|
|
270
|
+
# 模拟 get_response_from_pop_api 返回包含 defs/A 的结构
|
|
271
|
+
mock_pop_api.return_value = {
|
|
272
|
+
'defs': {
|
|
273
|
+
'A': {
|
|
274
|
+
'properties': {
|
|
275
|
+
'prop1': {'type': 'string'},
|
|
276
|
+
'prop2': {'type': 'integer'}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# 调用 get_ref_api_meta,传入 $ref 指向 #/defs/A
|
|
283
|
+
result = api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/defs/A'}, 'ecs', '2014-05-26')
|
|
284
|
+
|
|
285
|
+
# 验证返回结果是否与 defs/A 的结构一致
|
|
286
|
+
expected = {
|
|
287
|
+
'properties': {
|
|
288
|
+
'prop1': {'type': 'string'},
|
|
289
|
+
'prop2': {'type': 'integer'}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
assert result == expected
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from alibaba_cloud_ops_mcp_server.alibabacloud import exception
|
|
3
|
+
|
|
4
|
+
def test_acs_exception_default():
|
|
5
|
+
e = exception.AcsException()
|
|
6
|
+
s = str(e)
|
|
7
|
+
assert 'InternalError' in s
|
|
8
|
+
assert 'unknown exception' in s.lower()
|
|
9
|
+
assert e.status == 500
|
|
10
|
+
assert e.code == 'InternalError'
|
|
11
|
+
assert isinstance(e.__deepcopy__({}), exception.AcsException)
|
|
12
|
+
assert isinstance(e.__unicode__(), str)
|
|
13
|
+
|
|
14
|
+
def test_acs_exception_format():
|
|
15
|
+
class CustomEx(exception.AcsException):
|
|
16
|
+
msg_fmt = 'Error: {foo}.'
|
|
17
|
+
code = 'CustomError'
|
|
18
|
+
e = CustomEx(foo='bar')
|
|
19
|
+
assert 'bar' in str(e)
|
|
20
|
+
assert 'CustomError' in str(e)
|
|
21
|
+
|
|
22
|
+
def test_acs_exception_format_keyerror(caplog):
|
|
23
|
+
class CustomEx(exception.AcsException):
|
|
24
|
+
msg_fmt = 'Error: {foo}.'
|
|
25
|
+
code = 'CustomError'
|
|
26
|
+
with caplog.at_level('ERROR'):
|
|
27
|
+
e = CustomEx(badkey='baz')
|
|
28
|
+
assert 'badkey' in caplog.text
|
|
29
|
+
|
|
30
|
+
def test_oos_execution_failed():
|
|
31
|
+
e = exception.OOSExecutionFailed(reason='fail')
|
|
32
|
+
s = str(e)
|
|
33
|
+
assert 'OOS Execution Failed' in s
|
|
34
|
+
assert 'Execution.Failed' in s
|
|
35
|
+
assert e.status == 400
|
|
36
|
+
assert e.code == 'Execution.Failed'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from unittest.mock import patch, MagicMock
|
|
2
|
+
from alibaba_cloud_ops_mcp_server.alibabacloud import utils
|
|
3
|
+
|
|
4
|
+
def test_create_config():
|
|
5
|
+
with patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.CredClient') as mock_cred, \
|
|
6
|
+
patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.Config') as mock_cfg:
|
|
7
|
+
cred = MagicMock()
|
|
8
|
+
mock_cred.return_value = cred
|
|
9
|
+
cfg = MagicMock()
|
|
10
|
+
mock_cfg.return_value = cfg
|
|
11
|
+
result = utils.create_config()
|
|
12
|
+
assert result is cfg
|
|
13
|
+
assert cfg.user_agent == 'alibaba-cloud-ops-mcp-server'
|
|
14
|
+
mock_cred.assert_called_once()
|
|
15
|
+
mock_cfg.assert_called_once_with(credential=cred)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, AsyncMock
|
|
3
|
+
import alibaba_cloud_ops_mcp_server
|
|
4
|
+
|
|
5
|
+
def test_main_calls_server_main():
|
|
6
|
+
with patch('alibaba_cloud_ops_mcp_server.server.main', new_callable=AsyncMock) as mock_main:
|
|
7
|
+
alibaba_cloud_ops_mcp_server.main()
|
|
8
|
+
mock_main.assert_awaited_once()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
|
|
4
|
+
@patch('alibaba_cloud_ops_mcp_server.server.FastMCP')
|
|
5
|
+
@patch('alibaba_cloud_ops_mcp_server.server.api_tools.create_api_tools')
|
|
6
|
+
def test_main_run(mock_create_api_tools, mock_FastMCP):
|
|
7
|
+
with patch('alibaba_cloud_ops_mcp_server.server.oss_tools.tools', [lambda: None]), \
|
|
8
|
+
patch('alibaba_cloud_ops_mcp_server.server.oos_tools.tools', [lambda: None]), \
|
|
9
|
+
patch('alibaba_cloud_ops_mcp_server.server.cms_tools.tools', [lambda: None]):
|
|
10
|
+
from alibaba_cloud_ops_mcp_server import server
|
|
11
|
+
mcp = MagicMock()
|
|
12
|
+
mock_FastMCP.return_value = mcp
|
|
13
|
+
# 调用main函数
|
|
14
|
+
server.main.callback(transport='stdio', port=12345, host='127.0.0.1')
|
|
15
|
+
mock_FastMCP.assert_called_once_with(name='alibaba-cloud-ops-mcp-server', port=12345, host='127.0.0.1')
|
|
16
|
+
assert mcp.add_tool.call_count == 3 # oss/oos/cms 各1
|
|
17
|
+
mock_create_api_tools.assert_called_once()
|
|
18
|
+
mcp.run.assert_called_once_with(transport='stdio')
|
|
19
|
+
|
|
20
|
+
def test_run_as_main(monkeypatch):
|
|
21
|
+
import runpy, sys
|
|
22
|
+
from alibaba_cloud_ops_mcp_server import server
|
|
23
|
+
monkeypatch.setattr(server, 'main', lambda *a, **kw: None)
|
|
24
|
+
monkeypatch.setattr(sys, 'argv', ['server.py'])
|
|
25
|
+
import mcp.server.fastmcp
|
|
26
|
+
monkeypatch.setattr(mcp.server.fastmcp.FastMCP, 'run', lambda self, **kwargs: None)
|
|
27
|
+
import pytest
|
|
28
|
+
with pytest.raises(SystemExit) as e:
|
|
29
|
+
runpy.run_path('src/alibaba_cloud_ops_mcp_server/server.py', run_name='__main__')
|
|
30
|
+
assert e.value.code == 0
|