Skip to content

Commit 7c70262

Browse files
authored
Merge pull request #27 from aliyun/fix/parameters_list_json_str
Optimize the list parameter processing of ECS services
2 parents 35b8ceb + 6c63219 commit 7c70262

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "alibaba-cloud-ops-mcp-server"
3-
version = "0.8.7"
3+
version = "0.8.8"
44
description = "A MCP server for Alibaba Cloud"
55
readme = "README.md"
66
authors = [

src/alibaba_cloud_ops_mcp_server/tools/api_tools.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from mcp.server.fastmcp import FastMCP, Context
33
from pydantic import Field
44
import logging
5+
import json
56

67
import inspect
78
import types
@@ -32,15 +33,32 @@ def create_client(service: str, region_id: str) -> OpenApiClient:
3233
return OpenApiClient(config)
3334

3435

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+
3545
def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
3646
service = service.lower()
3747
api_meta, _ = ApiMetaClient.get_api_meta(service, api)
3848
version = ApiMetaClient.get_service_version(service)
3949
method = 'POST' if api_meta.get('methods', [])[0] == 'post' else 'GET'
4050
path = api_meta.get('path', '/')
4151
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+
4260
req = open_api_models.OpenApiRequest(
43-
query=OpenApiUtilClient.query(parameters)
61+
query=OpenApiUtilClient.query(processed_parameters)
4462
)
4563
params = open_api_models.Params(
4664
action=api,
@@ -53,7 +71,7 @@ def _tools_api_call(service: str, api: str, parameters: dict, ctx: Context):
5371
req_body_type='formData',
5472
body_type='json'
5573
)
56-
client = create_client(service, parameters.get('RegionId', 'cn-hangzhou'))
74+
client = create_client(service, processed_parameters.get('RegionId', 'cn-hangzhou'))
5775
runtime = util_models.RuntimeOptions()
5876
return client.call_api(params, req, runtime)
5977

@@ -75,9 +93,15 @@ def _create_function_schemas(service, api, api_meta):
7593
description = schema.get('description', '')
7694
example = schema.get('example', '')
7795
type_ = schema.get('type', '')
78-
description = f'{description} 请注意,提供参数要严格按照参数的类型和参数示例的提示,如果提到参数为String,且为一个 JSON 数组字符串,应在数组内使用单引号包裹对应的参数以避免转义问题,并在最外侧用双引号包裹以确保其是字符串,否则可能会导致参数解析错误。参数类型: {type_},参数示例:{example}'
96+
description = f'{description} 参数类型: {type_},参数示例:{example}'
7997
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:
101+
python_type = list
102+
else:
103+
python_type = type_map.get(type_, str)
104+
81105
field_info = (
82106
python_type,
83107
field(

tests/tools/test_api_tools.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from unittest.mock import patch, MagicMock
33
from alibaba_cloud_ops_mcp_server.tools import api_tools
4+
import json
45

56
def fake_api_meta(post=False, no_summary=False):
67
meta = {
@@ -127,3 +128,104 @@ def test_create_and_decorate_tool_api_meta_exception():
127128
with pytest.raises(Exception) as e:
128129
api_tools._create_and_decorate_tool(mcp, 'ecs', 'DescribeInstances')
129130
assert 'meta-fail' in str(e.value)
131+
132+
def test_create_function_schemas_ecs_list_parameters():
133+
# 测试ECS服务的特殊参数处理
134+
api_meta = {
135+
'parameters': [
136+
{'name': 'InstanceIds', 'schema': {'type': 'string', 'description': '实例ID列表', 'example': '["i-123", "i-456"]', 'required': True}},
137+
{'name': 'SecurityGroupIds', 'schema': {'type': 'string', 'description': '安全组ID列表', 'example': '["sg-123", "sg-456"]', 'required': False}},
138+
{'name': 'NormalParam', 'schema': {'type': 'string', 'description': '普通参数', 'example': 'test', 'required': False}},
139+
],
140+
'methods': ['get'],
141+
'path': '/test',
142+
'summary': '测试API'
143+
}
144+
145+
# 测试ECS服务
146+
schemas = api_tools._create_function_schemas('ecs', 'DescribeInstances', api_meta)
147+
assert schemas['DescribeInstances']['InstanceIds'][0] == list
148+
assert schemas['DescribeInstances']['SecurityGroupIds'][0] == list
149+
assert schemas['DescribeInstances']['NormalParam'][0] == str
150+
151+
# 测试非ECS服务
152+
schemas = api_tools._create_function_schemas('rds', 'DescribeInstances', api_meta)
153+
assert schemas['DescribeInstances']['InstanceIds'][0] == str
154+
assert schemas['DescribeInstances']['SecurityGroupIds'][0] == str
155+
assert schemas['DescribeInstances']['NormalParam'][0] == str
156+
157+
def test_tools_api_call_ecs_list_parameters():
158+
with patch('alibaba_cloud_ops_mcp_server.tools.api_tools.ApiMetaClient') as mock_ApiMetaClient, \
159+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.create_client') as mock_create_client, \
160+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.open_api_models') as mock_open_api_models, \
161+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.OpenApiUtilClient') as mock_OpenApiUtilClient, \
162+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.util_models') as mock_util_models:
163+
164+
mock_ApiMetaClient.get_api_meta.return_value = fake_api_meta()
165+
mock_ApiMetaClient.get_service_version.return_value = '2023-01-01'
166+
mock_ApiMetaClient.get_service_style.return_value = 'RPC'
167+
mock_open_api_models.OpenApiRequest.return_value = MagicMock()
168+
mock_open_api_models.Params.return_value = MagicMock()
169+
mock_create_client.return_value.call_api.return_value = {'result': 'ok'}
170+
mock_OpenApiUtilClient.query.return_value = {}
171+
mock_util_models.RuntimeOptions.return_value = MagicMock()
172+
173+
# 测试ECS服务的列表参数转换
174+
params = {
175+
'InstanceIds': ['i-123', 'i-456'],
176+
'SecurityGroupIds': ['sg-123', 'sg-456'],
177+
'NormalParam': 'test',
178+
'RegionId': 'cn-hangzhou'
179+
}
180+
181+
# 测试ECS服务
182+
result = api_tools._tools_api_call('ecs', 'DescribeInstances', params, None)
183+
# 验证传入query方法的参数
184+
query_args = mock_OpenApiUtilClient.query.call_args[0][0]
185+
assert isinstance(query_args['InstanceIds'], str)
186+
assert isinstance(query_args['SecurityGroupIds'], str)
187+
assert query_args['NormalParam'] == 'test'
188+
assert json.loads(query_args['InstanceIds']) == ['i-123', 'i-456']
189+
assert json.loads(query_args['SecurityGroupIds']) == ['sg-123', 'sg-456']
190+
191+
# 重置mock
192+
mock_OpenApiUtilClient.query.reset_mock()
193+
194+
# 测试非ECS服务
195+
result = api_tools._tools_api_call('rds', 'DescribeInstances', params, None)
196+
# 验证传入query方法的参数
197+
query_args = mock_OpenApiUtilClient.query.call_args[0][0]
198+
assert isinstance(query_args['InstanceIds'], list)
199+
assert isinstance(query_args['SecurityGroupIds'], list)
200+
assert query_args['InstanceIds'] == ['i-123', 'i-456']
201+
assert query_args['SecurityGroupIds'] == ['sg-123', 'sg-456']
202+
203+
def test_tools_api_call_ecs_list_parameters_non_list():
204+
with patch('alibaba_cloud_ops_mcp_server.tools.api_tools.ApiMetaClient') as mock_ApiMetaClient, \
205+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.create_client') as mock_create_client, \
206+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.open_api_models') as mock_open_api_models, \
207+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.OpenApiUtilClient') as mock_OpenApiUtilClient, \
208+
patch('alibaba_cloud_ops_mcp_server.tools.api_tools.util_models') as mock_util_models:
209+
210+
mock_ApiMetaClient.get_api_meta.return_value = fake_api_meta()
211+
mock_ApiMetaClient.get_service_version.return_value = '2023-01-01'
212+
mock_ApiMetaClient.get_service_style.return_value = 'RPC'
213+
mock_open_api_models.OpenApiRequest.return_value = MagicMock()
214+
mock_open_api_models.Params.return_value = MagicMock()
215+
mock_create_client.return_value.call_api.return_value = {'result': 'ok'}
216+
mock_OpenApiUtilClient.query.return_value = {}
217+
mock_util_models.RuntimeOptions.return_value = MagicMock()
218+
219+
# 测试非列表类型的特殊参数
220+
params = {
221+
'InstanceIds': 'i-123', # 字符串而不是列表
222+
'SecurityGroupIds': None, # None值
223+
'RegionId': 'cn-hangzhou'
224+
}
225+
226+
# 测试ECS服务
227+
result = api_tools._tools_api_call('ecs', 'DescribeInstances', params, None)
228+
# 验证传入query方法的参数
229+
query_args = mock_OpenApiUtilClient.query.call_args[0][0]
230+
assert query_args['InstanceIds'] == 'i-123'
231+
assert query_args['SecurityGroupIds'] is None

0 commit comments

Comments
 (0)