alibaba-cloud-ops-mcp-server 0.9.0__tar.gz → 0.9.2__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 (41) hide show
  1. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/PKG-INFO +2 -2
  2. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/pyproject.toml +2 -2
  3. alibaba_cloud_ops_mcp_server-0.9.2/src/alibaba_cloud_ops_mcp_server/alibabacloud/utils.py +46 -0
  4. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/config.py +1 -1
  5. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/server.py +5 -5
  6. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/api_tools.py +25 -3
  7. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/cms_tools.py +9 -9
  8. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/oos_tools.py +24 -24
  9. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/oss_tools.py +15 -8
  10. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/alibabacloud/test_api_meta_client.py +43 -2
  11. alibaba_cloud_ops_mcp_server-0.9.2/tests/alibabacloud/test_utils.py +75 -0
  12. alibaba_cloud_ops_mcp_server-0.9.2/tests/test_server.py +102 -0
  13. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/tools/test_oss_tools.py +14 -0
  14. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/uv.lock +49 -12
  15. alibaba_cloud_ops_mcp_server-0.9.0/src/alibaba_cloud_ops_mcp_server/alibabacloud/utils.py +0 -9
  16. alibaba_cloud_ops_mcp_server-0.9.0/tests/alibabacloud/test_utils.py +0 -15
  17. alibaba_cloud_ops_mcp_server-0.9.0/tests/test_server.py +0 -32
  18. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/.github/workflows/python-ci.yml +0 -0
  19. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/.gitignore +0 -0
  20. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/LICENSE +0 -0
  21. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/README.md +0 -0
  22. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/README_mcp_args.md +0 -0
  23. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/README_zh.md +0 -0
  24. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/examples/openapi_mcp_quickstart/server.py +0 -0
  25. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/image/Alibaba-Cloud-Ops-MCP-User-Group-en.png +0 -0
  26. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/image/Alibaba-Cloud-Ops-MCP-User-Group-zh.png +0 -0
  27. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/image/alibaba-cloud.png +0 -0
  28. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/__init__.py +0 -0
  29. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/__init__.py +0 -0
  30. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/alibabacloud/__init__.py +0 -0
  31. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/alibabacloud/api_meta_client.py +0 -0
  32. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/alibabacloud/exception.py +0 -0
  33. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/alibabacloud/static/PROMPT_UNDERSTANDING.md +0 -0
  34. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/alibabacloud/static/__init__.py +0 -0
  35. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/__init__.py +0 -0
  36. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/src/alibaba_cloud_ops_mcp_server/tools/common_api_tools.py +0 -0
  37. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/alibabacloud/test_exception.py +0 -0
  38. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/test_init.py +0 -0
  39. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/tools/test_api_tools.py +0 -0
  40. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/tools/test_cms_tools.py +0 -0
  41. {alibaba_cloud_ops_mcp_server-0.9.0 → alibaba_cloud_ops_mcp_server-0.9.2}/tests/tools/test_oos_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alibaba-cloud-ops-mcp-server
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -11,7 +11,7 @@ Requires-Dist: alibabacloud-ecs20140526>=6.1.0
11
11
  Requires-Dist: alibabacloud-oos20190601>=3.4.1
12
12
  Requires-Dist: alibabacloud-oss-v2>=1.1.0
13
13
  Requires-Dist: click>=8.1.8
14
- Requires-Dist: mcp[cli]>=1.9.0
14
+ Requires-Dist: fastmcp==2.8.0
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  # Alibaba Cloud Ops MCP Server
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alibaba-cloud-ops-mcp-server"
3
- version = "0.9.0"
3
+ version = "0.9.2"
4
4
  description = "A MCP server for Alibaba Cloud"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "alibabacloud_oss_v2>=1.1.0",
15
15
  "alibabacloud-credentials>=1.0.0",
16
16
  "click>=8.1.8",
17
- "mcp[cli]>=1.9.0",
17
+ "fastmcp==2.8.0"
18
18
  ]
19
19
 
20
20
  [build-system]
@@ -0,0 +1,46 @@
1
+ import logging
2
+
3
+ from alibabacloud_credentials.client import Client as CredClient
4
+ from alibabacloud_tea_openapi.models import Config
5
+ from fastmcp.server.dependencies import get_http_request
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def get_credentials_from_header():
11
+ credentials = None
12
+ try:
13
+ request = get_http_request()
14
+ headers = request.headers
15
+ access_key_id = headers.get('x-acs-accesskey-id', None)
16
+ access_key_secret = headers.get('x-acs-accesskey-secret', None)
17
+ token = headers.get('x-acs-security-token', None)
18
+
19
+ if access_key_id:
20
+ credentials = {
21
+ 'AccessKeyId': access_key_id,
22
+ 'AccessKeySecret': access_key_secret,
23
+ 'SecurityToken': token
24
+ }
25
+
26
+ except Exception as e:
27
+ logger.info(f'get_credentials_from_header error: {e}')
28
+ return credentials
29
+
30
+
31
+ def create_config():
32
+ credentials = get_credentials_from_header()
33
+ if credentials:
34
+ access_key_id = credentials.get('AccessKeyId', None)
35
+ access_key_secret = credentials.get('AccessKeySecret', None)
36
+ token = credentials.get('SecurityToken', None)
37
+ config = Config(
38
+ access_key_id=access_key_id,
39
+ access_key_secret=access_key_secret,
40
+ security_token=token
41
+ )
42
+ else:
43
+ credentialsClient = CredClient()
44
+ config = Config(credential=credentialsClient)
45
+ config.user_agent = 'alibaba-cloud-ops-mcp-server'
46
+ return config
@@ -21,7 +21,7 @@ config = {
21
21
  ],
