From e83629d452f34c67caa1d65bd12b86faf0dd5bdd Mon Sep 17 00:00:00 2001 From: Claudio Ferreira Filho Date: Thu, 26 Mar 2026 09:50:49 -0300 Subject: [PATCH 1/4] feat: add custom endpoint domain support for HCSO/on-premise deployments - Add HUAWEI_ENDPOINT_DOMAIN env var to override myhuaweicloud.com domain - Add endpoint_domain field to MCPConfig (config.yaml + env var) - Modify create_api_client to replace domain when endpoint_domain is set - Pass endpoint_domain from config through server to API client This enables usage with Huawei Cloud Stack Online (HCSO) and other on-premise deployments where API endpoints use a different domain. Closes #137 --- assets/utils/hwc_tools.py | 13 ++++++++++--- assets/utils/model.py | 1 + assets/utils/server.py | 2 +- assets/utils/variable.py | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/assets/utils/hwc_tools.py b/assets/utils/hwc_tools.py index 6ef73bd..995bc09 100644 --- a/assets/utils/hwc_tools.py +++ b/assets/utils/hwc_tools.py @@ -17,6 +17,7 @@ from .variable import ( HUAWEI_ACCESS_KEY, HUAWEI_SECRET_KEY, + HUAWEI_ENDPOINT_DOMAIN, MCP_SERVER_MODE, MCP_SERVER_PORT, ) @@ -160,11 +161,15 @@ 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 = 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) + + if "." in endpoint and not endpoint.startswith("http"): + endpoint = f"https://{endpoint}" if endpoint.find("{region}") != -1: endpoint = endpoint.replace("{region}", region) @@ -269,11 +274,13 @@ 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", ""), ) env_mapping = [ (HUAWEI_ACCESS_KEY, "ak", None, None), (HUAWEI_SECRET_KEY, "sk", None, None), + (HUAWEI_ENDPOINT_DOMAIN, "endpoint_domain", None, None), (MCP_SERVER_MODE, "transport", None, get_args(TransportType)), (MCP_SERVER_PORT, "port", int, None), ] diff --git a/assets/utils/model.py b/assets/utils/model.py index 9952c50..0eb288a 100644 --- a/assets/utils/model.py +++ b/assets/utils/model.py @@ -11,6 +11,7 @@ class MCPConfig: transport: TransportType ak: Optional[str] = None sk: Optional[str] = None + endpoint_domain: 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..0a6f455 100644 --- a/assets/utils/server.py +++ b/assets/utils/server.py @@ -135,7 +135,7 @@ 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, self.config.endpoint_domain) try: arguments = filter_parameters(arguments) diff --git a/assets/utils/variable.py b/assets/utils/variable.py index 3dc7102..92228cf 100644 --- a/assets/utils/variable.py +++ b/assets/utils/variable.py @@ -2,5 +2,6 @@ TRANSPORT_HTTP = "http" HUAWEI_ACCESS_KEY = "HUAWEI_ACCESS_KEY" HUAWEI_SECRET_KEY = "HUAWEI_SECRET_KEY" +HUAWEI_ENDPOINT_DOMAIN = "HUAWEI_ENDPOINT_DOMAIN" MCP_SERVER_MODE = "MCP_SERVER_MODE" MCP_SERVER_PORT = "MCP_SERVER_PORT" From 38ba449fcb69369dafde90868362644d54df4295 Mon Sep 17 00:00:00 2001 From: Claudio Ferreira Filho Date: Wed, 8 Apr 2026 09:03:33 -0300 Subject: [PATCH 2/4] feat: add HCSO internal network support (prefix, project_id, iam_endpoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HUAWEI_ENDPOINT_PREFIX: inject suffix into service name (e.g. '-prevnet') roma.region.domain → roma-prevnet.region.domain - HUAWEI_PROJECT_ID: required for HCSO on-premise authentication - HUAWEI_IAM_ENDPOINT: custom IAM endpoint for on-premise deployments Uses BasicCredentials.with_iam_endpoint() for HCSO IAM All new parameters supported via env vars and config.yaml. --- .gitignore | 3 ++- assets/utils/hwc_tools.py | 22 ++++++++++++++++++++-- assets/utils/model.py | 3 +++ assets/utils/server.py | 8 +++++++- assets/utils/variable.py | 3 +++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 150a2e8..300efc5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ test.py /.venv/ .vscode *.egg-info -uv.lock \ No newline at end of file +uv.lockdocs/ +NOTA-FORNECEDOR-HUAWEI.md diff --git a/assets/utils/hwc_tools.py b/assets/utils/hwc_tools.py index 995bc09..0211ce5 100644 --- a/assets/utils/hwc_tools.py +++ b/assets/utils/hwc_tools.py @@ -18,6 +18,9 @@ HUAWEI_ACCESS_KEY, HUAWEI_SECRET_KEY, HUAWEI_ENDPOINT_DOMAIN, + HUAWEI_ENDPOINT_PREFIX, + HUAWEI_PROJECT_ID, + HUAWEI_IAM_ENDPOINT, MCP_SERVER_MODE, MCP_SERVER_PORT, ) @@ -161,20 +164,29 @@ def do_http_request( return response -def create_api_client(ak, sk, x_host, region="cn-north-4", endpoint_domain=None): +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 # 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 @@ -275,12 +287,18 @@ def load_config(config_path: Union[str, Path]) -> MCPConfig: 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), ] diff --git a/assets/utils/model.py b/assets/utils/model.py index 0eb288a..4f7977b 100644 --- a/assets/utils/model.py +++ b/assets/utils/model.py @@ -12,6 +12,9 @@ class MCPConfig: 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 def check(self): if not self.service_code: diff --git a/assets/utils/server.py b/assets/utils/server.py index 0a6f455..1ecb557 100644 --- a/assets/utils/server.py +++ b/assets/utils/server.py @@ -135,7 +135,13 @@ async def call_tool( } raise ToolError(error_msg) - client = create_api_client(ak, sk, x_host, region, self.config.endpoint_domain) + client = create_api_client( + ak, sk, x_host, region, + self.config.endpoint_domain, + self.config.endpoint_prefix, + self.config.project_id, + self.config.iam_endpoint, + ) try: arguments = filter_parameters(arguments) diff --git a/assets/utils/variable.py b/assets/utils/variable.py index 92228cf..3102770 100644 --- a/assets/utils/variable.py +++ b/assets/utils/variable.py @@ -3,5 +3,8 @@ 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" MCP_SERVER_MODE = "MCP_SERVER_MODE" MCP_SERVER_PORT = "MCP_SERVER_PORT" From 6e6bbcfe666094217436b531a4955e9cd3af4177 Mon Sep 17 00:00:00 2001 From: Claudio Ferreira Filho Date: Fri, 10 Apr 2026 15:11:47 -0300 Subject: [PATCH 3/4] chore: prepare for PyPI publishing (v0.3.1) - Bump version to 0.3.1 - Add HCSO/on-premise to description and keywords - Fix license to Apache-2.0 (matching repo LICENSE) - Add project URLs (fork + upstream) - Add Python 3.11-3.13 classifiers - Add co-author --- pyproject.toml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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", From 879d5a30c8b7a537b08a84d32308b6f69d01d5a5 Mon Sep 17 00:00:00 2001 From: Claudio Ferreira Filho Date: Thu, 16 Apr 2026 09:32:39 -0300 Subject: [PATCH 4/4] feat: add multi-tenant support via HUAWEI_TENANTS_FILE - New TenantConfig dataclass for per-tenant credentials - HUAWEI_TENANTS_FILE env var points to JSON with tenant configs - 'tenant' parameter injected into all tools when multi-tenant active - call_tool resolves credentials from tenant > default_tenant > config - Backward compatible: without HUAWEI_TENANTS_FILE, behavior unchanged - tenants.example.json with sicar/mgi template - .gitignore: docs/, uv.lock, tenants.json --- .gitignore | 3 +++ assets/utils/hwc_tools.py | 34 ++++++++++++++++++++++++++- assets/utils/model.py | 17 +++++++++++++- assets/utils/server.py | 49 +++++++++++++++++++++++++++++++++------ assets/utils/variable.py | 1 + tenants.example.json | 23 ++++++++++++++++++ 6 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 tenants.example.json diff --git a/.gitignore b/.gitignore index 300efc5..5480500 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ test.py *.egg-info 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 0211ce5..55c868e 100644 --- a/assets/utils/hwc_tools.py +++ b/assets/utils/hwc_tools.py @@ -13,7 +13,7 @@ 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, @@ -21,6 +21,7 @@ HUAWEI_ENDPOINT_PREFIX, HUAWEI_PROJECT_ID, HUAWEI_IAM_ENDPOINT, + HUAWEI_TENANTS_FILE, MCP_SERVER_MODE, MCP_SERVER_PORT, ) @@ -269,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: @@ -314,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 4f7977b..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 @@ -15,6 +28,8 @@ class MCPConfig: 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 1ecb557..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 = { @@ -137,10 +172,10 @@ async def call_tool( client = create_api_client( ak, sk, x_host, region, - self.config.endpoint_domain, - self.config.endpoint_prefix, - self.config.project_id, - self.config.iam_endpoint, + 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 3102770..783ccab 100644 --- a/assets/utils/variable.py +++ b/assets/utils/variable.py @@ -6,5 +6,6 @@ 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/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": "" + } + } +}