@mguttmann/hetzner-cloud-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.env.example +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +148 -0
  4. package/dist/config.js +53 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/http/action-polling.js +34 -0
  7. package/dist/http/action-polling.js.map +1 -0
  8. package/dist/http/client.js +94 -0
  9. package/dist/http/client.js.map +1 -0
  10. package/dist/http/errors.js +36 -0
  11. package/dist/http/errors.js.map +1 -0
  12. package/dist/http/pagination.js +51 -0
  13. package/dist/http/pagination.js.map +1 -0
  14. package/dist/index.js +80 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/logger.js +12 -0
  17. package/dist/logger.js.map +1 -0
  18. package/dist/server.js +37 -0
  19. package/dist/server.js.map +1 -0
  20. package/dist/tools/_error.js +25 -0
  21. package/dist/tools/_error.js.map +1 -0
  22. package/dist/tools/confirm.js +46 -0
  23. package/dist/tools/confirm.js.map +1 -0
  24. package/dist/tools/generated/operations.js +49725 -0
  25. package/dist/tools/generated/operations.js.map +1 -0
  26. package/dist/tools/generated/tools.js +162 -0
  27. package/dist/tools/generated/tools.js.map +1 -0
  28. package/dist/tools/raw_request.js +87 -0
  29. package/dist/tools/raw_request.js.map +1 -0
  30. package/dist/tools/registry.js +19 -0
  31. package/dist/tools/registry.js.map +1 -0
  32. package/dist/tools/wait_action.js +37 -0
  33. package/dist/tools/wait_action.js.map +1 -0
  34. package/dist/tools/wrappers/_server_action.js +23 -0
  35. package/dist/tools/wrappers/_server_action.js.map +1 -0
  36. package/dist/tools/wrappers/apply_firewall_to_server.js +47 -0
  37. package/dist/tools/wrappers/apply_firewall_to_server.js.map +1 -0
  38. package/dist/tools/wrappers/server_backup.js +25 -0
  39. package/dist/tools/wrappers/server_backup.js.map +1 -0
  40. package/dist/tools/wrappers/server_change_type.js +27 -0
  41. package/dist/tools/wrappers/server_change_type.js.map +1 -0
  42. package/dist/tools/wrappers/server_get.js +53 -0
  43. package/dist/tools/wrappers/server_get.js.map +1 -0
  44. package/dist/tools/wrappers/server_list.js +69 -0
  45. package/dist/tools/wrappers/server_list.js.map +1 -0
  46. package/dist/tools/wrappers/server_metrics.js +55 -0
  47. package/dist/tools/wrappers/server_metrics.js.map +1 -0
  48. package/dist/tools/wrappers/server_power.js +32 -0
  49. package/dist/tools/wrappers/server_power.js.map +1 -0
  50. package/dist/tools/wrappers/server_rebuild.js +34 -0
  51. package/dist/tools/wrappers/server_rebuild.js.map +1 -0
  52. package/dist/tools/wrappers/server_rescue.js +34 -0
  53. package/dist/tools/wrappers/server_rescue.js.map +1 -0
  54. package/dist/tools/wrappers/server_snapshot.js +33 -0
  55. package/dist/tools/wrappers/server_snapshot.js.map +1 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +72 -0
  59. package/scripts/generate.ts +245 -0
  60. package/scripts/refresh-snapshot.ts +33 -0
  61. package/scripts/refresh-spec.ts +35 -0
  62. package/specs/cloud.spec.json +77624 -0