22
22
  'rds': [
23
23
  'DescribeDBInstances'
24
- ],
24
+ ]
25
25
  }
26
26
 
27
27
 
@@ -1,4 +1,4 @@
1
- from mcp.server.fastmcp import FastMCP
1
+ from fastmcp import FastMCP
2
2
  import click
3
3
  import logging
4
4
 
@@ -60,13 +60,13 @@ def main(transport: str, port: int, host: str, services: str):
60
60
  service_list = [(key, SUPPORTED_SERVICES_MAP.get(key, key)) for key in service_keys]
61
61
  set_custom_service_list(service_list)
62
62
  for tool in common_api_tools.tools:
63
- mcp.add_tool(tool)
63
+ mcp.tool(tool)
64
64
  for tool in oos_tools.tools:
65
- mcp.add_tool(tool)
65
+ mcp.tool(tool)
66
66
  for tool in cms_tools.tools:
67
- mcp.add_tool(tool)
67
+ mcp.tool(tool)
68
68
  for tool in oss_tools.tools:
69
- mcp.add_tool(tool)
69
+ mcp.tool(tool)
70
70
  api_tools.create_api_tools(mcp, config)
71
71
 
72
72
  # Initialize and run the server
@@ -110,24 +110,37 @@ def _create_function_schemas(service, api, api_meta):
110
110
  schemas = {}
111
111
  schemas[api] = {}
112
112
  parameters = api_meta.get('parameters', [])
113
+
114
+ required_params = []
115
+ optional_params = []
116
+
113
117
  for parameter in parameters:
114
118
  name = parameter.get('name')
115
119
  # TODO 目前忽略了带'.'的参数
116
120
  if '.' in name:
117
121
  continue
118
122
  schema = parameter.get('schema', '')
123
+ required = schema.get('required', False)
124
+
125
+ if required:
126
+ required_params.append(parameter)
127
+ else:
128
+ optional_params.append(parameter)
129
+
130
+ def process_parameter(parameter):
131
+ name = parameter.get('name')
132
+ schema = parameter.get('schema', '')
119
133
  description = schema.get('description', '')
120
134
  example = schema.get('example', '')
121
135
  type_ = schema.get('type', '')
122
136
  description = f'{description} 参数类型: {type_},参数示例:{example}'
123
137
  required = schema.get('required', False)
124
-
125
- # 只有在service为ecs时,才对特定参数进行特殊处理
138
+
126
139
  if service.lower() == 'ecs' and name in ECS_LIST_PARAMETERS and type_ == 'string':
127
140
  python_type = list
128
141
  else:
129
142
  python_type = type_map.get(type_, str)
130
-
143
+
131
144
  field_info = (
132
145
  python_type,
133
146
  field(
@@ -135,7 +148,16 @@ def _create_function_schemas(service, api, api_meta):
135
148
  metadata={'description': description, 'required': required}
136
149
  )
137
150
  )
151
+ return name, field_info
152
+
153
+ for parameter in required_params:
154
+ name, field_info = process_parameter(parameter)
138
155
  schemas[api][name] = field_info
156
+
157
+ for parameter in optional_params:
158
+ name, field_info = process_parameter(parameter)
159
+ schemas[api][name] = field_info
160
+
139
161
  if 'RegionId' not in schemas[api]:
140
162
  schemas[api]['RegionId'] = (
141
163
  str,
@@ -37,8 +37,8 @@ def _get_cms_metric_data(region_id: str, instance_ids: List[str], metric_name: s
37
37
 
38
38
  @tools.append
39
39
  def CMS_GetCpuUsageData(
40
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
41
40
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
41
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
42
42
  ):
43
43
  """获取ECS实例的CPU使用率数据"""
44
44
  return _get_cms_metric_data(RegionId, InstanceIds, 'cpu_total')
@@ -46,8 +46,8 @@ def CMS_GetCpuUsageData(
46
46
 
47
47
  @tools.append
48
48
  def CMS_GetCpuLoadavgData(
49
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
50
49
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
50
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
51
51
  ):
52
52
  """获取CPU一分钟平均负载指标数据"""
53
53
  return _get_cms_metric_data(RegionId, InstanceIds, 'load_1m')
@@ -55,8 +55,8 @@ def CMS_GetCpuLoadavgData(
55
55
 
56
56
  @tools.append
57
57
  def CMS_GetCpuloadavg5mData(
58
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
59
58
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
59
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
60
60
  ):
61
61
  """获取CPU五分钟平均负载指标数据"""
62
62
  return _get_cms_metric_data(RegionId, InstanceIds, 'load_5m')
@@ -64,16 +64,16 @@ def CMS_GetCpuloadavg5mData(
64
64
 
65
65
  @tools.append
66
66
  def CMS_GetCpuloadavg15mData(
67
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
68
67
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
68
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
69
69
  ):
70
70
  """获取CPU十五分钟平均负载指标数据"""
71
71
  return _get_cms_metric_data(RegionId, InstanceIds, 'load_15m')
72
72
 
73
73
  @tools.append
74
74
  def CMS_GetMemUsedData(
75
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
76
75
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
76
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
77
77
  ):
78
78
  """获取内存使用量指标数据"""
79
79
  return _get_cms_metric_data(RegionId, InstanceIds, 'memory_usedspace')
@@ -81,8 +81,8 @@ def CMS_GetMemUsedData(
81
81
 
82
82
  @tools.append
83
83
  def CMS_GetMemUsageData(
84
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
85
84
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
85
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
86
86
  ):
87
87
  """获取内存利用率指标数据"""
88
88
  return _get_cms_metric_data(RegionId, InstanceIds, 'memory_usedutilization')
@@ -90,8 +90,8 @@ def CMS_GetMemUsageData(
90
90
 
91
91
  @tools.append
92
92
  def CMS_GetDiskUsageData(
93
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
94
93
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
94
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
95
95
  ):
96
96
  """获取磁盘利用率指标数据"""
97
97
  return _get_cms_metric_data(RegionId, InstanceIds, 'diskusage_utilization')
@@ -99,8 +99,8 @@ def CMS_GetDiskUsageData(
99
99
 
100
100
  @tools.append
101
101
  def CMS_GetDiskTotalData(
102
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
103
102
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
103
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
104
104
  ):
105
105
  """获取磁盘分区总容量指标数据"""
106
106
  return _get_cms_metric_data(RegionId, InstanceIds, 'diskusage_total')
@@ -108,8 +108,8 @@ def CMS_GetDiskTotalData(
108
108
 
109
109
  @tools.append
110
110
  def CMS_GetDiskUsedData(
111
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
112
111
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
112
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
113
113
  ):
114
114
  """获取磁盘分区使用量指标数据"""
115
115
  return _get_cms_metric_data(RegionId, InstanceIds, 'diskusage_used')
@@ -47,10 +47,10 @@ def _start_execution_sync(region_id: str, template_name: str, parameters: dict):
47
47
  time.sleep(1)
48
48
  @tools.append
49
49
  def OOS_RunCommand(
50
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
51
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
52
- CommandType: str = Field(description='The type of command executed on the ECS instance, optional value:RunShellScript,RunPythonScript,RunPerlScript,RunBatScript,RunPowerShellScript', default='RunShellScript'),
53
- Command: str = Field(description='Content of the command executed on the ECS instance'),
50
+ Command: str = Field(description='Content of the command executed on the ECS instance'),
51
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
52
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
53
+ CommandType: str = Field(description='The type of command executed on the ECS instance, optional value:RunShellScript,RunPythonScript,RunPerlScript,RunBatScript,RunPowerShellScript', default='RunShellScript')
54
54
  ):
55
55
  """批量在多台ECS实例上运行云助手命令,适用于需要同时管理多台ECS实例的场景,如应用程序管理和资源标记操作等。"""
56
56
 
@@ -74,8 +74,8 @@ def OOS_RunCommand(
74
74
 
75
75
  @tools.append
76
76
  def OOS_StartInstances(
77
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
78
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
77
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
78
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
79
79
  ):
80
80
  """批量启动ECS实例,适用于需要同时管理和启动多台ECS实例的场景,例如应用部署和高可用性场景。"""
81
81
 
@@ -93,9 +93,9 @@ def OOS_StartInstances(
93
93
 
94
94
  @tools.append
95
95
  def OOS_StopInstances(
96
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
97
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
98
- ForeceStop: bool = Field(description='Is forced shutdown required', default=False),
96
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
97
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
98
+ ForeceStop: bool = Field(description='Is forced shutdown required', default=False)
99
99
  ):
100
100
  """批量停止ECS实例,适用于需要同时管理和停止多台ECS实例的场景。"""
101
101
 
@@ -114,9 +114,9 @@ def OOS_StopInstances(
114
114
 
115
115
  @tools.append
116
116
  def OOS_RebootInstances(
117
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
118
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
119
- ForeceStop: bool = Field(description='Is forced shutdown required', default=False),
117
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
118
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
119
+ ForeceStop: bool = Field(description='Is forced shutdown required', default=False)
120
120
  ):
121
121
  """批量重启ECS实例,适用于需要同时管理和重启多台ECS实例的场景。"""
122
122
 
@@ -135,13 +135,13 @@ def OOS_RebootInstances(
135
135
 
136
136
  @tools.append
137
137
  def OOS_RunInstances(
138
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
139
138
  ImageId: str = Field(description='Image ID'),
140
139
  InstanceType: str = Field(description='Instance Type'),
141
140
  SecurityGroupId: str = Field(description='SecurityGroup ID'),
142
141
  VSwitchId: str = Field(description='VSwitch ID'),
142
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
143
143
  Amount: int = Field(description='Number of ECS instances', default=1),
144
- InstanceName: str = Field(description='Instance Name', default=''),
144
+ InstanceName: str = Field(description='Instance Name', default='')
145
145
  ):
146
146
  """批量创建ECS实例,适用于需要同时创建多台ECS实例的场景,例如应用部署和高可用性场景。"""
147
147
 
@@ -158,9 +158,9 @@ def OOS_RunInstances(
158
158
 
159
159
  @tools.append
160
160
  def OOS_ResetPassword(
161
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
162
161
  InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
163
162
  Password: str = Field(description='The password of the ECS instance must be 8-30 characters and must contain only the following characters: lowercase letters, uppercase letters, numbers, and special characters only.()~!@#$%^&*-_+=(40:<>,?/'),
163
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
164
164
  ):
165
165
  """批量修改ECS实例的密码,请注意,本操作将会重启ECS实例"""
166
166
  parameters = {
@@ -177,9 +177,9 @@ def OOS_ResetPassword(
177
177
 
178
178
  @tools.append
179
179
  def OOS_ReplaceSystemDisk(
180
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
181
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
182
- ImageId: str = Field(description='Image ID')
180
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
181
+ ImageId: str = Field(description='Image ID'),
182
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
183
183
  ):
184
184
  """批量替换ECS实例的系统盘,更换操作系统"""
185
185
  parameters = {
@@ -197,8 +197,8 @@ def OOS_ReplaceSystemDisk(
197
197
 
198
198
  @tools.append
199
199
  def OOS_StartRDSInstances(
200
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
201
- InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
200
+ InstanceIds: List[str] = Field(description='AlibabaCloud ECS instance ID List'),
201
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
202
202
  ):
203
203
  """批量启动RDS实例,适用于需要同时管理和启动多台RDS实例的场景,例如应用部署和高可用性场景。"""
204
204
 
@@ -216,8 +216,8 @@ def OOS_StartRDSInstances(
216
216
 
217
217
  @tools.append
218
218
  def OOS_StopRDSInstances(
219
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
220
- InstanceIds: List[str] = Field(description='AlibabaCloud RDS instance ID List')
219
+ InstanceIds: List[str] = Field(description='AlibabaCloud RDS instance ID List'),
220
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
221
221
  ):
222
222
  """批量停止RDS实例,适用于需要同时管理和停止多台RDS实例的场景。"""
223
223
 
@@ -235,8 +235,8 @@ def OOS_StopRDSInstances(
235
235
 
236
236
  @tools.append
237
237
  def OOS_RebootRDSInstances(
238
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
239
- InstanceIds: List[str] = Field(description='AlibabaCloud RDS instance ID List')
238
+ InstanceIds: List[str] = Field(description='AlibabaCloud RDS instance ID List'),
239
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
240
240
  ):
241
241
  """批量重启RDS实例,适用于需要同时管理和重启多台RDS实例的场景。"""
242
242
 
@@ -1,6 +1,7 @@
1
1
  # oss_tools.py
2
2
  import os
3
3
  import alibabacloud_oss_v2 as oss
4
+ from alibaba_cloud_ops_mcp_server.alibabacloud.utils import get_credentials_from_header
4
5
 
5
6
  from pydantic import Field
6
7
  from alibabacloud_oss_v2 import Credentials
@@ -13,10 +14,16 @@ tools = []
13
14
 
14
15
  class CredentialsProvider(EnvironmentVariableCredentialsProvider):
15
16
  def __init__(self) -> None:
16
- credentialsClient = CredClient()
17
- access_key_id = credentialsClient.get_credential().access_key_id
18
- access_key_secret = credentialsClient.get_credential().access_key_secret
19
- session_token = credentialsClient.get_credential().security_token
17
+ credentials = get_credentials_from_header()
18
+ if credentials:
19
+ access_key_id = credentials.get('AccessKeyId', None)
20
+ access_key_secret = credentials.get('AccessKeySecret', None)
21
+ session_token = credentials.get('SecurityToken', None)
22
+ else:
23
+ credentialsClient = CredClient()
24
+ access_key_id = credentialsClient.get_credential().access_key_id
25
+ access_key_secret = credentialsClient.get_credential().access_key_secret
26
+ session_token = credentialsClient.get_credential().security_token
20
27
 
21
28
  self._credentials = Credentials(
22
29
  access_key_id, access_key_secret, session_token)
@@ -51,8 +58,8 @@ def OSS_ListBuckets(
51
58
 
52
59
  @tools.append
53
60
  def OSS_ListObjects(
54
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
55
61
  BucketName: str = Field(description='AlibabaCloud OSS Bucket Name'),
62
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
56
63
  Prefix: str = Field(description='AlibabaCloud OSS Bucket Name prefix', default=None)
57
64
  ):
58
65
  """获取指定OSS存储空间中的所有文件信息。"""
@@ -72,8 +79,8 @@ def OSS_ListObjects(
72
79
 
73
80
  @tools.append
74
81
  def OSS_PutBucket(
75
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
76
82
  BucketName: str = Field(description='AlibabaCloud OSS Bucket Name'),
83
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
77
84
  StorageClass: str = Field(description='The Storage Type of AlibabaCloud OSS Bucket, The value range is as follows: '
78
85
  'Standard (default): standard storage, '
79
86
  'IA: infrequent access, Archive: archive storage, '
@@ -100,8 +107,8 @@ def OSS_PutBucket(
100
107
 
101
108
  @tools.append
102
109
  def OSS_DeleteBucket(
103
- RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou'),
104
- BucketName: str = Field(description='AlibabaCloud OSS Bucket Name')
110
+ BucketName: str = Field(description='AlibabaCloud OSS Bucket Name'),
111
+ RegionId: str = Field(description='AlibabaCloud region ID', default='cn-hangzhou')
105
112
  ):
106
113
  """删除指定的OSS存储空间。"""
107
114
  client = create_client(region_id=RegionId)
@@ -301,9 +301,9 @@ def test_get_ref_api_meta_valid_path(mock_pop_api, mock_std):
301
301
  }
302
302
  assert result == expected
303
303
 
304
- @patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
304
+ @patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
305
305
  def test_get_all_service_info(mock_get):
306
- mock_get.return_value = [
306
+ mock_get.return_value.json.return_value = [
307
307
  {"code": "ecs", "name": "Elastic Compute Service"},
308
308
  {"code": "rds", "name": "Relational Database Service"}
309
309
  ]
@@ -312,3 +312,44 @@ def test_get_all_service_info(mock_get):
312
312
  {"code": "ecs", "name": "Elastic Compute Service"},
313
313
  {"code": "rds", "name": "Relational Database Service"}
314
314
  ]
315
+
316
+
317
+
318
+ @patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
319
+ @patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'DescribeInstances'))
320
+ @patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
321
+ def test_get_api_meta_success(mock_pop_api, mock_get_std, mock_get_ver):
322
+ """测试get_api_meta方法的正常成功路径,覆盖第90-91行"""
323
+ # 模拟get_response_from_pop_api返回的API元数据
324
+ mock_api_data = {
325
+ 'parameters': [
326
+ {'name': 'InstanceIds', 'in': 'query', 'schema': {'type': 'string'}}
327
+ ],
328
+ 'responses': {
329
+ '200': {
330
+ 'schema': {
331
+ 'properties': {
332
+ 'Instances': {'type': 'array'}
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ mock_pop_api.return_value = mock_api_data
339
+
340
+ # 调用get_api_meta
341
+ data, version = api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
342
+
343
+ # 验证返回值
344
+ assert data == mock_api_data
345
+ assert version == '2014-05-26'
346
+
347
+ # 验证调用了正确的方法
348
+ mock_get_ver.assert_called_once_with('ecs')
349
+ mock_get_std.assert_called_once_with('ecs', 'DescribeInstances', '2014-05-26')
350
+ mock_pop_api.assert_called_once_with(
351
+ api_meta_client.ApiMetaClient.GET_API_INFO,
352
+ 'ecs',
353
+ 'DescribeInstances',
354
+ '2014-05-26'
355
+ )
@@ -0,0 +1,75 @@
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)
16
+
17
+ def test_get_credentials_from_header_success():
18
+ """测试从header成功获取凭证的情况"""
19
+ with patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.get_http_request') as mock_get_request:
20
+ mock_request = MagicMock()
21
+ mock_request.headers = {
22
+ 'x-acs-accesskey-id': 'test_id',
23
+ 'x-acs-accesskey-secret': 'test_secret',
24
+ 'x-acs-security-token': 'test_token'
25
+ }
26
+ mock_get_request.return_value = mock_request
27
+
28
+ result = utils.get_credentials_from_header()
29
+ expected = {
30
+ 'AccessKeyId': 'test_id',
31
+ 'AccessKeySecret': 'test_secret',
32
+ 'SecurityToken': 'test_token'
33
+ }
34
+ assert result == expected
35
+
36
+ def test_get_credentials_from_header_no_access_key():
37
+ """测试header中没有access_key_id的情况"""
38
+ with patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.get_http_request') as mock_get_request:
39
+ mock_request = MagicMock()
40
+ mock_request.headers = {}
41
+ mock_get_request.return_value = mock_request
42
+
43
+ result = utils.get_credentials_from_header()
44
+ assert result is None
45
+
46
+ def test_get_credentials_from_header_exception():
47
+ """测试get_http_request抛出异常的情况"""
48
+ with patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.get_http_request') as mock_get_request, \
49
+ patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.logger') as mock_logger:
50
+ mock_get_request.side_effect = Exception('test error')
51
+
52
+ result = utils.get_credentials_from_header()
53
+ assert result is None
54
+ mock_logger.info.assert_called_once_with('get_credentials_from_header error: test error')
55
+
56
+ def test_create_config_with_credentials():
57
+ """测试使用header中的凭证创建config的情况"""
58
+ with patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.get_credentials_from_header') as mock_get_creds, \
59
+ patch('alibaba_cloud_ops_mcp_server.alibabacloud.utils.Config') as mock_cfg:
60
+ mock_get_creds.return_value = {
61
+ 'AccessKeyId': 'test_id',
62
+ 'AccessKeySecret': 'test_secret',
63
+ 'SecurityToken': 'test_token'
64
+ }
65
+ cfg = MagicMock()
66
+ mock_cfg.return_value = cfg
67
+
68
+ result = utils.create_config()
69
+ assert result is cfg
70
+ assert cfg.user_agent == 'alibaba-cloud-ops-mcp-server'
71
+ mock_cfg.assert_called_once_with(
72
+ access_key_id='test_id',
73
+ access_key_secret='test_secret',
74
+ security_token='test_token'
75
+ )
@@ -0,0 +1,102 @@
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', services='ecs')
15
+ mock_FastMCP.assert_called_once_with(
16
+ name='alibaba-cloud-ops-mcp-server',
17
+ port=12345, host='127.0.0.1')
18
+ assert mcp.tool.call_count == 7 # common_api_tools 4 + oss/oos/cms 各1
19
+ mock_create_api_tools.assert_called_once()
20
+ mcp.run.assert_called_once_with(transport='stdio')
21
+
22
+
23
+ @patch('alibaba_cloud_ops_mcp_server.server.FastMCP')
24
+ @patch('alibaba_cloud_ops_mcp_server.server.api_tools.create_api_tools')
25
+ def test_main_run_without_services(mock_create_api_tools, mock_FastMCP):
26
+ """测试不指定services参数时的情况"""
27
+ with patch('alibaba_cloud_ops_mcp_server.server.oss_tools.tools', [lambda: None]), \
28
+ patch('alibaba_cloud_ops_mcp_server.server.oos_tools.tools', [lambda: None]), \
29
+ patch('alibaba_cloud_ops_mcp_server.server.cms_tools.tools', [lambda: None]):
30
+ from alibaba_cloud_ops_mcp_server import server
31
+ mcp = MagicMock()
32
+ mock_FastMCP.return_value = mcp
33
+ # 调用main函数,不指定services
34
+ server.main.callback(transport='stdio', port=8000, host='127.0.0.1', services=None)
35
+ mock_FastMCP.assert_called_once_with(
36
+ name='alibaba-cloud-ops-mcp-server',
37
+ port=8000, host='127.0.0.1')
38
+ # 不指定services时,应该只有oss/oos/cms的工具被添加,没有common_api_tools
39
+ assert mcp.tool.call_count == 3 # oss/oos/cms 各1
40
+ mock_create_api_tools.assert_called_once()
41
+ mcp.run.assert_called_once_with(transport='stdio')
42
+
43
+
44
+ def test_main_module_execution():
45
+ """测试模块直接执行时的入口点(第78-79行)"""
46
+ import subprocess
47
+ import sys
48
+ import os
49
+
50
+ # 获取server.py的路径
51
+ server_path = os.path.join(os.path.dirname(__file__), '../src/alibaba_cloud_ops_mcp_server/server.py')
52
+ server_path = os.path.abspath(server_path)
53
+
54
+ # 使用subprocess来模拟直接执行模块,但立即终止以避免实际运行服务器
55
+ try:
56
+ # 使用timeout来快速终止进程,只是为了测试入口点能否正常启动
57
+ result = subprocess.run([sys.executable, server_path, '--help'],
58
+ capture_output=True, text=True, timeout=5)
59
+ # 如果能显示帮助信息,说明main函数和入口点工作正常
60
+ assert 'Transport type' in result.stdout or result.returncode == 0
61
+ except subprocess.TimeoutExpired:
62
+ # 超时也是可以接受的,说明程序启动了
63
+ pass
64
+
65
+
66
+ @patch('alibaba_cloud_ops_mcp_server.server.FastMCP')
67
+ @patch('alibaba_cloud_ops_mcp_server.server.api_tools.create_api_tools')
68
+ def test_main_run_multiple_services(mock_create_api_tools, mock_FastMCP):
69
+ """测试指定多个services的情况"""
70
+ with patch('alibaba_cloud_ops_mcp_server.server.oss_tools.tools', [lambda: None]), \
71
+ patch('alibaba_cloud_ops_mcp_server.server.oos_tools.tools', [lambda: None]), \
72
+ patch('alibaba_cloud_ops_mcp_server.server.cms_tools.tools', [lambda: None]), \
73
+ patch('alibaba_cloud_ops_mcp_server.server.common_api_tools.tools', [lambda: None, lambda: None]):
74
+ from alibaba_cloud_ops_mcp_server import server
75
+ mcp = MagicMock()
76
+ mock_FastMCP.return_value = mcp
77
+ # 调用main函数,指定多个services
78
+ server.main.callback(transport='sse', port=9000, host='0.0.0.0', services='ecs,vpc,rds')
79
+ mock_FastMCP.assert_called_once_with(
80
+ name='alibaba-cloud-ops-mcp-server',
81
+ port=9000, host='0.0.0.0')
82
+ # common_api_tools 2 + oss/oos/cms 各1 = 5
83
+ assert mcp.tool.call_count == 5
84
+ mock_create_api_tools.assert_called_once()
85
+ mcp.run.assert_called_once_with(transport='sse')
86
+
87
+
88
+ @patch('alibaba_cloud_ops_mcp_server.server.FastMCP')
89
+ @patch('alibaba_cloud_ops_mcp_server.server.api_tools.create_api_tools')
90
+ @patch('alibaba_cloud_ops_mcp_server.server.logger')
91
+ def test_main_run_with_logging(mock_logger, mock_create_api_tools, mock_FastMCP):
92
+ """测试日志输出(第77行)"""
93
+ with patch('alibaba_cloud_ops_mcp_server.server.oss_tools.tools', []), \
94
+ patch('alibaba_cloud_ops_mcp_server.server.oos_tools.tools', []), \
95
+ patch('alibaba_cloud_ops_mcp_server.server.cms_tools.tools', []):
96
+ from alibaba_cloud_ops_mcp_server import server
97
+ mcp = MagicMock()
98
+ mock_FastMCP.return_value = mcp
99
+ # 调用main函数
100
+ server.main.callback(transport='streamable-http', port=8080, host='localhost', services=None)
101
+ # 验证日志被调用
102
+ mock_logger.debug.assert_called_once_with('mcp server is running on streamable-http mode.')
@@ -68,6 +68,20 @@ def test_CredentialsProvider_and_get_credentials(mock_cred_client):
68
68
  assert credentials.access_key_secret == 'secret'
69
69
  assert credentials.security_token == 'token'
70
70
 
71
+ @patch('alibaba_cloud_ops_mcp_server.tools.oss_tools.get_credentials_from_header')
72
+ def test_CredentialsProvider_with_header_credentials(mock_get_creds):
73
+ """测试从header获取凭证的CredentialsProvider"""
74
+ mock_get_creds.return_value = {
75
+ 'AccessKeyId': 'header_id',
76
+ 'AccessKeySecret': 'header_secret',
77
+ 'SecurityToken': 'header_token'
78
+ }
79
+ provider = oss_tools.CredentialsProvider()
80
+ credentials = provider.get_credentials()
81
+ assert credentials.access_key_id == 'header_id'
82
+ assert credentials.access_key_secret == 'header_secret'
83
+ assert credentials.security_token == 'header_token'
84
+
71
85
  @patch('alibaba_cloud_ops_mcp_server.tools.oss_tools.CredentialsProvider')
72
86
  @patch('alibaba_cloud_ops_mcp_server.tools.oss_tools.oss')
73
87
  def test_create_client(mock_oss, mock_provider):
@@ -116,7 +116,7 @@ wheels = [
116
116
 
117
117
  [[package]]
118
118
  name = "alibaba-cloud-ops-mcp-server"
119
- version = "0.9.0"
119
+ version = "0.9.2"
120
120
  source = { editable = "." }
121
121
  dependencies = [
122
122
  { name = "alibabacloud-cms20190101" },
@@ -125,7 +125,7 @@ dependencies = [
125
125
  { name = "alibabacloud-oos20190601" },
126
126
  { name = "alibabacloud-oss-v2" },
127
127
  { name = "click" },
128
- { name = "mcp", extra = ["cli"] },
128
+ { name = "fastmcp" },
129
129
  ]
130
130
 
131
131
  [package.dev-dependencies]
@@ -142,7 +142,7 @@ requires-dist = [
142
142
  { name = "alibabacloud-oos20190601", specifier = ">=3.4.1" },
143
143
  { name = "alibabacloud-oss-v2", specifier = ">=1.1.0" },
144
144
  { name = "click", specifier = ">=8.1.8" },
145
- { name = "mcp", extras = ["cli"], specifier = ">=1.9.0" },
145
+ { name = "fastmcp", specifier = "==2.8.0" },
146
146
  ]
147
147
 
148
148
  [package.metadata.requires-dev]
@@ -348,6 +348,18 @@ wheels = [
348
348
  { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
349
349
  ]
350
350
 
351
+ [[package]]
352
+ name = "authlib"
353
+ version = "1.6.0"
354
+ source = { registry = "https://pypi.org/simple" }
355
+ dependencies = [
356
+ { name = "cryptography" },
357
+ ]
358
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371 }
359
+ wheels = [
360
+ { url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981 },
361
+ ]
362
+
351
363
  [[package]]
352
364
  name = "certifi"
353
365
  version = "2025.1.31"
@@ -652,6 +664,25 @@ wheels = [
652
664
  { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
653
665
  ]
654
666
 
667
+ [[package]]
668
+ name = "fastmcp"
669
+ version = "2.8.0"
670
+ source = { registry = "https://pypi.org/simple" }
671
+ dependencies = [
672
+ { name = "authlib" },
673
+ { name = "exceptiongroup" },
674
+ { name = "httpx" },
675
+ { name = "mcp" },
676
+ { name = "openapi-pydantic" },
677
+ { name = "python-dotenv" },
678
+ { name = "rich" },
679
+ { name = "typer" },
680
+ ]
681
+ sdist = { url = "https://files.pythonhosted.org/packages/8d/a3/d5b2c47b25d13cca8108e077bf4a72b255b113fb525f4c22ce9ca5af9b08/fastmcp-2.8.0.tar.gz", hash = "sha256:8a6427ece23d0a324d4be2043598c8b89a91b2b5688873d8ae1e7aeaa7960513", size = 2554559 }
682
+ wheels = [
683
+ { url = "https://files.pythonhosted.org/packages/2c/05/b9b0ee091578ff37da8ef0bee8fff80bed95daff61834982a6064e3e327f/fastmcp-2.8.0-py3-none-any.whl", hash = "sha256:772468e98dacd55ab3381f49dd2583341c41b0e5ef0d9c7620fd43833d949c0c", size = 137492 },
684
+ ]
685
+
655
686
  [[package]]
656
687
  name = "frozenlist"
657
688
  version = "1.5.0"
@@ -799,7 +830,7 @@ wheels = [
799
830
 
800
831
  [[package]]
801
832
  name = "mcp"
802
- version = "1.9.1"
833
+ version = "1.9.4"
803
834
  source = { registry = "https://pypi.org/simple" }
804
835
  dependencies = [
805
836
  { name = "anyio" },
@@ -812,15 +843,9 @@ dependencies = [
812
843
  { name = "starlette" },
813
844
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
814
845
  ]
815
- sdist = { url = "https://files.pythonhosted.org/packages/e7/bc/54aec2c334698cc575ca3b3481eed627125fb66544152fa1af927b1a495c/mcp-1.9.1.tar.gz", hash = "sha256:19879cd6dde3d763297617242888c2f695a95dfa854386a6a68676a646ce75e4", size = 316247 }
846
+ sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294 }
816
847
  wheels = [
817
- { url = "https://files.pythonhosted.org/packages/a6/c0/4ac795585a22a0a2d09cd2b1187b0252d2afcdebd01e10a68bbac4d34890/mcp-1.9.1-py3-none-any.whl", hash = "sha256:2900ded8ffafc3c8a7bfcfe8bc5204037e988e753ec398f371663e6a06ecd9a9", size = 130261 },
818
- ]
819
-
820
- [package.optional-dependencies]
821
- cli = [
822
- { name = "python-dotenv" },
823
- { name = "typer" },
848
+ { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232 },
824
849
  ]
825
850
 
826
851
  [[package]]
@@ -929,6 +954,18 @@ wheels = [
929
954
  { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 },
930
955
  ]
931
956
 
957
+ [[package]]
958
+ name = "openapi-pydantic"
959
+ version = "0.5.1"
960
+ source = { registry = "https://pypi.org/simple" }
961
+ dependencies = [
962
+ { name = "pydantic" },
963
+ ]
964
+ sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 }
965
+ wheels = [
966
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
967
+ ]
968
+
932
969
  [[package]]
933
970
  name = "packaging"
934
971
  version = "25.0"
@@ -1,9 +0,0 @@
1
- from alibabacloud_credentials.client import Client as CredClient
2
- from alibabacloud_tea_openapi.models import Config
3
-
4
-
5
- def create_config():
6
- credentialsClient = CredClient()
7
- config = Config(credential=credentialsClient)
8
- config.user_agent = 'alibaba-cloud-ops-mcp-server'
9
- return config
@@ -1,15 +0,0 @@
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)
@@ -1,32 +0,0 @@
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', services='ecs')
15
- mock_FastMCP.assert_called_once_with(
16
- name='alibaba-cloud-ops-mcp-server',
17
- port=12345, host='127.0.0.1')
18
- assert mcp.add_tool.call_count == 7 # common_api_tools 4 + oss/oos/cms 各1
19
- mock_create_api_tools.assert_called_once()
20
- mcp.run.assert_called_once_with(transport='stdio')
21
-
22
- def test_run_as_main(monkeypatch):
23
- import runpy, sys
24
- from alibaba_cloud_ops_mcp_server import server
25
- monkeypatch.setattr(server, 'main', lambda *a, **kw: None)
26
- monkeypatch.setattr(sys, 'argv', ['server.py'])
27
- import mcp.server.fastmcp
28
- monkeypatch.setattr(mcp.server.fastmcp.FastMCP, 'run', lambda self, **kwargs: None)
29
- import pytest
30
- with pytest.raises(SystemExit) as e:
31
- runpy.run_path('src/alibaba_cloud_ops_mcp_server/server.py', run_name='__main__')
32
- assert e.value.code == 0