diff --git a/.gitignore b/.gitignore index 150a2e8..5480500 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ test.py /.venv/ .vscode *.egg-info -uv.lock \ No newline at end of file +uv.lockdocs/ +NOTA-FORNECEDOR-HUAWEI.md +docs/ +uv.lock +tenants.json diff --git a/assets/utils/hwc_tools.py b/assets/utils/hwc_tools.py index 6ef73bd..55c868e 100644 --- a/assets/utils/hwc_tools.py +++ b/assets/utils/hwc_tools.py @@ -13,10 +13,15 @@ from huaweicloudsdkcore.sdk_response import FutureSdkResponse from huaweicloudsdkcore.utils import http_utils -from .model import MCPConfig, TransportType +from .model import MCPConfig, TransportType, TenantConfig from .variable import ( HUAWEI_ACCESS_KEY, HUAWEI_SECRET_KEY, + HUAWEI_ENDPOINT_DOMAIN, + HUAWEI_ENDPOINT_PREFIX, + HUAWEI_PROJECT_ID, + HUAWEI_IAM_ENDPOINT, + HUAWEI_TENANTS_FILE, MCP_SERVER_MODE, MCP_SERVER_PORT, ) @@ -160,16 +165,29 @@ def do_http_request( return response -def create_api_client(ak, sk, x_host, region="cn-north-4"): +def create_api_client(ak, sk, x_host, region="cn-north-4", endpoint_domain=None, + endpoint_prefix=None, project_id=None, iam_endpoint=None): endpoint = x_host - if x_host.find("com") != -1: - endpoint = f"https://{x_host}" + # Support custom endpoint domain for HCSO / on-premise deployments + if endpoint_domain and "myhuaweicloud.com" in endpoint: + endpoint = endpoint.replace("myhuaweicloud.com", endpoint_domain) + + # Inject prefix into service name (e.g. "-prevnet" for internal network) + # roma.region.domain → roma-prevnet.region.domain + if endpoint_prefix: + dot_idx = endpoint.index(".") + endpoint = endpoint[:dot_idx] + endpoint_prefix + endpoint[dot_idx:] + + if "." in endpoint and not endpoint.startswith("http"): + endpoint = f"https://{endpoint}" if endpoint.find("{region}") != -1: endpoint = endpoint.replace("{region}", region) - credentials = BasicCredentials(ak, sk) + credentials = BasicCredentials(ak, sk, project_id) + if iam_endpoint: + credentials = credentials.with_iam_endpoint(iam_endpoint) http_config = HttpConfig() http_config.ignore_ssl_verification = True @@ -252,6 +270,31 @@ def load_openapi(config_path): raise IOError(f"加载OpenAPI文件失败: {str(e)}") +def _load_tenants_file(path: str) -> dict[str, TenantConfig]: + """Load tenants from a JSON file. Format: + { + "default": "sicar", + "tenants": { + "sicar": {"ak": "...", "sk": "...", "project_id": "...", ...}, + "mgi": {"ak": "...", "sk": "...", "project_id": "...", ...} + } + } + """ + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + tenants = {} + for name, t in data.get("tenants", {}).items(): + tenants[name] = TenantConfig( + name=name, ak=t["ak"], sk=t["sk"], + endpoint_domain=t.get("endpoint_domain"), + endpoint_prefix=t.get("endpoint_prefix"), + project_id=t.get("project_id"), + iam_endpoint=t.get("iam_endpoint"), + region=t.get("region"), + ) + return data.get("default"), tenants + + def load_config(config_path: Union[str, Path]) -> MCPConfig: try: with open(config_path, "r", encoding="utf-8") as f: @@ -269,11 +312,19 @@ def load_config(config_path: Union[str, Path]) -> MCPConfig: port=config_dict.get("port", 8888), ak=config_dict.get("ak", ""), sk=config_dict.get("sk", ""), + endpoint_domain=config_dict.get("endpoint_domain", ""), + endpoint_prefix=config_dict.get("endpoint_prefix", ""), + project_id=config_dict.get("project_id", ""), + iam_endpoint=config_dict.get("iam_endpoint", ""), ) env_mapping = [ (HUAWEI_ACCESS_KEY, "ak", None, None), (HUAWEI_SECRET_KEY, "sk", None, None), + (HUAWEI_ENDPOINT_DOMAIN, "endpoint_domain", None, None), + (HUAWEI_ENDPOINT_PREFIX, "endpoint_prefix", None, None), + (HUAWEI_PROJECT_ID, "project_id", None, None), + (HUAWEI_IAM_ENDPOINT, "iam_endpoint", None, None), (MCP_SERVER_MODE, "transport", None, get_args(TransportType)), (MCP_SERVER_PORT, "port", int, None), ] @@ -289,6 +340,12 @@ def load_config(config_path: Union[str, Path]) -> MCPConfig: f"无效值 '{value_to_set}'. 有效值清单: {allowed_values}" ) setattr(cfg, attr_name, value_to_set) + + # Load multi-tenant config if HUAWEI_TENANTS_FILE is set + tenants_file = os.environ.get(HUAWEI_TENANTS_FILE) + if tenants_file: + cfg.default_tenant, cfg.tenants = _load_tenants_file(tenants_file) + # 参数校验 cfg.check() return cfg diff --git a/assets/utils/model.py b/assets/utils/model.py index 9952c50..42c8b32 100644 --- a/assets/utils/model.py +++ b/assets/utils/model.py @@ -1,9 +1,22 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Literal TransportType = Literal["sse", "stdio", "http"] +@dataclass +class TenantConfig: + """Credentials and endpoint config for a single HCSO tenant.""" + name: str + ak: str + sk: str + endpoint_domain: Optional[str] = None + endpoint_prefix: Optional[str] = None + project_id: Optional[str] = None + iam_endpoint: Optional[str] = None + region: Optional[str] = None + + @dataclass class MCPConfig: port: int @@ -11,6 +24,12 @@ class MCPConfig: transport: TransportType ak: Optional[str] = None sk: Optional[str] = None + endpoint_domain: Optional[str] = None + endpoint_prefix: Optional[str] = None + project_id: Optional[str] = None + iam_endpoint: Optional[str] = None + tenants: dict[str, TenantConfig] = field(default_factory=dict) + default_tenant: Optional[str] = None def check(self): if not self.service_code: diff --git a/assets/utils/server.py b/assets/utils/server.py index b9c1ad2..3299f67 100644 --- a/assets/utils/server.py +++ b/assets/utils/server.py @@ -1,6 +1,7 @@ import asyncio import contextlib import json +import os import time import uuid from pathlib import Path @@ -82,6 +83,19 @@ def initialize(self) -> None: self.tools = OpenAPIToToolsConverter(self.openapi_dict).convert() logger.info(f"成功加载 {len(self.tools)} 个工具") + # Inject 'tenant' parameter into all tools if multi-tenant is configured + tenants_file = os.environ.get("HUAWEI_TENANTS_FILE") + if tenants_file and self.config.tenants: + tenant_names = list(self.config.tenants.keys()) + for tool in self.tools: + schema = tool.inputSchema + if "properties" not in schema: + schema["properties"] = {} + schema["properties"]["tenant"] = { + "type": "string", + "description": f"Tenant name. Available: {tenant_names}", + } + # 注册工具处理函数 self._register_tool_handlers() @@ -122,11 +136,32 @@ async def list_tools() -> list[Tool]: async def call_tool( name: str, arguments: dict ) -> list[TextContent | ImageContent | EmbeddedResource]: - region = arguments.get("region") or "cn-north-4" + region = arguments.pop("region", None) or "cn-north-4" + tenant_name = arguments.pop("tenant", None) x_host = self.openapi_dict["info"]["x-host"] - ak = self.config.ak - sk = self.config.sk + # Resolve credentials: tenant > config defaults + ak, sk = self.config.ak, self.config.sk + endpoint_domain = self.config.endpoint_domain + endpoint_prefix = self.config.endpoint_prefix + project_id = self.config.project_id + iam_endpoint = self.config.iam_endpoint + + if self.config.tenants: + t_name = tenant_name or self.config.default_tenant + if t_name and t_name in self.config.tenants: + t = self.config.tenants[t_name] + ak, sk = t.ak, t.sk + endpoint_domain = t.endpoint_domain or endpoint_domain + endpoint_prefix = t.endpoint_prefix or endpoint_prefix + project_id = t.project_id or project_id + iam_endpoint = t.iam_endpoint or iam_endpoint + region = t.region or region + elif t_name: + available = list(self.config.tenants.keys()) + raise ToolError( + f"Unknown tenant '{t_name}'. Available: {available}" + ) if not ak or not sk: error_msg = { @@ -135,7 +170,13 @@ async def call_tool( } raise ToolError(error_msg) - client = create_api_client(ak, sk, x_host, region) + client = create_api_client( + ak, sk, x_host, region, + endpoint_domain, + endpoint_prefix, + project_id, + iam_endpoint, + ) try: arguments = filter_parameters(arguments) diff --git a/assets/utils/variable.py b/assets/utils/variable.py index 3dc7102..783ccab 100644 --- a/assets/utils/variable.py +++ b/assets/utils/variable.py @@ -2,5 +2,10 @@ TRANSPORT_HTTP = "http" HUAWEI_ACCESS_KEY = "HUAWEI_ACCESS_KEY" HUAWEI_SECRET_KEY = "HUAWEI_SECRET_KEY" +HUAWEI_ENDPOINT_DOMAIN = "HUAWEI_ENDPOINT_DOMAIN" +HUAWEI_ENDPOINT_PREFIX = "HUAWEI_ENDPOINT_PREFIX" +HUAWEI_PROJECT_ID = "HUAWEI_PROJECT_ID" +HUAWEI_IAM_ENDPOINT = "HUAWEI_IAM_ENDPOINT" +HUAWEI_TENANTS_FILE = "HUAWEI_TENANTS_FILE" MCP_SERVER_MODE = "MCP_SERVER_MODE" MCP_SERVER_PORT = "MCP_SERVER_PORT" diff --git a/pyproject.toml b/pyproject.toml index c6abd59..26c8b55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,21 +6,25 @@ build-backend = "setuptools.build_meta" [project] name = "huaweicloud-mcp-server" -version = "0.3.0" -description = "A Model Context Protocol server providing tools for Huawei Cloud" +version = "0.3.1" +description = "A Model Context Protocol server providing tools for Huawei Cloud. Fork with HCSO/on-premise support." readme = "README.md" requires-python = ">=3.10" authors = [ { name = "huaweicloud-dtse-team", email = "huaweicloud-dtse-team@huawei.com" }, + { name = "Claudio Filho", email = "filhocf@gmail.com" }, ] -keywords = ["huaweicloud", "mcp", "llm"] -license = { text = "MIT" } +keywords = ["huaweicloud", "mcp", "llm", "hcso", "on-premise"] +license = { text = "Apache-2.0" } classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "aiohttp>=3.11.18", @@ -32,6 +36,12 @@ dependencies = [ "huaweicloudsdkcore>=3.1.150" ] +[project.urls] +Homepage = "https://github.com/filhocf/mcp-server" +Repository = "https://github.com/filhocf/mcp-server" +Upstream = "https://github.com/HuaweiCloudDeveloper/mcp-server" +Issues = "https://github.com/HuaweiCloudDeveloper/mcp-server/issues" + [tool.setuptools] packages = [ "assets", diff --git a/tenants.example.json b/tenants.example.json new file mode 100644 index 0000000..56d948a --- /dev/null +++ b/tenants.example.json @@ -0,0 +1,23 @@ +{ + "default": "sicar", + "tenants": { + "sicar": { + "ak": "", + "sk": "", + "project_id": "", + "endpoint_domain": "hcso.dataprev.gov.br", + "endpoint_prefix": "-prevnet", + "iam_endpoint": "https://iam-pub-prevnet.la-south-6001.hcso.dataprev.gov.br", + "region": "la-south-6001" + }, + "mgi": { + "ak": "", + "sk": "", + "project_id": "", + "endpoint_domain": "", + "endpoint_prefix": "", + "iam_endpoint": "", + "region": "" + } + } +}