Metadata-Version: 2.5
Name: huaweicloud-open-mcp
Version: 0.2.0a4
Summary: 华为云 MCP server：本地通用网关（stdio / Streamable HTTP 多会话），openapi 模式 8 工具直连华为云全量 OpenAPI，discover 模式 8 工具发现连接云端 MCP server，data 模式 2 工具（DataFusion 只读 SQL 分析与转换落盘）
Project-URL: Repository, https://github.com/huaweicloud-mate/huaweicloud-open-mcp
Project-URL: Issues, https://github.com/huaweicloud-mate/huaweicloud-open-mcp/issues
Author-email: CheneyYin <cheneyyin@hotmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent,huaweicloud,llm,mcp,mcp-server,model-context-protocol,openapi
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: jsonpath-ng>=1.6
Requires-Dist: jsonschema>=4.10
Requires-Dist: mcp>=2.0.0
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: tabulate>=0.9.0
Requires-Dist: tantivy>=0.26.0
Requires-Dist: typing-extensions>=4.6
Provides-Extra: datafusion
Requires-Dist: datafusion>=43; extra == 'datafusion'
Requires-Dist: sqlparse>=0.5; extra == 'datafusion'
Description-Content-Type: text/markdown

<div align="center">
  <img src="logo.png" alt="Huawei Cloud Open MCP" width="240"/>
  <p><sub>English | <a href="README.zh-CN.md">中文</a></sub></p>
  <p>
    <a href="https://pypi.org/project/huaweicloud-open-mcp/">
      <img src="https://img.shields.io/pypi/v/huaweicloud-open-mcp?include_prereleases=1" alt="PyPI">
    </a>
  </p>
</div>

# Huawei Cloud Open MCP

**Open Connect. Explore What's Next.**

