@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.
- package/.env.example +25 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -0
- package/dist/http/action-polling.js +34 -0
- package/dist/http/action-polling.js.map +1 -0
- package/dist/http/client.js +94 -0
- package/dist/http/client.js.map +1 -0
- package/dist/http/errors.js +36 -0
- package/dist/http/errors.js.map +1 -0
- package/dist/http/pagination.js +51 -0
- package/dist/http/pagination.js.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.js +12 -0
- package/dist/logger.js.map +1 -0
- package/dist/server.js +37 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/_error.js +25 -0
- package/dist/tools/_error.js.map +1 -0
- package/dist/tools/confirm.js +46 -0
- package/dist/tools/confirm.js.map +1 -0
- package/dist/tools/generated/operations.js +49725 -0
- package/dist/tools/generated/operations.js.map +1 -0
- package/dist/tools/generated/tools.js +162 -0
- package/dist/tools/generated/tools.js.map +1 -0
- package/dist/tools/raw_request.js +87 -0
- package/dist/tools/raw_request.js.map +1 -0
- package/dist/tools/registry.js +19 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/wait_action.js +37 -0
- package/dist/tools/wait_action.js.map +1 -0
- package/dist/tools/wrappers/_server_action.js +23 -0
- package/dist/tools/wrappers/_server_action.js.map +1 -0
- package/dist/tools/wrappers/apply_firewall_to_server.js +47 -0
- package/dist/tools/wrappers/apply_firewall_to_server.js.map +1 -0
- package/dist/tools/wrappers/server_backup.js +25 -0
- package/dist/tools/wrappers/server_backup.js.map +1 -0
- package/dist/tools/wrappers/server_change_type.js +27 -0
- package/dist/tools/wrappers/server_change_type.js.map +1 -0
- package/dist/tools/wrappers/server_get.js +53 -0
- package/dist/tools/wrappers/server_get.js.map +1 -0
- package/dist/tools/wrappers/server_list.js +69 -0
- package/dist/tools/wrappers/server_list.js.map +1 -0
- package/dist/tools/wrappers/server_metrics.js +55 -0
- package/dist/tools/wrappers/server_metrics.js.map +1 -0
- package/dist/tools/wrappers/server_power.js +32 -0
- package/dist/tools/wrappers/server_power.js.map +1 -0
- package/dist/tools/wrappers/server_rebuild.js +34 -0
- package/dist/tools/wrappers/server_rebuild.js.map +1 -0
- package/dist/tools/wrappers/server_rescue.js +34 -0
- package/dist/tools/wrappers/server_rescue.js.map +1 -0
- package/dist/tools/wrappers/server_snapshot.js +33 -0
- package/dist/tools/wrappers/server_snapshot.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
- package/scripts/generate.ts +245 -0
- package/scripts/refresh-snapshot.ts +33 -0
- package/scripts/refresh-spec.ts +35 -0
- package/specs/cloud.spec.json +77624 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { asToolError } from "../_error.js";
|
|
2
|
+
const VALID_TYPES = ["cpu", "disk", "network"];
|
|
3
|
+
export function makeServerMetricsTool(deps) {
|
|
4
|
+
const now = deps.nowMs ?? (() => Date.now());
|
|
5
|
+
return {
|
|
6
|
+
name: "hcloud_get_server_metrics",
|
|
7
|
+
description: "Fetch a server's metrics (cpu / disk / network). Accept id OR name. Defaults to the last hour with step=60s. Hand-tuned wrapper.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
id: { type: "integer", minimum: 1 },
|
|
12
|
+
name: { type: "string", minLength: 1 },
|
|
13
|
+
type: { type: "string", enum: [...VALID_TYPES] },
|
|
14
|
+
start: { type: "string", description: "ISO-8601 timestamp; default: now - 1 hour" },
|
|
15
|
+
end: { type: "string", description: "ISO-8601 timestamp; default: now" },
|
|
16
|
+
step: { type: "integer", minimum: 1, default: 60 },
|
|
17
|
+
},
|
|
18
|
+
required: ["type"],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
22
|
+
handler: async (input) => {
|
|
23
|
+
const type = input.type;
|
|
24
|
+
if (!VALID_TYPES.includes(type)) {
|
|
25
|
+
return { content: [{ type: "text", text: `Invalid type="${type}". Use one of: ${VALID_TYPES.join(", ")}.` }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
let id = typeof input.id === "number" ? input.id : undefined;
|
|
28
|
+
if (id === undefined) {
|
|
29
|
+
if (typeof input.name !== "string") {
|
|
30
|
+
return { content: [{ type: "text", text: "Provide either `id` or `name`." }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
const lookup = await deps.client.request("GET", "/servers", { query: { name: input.name } });
|
|
33
|
+
const lookupErr = asToolError(lookup);
|
|
34
|
+
if (lookupErr)
|
|
35
|
+
return lookupErr;
|
|
36
|
+
const match = lookup.body?.servers?.[0];
|
|
37
|
+
if (!match) {
|
|
38
|
+
return { content: [{ type: "text", text: `Server with name="${input.name}" not found.` }], isError: true };
|
|
39
|
+
}
|
|
40
|
+
id = match.id;
|
|
41
|
+
}
|
|
42
|
+
const end = typeof input.end === "string" ? input.end : new Date(now()).toISOString();
|
|
43
|
+
const start = typeof input.start === "string"
|
|
44
|
+
? input.start
|
|
45
|
+
: new Date(now() - 60 * 60 * 1000).toISOString();
|
|
46
|
+
const step = typeof input.step === "number" ? input.step : 60;
|
|
47
|
+
const res = await deps.client.request("GET", `/servers/${id}/metrics`, { query: { type, start, end, step } });
|
|
48
|
+
const errResult = asToolError(res);
|
|
49
|
+
if (errResult)
|
|
50
|
+
return errResult;
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(res.body ?? {}, null, 2) }] };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=server_metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_metrics.js","sourceRoot":"","sources":["../../../src/tools/wrappers/server_metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { runServerAction } from "./_server_action.js";
|
|
2
|
+
const VALID_OPS = ["poweron", "poweroff", "reboot", "shutdown", "reset"];
|
|
3
|
+
export function makeServerPowerTool(deps) {
|
|
4
|
+
return {
|
|
5
|
+
name: "hcloud_server_power",
|
|
6
|
+
description: "Run a power operation on a server (poweron / poweroff / reboot / shutdown / reset). Hand-tuned wrapper that polls the resulting action by default.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
id: { type: "integer", minimum: 1 },
|
|
11
|
+
op: { type: "string", enum: [...VALID_OPS] },
|
|
12
|
+
wait: { type: "boolean", default: true },
|
|
13
|
+
},
|
|
14
|
+
required: ["id", "op"],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
},
|
|
17
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
18
|
+
handler: async (input) => {
|
|
19
|
+
const id = input.id;
|
|
20
|
+
const op = input.op;
|
|
21
|
+
if (!VALID_OPS.includes(op)) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: `Invalid op="${op}". Must be one of: ${VALID_OPS.join(", ")}.` }],
|
|
24
|
+
isError: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const wait = input.wait !== false;
|
|
28
|
+
return runServerAction(deps, id, op, undefined, wait);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=server_power.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_power.js","sourceRoot":"","sources":["../../../src/tools/wrappers/server_power.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AAE7E,MAAM,SAAS,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAU,CAAC;AAGlF,MAAM,UAAU,mBAAmB,CAAC,IAAsB;IACxD,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,WAAW,EACT,oJAAoJ;QACtJ,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;gBACnC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC,EAAE;gBAC5C,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;aACzC;YACD,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC;YACtB,oBAAoB,EAAE,KAAK;SAC5B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE;QAChF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAuB,EAAE;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAY,CAAC;YAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,EAAQ,CAAC;YAC1B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5B,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,sBAAsB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;oBACjG,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;YAClC,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QACxD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runServerAction } from "./_server_action.js";
|
|
2
|
+
export function makeServerRebuildTool(deps) {
|
|
3
|
+
return {
|
|
4
|
+
name: "hcloud_server_rebuild",
|
|
5
|
+
description: "Rebuild a server from an image. Accept image_id (integer) OR image_name (string). Hand-tuned wrapper; polls the resulting action by default.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
id: { type: "integer", minimum: 1 },
|
|
10
|
+
image_id: { type: "integer", minimum: 1 },
|
|
11
|
+
image_name: { type: "string", minLength: 1 },
|
|
12
|
+
wait: { type: "boolean", default: true },
|
|
13
|
+
},
|
|
14
|
+
required: ["id"],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
},
|
|
17
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
18
|
+
handler: async (input) => {
|
|
19
|
+
const id = input.id;
|
|
20
|
+
const hasId = typeof input.image_id === "number";
|
|
21
|
+
const hasName = typeof input.image_name === "string";
|
|
22
|
+
if (hasId === hasName) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: "Provide exactly one of `image_id` or `image_name`." }],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const image = hasId ? input.image_id : input.image_name;
|
|
29
|
+
const wait = input.wait !== false;
|
|
30
|
+
return runServerAction(deps, id, "rebuild", { image }, wait);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=server_rebuild.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_rebuild.js","sourceRoot":"","sources":["../../../src/tools/wrappers/server_rebuild.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AAE7E,MAAM,UAAU,qBAAqB,CAAC,IAAsB;IAC1D,OAAO;QACL,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EACT,8IAA8I;QAChJ,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;gBACnC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;gBACzC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;gBAC5C,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;aACzC;YACD,QAAQ,EAAE,CAAC,IAAI,CAAC;YAChB,oBAAoB,EAAE,KAAK;SAC5B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE;QAChF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAuB,EAAE;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAY,CAAC;YAC9B,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC;YACjD,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC;YACrD,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;gBACtB,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oDAAoD,EAAE,CAAC;oBACvF,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,KAAK,CAAC,QAAmB,CAAC,CAAC,CAAE,KAAK,CAAC,UAAqB,CAAC;YAChF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;YAClC,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAC/D,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runServerAction } from "./_server_action.js";
|
|
2
|
+
export function makeServerRescueTool(deps) {
|
|
3
|
+
return {
|
|
4
|
+
name: "hcloud_server_rescue",
|
|
5
|
+
description: "Toggle Hetzner rescue mode on a server. When enabling, defaults type to 'linux64' and accepts ssh_keys (array of SSH-key ids). Hand-tuned wrapper.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
id: { type: "integer", minimum: 1 },
|
|
10
|
+
enable: { type: "boolean", description: "true to enter rescue, false to leave." },
|
|
11
|
+
type: { type: "string", enum: ["linux64", "linux32", "freebsd64"], default: "linux64" },
|
|
12
|
+
ssh_keys: { type: "array", items: { type: "integer" } },
|
|
13
|
+
wait: { type: "boolean", default: true },
|
|
14
|
+
},
|
|
15
|
+
required: ["id", "enable"],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
19
|
+
handler: async (input) => {
|
|
20
|
+
const id = input.id;
|
|
21
|
+
const wait = input.wait !== false;
|
|
22
|
+
if (input.enable === true) {
|
|
23
|
+
const body = {
|
|
24
|
+
type: typeof input.type === "string" ? input.type : "linux64",
|
|
25
|
+
};
|
|
26
|
+
if (Array.isArray(input.ssh_keys))
|
|
27
|
+
body.ssh_keys = input.ssh_keys;
|
|
28
|
+
return runServerAction(deps, id, "enable_rescue", body, wait);
|
|
29
|
+
}
|
|
30
|
+
return runServerAction(deps, id, "disable_rescue", undefined, wait);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=server_rescue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_rescue.js","sourceRoot":"","sources":["../../../src/tools/wrappers/server_rescue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AAE7E,MAAM,UAAU,oBAAoB,CAAC,IAAsB;IACzD,OAAO;QACL,IAAI,EAAE,sBAAsB;QAC5B,WAAW,EACT,oJAAoJ;QACtJ,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;gBACnC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,uCAAuC,EAAE;gBACjF,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE;gBACvF,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;gBACvD,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;aACzC;YACD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC;YAC1B,oBAAoB,EAAE,KAAK;SAC5B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;QACjF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAuB,EAAE;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAY,CAAC;YAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;YAClC,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;gBAC1B,MAAM,IAAI,GAA4B;oBACpC,IAAI,EAAE,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;iBAC9D,CAAC;gBACF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;oBAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;gBAClE,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAChE,CAAC;YACD,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,gBAAgB,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QACtE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runServerAction } from "./_server_action.js";
|
|
2
|
+
export function makeServerSnapshotTool(deps) {
|
|
3
|
+
return {
|
|
4
|
+
name: "hcloud_server_snapshot",
|
|
5
|
+
description: "Create an image (snapshot or backup) of a server. Default type is 'snapshot'. Hand-tuned wrapper that polls the resulting action.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
id: { type: "integer", minimum: 1 },
|
|
10
|
+
type: { type: "string", enum: ["snapshot", "backup"], default: "snapshot" },
|
|
11
|
+
description: { type: "string" },
|
|
12
|
+
labels: { type: "object", additionalProperties: { type: "string" } },
|
|
13
|
+
wait: { type: "boolean", default: true },
|
|
14
|
+
},
|
|
15
|
+
required: ["id"],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
19
|
+
handler: async (input) => {
|
|
20
|
+
const id = input.id;
|
|
21
|
+
const body = {
|
|
22
|
+
type: typeof input.type === "string" ? input.type : "snapshot",
|
|
23
|
+
};
|
|
24
|
+
if (typeof input.description === "string")
|
|
25
|
+
body.description = input.description;
|
|
26
|
+
if (input.labels && typeof input.labels === "object")
|
|
27
|
+
body.labels = input.labels;
|
|
28
|
+
const wait = input.wait !== false;
|
|
29
|
+
return runServerAction(deps, id, "create_image", body, wait);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=server_snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_snapshot.js","sourceRoot":"","sources":["../../../src/tools/wrappers/server_snapshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AAE7E,MAAM,UAAU,sBAAsB,CAAC,IAAsB;IAC3D,OAAO;QACL,IAAI,EAAE,wBAAwB;QAC9B,WAAW,EACT,mIAAmI;QACrI,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;gBACnC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE;gBAC3E,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC/B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;gBACpE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;aACzC;YACD,QAAQ,EAAE,CAAC,IAAI,CAAC;YAChB,oBAAoB,EAAE,KAAK;SAC5B;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;QACjF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAuB,EAAE;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAY,CAAC;YAC9B,MAAM,IAAI,GAA4B;gBACpC,IAAI,EAAE,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU;aAC/D,CAAC;YACF,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;gBAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;YAChF,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;gBAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;YACjF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;YAClC,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAC/D,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mguttmann/hetzner-cloud-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local MCP server (stdio) covering the entire Hetzner Cloud API as 201 typed tools — read and write.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"hetzner-cloud-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"specs/cloud.spec.json",
|
|
14
|
+
"scripts/refresh-spec.ts",
|
|
15
|
+
"scripts/generate.ts",
|
|
16
|
+
"scripts/refresh-snapshot.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
".env.example"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
23
|
+
"prepublishOnly": "npm run build && npm test",
|
|
24
|
+
"start": "node dist/index.js",
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:live": "RUN_LIVE_TESTS=1 vitest run tests/live-smoke.test.ts",
|
|
30
|
+
"generate": "tsx scripts/generate.ts",
|
|
31
|
+
"refresh-spec": "tsx scripts/refresh-spec.ts",
|
|
32
|
+
"refresh-snapshot": "tsx scripts/refresh-snapshot.ts",
|
|
33
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"hetzner",
|
|
39
|
+
"hetzner-cloud",
|
|
40
|
+
"claude",
|
|
41
|
+
"ai-tools",
|
|
42
|
+
"stdio",
|
|
43
|
+
"openapi"
|
|
44
|
+
],
|
|
45
|
+
"author": "Manuel Guttmann",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/mguttmann/the-real-hetzner-mcp.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/mguttmann/the-real-hetzner-mcp#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/mguttmann/the-real-hetzner-mcp/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
60
|
+
"dotenv": "^16.4.0",
|
|
61
|
+
"pino": "^9.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^20.11.0",
|
|
65
|
+
"tsx": "^4.7.0",
|
|
66
|
+
"typescript": "^5.4.0",
|
|
67
|
+
"vitest": "^1.6.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=20.0.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import type { OperationDef, ParameterDef } from "../src/types.js";
|
|
4
|
+
|
|
5
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
|
|
6
|
+
type Method = (typeof HTTP_METHODS)[number];
|
|
7
|
+
|
|
8
|
+
const WRAPPER_COLLISIONS = new Set([
|
|
9
|
+
"hcloud_list_servers",
|
|
10
|
+
"hcloud_get_server",
|
|
11
|
+
"hcloud_get_server_metrics",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const DANGEROUS_ACTIONS = new Set([
|
|
15
|
+
"poweroff",
|
|
16
|
+
"reset",
|
|
17
|
+
"rebuild",
|
|
18
|
+
"request_console",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SINGLETON_PATHS = new Set(["pricing"]);
|
|
22
|
+
|
|
23
|
+
type SpecParam = {
|
|
24
|
+
name: string;
|
|
25
|
+
in: "path" | "query" | "header";
|
|
26
|
+
required?: boolean;
|
|
27
|
+
schema?: Record<string, unknown>;
|
|
28
|
+
description?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type SpecOperation = {
|
|
32
|
+
summary?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
operationId?: string;
|
|
35
|
+
parameters?: SpecParam[];
|
|
36
|
+
requestBody?: {
|
|
37
|
+
required?: boolean;
|
|
38
|
+
content?: { "application/json"?: { schema?: Record<string, unknown> } };
|
|
39
|
+
};
|
|
40
|
+
responses?: Record<
|
|
41
|
+
string,
|
|
42
|
+
{
|
|
43
|
+
content?: { "application/json"?: { schema?: Record<string, unknown> } };
|
|
44
|
+
}
|
|
45
|
+
>;
|
|
46
|
+
tags?: string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type Spec = {
|
|
50
|
+
paths: Record<string, Partial<Record<Method, SpecOperation>>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type { OperationDef, ParameterDef };
|
|
54
|
+
|
|
55
|
+
function singularize(plural: string): string {
|
|
56
|
+
if (plural.endsWith("ies")) return plural.slice(0, -3) + "y";
|
|
57
|
+
if (plural.endsWith("s") && !plural.endsWith("ss")) return plural.slice(0, -1);
|
|
58
|
+
return plural;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isPathParam(seg: string): boolean {
|
|
62
|
+
return seg.startsWith("{") && seg.endsWith("}");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resourceChain(segs: string[], stopBefore?: number): string[] {
|
|
66
|
+
// Singularized non-{param} segments, up to (but not including) `stopBefore`.
|
|
67
|
+
const end = stopBefore ?? segs.length;
|
|
68
|
+
const chain: string[] = [];
|
|
69
|
+
for (let i = 0; i < end; i++) {
|
|
70
|
+
const s = segs[i]!;
|
|
71
|
+
if (!isPathParam(s)) chain.push(singularize(s));
|
|
72
|
+
}
|
|
73
|
+
return chain;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deriveToolName(method: Method, path: string): string {
|
|
77
|
+
const segs = path.split("/").filter(Boolean);
|
|
78
|
+
|
|
79
|
+
if (segs.length === 1) {
|
|
80
|
+
const r = segs[0]!;
|
|
81
|
+
if (SINGLETON_PATHS.has(r)) return `hcloud_get_${r}`;
|
|
82
|
+
if (method === "get") return `hcloud_list_${r}`;
|
|
83
|
+
if (method === "post") return `hcloud_create_${singularize(r)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (segs.length === 2 && isPathParam(segs[1]!)) {
|
|
87
|
+
const r = singularize(segs[0]!);
|
|
88
|
+
if (method === "get") return `hcloud_get_${r}`;
|
|
89
|
+
if (method === "put" || method === "patch") return `hcloud_update_${r}`;
|
|
90
|
+
if (method === "delete") return `hcloud_delete_${r}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (segs.length === 3 && isPathParam(segs[1]!)) {
|
|
94
|
+
const r = singularize(segs[0]!);
|
|
95
|
+
const sub = segs[2]!;
|
|
96
|
+
if (sub === "actions" && method === "get") return `hcloud_list_${r}_actions`;
|
|
97
|
+
if (sub === "metrics" && method === "get") return `hcloud_get_${r}_metrics`;
|
|
98
|
+
return `hcloud_${method}_${r}_${sub}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
segs.length === 4 &&
|
|
103
|
+
isPathParam(segs[1]!) &&
|
|
104
|
+
segs[2] === "actions"
|
|
105
|
+
) {
|
|
106
|
+
const r = singularize(segs[0]!);
|
|
107
|
+
const sub = segs[3]!;
|
|
108
|
+
if (isPathParam(sub)) return `hcloud_get_${r}_action`;
|
|
109
|
+
return `hcloud_${r}_${sub}_action`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Deep paths (5+ segments). Hetzner pattern:
|
|
113
|
+
// <resA>/{idA}/<resB>/{idB}/<resC>[/actions/<name>]
|
|
114
|
+
// Build a name from the singularized resource chain, with a special case for the
|
|
115
|
+
// trailing /actions/<name> sub-path.
|
|
116
|
+
if (segs.length >= 5) {
|
|
117
|
+
const last = segs[segs.length - 1]!;
|
|
118
|
+
const beforeLast = segs[segs.length - 2]!;
|
|
119
|
+
// Trailing /actions/<action_name>: include the full chain up to "actions"
|
|
120
|
+
// and emit hcloud_<chain>_<action>_action.
|
|
121
|
+
if (beforeLast === "actions" && !isPathParam(last)) {
|
|
122
|
+
const chain = resourceChain(segs, segs.length - 2);
|
|
123
|
+
return `hcloud_${chain.join("_")}_${last}_action`;
|
|
124
|
+
}
|
|
125
|
+
// Trailing /actions/{id}: single action GET.
|
|
126
|
+
if (beforeLast === "actions" && isPathParam(last) && method === "get") {
|
|
127
|
+
const chain = resourceChain(segs, segs.length - 2);
|
|
128
|
+
return `hcloud_get_${chain.join("_")}_action`;
|
|
129
|
+
}
|
|
130
|
+
// Otherwise, derive from the full resource chain (no action segment).
|
|
131
|
+
const chain = resourceChain(segs);
|
|
132
|
+
if (chain.length > 0) {
|
|
133
|
+
if (method === "get") return `hcloud_get_${chain.join("_")}`;
|
|
134
|
+
if (method === "delete") return `hcloud_delete_${chain.join("_")}`;
|
|
135
|
+
if (method === "put" || method === "patch") return `hcloud_update_${chain.join("_")}`;
|
|
136
|
+
if (method === "post") return `hcloud_create_${chain.join("_")}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback: snake the whole path
|
|
141
|
+
const slug = segs
|
|
142
|
+
.map((s) => (isPathParam(s) ? s.slice(1, -1) : s))
|
|
143
|
+
.join("_");
|
|
144
|
+
return `hcloud_${method}_${slug}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyCollisionSuffix(toolName: string): string {
|
|
148
|
+
return WRAPPER_COLLISIONS.has(toolName) ? `${toolName}_raw` : toolName;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function detectReturnsAction(op: SpecOperation): boolean {
|
|
152
|
+
if (!op.responses) return false;
|
|
153
|
+
for (const status of ["200", "201", "202"]) {
|
|
154
|
+
const schema = op.responses[status]?.content?.["application/json"]?.schema;
|
|
155
|
+
if (!schema) continue;
|
|
156
|
+
const props = (schema as { properties?: Record<string, unknown> }).properties;
|
|
157
|
+
if (props && "action" in props) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function detectIsDestructive(
|
|
163
|
+
method: Method,
|
|
164
|
+
toolName: string,
|
|
165
|
+
): boolean {
|
|
166
|
+
if (method === "delete") return true;
|
|
167
|
+
for (const action of DANGEROUS_ACTIONS) {
|
|
168
|
+
if (toolName.endsWith(`_${action}_action`)) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function buildOperations(spec: Spec): OperationDef[] {
|
|
174
|
+
const ops: OperationDef[] = [];
|
|
175
|
+
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
|
|
176
|
+
for (const m of HTTP_METHODS) {
|
|
177
|
+
const op = methods?.[m];
|
|
178
|
+
if (!op) continue;
|
|
179
|
+
const baseName = deriveToolName(m, path);
|
|
180
|
+
const toolName = applyCollisionSuffix(baseName);
|
|
181
|
+
const parameters: ParameterDef[] = (op.parameters ?? []).map((p) => ({
|
|
182
|
+
name: p.name,
|
|
183
|
+
in: p.in,
|
|
184
|
+
required: !!p.required,
|
|
185
|
+
schema: p.schema ?? {},
|
|
186
|
+
...(p.description ? { description: p.description } : {}),
|
|
187
|
+
}));
|
|
188
|
+
const requestBodySchema =
|
|
189
|
+
op.requestBody?.content?.["application/json"]?.schema;
|
|
190
|
+
const requestBodyRequired = !!op.requestBody?.required;
|
|
191
|
+
const okResponse = ["200", "201", "202", "204"]
|
|
192
|
+
.map((s) => op.responses?.[s]?.content?.["application/json"]?.schema)
|
|
193
|
+
.find(Boolean);
|
|
194
|
+
ops.push({
|
|
195
|
+
operationId: op.operationId ?? `${m}_${path}`.replace(/\W+/g, "_"),
|
|
196
|
+
toolName,
|
|
197
|
+
method: m.toUpperCase() as OperationDef["method"],
|
|
198
|
+
path,
|
|
199
|
+
summary: op.summary ?? "",
|
|
200
|
+
description: op.description ?? "",
|
|
201
|
+
tags: op.tags ?? [],
|
|
202
|
+
parameters,
|
|
203
|
+
...(requestBodySchema ? { requestBodySchema } : {}),
|
|
204
|
+
...(requestBodySchema ? { requestBodyRequired } : {}),
|
|
205
|
+
...(okResponse ? { responseSchema: okResponse } : {}),
|
|
206
|
+
returnsAction: detectReturnsAction(op),
|
|
207
|
+
isDestructive: detectIsDestructive(m, toolName),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
ops.sort((a, b) => a.toolName.localeCompare(b.toolName));
|
|
212
|
+
return ops;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function emitTypeScript(operations: OperationDef[]): string {
|
|
216
|
+
const header =
|
|
217
|
+
"// AUTO-GENERATED by scripts/generate.ts. Do not edit by hand.\n" +
|
|
218
|
+
"// Run `npm run generate` after refreshing specs/cloud.spec.json.\n\n" +
|
|
219
|
+
'import type { OperationDef } from "../../types.js";\n\n' +
|
|
220
|
+
"export const OPERATIONS: OperationDef[] = " +
|
|
221
|
+
JSON.stringify(operations, null, 2) +
|
|
222
|
+
" as const;\n";
|
|
223
|
+
return header;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main(): Promise<void> {
|
|
227
|
+
const specPath = resolve(process.cwd(), "specs/cloud.spec.json");
|
|
228
|
+
const outPath = resolve(process.cwd(), "src/tools/generated/operations.ts");
|
|
229
|
+
const spec = JSON.parse(await readFile(specPath, "utf8")) as Spec;
|
|
230
|
+
const operations = buildOperations(spec);
|
|
231
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
232
|
+
await writeFile(outPath, emitTypeScript(operations), "utf8");
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`Wrote ${outPath} with ${operations.length} operations.\n`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const isDirectInvocation = process.argv[1]?.endsWith("generate.ts") ||
|
|
239
|
+
process.argv[1]?.endsWith("generate.js");
|
|
240
|
+
if (isDirectInvocation) {
|
|
241
|
+
main().catch((err) => {
|
|
242
|
+
process.stderr.write(`generate failed: ${err instanceof Error ? err.message : err}\n`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { OPERATIONS } from "../src/tools/generated/operations.js";
|
|
5
|
+
|
|
6
|
+
const SNAPSHOT_PATH = resolve(process.cwd(), "tests/snapshots/tool-registry.json");
|
|
7
|
+
|
|
8
|
+
function hashSchema(obj: unknown): string {
|
|
9
|
+
return createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main(): Promise<void> {
|
|
13
|
+
const snapshot = OPERATIONS.map((op) => ({
|
|
14
|
+
name: op.toolName,
|
|
15
|
+
schemaHash: hashSchema({
|
|
16
|
+
parameters: op.parameters,
|
|
17
|
+
requestBodySchema: op.requestBodySchema ?? null,
|
|
18
|
+
requestBodyRequired: op.requestBodyRequired ?? false,
|
|
19
|
+
returnsAction: op.returnsAction,
|
|
20
|
+
method: op.method,
|
|
21
|
+
path: op.path,
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
await writeFile(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
`Wrote ${SNAPSHOT_PATH} with ${snapshot.length} entries.\n`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main().catch((err) => {
|
|
31
|
+
process.stderr.write(`refresh-snapshot failed: ${err instanceof Error ? err.message : err}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const SPEC_URL = "https://docs.hetzner.cloud/cloud.spec.json";
|
|
5
|
+
const OUTPUT = resolve(process.cwd(), "specs/cloud.spec.json");
|
|
6
|
+
|
|
7
|
+
async function main(): Promise<void> {
|
|
8
|
+
process.stderr.write(`Fetching ${SPEC_URL}\n`);
|
|
9
|
+
const res = await fetch(SPEC_URL);
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
throw new Error(`Failed to fetch spec: HTTP ${res.status}`);
|
|
12
|
+
}
|
|
13
|
+
const text = await res.text();
|
|
14
|
+
let parsed: unknown;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(text);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
throw new Error(`Spec is not valid JSON: ${(err as Error).message}`);
|
|
19
|
+
}
|
|
20
|
+
const formatted = JSON.stringify(parsed, null, 2) + "\n";
|
|
21
|
+
|
|
22
|
+
await mkdir(dirname(OUTPUT), { recursive: true });
|
|
23
|
+
await writeFile(OUTPUT, formatted, "utf8");
|
|
24
|
+
|
|
25
|
+
const sizeKb = (Buffer.byteLength(formatted) / 1024).toFixed(0);
|
|
26
|
+
const info = parsed as { info?: { version?: string; title?: string } };
|
|
27
|
+
process.stderr.write(
|
|
28
|
+
`Wrote ${OUTPUT} (${sizeKb} KB, title="${info.info?.title ?? "?"}", version="${info.info?.version ?? "?"}")\n`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch((err) => {
|
|
33
|
+
process.stderr.write(`refresh-spec failed: ${err instanceof Error ? err.message : err}\n`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|