Skip to content

Commit dfa73b1

Browse files
authored
Merge pull request #1 from aliyun/feature/edit_ak_sk_load
Feature/edit ak sk load
2 parents 655c0c0 + 2640dbb commit dfa73b1

File tree

7 files changed

+1746
-54
lines changed

7 files changed

+1746
-54
lines changed

README.md

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,42 @@
5252
![image](./images/find_slowest_trace.png)
5353

5454

55+
### 权限要求
56+
57+
为了确保 MCP Server 能够成功访问和操作您的阿里云可观测性资源,您需要配置以下权限:
58+
59+
1. **阿里云访问密钥 (AccessKey)**
60+
* 服务运行需要有效的阿里云 AccessKey ID 和 AccessKey Secret。
61+
* 获取和管理 AccessKey,请参考 [阿里云 AccessKey 管理官方文档](https://help.aliyun.com/document_detail/53045.html)
62+
63+
2. 当你初始化时候不传入 AccessKey 和 AccessKey Secret 时,会使用[默认凭据链进行登录](https://www.alibabacloud.com/help/zh/sdk/developer-reference/v2-manage-python-access-credentials#62bf90d04dztq)
64+
1. 如果环境变量 中的ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET均存在且非空,则使用它们作为默认凭据。
65+
2. 如果同时设置了ALIBABA_CLOUD_ACCESS_KEY_ID、ALIBABA_CLOUD_ACCESS_KEY_SECRET和ALIBABA_CLOUD_SECURITY_TOKEN,则使用STS Token作为默认凭据。
66+
67+
3. **RAM 授权 (重要)**
68+
* 与 AccessKey 关联的 RAM 用户或角色**必须**被授予访问相关云服务所需的权限。
69+
* **强烈建议遵循"最小权限原则"**:仅授予运行您计划使用的 MCP 工具所必需的最小权限集,以降低安全风险。
70+
* 根据您需要使用的工具,参考以下文档进行权限配置:
71+
* **日志服务 (SLS)**:如果您需要使用 `sls_*` 相关工具,请参考 [日志服务权限说明](https://help.aliyun.com/zh/sls/overview-8),并授予必要的读取、查询等权限。
72+
* **应用实时监控服务 (ARMS)**:如果您需要使用 `arms_*` 相关工具,请参考 [ARMS 权限说明](https://help.aliyun.com/zh/arms/security-and-compliance/overview-8?scm=20140722.H_74783._.OR_help-T_cn~zh-V_1),并授予必要的查询权限。
73+
* 请根据您的实际应用场景,精细化配置所需权限。
74+
75+
### 安全与部署建议
76+
77+
请务必关注以下安全事项和部署最佳实践:
78+
79+
1. **密钥安全**
80+
* 本 MCP Server 在运行时会使用您提供的 AccessKey 调用阿里云 OpenAPI,但**不会以任何形式存储您的 AccessKey**,也不会将其用于设计功能之外的任何其他用途。
81+
82+
2. **访问控制 (关键)**
83+
* 当您选择通过 **SSE (Server-Sent Events) 协议** 访问 MCP Server 时,**您必须自行负责该服务接入点的访问控制和安全防护**
84+
* **强烈建议**将 MCP Server 部署在**内部网络或受信环境**中,例如您的私有 VPC (Virtual Private Cloud) 内,避免直接暴露于公共互联网。
85+
* 推荐的部署方式是使用**阿里云函数计算 (FC)**,并配置其网络设置为**仅 VPC 内访问**,以实现网络层面的隔离和安全。
86+
* **注意****切勿**在没有任何身份验证或访问控制机制的情况下,将配置了您 AccessKey 的 MCP Server SSE 端点暴露在公共互联网上,这会带来极高的安全风险。
87+
5588
### 使用说明
5689

90+
5791
在使用 MCP Server 之前,需要先获取阿里云的 AccessKeyId 和 AccessKeySecret,请参考 [阿里云 AccessKey 管理](https://help.aliyun.com/document_detail/53045.html)
5892

5993

@@ -64,25 +98,31 @@
6498
```bash
6599
pip install mcp-server-aliyun-observability
66100
```
67-
安装之后,直接运行即可,运行命令如下:
101+
1. 安装之后,直接运行即可,运行命令如下:
68102

69103
```bash
70104
python -m mcp_server_aliyun_observability --transport sse --access-key-id <your_access_key_id> --access-key-secret <your_access_key_secret>
71105
```
72106
可通过命令行传递指定参数:
73107
- `--transport` 指定传输方式,可选值为 `sse``stdio`,默认值为 `stdio`
74-
- `--access-key-id` 指定阿里云 AccessKeyId
75-
- `--access-key-secret` 指定阿里云 AccessKeySecret
108+
- `--access-key-id` 指定阿里云 AccessKeyId,不指定时会使用环境变量中的ALIBABA_CLOUD_ACCESS_KEY_ID
109+
- `--access-key-secret` 指定阿里云 AccessKeySecret,不指定时会使用环境变量中的ALIBABA_CLOUD_ACCESS_KEY_SECRET
76110
- `--log-level` 指定日志级别,可选值为 `DEBUG``INFO``WARNING``ERROR`,默认值为 `INFO`
77111
- `--transport-port` 指定传输端口,默认值为 `8000`,仅当 `--transport``sse` 时有效
78112

79-
80-
113+
2. 使用uv 命令启动
114+
115+
```bash
116+
uv run mcp-server-aliyun-observability
117+
```
81118
### 从源码安装
82119

83120
```bash
121+
84122
# clone 源码
85-
cd src/mcp_server_aliyun_observability
123+
git clone [email protected]:aliyun/alibabacloud-observability-mcp-server.git
124+
# 进入源码目录
125+
cd alibabacloud-observability-mcp-server
86126
# 安装
87127
pip install -e .
88128
# 运行
@@ -91,7 +131,62 @@ python -m mcp_server_aliyun_observability --transport sse --access-key-id <your_
91131

92132

93133
### AI 工具集成
134+
94135
> 以 SSE 启动方式为例,transport 端口为 8888,实际使用时需要根据实际情况修改
136+
137+
#### Cursor,Cline 等集成
138+
1. 使用 SSE 启动方式
139+
```json
140+
{
141+
"mcpServers": {
142+
"alibaba_cloud_observability": {
143+
"url": "http://localhost:7897/sse"
144+
}
145+
}
146+
}
147+
```
148+
2. 使用 stdio 启动方式
149+
直接从源码目录启动,注意
150+
1. 需要指定 `--directory` 参数,指定源码目录,最好是绝对路径
151+
2. uv命令 最好也使用绝对路径,如果使用了虚拟环境,则需要使用虚拟环境的绝对路径
152+
```json
153+
{
154+
"mcpServers": {
155+
"alibaba_cloud_observability": {
156+
"command": "uv",
157+
"args": [
158+
"--directory",
159+
"/path/to/your/alibabacloud-observability-mcp-server",
160+
"run",
161+
"mcp-server-aliyun-observability"
162+
],
163+
"env": {
164+
"ALIBABA_CLOUD_ACCESS_KEY_ID": "<your_access_key_id>",
165+
"ALIBABA_CLOUD_ACCESS_KEY_SECRET": "<your_access_key_secret>"
166+
}
167+
}
168+
}
169+
}
170+
```
171+
1. 使用 stdio 启动方式-从 module 启动
172+
```json
173+
{
174+
"mcpServers": {
175+
"alibaba_cloud_observability": {
176+
"command": "uv",
177+
"args": [
178+
"run",
179+
"mcp-server-aliyun-observability"
180+
],
181+
"env": {
182+
"ALIBABA_CLOUD_ACCESS_KEY_ID": "<your_access_key_id>",
183+
"ALIBABA_CLOUD_ACCESS_KEY_SECRET": "<your_access_key_secret>"
184+
}
185+
}
186+
}
187+
}
188+
```
189+
95190
#### Cherry Studio集成
96191

97192
![image](./images/cherry_studio_inter.png)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies = [
99
"pydantic>=2.10.0",
1010
"alibabacloud_arms20190808==8.0.0",
1111
"alibabacloud_sls20201230==5.7.0",
12+
"alibabacloud_credentials>=1.0.1",
1213
"tenacity>=8.0.0",
1314
]
1415

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
mcp>=1.3.0
22
pydantic>=2.10.0
33
alibabacloud_arms20190808>=8.0.0
4-
alibabacloud_sls20201230>=5.7.0
4+
alibabacloud_credentials>=1.0.1
55
tenacity>=8.0.0
66
pytest>=7.0.0
77
pytest-asyncio>=0.21.0
88
pytest-cov>=4.0.0
9-
pytest-mock>=3.10.0
9+
pytest-mock>=3.10.0

src/mcp_server_aliyun_observability/__init__.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dotenv
66

77
from mcp_server_aliyun_observability.server import server
8+
from mcp_server_aliyun_observability.utils import CredentialWrapper
89

910
dotenv.load_dotenv()
1011

@@ -14,22 +15,22 @@
1415
"--access-key-id",
1516
type=str,
1617
help="aliyun access key id",
17-
default=lambda: os.environ.get("ALIYUN_ACCESS_KEY_ID"),
18+
required=False,
1819
)
1920
@click.option(
2021
"--access-key-secret",
2122
type=str,
2223
help="aliyun access key secret",
23-
default=lambda: os.environ.get("ALIYUN_ACCESS_KEY_SECRET"),
24+
required=False,
2425
)
2526
@click.option(
2627
"--transport", type=str, help="transport type. stdio or sse", default="stdio"
2728
)
2829
@click.option("--log-level", type=str, help="log level", default="INFO")
2930
@click.option("--transport-port", type=int, help="transport port", default=8000)
3031
def main(access_key_id, access_key_secret, transport, log_level, transport_port):
31-
if not access_key_id or not access_key_secret:
32-
raise click.UsageError(
33-
"access_key_id and access_key_secret are required, please set them in environment variables or command line arguments"
34-
)
35-
server(access_key_id, access_key_secret, transport, log_level, transport_port)
32+
if access_key_id and access_key_secret:
33+
credential = CredentialWrapper(access_key_id, access_key_secret)
34+
else:
35+
credential = None
36+
server(credential, transport, log_level, transport_port)
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextlib import asynccontextmanager
2-
from typing import AsyncIterator
2+
from typing import AsyncIterator, Optional
33

4+
from alibabacloud_credentials.client import Client as CredClient
45
from mcp.server import FastMCP
56
from mcp.server.fastmcp import FastMCP
67

@@ -9,15 +10,16 @@
910
from mcp_server_aliyun_observability.toolkit.util_toolkit import UtilToolkit
1011
from mcp_server_aliyun_observability.utils import (
1112
ArmsClientWrapper,
13+
CredentialWrapper,
1214
SLSClientWrapper,
1315
)
1416

1517

16-
def create_lifespan(access_key_id: str, access_key_secret: str):
18+
def create_lifespan(credential: Optional[CredentialWrapper] = None):
1719
@asynccontextmanager
1820
async def lifespan(fastmcp: FastMCP) -> AsyncIterator[dict]:
19-
sls_client = SLSClientWrapper(access_key_id, access_key_secret)
20-
arms_client = ArmsClientWrapper(access_key_id, access_key_secret)
21+
sls_client = SLSClientWrapper(credential)
22+
arms_client = ArmsClientWrapper(credential)
2123
yield {
2224
"sls_client": sls_client,
2325
"arms_client": arms_client,
@@ -27,15 +29,14 @@ async def lifespan(fastmcp: FastMCP) -> AsyncIterator[dict]:
2729

2830

2931
def init_server(
30-
access_key_id: str,
31-
access_key_secret: str,
32+
credential: Optional[CredentialWrapper] = None,
3233
log_level: str = "INFO",
3334
transport_port: int = 8000,
3435
):
3536
"""initialize the global mcp server instance"""
3637
mcp_server = FastMCP(
3738
name="mcp_aliyun_observability_server",
38-
lifespan=create_lifespan(access_key_id, access_key_secret),
39+
lifespan=create_lifespan(credential),
3940
log_level=log_level,
4041
port=transport_port,
4142
)
@@ -46,13 +47,12 @@ def init_server(
4647

4748

4849
def server(
49-
access_key_id: str,
50-
access_key_secret: str,
51-
transport: str,
52-
log_level: str,
53-
transport_port: int,
50+
credential: Optional[CredentialWrapper] = None,
51+
transport: str = "stdio",
52+
log_level: str = "INFO",
53+
transport_port: int = 8000,
5454
):
5555
server: FastMCP = init_server(
56-
access_key_id, access_key_secret, log_level, transport_port
56+
credential, log_level, transport_port
5757
)
5858
server.run(transport)

src/mcp_server_aliyun_observability/utils.py

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
import hashlib
22
import logging
33
from functools import wraps
4-
from typing import (
5-
Any,
6-
Callable,
7-
Optional,
8-
TypeVar,
9-
cast,
10-
)
4+
from typing import Any, Callable, Optional, TypeVar, cast
115

126
from alibabacloud_arms20190808.client import Client as ArmsClient
7+
from alibabacloud_credentials.client import Client as CredClient
138
from alibabacloud_sls20201230.client import Client
149
from alibabacloud_sls20201230.client import Client as SLSClient
15-
from alibabacloud_sls20201230.models import (
16-
CallAiToolsRequest,
17-
CallAiToolsResponse,
18-
IndexJsonKey,
19-
)
10+
from alibabacloud_sls20201230.models import (CallAiToolsRequest,
11+
CallAiToolsResponse, IndexJsonKey)
2012
from alibabacloud_tea_openapi import models as open_api_models
2113
from alibabacloud_tea_util import models as util_models
2214
from mcp.server.fastmcp import Context
@@ -26,23 +18,40 @@
2618

2719
logger = logging.getLogger(__name__)
2820

29-
30-
class SLSClientWrapper:
21+
class CredentialWrapper:
3122
"""
32-
A wrapper for aliyun client
23+
A wrapper for aliyun credentials
3324
"""
3425

26+
access_key_id: str
27+
access_key_secret: str
28+
3529
def __init__(self, access_key_id: str, access_key_secret: str):
3630
self.access_key_id = access_key_id
3731
self.access_key_secret = access_key_secret
32+
33+
34+
35+
class SLSClientWrapper:
36+
"""
37+
A wrapper for aliyun client
38+
"""
39+
40+
def __init__(self, credential: Optional[CredentialWrapper] = None):
41+
self.credential = credential
42+
3843

3944
def with_region(
4045
self, region: str = None, endpoint: Optional[str] = None
4146
) -> SLSClient:
42-
config = open_api_models.Config(
43-
access_key_id=self.access_key_id,
44-
access_key_secret=self.access_key_secret,
45-
)
47+
if self.credential:
48+
config = open_api_models.Config(
49+
access_key_id=self.credential.access_key_id,
50+
access_key_secret=self.credential.access_key_secret,
51+
)
52+
else:
53+
credentialsClient = CredClient()
54+
config = open_api_models.Config(credential=credentialsClient)
4655
config.endpoint = f"{region}.log.aliyuncs.com"
4756
return SLSClient(config)
4857

@@ -52,15 +61,18 @@ class ArmsClientWrapper:
5261
A wrapper for aliyun arms client
5362
"""
5463

55-
def __init__(self, access_key_id: str, access_key_secret: str):
56-
self.access_key_id = access_key_id
57-
self.access_key_secret = access_key_secret
64+
def __init__(self, credential: Optional[CredentialWrapper] = None):
65+
self.credential = credential
5866

5967
def with_region(self, region: str, endpoint: Optional[str] = None) -> ArmsClient:
60-
config = open_api_models.Config(
61-
access_key_id=self.access_key_id,
62-
access_key_secret=self.access_key_secret,
63-
)
68+
if self.credential:
69+
config = open_api_models.Config(
70+
access_key_id=self.credential.access_key_id,
71+
access_key_secret=self.credential.access_key_secret,
72+
)
73+
else:
74+
credentialsClient = CredClient()
75+
config = open_api_models.Config(credential=credentialsClient)
6476
config.endpoint = endpoint or f"arms.{region}.aliyuncs.com"
6577
return ArmsClient(config)
6678

@@ -180,3 +192,4 @@ def text_to_sql(
180192
except Exception as e:
181193
logger.error(f"调用SLS AI工具失败: {str(e)}")
182194
raise
195+

0 commit comments

Comments
 (0)