One open, local [Model Context Protocol](https://modelcontextprotocol.io) server connects code agents to Huawei Cloud in natural language. It works with opencode, Codex, Cursor, or any other MCP-capable client. There are no per-service wrappers. The agent explores the full catalog (300+ products, 17,000+ APIs) step by step, narrows down to one concrete API call, then executes it with a locally signed request. This is a personal, local deployment. The gateway runs entirely on your machine, and your AK/SK never leave it.

Three composable modes, selected with `--mode` (comma-separated, e.g. `openapi,data`):

- `openapi` (default) talks to Huawei Cloud OpenAPI.
- `discover` connects to cloud-hosted Huawei Cloud MCP servers (experimental, not documented yet).
- `data` runs read-only SQL analytics and transformations over inline/local data with DataFusion, pure local compute that needs no credentials and is not governed by the safety policy.

A typical `openapi,data` workflow pulls a large dataset with `execute_api`, saves it to a file, then aggregates it with `query_data` or reshapes it into a new dataset with `transform_data`. Only the aggregated results or artifact metadata enter the model context.

The gateway serves stdio by default, the form your code agent launches. `--transport http` adds Streamable HTTP for multi-client deployments; see [HTTP transport](#http-transport-multi-session).

## How it works

- **Progressive workflow.** The agent explores step by step: `search_apis` (entity-graph cross-product search when the intent doesn't name a product) → `list_products → get_product → list_apis → get_api → (get_api_examples) → execute_api`, narrowing 17,000+ APIs to one concrete call. Each step keeps the LLM context bounded; the full guide ships in the server instructions.
- **Metadata-driven, zero SDK.** API metadata is fetched live from Huawei Cloud API Explorer and cached in memory. Requests are signed locally (SDK-HMAC-SHA256, self-implemented) and sent straight to Huawei Cloud.
- **Secure by default.** Every `execute_api` must pass a safety policy (allowlist/denylist); with no policy configured, everything is denied. Rules hot-reload, and the agent can request minimal grants via `manage_policy`.

## Quick start

Connect once, and your agent explores the rest.

### Prerequisites

- Python 3.10+ and [uv](https://docs.astral.sh/uv/) on your PATH (or `pip`); see [Compatibility](#compatibility)
- A code agent: [opencode](https://opencode.ai) or [Codex](https://developers.openai.com/codex/); any MCP-capable client works
- A Huawei Cloud Access Key (AK/SK) from a minimal-privilege IAM sub-user (recommended: read-only permissions for what you plan to query)
- Network access to `apiexplorer.cn-north-4.myhuaweicloud.com`

### Compatibility

**Supported**, per package metadata:

- **OS**: Windows, macOS, Linux. The base package is pure Python plus `tantivy` (search_apis BM25 engine), which ships native wheels for the major platforms; the optional `[datafusion]` extra (data mode) ships native wheels for Windows x86_64, macOS x86_64/arm64, and Linux x86_64/aarch64 (manylinux)
- **Python**: 3.10+ (`requires-python = ">=3.10"`); the `[datafusion]` extra covers the same range

**Tested**, full unit + integration suite (`uv run pytest`, e2e excluded; data-mode tests included via the dev dependency group), Linux x86_64:

| Python | 3.10 | 3.11 | 3.12 | 3.13 |
|---|---|---|---|---|
| Full suite (incl. data mode) | pass | pass | pass | pass |

Windows and macOS are expected to work. Dependency resolution for those platforms is verified, and the code has no OS-specific branches, but they are not machine-tested; there is no CI matrix yet.

### Install

The quick start runs the gateway via [uvx](https://docs.astral.sh/uv/), so there is no install step; the first invocation fetches the package automatically. For a persistent install:

```bash
pip install huaweicloud-open-mcp                  # or: uv tool install / pipx install — then replace `uvx huaweicloud-open-mcp` with `huaweicloud-open-mcp` in Step 3
pip install "huaweicloud-open-mcp[datafusion]"    # optional extra: data-mode SQL engine
```

### Step 1: Provide credentials

The gateway reads your AK/SK from `~/.huaweicloud/credentials` (INI format, `[basic]` section); on Windows that is `%USERPROFILE%\.huaweicloud\credentials`. See [Credentials](#credentials) for the inline environment-variable alternative.

Create a `.huaweicloud` directory in your home directory, then a `credentials` file inside it with the following content:

```ini
[basic]
ak = your-access-key-id
sk = your-secret-access-key

# optional — uncomment as needed:
# security_token = <temporary-security-token>
# project_id = <project-id>
# domain_id = <domain-id>
```

Optional keys (uncomment as needed): `security_token` (temporary credentials, IAM temporary AK/SK + securitytoken, valid for 15 minutes to 24 hours; attached automatically and included in the request signature), `project_id` (optional static value; substituted into `{project_id}` path parameters and the `X-Project-Id` header), `domain_id` (global-level services; full support in progress). Keep the file private; it holds your secret: run `chmod 600 ~/.huaweicloud/credentials` on macOS/Linux; on Windows a file in your user profile is only readable by your account by default. The server reads this file at startup; the log line `server start: ... credentials=configured` (see `--log-file`) confirms it was picked up.

### Step 2: Create a read-only safety policy

The gateway refuses every `execute_api` call unless a policy file explicitly allows it. With no policy configured, everything is denied.

Create a policy file, for example `hwc-policy.json` in your home directory, with the following content:

```json
[
  "ECS:*List*=allow",
  "*=deny"
]
```

Each rule has the form `product:apiPattern=allow|deny`, with fnmatch-style wildcards and case-insensitive matching; lines starting with `#` are comments. The gateway evaluates rules top-down and the first match wins, so this file allows every ECS API whose name contains `List` and denies everything else. Clients need the absolute path to this file, for example `/home/you/hwc-policy.json` on macOS/Linux or `C:\Users\you\hwc-policy.json` on Windows, because they spawn the server with their own working directory.

### Step 3: Register the gateway with your code agent

**opencode**: add to `opencode.json` (project-level, works on every OS) or the global config (`~/.config/opencode/opencode.json` on macOS/Linux; on Windows, prefer the project-level file or the `OPENCODE_CONFIG` environment variable pointing to an absolute path):

```json
{
  "mcp": {
    "huaweicloud": {
      "type": "local",
      "command": [
        "uvx", "huaweicloud-open-mcp",
        "--policy", "/home/you/hwc-policy.json"
      ],
      "enabled": true
    }
  }
}
```

**Codex**: add to `~/.codex/config.toml` (on Windows: `%USERPROFILE%\.codex\config.toml`) or run (single line, works in any shell):

```
codex mcp add huaweicloud -- uvx huaweicloud-open-mcp --policy /home/you/hwc-policy.json
```

```toml
[mcp_servers.huaweicloud]
command = "uvx"
args = ["huaweicloud-open-mcp", "--policy", "/home/you/hwc-policy.json"]
```

Replace the example path with your own absolute path from Step 2 (on Windows, e.g. `C:\Users\you\hwc-policy.json`; inside JSON/TOML strings write it with escaped backslashes: `"C:\\Users\\you\\hwc-policy.json"`).

**Output**: start your agent. The eight gateway tools appear, prefixed with your server name (`huaweicloud_search_apis`, `huaweicloud_list_products`, `huaweicloud_get_product`, `huaweicloud_list_apis`, `huaweicloud_get_api`, `huaweicloud_get_api_examples`, `huaweicloud_execute_api`, `huaweicloud_manage_policy`). In Codex, `codex mcp list` shows the server and `/mcp` in the TUI confirms it is connected.

Credentials come from Step 1, so no secrets live in the client config. If you prefer inline environment variables instead, see [Credentials](#credentials).

### Step 4: Browse the catalog (first real call)

**Input** (say to your agent):

> List the available Huawei Cloud products.

The agent calls `list_products`, which fetches live metadata from Huawei Cloud API Explorer.

**Output** (abridged):

```json
{
  "ok": true,
  "total": 310,
  "products": [
    { "product": "ECS", "name": "弹性云服务器", "category": "计算",
      "link": "https://www.huaweicloud.com/product/ecs.html" },
    { "product": "EVS", "name": "云硬盘", "category": "存储",
      "link": "https://www.huaweicloud.com/product/evs.html" }
  ]
}
```

### Step 5: Execute a real read-only API

**Input** (say to your agent):

> List my ECS servers in cn-north-4.

The agent runs the progressive workflow: `list_apis(ECS)` to find the API, `get_api` to read its parameters, then `execute_api`, which signs the request with your AK/SK and sends it to real Huawei Cloud.

**Output** (abridged):

```json
{
  "ok": true,
  "product": "ECS",
  "api": "ListServersDetails",
  "status": 200,
  "body": {
    "servers": [
      { "name": "ecs-01", "status": "ACTIVE", "id": "1d4e…" }
    ],
    "count": 2
  }
}
```

`"count": 0` with an empty `servers` list is also a success. Your account simply has no instances in that region; ask again with another `region` (for example `cn-east-3`).

On security: requests are signed locally and your SK never leaves your machine; the policy file confines the agent to read-only ECS `List*` APIs. Widen it deliberately, one pattern at a time; see [Safety policy](#safety-policy).

### No account yet? Mock mode

Run the same flow without credentials: skip Step 1, and add `--mock` to the server command in Step 3 (`uvx huaweicloud-open-mcp --mock --policy <policy-path>`, e.g. `/home/you/hwc-policy.json` or `C:\Users\you\hwc-policy.json`).

**Output**: Step 4 works identically; the product catalog is real metadata. Step 5 returns simulated server data shaped exactly like the real response (mock endpoint, no Huawei Cloud account involved).

## OBS uploads & downloads

OBS object APIs (`PutObject` / `GetObject` / `AppendObject` / `UploadPart`) never stream data through the gateway. In real mode, `execute_api` always answers with a presigned-URL envelope, no flag needed, and the client moves the bytes directly to or from OBS, with no size limit:

```json
{
  "ok": true,
  "presign": {
    "url": "https://<bucket>.obs.<region>.myhuaweicloud.com/<key>?AccessKeyId=...&Expires=...&Signature=...",
    "method": "PUT",
    "expires_in": 900,
    "signed_content_type": "application/octet-stream",
    "headers": { "Content-Type": "application/octet-stream" }
  }
}
```

Use the URL with any HTTP client; the gateway never sees the data:

```bash
curl -X PUT --upload-file big.dat '<url>' -H 'Content-Type: application/octet-stream'
```

Rules that matter:

- Content-Type is part of the signature. For uploads, pass `_presign_content_type` to lock it and send exactly the headers listed in `headers`; if you don't lock it, the signature assumes no Content-Type, and the direct request must not send one (`curl -H 'Content-Type:'`). The envelope's `note` field warns about this case.
- `_presign_expires` tunes validity in seconds (default 900).
- `GetObject` presigns run one `HEAD` metadata pre-check first (object bytes still never pass the gateway). The envelope then carries `expected_size` / `expected_etag` for post-download verification; a 404 (bucket or object gone, for example a deleted FunctionGraph source bucket) denies the presign outright instead of returning a URL that downloads an XML error page; other pre-check failures degrade gracefully (no expected fields, note explains).
- All other OBS APIs (bucket management, tagging, ACL, …) execute through the gateway as usual; pass `_presign=true` explicitly if you want a URL for one of them. Non-OBS products reject `_presign`. Mock mode keeps hitting the mock endpoint.

## HTTP transport (multi-session)

By default the gateway speaks stdio, the local form your code agent launches. It can also serve Streamable HTTP from a single process for multiple concurrent MCP client sessions:

```bash
uvx huaweicloud-open-mcp --transport http                  # listens on http://127.0.0.1:8000/mcp
uvx huaweicloud-open-mcp --transport http --http-host 0.0.0.0 --http-port 9000
```

Point any MCP-capable client at the endpoint:

```json
{ "mcpServers": { "huaweicloud": { "type": "http", "url": "http://127.0.0.1:8000/mcp" } } }
```

What changes over HTTP:

- Every client connection gets its own policy session. `session`-scope rules granted via `manage_policy` (directly or through elicitation) are visible only to that connection and invisible to every other; reconnecting starts a fresh session with no inherited grants; `once` rules burn within the granting session. `permanent` rules land in the policy file and stay shared across sessions and restarts, as always.
- Requests without MCP session identity (the modern single-exchange protocol) cannot write session-scoped grants; they get a structured rejection instead of silently sharing state.
- `GET /healthz` serves probes. Audit events (with `--audit-file`) carry a `session` field attributing each call to its MCP session; over stdio the audit output is byte-identical to before.
- Idle sessions are recycled: a session bucket untouched for 30 minutes is reclaimed, and any grant dies after 24 hours. A session that keeps working keeps its grants alive.

Security posture:

- By default the gateway binds `127.0.0.1` (loopback also auto-enables the SDK's DNS-rebinding protection). For containers, set `HUAWEICLOUD_MCP_HTTP_HOST=0.0.0.0` in the image or run command; exposing the port is already an explicit decision. Binding a non-loopback address prints a warning, because anyone who can reach the port can act with this deployment's AK/SK. Beyond a trusted network, put a reverse proxy with TLS and auth in front. v1 ships no built-in HTTP authentication.
- Multiple worker processes are not supported (session state lives in the process).

Embedding (ASGI): `build_app(...).streamable_http_app()` returns a Starlette app whose lifespan runs the MCP session manager. Drive it with any ASGI runner (uvicorn/gunicorn). If you compose the HTTP serving yourself, declare it in the args passed to `build_app` (`--transport http` / `HUAWEICLOUD_MCP_TRANSPORT=http`) so per-session isolation is wired in. Composing HTTP on a stdio-declared deployment would let session-scoped grants from session-less requests share one namespace.

## Tools (openapi mode)

All tools return one flat envelope, `ok` plus tool-specific fields, failures as `{"ok": false, "reason": ...}`, on both the text content and the structured content channels (absent optional fields may appear as `null` in structured content).

| Tool | Purpose |
| --- | --- |
| `search_apis` | Entity-graph cross-product search (workflow step 0): when the user intent doesn't name a product, returns candidate products + representative APIs + `matched_via` evidence (aliases / colloquial keywords / tags); ranking = tantivy BM25 over API text (in-RAM index built at startup) + hand-tuned identity signals; deprecated-API governance mirrors `list_apis` (annotate / hide); build-time snapshot, mounted via `--entity-index` |
| `list_products` | Full Huawei Cloud product catalog — identifier, display name, category, product link; `keyword`/`category` filter |
| `get_product` | One product's details (classification, API count, global vs regional) |
| `list_apis` | A product's API directory with `tag_groups` overview; `tag`/`search`/`limit`/`offset` to narrow |
| `get_api` | One API's full documentation (parameters, required fields, enums, constraints) — read before executing. Oversized docs (>200k chars) spill the full envelope to disk and stub the heaviest fields in the response |
| `get_api_examples` | Official request examples for one API |
| `execute_api` | Execute one API: path/query params flattened, request body under `body`; errors come back structured, 429 retried with backoff. Oversized responses (>200k chars) are spilled to disk automatically: the result carries a `spill` envelope (`path`/`format`/`bytes`/`note`) and `body` keeps a truncated preview; `_spill=false` opts out per call. `_jsonpath` projects the response body via JSONPath (e.g. `$.servers[*].id` or `{"id": "$.servers[0].id"}`): on full hit `body` is replaced with the projected value, on miss the original body is kept plus `extract.misses` hints. On GET requests, a required query param the caller omitted is auto-filled from the metadata default (e.g. DNS `ListPrivateZones` fills `type=private`); the result reports what was filled via `applied_defaults`, explicit values are never overridden |
| `manage_policy` | Read/add/remove safety-policy rules at runtime (hot effect, no restart); `line` accepts a single rule or an array for batch add/remove |

## Tools (data mode)

Local analytics and transformation on DataFusion (optional extra: `pip install "huaweicloud-open-mcp[datafusion]"`; the tools return a friendly install hint when missing).

| Tool | Purpose |
| --- | --- |
| `query_data` | Read-only SQL over named tables: `{"name": {"data": [objects]}}` (inline) or `{"name": {"path": "file"}}` (local csv/parquet/jsonl/json-array, format auto-detected by extension); returns column schema + JSON-safe rows with row-count/char-budget truncation |
| `transform_data` | Persist a read-only SQL transformation to a new data file: `out = {"path", "format"?}` (csv/parquet/jsonl), atomic write, refuse-overwrite by default (`overwrite=true` to allow); returns artifact metadata (path/format/rows/bytes) + a small preview |

Strictly read-only SQL: only `SELECT`/`WITH`/`EXPLAIN`/`SHOW`/`DESCRIBE` statements pass the guard; multi-statement and write statements (`INSERT`/`CREATE`/`COPY TO`/…) are rejected. In `transform_data` the engine applies the write *after* the guard, via the structured `out` parameter (audit NDJSON records the write path), never via SQL. Both tools touch no cloud APIs, need no credentials and are not subject to the safety policy. Deploy them only where the agent session is trusted to read (and, for `transform_data`, write) local files.

## Safety policy

A policy file is a JSON array (or plain text) of rules. The gateway evaluates rules top-down; the first match wins:

```json
[
  "ECS:*List*=allow",
  "VPC:*Show*=allow",
  "*=deny"
]
```

- Rules have the form `product:apiPattern=allow|deny`. Matching uses fnmatch-style wildcards, is case-insensitive for product and API, and lines starting with `#` are comments.
- With no `--policy` configured, every execution is denied.
- Grant scopes (via `manage_policy` add):
  - `once`: single execution, burned after use
  - `session` (default): this agent session only
  - `temporary`: TTL-based expiry
  - `permanent`: written to the policy file
- Batch grants: `line` also accepts an array of rules. The whole batch shares one scope; batch `add` is all-or-nothing (any invalid line rejects the entire batch, nothing applied); batch `remove` is per-rule best-effort (misses are reported per item). The batch envelope carries per-rule `results`; top-level `ok` is true only when every rule succeeded.
- Everything hot-reloads: external edits to the file apply immediately, and so do add/remove via `manage_policy`. Grant minimal rules first (`once`/`session`), product-wide only when justified.
- Denials return an actionable reason. With `--elicitation auto|required`, the server proposes a grant over MCP elicitation. Five choices:
  - `api`: minimal rule, one-shot
  - `api_session`: minimal rule, session-scoped
  - `product`: product-wide, session-scoped
  - `readonly`: product read-only rule set (`*List*/*Show*/*Get*/*Query*`, session-scoped; preferred for browsing tasks)
  - `none`: no grant
- The default is `off` for predictable cross-client behavior.

A richer example ships with the package: `configs/safety-policy.example.json`.

## Custom hints (optional)

A hints file lets a deployment inject its own guidance into the discovery chain: a global `instructions` block appended to the server instructions, plus per-product `notes` and per-API texts attached to discovery results (`list_products` / `get_product` / `list_apis` / `get_api`). A product-level SOP appears on the `get_product` / `list_apis` top-level envelopes as `sops_index`, a lightweight index (task name and description only), while the full rendered `sops` text (with steps) is attached only when the caller passes `include_sops=true`.

```json
{
  "instructions": "This deployment targets ops inspection: prefer List*/Show* APIs for batch lookups.",
  "products": {
    "ECS": {
      "notes": "Prefer ListServersDetails for listing servers.",
      "sops": {
        "Create a server": ["ListFlavors to check availability", "CreateServers to provision", "ShowServer to poll status=ACTIVE"],
        "Resize a server": {
          "description": "Online resize procedure (discovery exposes name + description only; steps via include_sops)",
          "steps": ["ShowServer to confirm", "ResizeServer to submit", "ConfirmResize to finalize"]
        }
      },
      "apis": {
        "ResizeServer": "Check flavor availability with ListFlavors first."
      }
    },
    "OBS": "Object upload/download always returns a presign envelope; the gateway never moves bytes."
  }
}
```

- Official metadata is never replaced; hints are returned in an extra `hints` field (product and API notes merged, product first).
- Product-level SOP (`sops`, optional): mapping-only (tasks must be named); a task value is a string (verbatim), an array of strings (auto-numbered `1. x`, blank steps dropped and numbering compacted), or a dict `{"description"?: str, "steps"?: string | array[string]}` (key whitelist `{description, steps}`, unknown keys fail fast; a task without `steps` is description-only). Discovery envelopes (`get_product` / `list_apis` top-level) always carry `sops_index`, a lightweight `[{name, description?}]` list (no steps); the full rendered text (`Task:\ncontent`, description first, blank line between tasks) comes as the extra `sops` string field, attached only when the tool is called with `include_sops=true` (default `false`; no-op when the product has no SOP configured). List items and `get_api` / `get_api_examples` / `execute_api` are never annotated.
- Injected only on successful discovery results, never on denials (policy rejections stay untouched); `get_api_examples` and `execute_api` are never annotated.
- Product keys and `apis` keys are case-insensitive; a product value may be a plain string (product note only) or an object with `notes` / `apis` / `sops`.
- Optional top-level boolean `api_notes_in_list_apis` (default `true`): when `false`, `list_apis` items carry no API-level notes (top-level product notes and `get_api` merged notes are unaffected). The generated help-center completion file sets this to `false`, so `get_api` remains the only response that carries these notes.
- Loaded at startup and hot-reloaded from the config file at runtime (no restart needed); invalid configs fail fast at startup.
- Default file: with no `--hints` and no `HUAWEICLOUD_MCP_OPENAPI_HINTS`, `configs/help-docs-hints.json` (repo-root copy first, then the one bundled in the installed package) is loaded automatically; if absent it is skipped silently. Pass `--hints off` (or an empty env value) to disable explicitly. Explicit paths/bare names keep fail-fast semantics on a missing file.
- The file path supports a bare filename: an existing explicit path (absolute or cwd-relative) is used as-is; otherwise it resolves as `configs/<name>` (repo-root `configs/` first, then the copy bundled in the installed package, handy for `uvx`/`pip` installs).
- Curated seed: product entries in `configs/help-docs-hints-curation.json` may carry `sops` (passed through as a raw mapping); curated entries survive `api-refresh helphints` regeneration.

Example: `configs/openapi-hints.example.json` (loadable as `--hints openapi-hints.example.json`).

### Help-center description completion (optional, build-time)

`api-refresh` ships two extra stages (not part of the default refresh range) that collect the richer "功能介绍" sections from the official help center and turn them into a hints file:

```bash
uv run api-refresh helpdocs    # crawl + parse help-center pages (sitemap seeds + same-docset link BFS, resumable)
uv run api-refresh helphints   # match to apiexplorer APIs, diff, emit data/help_completions/ + data/hints/help-docs-hints.json
uv run huaweicloud-open-mcp --hints data/hints/help-docs-hints.json
```

### Entity graph and `search_apis` (build-time)

`api-refresh graph` deterministically builds the entity association graph from `apis_docs.json` and `huawei_products.json` (product/API/tag nodes, same-name twins, attributions, tag coverage), merged with the `configs/entity-knowledge.json` knowledge base. With `--llm`, the build also runs LLM extraction of colloquial aliases, product semantic relations and per-API query keywords (`ENTITY_LLM_BASE_URL/API_KEY/MODEL`; fingerprint-incremental reruns touch only changed products; curated entries are never overwritten):

```bash
uv run api-refresh graph            # deterministic build (data/graph/entity-index.json + configs copy shipped in wheel)
uv run api-refresh graph --llm      # build-time LLM extraction (~150 calls, resumable via fingerprints)
uv run huaweicloud-open-mcp         # runtime defaults to configs/entity-index.json (silently disabled if missing)
```

At runtime `search_apis` feeds the snapshot through a tantivy BM25 engine. The engine tokenizes the ~18k API texts (ASCII words and camelCase splits; CJK 2-grams) into an in-RAM index built once at startup (~0.6s), answers queries in a few ms, and merges a hand-tuned identity layer (aliases, product names, discriminative tags, product-breadth prior) with the BM25 scores. A human-labeled golden set (`tests/fixtures/entity_eval.json`, 45 cases) guards ranking quality and runs as part of the test suite.

- Diff-only: an API enters the file only when the help-center intro is materially richer than the API Explorer description (`--min-gain`, default 20 chars).
- Each note carries the full intro (capped via `--cap`, default 2000 chars, `…` marker) plus the official doc URL.
- Rate-limited crawl (0.4s/page) with bot-verification backoff and resumable checkpoints; artifacts are rebuildable and not committed. See [AGENTS.md](AGENTS.md) for the full pipeline rules.
- `--hints` / `--deprecated-index` accept bare filenames resolved against `configs/` (repo root first, then the bundled package copy).

The same pipeline emits a deprecated-API index (`data/help_completions/deprecated.json`) sourced from the help center's own `（废弃）` titles. API Explorer's `op.deprecated` metadata is unreliable here: in the ECS pilot, the help center surfaces 45 deprecated ECS APIs while API Explorer flags only 1. Mount the index to govern the `list_apis` discovery surface:

```bash
uv run huaweicloud-open-mcp --deprecated-index data/help_completions/deprecated.json                 # annotate (default): list_apis items carry deprecated: true + replacement
uv run huaweicloud-open-mcp --deprecated-index ... --deprecated-mode hide                            # hide: deprecated APIs are filtered from list_apis (counts stay coherent)
```

- `annotate`/`hide` only affect the `list_apis` discovery surface; `get_api`/`execute_api` always work. A narrowed discovery list does not refuse detail.
- `off`: no governance even with the index mounted. Passing an explicit mode without `--deprecated-index` fails fast at startup.
- Without `--deprecated-index`, behavior is byte-for-byte unchanged (no mode means no governance).

## Configuration

### CLI flags

| Flag | Default | Description |
| --- | --- | --- |
| `--mode <modes>` | `openapi` | Run mode(s), comma-separated (`openapi`/`discover`/`data`, e.g. `openapi,data`; env `HUAWEICLOUD_MCP_MODE`) |
| `--transport stdio\|http` | `stdio` | Transport: stdio (default, client-launched local form) or `http` (Streamable HTTP, single process serving multiple MCP sessions; `streamable-http` normalizes to `http`; invalid values fail fast; env `HUAWEICLOUD_MCP_TRANSPORT`) |
| `--http-host <addr>` | `127.0.0.1` | HTTP bind address (loopback auto-enables DNS-rebinding protection; non-loopback warns — see [HTTP transport](#http-transport-multi-session); env `HUAWEICLOUD_MCP_HTTP_HOST`) |
| `--http-port <port>` | `8000` | HTTP port (env `HUAWEICLOUD_MCP_HTTP_PORT`) |
| `--http-path <path>` | `/mcp` | HTTP MCP endpoint path, must start with `/` (env `HUAWEICLOUD_MCP_HTTP_PATH`) |
| `--mock` | off | Point `execute_api` at the API Explorer mock endpoint (no credentials needed) |
| `--mock-base <url>` | — | Mock endpoint base URL override (env `HUAWEICLOUD_MCP_MOCK_BASE`) |
| `--mock-passthrough` | off | Mock mode: forward execute business params to the mock endpoint (env `HUAWEICLOUD_MCP_MOCK_PASSTHROUGH`) |
| `--policy <file>` | — | Safety policy file; missing → all executions denied |
| `--region <id>` | `cn-north-4` | Default region |
| `--hints <file\|off>` | `configs/help-docs-hints.json` (silent skip if absent) | Optional custom-hints file (deploy-side guidance injected into instructions and discovery results); `off` disables; bare filename resolves against `configs/` |
| `--deprecated-index <file>` | — | Optional deprecated-API index; enables `list_apis` annotate/hide governance; bare filename resolves against `configs/` |
| `--deprecated-mode <annotate\|hide\|off>` | `annotate` (when index configured) | `annotate` = `list_apis` items carry `deprecated: true` + `replacement` (counts unchanged); `hide` = deprecated APIs filtered from `list_apis` before pagination (counts stay coherent); `off` = no governance; affects `list_apis` only — `get_api`/`execute_api` always work; explicit mode requires `--deprecated-index` (env `HUAWEICLOUD_MCP_DEPRECATED_MODE`) |
| `--elicitation auto\|required\|off` | `off` | MCP-elicitation confirmation for policy changes |
| `--spill-dir <dir>` | system temp dir (`hwc-mcp-spill`) | Where oversized responses/envelopes are spilled (empty or `off` disables spilling; pure truncation returns) |
| `--auth-demote on\|off` | `on` | Auth-header `required` metadata normalization: at conversion time, `required` is demoted to `false` for x-auth-token/x-security-token/authorization (case-insensitive) — auth is always supplied by this gateway's AK/SK signing layer, agents never fabricate tokens; `off` keeps metadata as-is (env `HUAWEICLOUD_MCP_AUTH_DEMOTE`) |
| `--auth-demote-pass <list>` | — | Auth-demote exemption list: comma-separated `PRODUCT:API` (exact) or `PRODUCT`/`PRODUCT:*` (product-wide); exempted APIs keep their original metadata; affects metadata normalization only, not the validation layer (env `HUAWEICLOUD_MCP_AUTH_DEMOTE_PASS`) |
| `--metadata-corrections <path>` | bundled `configs/metadata-corrections.json` | Metadata corrections: doc-verified factual fixes for stale upstream metadata (first cases: `RDS:StartupInstance` x-constraint, `FunctionGraph:CreateEvent` malformed `name.pattern`), exact `PRODUCT:API` keys with `drop` (line-level) / `replace` (whole-field) patches on envelope/op fields, plus doc-level JSON Pointer patches (v1 whitelist `/definitions/`) applied copy-on-write before `get_api` formatting and `execute_api` validation — cached docs are never mutated; bare filenames resolve like `--hints` (env `HUAWEICLOUD_MCP_METADATA_CORRECTIONS`) |
| `--audit-file <file>` | disabled | Audit trail (NDJSON): one `{ts, tool, input, ok}` line per tool call |
| `--log-level` / `--log-file` | `INFO` / `logs/huaweicloud-open-mcp.log` | Logging (rotating file; stderr mirrors WARNING+) |

### Environment variables

| Variable | Purpose |
| --- | --- |
| `HUAWEICLOUD_SDK_AK` / `HUAWEICLOUD_SDK_SK` | Access key / secret key (real mode); see [Credentials](#credentials) for the profile-file alternative |
| `HUAWEICLOUD_SDK_SECURITY_TOKEN` | Optional temporary-security-credential token; attached as `X-Security-Token` (and `x-obs-security-token` for OBS) before signing, and included in the request signature |
| `HUAWEICLOUD_SDK_PROJECT_ID` | Optional static project ID (substituted into `{project_id}` path parameters and the `X-Project-Id` header) |
| `HUAWEICLOUD_SDK_DOMAIN_ID` | Optional; loaded for global-level services (full support in progress) |
| `HUAWEICLOUD_MCP_MODE` | Same as `--mode` |
| `HUAWEICLOUD_MCP_TRANSPORT` | Same as `--transport` |
| `HUAWEICLOUD_MCP_HTTP_HOST` / `HUAWEICLOUD_MCP_HTTP_PORT` / `HUAWEICLOUD_MCP_HTTP_PATH` | Same as `--http-host` / `--http-port` / `--http-path` |
| `HUAWEICLOUD_MCP_REGION` | Same as `--region` |
| `HUAWEICLOUD_MCP_MOCK` | Same as `--mock` (`1`/`true`/`yes`) |
| `HUAWEICLOUD_MCP_MOCK_BASE` | Mock endpoint base URL override |
| `HUAWEICLOUD_MCP_MOCK_PASSTHROUGH` | Same as `--mock-passthrough` |
| `HUAWEICLOUD_MCP_POLICY_FILE` | Same as `--policy` |
| `HUAWEICLOUD_MCP_OPENAPI_HINTS` | Same as `--hints` |
| `HUAWEICLOUD_MCP_DEPRECATED_INDEX` | Same as `--deprecated-index` |
| `HUAWEICLOUD_MCP_DEPRECATED_MODE` | Same as `--deprecated-mode` |
| `HUAWEICLOUD_MCP_AUDIT_FILE` | Same as `--audit-file` |
| `HUAWEICLOUD_MCP_SPILL_DIR` | Same as `--spill-dir` |
| `HUAWEICLOUD_MCP_AUTH_DEMOTE` | Same as `--auth-demote` (`on`/`off`) |
| `HUAWEICLOUD_MCP_AUTH_DEMOTE_PASS` | Same as `--auth-demote-pass` |
| `HUAWEICLOUD_MCP_METADATA_CORRECTIONS` | Same as `--metadata-corrections` |
| `HUAWEICLOUD_MCP_ELICIT` | Same as `--elicitation` |
| `HUAWEICLOUD_MCP_LOG_LEVEL` / `HUAWEICLOUD_MCP_LOG_FILE` | Same as `--log-level` / `--log-file` |

## Credentials

The gateway loads AK/SK from two sources, checked in order: environment variables first, then `~/.huaweicloud/credentials` (on Windows: `%USERPROFILE%\.huaweicloud\credentials`). When both are set, environment variables win.

### Option A: Profile file (quick-start main path)

`~/.huaweicloud/credentials`, exactly what [Step 1](#step-1-provide-credentials) creates:

```ini
[basic]
ak = your-access-key-id
sk = your-secret-access-key

# optional — uncomment as needed:
# security_token = <temporary-security-token>
# project_id = <project-id>
# domain_id = <domain-id>
```

- `[basic]` section with `ak` and `sk` is required; the three optional keys mirror the environment variables below.
- A missing file is silently skipped; the server simply runs without credentials (metadata tools keep working; see below).
- Keep it private; the file holds your secret: run `chmod 600 ~/.huaweicloud/credentials` on macOS/Linux; on Windows a file in your user profile is only readable by your account by default.

### Option B: Environment variables (inline in client registration)

Set them in the client registration instead of the profile file:

**opencode**: add an `environment` block next to `command`:

```json
"environment": {
  "HUAWEICLOUD_SDK_AK": "your-access-key-id",
  "HUAWEICLOUD_SDK_SK": "your-secret-access-key"
}
```

**Codex**: add `--env` flags before `--` (single line, works in any shell):

```
codex mcp add huaweicloud --env HUAWEICLOUD_SDK_AK=your-access-key-id --env HUAWEICLOUD_SDK_SK=your-secret-access-key -- uvx huaweicloud-open-mcp --policy /home/you/hwc-policy.json
```

| Variable | Purpose |
| --- | --- |
| `HUAWEICLOUD_SDK_AK` / `HUAWEICLOUD_SDK_SK` | Required pair |
| `HUAWEICLOUD_SDK_SECURITY_TOKEN` | Optional temporary-security-credential token; attached as `X-Security-Token` (and `x-obs-security-token` for OBS) before signing, and included in the request signature |
| `HUAWEICLOUD_SDK_PROJECT_ID` | Optional static project ID (substituted into `{project_id}` path parameters and the `X-Project-Id` header) |
| `HUAWEICLOUD_SDK_DOMAIN_ID` | Optional; loaded for global-level services (full support in progress) |

### Behavior notes

- Without credentials, metadata tools (`list_products`, `list_apis`, `get_api`, …) keep working; their data comes from the public API Explorer. Only `execute_api` needs credentials.
- Use a dedicated minimal-privilege IAM sub-user's AK/SK. The gateway can then only do what that user could do anyway.
- Signing happens locally; the SK never leaves your machine, and credentials never appear in logs.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Client shows "failed to connect" or the server exits immediately | `--policy` path is relative or the file is missing — the server fails fast on a bad policy file | Use an absolute path to a file that exists |
| The gateway tools never appear in the agent | `uvx` is not on the client's PATH | Find it with `which uvx` (macOS/Linux) or `where uvx` (Windows) and use the absolute path in place of `uvx` in the command |
| Metadata tools work but `execute_api` fails | Credentials not loaded (empty `[basic]` section, or env vars shadow an empty file) | Fix `~/.huaweicloud/credentials` (see [Credentials](#credentials)) or set the `HUAWEICLOUD_SDK_*` environment variables |
| `execute_api` returns `{"ok": false, "reason": ...}` mentioning policy | The API is not allowed by the policy file (the quick-start file allows only `ECS:*List*`) | Edit the policy file — changes hot-reload without restarting the server — or, after confirming with the user, have the agent add a rule via `manage_policy` |
| 401 / SignatureDoesNotMatch | Wrong AK or SK | Recheck the credentials source in use (env wins over profile file) |
| 403 from Huawei Cloud with a permission error | The IAM user lacks the permission | Grant the minimal IAM policy needed for that API |
| `"count": 0` but you have servers | Resources live in another region | Ask again with an explicit `region`, e.g. `cn-east-3` |
| Mock calls hang or time out | No network route to the API Explorer endpoint | Check proxy/firewall access to `apiexplorer.cn-north-4.myhuaweicloud.com` |

For deeper diagnosis, add `--log-level DEBUG --log-file <log>` to the registration command, for example `/tmp/hwc-mcp.log` on macOS/Linux or `%TEMP%\hwc-mcp.log` on Windows, and inspect the log file.

## Documentation

Explore the design behind the gateway:

| Document | Type | Language |
| --- | --- | --- |
| [docs/architecture.md](docs/architecture.md) | Design overview (layers, modules, logging, tests) | 中文 |
| [docs/mcp-openapi.md](docs/mcp-openapi.md) | openapi-mode design (workflow, signing, OBS lane) | 中文 |
| [AGENTS.md](AGENTS.md) | Contributor conventions (TDD seams) | 中文 |
| [benchmarks/README.md](benchmarks/README.md) | Workflow-benchmark design | 中文 |

## Development

```bash
uv sync                                  # deps (incl. dev)
uv run huaweicloud-open-mcp              # run from source
uv run pytest                            # unit + integration (e2e skipped by default)
uv run pytest -m e2e                     # real-credential E2E
uv run ruff check src tests              # lint
uv run mypy src                          # type check
```

Companion CLIs: `api-refresh` (offline APIE pipeline that turns API Explorer data into OpenAPI 2.0 docs; `graph` builds the entity graph with `--llm` semantic extraction; help-center completion stages `helpdocs`/`helphints`) and `api-docs` (metadata queries from the terminal). Details in [AGENTS.md](AGENTS.md).

## License

[Apache-2.0](LICENSE)
