Skip to content

Commit a97856e

Browse files
authored
Merge pull request #4 from vngcloud/feat/dynamic-spec-registry
feat: dynamic OpenAPI spec registry (replace bundled specs)
2 parents b79fdcb + 0663b1a commit a97856e

38 files changed

Lines changed: 2813 additions & 6792 deletions

.github/workflows/release.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ jobs:
7070
exit 1
7171
fi
7272
73+
- name: Bake docs portal URL
74+
working-directory: src/greenode-mcp-server
75+
env:
76+
DOCS_PORTAL_URL: ${{ vars.DOCS_PORTAL_URL || 'https://docs.api.vngcloud.vn' }}
77+
run: |
78+
cat > greennode/greenode_mcp_server/_build_info.py <<EOF
79+
"""Build-time configuration baked by CI."""
80+
from __future__ import annotations
81+
82+
DEFAULT_DOCS_PORTAL_URL = "${DOCS_PORTAL_URL}"
83+
EOF
84+
echo "Baked DEFAULT_DOCS_PORTAL_URL=${DOCS_PORTAL_URL}"
85+
7386
- name: Build package
7487
working-directory: src/greenode-mcp-server
7588
run: uv build

.github/workflows/run-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ jobs:
4040
run: |
4141
uv pip install ruff
4242
uv run ruff check greennode/
43+
44+
- name: MCP protocol smoke test
45+
working-directory: src/greenode-mcp-server
46+
run: python3 scripts/mcp_protocol_smoke.py

CLAUDE.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
GreenNode MCP Servers provide AI assistants (Claude, Cursor, Gemini, etc.) with tools to manage GreenNode services via the Model Context Protocol.
66

7-
- **Single server**`greenode-mcp-server` covers all products via bundled OpenAPI specs
7+
- **Single server**`greenode-mcp-server` covers all VNG Cloud products via a spec registry (`docs.api.vngcloud.vn`, fetched at startup, cached at `~/.greenode/mcp-specs/`)
88
- **Namespace package**`greennode.greenode_mcp_server`
99

1010
### Available servers
1111

1212
| Server | Package | Tools |
1313
|--------|---------|-------|
14-
| GreenNode MCP Server | `greennode.greenode_mcp_server` | search_api, call_api, 6 K8s tools |
14+
| GreenNode MCP Server | `greennode.greenode_mcp_server` | `search_api`, `call_api`, 6 K8s tools (`list_k8s_resources`, `get_pod_logs`, `get_k8s_events`, `list_api_versions`, `manage_k8s_resource`, `apply_yaml`) |
1515

1616
## Repository structure
1717

@@ -20,9 +20,11 @@ greenode-mcp/
2020
├── src/
2121
│ └── greenode-mcp-server/
2222
│ ├── pyproject.toml
23-
│ ├── specs/ # Bundled OpenAPI specs (*.json)
2423
│ ├── greennode/
2524
│ │ └── greenode_mcp_server/
25+
│ │ ├── registry/ # Spec registry (provider + cache + loader)
26+
│ │ ├── _build_info.py # Build-time baked DEFAULT_DOCS_PORTAL_URL
27+
│ │ └── ...
2628
│ └── tests/
2729
├── scripts/
2830
├── docs/
@@ -43,8 +45,12 @@ greenode-mcp/
4345
## VNG Cloud API quirks
4446

4547
- **IAM API uses camelCase**: `grantType`, `accessToken`, `expiresIn` (not snake_case OAuth2 standard)
46-
- **VKS API pagination is 0-based**: page 0 = first page
48+
- **Pagination is 1-based across VNG Cloud APIs**: page 1 = first page (standard convention for all products)
4749
- **API returns 202** for most successful operations (not 200)
50+
- **List response wrapper keys vary**: `items`, `listData`, `data`, `results`, `records` — the formatter in `api_caller.py` recognises all of them
51+
- **`{projectId}` / `{project_id}` path placeholders** are auto-substituted by `call_api` from `config.default_project_id` (set by `grn configure` or `GRN_DEFAULT_PROJECT_ID` env var)
52+
- **K8s `api_version`** is optional for common kinds (Pod, Deployment, PVC, ...) via `COMMON_API_VERSIONS` in `k8s_handler.py`; custom resources still need it explicit
53+
- **VKS kubeconfig endpoint** returns `{kubeConfig: "<yaml>", status: "ACTIVE"|"CREATING"|...}` — not raw YAML. `k8s_client_cache.py` extracts the `kubeConfig` field and checks `status`
4854

