├── .python-version ├── libs ├── MANIFEST.in ├── setup.cfg ├── sts-20150401 │ ├── MANIFEST.in │ ├── setup.cfg │ ├── alibabacloud_sts20150401 │ │ ├── __init__.py │ │ └── models │ │ │ ├── _generate_session_access_key_request.py │ │ │ ├── _generate_token_by_ticket_request.py │ │ │ ├── _generate_token_by_ticket_response_body_assumed_role_user.py │ │ │ ├── _get_federation_token_response_body_federated_user.py │ │ │ ├── _assume_role_with_service_identity_response_body_assumed_role_user.py │ │ │ ├── _assume_role_response_body_assumed_role_user.py │ │ │ ├── _assume_role_with_oidcresponse_body_assumed_role_user.py │ │ │ ├── _assume_role_with_samlresponse_body_assumed_role_user.py │ │ │ ├── _get_federation_token_request.py │ │ │ ├── _generate_session_access_key_response_body.py │ │ │ ├── _assume_role_response.py │ │ │ ├── _get_caller_identity_response.py │ │ │ ├── _assume_role_with_oidcresponse.py │ │ │ ├── _assume_role_with_samlresponse.py │ │ │ ├── _get_federation_token_response.py │ │ │ ├── _generate_token_by_ticket_response.py │ │ │ ├── _generate_session_access_key_response.py │ │ │ ├── _generate_session_access_key_response_body_session_access_key.py │ │ │ ├── _assume_role_with_service_identity_response.py │ │ │ ├── _get_federation_token_response_body_credentials.py │ │ │ ├── _generate_token_by_ticket_response_body_credentials.py │ │ │ ├── _assume_role_with_service_identity_response_body_credentials.py │ │ │ ├── _get_federation_token_response_body.py │ │ │ ├── _generate_token_by_ticket_response_body.py │ │ │ ├── _assume_role_with_service_identity_response_body.py │ │ │ ├── _assume_role_response_body.py │ │ │ ├── _assume_role_response_body_credentials.py │ │ │ ├── _assume_role_with_oidcresponse_body_credentials.py │ │ │ ├── _assume_role_with_samlresponse_body_credentials.py │ │ │ ├── _assume_role_with_service_identity_request.py │ │ │ ├── _assume_role_with_samlresponse_body_samlassertion_info.py │ │ │ ├── _get_caller_identity_response_body.py │ │ │ ├── _assume_role_with_oidcresponse_body.py │ │ │ ├── _assume_role_with_samlresponse_body.py │ │ │ ├── _assume_role_with_oidcresponse_body_oidctoken_info.py │ │ │ ├── _assume_role_with_samlrequest.py │ │ │ ├── __init__.py │ │ │ ├── _assume_role_with_oidcrequest.py │ │ │ └── _assume_role_request.py │ ├── LICENSE │ ├── ChangeLog.md │ ├── README-CN.md │ ├── .gitignore │ ├── README.md │ └── setup.py ├── LICENSE ├── ChangeLog.md ├── README-CN.md ├── .gitignore ├── README.md └── setup.py ├── tests ├── mcp_server_aliyun_observability │ └── toolkits │ │ ├── iaas │ │ └── __init__.py │ │ ├── paas │ │ ├── __init__.py │ │ ├── test_paas_entity_toolkit.py │ │ └── test_paas_dataset_toolkit.py │ │ └── shared │ │ └── __init__.py └── test_settings_endpoints.py ├── requirements-dev.txt ├── src └── mcp_server_aliyun_observability │ ├── toolkits │ ├── paas │ │ ├── __init__.py │ │ ├── toolkit.py │ │ ├── time_utils.py │ │ └── dataset_toolkit.py │ ├── iaas │ │ └── __init__.py │ ├── shared │ │ ├── __init__.py │ │ └── toolkit.py │ └── __init__.py │ ├── __main__.py │ ├── core │ ├── __init__.py │ ├── models.py │ └── utils.py │ ├── config.py │ ├── server.py │ ├── __init__.py │ ├── settings.py │ ├── api_error.py │ └── logger.py ├── images ├── npx_debug.png ├── chatwise_demo.png ├── cursor_demo.png ├── cursor_inter.png ├── cursor_tools.png ├── chatwise_inter.png ├── cherry_studio_demo.png ├── find_slowest_trace.png ├── search_log_store.png ├── cherry_studio_inter.png └── fuzzy_search_and_get_logs.png ├── pytest.ini ├── FAQ.md ├── requirements.txt ├── sample └── config │ └── knowledge_config.json ├── license ├── .gitignore ├── pyproject.toml ├── CHANGELOG.md ├── CONTRIBUTION.md └── README_EN.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 -------------------------------------------------------------------------------- /libs/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md -------------------------------------------------------------------------------- /libs/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE -------------------------------------------------------------------------------- /libs/sts-20150401/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md -------------------------------------------------------------------------------- /tests/mcp_server_aliyun_observability/toolkits/iaas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mcp_server_aliyun_observability/toolkits/paas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/sts-20150401/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE -------------------------------------------------------------------------------- /tests/mcp_server_aliyun_observability/toolkits/shared/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.6" 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.0.0 2 | pytest-asyncio>=0.21.0 3 | pytest-cov>=4.0.0 4 | pytest-mock>=3.10.0 -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/paas/__init__.py: -------------------------------------------------------------------------------- 1 | """PaaS Layer Toolkit - Platform as a Service""" -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/iaas/__init__.py: -------------------------------------------------------------------------------- 1 | """IaaS Layer Toolkit - Infrastructure as a Service""" -------------------------------------------------------------------------------- /images/npx_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/npx_debug.png -------------------------------------------------------------------------------- /images/chatwise_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/chatwise_demo.png -------------------------------------------------------------------------------- /images/cursor_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/cursor_demo.png -------------------------------------------------------------------------------- /images/cursor_inter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/cursor_inter.png -------------------------------------------------------------------------------- /images/cursor_tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/cursor_tools.png -------------------------------------------------------------------------------- /images/chatwise_inter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/chatwise_inter.png -------------------------------------------------------------------------------- /images/cherry_studio_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/cherry_studio_demo.png -------------------------------------------------------------------------------- /images/find_slowest_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/find_slowest_trace.png -------------------------------------------------------------------------------- /images/search_log_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/search_log_store.png -------------------------------------------------------------------------------- /images/cherry_studio_inter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/cherry_studio_inter.png -------------------------------------------------------------------------------- /images/fuzzy_search_and_get_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-observability-mcp-server/HEAD/images/fuzzy_search_and_get_logs.png -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/__main__.py: -------------------------------------------------------------------------------- 1 | from mcp_server_aliyun_observability import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | addopts = -v --tb=short 7 | asyncio_default_fixture_loop_scope = function -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## CherryStudio 问题 4 | ### 使用 SSE 访问时候,提示 "启动失败" Error invoking remote method 'mcp::list-tools':Error: SSE error: Non-200 status code (404)" 5 | 6 | 这个一般是端口被其他服务占用,可以检查下端口是否被占用,或者更换端口。 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/shared/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared toolkits for PaaS and DoAI layers 3 | 4 | This module contains tools that are shared between PaaS and DoAI layers, 5 | but not needed by the IaaS layer. 6 | """ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.12.0 2 | pydantic>=2.10.0 3 | alibabacloud_arms20190808>=8.0.0 4 | alibabacloud_credentials>=1.0.1 5 | tenacity>=8.0.0 6 | pytest>=7.0.0 7 | pytest-asyncio>=0.21.0 8 | pytest-cov>=4.0.0 9 | pytest-mock>=3.10.0 10 | alibabacloud_sls20201230==5.7.0 11 | rich>=13.0.0 12 | pandas==2.3.0 13 | jinja2>=3.1.0 14 | numpy 15 | pyyaml>=6.0 16 | aiohttp>=3.8.0 17 | requests>=2.28.0 18 | libs/sts-20150401 -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | New MCP Toolkits - Three Layer Architecture 3 | 4 | This module contains the restructured MCP toolkits organized into three layers: 5 | - iaas: Infrastructure as a Service layer (text_to_sql, execute_sql, execute_promql) 6 | - paas: Platform as a Service layer (ported from umodel-mcp handlers with paas_ prefix) 7 | """ 8 | 9 | from mcp_server_aliyun_observability.toolkits.iaas.toolkit import IaaSToolkit 10 | from mcp_server_aliyun_observability.toolkits.paas.toolkit import PaaSToolkit 11 | 12 | __all__ = ["IaaSToolkit", "PaaSToolkit"] -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Core infrastructure for Alibaba Cloud Observability MCP Server""" 2 | 3 | from mcp_server_aliyun_observability.core.models import ( 4 | EntitySelector, 5 | TimeRange, 6 | MetricQuery, 7 | TraceFilter, 8 | EventFilter, 9 | BaseToolParams, 10 | ) 11 | # from mcp_server_aliyun_observability.core.decorators import validate_args # DEPRECATED 12 | 13 | __all__ = [ 14 | "EntitySelector", 15 | "TimeRange", 16 | "MetricQuery", 17 | "TraceFilter", 18 | "EventFilter", 19 | "BaseToolParams", 20 | # "validate_args", # DEPRECATED 21 | ] -------------------------------------------------------------------------------- /libs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /libs/sts-20150401/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /libs/ChangeLog.md: -------------------------------------------------------------------------------- 1 | 2025-04-01 Version: 1.1.5 2 | - Generated python 2015-04-01 for Sts. 3 | 4 | 2023-10-11 Version: 1.1.4 5 | - Generated python 2015-04-01 for Sts. 6 | 7 | 2023-03-02 Version: 1.1.3 8 | - Publish AssumeRole API With ExternalId Parameter. 9 | 10 | 2022-09-05 Version: 1.1.2 11 | - Generated python 2015-04-01 for Sts. 12 | 13 | 2022-03-26 Version: 1.1.1 14 | - Generated python 2015-04-01 for Sts. 15 | 16 | 2021-09-29 Version: 1.1.0 17 | - Supported AssumeRoleWithOIDC. 18 | 19 | 2021-07-27 Version: 1.0.1 20 | - Generated python 2015-04-01 for Sts. 21 | 22 | 2020-12-30 Version: 1.0.0 23 | - AMP Version Change. 24 | 25 | 2020-12-30 Version: 1.0.0 26 | - AMP Version Change. 27 | 28 | -------------------------------------------------------------------------------- /libs/sts-20150401/ChangeLog.md: -------------------------------------------------------------------------------- 1 | 2025-04-01 Version: 1.1.5 2 | - Generated python 2015-04-01 for Sts. 3 | 4 | 2023-10-11 Version: 1.1.4 5 | - Generated python 2015-04-01 for Sts. 6 | 7 | 2023-03-02 Version: 1.1.3 8 | - Publish AssumeRole API With ExternalId Parameter. 9 | 10 | 2022-09-05 Version: 1.1.2 11 | - Generated python 2015-04-01 for Sts. 12 | 13 | 2022-03-26 Version: 1.1.1 14 | - Generated python 2015-04-01 for Sts. 15 | 16 | 2021-09-29 Version: 1.1.0 17 | - Supported AssumeRoleWithOIDC. 18 | 19 | 2021-07-27 Version: 1.0.1 20 | - Generated python 2015-04-01 for Sts. 21 | 22 | 2020-12-30 Version: 1.0.0 23 | - AMP Version Change. 24 | 25 | 2020-12-30 Version: 1.0.0 26 | - AMP Version Change. 27 | 28 | -------------------------------------------------------------------------------- /sample/config/knowledge_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_endpoint": { 3 | "uri": "https://api.default.com", 4 | "key": "Bearer dataset-***" 5 | }, 6 | "projects": { 7 | "project1": { 8 | "default_endpoint": { 9 | "uri": "https://api.project1.com", 10 | "key": "Bearer dataset-***" 11 | }, 12 | "logstore1": { 13 | "uri": "https://api.project1.logstore1.com", 14 | "key": "Bearer dataset-***" 15 | }, 16 | "logstore2": { 17 | "uri": "https://api.project1.logstore2.com", 18 | "key": "Bearer dataset-***" 19 | } 20 | }, 21 | "project2": { 22 | "logstore3": { 23 | "uri": "https://api.project2.logstore3.com", 24 | "key": "Bearer dataset-***" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_session_access_key_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GenerateSessionAccessKeyRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | duration_seconds: int = None, 13 | ): 14 | self.duration_seconds = duration_seconds 15 | 16 | def validate(self): 17 | pass 18 | 19 | def to_map(self): 20 | _map = super().to_map() 21 | if _map is not None: 22 | return _map 23 | 24 | result = dict() 25 | if self.duration_seconds is not None: 26 | result["DurationSeconds"] = self.duration_seconds 27 | return result 28 | 29 | def from_map(self, m: dict = None): 30 | m = m or dict() 31 | if m.get("DurationSeconds") is not None: 32 | self.duration_seconds = m.get("DurationSeconds") 33 | return self 34 | -------------------------------------------------------------------------------- /libs/README-CN.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 简体中文 2 | 3 | ![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg) 4 | 5 | ## Alibaba Cloud Sts SDK for Python 6 | 7 | ## 要求 8 | 9 | - Python >= 3.7 10 | 11 | ## 安装 12 | 13 | - **使用 pip 安装(推荐)** 14 | 15 | 如未安装 `pip`, 请先至pip官网 [pip user guide](https://pip.pypa.io/en/stable/installing/ "pip User Guide") 安装pip . 16 | 17 | ```bash 18 | # 安装 alibabacloud_sts20150401 19 | pip install alibabacloud_sts20150401 20 | ``` 21 | 22 | ## 问题 23 | 24 | [提交 Issue](https://github.com/aliyun/alibabacloud-python-sdk/issues/new),不符合指南的问题可能会立即关闭。 25 | 26 | ## 使用说明 27 | 28 | [快速使用](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/docs/0-Usage-CN.md#%E5%BF%AB%E9%80%9F%E4%BD%BF%E7%94%A8) 29 | 30 | ## 发行说明 31 | 32 | 每个版本的详细更改记录在[发行说明](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/sts-20150401/ChangeLog.md)中。 33 | 34 | ## 相关 35 | 36 | - [最新源码](https://github.com/aliyun/alibabacloud-python-sdk/) 37 | 38 | ## 许可证 39 | 40 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) 41 | 42 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 43 | -------------------------------------------------------------------------------- /libs/sts-20150401/README-CN.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 简体中文 2 | 3 | ![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg) 4 | 5 | ## Alibaba Cloud Sts SDK for Python 6 | 7 | ## 要求 8 | 9 | - Python >= 3.7 10 | 11 | ## 安装 12 | 13 | - **使用 pip 安装(推荐)** 14 | 15 | 如未安装 `pip`, 请先至pip官网 [pip user guide](https://pip.pypa.io/en/stable/installing/ "pip User Guide") 安装pip . 16 | 17 | ```bash 18 | # 安装 alibabacloud_sts20150401 19 | pip install alibabacloud_sts20150401 20 | ``` 21 | 22 | ## 问题 23 | 24 | [提交 Issue](https://github.com/aliyun/alibabacloud-python-sdk/issues/new),不符合指南的问题可能会立即关闭。 25 | 26 | ## 使用说明 27 | 28 | [快速使用](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/docs/0-Usage-CN.md#%E5%BF%AB%E9%80%9F%E4%BD%BF%E7%94%A8) 29 | 30 | ## 发行说明 31 | 32 | 每个版本的详细更改记录在[发行说明](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/sts-20150401/ChangeLog.md)中。 33 | 34 | ## 相关 35 | 36 | - [最新源码](https://github.com/aliyun/alibabacloud-python-sdk/) 37 | 38 | ## 许可证 39 | 40 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) 41 | 42 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alibaba Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /libs/.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # IDE 5 | .idea 6 | .settings 7 | .cache/ 8 | .tmp 9 | .vscode/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.coverage 55 | .hypothesis/ 56 | .pytest_cache/ 57 | pytestdebug.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # Environments 69 | .env 70 | .venv 71 | env/ 72 | venv/ 73 | ENV/ 74 | env.bak/ 75 | venv.bak/ 76 | 77 | # Pyre type checker 78 | .pyre/ 79 | 80 | # pytype static type analyzer 81 | .pytype/ -------------------------------------------------------------------------------- /libs/sts-20150401/.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # IDE 5 | .idea 6 | .settings 7 | .cache/ 8 | .tmp 9 | .vscode/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.coverage 55 | .hypothesis/ 56 | .pytest_cache/ 57 | pytestdebug.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # Environments 69 | .env 70 | .venv 71 | env/ 72 | venv/ 73 | ENV/ 74 | env.bak/ 75 | venv.bak/ 76 | 77 | # Pyre type checker 78 | .pyre/ 79 | 80 | # pytype static type analyzer 81 | .pytype/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | share/python-wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | *.manifest 27 | *.spec 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .nox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *.cover 39 | *.py,cover 40 | .hypothesis/ 41 | .pytest_cache/ 42 | cover/ 43 | 44 | # Environments 45 | .env 46 | .venv 47 | **/*.egg-info 48 | **/*.egg 49 | **/*.dist-info 50 | **/*.whl 51 | **/*.tar.gz 52 | **/*.zip 53 | .cursor 54 | .pytest_cache 55 | **/*.tar.bz2 56 | uv.lock 57 | CLAUDE.md 58 | **.log 59 | env/ 60 | venv/ 61 | ENV/ 62 | env.bak/ 63 | venv.bak/ 64 | 65 | # IDEs 66 | .idea/ 67 | .vscode/ 68 | *.swp 69 | *.swo 70 | 71 | # OS 72 | .DS_Store 73 | .DS_Store? 74 | ._* 75 | .Spotlight-V100 76 | .Trashes 77 | ehthumbs.db 78 | Thumbs.db 79 | 80 | 81 | 82 | # 敏感配置文件 83 | *.env.local 84 | *.env.production 85 | config/secrets/ 86 | logs/** 87 | CLAUDE.md 88 | **.log 89 | .CLAUDE 90 | .claude-trace 91 | docs/** 92 | deploy/** 93 | agents.md 94 | .cursor/** -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_token_by_ticket_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GenerateTokenByTicketRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | ticket: str = None, 13 | ticket_type: str = None, 14 | ): 15 | # This parameter is required. 16 | self.ticket = ticket 17 | self.ticket_type = ticket_type 18 | 19 | def validate(self): 20 | pass 21 | 22 | def to_map(self): 23 | _map = super().to_map() 24 | if _map is not None: 25 | return _map 26 | 27 | result = dict() 28 | if self.ticket is not None: 29 | result["Ticket"] = self.ticket 30 | if self.ticket_type is not None: 31 | result["TicketType"] = self.ticket_type 32 | return result 33 | 34 | def from_map(self, m: dict = None): 35 | m = m or dict() 36 | if m.get("Ticket") is not None: 37 | self.ticket = m.get("Ticket") 38 | if m.get("TicketType") is not None: 39 | self.ticket_type = m.get("TicketType") 40 | return self 41 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_token_by_ticket_response_body_assumed_role_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GenerateTokenByTicketResponseBodyAssumedRoleUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | assumed_role_id: str = None, 14 | ): 15 | self.arn = arn 16 | self.assumed_role_id = assumed_role_id 17 | 18 | def validate(self): 19 | pass 20 | 21 | def to_map(self): 22 | _map = super().to_map() 23 | if _map is not None: 24 | return _map 25 | 26 | result = dict() 27 | if self.arn is not None: 28 | result["Arn"] = self.arn 29 | if self.assumed_role_id is not None: 30 | result["AssumedRoleId"] = self.assumed_role_id 31 | return result 32 | 33 | def from_map(self, m: dict = None): 34 | m = m or dict() 35 | if m.get("Arn") is not None: 36 | self.arn = m.get("Arn") 37 | if m.get("AssumedRoleId") is not None: 38 | self.assumed_role_id = m.get("AssumedRoleId") 39 | return self 40 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_federation_token_response_body_federated_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GetFederationTokenResponseBodyFederatedUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | federated_user_id: str = None, 14 | ): 15 | self.arn = arn 16 | self.federated_user_id = federated_user_id 17 | 18 | def validate(self): 19 | pass 20 | 21 | def to_map(self): 22 | _map = super().to_map() 23 | if _map is not None: 24 | return _map 25 | 26 | result = dict() 27 | if self.arn is not None: 28 | result["Arn"] = self.arn 29 | if self.federated_user_id is not None: 30 | result["FederatedUserId"] = self.federated_user_id 31 | return result 32 | 33 | def from_map(self, m: dict = None): 34 | m = m or dict() 35 | if m.get("Arn") is not None: 36 | self.arn = m.get("Arn") 37 | if m.get("FederatedUserId") is not None: 38 | self.federated_user_id = m.get("FederatedUserId") 39 | return self 40 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_service_identity_response_body_assumed_role_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithServiceIdentityResponseBodyAssumedRoleUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | assumed_role_id: str = None, 14 | ): 15 | self.arn = arn 16 | self.assumed_role_id = assumed_role_id 17 | 18 | def validate(self): 19 | pass 20 | 21 | def to_map(self): 22 | _map = super().to_map() 23 | if _map is not None: 24 | return _map 25 | 26 | result = dict() 27 | if self.arn is not None: 28 | result["Arn"] = self.arn 29 | if self.assumed_role_id is not None: 30 | result["AssumedRoleId"] = self.assumed_role_id 31 | return result 32 | 33 | def from_map(self, m: dict = None): 34 | m = m or dict() 35 | if m.get("Arn") is not None: 36 | self.arn = m.get("Arn") 37 | if m.get("AssumedRoleId") is not None: 38 | self.assumed_role_id = m.get("AssumedRoleId") 39 | return self 40 | -------------------------------------------------------------------------------- /libs/README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README-CN.md) 2 | ![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg) 3 | 4 | ## Alibaba Cloud Sts SDK for Python 5 | 6 | ## Requirements 7 | 8 | - Python >= 3.7 9 | 10 | ## Installation 11 | 12 | - **Install with pip** 13 | 14 | Python SDK uses a common package management tool named `pip`. If pip is not installed, see the [pip user guide](https://pip.pypa.io/en/stable/installing/ "pip User Guide") to install pip. 15 | 16 | ```bash 17 | # Install the alibabacloud_sts20150401 18 | pip install alibabacloud_sts20150401 19 | ``` 20 | 21 | ## Issues 22 | 23 | [Opening an Issue](https://github.com/aliyun/alibabacloud-sdk/issues/new), Issues not conforming to the guidelines may be closed immediately. 24 | 25 | ## Usage 26 | 27 | [Quick Examples](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/docs/0-Usage-EN.md#quick-examples) 28 | 29 | ## Changelog 30 | 31 | Detailed changes for each release are documented in the [release notes](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/sts-20150401/ChangeLog.md). 32 | 33 | ## References 34 | 35 | - [Latest Release](https://github.com/aliyun/alibabacloud-sdk/tree/master/python) 36 | 37 | ## License 38 | 39 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) 40 | 41 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 42 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/config.py: -------------------------------------------------------------------------------- 1 | """通用配置类""" 2 | 3 | import os 4 | 5 | 6 | class Config: 7 | """MCP服务器的通用配置类""" 8 | 9 | # 重试配置 10 | MAX_RETRY_ATTEMPTS = int(os.getenv("MAX_RETRY_ATTEMPTS", "1")) # 默认重试1次 11 | RETRY_WAIT_SECONDS = int(os.getenv("RETRY_WAIT_SECONDS", "1")) # 重试等待时间(秒) 12 | 13 | # 超时配置 14 | READ_TIMEOUT_MS = int( 15 | os.getenv("READ_TIMEOUT_MS", "610000") 16 | ) # 读取超时(毫秒),默认10秒 17 | CONNECT_TIMEOUT_MS = int( 18 | os.getenv("CONNECT_TIMEOUT_MS", "30000") 19 | ) # 连接超时(毫秒),默认10秒 20 | 21 | # 调试配置 22 | DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() in ["true", "1", "yes", "on"] 23 | 24 | @classmethod 25 | def is_test_mode(cls) -> bool: 26 | """检查是否在测试模式下运行""" 27 | # 通过环境变量或pytest标记来判断 28 | return os.getenv("PYTEST_CURRENT_TEST") is not None or os.getenv( 29 | "TEST_MODE", "false" 30 | ).lower() in ["true", "1", "yes", "on"] 31 | 32 | @classmethod 33 | def get_retry_attempts(cls) -> int: 34 | """获取重试次数,测试模式下返回1""" 35 | if cls.is_test_mode(): 36 | return 1 37 | return cls.MAX_RETRY_ATTEMPTS 38 | 39 | @classmethod 40 | def get_timeouts(cls) -> tuple[int, int]: 41 | """获取超时配置,返回(读取超时, 连接超时)""" 42 | return cls.READ_TIMEOUT_MS, cls.CONNECT_TIMEOUT_MS 43 | -------------------------------------------------------------------------------- /libs/sts-20150401/README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README-CN.md) 2 | ![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg) 3 | 4 | ## Alibaba Cloud Sts SDK for Python 5 | 6 | ## Requirements 7 | 8 | - Python >= 3.7 9 | 10 | ## Installation 11 | 12 | - **Install with pip** 13 | 14 | Python SDK uses a common package management tool named `pip`. If pip is not installed, see the [pip user guide](https://pip.pypa.io/en/stable/installing/ "pip User Guide") to install pip. 15 | 16 | ```bash 17 | # Install the alibabacloud_sts20150401 18 | pip install alibabacloud_sts20150401 19 | ``` 20 | 21 | ## Issues 22 | 23 | [Opening an Issue](https://github.com/aliyun/alibabacloud-sdk/issues/new), Issues not conforming to the guidelines may be closed immediately. 24 | 25 | ## Usage 26 | 27 | [Quick Examples](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/docs/0-Usage-EN.md#quick-examples) 28 | 29 | ## Changelog 30 | 31 | Detailed changes for each release are documented in the [release notes](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/sts-20150401/ChangeLog.md). 32 | 33 | ## References 34 | 35 | - [Latest Release](https://github.com/aliyun/alibabacloud-sdk/tree/master/python) 36 | 37 | ## License 38 | 39 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) 40 | 41 | Copyright (c) 2009-present, Alibaba Cloud All rights reserved. 42 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_response_body_assumed_role_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleResponseBodyAssumedRoleUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | assumed_role_id: str = None, 14 | ): 15 | # The ARN of the temporary identity that you use to assume the RAM role. 16 | self.arn = arn 17 | # The ID of the temporary identity that you use to assume the RAM role. 18 | self.assumed_role_id = assumed_role_id 19 | 20 | def validate(self): 21 | pass 22 | 23 | def to_map(self): 24 | _map = super().to_map() 25 | if _map is not None: 26 | return _map 27 | 28 | result = dict() 29 | if self.arn is not None: 30 | result["Arn"] = self.arn 31 | if self.assumed_role_id is not None: 32 | result["AssumedRoleId"] = self.assumed_role_id 33 | return result 34 | 35 | def from_map(self, m: dict = None): 36 | m = m or dict() 37 | if m.get("Arn") is not None: 38 | self.arn = m.get("Arn") 39 | if m.get("AssumedRoleId") is not None: 40 | self.assumed_role_id = m.get("AssumedRoleId") 41 | return self 42 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcresponse_body_assumed_role_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithOIDCResponseBodyAssumedRoleUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | assumed_role_id: str = None, 14 | ): 15 | # The ARN of the temporary identity that you use to assume the RAM role. 16 | self.arn = arn 17 | # The ID of the temporary identity that you use to assume the RAM role. 18 | self.assumed_role_id = assumed_role_id 19 | 20 | def validate(self): 21 | pass 22 | 23 | def to_map(self): 24 | _map = super().to_map() 25 | if _map is not None: 26 | return _map 27 | 28 | result = dict() 29 | if self.arn is not None: 30 | result["Arn"] = self.arn 31 | if self.assumed_role_id is not None: 32 | result["AssumedRoleId"] = self.assumed_role_id 33 | return result 34 | 35 | def from_map(self, m: dict = None): 36 | m = m or dict() 37 | if m.get("Arn") is not None: 38 | self.arn = m.get("Arn") 39 | if m.get("AssumedRoleId") is not None: 40 | self.assumed_role_id = m.get("AssumedRoleId") 41 | return self 42 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlresponse_body_assumed_role_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithSAMLResponseBodyAssumedRoleUser(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | arn: str = None, 13 | assumed_role_id: str = None, 14 | ): 15 | # The ARN of the temporary identity that you use to assume the RAM role. 16 | self.arn = arn 17 | # The ID of the temporary identity that you use to assume the RAM role. 18 | self.assumed_role_id = assumed_role_id 19 | 20 | def validate(self): 21 | pass 22 | 23 | def to_map(self): 24 | _map = super().to_map() 25 | if _map is not None: 26 | return _map 27 | 28 | result = dict() 29 | if self.arn is not None: 30 | result["Arn"] = self.arn 31 | if self.assumed_role_id is not None: 32 | result["AssumedRoleId"] = self.assumed_role_id 33 | return result 34 | 35 | def from_map(self, m: dict = None): 36 | m = m or dict() 37 | if m.get("Arn") is not None: 38 | self.arn = m.get("Arn") 39 | if m.get("AssumedRoleId") is not None: 40 | self.assumed_role_id = m.get("AssumedRoleId") 41 | return self 42 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/paas/toolkit.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | from mcp_server_aliyun_observability.toolkits.paas.data_toolkit import \ 6 | PaasDataToolkit 7 | from mcp_server_aliyun_observability.toolkits.paas.dataset_toolkit import \ 8 | PaaSDatasetToolkit 9 | from mcp_server_aliyun_observability.toolkits.paas.entity_toolkit import \ 10 | PaaSEntityToolkit 11 | 12 | 13 | class PaaSToolkit: 14 | """Platform as a Service Layer Toolkit 15 | 16 | Provides structured query tools ported from umodel-mcp handlers. 17 | All tools use umodel_ prefix and execute SPL queries with precise parameter control. 18 | No natural language parameters - only structured data. 19 | """ 20 | 21 | def __init__(self, server: FastMCP): 22 | """Initialize the PaaS toolkit 23 | 24 | Args: 25 | server: FastMCP server instance 26 | """ 27 | self.server = server 28 | self._register_toolkits() 29 | 30 | def _register_toolkits(self): 31 | """Register all PaaS sub-toolkits""" 32 | 33 | # Initialize sub-toolkits 34 | PaaSEntityToolkit(self.server) 35 | PaaSDatasetToolkit(self.server) 36 | PaasDataToolkit(self.server) 37 | 38 | 39 | def register_paas_tools(server: FastMCP): 40 | """Register PaaS toolkit tools with the FastMCP server 41 | 42 | Args: 43 | server: FastMCP server instance 44 | """ 45 | PaaSToolkit(server) -------------------------------------------------------------------------------- /tests/test_settings_endpoints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mcp_server_aliyun_observability.settings import ( 4 | CMSSettings, 5 | SLSSettings, 6 | build_endpoint_mapping, 7 | ) 8 | 9 | 10 | def test_build_endpoint_mapping_precedence_and_normalization(): 11 | combined = ( 12 | "cn-beijing=https://combined.example.com,cn-hangzhou=combined-hz.example.com" 13 | ) 14 | cli_pairs = [ 15 | "cn-beijing=cli.example.com", 16 | "cn-shanghai=http://cli-sh.example.com/", 17 | ] 18 | 19 | mapping = build_endpoint_mapping(cli_pairs, combined) 20 | 21 | assert mapping["cn-beijing"] == "cli.example.com" 22 | assert mapping["cn-shanghai"] == "cli-sh.example.com" 23 | assert mapping["cn-hangzhou"] == "combined-hz.example.com" 24 | 25 | 26 | def test_settings_resolve_fallback_templates(): 27 | sls_settings = SLSSettings(endpoints={"cn-beijing": "custom.example.com"}) 28 | cms_settings = CMSSettings(endpoints={"cn-hangzhou": "cms.hz.example.com"}) 29 | 30 | assert sls_settings.resolve("cn-beijing") == "custom.example.com" 31 | assert sls_settings.resolve("cn-shanghai") == "cn-shanghai.log.aliyuncs.com" 32 | 33 | assert cms_settings.resolve("cn-hangzhou") == "cms.hz.example.com" 34 | assert cms_settings.resolve("cn-shanghai") == "cms.cn-shanghai.aliyuncs.com" 35 | 36 | 37 | def test_settings_resolve_requires_region(): 38 | with pytest.raises(ValueError): 39 | SLSSettings().resolve("") 40 | 41 | with pytest.raises(ValueError): 42 | CMSSettings().resolve("") 43 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_federation_token_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GetFederationTokenRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | duration_seconds: int = None, 13 | name: str = None, 14 | policy: str = None, 15 | ): 16 | # This parameter is required. 17 | self.duration_seconds = duration_seconds 18 | # This parameter is required. 19 | self.name = name 20 | # This parameter is required. 21 | self.policy = policy 22 | 23 | def validate(self): 24 | pass 25 | 26 | def to_map(self): 27 | _map = super().to_map() 28 | if _map is not None: 29 | return _map 30 | 31 | result = dict() 32 | if self.duration_seconds is not None: 33 | result["DurationSeconds"] = self.duration_seconds 34 | if self.name is not None: 35 | result["Name"] = self.name 36 | if self.policy is not None: 37 | result["Policy"] = self.policy 38 | return result 39 | 40 | def from_map(self, m: dict = None): 41 | m = m or dict() 42 | if m.get("DurationSeconds") is not None: 43 | self.duration_seconds = m.get("DurationSeconds") 44 | if m.get("Name") is not None: 45 | self.name = m.get("Name") 46 | if m.get("Policy") is not None: 47 | self.policy = m.get("Policy") 48 | return self 49 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_session_access_key_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class GenerateSessionAccessKeyResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | request_id: str = None, 15 | session_access_key: main_models.GenerateSessionAccessKeyResponseBodySessionAccessKey = None, 16 | ): 17 | self.request_id = request_id 18 | self.session_access_key = session_access_key 19 | 20 | def validate(self): 21 | if self.session_access_key: 22 | self.session_access_key.validate() 23 | 24 | def to_map(self): 25 | _map = super().to_map() 26 | if _map is not None: 27 | return _map 28 | 29 | result = dict() 30 | if self.request_id is not None: 31 | result["RequestId"] = self.request_id 32 | if self.session_access_key is not None: 33 | result["SessionAccessKey"] = self.session_access_key.to_map() 34 | 35 | return result 36 | 37 | def from_map(self, m: dict = None): 38 | m = m or dict() 39 | if m.get("RequestId") is not None: 40 | self.request_id = m.get("RequestId") 41 | if m.get("SessionAccessKey") is not None: 42 | temp_model = main_models.GenerateSessionAccessKeyResponseBodySessionAccessKey() 43 | self.session_access_key = temp_model.from_map(m.get("SessionAccessKey")) 44 | 45 | return self 46 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class AssumeRoleResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.AssumeRoleResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.AssumeRoleResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_caller_identity_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class GetCallerIdentityResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.GetCallerIdentityResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.GetCallerIdentityResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcresponse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class AssumeRoleWithOIDCResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.AssumeRoleWithOIDCResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.AssumeRoleWithOIDCResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlresponse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class AssumeRoleWithSAMLResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.AssumeRoleWithSAMLResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.AssumeRoleWithSAMLResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_federation_token_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class GetFederationTokenResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.GetFederationTokenResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.GetFederationTokenResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_token_by_ticket_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class GenerateTokenByTicketResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.GenerateTokenByTicketResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.GenerateTokenByTicketResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_session_access_key_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class GenerateSessionAccessKeyResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.GenerateSessionAccessKeyResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.GenerateSessionAccessKeyResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_session_access_key_response_body_session_access_key.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GenerateSessionAccessKeyResponseBodySessionAccessKey(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | expiration: str = None, 13 | session_access_key_id: str = None, 14 | session_access_key_secret: str = None, 15 | ): 16 | self.expiration = expiration 17 | self.session_access_key_id = session_access_key_id 18 | self.session_access_key_secret = session_access_key_secret 19 | 20 | def validate(self): 21 | pass 22 | 23 | def to_map(self): 24 | _map = super().to_map() 25 | if _map is not None: 26 | return _map 27 | 28 | result = dict() 29 | if self.expiration is not None: 30 | result["Expiration"] = self.expiration 31 | if self.session_access_key_id is not None: 32 | result["SessionAccessKeyId"] = self.session_access_key_id 33 | if self.session_access_key_secret is not None: 34 | result["SessionAccessKeySecret"] = self.session_access_key_secret 35 | return result 36 | 37 | def from_map(self, m: dict = None): 38 | m = m or dict() 39 | if m.get("Expiration") is not None: 40 | self.expiration = m.get("Expiration") 41 | if m.get("SessionAccessKeyId") is not None: 42 | self.session_access_key_id = m.get("SessionAccessKeyId") 43 | if m.get("SessionAccessKeySecret") is not None: 44 | self.session_access_key_secret = m.get("SessionAccessKeySecret") 45 | return self 46 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_service_identity_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | 7 | from darabonba.model import DaraModel 8 | 9 | from alibabacloud_sts20150401 import models as main_models 10 | 11 | 12 | class AssumeRoleWithServiceIdentityResponse(DaraModel): 13 | def __init__( 14 | self, 15 | *, 16 | headers: Dict[str, str] = None, 17 | status_code: int = None, 18 | body: main_models.AssumeRoleWithServiceIdentityResponseBody = None, 19 | ): 20 | self.headers = headers 21 | self.status_code = status_code 22 | self.body = body 23 | 24 | def validate(self): 25 | if self.body: 26 | self.body.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.headers is not None: 35 | result["headers"] = self.headers 36 | if self.status_code is not None: 37 | result["statusCode"] = self.status_code 38 | if self.body is not None: 39 | result["body"] = self.body.to_map() 40 | 41 | return result 42 | 43 | def from_map(self, m: dict = None): 44 | m = m or dict() 45 | if m.get("headers") is not None: 46 | self.headers = m.get("headers") 47 | if m.get("statusCode") is not None: 48 | self.status_code = m.get("statusCode") 49 | if m.get("body") is not None: 50 | temp_model = main_models.AssumeRoleWithServiceIdentityResponseBody() 51 | self.body = temp_model.from_map(m.get("body")) 52 | 53 | return self 54 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_federation_token_response_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GetFederationTokenResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | self.access_key_id = access_key_id 18 | self.access_key_secret = access_key_secret 19 | self.expiration = expiration 20 | self.security_token = security_token 21 | 22 | def validate(self): 23 | pass 24 | 25 | def to_map(self): 26 | _map = super().to_map() 27 | if _map is not None: 28 | return _map 29 | 30 | result = dict() 31 | if self.access_key_id is not None: 32 | result["AccessKeyId"] = self.access_key_id 33 | if self.access_key_secret is not None: 34 | result["AccessKeySecret"] = self.access_key_secret 35 | if self.expiration is not None: 36 | result["Expiration"] = self.expiration 37 | if self.security_token is not None: 38 | result["SecurityToken"] = self.security_token 39 | return result 40 | 41 | def from_map(self, m: dict = None): 42 | m = m or dict() 43 | if m.get("AccessKeyId") is not None: 44 | self.access_key_id = m.get("AccessKeyId") 45 | if m.get("AccessKeySecret") is not None: 46 | self.access_key_secret = m.get("AccessKeySecret") 47 | if m.get("Expiration") is not None: 48 | self.expiration = m.get("Expiration") 49 | if m.get("SecurityToken") is not None: 50 | self.security_token = m.get("SecurityToken") 51 | return self 52 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_token_by_ticket_response_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GenerateTokenByTicketResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | self.access_key_id = access_key_id 18 | self.access_key_secret = access_key_secret 19 | self.expiration = expiration 20 | self.security_token = security_token 21 | 22 | def validate(self): 23 | pass 24 | 25 | def to_map(self): 26 | _map = super().to_map() 27 | if _map is not None: 28 | return _map 29 | 30 | result = dict() 31 | if self.access_key_id is not None: 32 | result["AccessKeyId"] = self.access_key_id 33 | if self.access_key_secret is not None: 34 | result["AccessKeySecret"] = self.access_key_secret 35 | if self.expiration is not None: 36 | result["Expiration"] = self.expiration 37 | if self.security_token is not None: 38 | result["SecurityToken"] = self.security_token 39 | return result 40 | 41 | def from_map(self, m: dict = None): 42 | m = m or dict() 43 | if m.get("AccessKeyId") is not None: 44 | self.access_key_id = m.get("AccessKeyId") 45 | if m.get("AccessKeySecret") is not None: 46 | self.access_key_secret = m.get("AccessKeySecret") 47 | if m.get("Expiration") is not None: 48 | self.expiration = m.get("Expiration") 49 | if m.get("SecurityToken") is not None: 50 | self.security_token = m.get("SecurityToken") 51 | return self 52 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_service_identity_response_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithServiceIdentityResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | self.access_key_id = access_key_id 18 | self.access_key_secret = access_key_secret 19 | self.expiration = expiration 20 | self.security_token = security_token 21 | 22 | def validate(self): 23 | pass 24 | 25 | def to_map(self): 26 | _map = super().to_map() 27 | if _map is not None: 28 | return _map 29 | 30 | result = dict() 31 | if self.access_key_id is not None: 32 | result["AccessKeyId"] = self.access_key_id 33 | if self.access_key_secret is not None: 34 | result["AccessKeySecret"] = self.access_key_secret 35 | if self.expiration is not None: 36 | result["Expiration"] = self.expiration 37 | if self.security_token is not None: 38 | result["SecurityToken"] = self.security_token 39 | return result 40 | 41 | def from_map(self, m: dict = None): 42 | m = m or dict() 43 | if m.get("AccessKeyId") is not None: 44 | self.access_key_id = m.get("AccessKeyId") 45 | if m.get("AccessKeySecret") is not None: 46 | self.access_key_secret = m.get("AccessKeySecret") 47 | if m.get("Expiration") is not None: 48 | self.expiration = m.get("Expiration") 49 | if m.get("SecurityToken") is not None: 50 | self.security_token = m.get("SecurityToken") 51 | return self 52 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_federation_token_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class GetFederationTokenResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | credentials: main_models.GetFederationTokenResponseBodyCredentials = None, 15 | federated_user: main_models.GetFederationTokenResponseBodyFederatedUser = None, 16 | request_id: str = None, 17 | ): 18 | self.credentials = credentials 19 | self.federated_user = federated_user 20 | self.request_id = request_id 21 | 22 | def validate(self): 23 | if self.credentials: 24 | self.credentials.validate() 25 | if self.federated_user: 26 | self.federated_user.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.credentials is not None: 35 | result["Credentials"] = self.credentials.to_map() 36 | 37 | if self.federated_user is not None: 38 | result["FederatedUser"] = self.federated_user.to_map() 39 | 40 | if self.request_id is not None: 41 | result["RequestId"] = self.request_id 42 | return result 43 | 44 | def from_map(self, m: dict = None): 45 | m = m or dict() 46 | if m.get("Credentials") is not None: 47 | temp_model = main_models.GetFederationTokenResponseBodyCredentials() 48 | self.credentials = temp_model.from_map(m.get("Credentials")) 49 | 50 | if m.get("FederatedUser") is not None: 51 | temp_model = main_models.GetFederationTokenResponseBodyFederatedUser() 52 | self.federated_user = temp_model.from_map(m.get("FederatedUser")) 53 | 54 | if m.get("RequestId") is not None: 55 | self.request_id = m.get("RequestId") 56 | return self 57 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_generate_token_by_ticket_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class GenerateTokenByTicketResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | assumed_role_user: main_models.GenerateTokenByTicketResponseBodyAssumedRoleUser = None, 15 | credentials: main_models.GenerateTokenByTicketResponseBodyCredentials = None, 16 | request_id: str = None, 17 | ): 18 | self.assumed_role_user = assumed_role_user 19 | self.credentials = credentials 20 | self.request_id = request_id 21 | 22 | def validate(self): 23 | if self.assumed_role_user: 24 | self.assumed_role_user.validate() 25 | if self.credentials: 26 | self.credentials.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.assumed_role_user is not None: 35 | result["AssumedRoleUser"] = self.assumed_role_user.to_map() 36 | 37 | if self.credentials is not None: 38 | result["Credentials"] = self.credentials.to_map() 39 | 40 | if self.request_id is not None: 41 | result["RequestId"] = self.request_id 42 | return result 43 | 44 | def from_map(self, m: dict = None): 45 | m = m or dict() 46 | if m.get("AssumedRoleUser") is not None: 47 | temp_model = main_models.GenerateTokenByTicketResponseBodyAssumedRoleUser() 48 | self.assumed_role_user = temp_model.from_map(m.get("AssumedRoleUser")) 49 | 50 | if m.get("Credentials") is not None: 51 | temp_model = main_models.GenerateTokenByTicketResponseBodyCredentials() 52 | self.credentials = temp_model.from_map(m.get("Credentials")) 53 | 54 | if m.get("RequestId") is not None: 55 | self.request_id = m.get("RequestId") 56 | return self 57 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_service_identity_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class AssumeRoleWithServiceIdentityResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | assumed_role_user: main_models.AssumeRoleWithServiceIdentityResponseBodyAssumedRoleUser = None, 15 | credentials: main_models.AssumeRoleWithServiceIdentityResponseBodyCredentials = None, 16 | request_id: str = None, 17 | ): 18 | self.assumed_role_user = assumed_role_user 19 | self.credentials = credentials 20 | self.request_id = request_id 21 | 22 | def validate(self): 23 | if self.assumed_role_user: 24 | self.assumed_role_user.validate() 25 | if self.credentials: 26 | self.credentials.validate() 27 | 28 | def to_map(self): 29 | _map = super().to_map() 30 | if _map is not None: 31 | return _map 32 | 33 | result = dict() 34 | if self.assumed_role_user is not None: 35 | result["AssumedRoleUser"] = self.assumed_role_user.to_map() 36 | 37 | if self.credentials is not None: 38 | result["Credentials"] = self.credentials.to_map() 39 | 40 | if self.request_id is not None: 41 | result["RequestId"] = self.request_id 42 | return result 43 | 44 | def from_map(self, m: dict = None): 45 | m = m or dict() 46 | if m.get("AssumedRoleUser") is not None: 47 | temp_model = main_models.AssumeRoleWithServiceIdentityResponseBodyAssumedRoleUser() 48 | self.assumed_role_user = temp_model.from_map(m.get("AssumedRoleUser")) 49 | 50 | if m.get("Credentials") is not None: 51 | temp_model = main_models.AssumeRoleWithServiceIdentityResponseBodyCredentials() 52 | self.credentials = temp_model.from_map(m.get("Credentials")) 53 | 54 | if m.get("RequestId") is not None: 55 | self.request_id = m.get("RequestId") 56 | return self 57 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class AssumeRoleResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | assumed_role_user: main_models.AssumeRoleResponseBodyAssumedRoleUser = None, 15 | credentials: main_models.AssumeRoleResponseBodyCredentials = None, 16 | request_id: str = None, 17 | ): 18 | # The temporary identity that you use to assume the RAM role. 19 | self.assumed_role_user = assumed_role_user 20 | # The STS credentials. 21 | self.credentials = credentials 22 | # The ID of the request. 23 | self.request_id = request_id 24 | 25 | def validate(self): 26 | if self.assumed_role_user: 27 | self.assumed_role_user.validate() 28 | if self.credentials: 29 | self.credentials.validate() 30 | 31 | def to_map(self): 32 | _map = super().to_map() 33 | if _map is not None: 34 | return _map 35 | 36 | result = dict() 37 | if self.assumed_role_user is not None: 38 | result["AssumedRoleUser"] = self.assumed_role_user.to_map() 39 | 40 | if self.credentials is not None: 41 | result["Credentials"] = self.credentials.to_map() 42 | 43 | if self.request_id is not None: 44 | result["RequestId"] = self.request_id 45 | return result 46 | 47 | def from_map(self, m: dict = None): 48 | m = m or dict() 49 | if m.get("AssumedRoleUser") is not None: 50 | temp_model = main_models.AssumeRoleResponseBodyAssumedRoleUser() 51 | self.assumed_role_user = temp_model.from_map(m.get("AssumedRoleUser")) 52 | 53 | if m.get("Credentials") is not None: 54 | temp_model = main_models.AssumeRoleResponseBodyCredentials() 55 | self.credentials = temp_model.from_map(m.get("Credentials")) 56 | 57 | if m.get("RequestId") is not None: 58 | self.request_id = m.get("RequestId") 59 | return self 60 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_response_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | # The AccessKey ID. 18 | self.access_key_id = access_key_id 19 | # The AccessKey secret. 20 | self.access_key_secret = access_key_secret 21 | # The time when the STS token expires. The time is displayed in UTC. 22 | self.expiration = expiration 23 | # The STS token. 24 | # 25 | # > Alibaba Cloud STS does not impose limits on the length of STS tokens. We strongly recommend that you do not specify a maximum length for STS tokens. 26 | self.security_token = security_token 27 | 28 | def validate(self): 29 | pass 30 | 31 | def to_map(self): 32 | _map = super().to_map() 33 | if _map is not None: 34 | return _map 35 | 36 | result = dict() 37 | if self.access_key_id is not None: 38 | result["AccessKeyId"] = self.access_key_id 39 | if self.access_key_secret is not None: 40 | result["AccessKeySecret"] = self.access_key_secret 41 | if self.expiration is not None: 42 | result["Expiration"] = self.expiration 43 | if self.security_token is not None: 44 | result["SecurityToken"] = self.security_token 45 | return result 46 | 47 | def from_map(self, m: dict = None): 48 | m = m or dict() 49 | if m.get("AccessKeyId") is not None: 50 | self.access_key_id = m.get("AccessKeyId") 51 | if m.get("AccessKeySecret") is not None: 52 | self.access_key_secret = m.get("AccessKeySecret") 53 | if m.get("Expiration") is not None: 54 | self.expiration = m.get("Expiration") 55 | if m.get("SecurityToken") is not None: 56 | self.security_token = m.get("SecurityToken") 57 | return self 58 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcresponse_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithOIDCResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | # The AccessKey ID. 18 | self.access_key_id = access_key_id 19 | # The AccessKey secret. 20 | self.access_key_secret = access_key_secret 21 | # The time when the STS token expires. The time is displayed in UTC. 22 | self.expiration = expiration 23 | # The STS token. 24 | # 25 | # > Alibaba Cloud STS does not impose limits on the length of STS tokens. We strongly recommend that you do not specify a maximum length for STS tokens. 26 | self.security_token = security_token 27 | 28 | def validate(self): 29 | pass 30 | 31 | def to_map(self): 32 | _map = super().to_map() 33 | if _map is not None: 34 | return _map 35 | 36 | result = dict() 37 | if self.access_key_id is not None: 38 | result["AccessKeyId"] = self.access_key_id 39 | if self.access_key_secret is not None: 40 | result["AccessKeySecret"] = self.access_key_secret 41 | if self.expiration is not None: 42 | result["Expiration"] = self.expiration 43 | if self.security_token is not None: 44 | result["SecurityToken"] = self.security_token 45 | return result 46 | 47 | def from_map(self, m: dict = None): 48 | m = m or dict() 49 | if m.get("AccessKeyId") is not None: 50 | self.access_key_id = m.get("AccessKeyId") 51 | if m.get("AccessKeySecret") is not None: 52 | self.access_key_secret = m.get("AccessKeySecret") 53 | if m.get("Expiration") is not None: 54 | self.expiration = m.get("Expiration") 55 | if m.get("SecurityToken") is not None: 56 | self.security_token = m.get("SecurityToken") 57 | return self 58 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlresponse_body_credentials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithSAMLResponseBodyCredentials(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | access_key_id: str = None, 13 | access_key_secret: str = None, 14 | expiration: str = None, 15 | security_token: str = None, 16 | ): 17 | # The AccessKey ID. 18 | self.access_key_id = access_key_id 19 | # The AccessKey secret. 20 | self.access_key_secret = access_key_secret 21 | # The time when the STS token expires. The time is displayed in UTC. 22 | self.expiration = expiration 23 | # The STS token. 24 | # 25 | # > Alibaba Cloud STS does not impose limits on the length of STS tokens. We strongly recommend that you do not specify a maximum length for STS tokens. 26 | self.security_token = security_token 27 | 28 | def validate(self): 29 | pass 30 | 31 | def to_map(self): 32 | _map = super().to_map() 33 | if _map is not None: 34 | return _map 35 | 36 | result = dict() 37 | if self.access_key_id is not None: 38 | result["AccessKeyId"] = self.access_key_id 39 | if self.access_key_secret is not None: 40 | result["AccessKeySecret"] = self.access_key_secret 41 | if self.expiration is not None: 42 | result["Expiration"] = self.expiration 43 | if self.security_token is not None: 44 | result["SecurityToken"] = self.security_token 45 | return result 46 | 47 | def from_map(self, m: dict = None): 48 | m = m or dict() 49 | if m.get("AccessKeyId") is not None: 50 | self.access_key_id = m.get("AccessKeyId") 51 | if m.get("AccessKeySecret") is not None: 52 | self.access_key_secret = m.get("AccessKeySecret") 53 | if m.get("Expiration") is not None: 54 | self.expiration = m.get("Expiration") 55 | if m.get("SecurityToken") is not None: 56 | self.security_token = m.get("SecurityToken") 57 | return self 58 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_service_identity_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithServiceIdentityRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | assume_role_for: str = None, 13 | duration_seconds: int = None, 14 | policy: str = None, 15 | role_arn: str = None, 16 | role_session_name: str = None, 17 | ): 18 | # This parameter is required. 19 | self.assume_role_for = assume_role_for 20 | self.duration_seconds = duration_seconds 21 | self.policy = policy 22 | # This parameter is required. 23 | self.role_arn = role_arn 24 | # This parameter is required. 25 | self.role_session_name = role_session_name 26 | 27 | def validate(self): 28 | pass 29 | 30 | def to_map(self): 31 | _map = super().to_map() 32 | if _map is not None: 33 | return _map 34 | 35 | result = dict() 36 | if self.assume_role_for is not None: 37 | result["AssumeRoleFor"] = self.assume_role_for 38 | if self.duration_seconds is not None: 39 | result["DurationSeconds"] = self.duration_seconds 40 | if self.policy is not None: 41 | result["Policy"] = self.policy 42 | if self.role_arn is not None: 43 | result["RoleArn"] = self.role_arn 44 | if self.role_session_name is not None: 45 | result["RoleSessionName"] = self.role_session_name 46 | return result 47 | 48 | def from_map(self, m: dict = None): 49 | m = m or dict() 50 | if m.get("AssumeRoleFor") is not None: 51 | self.assume_role_for = m.get("AssumeRoleFor") 52 | if m.get("DurationSeconds") is not None: 53 | self.duration_seconds = m.get("DurationSeconds") 54 | if m.get("Policy") is not None: 55 | self.policy = m.get("Policy") 56 | if m.get("RoleArn") is not None: 57 | self.role_arn = m.get("RoleArn") 58 | if m.get("RoleSessionName") is not None: 59 | self.role_session_name = m.get("RoleSessionName") 60 | return self 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-server-aliyun-observability" 3 | version = "1.0.2" 4 | description = "aliyun observability mcp server" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "mcp>=1.12.0", 9 | "pydantic>=2.10.0", 10 | "alibabacloud_arms20190808>=8.0.0", 11 | "alibabacloud_sls20201230==5.7.0", 12 | "alibabacloud_credentials>=1.0.1", 13 | "tenacity>=8.0.0", 14 | "rich>=13.0.0", 15 | "pandas>=2.3.0", 16 | "numpy", 17 | "jinja2>=3.1.0", 18 | "pyyaml>=6.0", 19 | "aiohttp>=3.8.0", 20 | "requests>=2.28.0", 21 | "alibabacloud-cms20240330==3.1.0", 22 | "alibabacloud-sts20150401==1.1.6", 23 | ] 24 | 25 | [build-system] 26 | requires = ["hatchling", "wheel", "setuptools"] 27 | build-backend = "hatchling.build" 28 | 29 | [tool.uv] 30 | dev-dependencies = ["pyright>=1.1.389"] 31 | 32 | [tool.hatch.build.targets.wheel] 33 | packages = ["src/mcp_server_aliyun_observability"] 34 | 35 | [tool.hatch.build] 36 | include = [ 37 | "src/**/*.py", 38 | "README.md", 39 | "LICENSE", 40 | "pyproject.toml", 41 | "libs/**/*", 42 | ] 43 | 44 | exclude = [ 45 | "**/*.pyc", 46 | "**/__pycache__", 47 | "**/*.pyo", 48 | "**/*.pyd", 49 | "**/*.png", 50 | ".git", 51 | ".env", 52 | ".gitignore", 53 | "*.so", 54 | "*.dylib", 55 | "*.dll", 56 | "docs/", 57 | "deploy/", 58 | "tests/", 59 | "htmlcov/", 60 | "images/", 61 | "sample/", 62 | "**/*_commit.md", 63 | "poetry.lock", 64 | "uv.lock", 65 | "requirements-dev.txt", 66 | "pytest.ini", 67 | "CLAUDE.md", 68 | "CONTRIBUTION.md", 69 | "FAQ.md", 70 | "CHANGELOG.md", 71 | ".coverage", 72 | "dist/", 73 | "build/", 74 | "*.egg-info/", 75 | "libs/**/build/", 76 | "libs/**/*.egg-info/", 77 | "src/**/admin/", 78 | ] 79 | 80 | [tool.hatch.metadata] 81 | allow-direct-references = true 82 | 83 | [project.optional-dependencies] 84 | dev = [ 85 | "pytest>=7.0.0", 86 | "pytest-asyncio>=0.21.0", 87 | "pytest-cov>=4.0.0", 88 | "pytest-mock>=3.10.0", 89 | ] 90 | admin = [ 91 | # Optional admin extensions for advanced features 92 | # Includes ExtendedFastMCP, ThreadContext, STS client support, and K8S tools 93 | ] 94 | 95 | [project.urls] 96 | 97 | 98 | [project.scripts] 99 | mcp-server-aliyun-observability = "mcp_server_aliyun_observability:main" -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlresponse_body_samlassertion_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithSAMLResponseBodySAMLAssertionInfo(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | issuer: str = None, 13 | recipient: str = None, 14 | subject: str = None, 15 | subject_type: str = None, 16 | ): 17 | # The value in the `Issuer` element in the SAML assertion. 18 | self.issuer = issuer 19 | # The `Recipient` attribute of the SubjectConfirmationData sub-element. SubjectConfirmationData is a sub-element of the `Subject` element in the SAML assertion. 20 | self.recipient = recipient 21 | # The value in the NameID sub-element of the `Subject` element in the SAML assertion. 22 | self.subject = subject 23 | # The Format attribute of the `NameID` element in the SAML assertion. If the Format attribute is prefixed with `urn:oasis:names:tc:SAML:2.0:nameid-format:`, the prefix is not included in the value of this parameter. For example, if the value of the Format attribute is urn:oasis:names:tc:SAML:2.0:nameid-format:persistent/transient, the value of this parameter is `persistent/transient`. 24 | self.subject_type = subject_type 25 | 26 | def validate(self): 27 | pass 28 | 29 | def to_map(self): 30 | _map = super().to_map() 31 | if _map is not None: 32 | return _map 33 | 34 | result = dict() 35 | if self.issuer is not None: 36 | result["Issuer"] = self.issuer 37 | if self.recipient is not None: 38 | result["Recipient"] = self.recipient 39 | if self.subject is not None: 40 | result["Subject"] = self.subject 41 | if self.subject_type is not None: 42 | result["SubjectType"] = self.subject_type 43 | return result 44 | 45 | def from_map(self, m: dict = None): 46 | m = m or dict() 47 | if m.get("Issuer") is not None: 48 | self.issuer = m.get("Issuer") 49 | if m.get("Recipient") is not None: 50 | self.recipient = m.get("Recipient") 51 | if m.get("Subject") is not None: 52 | self.subject = m.get("Subject") 53 | if m.get("SubjectType") is not None: 54 | self.subject_type = m.get("SubjectType") 55 | return self 56 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_get_caller_identity_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class GetCallerIdentityResponseBody(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | account_id: str = None, 13 | arn: str = None, 14 | identity_type: str = None, 15 | principal_id: str = None, 16 | request_id: str = None, 17 | role_id: str = None, 18 | user_id: str = None, 19 | ): 20 | self.account_id = account_id 21 | self.arn = arn 22 | self.identity_type = identity_type 23 | self.principal_id = principal_id 24 | self.request_id = request_id 25 | self.role_id = role_id 26 | self.user_id = user_id 27 | 28 | def validate(self): 29 | pass 30 | 31 | def to_map(self): 32 | _map = super().to_map() 33 | if _map is not None: 34 | return _map 35 | 36 | result = dict() 37 | if self.account_id is not None: 38 | result["AccountId"] = self.account_id 39 | if self.arn is not None: 40 | result["Arn"] = self.arn 41 | if self.identity_type is not None: 42 | result["IdentityType"] = self.identity_type 43 | if self.principal_id is not None: 44 | result["PrincipalId"] = self.principal_id 45 | if self.request_id is not None: 46 | result["RequestId"] = self.request_id 47 | if self.role_id is not None: 48 | result["RoleId"] = self.role_id 49 | if self.user_id is not None: 50 | result["UserId"] = self.user_id 51 | return result 52 | 53 | def from_map(self, m: dict = None): 54 | m = m or dict() 55 | if m.get("AccountId") is not None: 56 | self.account_id = m.get("AccountId") 57 | if m.get("Arn") is not None: 58 | self.arn = m.get("Arn") 59 | if m.get("IdentityType") is not None: 60 | self.identity_type = m.get("IdentityType") 61 | if m.get("PrincipalId") is not None: 62 | self.principal_id = m.get("PrincipalId") 63 | if m.get("RequestId") is not None: 64 | self.request_id = m.get("RequestId") 65 | if m.get("RoleId") is not None: 66 | self.role_id = m.get("RoleId") 67 | if m.get("UserId") is not None: 68 | self.user_id = m.get("UserId") 69 | return self 70 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import asynccontextmanager 3 | from typing import AsyncIterator, Optional 4 | 5 | from alibabacloud_credentials.client import Client as CredClient 6 | from mcp.server import FastMCP 7 | from mcp_server_aliyun_observability.toolkits.iaas.toolkit import register_iaas_tools 8 | from mcp_server_aliyun_observability.toolkits.paas.toolkit import register_paas_tools 9 | from mcp_server_aliyun_observability.toolkits.shared.toolkit import ( 10 | register_shared_tools, 11 | ) 12 | from mcp_server_aliyun_observability.utils import ( 13 | CMSClientWrapper, 14 | CredentialWrapper, 15 | SLSClientWrapper, 16 | ) 17 | 18 | 19 | def create_lifespan(credential: Optional[CredentialWrapper] = None): 20 | @asynccontextmanager 21 | async def lifespan(fastmcp) -> AsyncIterator[dict]: 22 | sls_client = SLSClientWrapper(credential) 23 | cms_client = CMSClientWrapper(credential) 24 | yield { 25 | "sls_client": sls_client, 26 | "cms_client": cms_client, 27 | } 28 | 29 | return lifespan 30 | 31 | 32 | def init_server( 33 | credential: Optional[CredentialWrapper] = None, 34 | log_level: str = "INFO", 35 | transport_port: int = 8000, 36 | host: str = "0.0.0.0", 37 | ): 38 | """initialize the global mcp server instance""" 39 | mcp_server = FastMCP( 40 | name="mcp_aliyun_observability_server", 41 | lifespan=create_lifespan(credential), 42 | log_level=log_level, 43 | port=transport_port, 44 | host=host, 45 | ) 46 | 47 | # 根据 scope 环境变量注册相应的工具包 48 | scope = os.environ.get("MCP_TOOLKIT_SCOPE", "all").lower() 49 | registered_scopes = [] 50 | 51 | if scope == "all" or scope == "iaas": 52 | register_iaas_tools(mcp_server) 53 | registered_scopes.append("IaaS") 54 | 55 | if scope == "all" or scope == "paas": 56 | register_paas_tools(mcp_server) 57 | registered_scopes.append("PaaS") 58 | 59 | # 注册共享工具 (所有层级都需要) 60 | register_shared_tools(mcp_server) 61 | print("已注册共享工具: list_workspace, list_domains") 62 | 63 | print(f"已注册工具包范围 [{scope}]: {', '.join(registered_scopes)}") 64 | 65 | return mcp_server 66 | 67 | 68 | def server( 69 | credential: Optional[CredentialWrapper] = None, 70 | transport: str = "stdio", 71 | log_level: str = "INFO", 72 | transport_port: int = 8000, 73 | host: str = "0.0.0.0", 74 | ): 75 | server = init_server(credential, log_level, transport_port, host) 76 | server.run(transport) 77 | host: str = ("0.0.0.0",) 78 | -------------------------------------------------------------------------------- /libs/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Licensed to the Apache Software Foundation (ASF) under one 4 | or more contributor license agreements. See the NOTICE file 5 | distributed with this work for additional information 6 | regarding copyright ownership. The ASF licenses this file 7 | to you under the Apache License, Version 2.0 (the 8 | "License"); you may not use this file except in compliance 9 | with the License. You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, 14 | software distributed under the License is distributed on an 15 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | KIND, either express or implied. See the License for the 17 | specific language governing permissions and limitations 18 | under the License. 19 | """ 20 | 21 | import os 22 | 23 | from setuptools import find_packages, setup 24 | 25 | """ 26 | setup module for tea_python_tests. 27 | 28 | Created on * 29 | 30 | @author: Alibaba 31 | """ 32 | 33 | PACKAGE = "alibabacloud_sts20150401" 34 | NAME = "alibabacloud_sts20150401" 35 | DESCRIPTION = "" 36 | AUTHOR = "Alibaba" 37 | AUTHOR_EMAIL = "" 38 | URL = "https://github.com/" 39 | VERSION = __import__(PACKAGE).__version__ 40 | REQUIRES = ["darabonba-core>=1.0.0, <2.0.0", "alibabacloud_tea_openapi==0.4.0rc3"] 41 | 42 | LONG_DESCRIPTION = "" 43 | if os.path.exists("./README.md"): 44 | with open("README.md", encoding="utf-8") as fp: 45 | LONG_DESCRIPTION = fp.read() 46 | 47 | setup( 48 | name=NAME, 49 | version=VERSION, 50 | description=DESCRIPTION, 51 | long_description=LONG_DESCRIPTION, 52 | long_description_content_type="text/markdown", 53 | author=AUTHOR, 54 | author_email=AUTHOR_EMAIL, 55 | license="Apache License 2.0", 56 | url=URL, 57 | keywords=["tea", "python", "tests"], 58 | packages=find_packages(exclude=["tests*"]), 59 | include_package_data=True, 60 | platforms="any", 61 | install_requires=REQUIRES, 62 | python_requires=">=3.6", 63 | classifiers=( 64 | "Development Status :: 4 - Beta", 65 | "Intended Audience :: Developers", 66 | "License :: OSI Approved :: Apache Software License", 67 | "Programming Language :: Python", 68 | "Programming Language :: Python :: 3", 69 | "Programming Language :: Python :: 3.6", 70 | "Programming Language :: Python :: 3.7", 71 | "Programming Language :: Python :: 3.8", 72 | "Programming Language :: Python :: 3.9", 73 | "Topic :: Software Development", 74 | ), 75 | ) 76 | -------------------------------------------------------------------------------- /libs/sts-20150401/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Licensed to the Apache Software Foundation (ASF) under one 4 | or more contributor license agreements. See the NOTICE file 5 | distributed with this work for additional information 6 | regarding copyright ownership. The ASF licenses this file 7 | to you under the Apache License, Version 2.0 (the 8 | "License"); you may not use this file except in compliance 9 | with the License. You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, 14 | software distributed under the License is distributed on an 15 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | KIND, either express or implied. See the License for the 17 | specific language governing permissions and limitations 18 | under the License. 19 | """ 20 | 21 | import os 22 | 23 | from setuptools import find_packages, setup 24 | 25 | """ 26 | setup module for tea_python_tests. 27 | 28 | Created on * 29 | 30 | @author: Alibaba 31 | """ 32 | 33 | PACKAGE = "alibabacloud_sts20150401" 34 | NAME = "alibabacloud_sts20150401" 35 | DESCRIPTION = "" 36 | AUTHOR = "Alibaba" 37 | AUTHOR_EMAIL = "" 38 | URL = "https://github.com/" 39 | VERSION = __import__(PACKAGE).__version__ 40 | REQUIRES = ["darabonba-core>=1.0.0, <2.0.0", "alibabacloud_tea_openapi==0.4.0rc3"] 41 | 42 | LONG_DESCRIPTION = "" 43 | if os.path.exists("./README.md"): 44 | with open("README.md", encoding="utf-8") as fp: 45 | LONG_DESCRIPTION = fp.read() 46 | 47 | setup( 48 | name=NAME, 49 | version=VERSION, 50 | description=DESCRIPTION, 51 | long_description=LONG_DESCRIPTION, 52 | long_description_content_type="text/markdown", 53 | author=AUTHOR, 54 | author_email=AUTHOR_EMAIL, 55 | license="Apache License 2.0", 56 | url=URL, 57 | keywords=["tea", "python", "tests"], 58 | packages=find_packages(exclude=["tests*"]), 59 | include_package_data=True, 60 | platforms="any", 61 | install_requires=REQUIRES, 62 | python_requires=">=3.6", 63 | classifiers=( 64 | "Development Status :: 4 - Beta", 65 | "Intended Audience :: Developers", 66 | "License :: OSI Approved :: Apache Software License", 67 | "Programming Language :: Python", 68 | "Programming Language :: Python :: 3", 69 | "Programming Language :: Python :: 3.6", 70 | "Programming Language :: Python :: 3.7", 71 | "Programming Language :: Python :: 3.8", 72 | "Programming Language :: Python :: 3.9", 73 | "Topic :: Software Development", 74 | ), 75 | ) 76 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcresponse_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class AssumeRoleWithOIDCResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | assumed_role_user: main_models.AssumeRoleWithOIDCResponseBodyAssumedRoleUser = None, 15 | credentials: main_models.AssumeRoleWithOIDCResponseBodyCredentials = None, 16 | oidctoken_info: main_models.AssumeRoleWithOIDCResponseBodyOIDCTokenInfo = None, 17 | request_id: str = None, 18 | ): 19 | # The temporary identity that you use to assume the RAM role. 20 | self.assumed_role_user = assumed_role_user 21 | # The access credentials. 22 | self.credentials = credentials 23 | # The information about the OIDC token. 24 | self.oidctoken_info = oidctoken_info 25 | # The ID of the request. 26 | self.request_id = request_id 27 | 28 | def validate(self): 29 | if self.assumed_role_user: 30 | self.assumed_role_user.validate() 31 | if self.credentials: 32 | self.credentials.validate() 33 | if self.oidctoken_info: 34 | self.oidctoken_info.validate() 35 | 36 | def to_map(self): 37 | _map = super().to_map() 38 | if _map is not None: 39 | return _map 40 | 41 | result = dict() 42 | if self.assumed_role_user is not None: 43 | result["AssumedRoleUser"] = self.assumed_role_user.to_map() 44 | 45 | if self.credentials is not None: 46 | result["Credentials"] = self.credentials.to_map() 47 | 48 | if self.oidctoken_info is not None: 49 | result["OIDCTokenInfo"] = self.oidctoken_info.to_map() 50 | 51 | if self.request_id is not None: 52 | result["RequestId"] = self.request_id 53 | return result 54 | 55 | def from_map(self, m: dict = None): 56 | m = m or dict() 57 | if m.get("AssumedRoleUser") is not None: 58 | temp_model = main_models.AssumeRoleWithOIDCResponseBodyAssumedRoleUser() 59 | self.assumed_role_user = temp_model.from_map(m.get("AssumedRoleUser")) 60 | 61 | if m.get("Credentials") is not None: 62 | temp_model = main_models.AssumeRoleWithOIDCResponseBodyCredentials() 63 | self.credentials = temp_model.from_map(m.get("Credentials")) 64 | 65 | if m.get("OIDCTokenInfo") is not None: 66 | temp_model = main_models.AssumeRoleWithOIDCResponseBodyOidctokenInfo() 67 | self.oidctoken_info = temp_model.from_map(m.get("OIDCTokenInfo")) 68 | 69 | if m.get("RequestId") is not None: 70 | self.request_id = m.get("RequestId") 71 | return self 72 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlresponse_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | from alibabacloud_sts20150401 import models as main_models 8 | 9 | 10 | class AssumeRoleWithSAMLResponseBody(DaraModel): 11 | def __init__( 12 | self, 13 | *, 14 | assumed_role_user: main_models.AssumeRoleWithSAMLResponseBodyAssumedRoleUser = None, 15 | credentials: main_models.AssumeRoleWithSAMLResponseBodyCredentials = None, 16 | request_id: str = None, 17 | samlassertion_info: main_models.AssumeRoleWithSAMLResponseBodySAMLAssertionInfo = None, 18 | ): 19 | # The temporary identity that you use to assume the RAM role. 20 | self.assumed_role_user = assumed_role_user 21 | # The STS credentials. 22 | self.credentials = credentials 23 | # The ID of the request. 24 | self.request_id = request_id 25 | # The information in the SAML assertion. 26 | self.samlassertion_info = samlassertion_info 27 | 28 | def validate(self): 29 | if self.assumed_role_user: 30 | self.assumed_role_user.validate() 31 | if self.credentials: 32 | self.credentials.validate() 33 | if self.samlassertion_info: 34 | self.samlassertion_info.validate() 35 | 36 | def to_map(self): 37 | _map = super().to_map() 38 | if _map is not None: 39 | return _map 40 | 41 | result = dict() 42 | if self.assumed_role_user is not None: 43 | result["AssumedRoleUser"] = self.assumed_role_user.to_map() 44 | 45 | if self.credentials is not None: 46 | result["Credentials"] = self.credentials.to_map() 47 | 48 | if self.request_id is not None: 49 | result["RequestId"] = self.request_id 50 | if self.samlassertion_info is not None: 51 | result["SAMLAssertionInfo"] = self.samlassertion_info.to_map() 52 | 53 | return result 54 | 55 | def from_map(self, m: dict = None): 56 | m = m or dict() 57 | if m.get("AssumedRoleUser") is not None: 58 | temp_model = main_models.AssumeRoleWithSAMLResponseBodyAssumedRoleUser() 59 | self.assumed_role_user = temp_model.from_map(m.get("AssumedRoleUser")) 60 | 61 | if m.get("Credentials") is not None: 62 | temp_model = main_models.AssumeRoleWithSAMLResponseBodyCredentials() 63 | self.credentials = temp_model.from_map(m.get("Credentials")) 64 | 65 | if m.get("RequestId") is not None: 66 | self.request_id = m.get("RequestId") 67 | if m.get("SAMLAssertionInfo") is not None: 68 | temp_model = main_models.AssumeRoleWithSAMLResponseBodySamlassertionInfo() 69 | self.samlassertion_info = temp_model.from_map(m.get("SAMLAssertionInfo")) 70 | 71 | return self 72 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcresponse_body_oidctoken_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithOIDCResponseBodyOIDCTokenInfo(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | client_ids: str = None, 13 | expiration_time: str = None, 14 | issuance_time: str = None, 15 | issuer: str = None, 16 | subject: str = None, 17 | verification_info: str = None, 18 | ): 19 | # The audience. If multiple audiences are returned, the audiences are separated by commas (,). 20 | # 21 | # The audience is represented by the `aud` field in the OIDC Token. 22 | self.client_ids = client_ids 23 | # The time when the OIDC token expires. 24 | self.expiration_time = expiration_time 25 | # The time when the OIDC token was issued. 26 | self.issuance_time = issuance_time 27 | # The URL of the issuer, 28 | # 29 | # which is represented by the `iss` field in the OIDC Token. 30 | self.issuer = issuer 31 | # The subject, 32 | # 33 | # which is represented by the `sub` field in the OIDC Token. 34 | self.subject = subject 35 | # The verification information about the OIDC token. For more information, see [Manage an OIDC IdP](https://help.aliyun.com/document_detail/327123.html). 36 | self.verification_info = verification_info 37 | 38 | def validate(self): 39 | pass 40 | 41 | def to_map(self): 42 | _map = super().to_map() 43 | if _map is not None: 44 | return _map 45 | 46 | result = dict() 47 | if self.client_ids is not None: 48 | result["ClientIds"] = self.client_ids 49 | if self.expiration_time is not None: 50 | result["ExpirationTime"] = self.expiration_time 51 | if self.issuance_time is not None: 52 | result["IssuanceTime"] = self.issuance_time 53 | if self.issuer is not None: 54 | result["Issuer"] = self.issuer 55 | if self.subject is not None: 56 | result["Subject"] = self.subject 57 | if self.verification_info is not None: 58 | result["VerificationInfo"] = self.verification_info 59 | return result 60 | 61 | def from_map(self, m: dict = None): 62 | m = m or dict() 63 | if m.get("ClientIds") is not None: 64 | self.client_ids = m.get("ClientIds") 65 | if m.get("ExpirationTime") is not None: 66 | self.expiration_time = m.get("ExpirationTime") 67 | if m.get("IssuanceTime") is not None: 68 | self.issuance_time = m.get("IssuanceTime") 69 | if m.get("Issuer") is not None: 70 | self.issuer = m.get("Issuer") 71 | if m.get("Subject") is not None: 72 | self.subject = m.get("Subject") 73 | if m.get("VerificationInfo") is not None: 74 | self.verification_info = m.get("VerificationInfo") 75 | return self 76 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | import dotenv 5 | 6 | from mcp_server_aliyun_observability.settings import ( 7 | GlobalSettings, 8 | SLSSettings, 9 | CMSSettings, 10 | configure_settings, 11 | build_endpoint_mapping, 12 | ) 13 | 14 | dotenv.load_dotenv() 15 | 16 | 17 | @click.command() 18 | @click.option( 19 | "--access-key-id", 20 | type=str, 21 | help="aliyun access key id", 22 | required=False, 23 | ) 24 | @click.option( 25 | "--access-key-secret", 26 | type=str, 27 | help="aliyun access key secret", 28 | required=False, 29 | ) 30 | @click.option( 31 | "--knowledge-config", 32 | type=str, 33 | help="knowledge config file path", 34 | required=False, 35 | ) 36 | @click.option( 37 | "--transport", 38 | type=click.Choice(["stdio", "sse", "streamable-http"]), 39 | help="transport type: stdio or sse (streamableHttp coming soon)", 40 | default="streamable-http", 41 | ) 42 | @click.option("--host", type=str, help="host", default="127.0.0.1") 43 | @click.option("--log-level", type=str, help="log level", default="INFO") 44 | @click.option("--transport-port", type=int, help="transport port", default=8080) 45 | @click.option( 46 | "--sls-endpoints", 47 | "sls_endpoints", 48 | type=str, 49 | help="REGION=HOST pairs (comma/space separated) for SLS", 50 | ) 51 | @click.option( 52 | "--cms-endpoints", 53 | "cms_endpoints", 54 | type=str, 55 | help="REGION=HOST pairs (comma/space separated) for CMS", 56 | ) 57 | @click.option( 58 | "--scope", 59 | type=click.Choice(["paas", "iaas", "all"]), 60 | help="工具范围: paas(平台API), iaas(基础设施), all(全部)", 61 | default="all", 62 | ) 63 | def main( 64 | access_key_id, 65 | access_key_secret, 66 | knowledge_config, 67 | transport, 68 | log_level, 69 | transport_port, 70 | host, 71 | sls_endpoints, 72 | cms_endpoints, 73 | scope, 74 | ): 75 | # Lazy import heavy modules to keep package import light for library/test usage 76 | from mcp_server_aliyun_observability.server import server 77 | from mcp_server_aliyun_observability.utils import CredentialWrapper 78 | 79 | # Configure global endpoint settings (process-wide, frozen) 80 | try: 81 | sls_mapping = build_endpoint_mapping(cli_pairs=None, combined=sls_endpoints) 82 | cms_mapping = build_endpoint_mapping(cli_pairs=None, combined=cms_endpoints) 83 | settings = GlobalSettings( 84 | sls=SLSSettings(endpoints=sls_mapping), 85 | cms=CMSSettings(endpoints=cms_mapping), 86 | ) 87 | configure_settings(settings) 88 | except Exception as e: 89 | click.echo(f"[warn] failed to configure endpoints: {e}", err=True) 90 | 91 | if access_key_id and access_key_secret: 92 | credential = CredentialWrapper( 93 | access_key_id, access_key_secret, knowledge_config 94 | ) 95 | else: 96 | credential = None 97 | # 设置环境变量,传递给服务器 98 | if scope and scope != "all": 99 | os.environ['MCP_TOOLKIT_SCOPE'] = scope 100 | 101 | server(credential, transport, log_level, transport_port, host) 102 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 版本更新 2 | 3 | ## 1.0.2 4 | ### 新功能 5 | - `umodel_get_metrics` 工具新增高级分析模式支持 6 | - **cluster (时序聚类)**: 使用 K-Means 算法对多实体指标进行聚类分析,自动识别相似行为模式 7 | - 输出: `__cluster_index__`, `__entities__`, `__sample_ts__`, `__sample_value__`, `__sample_value_max/min/avg__` 8 | - 聚类数根据实体数量自动计算 (2-7) 9 | - **forecast (时序预测)**: 基于历史数据预测未来指标趋势,支持自定义预测时长 10 | - 输出: `__forecast_ts__`, `__forecast_value__`, `__forecast_lower/upper_value__`, `__labels__`, `__name__`, `__entity_id__` 11 | - 自动调整学习时间范围 (1-5天) 12 | - 新增 `forecast_duration` 参数,支持 `30m`, `1h`, `2d` 等格式 13 | - **anomaly_detection (异常检测)**: 使用时序分解算法识别指标中的异常点 14 | - 输出: `__entity_id__`, `__anomaly_list_`, `__anomaly_msg__`, `__value_min/max/avg__` 15 | - 自动调整学习时间范围 (1-3天) 16 | 17 | - `umodel_get_traces` 工具新增独占耗时计算 18 | - 新增输出字段 `exclusive_duration_ms`:span 独占耗时(排除子 span 后的实际执行时间) 19 | - 结果按独占耗时降序排序,便于快速定位性能瓶颈 20 | 21 | ## 1.0.0 (重建) 22 | - `master` 已重建为 `1.x.x` 最新内容的单提交快照,旧 `master` 历史迁移至 `0.3.x` 分支。 23 | - README 增加分支说明、工具差异对照表,明确后续基于 1.x.x 维护。 24 | - `.gitignore` 补充 `docs/`、`agents.md` 以避免无意提交。 25 | 26 | ## 0.2.9 27 | - 修复获取logstore时候类型不匹配问题 28 | ## 0.2.8 29 | - 增加 streamable-http 支持,可通过 --transport streamable-http 指定 30 | - 增加 host 参数,可通过 --host 指定 MCP Server 的监听地址 31 | - 重构日志系统,使用统一的Logger类替换标准logging 32 | - 新增自定义MCPLogger类,支持居中显示和富文本格式 33 | - 所有toolkit模块统一使用新的日志函数(log_error, log_info等) 34 | - 日志文件自动保存到用户目录~/mcp_server_aliyun_observability/,按日期命名 35 | - 支持终端彩色输出和文件日志双重记录 36 | 37 | ## 0.2.7 38 | - 修复sls_list_projects 工具返回结果类型错误问题,会导致高版本的MCP出现返回值提取错误 39 | 40 | ## 0.2.6 41 | - 增加 用户私有知识库 RAG 支持,在启动 MCP Server 时,设置可选参数--knowledge-config ./knowledge_config.json,配置文件样例请参见sample/config/knowledge_config.json 42 | 43 | ## 0.2.5 44 | - 增加 ARMS 慢 Trace 分析工具 45 | 46 | ## 0.2.4 47 | - 增加 ARMS 火焰图工具,支持单火焰图分析以及差分火焰图 48 | 49 | ## 0.2.3 50 | - 增加 ARMS 应用详情工具 51 | - 优化一些tool 的命名,更加规范,提升模型解析成功率 52 | 53 | ## 0.2.2 54 | - 优化 SLS 查询工具,时间范围不显示传入,由SQL 生成工具直接返回判定 55 | - sls_list_projects 工具增加个数限制,并且做出提示 56 | 57 | ## 0.2.1 58 | - 优化 SLS 查询工具,增加 from_timestamp 和 to_timestamp 参数,确保查询语句的正确性 59 | - 增加 SLS 日志查询的 prompts 60 | 61 | ## 0.2.0 62 | - 增加 cms_translate_natural_language_to_promql 工具,根据自然语言生成 promql 查询语句 63 | 64 | ## 0.1.9 65 | - 支持 STS Token 方式登录,可通过环境变量ALIBABA_CLOUD_SECURITY_TOKEN 指定 66 | - 修改 README.md 文档,增加 Cursor,Cline 等集成说明以及 UV 命令等说明 67 | 68 | ## 0.1.8 69 | - 优化 SLS 列出日志库工具,添加日志库类型验证,确保参数符合规范 70 | 71 | 72 | ## 0.1.7 73 | - 优化错误处理机制,简化错误代码,提高系统稳定性 74 | - 改进 SLS 日志服务相关工具 75 | - 增强 sls_list_logstores 工具,添加日志库类型验证,确保参数符合规范 76 | - 完善日志库类型描述,明确区分日志类型(logs)和指标类型(metrics) 77 | - 优化指标类型日志库筛选逻辑,仅当用户明确需要时才返回指标类型 78 | 79 | ## 0.1.6 80 | ### 工具列表 81 | - 增加 SQL 诊断工具, 当 SLS 查询语句执行失败时,可以调用该工具,根据错误信息,生成诊断结果。诊断结果会包含查询语句的正确性、性能分析、优化建议等信息。 82 | 83 | 84 | ## 0.1.0 85 | 本次发布版本为 0.1.0,以新增工具为主,主要包含 SLS 日志服务和 ARMS 应用实时监控服务相关工具。 86 | 87 | 88 | ### 工具列表 89 | 90 | - 增加 SLS 日志服务相关工具 91 | - `sls_describe_logstore` 92 | - 获取 SLS Logstore 的索引信息 93 | - `sls_list_projects` 94 | - 获取 SLS 项目列表 95 | - `sls_list_logstores` 96 | - 获取 SLS Logstore 列表 97 | - `sls_describe_logstore` 98 | - 获取 SLS Logstore 的索引信息 99 | - `sls_execute_query` 100 | - 执行SLS 日志查询 101 | - `sls_translate_natural_language_to_query` 102 | - 翻译自然语言为SLS 查询语句 103 | 104 | - 增加 ARMS 应用实时监控服务相关工具 105 | - `arms_search_apps` 106 | - 搜索 ARMS 应用 107 | - `arms_generate_trace_query` 108 | - 根据自然语言生成 trace 查询语句 109 | 110 | ### 场景举例 111 | 112 | - 场景一: 快速查询某个 logstore 相关结构 113 | - 使用工具: 114 | - `sls_list_logstores` 115 | - `sls_describe_logstore` 116 | ![image](./images/search_log_store.png) 117 | 118 | 119 | - 场景二: 模糊查询最近一天某个 logstore下面访问量最高的应用是什么 120 | - 分析: 121 | - 需要判断 logstore 是否存在 122 | - 获取 logstore 相关结构 123 | - 根据要求生成查询语句(对于语句用户可确认修改) 124 | - 执行查询语句 125 | - 根据查询结果生成响应 126 | - 使用工具: 127 | - `sls_list_logstores` 128 | - `sls_describe_logstore` 129 | - `sls_translate_natural_language_to_query` 130 | - `sls_execute_query` 131 | ![image](./images/fuzzy_search_and_get_logs.png) 132 | 133 | 134 | - 场景三: 查询 ARMS 某个应用下面响应最慢的几条 Trace 135 | - 分析: 136 | - 需要判断应用是否存在 137 | - 获取应用相关结构 138 | - 根据要求生成查询语句(对于语句用户可确认修改) 139 | - 执行查询语句 140 | - 根据查询结果生成响应 141 | - 使用工具: 142 | - `arms_search_apps` 143 | - `arms_generate_trace_query` 144 | - `sls_translate_natural_language_to_query` 145 | - `sls_execute_query` 146 | ![image](./images/find_slowest_trace.png) 147 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/core/models.py: -------------------------------------------------------------------------------- 1 | """通用数据模型定义""" 2 | from typing import Any, List, Optional 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class BaseToolParams(BaseModel): 8 | """所有工具的基础参数""" 9 | workspace: str = Field( 10 | ..., 11 | description="工作空间名称,数据隔离的基本单位,不同工作空间对应不同的数据域或项目。可使用 workspaces_list 工具获取可用的工作空间列表" 12 | ) 13 | region_id: str = Field( 14 | default="cn-shanghai", 15 | description="阿里云区域ID,如 cn-shanghai, cn-beijing, cn-hangzhou 等" 16 | ) 17 | 18 | 19 | # EntitySelector 已废弃,直接使用参数 20 | # 保留类定义用于兼容性,后续可以完全删除 21 | class EntitySelector(BaseModel): 22 | """[已废弃] 实体选择器,请直接使用domain, type, filters参数""" 23 | domain: str = Field( 24 | ..., 25 | description="必选实体域,如 apm, k8s, cloud_product。可使用 entities_list_domains 工具获取所有可用的实体域" 26 | ) 27 | type: str = Field( 28 | ..., 29 | description="必选实体类型,如 apm.service, k8s.pod, k8s.cluster。可使用 entities_list_types 工具获取指定域下的所有实体类型" 30 | ) 31 | filters: Optional[str] = Field( 32 | None, 33 | description="(可选)过滤条件,用于筛选特定的实体。支持自然语言描述,如 '名称为frontend'、'状态为健康'、'CPU使用率大于80%' 等" 34 | ) 35 | 36 | # EntitySelectorWithQuery 已废弃,直接使用参数 37 | # 保留类定义用于兼容性,后续可以完全删除 38 | class EntitySelectorWithQuery(BaseModel): 39 | """[已废弃] 带查询功能的实体选择器,请直接使用domain, type, filters, query参数""" 40 | domain: Optional[str] = Field( 41 | None, 42 | description="(可选)实体域,如 apm, k8s, cloud_product。可使用 entities_list_domains 工具获取所有可用的实体域" 43 | ) 44 | type: Optional[str] = Field( 45 | None, 46 | description="(可选)实体类型,如 apm.service, k8s.pod, k8s.cluster。可使用 entities_list_types 工具获取指定域下的所有实体类型" 47 | ) 48 | filters: Optional[str] = Field( 49 | None, 50 | description="(可选)过滤条件,用于筛选特定的实体。支持自然语言描述,如 '名称为frontend'、'状态为健康'、'CPU使用率大于80%' 等" 51 | ) 52 | query: Optional[str] = Field( 53 | None, 54 | description="自然语言过滤条件,用于复杂查询和智能筛选。如果提供此字段,将启用深度搜索(deepSearch);否则使用快速搜索(fastSearch)。示例:'service包含web的服务' 或 'CPU使用率大于80%的ECS实例' 或 'region等于cn-hangzhou的健康服务'" 55 | ) 56 | output_mode: str = Field( 57 | default="list", 58 | description="输出模式:'list' 返回实体详细列表(默认),'count' 只返回实体数量统计。当用户询问数量或需要避免大量数据时使用count模式" 59 | ) 60 | 61 | 62 | class EntityFuzzySelector(BaseModel): 63 | """实体模糊搜索选择器,专用于entities_fuzzy_search工具""" 64 | query: str = Field( 65 | ..., 66 | description="搜索关键词,支持实体名称、ID等关键词模糊匹配。示例:'payment'、'order-service'、'web-001'" 67 | ) 68 | domain: Optional[str] = Field( 69 | None, 70 | description="(可选)实体域过滤,如 apm, k8s, cloud_product。可使用 entities_list_domains 工具获取所有可用的实体域" 71 | ) 72 | type: Optional[str] = Field( 73 | None, 74 | description="(可选)实体类型过滤,如 apm.service, k8s.pod, k8s.cluster。可使用 entities_list_types 工具获取指定域下的所有实体类型" 75 | ) 76 | limit: int = Field( 77 | default=100, 78 | description="返回结果数量限制,默认100" 79 | ) 80 | 81 | 82 | class TimeRange(BaseModel): 83 | """时间范围定义""" 84 | start_time: str = Field( 85 | default="now()-1h", 86 | description="开始时间,支持相对时间表达式如 now()-1h 或绝对时间戳" 87 | ) 88 | end_time: str = Field( 89 | default="now()", 90 | description="结束时间,支持相对时间表达式如 now() 或绝对时间戳" 91 | ) 92 | 93 | 94 | class MetricQuery(BaseModel): 95 | """指标查询参数""" 96 | metric_name: str = Field( 97 | ..., 98 | description="指标名称。可使用 metrics_list 工具获取实体支持的所有指标列表" 99 | ) 100 | aggregation: Optional[str] = Field( 101 | None, 102 | description="聚合方式(如 avg, sum, max, min, count)" 103 | ) 104 | interval: Optional[str] = Field( 105 | "1m", 106 | description="数据点间隔,默认1分钟" 107 | ) 108 | 109 | 110 | class TraceFilter(BaseModel): 111 | """链路过滤条件""" 112 | error: Optional[bool] = Field( 113 | None, 114 | description="是否只查询错误链路" 115 | ) 116 | min_duration: Optional[int] = Field( 117 | None, 118 | description="最小耗时(毫秒)" 119 | ) 120 | max_duration: Optional[int] = Field( 121 | None, 122 | description="最大耗时(毫秒)" 123 | ) 124 | status_codes: Optional[List[str]] = Field( 125 | None, 126 | description="HTTP状态码列表" 127 | ) 128 | 129 | 130 | class EventFilter(BaseModel): 131 | """事件过滤条件""" 132 | event_type: Optional[str] = Field( 133 | None, 134 | description="事件类型(如 change, alert)" 135 | ) 136 | severity: Optional[str] = Field( 137 | None, 138 | description="严重程度(如 critical, warning, info)" 139 | ) 140 | source: Optional[str] = Field( 141 | None, 142 | description="事件来源" 143 | ) -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # MCP 贡献指南 2 | 3 | ## 步骤 4 | 1. 从 master 分支创建一个分支 5 | 2. 在分支上进行开发测试 6 | 3. 测试完毕之后提交PR 7 | 4. 合并PR到Release分支 8 | 5. 基于 Release 分支发布新版本 9 | 6. 更新 master 分支 10 | 7. 生成版本 tag 11 | 12 | ## 项目结构 13 | 14 | ``` 15 | mcp_server_aliyun_observability/ 16 | ├── src/ 17 | │ ├── mcp_server_aliyun_observability/ 18 | │ │ ├── server.py # MCP 服务端核心 19 | │ │ ├── core/ # 核心基础设施 20 | │ │ │ ├── models.py # 数据模型 21 | │ │ │ ├── decorators.py # 装饰器 22 | │ │ │ ├── utils.py # 工具函数 23 | │ │ │ └── inner/ # 内部模块 24 | │ │ ├── toolkits/ # 工具包目录 25 | │ │ │ ├── entities/ # 实体查询工具包 26 | │ │ │ ├── metrics/ # 指标查询工具包 27 | │ │ │ ├── traces/ # 链路查询工具包 28 | │ │ │ ├── events/ # 事件查询工具包 29 | │ │ │ ├── topologies/ # 拓扑查询工具包 30 | │ │ │ ├── diagnosis/ # 诊断查询工具包 31 | │ │ │ ├── drilldown/ # 下钻查询工具包 32 | │ │ │ ├── workspace/ # 工作空间工具包 33 | │ │ │ └── iaas/ # V1兼容工具包 34 | │ │ └── utils.py # 客户端包装器 35 | │ └── tests/ # 测试目录 36 | │ ├── mcp_server_aliyun_observability/ 37 | │ │ ├── core/ # 核心模块测试 38 | │ │ └── toolkits/ # 工具包测试 39 | │ └── conftest.py 40 | ``` 41 | 42 | ### 架构说明 43 | 1. **server.py**: MCP 服务端代码,负责处理 MCP 请求和动态工具包注册 44 | 2. **core/**: 核心基础设施,包含通用模型、装饰器、工具函数和内部模块 45 | 3. **toolkits/**: 模块化工具包目录,按功能域组织: 46 | - **CMS工具集**: entities, metrics, traces, events, topologies, diagnosis, drilldown, workspace (可观测2.0) 47 | - **IaaS工具集**: iaas/ (V1兼容架构,包含传统SLS、ARMS、CMS工具) 48 | 4. **utils.py**: 客户端包装器和通用工具函数 49 | 5. **tests/**: 按模块组织的测试用例 50 | 51 | ## 如何增加一个 MCP 工具 52 | 53 | Python 版本要求 >=3.10(MCP SDK 的版本要求),建议通过venv或者 conda 来创建虚拟环境 54 | 55 | ## 任务拆解 56 | 57 | 1. 首先需要明确提供什么样的场景,然后再根据场景拆解需要提供什么功能 58 | 2. 对于复杂的场景不建议提供一个工具,而是拆分成多个工具,然后由 LLM 来组合完成任务 59 | - 好处:提升工具的执行成功率 60 | - 如果其中一步失败,模型也可以尝试纠正 61 | - 示例:查询 APM 一个应用的慢调用可拆解为查询应用信息、生成查询慢调用 SQL、执行查询慢调用 SQL 等步骤 62 | 3. 尽量复用已有工具,不要新增相同含义的工具 63 | 64 | ## 工具定义 65 | 1. 新增的工具位于 `src/mcp_server_aliyun_observability/toolkit` 目录下,通过增加 `@self.server.tool()` 注解来定义一个工具。 66 | 2. 当前可按照产品来组织文件,比如 `src/mcp_server_aliyun_observability/toolkit/sls_toolkit.py` 来定义SLS相关的工具,`src/mcp_server_aliyun_observability/toolkit/arms_toolkit.py` 来定义ARMS相关的工具。 67 | 3. 工具上需要增加@tool 注解 68 | 69 | ### 1. 工具命名 70 | 71 | * 格式为 `{product_name}_{function_name}` 72 | * 示例:`sls_describe_logstore`、`arms_search_apps` 等 73 | * 优势:方便模型识别,当用户集成的工具较多时不会造成歧义和冲突 74 | 75 | ### 2. 参数描述 76 | 77 | * 需要尽可能详细,包括输入输出明确定义、示例、使用指导 78 | * 参数使用 pydantic 的模型来定义,示例: 79 | 80 | ```python 81 | @self.server.tool() 82 | def sls_list_projects( 83 | ctx: Context, 84 | project_name_query: str = Field( 85 | None, description="project name,fuzzy search" 86 | ), 87 | limit: int = Field( 88 | default=10, description="limit,max is 100", ge=1, le=100 89 | ), 90 | region_id: str = Field(default=..., description="aliyun region id"), 91 | ) -> list[dict[str, Any]]: 92 | ``` 93 | 94 | * 参数注意事项: 95 | - 参数个数尽量控制在五个以内,超过需考虑拆分工具 96 | - 相同含义字段定义保持一致(避免一会叫 `project_name`,一会叫 `project`) 97 | - 参数类型使用基础类型(str, int, list, dict 等),不使用自定义类型 98 | - 如果参数可选值是固定枚举类,在字段描述中要说明可选择的值,同时在代码方法里面也要增加可选值的校验 99 | 100 | ### 3. 返回值设计 101 | 102 | * 优先使用基础类型,不使用自定义类型 103 | * 控制返回内容长度,特别是数据查询类场景考虑分页返回,防止用户上下文占用过大 104 | * 返回内容字段清晰,数据类最好转换为明确的 key-value 形式 105 | * 针对无返回值的情况,比如数据查询为空,不要直接返回空列表,可以返回文本提示比如 `"没有找到相关数据"`供大模型使用 106 | 107 | ### 4. 异常处理 108 | 109 | * 直接调用 API 且异常信息清晰的情况下可不做处理,直接抛出原始错误日志有助于模型识别 110 | * 如遇 SYSTEM_ERROR 等模糊不清的异常,应处理后返回友好提示 111 | * 做好重试机制,比如网络抖动、服务端限流等,避免模型因此类问题而重复调用 112 | 113 | ### 5. 工具描述 114 | 115 | * 添加工具描述有两种方法: 116 | - 在 `@self.server.tool()` 中增加 description 参数 117 | - 使用 Python 的 docstring 描述 118 | * 描述内容应包括:功能概述、使用场景、返回数据结构、查询示例、参数说明等,示例: 119 | 120 | ``` 121 | 列出阿里云日志服务中的所有项目。 122 | 123 | ## 功能概述 124 | 125 | 该工具可以列出指定区域中的所有SLS项目,支持通过项目名进行模糊搜索。如果不提供项目名称,则返回该区域的所有项目。 126 | 127 | ## 使用场景 128 | 129 | - 当需要查找特定项目是否存在时 130 | - 当需要获取某个区域下所有可用的SLS项目列表时 131 | - 当需要根据项目名称的部分内容查找相关项目时 132 | 133 | ## 返回数据结构 134 | 135 | 返回的项目信息包含: 136 | - project_name: 项目名称 137 | - description: 项目描述 138 | - region_id: 项目所在区域 139 | 140 | ## 查询示例 141 | 142 | - "有没有叫 XXX 的 project" 143 | - "列出所有SLS项目" 144 | 145 | Args: 146 | ctx: MCP上下文,用于访问SLS客户端 147 | project_name_query: 项目名称查询字符串,支持模糊搜索 148 | limit: 返回结果的最大数量,范围1-100,默认10 149 | region_id: 阿里云区域ID 150 | 151 | Returns: 152 | 包含项目信息的字典列表,每个字典包含project_name、description和region_id 153 | ``` 154 | * 可以使用 LLM 生成初步描述,然后根据需要进行调整完善 155 | 156 | ### 如何测试 157 | 158 | #### [阶段1] 不基于 LLM,使用测试用例测试 159 | 160 | 1. 补充下测试用例,在 tests目录下,可有参考 test_sls_toolkit.py 的实现 161 | 2. 使用 `pytest` 运行测试用例,保证功能是正确可用 162 | 163 | #### [阶段2] 基于 LLM,使用测试用例测试 164 | 1. 通过 Cursor,Client 等客户端来测试和大模型集成后的最终效果 -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_samlrequest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithSAMLRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | duration_seconds: int = None, 13 | policy: str = None, 14 | role_arn: str = None, 15 | samlassertion: str = None, 16 | samlprovider_arn: str = None, 17 | ): 18 | # The validity period of the STS token. Unit: seconds. 19 | # 20 | # Minimum value: 900. Maximum value: the value of the `MaxSessionDuration` parameter. Default value: 3600. 21 | # 22 | # You can call the CreateRole or UpdateRole operation to configure the `MaxSessionDuration` parameter. For more information, see [CreateRole](https://help.aliyun.com/document_detail/28710.html) or [UpdateRole](https://help.aliyun.com/document_detail/28712.html). 23 | self.duration_seconds = duration_seconds 24 | # The policy that specifies the permissions of the returned STS token. You can use this parameter to grant the STS token fewer permissions than the permissions granted to the RAM role. 25 | # 26 | # * If you specify this parameter, the permissions of the returned STS token are the permissions that are included in the value of this parameter and owned by the RAM role. 27 | # * If you do not specify this parameter, the returned STS token has all the permissions of the RAM role. 28 | # 29 | # The value must be 1 to 2,048 characters in length. 30 | self.policy = policy 31 | # The ARN of the RAM role. 32 | # 33 | # The trust entity of the RAM role is a SAML IdP. For more information, see [Create a RAM role for a trusted IdP](https://help.aliyun.com/document_detail/116805.html) or [CreateRole](https://help.aliyun.com/document_detail/28710.html). 34 | # 35 | # Format: `acs:ram:::role/`. 36 | # 37 | # You can view the ARN in the RAM console or by calling operations. 38 | # 39 | # * For more information about how to view the ARN in the RAM console, see [How do I view the ARN of the RAM role?](https://help.aliyun.com/document_detail/39744.html). 40 | # * For more information about how to view the ARN by calling operations, see [ListRoles](https://help.aliyun.com/document_detail/28713.html) or [GetRole](https://help.aliyun.com/document_detail/28711.html). 41 | self.role_arn = role_arn 42 | # The Base64-encoded SAML assertion. 43 | # 44 | # The value must be 4 to 100,000 characters in length. 45 | # 46 | # > A complete SAML response rather than a single SAMLAssertion field must be retrieved from the external IdP. 47 | self.samlassertion = samlassertion 48 | # The Alibaba Cloud Resource Name (ARN) of the SAML IdP that is created in the RAM console. 49 | # 50 | # Format: `acs:ram:::saml-provider/`. 51 | # 52 | # You can view the ARN in the RAM console or by calling operations. 53 | # 54 | # * For more information about how to view the ARN in the RAM console, see [How do I view the ARN of a RAM role?](https://help.aliyun.com/document_detail/116795.html) 55 | # * For more information about how to view the ARN by calling operations, see [GetSAMLProvider](https://help.aliyun.com/document_detail/186833.html) or [ListSAMLProviders](https://help.aliyun.com/document_detail/186851.html). 56 | self.samlprovider_arn = samlprovider_arn 57 | 58 | def validate(self): 59 | pass 60 | 61 | def to_map(self): 62 | _map = super().to_map() 63 | if _map is not None: 64 | return _map 65 | 66 | result = dict() 67 | if self.duration_seconds is not None: 68 | result["DurationSeconds"] = self.duration_seconds 69 | if self.policy is not None: 70 | result["Policy"] = self.policy 71 | if self.role_arn is not None: 72 | result["RoleArn"] = self.role_arn 73 | if self.samlassertion is not None: 74 | result["SAMLAssertion"] = self.samlassertion 75 | if self.samlprovider_arn is not None: 76 | result["SAMLProviderArn"] = self.samlprovider_arn 77 | return result 78 | 79 | def from_map(self, m: dict = None): 80 | m = m or dict() 81 | if m.get("DurationSeconds") is not None: 82 | self.duration_seconds = m.get("DurationSeconds") 83 | if m.get("Policy") is not None: 84 | self.policy = m.get("Policy") 85 | if m.get("RoleArn") is not None: 86 | self.role_arn = m.get("RoleArn") 87 | if m.get("SAMLAssertion") is not None: 88 | self.samlassertion = m.get("SAMLAssertion") 89 | if m.get("SAMLProviderArn") is not None: 90 | self.samlprovider_arn = m.get("SAMLProviderArn") 91 | return self 92 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global settings holder for the MCP server. 3 | 4 | This module centralizes process-wide configuration such as service endpoint 5 | overrides, providing a single place to resolve endpoints for SLS/ARMS clients. 6 | """ 7 | from __future__ import annotations 8 | 9 | from dataclasses import dataclass, field 10 | from threading import RLock 11 | from typing import Dict, Iterable, Optional 12 | import re 13 | 14 | 15 | # ---------------------- 16 | # Helpers & parsing 17 | # ---------------------- 18 | 19 | def normalize_host(val: str) -> str: 20 | """Normalize an endpoint host: strip scheme and trailing slash.""" 21 | v = (val or "").strip() 22 | v = re.sub(r"^https?://", "", v, flags=re.IGNORECASE) 23 | return v.rstrip("/") 24 | 25 | 26 | def _parse_pairs_str(s: str | None) -> Dict[str, str]: 27 | """Parse 'REGION=HOST,REGION2=HOST2' or whitespace/comma separated pairs.""" 28 | out: Dict[str, str] = {} 29 | if not s: 30 | return out 31 | tokens = re.split(r"[,\s]+", s.strip()) 32 | for t in tokens: 33 | if not t: 34 | continue 35 | if "=" not in t: 36 | raise ValueError(f"Invalid endpoint pair: '{t}', expected REGION=HOST") 37 | region, host = t.split("=", 1) 38 | out[region.strip()] = host.strip() 39 | return out 40 | 41 | 42 | def build_endpoint_mapping( 43 | cli_pairs: Iterable[str] | None, 44 | combined: Optional[str], 45 | ) -> Dict[str, str]: 46 | """Build endpoint mapping from CLI inputs only. 47 | 48 | Precedence: combined string < repeated cli_pairs (last wins). 49 | """ 50 | mapping: Dict[str, str] = {} 51 | 52 | # Combined string 53 | if combined: 54 | mapping.update(_parse_pairs_str(combined.strip())) 55 | 56 | # Repeated CLI pairs (override) 57 | for pair in (cli_pairs or []): 58 | mapping.update(_parse_pairs_str(pair)) 59 | 60 | # Normalize hosts 61 | return {k.strip(): normalize_host(v) for k, v in mapping.items()} 62 | 63 | 64 | # ---------------------- 65 | # Settings schema 66 | # ---------------------- 67 | 68 | 69 | @dataclass(frozen=True) 70 | class SLSSettings: 71 | """Settings for SLS related configuration.""" 72 | 73 | endpoints: Dict[str, str] = field(default_factory=dict) 74 | template: str = "{region}.log.aliyuncs.com" 75 | 76 | def __post_init__(self): 77 | normalized = {k: normalize_host(v) for k, v in (self.endpoints or {}).items()} 78 | object.__setattr__(self, "endpoints", normalized) 79 | 80 | def resolve(self, region: str) -> str: 81 | if not region: 82 | raise ValueError("region is required") 83 | host = self.endpoints.get(region) 84 | if host: 85 | return host 86 | return self.template.format(region=region) 87 | 88 | 89 | @dataclass(frozen=True) 90 | class CMSSettings: 91 | """Settings for CMS (Cloud Monitor Service) related configuration.""" 92 | 93 | endpoints: Dict[str, str] = field(default_factory=dict) 94 | template: str = "cms.{region}.aliyuncs.com" 95 | 96 | def __post_init__(self): 97 | normalized = {k: normalize_host(v) for k, v in (self.endpoints or {}).items()} 98 | object.__setattr__(self, "endpoints", normalized) 99 | 100 | def resolve(self, region: str) -> str: 101 | if not region: 102 | raise ValueError("region is required") 103 | host = self.endpoints.get(region) 104 | if host: 105 | return host 106 | return self.template.format(region=region) 107 | 108 | 109 | @dataclass(frozen=True) 110 | class GlobalSettings: 111 | """Top-level settings container. Extend with more sections when needed.""" 112 | 113 | sls: SLSSettings = field(default_factory=SLSSettings) 114 | cms: CMSSettings = field(default_factory=CMSSettings) 115 | 116 | 117 | # ---------------------- 118 | # Global holder (configure once, then freeze) 119 | # ---------------------- 120 | 121 | _lock = RLock() 122 | _settings: Optional[GlobalSettings] = None 123 | _frozen: bool = False 124 | 125 | 126 | def configure_settings(settings: GlobalSettings, freeze: bool = True) -> GlobalSettings: 127 | """Configure global settings. Call once at process startup.""" 128 | global _settings, _frozen 129 | with _lock: 130 | if _frozen: 131 | raise RuntimeError("GlobalSettings already frozen") 132 | _settings = settings 133 | if freeze: 134 | _frozen = True 135 | return _settings 136 | 137 | 138 | def get_settings() -> GlobalSettings: 139 | """Get current global settings (lazily creates default).""" 140 | global _settings 141 | with _lock: 142 | if _settings is None: 143 | _settings = GlobalSettings() 144 | return _settings 145 | 146 | 147 | # Test helpers (avoid using in production code) 148 | def _override_settings(settings: GlobalSettings): 149 | global _settings 150 | with _lock: 151 | _settings = settings 152 | 153 | 154 | def _reset_settings(): 155 | global _settings, _frozen 156 | with _lock: 157 | _settings = None 158 | _frozen = False 159 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/paas/time_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from typing import Tuple, Union 4 | 5 | 6 | class TimeRangeParser: 7 | """时间范围解析工具类 8 | 9 | 支持两种时间格式的解析: 10 | 1. Unix时间戳(整数) 11 | 2. 相对时间表达式(如 "now-1h", "now-30m", "now-1d") 12 | """ 13 | 14 | @staticmethod 15 | def parse_time_expression(time_expr: Union[str, int]) -> int: 16 | """解析时间表达式为Unix时间戳(秒) 17 | 18 | Args: 19 | time_expr: 时间表达式,支持: 20 | - Unix时间戳(整数,秒或毫秒) 21 | - 相对时间表达式:now-1h, now-30m, now-1d, now-7d 22 | 23 | Returns: 24 | Unix时间戳(秒) 25 | 26 | Examples: 27 | parse_time_expression(1640995200) -> 1640995200 (秒时间戳) 28 | parse_time_expression(1640995200000) -> 1640995200 (毫秒转秒) 29 | parse_time_expression("now-1h") -> 当前时间-1小时的时间戳 30 | parse_time_expression("now-30m") -> 当前时间-30分钟的时间戳 31 | """ 32 | # 如果是整数,需要判断是秒还是毫秒时间戳 33 | if isinstance(time_expr, int): 34 | return TimeRangeParser._normalize_timestamp(time_expr) 35 | 36 | if isinstance(time_expr, str) and time_expr.isdigit(): 37 | return TimeRangeParser._normalize_timestamp(int(time_expr)) 38 | 39 | # 解析相对时间表达式 40 | if isinstance(time_expr, str) and time_expr.startswith("now"): 41 | return TimeRangeParser._parse_relative_time(time_expr) 42 | 43 | # 如果都不匹配,尝试直接转换为整数 44 | try: 45 | timestamp = int(time_expr) 46 | return TimeRangeParser._normalize_timestamp(timestamp) 47 | except (ValueError, TypeError): 48 | raise ValueError(f"不支持的时间格式: {time_expr}") 49 | 50 | @staticmethod 51 | def _normalize_timestamp(timestamp: int) -> int: 52 | """标准化时间戳为秒级 53 | 54 | 自动判断输入的时间戳是秒还是毫秒,并转换为秒级时间戳 55 | 56 | Args: 57 | timestamp: 时间戳(秒或毫秒) 58 | 59 | Returns: 60 | 秒级时间戳 61 | """ 62 | # 判断是否为毫秒时间戳(通常毫秒时间戳长度为13位,秒级为10位) 63 | # 2000年1月1日的时间戳约为946684800(10位) 64 | # 如果时间戳大于这个值的1000倍,则认为是毫秒时间戳 65 | if timestamp > 946684800000: # 大于2000年的毫秒时间戳 66 | return timestamp // 1000 67 | else: 68 | return timestamp 69 | 70 | @staticmethod 71 | def _parse_relative_time(time_expr: str) -> int: 72 | """解析相对时间表达式 73 | 74 | Args: 75 | time_expr: 相对时间表达式,如 "now-1h", "now-30m" 76 | 77 | Returns: 78 | Unix时间戳(秒) 79 | """ 80 | now = int(time.time()) 81 | 82 | # 如果只是 "now" 83 | if time_expr.strip().lower() == "now": 84 | return now 85 | 86 | # 匹配模式: now-{数字}{单位} 87 | pattern = r'^now([+-])(\d+)([smhd])$' 88 | match = re.match(pattern, time_expr.strip().lower()) 89 | 90 | if not match: 91 | raise ValueError(f"无效的相对时间格式: {time_expr}. 支持格式: now, now-1h, now-30m, now-1d") 92 | 93 | operator, amount_str, unit = match.groups() 94 | amount = int(amount_str) 95 | 96 | # 计算时间偏移(秒) 97 | unit_multipliers = { 98 | 's': 1, # 秒 99 | 'm': 60, # 分钟 100 | 'h': 3600, # 小时 101 | 'd': 86400, # 天 102 | } 103 | 104 | if unit not in unit_multipliers: 105 | raise ValueError(f"不支持的时间单位: {unit}. 支持单位: s, m, h, d") 106 | 107 | offset_seconds = amount * unit_multipliers[unit] 108 | 109 | # 根据操作符计算最终时间 110 | if operator == '-': 111 | return now - offset_seconds 112 | else: # operator == '+' 113 | return now + offset_seconds 114 | 115 | @staticmethod 116 | def parse_time_range(from_time: Union[str, int], to_time: Union[str, int]) -> Tuple[int, int]: 117 | """解析时间范围 118 | 119 | Args: 120 | from_time: 开始时间表达式 121 | to_time: 结束时间表达式 122 | 123 | Returns: 124 | (开始时间戳, 结束时间戳) 的元组 125 | 126 | Examples: 127 | parse_time_range("now-1h", "now") -> (当前时间-1小时, 当前时间) 128 | parse_time_range(1640995200, 1640998800) -> (1640995200, 1640998800) 129 | """ 130 | from_timestamp = TimeRangeParser.parse_time_expression(from_time) 131 | to_timestamp = TimeRangeParser.parse_time_expression(to_time) 132 | 133 | # 确保时间范围有效 134 | if from_timestamp >= to_timestamp: 135 | raise ValueError(f"开始时间({from_timestamp})必须小于结束时间({to_timestamp})") 136 | 137 | return from_timestamp, to_timestamp 138 | 139 | @staticmethod 140 | def get_default_time_range(duration_minutes: int = 15) -> Tuple[int, int]: 141 | """获取默认时间范围 142 | 143 | Args: 144 | duration_minutes: 时间范围长度(分钟),默认15分钟 145 | 146 | Returns: 147 | (开始时间戳, 结束时间戳) 的元组 148 | """ 149 | now = int(time.time()) 150 | from_time = now - (duration_minutes * 60) 151 | return from_time, now -------------------------------------------------------------------------------- /tests/mcp_server_aliyun_observability/toolkits/paas/test_paas_entity_toolkit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # Add the src directory to Python path to ensure imports work 5 | src_dir = '/apsarapangu/SSDCache1/workspace/code/alibabacloud-observability-mcp-server/src' 6 | if src_dir not in sys.path: 7 | sys.path.insert(0, src_dir) 8 | 9 | import dotenv 10 | import pytest 11 | from mcp.server.fastmcp import Context, FastMCP 12 | from mcp.shared.context import RequestContext 13 | 14 | from mcp_server_aliyun_observability.server import create_lifespan 15 | from mcp_server_aliyun_observability.toolkits.paas.toolkit import register_paas_tools 16 | from mcp_server_aliyun_observability.toolkits.shared.toolkit import ( 17 | register_shared_tools, 18 | ) 19 | from mcp_server_aliyun_observability.utils import CMSClientWrapper, CredentialWrapper 20 | 21 | dotenv.load_dotenv() 22 | 23 | import logging 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def check_credentials_and_result(result): 29 | """检查凭证和结果,如果有凭证问题则跳过测试""" 30 | assert result is not None 31 | logger.info(f"测试结果: {result}") 32 | error = result.get("error") 33 | if error and "InvalidCredentials" in str(result.get("message", "")): 34 | pytest.skip("需要有效的阿里云凭证才能运行此测试") 35 | # 如果有error且不是凭证问题,则测试失败 36 | assert not error, f"测试结果: {result}" 37 | return result 38 | 39 | 40 | @pytest.fixture 41 | def mcp_server(): 42 | """创建模拟的FastMCP服务器实例用于实体工具测试""" 43 | mcp_server = FastMCP( 44 | name="mcp_aliyun_observability_entity_server", 45 | lifespan=create_lifespan( 46 | credential=CredentialWrapper( 47 | access_key_id=os.getenv("ALIYUN_ACCESS_KEY_ID"), 48 | access_key_secret=os.getenv("ALIYUN_ACCESS_KEY_SECRET"), 49 | knowledge_config=None, 50 | ), 51 | ), 52 | ) 53 | # 注册PaaS工具包 54 | register_paas_tools(mcp_server) 55 | register_shared_tools(mcp_server) 56 | return mcp_server 57 | 58 | 59 | @pytest.fixture 60 | def mock_request_context(): 61 | """创建模拟的RequestContext实例""" 62 | context = Context( 63 | request_context=RequestContext( 64 | request_id="test_entity_request_id", 65 | meta=None, 66 | session=None, 67 | lifespan_context={ 68 | "cms_client": CMSClientWrapper( 69 | credential=CredentialWrapper( 70 | access_key_id=os.getenv("ALIYUN_ACCESS_KEY_ID"), 71 | access_key_secret=os.getenv("ALIYUN_ACCESS_KEY_SECRET"), 72 | knowledge_config=None, 73 | ), 74 | ), 75 | "sls_client": None, 76 | "arms_client": None, 77 | }, 78 | ) 79 | ) 80 | return context 81 | 82 | 83 | class TestPaaSEntityToolkit: 84 | """PaaS实体工具的测试类""" 85 | 86 | @pytest.mark.asyncio 87 | async def test_paas_get_entities_success( 88 | self, 89 | mcp_server: FastMCP, 90 | mock_request_context: Context, 91 | ): 92 | """测试PaaS实体查询""" 93 | tool = mcp_server._tool_manager.get_tool("umodel_get_entities") 94 | result = await tool.run( 95 | { 96 | "domain": "apm", 97 | "entity_set_name": "apm.service", 98 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 99 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 100 | }, 101 | context=mock_request_context, 102 | ) 103 | result = check_credentials_and_result(result) 104 | 105 | @pytest.mark.asyncio 106 | async def test_paas_get_neighbor_entities_success( 107 | self, 108 | mcp_server: FastMCP, 109 | mock_request_context: Context, 110 | ): 111 | """测试PaaS邻居实体查询""" 112 | tool = mcp_server._tool_manager.get_tool("umodel_get_neighbor_entities") 113 | result = await tool.run( 114 | { 115 | "domain": "apm", 116 | "entity_set_name": "apm.service", 117 | "entity_id": "5a81706b75fe1295797af01544a31264", 118 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 119 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 120 | }, 121 | context=mock_request_context, 122 | ) 123 | result = check_credentials_and_result(result) 124 | 125 | @pytest.mark.asyncio 126 | async def test_paas_search_entities_success( 127 | self, 128 | mcp_server: FastMCP, 129 | mock_request_context: Context, 130 | ): 131 | """测试PaaS实体搜索""" 132 | tool = mcp_server._tool_manager.get_tool("umodel_search_entities") 133 | result = await tool.run( 134 | { 135 | "domain": "apm", 136 | "entity_set_name": "apm.service", 137 | "search_text": "payment", 138 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 139 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 140 | }, 141 | context=mock_request_context, 142 | ) 143 | result = check_credentials_and_result(result) 144 | 145 | 146 | if __name__ == "__main__": 147 | pytest.main([__file__, "-v"]) 148 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from ._assume_role_request import AssumeRoleRequest 6 | from ._assume_role_response import AssumeRoleResponse 7 | from ._assume_role_response_body import AssumeRoleResponseBody 8 | from ._assume_role_response_body_assumed_role_user import AssumeRoleResponseBodyAssumedRoleUser 9 | from ._assume_role_response_body_credentials import AssumeRoleResponseBodyCredentials 10 | from ._assume_role_with_oidcrequest import AssumeRoleWithOIDCRequest 11 | from ._assume_role_with_oidcresponse import AssumeRoleWithOIDCResponse 12 | from ._assume_role_with_oidcresponse_body import AssumeRoleWithOIDCResponseBody 13 | from ._assume_role_with_oidcresponse_body_assumed_role_user import AssumeRoleWithOIDCResponseBodyAssumedRoleUser 14 | from ._assume_role_with_oidcresponse_body_credentials import AssumeRoleWithOIDCResponseBodyCredentials 15 | from ._assume_role_with_oidcresponse_body_oidctoken_info import AssumeRoleWithOIDCResponseBodyOIDCTokenInfo 16 | from ._assume_role_with_samlrequest import AssumeRoleWithSAMLRequest 17 | from ._assume_role_with_samlresponse import AssumeRoleWithSAMLResponse 18 | from ._assume_role_with_samlresponse_body import AssumeRoleWithSAMLResponseBody 19 | from ._assume_role_with_samlresponse_body_assumed_role_user import AssumeRoleWithSAMLResponseBodyAssumedRoleUser 20 | from ._assume_role_with_samlresponse_body_credentials import AssumeRoleWithSAMLResponseBodyCredentials 21 | from ._assume_role_with_samlresponse_body_samlassertion_info import AssumeRoleWithSAMLResponseBodySAMLAssertionInfo 22 | from ._assume_role_with_service_identity_request import AssumeRoleWithServiceIdentityRequest 23 | from ._assume_role_with_service_identity_response import AssumeRoleWithServiceIdentityResponse 24 | from ._assume_role_with_service_identity_response_body import AssumeRoleWithServiceIdentityResponseBody 25 | from ._assume_role_with_service_identity_response_body_assumed_role_user import ( 26 | AssumeRoleWithServiceIdentityResponseBodyAssumedRoleUser, 27 | ) 28 | from ._assume_role_with_service_identity_response_body_credentials import ( 29 | AssumeRoleWithServiceIdentityResponseBodyCredentials, 30 | ) 31 | from ._generate_session_access_key_request import GenerateSessionAccessKeyRequest 32 | from ._generate_session_access_key_response import GenerateSessionAccessKeyResponse 33 | from ._generate_session_access_key_response_body import GenerateSessionAccessKeyResponseBody 34 | from ._generate_session_access_key_response_body_session_access_key import ( 35 | GenerateSessionAccessKeyResponseBodySessionAccessKey, 36 | ) 37 | from ._generate_token_by_ticket_request import GenerateTokenByTicketRequest 38 | from ._generate_token_by_ticket_response import GenerateTokenByTicketResponse 39 | from ._generate_token_by_ticket_response_body import GenerateTokenByTicketResponseBody 40 | from ._generate_token_by_ticket_response_body_assumed_role_user import GenerateTokenByTicketResponseBodyAssumedRoleUser 41 | from ._generate_token_by_ticket_response_body_credentials import GenerateTokenByTicketResponseBodyCredentials 42 | from ._get_caller_identity_response import GetCallerIdentityResponse 43 | from ._get_caller_identity_response_body import GetCallerIdentityResponseBody 44 | from ._get_federation_token_request import GetFederationTokenRequest 45 | from ._get_federation_token_response import GetFederationTokenResponse 46 | from ._get_federation_token_response_body import GetFederationTokenResponseBody 47 | from ._get_federation_token_response_body_credentials import GetFederationTokenResponseBodyCredentials 48 | from ._get_federation_token_response_body_federated_user import GetFederationTokenResponseBodyFederatedUser 49 | 50 | __all__ = [ 51 | AssumeRoleResponseBodyAssumedRoleUser, 52 | AssumeRoleResponseBodyCredentials, 53 | AssumeRoleWithOIDCResponseBodyAssumedRoleUser, 54 | AssumeRoleWithOIDCResponseBodyCredentials, 55 | AssumeRoleWithOIDCResponseBodyOIDCTokenInfo, 56 | AssumeRoleWithSAMLResponseBodyAssumedRoleUser, 57 | AssumeRoleWithSAMLResponseBodyCredentials, 58 | AssumeRoleWithSAMLResponseBodySAMLAssertionInfo, 59 | AssumeRoleWithServiceIdentityResponseBodyAssumedRoleUser, 60 | AssumeRoleWithServiceIdentityResponseBodyCredentials, 61 | GenerateSessionAccessKeyResponseBodySessionAccessKey, 62 | GenerateTokenByTicketResponseBodyAssumedRoleUser, 63 | GenerateTokenByTicketResponseBodyCredentials, 64 | GetFederationTokenResponseBodyCredentials, 65 | GetFederationTokenResponseBodyFederatedUser, 66 | AssumeRoleRequest, 67 | AssumeRoleResponseBody, 68 | AssumeRoleResponse, 69 | AssumeRoleWithOIDCRequest, 70 | AssumeRoleWithOIDCResponseBody, 71 | AssumeRoleWithOIDCResponse, 72 | AssumeRoleWithSAMLRequest, 73 | AssumeRoleWithSAMLResponseBody, 74 | AssumeRoleWithSAMLResponse, 75 | AssumeRoleWithServiceIdentityRequest, 76 | AssumeRoleWithServiceIdentityResponseBody, 77 | AssumeRoleWithServiceIdentityResponse, 78 | GenerateSessionAccessKeyRequest, 79 | GenerateSessionAccessKeyResponseBody, 80 | GenerateSessionAccessKeyResponse, 81 | GenerateTokenByTicketRequest, 82 | GenerateTokenByTicketResponseBody, 83 | GenerateTokenByTicketResponse, 84 | GetCallerIdentityResponseBody, 85 | GetCallerIdentityResponse, 86 | GetFederationTokenRequest, 87 | GetFederationTokenResponseBody, 88 | GetFederationTokenResponse, 89 | ] 90 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_with_oidcrequest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleWithOIDCRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | duration_seconds: int = None, 13 | oidcprovider_arn: str = None, 14 | oidctoken: str = None, 15 | policy: str = None, 16 | role_arn: str = None, 17 | role_session_name: str = None, 18 | ): 19 | # The validity period of the STS token. Unit: seconds. 20 | # 21 | # Default value: 3600. Minimum value: 900. Maximum value: the value of the `MaxSessionDuration` parameter. 22 | # 23 | # For more information about how to specify `MaxSessionDuration`, see [CreateRole](https://help.aliyun.com/document_detail/28710.html) or [UpdateRole](https://help.aliyun.com/document_detail/28712.html). 24 | self.duration_seconds = duration_seconds 25 | # The Alibaba Cloud Resource Name (ARN) of the OIDC IdP. 26 | # 27 | # You can view the ARN in the RAM console or by calling operations. 28 | # 29 | # * For more information about how to view the ARN in the RAM console, see [View the information about an OIDC IdP](https://help.aliyun.com/document_detail/327123.html). 30 | # * For more information about how to view the ARN by calling operations, see [GetOIDCProvider](https://help.aliyun.com/document_detail/327126.html) or [ListOIDCProviders](https://help.aliyun.com/document_detail/327127.html). 31 | self.oidcprovider_arn = oidcprovider_arn 32 | # The OIDC token that is issued by the external IdP. 33 | # 34 | # The OIDC token must be 4 to 20,000 characters in length. 35 | # 36 | # > You must enter the original OIDC token. You do not need to enter the Base64-encoded OIDC token. 37 | self.oidctoken = oidctoken 38 | # The policy that specifies the permissions of the returned STS token. You can use this parameter to grant the STS token fewer permissions than the permissions granted to the RAM role. 39 | # 40 | # * If you specify this parameter, the permissions of the returned STS token are the permissions that are included in the value of this parameter and owned by the RAM role. 41 | # * If you do not specify this parameter, the returned STS token has all the permissions of the RAM role. 42 | # 43 | # The value must be 1 to 2,048 characters in length. 44 | self.policy = policy 45 | # The ARN of the RAM role. 46 | # 47 | # You can view the ARN in the RAM console or by calling operations. 48 | # 49 | # * For more information about how to view the ARN in the RAM console, see [How do I view the ARN of the RAM role?](https://help.aliyun.com/document_detail/39744.html) 50 | # * For more information about how to view the ARN by calling operations, see [ListRoles](https://help.aliyun.com/document_detail/28713.html) or [GetRole](https://help.aliyun.com/document_detail/28711.html). 51 | self.role_arn = role_arn 52 | # The custom name of the role session. 53 | # 54 | # Set this parameter based on your business requirements. In most cases, this parameter is set to the identity of the user who calls the operation, for example, the username. In ActionTrail logs, you can distinguish the users who assume the same RAM role to perform operations based on the value of the RoleSessionName parameter. This way, you can perform user-specific auditing. 55 | # 56 | # The value can contain letters, digits, periods (.), at signs (@), hyphens (-), and underscores (_). 57 | # 58 | # The value must be 2 to 64 characters in length. 59 | self.role_session_name = role_session_name 60 | 61 | def validate(self): 62 | pass 63 | 64 | def to_map(self): 65 | _map = super().to_map() 66 | if _map is not None: 67 | return _map 68 | 69 | result = dict() 70 | if self.duration_seconds is not None: 71 | result["DurationSeconds"] = self.duration_seconds 72 | if self.oidcprovider_arn is not None: 73 | result["OIDCProviderArn"] = self.oidcprovider_arn 74 | if self.oidctoken is not None: 75 | result["OIDCToken"] = self.oidctoken 76 | if self.policy is not None: 77 | result["Policy"] = self.policy 78 | if self.role_arn is not None: 79 | result["RoleArn"] = self.role_arn 80 | if self.role_session_name is not None: 81 | result["RoleSessionName"] = self.role_session_name 82 | return result 83 | 84 | def from_map(self, m: dict = None): 85 | m = m or dict() 86 | if m.get("DurationSeconds") is not None: 87 | self.duration_seconds = m.get("DurationSeconds") 88 | if m.get("OIDCProviderArn") is not None: 89 | self.oidcprovider_arn = m.get("OIDCProviderArn") 90 | if m.get("OIDCToken") is not None: 91 | self.oidctoken = m.get("OIDCToken") 92 | if m.get("Policy") is not None: 93 | self.policy = m.get("Policy") 94 | if m.get("RoleArn") is not None: 95 | self.role_arn = m.get("RoleArn") 96 | if m.get("RoleSessionName") is not None: 97 | self.role_session_name = m.get("RoleSessionName") 98 | return self 99 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/api_error.py: -------------------------------------------------------------------------------- 1 | TEQ_EXCEPTION_ERROR = [ 2 | { 3 | "httpStatusCode": 400, 4 | "errorCode": "RequestTimeExpired", 5 | "errorMessage": "Request time _requestTime_ has been expired while server time is _server time_.", 6 | "description": "请求时间和服务端时间差别超过15分钟。", 7 | "solution": "请您检查请求端时间,稍后重试。", 8 | }, 9 | { 10 | "httpStatusCode": 400, 11 | "errorCode": "ProjectAlreadyExist", 12 | "errorMessage": "Project _ProjectName_ already exist.", 13 | "description": "Project名称已存在。Project名称在阿里云地域内全局唯一。", 14 | "solution": "请您更换Project名称后重试。", 15 | }, 16 | { 17 | "httpStatusCode": 401, 18 | "errorCode": "SignatureNotMatch", 19 | "errorMessage": "Signature _signature_ not matched.", 20 | "description": "请求的数字签名不匹配。", 21 | "solution": "请您重试或更换AccessKey后重试。", 22 | }, 23 | { 24 | "httpStatusCode": 401, 25 | "errorCode": "Unauthorized", 26 | "errorMessage": "The AccessKeyId is unauthorized.", 27 | "description": "提供的AccessKey ID值未授权。", 28 | "solution": "请确认您的AccessKey ID有访问日志服务权限。", 29 | }, 30 | { 31 | "httpStatusCode": 401, 32 | "errorCode": "Unauthorized", 33 | "errorMessage": "The security token you provided is invalid.", 34 | "description": "STS Token不合法。", 35 | "solution": "请检查您的STS接口请求,确认STS Token是合法有效的。", 36 | }, 37 | { 38 | "httpStatusCode": 401, 39 | "errorCode": "Unauthorized", 40 | "errorMessage": "The security token you provided has expired.", 41 | "description": "STS Token已经过期。", 42 | "solution": "请重新申请STS Token后发起请求。", 43 | }, 44 | { 45 | "httpStatusCode": 401, 46 | "errorCode": "Unauthorized", 47 | "errorMessage": "AccessKeyId not found: _AccessKey ID_", 48 | "description": "AccessKey ID不存在。", 49 | "solution": "请检查您的AccessKey ID,重新获取后再发起请求。", 50 | }, 51 | { 52 | "httpStatusCode": 401, 53 | "errorCode": "Unauthorized", 54 | "errorMessage": "AccessKeyId is disabled: _AccessKey ID_", 55 | "description": "AccessKey ID是禁用状态。", 56 | "solution": "请检查您的AccessKey ID,确认为已启用状态后重新发起请求。", 57 | }, 58 | { 59 | "httpStatusCode": 401, 60 | "errorCode": "Unauthorized", 61 | "errorMessage": "Your SLS service has been forbidden.", 62 | "description": "日志服务已经被禁用。", 63 | "solution": "请检查您的日志服务状态,例如是否已欠费。", 64 | }, 65 | { 66 | "httpStatusCode": 401, 67 | "errorCode": "Unauthorized", 68 | "errorMessage": "The project does not belong to you.", 69 | "description": "Project不属于当前访问用户。", 70 | "solution": "请更换Project或者访问用户后重试。", 71 | }, 72 | { 73 | "httpStatusCode": 401, 74 | "errorCode": "InvalidAccessKeyId", 75 | "errorMessage": "The access key id you provided is invalid: _AccessKey ID_.", 76 | "description": "AccessKey ID不合法。", 77 | "solution": "请检查您的AccessKey ID,确认AccessKey ID是合法有效的。", 78 | }, 79 | { 80 | "httpStatusCode": 401, 81 | "errorCode": "InvalidAccessKeyId", 82 | "errorMessage": "Your SLS service has not opened.", 83 | "description": "日志服务没有开通。", 84 | "solution": "请登录日志服务控制台或者通过API开通日志服务后,重新发起请求。", 85 | }, 86 | { 87 | "httpStatusCode": 403, 88 | "errorCode": "WriteQuotaExceed", 89 | "errorMessage": "Write quota is exceeded.", 90 | "description": "超过写入日志限额。", 91 | "solution": "请您优化写入日志请求,减少写入日志数量。", 92 | }, 93 | { 94 | "httpStatusCode": 403, 95 | "errorCode": "ReadQuotaExceed", 96 | "errorMessage": "Read quota is exceeded.", 97 | "description": "超过读取日志限额。", 98 | "solution": "请您优化读取日志请求,减少读取日志数量。", 99 | }, 100 | { 101 | "httpStatusCode": 403, 102 | "errorCode": "MetaOperationQpsLimitExceeded", 103 | "errorMessage": "Qps limit for the meta operation is exceeded.", 104 | "description": "超出默认设置的QPS阈值。", 105 | "solution": "请您优化资源操作请求,减少资源操作次数。建议您延迟几秒后重试。", 106 | }, 107 | { 108 | "httpStatusCode": 403, 109 | "errorCode": "ProjectForbidden", 110 | "errorMessage": "Project _ProjectName_ has been forbidden.", 111 | "description": "Project已经被禁用。", 112 | "solution": "请检查Project状态,您的Project当前可能已经欠费。", 113 | }, 114 | { 115 | "httpStatusCode": 404, 116 | "errorCode": "ProjectNotExist", 117 | "errorMessage": "The Project does not exist : _name_", 118 | "description": "日志项目(Project)不存在。", 119 | "solution": "请您检查Project名称,确认已存在该Project或者地域是否正确。", 120 | }, 121 | { 122 | "httpStatusCode": 413, 123 | "errorCode": "PostBodyTooLarge", 124 | "errorMessage": "Body size _bodysize_ must little than 10485760.", 125 | "description": "请求消息体body不能超过10M。", 126 | "solution": "请您调整请求消息体的大小后重试。", 127 | }, 128 | { 129 | "httpStatusCode": 500, 130 | "errorCode": "InternalServerError", 131 | "errorMessage": "Internal server error message.", 132 | "description": "服务器内部错误。", 133 | "solution": "请您稍后重试。", 134 | }, 135 | { 136 | "httpStatusCode": 500, 137 | "errorCode": "RequestTimeout", 138 | "errorMessage": "The request is timeout. Please try again later.", 139 | "description": "请求处理超时。", 140 | "solution": "请您稍后重试。", 141 | }, 142 | ] 143 | -------------------------------------------------------------------------------- /libs/sts-20150401/alibabacloud_sts20150401/models/_assume_role_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is auto-generated, don't edit it. Thanks. 3 | from __future__ import annotations 4 | 5 | from darabonba.model import DaraModel 6 | 7 | 8 | class AssumeRoleRequest(DaraModel): 9 | def __init__( 10 | self, 11 | *, 12 | duration_seconds: int = None, 13 | external_id: str = None, 14 | policy: str = None, 15 | role_arn: str = None, 16 | role_session_name: str = None, 17 | ): 18 | # The validity period of the STS token. Unit: seconds. 19 | # 20 | # Minimum value: 900. Maximum value: the value of the `MaxSessionDuration` parameter. Default value: 3600. 21 | # 22 | # You can call the CreateRole or UpdateRole operation to configure the `MaxSessionDuration` parameter. For more information, see [CreateRole](https://help.aliyun.com/document_detail/28710.html) or [UpdateRole](https://help.aliyun.com/document_detail/28712.html). 23 | self.duration_seconds = duration_seconds 24 | # The external ID of the RAM role. 25 | # 26 | # This parameter is provided by an external party and is used to prevent the confused deputy problem. For more information, see [Use ExternalId to prevent the confused deputy problem](https://help.aliyun.com/document_detail/2361741.html). 27 | # 28 | # The value must be 2 to 1,224 characters in length and can contain letters, digits, and the following special characters: `= , . @ : / - _`. The regular expression for this parameter is `[\\w+=,.@:\\/-]*`. 29 | self.external_id = external_id 30 | # The policy that specifies the permissions of the returned STS token. You can use this parameter to grant the STS token fewer permissions than the permissions granted to the RAM role. 31 | # 32 | # * If you specify this parameter, the permissions of the returned STS token are the permissions that are included in the value of this parameter and owned by the RAM role. 33 | # * If you do not specify this parameter, the returned STS token has all the permissions of the RAM role. 34 | # 35 | # The value must be 1 to 2,048 characters in length. 36 | # 37 | # For more information about policy elements and sample policies, see [Policy elements](https://help.aliyun.com/document_detail/93738.html) and [Overview of sample policies](https://help.aliyun.com/document_detail/210969.html). 38 | self.policy = policy 39 | # The Alibaba Cloud Resource Name (ARN) of the RAM role. 40 | # 41 | # The trusted entity of the RAM role is an Alibaba Cloud account. For more information, see [Create a RAM role for a trusted Alibaba Cloud account](https://help.aliyun.com/document_detail/93691.html) or [CreateRole](https://help.aliyun.com/document_detail/28710.html). 42 | # 43 | # Format: `acs:ram:::role/`. 44 | # 45 | # You can view the ARN in the RAM console or by calling operations. The following items describe the validity periods of storage addresses: 46 | # 47 | # * For more information about how to view the ARN in the RAM console, see [How do I find the ARN of the RAM role?](https://help.aliyun.com/document_detail/39744.html) 48 | # * For more information about how to view the ARN by calling operations, see [ListRoles](https://help.aliyun.com/document_detail/28713.html) or [GetRole](https://help.aliyun.com/document_detail/28711.html). 49 | # 50 | # This parameter is required. 51 | self.role_arn = role_arn 52 | # The custom name of the role session. 53 | # 54 | # Set this parameter based on your business requirements. In most cases, you can set this parameter to the identity of the API caller. For example, you can specify a username. You can specify `RoleSessionName` to identify API callers that assume the same RAM role in ActionTrail logs. This allows you to track the users that perform the operations. 55 | # 56 | # The value must be 2 to 64 characters in length and can contain letters, digits, and the following special characters: `. @ - _`. 57 | # 58 | # This parameter is required. 59 | self.role_session_name = role_session_name 60 | 61 | def validate(self): 62 | pass 63 | 64 | def to_map(self): 65 | _map = super().to_map() 66 | if _map is not None: 67 | return _map 68 | 69 | result = dict() 70 | if self.duration_seconds is not None: 71 | result["DurationSeconds"] = self.duration_seconds 72 | if self.external_id is not None: 73 | result["ExternalId"] = self.external_id 74 | if self.policy is not None: 75 | result["Policy"] = self.policy 76 | if self.role_arn is not None: 77 | result["RoleArn"] = self.role_arn 78 | if self.role_session_name is not None: 79 | result["RoleSessionName"] = self.role_session_name 80 | return result 81 | 82 | def from_map(self, m: dict = None): 83 | m = m or dict() 84 | if m.get("DurationSeconds") is not None: 85 | self.duration_seconds = m.get("DurationSeconds") 86 | if m.get("ExternalId") is not None: 87 | self.external_id = m.get("ExternalId") 88 | if m.get("Policy") is not None: 89 | self.policy = m.get("Policy") 90 | if m.get("RoleArn") is not None: 91 | self.role_arn = m.get("RoleArn") 92 | if m.get("RoleSessionName") is not None: 93 | self.role_session_name = m.get("RoleSessionName") 94 | return self 95 | -------------------------------------------------------------------------------- /tests/mcp_server_aliyun_observability/toolkits/paas/test_paas_dataset_toolkit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import dotenv 6 | import pytest 7 | from mcp.server.fastmcp import Context, FastMCP 8 | from mcp.shared.context import RequestContext 9 | 10 | from mcp_server_aliyun_observability.server import create_lifespan 11 | from mcp_server_aliyun_observability.toolkits.paas.toolkit import register_paas_tools 12 | from mcp_server_aliyun_observability.toolkits.shared.toolkit import ( 13 | register_shared_tools, 14 | ) 15 | from mcp_server_aliyun_observability.utils import CMSClientWrapper, CredentialWrapper 16 | 17 | logger = logging.getLogger(__name__) 18 | dotenv.load_dotenv() 19 | 20 | 21 | def check_credentials_and_result(result): 22 | """检查凭证和结果,如果有凭证问题则跳过测试""" 23 | assert result is not None 24 | logger.info(f"测试结果: {result}") 25 | error = result.get("error") 26 | if error and "InvalidCredentials" in str(result.get("message", "")): 27 | pytest.skip("需要有效的阿里云凭证才能运行此测试") 28 | # 如果有error且不是凭证问题,则测试失败 29 | assert not error, f"测试结果: {result}" 30 | return result 31 | 32 | 33 | @pytest.fixture 34 | def mcp_server(): 35 | """创建模拟的FastMCP服务器实例用于数据集工具测试""" 36 | mcp_server = FastMCP( 37 | name="mcp_aliyun_observability_dataset_server", 38 | lifespan=create_lifespan(), 39 | ) 40 | # 注册PaaS工具包 41 | register_paas_tools(mcp_server) 42 | register_shared_tools(mcp_server) 43 | return mcp_server 44 | 45 | 46 | @pytest.fixture 47 | def mock_request_context(): 48 | """创建模拟的RequestContext实例""" 49 | context = Context( 50 | request_context=RequestContext( 51 | request_id="test_dataset_request_id", 52 | meta=None, 53 | session=None, 54 | lifespan_context={ 55 | "cms_client": CMSClientWrapper(), 56 | "sls_client": None, 57 | "arms_client": None, 58 | }, 59 | ) 60 | ) 61 | return context 62 | 63 | 64 | class TestPaaSDatasetToolkit: 65 | """PaaS数据集工具的测试类""" 66 | 67 | @pytest.mark.asyncio 68 | async def test_paas_list_data_set_success( 69 | self, 70 | mcp_server: FastMCP, 71 | mock_request_context: Context, 72 | ): 73 | """测试PaaS数据集列表查询""" 74 | tool = mcp_server._tool_manager.get_tool("umodel_list_data_set") 75 | result = await tool.run( 76 | { 77 | "domain": "apm", 78 | "entity_set_name": "apm.service", 79 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 80 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 81 | }, 82 | context=mock_request_context, 83 | ) 84 | result = check_credentials_and_result(result) 85 | 86 | @pytest.mark.asyncio 87 | async def test_list_workspace_success( 88 | self, 89 | mcp_server: FastMCP, 90 | mock_request_context: Context, 91 | ): 92 | """测试PaaS数据集列表查询""" 93 | tool = mcp_server._tool_manager.get_tool("list_workspace") 94 | result = await tool.run( 95 | { 96 | "regionId": "cn-qingdao", 97 | }, 98 | context=mock_request_context, 99 | ) 100 | result = check_credentials_and_result(result) 101 | 102 | @pytest.mark.asyncio 103 | async def test_paas_list_data_set_with_types( 104 | self, 105 | mcp_server: FastMCP, 106 | mock_request_context: Context, 107 | ): 108 | """测试PaaS数据集列表查询 - 指定类型""" 109 | tool = mcp_server._tool_manager.get_tool("umodel_list_data_set") 110 | result = await tool.run( 111 | { 112 | "domain": "apm", 113 | "entity_set_name": "apm.service", 114 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 115 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 116 | "data_set_types": "metric_set", 117 | }, 118 | context=mock_request_context, 119 | ) 120 | result = check_credentials_and_result(result) 121 | 122 | @pytest.mark.asyncio 123 | async def test_paas_search_entity_set_success( 124 | self, 125 | mcp_server: FastMCP, 126 | mock_request_context: Context, 127 | ): 128 | """测试PaaS实体集合搜索""" 129 | tool = mcp_server._tool_manager.get_tool("umodel_search_entity_set") 130 | result = await tool.run( 131 | { 132 | "search_text": "service", 133 | "domain": "apm", 134 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 135 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 136 | }, 137 | context=mock_request_context, 138 | ) 139 | result = check_credentials_and_result(result) 140 | 141 | @pytest.mark.asyncio 142 | async def test_paas_list_related_entity_set_success( 143 | self, 144 | mcp_server: FastMCP, 145 | mock_request_context: Context, 146 | ): 147 | """测试PaaS相关实体集合列表查询""" 148 | tool = mcp_server._tool_manager.get_tool("umodel_list_related_entity_set") 149 | result = await tool.run( 150 | { 151 | "domain": "apm", 152 | "entity_set_name": "apm.service", 153 | "workspace": os.getenv("TEST_CMS_WORKSPACE", "apm"), 154 | "regionId": os.getenv("TEST_REGION", "cn-hangzhou"), 155 | "direction": "both", 156 | }, 157 | context=mock_request_context, 158 | ) 159 | result = check_credentials_and_result(result) 160 | 161 | 162 | if __name__ == "__main__": 163 | pytest.main([__file__, "-v"]) 164 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import os 4 | from datetime import datetime 5 | from os import getenv 6 | from pathlib import Path 7 | from typing import Any, Literal, Optional, Union 8 | 9 | from rich.console import Console 10 | from rich.logging import RichHandler 11 | from rich.text import Text 12 | 13 | LOGGER_NAME = "mcp_server_aliyun_observability" 14 | 15 | # Define custom styles for log sources 16 | LOG_STYLES = { 17 | "server": { 18 | "debug": "green", 19 | "info": "blue", 20 | }, 21 | } 22 | 23 | 24 | class ColoredRichHandler(RichHandler): 25 | def __init__(self, *args, source_type: Optional[str] = None, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.source_type = source_type 28 | 29 | def get_level_text(self, record: logging.LogRecord) -> Text: 30 | # Return empty Text if message is empty 31 | if not record.msg: 32 | return Text("") 33 | 34 | level_name = record.levelname.lower() 35 | if self.source_type and self.source_type in LOG_STYLES: 36 | if level_name in LOG_STYLES[self.source_type]: 37 | color = LOG_STYLES[self.source_type][level_name] 38 | return Text(record.levelname, style=color) 39 | return super().get_level_text(record) 40 | 41 | 42 | class MCPLogger(logging.Logger): 43 | def __init__(self, name: str, level: int = logging.NOTSET): 44 | super().__init__(name, level) 45 | 46 | def debug(self, msg: str, center: bool = False, symbol: str = "*", *args, **kwargs): 47 | if center: 48 | msg = center_header(str(msg), symbol) 49 | super().debug(msg, *args, **kwargs) 50 | 51 | def info(self, msg: str, center: bool = False, symbol: str = "*", *args, **kwargs): 52 | if center: 53 | msg = center_header(str(msg), symbol) 54 | super().info(msg, *args, **kwargs) 55 | 56 | 57 | def setup_file_handler(logger_instance: logging.Logger) -> None: 58 | """设置文件处理器,将日志写入到指定文件""" 59 | # 创建日志目录 60 | log_dir = Path.home() / "mcp_server_aliyun_observability" 61 | log_dir.mkdir(exist_ok=True) 62 | 63 | # 生成日志文件名(包含日期) 64 | today = datetime.now().strftime("%Y%m%d") 65 | log_file = log_dir / f"mcp_server_{today}.log" 66 | 67 | # 创建文件处理器 68 | file_handler = logging.FileHandler(log_file, encoding='utf-8') 69 | file_handler.setLevel(logging.DEBUG) 70 | 71 | # 设置文件日志格式 72 | file_formatter = logging.Formatter( 73 | fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 74 | datefmt='%Y-%m-%d %H:%M:%S' 75 | ) 76 | file_handler.setFormatter(file_formatter) 77 | 78 | # 添加到logger 79 | logger_instance.addHandler(file_handler) 80 | 81 | 82 | def build_logger(logger_name: str, source_type: Optional[str] = None) -> Any: 83 | # Set the custom logger class as the default for this logger 84 | logging.setLoggerClass(MCPLogger) 85 | 86 | # Create logger with custom class 87 | _logger = logging.getLogger(logger_name) 88 | 89 | # Reset logger class to default to avoid affecting other loggers 90 | logging.setLoggerClass(logging.Logger) 91 | 92 | # 创建自定义控制台,设置合适的宽度 93 | try: 94 | import shutil 95 | 96 | terminal_width = shutil.get_terminal_size().columns 97 | # 确保最小宽度,避免过窄 98 | console_width = max(terminal_width, 120) 99 | except Exception: 100 | console_width = 120 # 默认宽度 101 | 102 | console = Console( 103 | width=console_width, 104 | force_terminal=True, 105 | no_color=False, 106 | legacy_windows=False, 107 | ) 108 | 109 | # https://rich.readthedocs.io/en/latest/reference/logging.html#rich.logging.RichHandler 110 | # https://rich.readthedocs.io/en/latest/logging.html#handle-exceptions 111 | rich_handler = ColoredRichHandler( 112 | show_time=True, 113 | rich_tracebacks=True, 114 | show_path=True if getenv("MCP_DEBUG") == "true" else False, 115 | tracebacks_show_locals=False, 116 | source_type=source_type or "server", 117 | console=console, # 使用自定义控制台 118 | omit_repeated_times=False, 119 | keywords=[], # 关键字高亮列表 120 | markup=False, # 禁用标记,避免格式冲突 121 | ) 122 | rich_handler.setFormatter( 123 | logging.Formatter( 124 | fmt="%(message)s", 125 | datefmt="[%X]", 126 | ) 127 | ) 128 | 129 | _logger.addHandler(rich_handler) 130 | 131 | # 添加文件处理器 132 | setup_file_handler(_logger) 133 | 134 | _logger.setLevel(logging.INFO) 135 | _logger.propagate = False 136 | return _logger 137 | 138 | 139 | # 创建统一的logger实例 140 | logger: MCPLogger = build_logger(LOGGER_NAME, source_type="server") 141 | 142 | debug_on: bool = False 143 | debug_level: Literal[1, 2] = 1 144 | 145 | 146 | def set_log_level_to_debug(source_type: Optional[str] = None, level: Literal[1, 2] = 1): 147 | """设置日志级别为DEBUG""" 148 | _logger = logging.getLogger(LOGGER_NAME if source_type is None else f"{LOGGER_NAME}-{source_type}") 149 | _logger.setLevel(logging.DEBUG) 150 | 151 | global debug_on 152 | debug_on = True 153 | 154 | global debug_level 155 | debug_level = level 156 | 157 | 158 | def set_log_level_to_info(source_type: Optional[str] = None): 159 | """设置日志级别为INFO""" 160 | _logger = logging.getLogger(LOGGER_NAME if source_type is None else f"{LOGGER_NAME}-{source_type}") 161 | _logger.setLevel(logging.INFO) 162 | 163 | global debug_on 164 | debug_on = False 165 | 166 | 167 | def center_header(message: str, symbol: str = "*") -> str: 168 | """将消息居中显示""" 169 | try: 170 | import shutil 171 | 172 | terminal_width = shutil.get_terminal_size().columns 173 | except Exception: 174 | terminal_width = 80 # fallback width 175 | 176 | header = f" {message} " 177 | return f"{header.center(terminal_width - 20, symbol)}" 178 | 179 | 180 | def log_debug(msg, center: bool = False, symbol: str = "*", log_level: Literal[1, 2] = 1, *args, **kwargs): 181 | """记录DEBUG级别日志""" 182 | global logger 183 | global debug_on 184 | global debug_level 185 | 186 | if debug_on: 187 | if debug_level >= log_level: 188 | logger.debug(msg, center, symbol, *args, **kwargs) 189 | 190 | 191 | def log_info(msg, center: bool = False, symbol: str = "*", *args, **kwargs): 192 | """记录INFO级别日志""" 193 | global logger 194 | logger.info(msg, center, symbol, *args, **kwargs) 195 | 196 | 197 | def log_warning(msg, *args, **kwargs): 198 | """记录WARNING级别日志""" 199 | global logger 200 | logger.warning(msg, *args, **kwargs) 201 | 202 | 203 | def log_error(msg, *args, **kwargs): 204 | """记录ERROR级别日志""" 205 | global logger 206 | logger.error(msg, *args, **kwargs) 207 | 208 | 209 | def log_exception(msg, *args, **kwargs): 210 | """记录异常日志""" 211 | global logger 212 | logger.exception(msg, *args, **kwargs) 213 | 214 | 215 | # 便捷函数:获取logger实例 216 | def get_logger() -> MCPLogger: 217 | """获取日志器实例""" 218 | return logger -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/core/utils.py: -------------------------------------------------------------------------------- 1 | """工具类通用函数""" 2 | 3 | import json 4 | from typing import Any, Dict, List, Optional 5 | 6 | from alibabacloud_sls20201230.client import Client 7 | from alibabacloud_sls20201230.models import CallAiToolsRequest, CallAiToolsResponse 8 | from alibabacloud_tea_util import models as util_models 9 | from mcp.server.fastmcp import Context 10 | from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed 11 | 12 | from mcp_server_aliyun_observability.config import Config 13 | from mcp_server_aliyun_observability.logger import logger 14 | 15 | 16 | def build_user_context_from_params(**kwargs) -> List[Dict[str, Any]]: 17 | """ 18 | 通用的用户上下文构建函数,从各种参数中提取实体信息 19 | 20 | 支持的参数(统一参数结构): 21 | - entity_domain: 实体域(如 apm, k8s, cloud_product) 22 | - entity_type: 实体类型(如 apm.service, k8s.pod) 23 | - entity_id: 实体ID(可选,精确指定实体) 24 | 25 | Args: 26 | **kwargs: 各种MCP工具的参数 27 | 28 | Returns: 29 | List[Dict[str, Any]]: 符合规范的user_context数组 30 | """ 31 | user_context = [] 32 | 33 | # 处理统一参数结构 entity_domain 和 entity_type 34 | if "entity_domain" in kwargs or "entity_type" in kwargs: 35 | entity_domain = kwargs.get("entity_domain", "") or "" 36 | entity_type = kwargs.get("entity_type", "") or "" 37 | 38 | # 只有在有domain或type时才添加entity上下文 39 | if entity_domain or entity_type: 40 | entity_context = { 41 | "type": "entity", 42 | "data": { 43 | "entity_type": entity_type, 44 | "entity_domain": entity_domain, 45 | }, 46 | } 47 | 48 | # 如果有entity_id,则添加到data中 49 | if "entity_id" in kwargs and kwargs["entity_id"]: 50 | entity_context["data"]["entity_id"] = kwargs["entity_id"] 51 | 52 | user_context.append(entity_context) 53 | 54 | # 兼容旧的 domain 参数(保留最小兼容性) 55 | elif "domain" in kwargs: 56 | entity_domain = kwargs.get("domain", "") or "" 57 | entity_type = kwargs.get("entity_type", "") or kwargs.get("type", "") or "" 58 | 59 | if entity_domain or entity_type: 60 | entity_context = { 61 | "type": "entity", 62 | "data": { 63 | "entity_type": entity_type, 64 | "entity_domain": entity_domain, 65 | }, 66 | } 67 | 68 | if "entity_id" in kwargs and kwargs["entity_id"]: 69 | entity_context["data"]["entity_id"] = kwargs["entity_id"] 70 | 71 | user_context.append(entity_context) 72 | user_context.append({"type": "metadata", "data": {"source": "mcp"}}) 73 | 74 | return user_context 75 | 76 | 77 | @retry( 78 | stop=stop_after_attempt(Config.get_retry_attempts()), 79 | wait=wait_fixed(Config.RETRY_WAIT_SECONDS), 80 | retry=retry_if_exception_type(Exception), 81 | reraise=True, 82 | ) 83 | def call_problem_agent( 84 | query: str, 85 | region_id: str, 86 | workspace: str, 87 | sls_client: Client, 88 | user_context: Optional[List[Dict[str, Any]]] = None, 89 | ) -> Any: 90 | """ 91 | 调用问题分析agent,使用call_ai_tools接口 92 | 93 | Args: 94 | query: 查询语句或问题描述 95 | region_id: 区域ID 96 | workspace: 工作空间(通常是logstore名称) 97 | sls_client: SLS客户端实例 98 | user_context: 用户上下文信息 99 | 100 | Returns: 101 | Any: AI工具返回的结果(通常是字典或其他结构化数据) 102 | 103 | Raises: 104 | Exception: 调用失败时抛出异常 105 | """ 106 | try: 107 | # 构建请求 108 | request = CallAiToolsRequest() 109 | request.tool_name = "_data_query" 110 | request.region_id = region_id 111 | 112 | # 构建参数 113 | params: Dict[str, Any] = { 114 | "query": query, 115 | "user_context": json.dumps(user_context) if user_context else None, 116 | "workspace": workspace, 117 | "regionId": region_id, 118 | } 119 | 120 | request.params = params 121 | 122 | # 设置运行时配置 123 | runtime = util_models.RuntimeOptions() 124 | read_timeout, connect_timeout = Config.get_timeouts() 125 | runtime.read_timeout = read_timeout 126 | runtime.connect_timeout = connect_timeout 127 | 128 | # 调用AI工具 129 | logger.info(f"调用AI工具 data_query,参数: {params}") 130 | tool_response: CallAiToolsResponse = sls_client.call_ai_tools_with_options( 131 | request=request, headers={}, runtime=runtime 132 | ) 133 | 134 | # 处理响应 135 | data = tool_response.body 136 | 137 | # 提取实际答案(如果有分隔符) 138 | if "------answer------\n" in data: 139 | data = data.split("------answer------\n")[1] 140 | 141 | logger.debug(f"AI工具 data_query 返回结果长度: {len(data)}") 142 | return data 143 | 144 | except Exception as e: 145 | logger.error(f"调用AI工具 data_query 失败: {str(e)}") 146 | raise 147 | 148 | 149 | def extract_answer_from_ai_response(response: str) -> str: 150 | """ 151 | 从AI响应中提取答案部分 152 | 153 | Args: 154 | response: AI工具的原始响应 155 | 156 | Returns: 157 | str: 提取后的答案 158 | """ 159 | if "------answer------\n" in response: 160 | return response.split("------answer------\n")[1] 161 | return response 162 | 163 | 164 | def build_ai_tool_params( 165 | project: str, logstore: str, query: str, region_id: str, **kwargs: Any 166 | ) -> Dict[str, Any]: 167 | """ 168 | 构建AI工具调用参数 169 | 170 | Args: 171 | project: SLS项目名称 172 | logstore: 日志库名称 173 | query: 查询语句 174 | region_id: 区域ID 175 | **kwargs: 其他额外参数 176 | 177 | Returns: 178 | Dict[str, Any]: 参数字典 179 | """ 180 | params = { 181 | "project": project, 182 | "logstore": logstore, 183 | "query": query, 184 | "region_id": region_id, 185 | } 186 | params.update(kwargs) 187 | return params 188 | 189 | 190 | def call_data_query( 191 | ctx: Context, 192 | query: str, 193 | region_id: str, 194 | workspace: str, 195 | domain: Optional[str] = None, 196 | entity_type: Optional[str] = None, 197 | entity_id: Optional[str] = None, 198 | start_time: Optional[str] = None, 199 | end_time: Optional[str] = None, 200 | output_mode: Optional[str] = None, 201 | user_context: Optional[List[Dict[str, Any]]] = None, 202 | error_message_prefix: str = "查询失败", 203 | client_type: str = "sls_client", 204 | **kwargs: Any, 205 | ) -> Dict[str, Any]: 206 | """ 207 | 调用数据查询的通用方法 208 | 209 | Args: 210 | ctx: MCP上下文 211 | query: 查询语句 212 | region_id: 区域ID 213 | workspace: 工作空间 214 | domain: 实体域(可选) 215 | entity_type: 实体类型(可选) 216 | entity_id: 实体ID(可选) 217 | start_time: 开始时间(可选) 218 | end_time: 结束时间(可选) 219 | output_mode: 输出模式(可选) 220 | user_context: 用户上下文参数(可选,如果不提供会自动构建) 221 | error_message_prefix: 错误消息前缀 222 | client_type: 客户端类型(sls_client, cms_client, arms_client) 223 | **kwargs: 其他参数 224 | 225 | Returns: 226 | 查询结果或错误信息 227 | """ 228 | try: 229 | # 获取客户端 230 | client_wrapper = ctx.request_context.lifespan_context.get(client_type) 231 | if not client_wrapper: 232 | return { 233 | "error": True, 234 | "message": f"{client_type} 未初始化", 235 | } 236 | 237 | # 如果没有提供user_context,则自动构建 238 | if user_context is None: 239 | # 构建参数字典 240 | params = { 241 | "entity_domain": domain, 242 | "entity_type": entity_type, 243 | "entity_id": entity_id, 244 | } 245 | # 合并kwargs中的其他参数 246 | params.update(kwargs) 247 | user_context = build_user_context_from_params(**params) 248 | 249 | # 调用AI工具 250 | result = call_problem_agent( 251 | query=query, 252 | region_id=region_id, 253 | workspace=workspace, 254 | sls_client=client_wrapper.with_region("cn-shanghai"), 255 | user_context=user_context or [], 256 | ) 257 | return { 258 | "error": False, 259 | "message": result, 260 | } 261 | 262 | except Exception as e: 263 | logger.error(f"{error_message_prefix}: {str(e)}") 264 | return { 265 | "error": True, 266 | "message": f"{error_message_prefix}: {str(e)}", 267 | } 268 | -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/shared/toolkit.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | from alibabacloud_cms20240330.client import Client as CmsClient 3 | from alibabacloud_cms20240330.models import (ListWorkspacesRequest, 4 | ListWorkspacesResponse, 5 | ListWorkspacesResponseBody) 6 | from mcp.server.fastmcp import Context, FastMCP 7 | from pydantic import Field 8 | from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed 9 | 10 | from mcp_server_aliyun_observability.utils import handle_tea_exception, execute_cms_query_with_context 11 | 12 | 13 | class SharedToolkit: 14 | """Shared Toolkit 15 | 16 | Provides common functionality used by both PaaS and DoAI layers, including: 17 | - Workspace management (list_workspace) 18 | - Entity discovery (list_domains) 19 | """ 20 | 21 | def __init__(self, server: FastMCP): 22 | """Initialize the shared toolkit 23 | 24 | Args: 25 | server: FastMCP server instance 26 | """ 27 | self.server = server 28 | self.register_tools() 29 | 30 | def register_tools(self): 31 | """Register all shared tools""" 32 | self._register_workspace_tools() 33 | self._register_discovery_tools() 34 | self._register_info_tools() 35 | 36 | def _register_workspace_tools(self): 37 | """Register workspace management tools""" 38 | 39 | @self.server.tool() 40 | @retry( 41 | wait=wait_fixed(1), 42 | stop=stop_after_attempt(3), 43 | retry=retry_if_exception_type(Exception), 44 | reraise=True, 45 | ) 46 | @handle_tea_exception 47 | def list_workspace( 48 | ctx: Context, 49 | regionId: str = Field(..., description="阿里云区域ID"), 50 | ) -> Dict[str, Any]: 51 | """列出可用的CMS工作空间 52 | 53 | ## 功能概述 54 | 获取指定区域内可用的Cloud Monitor Service (CMS)工作空间列表。 55 | 工作空间是CMS中用于组织和管理监控数据的逻辑容器。 56 | 57 | ## 参数说明 58 | - regionId: 阿里云区域标识符,如 "cn-hangzhou", "cn-beijing" 等 59 | 60 | ## 返回结果 61 | 返回包含工作空间信息的字典,包括: 62 | - workspaces: 工作空间列表,每个工作空间包含名称、ID、描述等信息 63 | - total_count: 工作空间总数 64 | - region: 查询的区域ID 65 | 66 | ## 使用场景 67 | - 在使用PaaS层API之前,需要先获取可用的工作空间 68 | - 为DoAI层查询提供工作空间选择 69 | - 管理和监控多个工作空间的资源使用情况 70 | 71 | ## 注意事项 72 | - 不同区域的工作空间是独立的 73 | - 工作空间的可见性取决于当前用户的权限 74 | - 这是一个基础工具,为其他PaaS和DoAI工具提供工作空间选择 75 | """ 76 | try: 77 | # 获取CMS客户端 78 | cms_client: CmsClient = ctx.request_context.lifespan_context.get("cms_client") 79 | if not cms_client: 80 | return { 81 | "error": True, 82 | "workspaces": [], 83 | "total_count": 0, 84 | "region": regionId, 85 | "message": "CMS客户端未初始化", 86 | } 87 | 88 | cms_client = cms_client.with_region(regionId) 89 | 90 | # 构建请求 - 获取所有工作空间 91 | request = ListWorkspacesRequest( 92 | max_results=100, 93 | next_token=None, 94 | region=regionId, 95 | workspace_name=None # 获取所有工作空间 96 | ) 97 | 98 | # 调用CMS API 99 | response: ListWorkspacesResponse = cms_client.list_workspaces(request) 100 | body: ListWorkspacesResponseBody = response.body 101 | 102 | # 处理响应 103 | workspaces = [] 104 | if body.workspaces: 105 | workspaces = [w.to_map() for w in body.workspaces] 106 | 107 | return { 108 | "error": False, 109 | "workspaces": workspaces, 110 | "total_count": body.total if body.total else len(workspaces), 111 | "region": regionId, 112 | "message": f"Successfully retrieved {len(workspaces)} workspaces from region {regionId}" 113 | } 114 | 115 | except Exception as e: 116 | return { 117 | "error": True, 118 | "workspaces": [], 119 | "total_count": 0, 120 | "region": regionId, 121 | "message": f"Failed to retrieve workspaces: {str(e)}" 122 | } 123 | 124 | def _register_discovery_tools(self): 125 | """Register discovery tools""" 126 | 127 | @self.server.tool() 128 | @retry( 129 | wait=wait_fixed(1), 130 | stop=stop_after_attempt(3), 131 | retry=retry_if_exception_type(Exception), 132 | reraise=True, 133 | ) 134 | @handle_tea_exception 135 | def list_domains( 136 | ctx: Context, 137 | workspace: str = Field(..., description="CMS工作空间名称,可通过list_workspace获取"), 138 | regionId: str = Field(..., description="阿里云区域ID"), 139 | ) -> Dict[str, Any]: 140 | """列出所有可用的实体域 141 | 142 | ## 功能概述 143 | 获取系统中所有可用的实体域(domain)列表。实体域是实体的最高级分类, 144 | 如 APM、容器、云产品等。这是发现系统支持实体类型的第一步。 145 | 146 | ## 使用场景 147 | - 了解系统支持的所有实体域 148 | - 为后续查询选择正确的domain参数 149 | - 构建动态的域选择界面 150 | 151 | ## 返回数据 152 | 每个域包含: 153 | - __domain__: 域名称(如 apm, k8s, cloud) 154 | - cnt: 该域下的实体总数量 155 | 156 | Args: 157 | ctx: MCP上下文 158 | workspace: CMS工作空间名称 159 | regionId: 阿里云区域ID 160 | 161 | Returns: 162 | 包含实体域列表的响应对象 163 | """ 164 | # 使用.entity查询来获取所有域的统计信息 165 | query = ".entity with(domain='*', type='*', topk=1000) | stats cnt=count(1) by __domain__ | project __domain__, cnt | sort cnt desc" 166 | return execute_cms_query_with_context(ctx, query, workspace, regionId, "now-24h", "now", 1000) 167 | 168 | def _register_info_tools(self): 169 | """Register information and help tools""" 170 | 171 | @self.server.tool() 172 | def introduction() -> Dict[str, Any]: 173 | """获取阿里云可观测性MCP Server的介绍和使用说明 174 | ## 功能概述 175 | 返回阿里云可观测性 MCP Server 的服务概述、核心能力和使用限制说明。 176 | 帮助用户快速了解服务能做什么,以及使用各层工具的前提条件。 177 | Observable MCP Server 是阿里云可观测官方推出的 MCP (Model Context Protocol) 服务, 178 | 提供统一的 AI 可观测数据访问能力。 179 | ## 使用场景 180 | - 首次接入时了解服务能力和限制 181 | - 了解不同工具层的使用前提 182 | 183 | ## 注意事项 184 | - 此工具不需要任何参数,可直接调用 185 | - 返回信息包含各层工具的使用前提条件 186 | """ 187 | return { 188 | "name": "Alibaba Cloud Observability MCP Server", 189 | "version": "1.0.0", 190 | "description": "阿里云可观测性 MCP 服务 - 提供 AI 驱动的可观测数据访问能力", 191 | "capabilities": { 192 | "data_access": [ 193 | "查询日志数据(SLS 日志库)", 194 | "查询指标数据(时序指标)", 195 | "查询链路数据(分布式追踪)", 196 | "查询事件数据(异常事件)", 197 | "查询实体信息(应用、容器、云产品等)", 198 | "性能剖析数据查询" 199 | ], 200 | "ai_features": [ 201 | "自然语言转 SQL 查询", 202 | "自然语言转 PromQL 查询", 203 | "智能实体发现和关系分析" 204 | ] 205 | }, 206 | "tool_layers": { 207 | "paas": { 208 | "description": "PaaS 层工具集(推荐)- 基于云监控 2.0 的现代化可观测能力", 209 | "capabilities": [ 210 | "实体发现和管理", 211 | "指标、日志、事件、链路、性能剖析的统一查询", 212 | "数据集和元数据管理" 213 | ], 214 | "prerequisites": "⚠️ 需要开通阿里云监控 2.0 服务", 215 | "note": "适用于需要统一数据模型和实体关系分析的场景" 216 | }, 217 | "iaas": { 218 | "description": "IaaS 层工具集 - 直接访问底层存储服务", 219 | "capabilities": [ 220 | "直接查询 SLS 日志库(Log Store)", 221 | "直接查询 SLS 指标库(Metric Store)", 222 | "执行原生 SQL/PromQL 查询", 223 | "日志库和项目管理" 224 | ], 225 | "prerequisites": "✓ 无需云监控 2.0,仅需 SLS 服务权限", 226 | "note": "适用于直接访问 SLS 数据或不依赖云监控 2.0 的场景" 227 | }, 228 | "shared": { 229 | "description": "共享工具集 - 基础服务发现和管理", 230 | "capabilities": [ 231 | "工作空间管理", 232 | "实体域发现", 233 | "服务介绍" 234 | ], 235 | "prerequisites": "✓ 所有场景可用" 236 | } 237 | }, 238 | "important_notes": [ 239 | "PaaS 层工具(umodel_* 系列)依赖云监控 2.0,需要先开通服务", 240 | "IaaS 层工具(sls_* 系列)直接访问 SLS,无需云监控 2.0", 241 | "建议优先使用 PaaS 层工具以获得更好的实体关系和统一数据模型体验", 242 | "如果未开通云监控 2.0,可使用 IaaS 层工具直接查询 SLS 数据" 243 | ], 244 | "references": { 245 | "cloudmonitor_2_0": "https://help.aliyun.com/zh/cms/cloudmonitor-2-0/product-overview/what-is-cloud-monitor-2-0", 246 | "sls_overview": "https://help.aliyun.com/zh/sls/?spm=5176.29508878.J_AHgvE-XDhTWrtotIBlDQQ.8.79815c7ffN3uWE", 247 | "github": "https://github.com/aliyun/alibabacloud-observability-mcp-server" 248 | } 249 | } 250 | 251 | 252 | def register_shared_tools(server: FastMCP): 253 | """Register shared toolkit tools with the FastMCP server 254 | 255 | Args: 256 | server: FastMCP server instance 257 | """ 258 | SharedToolkit(server) -------------------------------------------------------------------------------- /src/mcp_server_aliyun_observability/toolkits/paas/dataset_toolkit.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from mcp.server.fastmcp import Context, FastMCP 4 | from pydantic import Field 5 | from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed 6 | 7 | from mcp_server_aliyun_observability.config import Config 8 | from mcp_server_aliyun_observability.utils import ( 9 | execute_cms_query_with_context, 10 | handle_tea_exception, 11 | ) 12 | 13 | 14 | class PaaSDatasetToolkit: 15 | """PaaS Dataset Management Toolkit 16 | 17 | Provides structured dataset query tools ported from umodel metadata handlers. 18 | """ 19 | 20 | def __init__(self, server: FastMCP): 21 | self.server = server 22 | self._register_tools() 23 | 24 | def _register_tools(self): 25 | """Register metadata-related PaaS tools""" 26 | 27 | @self.server.tool() 28 | @retry( 29 | stop=stop_after_attempt(Config.get_retry_attempts()), 30 | wait=wait_fixed(Config.RETRY_WAIT_SECONDS), 31 | retry=retry_if_exception_type(Exception), 32 | reraise=True, 33 | ) 34 | @handle_tea_exception 35 | def umodel_list_data_set( 36 | ctx: Context, 37 | workspace: str = Field( 38 | ..., description="CMS工作空间名称,可通过list_workspace获取" 39 | ), 40 | domain: str = Field(..., description="实体域, cannot be '*'"), 41 | entity_set_name: str = Field(..., description="实体类型, cannot be '*'"), 42 | data_set_types: Optional[str] = Field( 43 | None, description="Comma-separated data set types" 44 | ), 45 | from_time: Union[str, int] = Field( 46 | "now-5m", description="开始时间: Unix时间戳(秒/毫秒)或相对时间(now-5m)" 47 | ), 48 | to_time: Union[str, int] = Field( 49 | "now", description="结束时间: Unix时间戳(秒/毫秒)或相对时间(now)" 50 | ), 51 | regionId: str = Field(..., description="Region ID"), 52 | ) -> Dict[str, Any]: 53 | """列出指定实体的可用数据集合,为其他PaaS工具提供参数选项。 54 | 55 | ## 功能概述 56 | 57 | 该工具是一个元数据查询接口,用于获取指定实体域和类型下可用的数据集合信息。 58 | 主要作用是为其他PaaS层工具(如observability、entity等工具)提供可选的参数列表, 59 | 包括指标集合、日志集合、事件集合等存储信息。 60 | 61 | ## 使用场景 62 | 63 | - **参数发现**: 为 umodel_get_metrics 提供可用的指标集合(metric_set)列表 64 | - **日志源查询**: 为 umodel_get_logs 提供可用的日志集合(log_set)列表 65 | - **事件源发现**: 为 umodel_get_events 提供可用的事件集合(event_set)列表 66 | - **追踪数据源**: 为 umodel_get_traces 提供可用的追踪集合(trace_set)列表 67 | - **实体关联**: 为实体查询工具提供关联的数据集合信息 68 | 69 | ## 参数说明 70 | 71 | - data_set_types: 数据集合类型过滤器,常见类型包括: 72 | * 'metric_set': 指标集合,用于获取可查询的指标名称列表 73 | * 'log_set': 日志集合,用于获取可查询的日志数据源 74 | * 'event_set': 事件集合,用于获取可查询的事件数据源 75 | * 'trace_set': 追踪集合,用于获取可查询的追踪数据源 76 | * 'entity_set_name': 实体集合,用于获取实体关联的数据集合 77 | 78 | ## 工具依赖关系 79 | 80 | 这个工具的输出为其他PaaS工具提供参数选项: 81 | - umodel_get_metrics → 使用返回的 metric_set 信息选择指标 82 | - umodel_get_logs → 使用返回的 log_set 信息选择日志源 83 | - umodel_get_events → 使用返回的 event_set 信息选择事件源 84 | - umodel_get_traces → 使用返回的 trace_set 信息选择追踪源 85 | 86 | ## 示例用法 87 | 88 | ``` 89 | # 获取服务实体的所有数据集合类型 90 | umodel_list_data_set(domain="apm", entity_set_name="apm.service") 91 | 92 | # 仅获取指标集合,用于后续指标查询 93 | umodel_list_data_set( 94 | domain="apm", 95 | entity_set_name="apm.service", 96 | data_set_types="metric_set" 97 | ) 98 | 99 | # 获取日志和事件集合,用于故障分析 100 | umodel_list_data_set( 101 | domain="apm", 102 | entity_set_name="apm.service", 103 | data_set_types="log_set,event_set" 104 | ) 105 | ``` 106 | 107 | Args: 108 | ctx: MCP上下文,用于访问CMS客户端 109 | workspace: CMS工作空间名称 110 | domain: 实体域,不能为通配符 '*' 111 | entity_set_name: 实体类型,不能为通配符 '*' 112 | data_set_types: 数据集合类型过滤器,逗号分隔的类型列表 113 | from_time: 查询开始时间 114 | to_time: 查询结束时间 115 | regionId: 阿里云区域ID 116 | 117 | Returns: 118 | 包含指定实体的可用数据集合列表,每个数据集合包含名称、类型、存储信息等元数据 119 | """ 120 | types_param = "" 121 | if data_set_types: 122 | types_list = [f"'{t.strip()}'" for t in data_set_types.split(",")] 123 | types_param = f"[{','.join(types_list)}]" 124 | else: 125 | types_param = "[]" 126 | query = f".entity_set with(domain='{domain}', name='{entity_set_name}') | entity-call list_data_set({types_param})" 127 | return execute_cms_query_with_context( 128 | ctx, query, workspace, regionId, from_time, to_time, 1000 129 | ) 130 | 131 | @self.server.tool() 132 | @retry( 133 | stop=stop_after_attempt(Config.get_retry_attempts()), 134 | wait=wait_fixed(Config.RETRY_WAIT_SECONDS), 135 | retry=retry_if_exception_type(Exception), 136 | reraise=True, 137 | ) 138 | @handle_tea_exception 139 | def umodel_search_entity_set( 140 | ctx: Context, 141 | search_text: str = Field(..., description="搜索关键词,用于全文搜索"), 142 | workspace: str = Field( 143 | ..., description="CMS工作空间名称,可通过list_workspace获取" 144 | ), 145 | domain: Optional[str] = Field(None, description="可选的实体域过滤"), 146 | entity_set_name: Optional[str] = Field( 147 | None, description="可选的实体类型过滤" 148 | ), 149 | limit: int = Field( 150 | 10, description="返回多少个实体集合,默认10个", ge=1, le=100 151 | ), 152 | regionId: str = Field(..., description="Region ID"), 153 | ) -> Dict[str, Any]: 154 | """搜索实体集合,支持全文搜索并按相关度排序。 155 | 156 | ## 功能概述 157 | 158 | 该工具用于在UModel元数据中搜索实体集合定义,支持按关键词进行全文搜索。 159 | 主要用于发现可用的实体集合类型和它们的元数据信息。 160 | 161 | ## 功能特点 162 | 163 | - **全文搜索**: 支持在实体集合的元数据和规格中进行全文搜索 164 | - **相关度排序**: 搜索结果按相关度进行排序 165 | - **元数据查询**: 返回实体集合的domain、name、display_name等元数据信息 166 | - **可选过滤**: 支持按domain和name进行额外过滤 167 | 168 | ## 使用场景 169 | 170 | - **实体集合发现**: 搜索包含特定关键词的实体集合类型 171 | - **元数据探索**: 了解系统中可用的实体集合及其描述信息 172 | - **工具链集成**: 为其他工具提供实体集合的发现能力 173 | 174 | Args: 175 | ctx: MCP上下文,用于访问CMS客户端 176 | search_text: 搜索关键词 177 | workspace: CMS工作空间名称 178 | domain: 可选的域过滤 179 | entity_set_name: 可选的实体类型过滤 180 | limit: 返回结果数量限制 181 | regionId: 阿里云区域ID 182 | 183 | Returns: 184 | 包含搜索到的实体集合信息的响应对象 185 | """ 186 | # 基于Go实现构建SPL查询 187 | query = ".umodel | where kind = 'entity_set' and __type__ = 'node'" 188 | 189 | if domain: 190 | query += ( 191 | f" | where json_extract_scalar(metadata, '$.domain') = '{domain}'" 192 | ) 193 | if entity_set_name: 194 | query += f" | where json_extract_scalar(metadata, '$.name') = '{entity_set_name}'" 195 | # 添加全文搜索过滤 196 | query += f" | where strpos(metadata, '{search_text}') > 0 or strpos(spec, '{search_text}') > 0" 197 | 198 | # 只返回name列表,简化输出 199 | query += " | extend name = json_extract_scalar(metadata, '$.name') | project name | limit 100" 200 | 201 | result = execute_cms_query_with_context( 202 | ctx, query, workspace, regionId, "now-1h", "now", limit 203 | ) 204 | return result 205 | 206 | @self.server.tool() 207 | @retry( 208 | stop=stop_after_attempt(Config.get_retry_attempts()), 209 | wait=wait_fixed(Config.RETRY_WAIT_SECONDS), 210 | retry=retry_if_exception_type(Exception), 211 | reraise=True, 212 | ) 213 | @handle_tea_exception 214 | def umodel_list_related_entity_set( 215 | ctx: Context, 216 | domain: str = Field(..., description="实体域,如'apm'"), 217 | entity_set_name: str = Field(..., description="实体类型,如'apm.service'"), 218 | workspace: str = Field( 219 | ..., description="CMS工作空间名称,可通过list_workspace获取" 220 | ), 221 | relation_type: Optional[str] = Field( 222 | None, description="关系类型过滤,如'calls'" 223 | ), 224 | direction: str = Field( 225 | "both", description="关系方向: 'in', 'out', 或 'both'" 226 | ), 227 | detail: bool = Field(False, description="是否返回详细信息"), 228 | regionId: str = Field(..., description="Region ID"), 229 | ) -> Dict[str, Any]: 230 | """列出与指定实体集合相关的其他实体集合。 231 | 232 | ## 功能概述 233 | 234 | 该工具用于发现与指定实体集合存在关系定义的其他实体集合类型。 235 | 这是一个元数据级别的工具,用于探索UModel拓扑的高级蓝图。 236 | 237 | ## 功能特点 238 | 239 | - **关系发现**: 查找与源实体集合有关系定义的其他实体集合 240 | - **方向控制**: 支持查看入向、出向或双向关系 241 | - **类型过滤**: 可按特定关系类型进行过滤 242 | - **元数据级别**: 显示可能的关系,不保证实际实体间存在连接 243 | 244 | ## 使用场景 245 | 246 | - **拓扑探索**: 了解实体集合间可能存在的关系类型 247 | - **依赖分析**: 发现服务可以调用的其他实体类型 248 | - **关系建模**: 为关系查询工具提供参数信息 249 | 250 | Args: 251 | ctx: MCP上下文,用于访问CMS客户端 252 | domain: 源实体集合的域 253 | entity_set_name: 源实体集合的名称 254 | workspace: CMS工作空间名称 255 | relation_type: 可选的关系类型过滤 256 | direction: 关系方向 257 | detail: 是否返回详细信息 258 | regionId: 阿里云区域ID 259 | 260 | Returns: 261 | 包含相关实体集合信息的响应对象 262 | """ 263 | # 构建参数 264 | relation_type_param = f"'{relation_type}'" if relation_type else "''" 265 | direction_param = f"'{direction}'" 266 | detail_param = "true" if detail else "false" 267 | 268 | # 基于Go实现构建查询 269 | query = f".entity_set with(domain='{domain}', name='{entity_set_name}') | entity-call list_related_entity_set({relation_type_param}, {direction_param}, {detail_param})" 270 | 271 | return execute_cms_query_with_context( 272 | ctx, query, workspace, regionId, "now-1h", "now", 1000 273 | ) 274 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | ## Alibaba Cloud Observability MCP Server 2 | 3 | ### Introduction 4 | 5 | Alibaba Cloud Observability MCP Server provides a set of tools for accessing various products in Alibaba Cloud's observability suite. It covers products including Alibaba Cloud Log Service (SLS), Alibaba Cloud Application Real-Time Monitoring Service (ARMS), and Alibaba Cloud CloudMonitor. Any intelligent agent that supports the MCP protocol can quickly integrate with it. Supported products include: 6 | 7 | - [Alibaba Cloud Log Service (SLS)](https://www.alibabacloud.com/help/en/sls/product-overview/what-is-log-service) 8 | - [Alibaba Cloud Application Real-Time Monitoring Service (ARMS)](https://www.alibabacloud.com/help/en/arms) 9 | 10 | ### Tool Architecture 11 | The project adopts a modular architecture with two main toolsets: 12 | 13 | - **CMS Toolkit** (Observability 2.0, Recommended): Contains 8 modern tool modules including entities, metrics, traces, events, topologies, diagnosis, drilldown, and workspace 14 | - **IaaS Toolkit** (V1 Compatibility): Legacy SLS, ARMS, CMS tools that maintain backward compatibility 15 | 16 | The system will automatically register available toolkits based on installed optional dependencies. 17 | 18 | ### Version History 19 | You can check the [CHANGELOG.md](./CHANGELOG.md) 20 | 21 | ### FAQ 22 | You can check the [FAQ.md](./FAQ.md) 23 | 24 | ##### Example Scenarios 25 | 26 | - Scenario 1: Quickly query the structure of a specific logstore 27 | - Tools used: 28 | - `sls_list_logstores` 29 | - `sls_describe_logstore` 30 | ![image](./images/search_log_store.png) 31 | 32 | 33 | - Scenario 2: Fuzzy query to find the application with the highest traffic in a logstore over the past day 34 | - Analysis: 35 | - Need to verify if the logstore exists 36 | - Get the logstore structure 37 | - Generate query statements based on requirements (users can confirm and modify statements) 38 | - Execute query statements 39 | - Generate responses based on query results 40 | - Tools used: 41 | - `sls_list_logstores` 42 | - `sls_describe_logstore` 43 | - `sls_translate_natural_language_to_query` 44 | - `sls_execute_query` 45 | ![image](./images/fuzzy_search_and_get_logs.png) 46 | 47 | 48 | - Scenario 3: Query the slowest traces in an ARMS application 49 | - Analysis: 50 | - Need to verify if the application exists 51 | - Get the application structure 52 | - Generate query statements based on requirements (users can confirm and modify statements) 53 | - Execute query statements 54 | - Generate responses based on query results 55 | - Tools used: 56 | - `arms_search_apps` 57 | - `arms_generate_trace_query` 58 | - `sls_translate_natural_language_to_query` 59 | - `sls_execute_query` 60 | ![image](./images/find_slowest_trace.png) 61 | 62 | 63 | ### Permission Requirements 64 | 65 | To ensure MCP Server can successfully access and operate your Alibaba Cloud observability resources, you need to configure the following permissions: 66 | 67 | 1. **Alibaba Cloud Access Key (AccessKey)**: 68 | * Service requires a valid Alibaba Cloud AccessKey ID and AccessKey Secret. 69 | * To obtain and manage AccessKeys, refer to the [Alibaba Cloud AccessKey Management official documentation](https://www.alibabacloud.com/help/en/basics-for-beginners/latest/obtain-an-accesskey-pair). 70 | 71 | 2. When you initialize without providing AccessKey and AccessKey Secret, it will use the [Default Credential Chain for login](https://www.alibabacloud.com/help/en/sdk/developer-reference/v2-manage-python-access-credentials#62bf90d04dztq) 72 | 1. If environment variables ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET exist and are non-empty, they will be used as default credentials. 73 | 2. If ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET, and ALIBABA_CLOUD_SECURITY_TOKEN are all set, STS Token will be used as default credentials. 74 | 75 | 3. **RAM Authorization (Important)**: 76 | * The RAM user or role associated with the AccessKey **must** be granted the necessary permissions to access the relevant cloud services. 77 | * **Strongly recommended to follow the "Principle of Least Privilege"**: Only grant the minimum set of permissions necessary to run the MCP tools you plan to use, to reduce security risks. 78 | * Based on the tools you need to use, refer to the following documentation for permission configuration: 79 | * **Log Service (SLS)**: If you need to use `sls_*` related tools, refer to [Log Service Permissions](https://www.alibabacloud.com/help/en/sls/user-guide/overview-2) and grant necessary read, query, and other permissions. 80 | * **Application Real-Time Monitoring Service (ARMS)**: If you need to use `arms_*` related tools, refer to [ARMS Permissions](https://www.alibabacloud.com/help/en/arms/user-guide/manage-ram-permissions) and grant necessary query permissions. 81 | * Please configure the required permissions in detail according to your actual application scenario. 82 | 83 | ### Security and Deployment Recommendations 84 | 85 | Please pay attention to the following security issues and deployment best practices: 86 | 87 | 1. **Key Security**: 88 | * This MCP Server will use the AccessKey you provide to call Alibaba Cloud OpenAPI when running, but **will not store your AccessKey in any form**, nor will it be used for any other purpose beyond the designed functionality. 89 | 90 | 2. **Access Control (Critical)**: 91 | * When you choose to access the MCP Server through the **SSE (Server-Sent Events) protocol**, **you must take responsibility for access control and security protection of the service access point**. 92 | * **Strongly recommended** to deploy the MCP Server in an **internal network or trusted environment**, such as your private VPC (Virtual Private Cloud), avoiding direct exposure to the public internet. 93 | * The recommended deployment method is to use **Alibaba Cloud Function Compute (FC)** and configure its network settings to **VPC-only access** to achieve network-level isolation and security. 94 | * **Note**: **Never** expose the MCP Server SSE endpoint configured with your AccessKey on the public internet without any identity verification or access control mechanisms, as this poses a high security risk. 95 | 96 | ### Usage Instructions 97 | 98 | 99 | Before using the MCP Server, you need to obtain Alibaba Cloud's AccessKeyId and AccessKeySecret. Please refer to [Alibaba Cloud AccessKey Management](https://www.alibabacloud.com/help/en/basics-for-beginners/latest/obtain-an-accesskey-pair) 100 | 101 | 102 | #### Install using pip 103 | > ⚠️ Requires Python 3.10 or higher. 104 | 105 | Simply install using pip: 106 | 107 | ```bash 108 | pip install mcp-server-aliyun-observability 109 | ``` 110 | 1. After installation, run directly with the following command: 111 | 112 | ```bash 113 | python -m mcp_server_aliyun_observability --transport sse --access-key-id --access-key-secret 114 | ``` 115 | You can pass specific parameters through the command line: 116 | - `--transport` Specify the transport method, options are `stdio`, `sse`, or `streamable-http`, default is `streamable-http` 117 | - `--access-key-id` Specify Alibaba Cloud AccessKeyId, if not specified, ALIBABA_CLOUD_ACCESS_KEY_ID from environment variables will be used 118 | - `--access-key-secret` Specify Alibaba Cloud AccessKeySecret, if not specified, ALIBABA_CLOUD_ACCESS_KEY_SECRET from environment variables will be used 119 | - `--sls-endpoints` Override SLS endpoint mapping with `REGION=HOST` pairs separated by comma/space, e.g. `--sls-endpoints "cn-shanghai=cn-hangzhou.log.aliyuncs.com"` 120 | - `--cms-endpoints` Override CMS endpoint mapping, same format as above, e.g. `--cms-endpoints "cn-shanghai=cms.internal.aliyuncs.com"` 121 | - `--scope` Specify tool scope, options: `paas`, `iaas`, `all`, default: `all` 122 | - `--log-level` Specify log level, options are `DEBUG`, `INFO`, `WARNING`, `ERROR`, default is `INFO` 123 | - `--transport-port` Specify transport port, default is `8080`, only effective when `--transport` is `sse` or `streamable-http` 124 | 125 | 2. Start using uv command 126 | 127 | ```bash 128 | uv run mcp-server-aliyun-observability 129 | ``` 130 | ### Install from source code 131 | 132 | ```bash 133 | 134 | # clone the source code 135 | git clone git@github.com:aliyun/alibabacloud-observability-mcp-server.git 136 | # enter the source directory 137 | cd alibabacloud-observability-mcp-server 138 | # install 139 | pip install -e . 140 | # run 141 | python -m mcp_server_aliyun_observability --transport sse --access-key-id --access-key-secret 142 | ``` 143 | 144 | 145 | ### AI Tool Integration 146 | 147 | > Taking SSE startup mode as an example, with transport port 8888. In actual use, you need to modify according to your specific situation. 148 | 149 | #### Integration with Cursor, Cline, etc. 150 | 1. Using SSE startup method 151 | ```json 152 | { 153 | "mcpServers": { 154 | "alibaba_cloud_observability": { 155 | "url": "http://localhost:7897/sse" 156 | } 157 | } 158 | } 159 | ``` 160 | 2. Using stdio startup method 161 | Start directly from the source code directory, note: 162 | 1. Need to specify the `--directory` parameter to indicate the source code directory, preferably an absolute path 163 | 2. The uv command should also use an absolute path; if using a virtual environment, you need to use the absolute path of the virtual environment 164 | ```json 165 | { 166 | "mcpServers": { 167 | "alibaba_cloud_observability": { 168 | "command": "uv", 169 | "args": [ 170 | "--directory", 171 | "/path/to/your/alibabacloud-observability-mcp-server", 172 | "run", 173 | "mcp-server-aliyun-observability" 174 | ], 175 | "env": { 176 | "ALIBABA_CLOUD_ACCESS_KEY_ID": "", 177 | "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "" 178 | } 179 | } 180 | } 181 | } 182 | ``` 183 | 3. Using stdio startup method - start from module 184 | ```json 185 | { 186 | "mcpServers": { 187 | "alibaba_cloud_observability": { 188 | "command": "uv", 189 | "args": [ 190 | "run", 191 | "mcp-server-aliyun-observability" 192 | ], 193 | "env": { 194 | "ALIBABA_CLOUD_ACCESS_KEY_ID": "", 195 | "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "" 196 | } 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | #### Cherry Studio Integration 203 | 204 | ![image](./images/cherry_studio_inter.png) 205 | 206 | ![image](./images/cherry_studio_demo.png) 207 | 208 | 209 | #### Cursor Integration 210 | 211 | ![image](./images/cursor_inter.png) 212 | 213 | ![image](./images/cursor_tools.png) 214 | 215 | ![image](./images/cursor_demo.png) 216 | 217 | 218 | #### ChatWise Integration 219 | 220 | ![image](./images/chatwise_inter.png) 221 | 222 | ![image](./images/chatwise_demo.png) 223 | --------------------------------------------------------------------------------