package/.env.example ADDED
@@ -0,0 +1,25 @@
1
+ # --- Hetzner Cloud API ----------------------------------------------------
2
+ # Project-bound API token with read & write permissions.
3
+ # How to create one: https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/
4
+ HETZNER_API_TOKEN=
5
+
6
+ # API base URL. The default is fine; only override for tests/mock servers.
7
+ HETZNER_API_BASE=https://api.hetzner.cloud/v1
8
+
9
+ # --- Optional write guard -------------------------------------------------
10
+ # false: write tools execute directly.
11
+ # true: write tools return a preview only unless `confirm: "YES"` is set.
12
+ HETZNER_CONFIRM_WRITES=false
13
+
14
+ # --- Logging --------------------------------------------------------------
15
+ # pino level: debug | info | warn | error
16
+ LOG_LEVEL=warn
17
+
18
+ # --- Timeouts / Polling ---------------------------------------------------
19
+ HTTP_TIMEOUT_MS=30000
20
+ ACTION_POLL_TIMEOUT_MS=60000
21
+ ACTION_POLL_INTERVAL_MS=2000
22
+
23
+ # --- Pagination Soft-Cap --------------------------------------------------
24
+ PAGINATION_MAX_ITEMS=500
25
+ PAGINATION_MAX_PAGES=10
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manuel Guttmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # the-real-hetzner-mcp
2
+
3
+ Local MCP server (stdio) that exposes the **Hetzner Cloud API 100% as tools — read and write**.
4
+
5
+ > Consistent with the UniFi-MCP and Lokka: Node/TypeScript, stdio transport, locally embedded connector.
6
+
7
+ ## Features
8
+
9
+ - Full coverage of the Hetzner Cloud API via codegen from `specs/cloud.spec.json` (189 generated tools + 12 hand-tuned wrappers = 201 tools total).
10
+ - 12 hand-tuned power wrappers for the common server operations.
11
+ - `hcloud_raw_request` as a generic escape hatch for any endpoint.
12
+ - 429 backoff, automatic pagination with a soft-cap, action polling with a 60-second timeout.
13
+ - Optional write guard `HETZNER_CONFIRM_WRITES=true` that returns a preview for destructive tools unless `confirm: "YES"` is set.
14
+
15
+ ## Out of Scope
16
+
17
+ Hetzner Robot, Hetzner DNS, Hetzner Storage Boxes (separate APIs).
18
+
19
+ ## Installation
20
+
21
+ ### As an npm package (recommended for MCP-client use)
22
+
23
+ ```bash
24
+ npm install -g @mguttmann/hetzner-cloud-mcp
25
+ ```
26
+
27
+ Then in your MCP client config, the `command` is just `hetzner-cloud-mcp` (no path needed).
28
+
29
+ ### From source
30
+
31
+ ```bash
32
+ git clone https://github.com/mguttmann/the-real-hetzner-mcp.git
33
+ cd the-real-hetzner-mcp
34
+ npm install
35
+ npm run build
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ ```bash
41
+ cp .env.example .env
42
+ # Add HETZNER_API_TOKEN to .env (Read & Write token from the Hetzner Cloud Console)
43
+ npm run refresh-spec
44
+ npm run generate
45
+ npm run build
46
+ ```
47
+
48
+ Optional smoke test against the real API (four read-only calls):
49
+
50
+ ```bash
51
+ npm run test:live
52
+ ```
53
+
54
+ ## MCP Connector Setup (Claude Desktop / Claude Code)
55
+
56
+ Append to your client's MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json` for Claude Desktop, or the equivalent in Claude Code).
57
+
58
+ ### Variant A — installed via npm
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "hetzner-cloud": {
64
+ "command": "hetzner-cloud-mcp",
65
+ "env": {
66
+ "HETZNER_API_TOKEN": "...",
67
+ "HETZNER_CONFIRM_WRITES": "false",
68
+ "LOG_LEVEL": "warn"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Variant B — from source
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "hetzner-cloud": {
81
+ "command": "node",
82
+ "args": ["/absolute/path/to/the-real-hetzner-mcp/dist/index.js"],
83
+ "env": {
84
+ "HETZNER_API_TOKEN": "...",
85
+ "HETZNER_CONFIRM_WRITES": "false",
86
+ "LOG_LEVEL": "warn"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Restart the client to pick up the change. `tools/list` should expose 201 tools whose names start with `hcloud_`.
94
+
95
+ ## Tools — overview
96
+
97
+ ### Hand-tuned Wrappers
98
+
99
+ | Tool | What it does |
100
+ |-----------------------------------|-------------------------------------------------------------------------------|
101
+ | `hcloud_list_servers` | Server list with `name_contains` filter and default sort by `name`. |
102
+ | `hcloud_get_server` | Fetch a server by `id` OR `name`. |
103
+ | `hcloud_server_power` | poweron / poweroff / reboot / shutdown / reset in a single tool. |
104
+ | `hcloud_server_rebuild` | Rebuild a server, `image_id` OR `image_name`. |
105
+ | `hcloud_server_snapshot` | `create_image` with default `type=snapshot`. |
106
+ | `hcloud_server_backup` | Toggle automatic backups. |
107
+ | `hcloud_server_rescue` | Toggle rescue mode, default `type=linux64`. |
108
+ | `hcloud_server_change_type` | Change server type, default `upgrade_disk=false`. |
109
+ | `hcloud_get_server_metrics` | Server metrics, `id` OR `name`, default time window "last hour". |
110
+ | `hcloud_apply_firewall_to_server` | Apply a firewall to a server, by-id OR by-name. |
111
+ | `hcloud_raw_request` | Arbitrary request against the API. |
112
+ | `hcloud_wait_action` | Poll an action until success/error or timeout. |
113
+
114
+ ### Generated Tools
115
+
116
+ One tool per OpenAPI operation, naming scheme `hcloud_<verb>_<resource>` or `hcloud_<resource>_<action>_action`. On name collision with a wrapper the generated tool gets the suffix `_raw` (`hcloud_list_servers_raw`, `hcloud_get_server_raw`, `hcloud_get_server_metrics_raw`).
117
+
118
+ Run `tools/list` in the Inspector to see the full list.
119
+
120
+ ## Spec Updates
121
+
122
+ When Hetzner updates the OpenAPI:
123
+
124
+ ```bash
125
+ npm run refresh-spec # re-downloads specs/cloud.spec.json
126
+ npm run generate # rebuilds src/tools/generated/operations.ts
127
+ npm test # codegen snapshot shows the diff
128
+ npm run refresh-snapshot # rewrite the snapshot deliberately (after review)
129
+ git add specs/cloud.spec.json src/tools/generated/operations.ts tests/snapshots/tool-registry.json
130
+ git commit -m "chore(spec): refresh from Hetzner"
131
+ ```
132
+
133
+ ## Manual Verification Recipe
134
+
135
+ Run before declaring a release ready:
136
+
137
+ 1. `npm run inspector` — opens the MCP Inspector with this server.
138
+ 2. `tools/list` — verify count ≥ 200 and that all 12 wrapper names appear.
139
+ 3. `hcloud_list_servers` (no args) — verify against at least one known server in your account.
140
+ 4. `hcloud_get_server` with `name: "<one of your server names>"` — should return id, type, ip.
141
+ 5. `hcloud_create_ssh_key` with `{ name: "mcp-smoke", public_key: "<your test key>" }` — note the returned id.
142
+ 6. `hcloud_delete_ssh_key` with that id — confirm clean removal.
143
+
144
+ Do **not** run power/rebuild/snapshot operations against production servers as part of a smoke test.
145
+
146
+ ## License
147
+
148
+ MIT — see [LICENSE](./LICENSE).
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ import "dotenv/config";
2
+ const DEFAULTS = {
3
+ baseUrl: "https://api.hetzner.cloud/v1",
4
+ confirmWrites: false,
5
+ logLevel: "warn",
6
+ httpTimeoutMs: 30_000,
7
+ actionPollTimeoutMs: 60_000,
8
+ actionPollIntervalMs: 2_000,
9
+ paginationMaxItems: 500,
10
+ paginationMaxPages: 10,
11
+ };
12
+ function parseBool(value, fallback) {
13
+ if (value === undefined)
14
+ return fallback;
15
+ const v = value.trim().toLowerCase();
16
+ if (v === "true" || v === "1" || v === "yes")
17
+ return true;
18
+ if (v === "false" || v === "0" || v === "no")
19
+ return false;
20
+ return fallback;
21
+ }
22
+ function parsePositiveInt(value, fallback) {
23
+ if (value === undefined)
24
+ return fallback;
25
+ const n = Number(value);
26
+ if (!Number.isFinite(n) || n <= 0)
27
+ return fallback;
28
+ return Math.trunc(n);
29
+ }
30
+ function parseLogLevel(value) {
31
+ if (value === "debug" || value === "info" || value === "warn" || value === "error") {
32
+ return value;
33
+ }
34
+ return DEFAULTS.logLevel;
35
+ }
36
+ export function loadConfig(env = process.env) {
37
+ const token = env.HETZNER_API_TOKEN;
38
+ if (!token) {
39
+ throw new Error("HETZNER_API_TOKEN is required. Set it in .env or your environment.");
40
+ }
41
+ return {
42
+ token,
43
+ baseUrl: env.HETZNER_API_BASE ?? DEFAULTS.baseUrl,
44
+ confirmWrites: parseBool(env.HETZNER_CONFIRM_WRITES, DEFAULTS.confirmWrites),
45
+ logLevel: parseLogLevel(env.LOG_LEVEL),
46
+ httpTimeoutMs: parsePositiveInt(env.HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),
47
+ actionPollTimeoutMs: parsePositiveInt(env.ACTION_POLL_TIMEOUT_MS, DEFAULTS.actionPollTimeoutMs),
48
+ actionPollIntervalMs: parsePositiveInt(env.ACTION_POLL_INTERVAL_MS, DEFAULTS.actionPollIntervalMs),
49
+ paginationMaxItems: parsePositiveInt(env.PAGINATION_MAX_ITEMS, DEFAULTS.paginationMaxItems),
50
+ paginationMaxPages: parsePositiveInt(env.PAGINATION_MAX_PAGES, DEFAULTS.paginationMaxPages),
51
+ };
52
+ }
53
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAgBvB,MAAM,QAAQ,GAAG;IACf,OAAO,EAAE,8BAA8B;IACvC,aAAa,EAAE,KAAK;IACpB,QAAQ,EAAE,MAAkB;IAC5B,aAAa,EAAE,MAAM;IACrB,mBAAmB,EAAE,MAAM;IAC3B,oBAAoB,EAAE,KAAK;IAC3B,kBAAkB,EAAE,GAAG;IACvB,kBAAkB,EAAE,EAAE;CACd,CAAC;AAEX,SAAS,SAAS,CAAC,KAAyB,EAAE,QAAiB;IAC7D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAyB,EAAE,QAAgB;IACnE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IACnD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,aAAa,CAAC,KAAyB;IAC9C,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACnF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,QAAQ,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,MAA8D,OAAO,CAAC,GAAG;IAEzE,MAAM,KAAK,GAAG,GAAG,CAAC,iBAAiB,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,oEAAoE,CACrE,CAAC;IACJ,CAAC;IACD,OAAO;QACL,KAAK;QACL,OAAO,EAAE,GAAG,CAAC,gBAAgB,IAAI,QAAQ,CAAC,OAAO;QACjD,aAAa,EAAE,SAAS,CAAC,GAAG,CAAC,sBAAsB,EAAE,QAAQ,CAAC,aAAa,CAAC;QAC5E,QAAQ,EAAE,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC;QACtC,aAAa,EAAE,gBAAgB,CAAC,GAAG,CAAC,eAAe,EAAE,QAAQ,CAAC,aAAa,CAAC;QAC5E,mBAAmB,EAAE,gBAAgB,CACnC,GAAG,CAAC,sBAAsB,EAC1B,QAAQ,CAAC,mBAAmB,CAC7B;QACD,oBAAoB,EAAE,gBAAgB,CACpC,GAAG,CAAC,uBAAuB,EAC3B,QAAQ,CAAC,oBAAoB,CAC9B;QACD,kBAAkB,EAAE,gBAAgB,CAClC,GAAG,CAAC,oBAAoB,EACxB,QAAQ,CAAC,kBAAkB,CAC5B;QACD,kBAAkB,EAAE,gBAAgB,CAClC,GAAG,CAAC,oBAAoB,EACxB,QAAQ,CAAC,kBAAkB,CAC5B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
2
+ export async function pollAction(request, actionId, opts) {
3
+ const sleep = opts.sleep ?? defaultSleep;
4
+ const now = opts.now ?? Date.now;
5
+ const start = now();
6
+ let lastAction;
7
+ while (true) {
8
+ const res = await request("GET", `/actions/${actionId}`);
9
+ const action = res.body?.action;
10
+ if (action) {
11
+ lastAction = action;
12
+ if (action.status === "success" || action.status === "error") {
13
+ return { action, timedOut: false };
14
+ }
15
+ }
16
+ if (now() - start >= opts.timeoutMs) {
17
+ return {
18
+ action: lastAction ?? {
19
+ id: actionId,
20
+ command: "unknown",
21
+ status: "running",
22
+ progress: 0,
23
+ started: "",
24
+ finished: null,
25
+ resources: [],
26
+ error: null,
27
+ },
28
+ timedOut: true,
29
+ };
30
+ }
31
+ await sleep(opts.intervalMs);
32
+ }
33
+ }
34
+ //# sourceMappingURL=action-polling.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-polling.js","sourceRoot":"","sources":["../../src/http/action-polling.ts"],"names":[],"mappings":"AA+BA,MAAM,YAAY,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEjF,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAkB,EAClB,QAAgB,EAChB,IAAiB;IAEjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;IACpB,IAAI,UAA8B,CAAC;IAEnC,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,OAAO,CAAqB,KAAK,EAAE,YAAY,QAAQ,EAAE,CAAC,CAAC;QAC7E,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC;QAChC,IAAI,MAAM,EAAE,CAAC;YACX,UAAU,GAAG,MAAM,CAAC;YACpB,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC7D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;YACrC,CAAC;QACH,CAAC;QACD,IAAI,GAAG,EAAE,GAAG,KAAK,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACpC,OAAO;gBACL,MAAM,EAAE,UAAU,IAAK;oBACrB,EAAE,EAAE,QAAQ;oBACZ,OAAO,EAAE,SAAS;oBAClB,MAAM,EAAE,SAAS;oBACjB,QAAQ,EAAE,CAAC;oBACX,OAAO,EAAE,EAAE;oBACX,QAAQ,EAAE,IAAI;oBACd,SAAS,EAAE,EAAE;oBACb,KAAK,EAAE,IAAI;iBACM;gBACnB,QAAQ,EAAE,IAAI;aACf,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
@@ -0,0 +1,94 @@
1
+ const DEFAULT_TIMEOUT_MS = 30_000;
2
+ const DEFAULT_MAX_RETRIES = 3;
3
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
+ export class HetznerHttpClient {
5
+ baseUrl;
6
+ token;
7
+ fetchImpl;
8
+ defaultTimeoutMs;
9
+ sleep;
10
+ defaultMaxRetries;
11
+ constructor(opts) {
12
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
13
+ this.token = opts.token;
14
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
15
+ this.defaultTimeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
16
+ this.sleep = opts.sleep ?? defaultSleep;
17
+ this.defaultMaxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
18
+ }
19
+ async request(method, path, options = {}) {
20
+ const maxRetries = options.maxRetries ?? this.defaultMaxRetries;
21
+ let attempt = 0;
22
+ while (true) {
23
+ const res = await this.doFetch(method, path, options);
24
+ const retryDelayMs = this.computeRetryDelay(res.status, res.headers, attempt);
25
+ if (retryDelayMs === null || attempt >= maxRetries) {
26
+ return res;
27
+ }
28
+ await this.sleep(retryDelayMs);
29
+ attempt++;
30
+ }
31
+ }
32
+ async doFetch(method, path, options) {
33
+ const url = this.buildUrl(path, options.query);
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(new Error("request timeout")), options.timeoutMs ?? this.defaultTimeoutMs);
36
+ try {
37
+ const res = await this.fetchImpl(url, {
38
+ method,
39
+ headers: {
40
+ authorization: `Bearer ${this.token}`,
41
+ "content-type": "application/json",
42
+ accept: "application/json",
43
+ },
44
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
45
+ signal: controller.signal,
46
+ });
47
+ const body = await this.parseBody(res);
48
+ return { status: res.status, headers: res.headers, body };
49
+ }
50
+ finally {
51
+ clearTimeout(timeout);
52
+ }
53
+ }
54
+ computeRetryDelay(status, headers, attempt) {
55
+ if (status === 429) {
56
+ const retryAfter = headers.get("retry-after");
57
+ const reset = headers.get("ratelimit-reset");
58
+ const retryAfterMs = retryAfter ? Number(retryAfter) * 1000 : 0;
59
+ const resetMs = reset
60
+ ? Math.max(0, Number(reset) * 1000 - Date.now())
61
+ : 0;
62
+ return Math.max(retryAfterMs, resetMs, 0);
63
+ }
64
+ if (status >= 500 && status < 600) {
65
+ return 1000 * 2 ** attempt;
66
+ }
67
+ return null;
68
+ }
69
+ buildUrl(path, query) {
70
+ const url = new URL(this.baseUrl + (path.startsWith("/") ? path : `/${path}`));
71
+ if (query) {
72
+ for (const [k, v] of Object.entries(query)) {
73
+ if (v === undefined || v === null)
74
+ continue;
75
+ url.searchParams.set(k, String(v));
76
+ }
77
+ }
78
+ return url.toString();
79
+ }
80
+ async parseBody(res) {
81
+ if (res.status === 204)
82
+ return undefined;
83
+ const text = await res.text();
84
+ if (!text)
85
+ return undefined;
86
+ try {
87
+ return JSON.parse(text);
88
+ }
89
+ catch {
90
+ return text;
91
+ }
92
+ }
93
+ }
94
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/http/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ function isErrorEnvelope(body) {
2
+ return (typeof body === "object" &&
3
+ body !== null &&
4
+ "error" in body &&
5
+ typeof body.error === "object" &&
6
+ body.error !== null &&
7
+ typeof body.error.code === "string" &&
8
+ typeof body.error.message === "string");
9
+ }
10
+ export class HetznerApiError extends Error {
11
+ status;
12
+ code;
13
+ details;
14
+ constructor(status, code, message, details) {
15
+ super(message);
16
+ this.name = "HetznerApiError";
17
+ this.status = status;
18
+ this.code = code;
19
+ this.details = details;
20
+ }
21
+ toString() {
22
+ return `HetznerApiError[${this.status} ${this.code}]: ${this.message}`;
23
+ }
24
+ }
25
+ export function mapErrorResponse(status, body) {
26
+ if (status >= 200 && status < 400)
27
+ return null;
28
+ if (isErrorEnvelope(body)) {
29
+ return new HetznerApiError(status, body.error.code, body.error.message, body.error.details);
30
+ }
31
+ const fallbackMessage = typeof body === "string" && body.length > 0
32
+ ? body
33
+ : `HTTP ${status}`;
34
+ return new HetznerApiError(status, "upstream_error", fallbackMessage);
35
+ }
36
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/http/errors.ts"],"names":[],"mappings":"AAQA,SAAS,eAAe,CAAC,IAAa;IACpC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACb,OAAO,IAAI,IAAI;QACf,OAAQ,IAA2B,CAAC,KAAK,KAAK,QAAQ;QACrD,IAAyD,CAAC,KAAK,KAAK,IAAI;QACzE,OAAQ,IAAqC,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ;QACrE,OAAQ,IAAwC,CAAC,KAAK,CAAC,OAAO,KAAK,QAAQ,CAC5E,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,MAAM,CAAS;IACf,IAAI,CAAS;IACb,OAAO,CAAU;IAE1B,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe,EAAE,OAAiB;QAC1E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAEQ,QAAQ;QACf,OAAO,mBAAmB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACzE,CAAC;CACF;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,IAAa;IAEb,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/C,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,eAAe,CACxB,MAAM,EACN,IAAI,CAAC,KAAK,CAAC,IAAI,EACf,IAAI,CAAC,KAAK,CAAC,OAAO,EAClB,IAAI,CAAC,KAAK,CAAC,OAAO,CACnB,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GACnB,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QACzC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC;IACvB,OAAO,IAAI,eAAe,CAAC,MAAM,EAAE,gBAAgB,EAAE,eAAe,CAAC,CAAC;AACxE,CAAC"}
@@ -0,0 +1,51 @@
1
+ export async function fetchAllPages(request, method, path, query, opts) {
2
+ const explicitPage = typeof query.page === "number";
3
+ const items = [];
4
+ let page = explicitPage ? query.page : 1;
5
+ let pageCount = 0;
6
+ let truncated = false;
7
+ let lastPagination;
8
+ while (true) {
9
+ const res = await request(method, path, {
10
+ query: { ...query, page },
11
+ });
12
+ pageCount++;
13
+ const body = res.body ?? {};
14
+ const pageItems = body[opts.resourceKey] ?? [];
15
+ const pagination = body.meta?.pagination;
16
+ lastPagination = pagination ?? lastPagination;
17
+ for (const item of pageItems) {
18
+ if (items.length >= opts.maxItems) {
19
+ truncated = true;
20
+ break;
21
+ }
22
+ items.push(item);
23
+ }
24
+ if (truncated)
25
+ break;
26
+ // Caller asked for a specific page — return that page only, no forward-walk.
27
+ if (explicitPage)
28
+ break;
29
+ if (pageCount >= opts.maxPages) {
30
+ const hasMore = pagination?.next_page != null;
31
+ if (hasMore)
32
+ truncated = true;
33
+ break;
34
+ }
35
+ if (!pagination || pagination.next_page == null)
36
+ break;
37
+ page = pagination.next_page;
38
+ }
39
+ if (truncated && lastPagination) {
40
+ return {
41
+ items,
42
+ truncated,
43
+ pagination: {
44
+ next_page: lastPagination.next_page,
45
+ last_page: lastPagination.last_page,
46
+ },
47
+ };
48
+ }
49
+ return { items, truncated };
50
+ }
51
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/http/pagination.ts"],"names":[],"mappings":"AAiCA,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAkB,EAClB,MAAkB,EAClB,IAAY,EACZ,KAA8B,EAC9B,IAAqB;IAErB,MAAM,YAAY,GAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;IACpD,MAAM,KAAK,GAAQ,EAAE,CAAC;IACtB,IAAI,IAAI,GAAG,YAAY,CAAC,CAAC,CAAE,KAAK,CAAC,IAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,cAA0C,CAAC;IAE/C,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,OAAO,CAAkB,MAAM,EAAE,IAAI,EAAE;YACvD,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE;SAC1B,CAAC,CAAC;QACH,SAAS,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAK,EAAsB,CAAC;QACjD,MAAM,SAAS,GAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAqB,IAAI,EAAE,CAAC;QACpE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC;QACzC,cAAc,GAAG,UAAU,IAAI,cAAc,CAAC;QAE9C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClC,SAAS,GAAG,IAAI,CAAC;gBACjB,MAAM;YACR,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,SAAS;YAAE,MAAM;QACrB,6EAA6E;QAC7E,IAAI,YAAY;YAAE,MAAM;QACxB,IAAI,SAAS,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,UAAU,EAAE,SAAS,IAAI,IAAI,CAAC;YAC9C,IAAI,OAAO;gBAAE,SAAS,GAAG,IAAI,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,SAAS,IAAI,IAAI;YAAE,MAAM;QACvD,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC;IAC9B,CAAC;IAED,IAAI,SAAS,IAAI,cAAc,EAAE,CAAC;QAChC,OAAO;YACL,KAAK;YACL,SAAS;YACT,UAAU,EAAE;gBACV,SAAS,EAAE,cAAc,CAAC,SAAS;gBACnC,SAAS,EAAE,cAAc,CAAC,SAAS;aACpC;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { createLogger } from "./logger.js";
5
+ import { HetznerHttpClient } from "./http/client.js";
6
+ import { createMcpServer } from "./server.js";
7
+ import { ToolRegistry } from "./tools/registry.js";
8
+ import { makeServerListTool } from "./tools/wrappers/server_list.js";
9
+ import { makeServerGetTool } from "./tools/wrappers/server_get.js";
10
+ import { makeServerPowerTool } from "./tools/wrappers/server_power.js";
11
+ import { makeServerRebuildTool } from "./tools/wrappers/server_rebuild.js";
12
+ import { makeServerChangeTypeTool } from "./tools/wrappers/server_change_type.js";
13
+ import { makeServerSnapshotTool } from "./tools/wrappers/server_snapshot.js";
14
+ import { makeServerBackupTool } from "./tools/wrappers/server_backup.js";
15
+ import { makeServerRescueTool } from "./tools/wrappers/server_rescue.js";
16
+ import { makeServerMetricsTool } from "./tools/wrappers/server_metrics.js";
17
+ import { makeApplyFirewallToServerTool } from "./tools/wrappers/apply_firewall_to_server.js";
18
+ import { makeRawRequestTool } from "./tools/raw_request.js";
19
+ import { makeWaitActionTool } from "./tools/wait_action.js";
20
+ import { withConfirmGuard } from "./tools/confirm.js";
21
+ import { OPERATIONS } from "./tools/generated/operations.js";
22
+ import { buildGeneratedTools } from "./tools/generated/tools.js";
23
+ async function main() {
24
+ const config = loadConfig();
25
+ const logger = createLogger(config.logLevel);
26
+ const client = new HetznerHttpClient({
27
+ baseUrl: config.baseUrl,
28
+ token: config.token,
29
+ timeoutMs: config.httpTimeoutMs,
30
+ });
31
+ const registry = new ToolRegistry();
32
+ const limits = {
33
+ maxItems: config.paginationMaxItems,
34
+ maxPages: config.paginationMaxPages,
35
+ };
36
+ function register(reg, tool) {
37
+ reg.register(withConfirmGuard(tool, config.confirmWrites));
38
+ }
39
+ register(registry, makeServerListTool(client, limits));
40
+ register(registry, makeServerGetTool(client));
41
+ const actionPoll = {
42
+ timeoutMs: config.actionPollTimeoutMs,
43
+ intervalMs: config.actionPollIntervalMs,
44
+ };
45
+ const actionDeps = { client, actionPoll };
46
+ register(registry, makeServerPowerTool(actionDeps));
47
+ register(registry, makeServerRebuildTool(actionDeps));
48
+ register(registry, makeServerChangeTypeTool(actionDeps));
49
+ register(registry, makeServerSnapshotTool(actionDeps));
50
+ register(registry, makeServerBackupTool(actionDeps));
51
+ register(registry, makeServerRescueTool(actionDeps));
52
+ register(registry, makeServerMetricsTool({ client }));
53
+ register(registry, makeApplyFirewallToServerTool({ client, actionPoll }));
54
+ for (const tool of buildGeneratedTools(client, OPERATIONS, {
55
+ maxItems: config.paginationMaxItems,
56
+ maxPages: config.paginationMaxPages,
57
+ actionPoll,
58
+ })) {
59
+ // Wrapper takes precedence on name collision (wrappers register first, generated tools have _raw suffix)
60
+ if (registry.getByName(tool.name))
61
+ continue;
62
+ register(registry, tool);
63
+ }
64
+ // raw_request handles confirm-mode itself (only for non-GET methods per spec §16).
65
+ // Bypass withConfirmGuard so GET passes through even when HETZNER_CONFIRM_WRITES=true.
66
+ registry.register(makeRawRequestTool({ client, confirmWrites: config.confirmWrites }));
67
+ register(registry, makeWaitActionTool({
68
+ client,
69
+ defaultPoll: actionPoll,
70
+ }));
71
+ const server = createMcpServer(registry, logger);
72
+ const transport = new StdioServerTransport();
73
+ await server.connect(transport);
74
+ logger.info({ tools: registry.size() }, "hetzner-cloud-mcp ready");
75
+ }
76
+ main().catch((err) => {
77
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : err}\n`);
78
+ process.exit(1);
79
+ });
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/logger.js ADDED
@@ -0,0 +1,12 @@
1
+ import pino from "pino";
2
+ export function createLogger(level) {
3
+ const opts = {
4
+ level,
5
+ redact: {
6
+ paths: ["headers.authorization", "*.authorization", "*.HETZNER_API_TOKEN"],
7
+ remove: true,
8
+ },
9
+ };
10
+ return pino(opts, pino.destination(2));
11
+ }
12
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAyC,MAAM,MAAM,CAAC;AAG7D,MAAM,UAAU,YAAY,CAAC,KAAe;IAC1C,MAAM,IAAI,GAAkB;QAC1B,KAAK;QACL,MAAM,EAAE;YACN,KAAK,EAAE,CAAC,uBAAuB,EAAE,iBAAiB,EAAE,qBAAqB,CAAC;YAC1E,MAAM,EAAE,IAAI;SACb;KACF,CAAC;IACF,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,37 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ export function createMcpServer(registry, logger) {
4
+ const server = new Server({ name: "the-real-hetzner-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
5
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
6
+ tools: registry.getAll().map((t) => ({
7
+ name: t.name,
8
+ description: t.description,
9
+ inputSchema: t.inputSchema,
10
+ annotations: t.annotations,
11
+ })),
12
+ }));
13
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
14
+ const tool = registry.getByName(req.params.name);
15
+ if (!tool) {
16
+ throw new Error(`Unknown tool: ${req.params.name}`);
17
+ }
18
+ logger.debug({ tool: tool.name }, "tools/call");
19
+ try {
20
+ return await tool.handler(req.params.arguments ?? {});
21
+ }
22
+ catch (err) {
23
+ logger.warn({ tool: tool.name, err }, "tool handler failed");
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: err instanceof Error ? err.message : String(err),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ });
35
+ return server;
36
+ }
37
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAI5C,MAAM,UAAU,eAAe,CAC7B,QAAsB,EACtB,MAAc;IAEd,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,EAClD,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;SAC3B,CAAC,CAAC;KACJ,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC5D,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,CAAC;QAChD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,qBAAqB,CAAC,CAAC;YAC7D,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;qBACvD;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}