4955
## Adding a new tool (to existing server)
5056

@@ -57,7 +63,7 @@ greenode-mcp/
5763

5864
## Adding a new MCP server (new product)
5965

60-
The greenode-mcp-server now covers all products via bundled OpenAPI specs, so adding a new product typically means adding a new spec file to `src/greenode-mcp-server/specs/` rather than creating a separate server.
66+
The greenode-mcp-server covers all products via the spec registry (`docs.api.vngcloud.vn`). Adding a new product = VNG Cloud team publishes the product's OpenAPI page on the docs portal. The server picks it up on next restart — no code change, no release needed.
6167

6268
If a truly separate server is needed:
6369

@@ -74,8 +80,10 @@ See `src/greenode-mcp-server/` as reference.
7480

7581
- **Input validation**: All resource IDs validated via `validators.validate_id()` before URL construction — prevents path traversal
7682
- **Write guard**: Mutating operations must check `self.allow_write` flag
77-
- **Sensitive data guard**: K8s Secret reads must check `self.allow_sensitive_data_access`
78-
- **Credential env vars supported**: `GRN_ACCESS_KEY_ID`/`GRN_SECRET_ACCESS_KEY` override credentials file (highest priority)
83+
- **Sensitive data guard**: Only K8s Secret reads check `self.allow_sensitive_data_access`. Pod logs and K8s events are NOT guarded — they're routine debug reads
84+
- **Credential env vars supported**: `GRN_ACCESS_KEY_ID`/`GRN_SECRET_ACCESS_KEY` override credentials file; `GRN_DEFAULT_PROJECT_ID` overrides config file `project_id` (highest priority)
85+
- **Response size cap**: `call_api` rejects responses > 800 KB with an actionable error — prevents context blow-up
86+
- **Row cap**: list responses truncate at 100 rows by default (with a footer telling the caller to paginate)
7987
- **Tokens in memory only**: Never written to disk or logged
8088
- **Credentials not logged**: Error messages and debug logs never include tokens or secrets
8189
- **Timeout**: All HTTP requests have 30s timeout
@@ -92,9 +100,10 @@ uv sync --all-extras
92100
uv run python -m pytest tests/ -v
93101
```
94102

95-
- 65 tests for GreenNode MCP Server
103+
- ~175 tests for GreenNode MCP Server
96104
- Uses `respx` for mocking async HTTP calls
97105
- Uses `pytest-asyncio` for async test support
106+
- MCP protocol smoke test: `python3 scripts/mcp_protocol_smoke.py` (runs the server via stdio, walks initialize → tools/list → tools/call)
98107

99108
## Git workflow
100109

@@ -131,10 +140,17 @@ Code without docs is not done.
131140

132141
| File | Purpose |
133142
|------|---------|
134-
| `greennode/greenode_mcp_server/server.py` | FastMCP entry point, tool registration, CLI flags |
135-
| `greennode/greenode_mcp_server/api_index.py` | Spec loader, in-memory index, keyword search |
143+
| `greennode/greenode_mcp_server/server.py` | FastMCP entry point, tool registration, CLI flags (`--refresh-specs`, `--offline`, ...) |
144+
| `greennode/greenode_mcp_server/api_index.py` | In-memory endpoint index, keyword search; delegates loading to `registry/` |
136145
| `greennode/greenode_mcp_server/api_caller.py` | call_api tool — write guard, auth injection, response formatting |
137-
| `greennode/greenode_mcp_server/config.py` | Config loading, REGIONS dict |
146+
| `greennode/greenode_mcp_server/_build_info.py` | Build-time baked `DEFAULT_DOCS_PORTAL_URL` (CI overwrites at release) |
147+
| `greennode/greenode_mcp_server/registry/provider.py` | `SpecProvider` Protocol, `ProductRef`, error types |
148+
| `greennode/greenode_mcp_server/registry/factory.py` | Selects active provider (swap here to migrate sources) |
149+
| `greennode/greenode_mcp_server/registry/redocly_portal.py` | Default provider — scrapes docs portal inline OpenAPI JSON |
150+
| `greennode/greenode_mcp_server/registry/local_dir.py` | Dev/test provider — reads specs from `GRN_MCP_SPEC_DIR` |
151+
| `greennode/greenode_mcp_server/registry/cache.py` | On-disk spec cache at `~/.greenode/mcp-specs/` with TTL + ETag |
152+
| `greennode/greenode_mcp_server/registry/loader.py` | Orchestrator — ties provider + cache + CLI flags together |
153+
| `greennode/greenode_mcp_server/config.py` | `GreenodeConfig` loader — reads `~/.greenode/credentials` + `~/.greenode/config` (incl. `project_id`), env-var overrides, REGIONS dict |
138154
| `greennode/greenode_mcp_server/auth.py` | TokenManager — async OAuth2 with auto-refresh |
139155
| `greennode/greenode_mcp_server/client.py` | GreenodeClient — used by K8s handler for kubeconfig fetch |
140156
| `greennode/greenode_mcp_server/k8s_handler.py` | 6 K8s tools |
@@ -148,7 +164,9 @@ Both projects share:
148164
- Same config files (`~/.greenode/credentials`, `~/.greenode/config`)
149165
- Same REGIONS dict (HCM-3, HAN endpoints)
150166
- Same IAM auth flow (camelCase fields)
151-
- Same env var names (`GRN_ACCESS_KEY_ID`, `GRN_SECRET_ACCESS_KEY`, `GRN_PROFILE`, `GRN_DEFAULT_REGION`, etc.)
167+
- Same env var names (`GRN_ACCESS_KEY_ID`, `GRN_SECRET_ACCESS_KEY`, `GRN_PROFILE`, `GRN_DEFAULT_REGION`, `GRN_DEFAULT_PROJECT_ID`)
168+
- Same profile section convention (`[default]` vs `[profile <name>]` in the config file, AWS-style)
169+
- `grn configure` auto-detects `project_id` and writes it to `~/.greenode/config` — MCP reads that value without a separate API call
152170

153171
Key differences:
154172
- greenode-mcp is **async**, greenode-cli is **sync**

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,23 @@ The [Model Context Protocol](https://modelcontextprotocol.io/) lets AI assistant
2626
export GRN_ACCESS_KEY_ID=your-client-id
2727
export GRN_SECRET_ACCESS_KEY=your-client-secret
2828
export GRN_DEFAULT_REGION=HCM-3
29+
export GRN_DEFAULT_PROJECT_ID=pro-xxxxxxxx # required for many APIs that embed project in the URL
2930
```
3031

