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.
Files changed (35) hide show
  1. alibaba_cloud_ops_mcp_server-0.8.9/.github/workflows/python-ci.yml +28 -0
  2. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/.gitignore +1 -0
  3. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/PKG-INFO +6 -3
  4. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/README.md +5 -2
  5. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/README_zh.md +3 -0
  6. alibaba_cloud_ops_mcp_server-0.8.9/examples/openapi_mcp_quickstart/server.py +17 -0
  7. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/pyproject.toml +7 -1
  8. {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
  9. {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
  10. {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
  11. alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_api_meta_client.py +292 -0
  12. alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_exception.py +36 -0
  13. alibaba_cloud_ops_mcp_server-0.8.9/tests/alibabacloud/test_utils.py +15 -0
  14. alibaba_cloud_ops_mcp_server-0.8.9/tests/test_init.py +8 -0
  15. alibaba_cloud_ops_mcp_server-0.8.9/tests/test_server.py +30 -0
  16. alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_api_tools.py +324 -0
  17. alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_cms_tools.py +179 -0
  18. alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_oos_tools.py +221 -0
  19. alibaba_cloud_ops_mcp_server-0.8.9/tests/tools/test_oss_tools.py +84 -0
  20. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/uv.lock +179 -1
  21. alibaba_cloud_ops_mcp_server-0.8.7/examples/openapi_mcp_quickstart/server.py +0 -0
  22. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/LICENSE +0 -0
  23. {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
  24. {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
  25. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/image/alibaba-cloud.png +0 -0
  26. {alibaba_cloud_ops_mcp_server-0.8.7 → alibaba_cloud_ops_mcp_server-0.8.9}/src/__init__.py +0 -0
  27. {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
  28. {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
  29. {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
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
@@ -5,6 +5,7 @@ build/
5
5
  dist/
6
6
  wheels/
7
7
  *.egg-info
8
+ .coverage
8
9
 
9
10
  # Virtual environments
10
11
  .venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alibaba-cloud-ops-mcp-server
3
- Version: 0.8.7
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) [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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) [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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) [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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.7"
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[cls.PATH]}, error: {e}')
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
- def main(transport: str, port: int):
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(parameters)
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, parameters.get('RegionId', 'cn-hangzhou'))
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} 请注意,提供参数要严格按照参数的类型和参数示例的提示,如果提到参数为String,且为一个 JSON 数组字符串,应在数组内使用单引号包裹对应的参数以避免转义问题,并在最外侧用双引号包裹以确保其是字符串,否则可能会导致参数解析错误。参数类型: {type_},参数示例:{example}'
96
+ description = f'{description} 参数类型: {type_},参数示例:{example}'
79
97
  required = schema.get('required', False)
80
- python_type = type_map.get(type_, str)
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