31-
**Option B: Credentials file (via GreenNode CLI)**
32+
**Option B: GreenNode CLI (recommended)**
3233

3334
```bash
34-
pip install grncli
3535
grn configure
3636
```
3737

38-
This creates `~/.greenode/credentials` which all MCP servers read automatically. Environment variables take priority over the credentials file.
38+
The wizard auto-detects your `project_id` and writes:
3939

40-
> **Note:** All MCP servers require credentials configured via one of these methods.
40+
- `~/.greenode/credentials``client_id`, `client_secret`
41+
- `~/.greenode/config``region`, `project_id`, `output`
42+
43+
See [greenode-cli](https://github.com/vngcloud/greennode-cli).
44+
45+
Environment variables take priority over the files. All MCP servers in this repo read these same paths and env vars.
4146

4247
## Quick Start
4348

@@ -68,9 +73,8 @@ greenode-mcp/
6873
│ └── greenode-mcp-server/ # GreenNode MCP Server
6974
│ ├── README.md # Server-specific docs, tools, security
7075
│ ├── pyproject.toml # Package config + dependencies
71-
│ ├── specs/ # Bundled OpenAPI specs (*.json)
7276
│ ├── greennode/
73-
│ │ └── greenode_mcp_server/ # Source code
77+
│ │ └── greenode_mcp_server/ # Source code (incl. registry/ module)
7478
│ └── tests/ # Test suite
7579
├── scripts/ # Release scripts
7680
├── docs/ # Development guide
@@ -96,7 +100,7 @@ See [GreenNode MCP Server](src/greenode-mcp-server/) as reference.
96100
All GreenNode MCP servers share these security principles:
97101

98102
- **Read-only by default** — Write operations require explicit `--allow-write` flag
99-
- **Sensitive data protection** — Kubernetes Secrets require `--allow-sensitive-data-access`
103+
- **Sensitive data protection**Reading Kubernetes Secrets requires `--allow-sensitive-data-access`; other K8s reads (pods, deployments, logs, events) are allowed by default
100104
- **Credential security**`~/.greenode/credentials` stored with `0600` permissions
101105
- **Input validation** — All resource IDs validated to prevent path traversal
102106
- **Token handling** — In memory only, never written to disk or logged

0 commit comments

Comments
 (0)