vpsnet-mcp 2.1.0 → 2.1.1
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/README.md +6 -0
- package/build/api-path.js +26 -0
- package/build/api.js +5 -0
- package/build/certificate-contract.js +7 -0
- package/build/file-restore-contract.js +1 -0
- package/build/index.js +240 -225
- package/build/temp-vm-contract.js +2 -0
- package/build/tool-result.js +49 -0
- package/package.json +4 -2
package/build/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { isIP } from "node:net";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { apiRequest, formatJson } from "./api.js";
|
|
8
|
+
import { installToolResultErrorFlag } from "./tool-result.js";
|
|
8
9
|
import { applicationAccessConfigurationRequestBody, applicationAccessSchema, applicationSingleAccessSchema, applicationActionCancellationIsAdvertised, applicationActionCancellationRequestBody, applicationActionIdSchema, applicationActionSchema, applicationDataRestoreIdSchema, applicationDataRestorePointIdSchema, applicationDataRestoreQuoteRequestBody, applicationDataRestoreQuoteTokenSchema, applicationDataRestoreRequestBody, applicationExpectedVersionSchema, applicationInstallRequestBody, applicationLifecycleRequestBody, applicationLogMaxBytesSchema, applicationLogServiceSchema, applicationLogTailLinesSchema, applicationRevisionSchema, applicationResourceCpuPercentSchema, applicationResourceEmailEnabledSchema, applicationResourceMemoryMiBSchema, applicationResourceNetworkMiBPerMinuteSchema, applicationResourceRestartDeltaSchema, applicationResourceThresholdRequestBody, applicationUpdateCandidateMatches, safeApplicationInspectionPayload, safeApplicationDataRestorePayload, safeApplicationDataRestorePointsPayload, safeApplicationDataRestoreQuotePayload, safeApplicationMutationPayload, safeApplicationRegistryCredentialPayload, } from "./application-contract.js";
|
|
9
10
|
import { composeAdoptionIdSchema, composeProjectLabelSchema, customProjectComposeSchema, customProjectDefinitionRequestBody, customProjectEnvironmentSchema, customProjectIdSchema, customProjectNameSchema, customProjectRegistryCredentialIdsSchema, customProjectRevisionSchema, customProjectSecretNamesSchema, customProjectSecretsSchema, safeComposeAdoptionConfirmationPayload, safeComposeAdoptionPayload, safeContainerDiscoveryPayload, safeCustomProjectInstallPayload, safeCustomProjectPayload, safeCustomProjectReceiptPayload, safeCustomProjectValidationPayload, } from "./custom-project-contract.js";
|
|
10
11
|
import { certificateActionRequestBody, certificateActionRequestSchema, certificateAcmeSubscriptionActionRequestBody, certificateAcmeSubscriptionActionRequestSchema, certificateAcmeSubscriptionDomainInputShape, certificateAcmeSubscriptionInputShape, certificateOrderIdSchema, certificateOrderInputShape, certificateProductIdSchema, certificateSubscriptionIdSchema, freeCertificateActionRequestBody, freeCertificateActionRequestSchema, freeCertificateCreateInputShape, freeCertificateCreateRequestSchema, freeCertificatePreflightInputShape, freeCertificatePreflightRequestSchema, freeCertificateRequestIdSchema, freeCertificateStateSchema, safeCertificatePayload, } from "./certificate-contract.js";
|
|
@@ -17,14 +18,21 @@ const GRAPH_METRICS = [
|
|
|
17
18
|
"fc_block", "fc_net_native", "fc_pressure",
|
|
18
19
|
];
|
|
19
20
|
const GRAPH_PERIODS = ["5m", "15m", "30m", "1h", "12h", "1d", "3d", "7d", "30d", "60d", "90d", "1y"];
|
|
21
|
+
const GRAPH_FIELDS = {
|
|
22
|
+
io: ["ior", "iow", "iops"],
|
|
23
|
+
net: ["rx", "tx", "rxp", "txp"],
|
|
24
|
+
fc_block: ["read_bytes", "write_bytes", "read_ops", "write_ops", "queue_events", "throttle_events"],
|
|
25
|
+
fc_net_native: ["rx_bytes", "tx_bytes", "rx_packets", "tx_packets", "queue_events"],
|
|
26
|
+
fc_pressure: ["major_faults", "minor_faults", "swap_in", "swap_out", "oom_kills", "alloc_stalls", "async_scan", "direct_scan", "async_reclaim", "direct_reclaim"],
|
|
27
|
+
};
|
|
20
28
|
// A numeric event id, or the lowercase/uppercase `noty` UUID an action returned.
|
|
21
29
|
const EVENT_ID_SCHEMA = z
|
|
22
30
|
.union([
|
|
23
|
-
z.number().int().positive(),
|
|
31
|
+
z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
24
32
|
z.string().regex(/^(?:[0-9]{1,20}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/),
|
|
25
33
|
])
|
|
26
34
|
.describe("Numeric `event` id or `noty` UUID returned by the action");
|
|
27
|
-
const server = new McpServer({ name: "vpsnet", version: "2.1.
|
|
35
|
+
const server = new McpServer({ name: "vpsnet", version: "2.1.1" }, {
|
|
28
36
|
instructions: [
|
|
29
37
|
"This MCP server controls VPSnet.com services, including VPS service management, DNS zones, domain registration, domain contacts, API keys, billing, and related paid actions.",
|
|
30
38
|
"Use the tool descriptions and API-key scopes to choose the correct surface; do not assume this server is limited to VPS-only operations.",
|
|
@@ -172,8 +180,9 @@ const server = new McpServer({ name: "vpsnet", version: "2.1.0" }, {
|
|
|
172
180
|
"Management actions are durable and idempotent. Reuse an idempotency key only for an exact retry of the same action. If an outcome is reconciliation_required or outcome_ambiguous, use refresh_certificate and read state; do not submit the mutation again with a new key.",
|
|
173
181
|
].join("\n"),
|
|
174
182
|
});
|
|
183
|
+
installToolResultErrorFlag(server);
|
|
175
184
|
// Helper to build service settings path
|
|
176
|
-
const svc = (orderNo, action) => `/account/services/${orderNo}/${action}`;
|
|
185
|
+
const svc = (orderNo, action) => `/account/services/${encodeURIComponent(serviceOrderNoSchema.parse(orderNo))}/${action}`;
|
|
177
186
|
const idempotencyKeySchema = z
|
|
178
187
|
.string()
|
|
179
188
|
.min(8)
|
|
@@ -186,7 +195,13 @@ const serviceOrderNoSchema = z
|
|
|
186
195
|
.string()
|
|
187
196
|
.min(1)
|
|
188
197
|
.max(64)
|
|
189
|
-
.
|
|
198
|
+
.regex(/^[A-Za-z0-9-]+$/, "Use the exact service order number returned by list_services")
|
|
199
|
+
.describe("Tenant-owned service order number: 1-64 ASCII letters, digits or hyphens, e.g. VP88318 or VP88318-1");
|
|
200
|
+
const positiveIdSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
201
|
+
const snapshotNameSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/);
|
|
202
|
+
const invocationIdSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9-]+$/);
|
|
203
|
+
const invoiceHashSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9]+$/);
|
|
204
|
+
const dnsTemplateIdSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
|
|
190
205
|
const serviceHostnameSchema = z
|
|
191
206
|
.string()
|
|
192
207
|
.min(3)
|
|
@@ -211,10 +226,7 @@ const servicePtrSchema = z
|
|
|
211
226
|
canonical.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label)));
|
|
212
227
|
}, "PTR must be a 3-253 character hostname with 1-63 character labels and no reserved VPSnet name")
|
|
213
228
|
.describe("PTR hostname. Canonical length 3-253, labels 1-63 ASCII alphanumeric/hyphen characters, no leading/trailing hyphen; an optional trailing dot is removed");
|
|
214
|
-
const applicationOrderNoSchema =
|
|
215
|
-
.string()
|
|
216
|
-
.regex(/^[A-Z]{2}[0-9]+$/)
|
|
217
|
-
.describe("Tenant-owned service order number, e.g. VP88146");
|
|
229
|
+
const applicationOrderNoSchema = serviceOrderNoSchema;
|
|
218
230
|
const applicationInstallationIdSchema = z
|
|
219
231
|
.string()
|
|
220
232
|
.uuid()
|
|
@@ -231,7 +243,7 @@ const applicationVariablesSchema = z
|
|
|
231
243
|
.optional()
|
|
232
244
|
.describe("Configuration values keyed by the public variable names in the catalog. Values are submitted to VPSnet but never returned by this tool.");
|
|
233
245
|
function applicationPath(orderNo, suffix) {
|
|
234
|
-
return `/account/services/${encodeURIComponent(orderNo)}/applications/${suffix}`;
|
|
246
|
+
return `/account/services/${encodeURIComponent(serviceOrderNoSchema.parse(orderNo))}/applications/${suffix}`;
|
|
235
247
|
}
|
|
236
248
|
function safeApplicationMutationResult(status, data, orderNo) {
|
|
237
249
|
return formatJson(safeApplicationMutationPayload(status, data, `/management/service/${encodeURIComponent(orderNo)}/applications`));
|
|
@@ -344,32 +356,36 @@ server.registerTool("list_services", {
|
|
|
344
356
|
server.registerTool("get_service", {
|
|
345
357
|
description: "Get detailed info for a service by order number. Resource-usage rows include available; false means the numeric zero is a compatibility placeholder, not a measurement. Requires services:read when called with an API key.",
|
|
346
358
|
inputSchema: {
|
|
347
|
-
orderNo:
|
|
359
|
+
orderNo: serviceOrderNoSchema,
|
|
348
360
|
},
|
|
349
361
|
}, async ({ orderNo }) => {
|
|
350
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}`);
|
|
362
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}`);
|
|
351
363
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
352
364
|
});
|
|
353
365
|
server.registerTool("get_service_graphs", {
|
|
354
366
|
description: "Get one performance graph series for a service: pick a metric and a period. fc_block, fc_net_native and fc_pressure exist only for Firecracker VPS. fields narrows io/net/fc_* metrics to some series. A wrong metric, period or field answers 422 with invalidParameter and allowedValues. Requires services:read when called with an API key.",
|
|
355
367
|
inputSchema: {
|
|
356
|
-
orderNo:
|
|
368
|
+
orderNo: serviceOrderNoSchema,
|
|
357
369
|
metric: z
|
|
358
370
|
.enum(GRAPH_METRICS)
|
|
359
371
|
.describe("Metric: cpu, ram, ssd (disk use; storage is the same), swap, lavg (load average), io, inode, net, proc, or a Firecracker-only fc_* metric"),
|
|
360
372
|
period: z.enum(GRAPH_PERIODS).describe("Time window ending now"),
|
|
361
373
|
fields: z
|
|
362
|
-
.array(z.string().regex(/^[a-z_]+$/))
|
|
374
|
+
.array(z.string().max(32).regex(/^[a-z_]+$/))
|
|
363
375
|
.min(1)
|
|
376
|
+
.max(10)
|
|
364
377
|
.optional()
|
|
365
378
|
.describe("Only these series. io: ior, iow, iops. net: rx, tx, rxp, txp. fc_block: read_bytes, write_bytes, read_ops, write_ops, queue_events, throttle_events. fc_net_native: rx_bytes, tx_bytes, rx_packets, tx_packets, queue_events. fc_pressure: major_faults, minor_faults, swap_in, swap_out, oom_kills, alloc_stalls, async_scan, direct_scan, async_reclaim, direct_reclaim."),
|
|
366
379
|
},
|
|
367
380
|
}, async ({ orderNo, metric, period, fields }) => {
|
|
381
|
+
if (fields !== undefined && fields.some((field) => !GRAPH_FIELDS[metric]?.includes(field))) {
|
|
382
|
+
throw new Error(`Invalid graph fields for ${metric}; allowed fields: ${(GRAPH_FIELDS[metric] || []).join(", ") || "none"}`);
|
|
383
|
+
}
|
|
368
384
|
const query = new URLSearchParams({ m: metric, p: period });
|
|
369
385
|
if (fields !== undefined) {
|
|
370
386
|
query.set("f", fields.join(","));
|
|
371
387
|
}
|
|
372
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/graphs?${query.toString()}`);
|
|
388
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/graphs?${query.toString()}`);
|
|
373
389
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
374
390
|
});
|
|
375
391
|
server.registerTool("get_event", {
|
|
@@ -408,10 +424,10 @@ server.registerTool("wait_for_event", {
|
|
|
408
424
|
server.registerTool("get_service_history", {
|
|
409
425
|
description: "Get action history for a service Requires services:read when called with an API key.",
|
|
410
426
|
inputSchema: {
|
|
411
|
-
orderNo:
|
|
427
|
+
orderNo: serviceOrderNoSchema,
|
|
412
428
|
},
|
|
413
429
|
}, async ({ orderNo }) => {
|
|
414
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/history`);
|
|
430
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/history`);
|
|
415
431
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
416
432
|
});
|
|
417
433
|
server.registerTool("get_service_rescue", {
|
|
@@ -426,7 +442,7 @@ server.registerTool("get_service_rescue", {
|
|
|
426
442
|
idempotentHint: true,
|
|
427
443
|
},
|
|
428
444
|
}, async ({ orderNo }) => {
|
|
429
|
-
const { status, data } = await apiRequest("GET", svc(
|
|
445
|
+
const { status, data } = await apiRequest("GET", svc(orderNo, "rescue"));
|
|
430
446
|
return {
|
|
431
447
|
content: [{
|
|
432
448
|
type: "text",
|
|
@@ -454,7 +470,7 @@ server.registerTool("enter_service_rescue", {
|
|
|
454
470
|
if (acknowledge_reboot !== true) {
|
|
455
471
|
throw new Error("Explicit reboot acknowledgement is required.");
|
|
456
472
|
}
|
|
457
|
-
const current = await apiRequest("GET", svc(
|
|
473
|
+
const current = await apiRequest("GET", svc(orderNo, "rescue"));
|
|
458
474
|
const statusPayload = parseServiceRescueStatus(current.data);
|
|
459
475
|
const advertised = statusPayload?.rescue.capability.images.some((candidate) => candidate.id === image);
|
|
460
476
|
if (current.status < 200 ||
|
|
@@ -472,7 +488,7 @@ server.registerTool("enter_service_rescue", {
|
|
|
472
488
|
}],
|
|
473
489
|
};
|
|
474
490
|
}
|
|
475
|
-
const { status, data } = await apiRequest("POST", svc(
|
|
491
|
+
const { status, data } = await apiRequest("POST", svc(orderNo, "rescue"), serviceRescueEnterRequestBody(image), { "Idempotency-Key": idempotencyKey });
|
|
476
492
|
return {
|
|
477
493
|
content: [{
|
|
478
494
|
type: "text",
|
|
@@ -503,7 +519,7 @@ server.registerTool("exit_service_rescue", {
|
|
|
503
519
|
if (acknowledge_restart !== true) {
|
|
504
520
|
throw new Error("Explicit restart acknowledgement is required.");
|
|
505
521
|
}
|
|
506
|
-
const current = await apiRequest("GET", svc(
|
|
522
|
+
const current = await apiRequest("GET", svc(orderNo, "rescue"));
|
|
507
523
|
const statusPayload = parseServiceRescueStatus(current.data);
|
|
508
524
|
const currentSession = statusPayload?.rescue.session;
|
|
509
525
|
if (current.status < 200 ||
|
|
@@ -522,7 +538,7 @@ server.registerTool("exit_service_rescue", {
|
|
|
522
538
|
}],
|
|
523
539
|
};
|
|
524
540
|
}
|
|
525
|
-
const { status, data } = await apiRequest("DELETE", svc(
|
|
541
|
+
const { status, data } = await apiRequest("DELETE", svc(orderNo, "rescue"), undefined, { "Idempotency-Key": idempotencyKey });
|
|
526
542
|
return {
|
|
527
543
|
content: [{
|
|
528
544
|
type: "text",
|
|
@@ -933,7 +949,7 @@ server.registerTool("validate_application_recipe", {
|
|
|
933
949
|
},
|
|
934
950
|
}, async ({ orderNo, compose_yaml, registry_credential_ids }) => {
|
|
935
951
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
936
|
-
const { status, data } = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${id}`, "validation", { compose_yaml, registry_credential_ids });
|
|
952
|
+
const { status, data } = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${encodeURIComponent(String(id))}`, "validation", { compose_yaml, registry_credential_ids });
|
|
937
953
|
return {
|
|
938
954
|
content: [{
|
|
939
955
|
type: "text",
|
|
@@ -1002,7 +1018,7 @@ server.registerTool("create_application_recipe", {
|
|
|
1002
1018
|
},
|
|
1003
1019
|
}, async ({ orderNo, name, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1004
1020
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
1005
|
-
const validation = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${id}`, "validation", { compose_yaml, registry_credential_ids });
|
|
1021
|
+
const validation = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${encodeURIComponent(String(id))}`, "validation", { compose_yaml, registry_credential_ids });
|
|
1006
1022
|
if (validation.status >= 400
|
|
1007
1023
|
|| !applicationRecipeValidationPassed(validation.data)) {
|
|
1008
1024
|
return {
|
|
@@ -1059,7 +1075,7 @@ server.registerTool("create_application_recipe_revision", {
|
|
|
1059
1075
|
},
|
|
1060
1076
|
}, async ({ orderNo, project_id, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1061
1077
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
1062
|
-
const validation = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${id}`, "validation", { compose_yaml, registry_credential_ids });
|
|
1078
|
+
const validation = await runApplicationAsyncOperation(`${basePath}/validate`, (id) => `${basePath}/validations/${encodeURIComponent(String(id))}`, "validation", { compose_yaml, registry_credential_ids });
|
|
1063
1079
|
if (validation.status >= 400
|
|
1064
1080
|
|| !applicationRecipeValidationPassed(validation.data)) {
|
|
1065
1081
|
return {
|
|
@@ -1163,7 +1179,7 @@ server.registerTool("discover_service_containers", {
|
|
|
1163
1179
|
},
|
|
1164
1180
|
}, async ({ orderNo }) => {
|
|
1165
1181
|
const basePath = applicationPath(orderNo, "container-discoveries");
|
|
1166
|
-
const { status, data } = await runApplicationAsyncOperation(basePath, (id) => `${basePath}/${id}`, "discovery");
|
|
1182
|
+
const { status, data } = await runApplicationAsyncOperation(basePath, (id) => `${basePath}/${encodeURIComponent(String(id))}`, "discovery");
|
|
1167
1183
|
return {
|
|
1168
1184
|
content: [{
|
|
1169
1185
|
type: "text",
|
|
@@ -1186,7 +1202,7 @@ server.registerTool("prepare_application_compose_adoption", {
|
|
|
1186
1202
|
},
|
|
1187
1203
|
}, async ({ orderNo, discovery_id, compose_project }) => {
|
|
1188
1204
|
const discoveryPath = applicationPath(orderNo, `container-discoveries/${encodeURIComponent(discovery_id)}`);
|
|
1189
|
-
const { status, data } = await runApplicationAsyncOperation(`${discoveryPath}/adoptions`, (id) => applicationPath(orderNo, `compose-adoptions/${id}`), "adoption", { compose_project });
|
|
1205
|
+
const { status, data } = await runApplicationAsyncOperation(`${discoveryPath}/adoptions`, (id) => applicationPath(orderNo, `compose-adoptions/${encodeURIComponent(String(id))}`), "adoption", { compose_project });
|
|
1190
1206
|
return {
|
|
1191
1207
|
content: [{
|
|
1192
1208
|
type: "text",
|
|
@@ -1476,7 +1492,7 @@ server.registerTool("cancel_application_action", {
|
|
|
1476
1492
|
server.registerTool("start_service", {
|
|
1477
1493
|
description: "Start a stopped VPS. Returns a numeric event id; follow it with wait_for_event. Requires services:manage and a full-access API key.",
|
|
1478
1494
|
inputSchema: {
|
|
1479
|
-
orderNo:
|
|
1495
|
+
orderNo: serviceOrderNoSchema,
|
|
1480
1496
|
},
|
|
1481
1497
|
}, async ({ orderNo }) => {
|
|
1482
1498
|
const { data } = await apiRequest("POST", svc(orderNo, "start"));
|
|
@@ -1485,7 +1501,7 @@ server.registerTool("start_service", {
|
|
|
1485
1501
|
server.registerTool("stop_service", {
|
|
1486
1502
|
description: "Stop a running VPS. Returns a numeric event id; follow it with wait_for_event. Requires services:manage and a full-access API key.",
|
|
1487
1503
|
inputSchema: {
|
|
1488
|
-
orderNo:
|
|
1504
|
+
orderNo: serviceOrderNoSchema,
|
|
1489
1505
|
},
|
|
1490
1506
|
}, async ({ orderNo }) => {
|
|
1491
1507
|
const { data } = await apiRequest("POST", svc(orderNo, "stop"));
|
|
@@ -1494,7 +1510,7 @@ server.registerTool("stop_service", {
|
|
|
1494
1510
|
server.registerTool("restart_service", {
|
|
1495
1511
|
description: "Restart a VPS. Returns a numeric event id; follow it with wait_for_event. Requires services:manage and a full-access API key.",
|
|
1496
1512
|
inputSchema: {
|
|
1497
|
-
orderNo:
|
|
1513
|
+
orderNo: serviceOrderNoSchema,
|
|
1498
1514
|
},
|
|
1499
1515
|
}, async ({ orderNo }) => {
|
|
1500
1516
|
const { data } = await apiRequest("POST", svc(orderNo, "restart"));
|
|
@@ -1503,7 +1519,7 @@ server.registerTool("restart_service", {
|
|
|
1503
1519
|
server.registerTool("console_service", {
|
|
1504
1520
|
description: "Open VNC console access to a running VPS. Read-ish: it requests a console session and returns the tracking event ID (a console URL/token is delivered out-of-band). The service must be running. Requires services:manage and a full-access API key.",
|
|
1505
1521
|
inputSchema: {
|
|
1506
|
-
orderNo:
|
|
1522
|
+
orderNo: serviceOrderNoSchema,
|
|
1507
1523
|
},
|
|
1508
1524
|
}, async ({ orderNo }) => {
|
|
1509
1525
|
const { data } = await apiRequest("POST", svc(orderNo, "console"));
|
|
@@ -1512,7 +1528,7 @@ server.registerTool("console_service", {
|
|
|
1512
1528
|
server.registerTool("suspend_service", {
|
|
1513
1529
|
description: "Suspend a running Cloud VPS (KVM/VDS) service. Changes service state to suspended. Returns a tracking event ID. VDS/Cloud VPS only; the service must be running. Requires services:manage and a full-access API key.",
|
|
1514
1530
|
inputSchema: {
|
|
1515
|
-
orderNo:
|
|
1531
|
+
orderNo: serviceOrderNoSchema,
|
|
1516
1532
|
},
|
|
1517
1533
|
}, async ({ orderNo }) => {
|
|
1518
1534
|
const { data } = await apiRequest("POST", svc(orderNo, "suspend"));
|
|
@@ -1521,7 +1537,7 @@ server.registerTool("suspend_service", {
|
|
|
1521
1537
|
server.registerTool("resume_service", {
|
|
1522
1538
|
description: "Resume a suspended Cloud VPS (KVM/VDS) service. Changes service state back to running. Returns a tracking event ID. VDS/Cloud VPS only; the service must be suspended. Requires services:manage and a full-access API key.",
|
|
1523
1539
|
inputSchema: {
|
|
1524
|
-
orderNo:
|
|
1540
|
+
orderNo: serviceOrderNoSchema,
|
|
1525
1541
|
},
|
|
1526
1542
|
}, async ({ orderNo }) => {
|
|
1527
1543
|
const { data } = await apiRequest("POST", svc(orderNo, "resume"));
|
|
@@ -1559,7 +1575,7 @@ server.registerTool("reset_hostname", {
|
|
|
1559
1575
|
server.registerTool("change_root_password", {
|
|
1560
1576
|
description: "Change VPS root password. Rules: 6-40 chars, alphanumeric, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'. Requires services:manage and a full-access API key.",
|
|
1561
1577
|
inputSchema: {
|
|
1562
|
-
orderNo:
|
|
1578
|
+
orderNo: serviceOrderNoSchema,
|
|
1563
1579
|
password: z
|
|
1564
1580
|
.string()
|
|
1565
1581
|
.describe("New root password. 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit"),
|
|
@@ -1601,7 +1617,7 @@ server.registerTool("clear_rdns", {
|
|
|
1601
1617
|
server.registerTool("flush_iptables", {
|
|
1602
1618
|
description: "Flush iptables rules on VPS (useful when locked out) Requires services:manage and a full-access API key.",
|
|
1603
1619
|
inputSchema: {
|
|
1604
|
-
orderNo:
|
|
1620
|
+
orderNo: serviceOrderNoSchema,
|
|
1605
1621
|
},
|
|
1606
1622
|
}, async ({ orderNo }) => {
|
|
1607
1623
|
const { data } = await apiRequest("POST", svc(orderNo, "flush-ip-tables"));
|
|
@@ -1634,7 +1650,7 @@ server.registerTool("change_title", {
|
|
|
1634
1650
|
server.registerTool("toggle_ipv6", {
|
|
1635
1651
|
description: "Enable or disable IPv6 on VPS Requires services:manage and a full-access API key.",
|
|
1636
1652
|
inputSchema: {
|
|
1637
|
-
orderNo:
|
|
1653
|
+
orderNo: serviceOrderNoSchema,
|
|
1638
1654
|
value: z.boolean().describe("true to enable, false to disable"),
|
|
1639
1655
|
},
|
|
1640
1656
|
}, async ({ orderNo, value }) => {
|
|
@@ -1644,7 +1660,7 @@ server.registerTool("toggle_ipv6", {
|
|
|
1644
1660
|
server.registerTool("toggle_extra_settings", {
|
|
1645
1661
|
description: "Toggle extra VPS settings: ppp, fuse, tuntap, or nfs Requires services:manage and a full-access API key.",
|
|
1646
1662
|
inputSchema: {
|
|
1647
|
-
orderNo:
|
|
1663
|
+
orderNo: serviceOrderNoSchema,
|
|
1648
1664
|
name: z
|
|
1649
1665
|
.enum(["ppp", "fuse", "tuntap", "nfs"])
|
|
1650
1666
|
.describe("Setting name"),
|
|
@@ -1657,8 +1673,8 @@ server.registerTool("toggle_extra_settings", {
|
|
|
1657
1673
|
server.registerTool("deploy_ssh_key", {
|
|
1658
1674
|
description: "Deploy an SSH key to VPS. Returns a noty UUID; follow it with wait_for_event. ASYNC — wait 15-30 seconds after deploying before attempting SSH. Use list_ssh_keys to get available key IDs. To add your own key first: read ~/.ssh/id_rsa.pub from local machine, then create_ssh_key, then deploy it here. Requires services:manage and a full-access API key.",
|
|
1659
1675
|
inputSchema: {
|
|
1660
|
-
orderNo:
|
|
1661
|
-
ssh_key:
|
|
1676
|
+
orderNo: serviceOrderNoSchema,
|
|
1677
|
+
ssh_key: positiveIdSchema.describe("SSH key ID from list_ssh_keys"),
|
|
1662
1678
|
},
|
|
1663
1679
|
}, async ({ orderNo, ssh_key }) => {
|
|
1664
1680
|
const { data } = await apiRequest("POST", svc(orderNo, "change-ssh-key"), { ssh_key });
|
|
@@ -1668,7 +1684,7 @@ server.registerTool("deploy_ssh_key", {
|
|
|
1668
1684
|
server.registerTool("get_os_options", {
|
|
1669
1685
|
description: "Get available OS templates for reinstall Requires services:read when called with an API key.",
|
|
1670
1686
|
inputSchema: {
|
|
1671
|
-
orderNo:
|
|
1687
|
+
orderNo: serviceOrderNoSchema,
|
|
1672
1688
|
},
|
|
1673
1689
|
}, async ({ orderNo }) => {
|
|
1674
1690
|
const { data } = await apiRequest("GET", svc(orderNo, "change-os"));
|
|
@@ -1677,7 +1693,7 @@ server.registerTool("get_os_options", {
|
|
|
1677
1693
|
server.registerTool("reinstall_os", {
|
|
1678
1694
|
description: "Reinstall OS on VPS. WARNING: destroys all data! If the service supports snapshots (Cloud VPS or Firecracker VPS), take one first — it's free for an initial window, so it's cheap insurance you can roll back to; then DELETE it once the reinstall succeeds, because after the free window it is billed per GB while kept (Cloud VPS snapshots do NOT auto-expire) — never leave snapshots lying around. Container VPS and Dedicated have no snapshots, so there is no rollback safety net — confirm with the user before reinstalling. Returns noty UUID. Password rules: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Requires services:manage and a full-access API key.",
|
|
1679
1695
|
inputSchema: {
|
|
1680
|
-
orderNo:
|
|
1696
|
+
orderNo: serviceOrderNoSchema,
|
|
1681
1697
|
osVersion: z
|
|
1682
1698
|
.number()
|
|
1683
1699
|
.describe("OS version ID from get_os_options"),
|
|
@@ -1697,7 +1713,7 @@ server.registerTool("reinstall_os", {
|
|
|
1697
1713
|
server.registerTool("get_plan_options", {
|
|
1698
1714
|
description: "Get available plans for upgrade/downgrade. Plan changes are FREE. Requires services:read when called with an API key.",
|
|
1699
1715
|
inputSchema: {
|
|
1700
|
-
orderNo:
|
|
1716
|
+
orderNo: serviceOrderNoSchema,
|
|
1701
1717
|
},
|
|
1702
1718
|
}, async ({ orderNo }) => {
|
|
1703
1719
|
const { data } = await apiRequest("GET", svc(orderNo, "plans-options"));
|
|
@@ -1706,18 +1722,18 @@ server.registerTool("get_plan_options", {
|
|
|
1706
1722
|
server.registerTool("get_plan_resources", {
|
|
1707
1723
|
description: "Get configurable resources for a specific plan Requires services:read when called with an API key.",
|
|
1708
1724
|
inputSchema: {
|
|
1709
|
-
orderNo:
|
|
1710
|
-
plan:
|
|
1725
|
+
orderNo: serviceOrderNoSchema,
|
|
1726
|
+
plan: positiveIdSchema.describe("Plan ID from get_plan_options"),
|
|
1711
1727
|
},
|
|
1712
1728
|
}, async ({ orderNo, plan }) => {
|
|
1713
|
-
const { data } = await apiRequest("GET", svc(orderNo, `plans-options/${plan}/options`));
|
|
1729
|
+
const { data } = await apiRequest("GET", svc(orderNo, `plans-options/${encodeURIComponent(String(plan))}/options`));
|
|
1714
1730
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1715
1731
|
});
|
|
1716
1732
|
server.registerTool("calculate_plan_change", {
|
|
1717
1733
|
description: "Preview a plan change. No payment is taken: the unused value of the current plan is converted at the new plan's price, so an upgrade moves the expiry EARLIER and a downgrade moves it later. Returns expirationDate, currentExpirationDate, expirationChangeDays (negative = shorter) and paymentRequired=false; show the customer both dates. Use get_plan_resources first to see available resource IDs for the target plan. Requires services:read when called with an API key.",
|
|
1718
1734
|
inputSchema: {
|
|
1719
|
-
orderNo:
|
|
1720
|
-
plan:
|
|
1735
|
+
orderNo: serviceOrderNoSchema,
|
|
1736
|
+
plan: positiveIdSchema.describe("Plan ID from get_plan_options"),
|
|
1721
1737
|
resources: z
|
|
1722
1738
|
.array(z.number())
|
|
1723
1739
|
.describe("Array of numeric resource value IDs — one per resource type (RAM, SSD, IP, etc.). Get IDs from get_plan_resources response: each resource type has 'values' array, pick one value's 'id' per type. A type left out keeps its current value; an id the target plan does not offer is refused with 422 invalidResources."),
|
|
@@ -1729,8 +1745,8 @@ server.registerTool("calculate_plan_change", {
|
|
|
1729
1745
|
server.registerTool("change_plan", {
|
|
1730
1746
|
description: "Change VPS plan. No payment is taken; the remaining value is converted at the new price, so an upgrade shortens the service period. Always call calculate_plan_change first and confirm the new expiry with the customer. Use get_plan_resources to get resource IDs for the target plan. Requires services:manage and a full-access API key.",
|
|
1731
1747
|
inputSchema: {
|
|
1732
|
-
orderNo:
|
|
1733
|
-
plan:
|
|
1748
|
+
orderNo: serviceOrderNoSchema,
|
|
1749
|
+
plan: positiveIdSchema.describe("Plan ID from get_plan_options"),
|
|
1734
1750
|
resources: z
|
|
1735
1751
|
.array(z.number())
|
|
1736
1752
|
.describe("Array of numeric resource value IDs — one per resource type (RAM, SSD, IP, etc.). Get IDs from get_plan_resources response: each resource type has 'values' array, pick one value's 'id' per type. A type left out keeps its current value; an id the target plan does not offer is refused with 422 invalidResources."),
|
|
@@ -1743,7 +1759,7 @@ server.registerTool("change_plan", {
|
|
|
1743
1759
|
server.registerTool("get_period_options", {
|
|
1744
1760
|
description: "Get billing period and auto-renewal options Requires services:read when called with an API key.",
|
|
1745
1761
|
inputSchema: {
|
|
1746
|
-
orderNo:
|
|
1762
|
+
orderNo: serviceOrderNoSchema,
|
|
1747
1763
|
},
|
|
1748
1764
|
}, async ({ orderNo }) => {
|
|
1749
1765
|
const { data } = await apiRequest("GET", svc(orderNo, "periods-options"));
|
|
@@ -1752,7 +1768,7 @@ server.registerTool("get_period_options", {
|
|
|
1752
1768
|
server.registerTool("set_auto_renew", {
|
|
1753
1769
|
description: "Enable or disable auto-renewal for a service. Note: enabling auto-renewal will automatically charge the account balance at each renewal (creating an invoice) without further confirmation. Requires services:manage and a full-access API key.",
|
|
1754
1770
|
inputSchema: {
|
|
1755
|
-
orderNo:
|
|
1771
|
+
orderNo: serviceOrderNoSchema,
|
|
1756
1772
|
state: z.boolean().describe("true to enable, false to disable"),
|
|
1757
1773
|
period: z
|
|
1758
1774
|
.number()
|
|
@@ -1769,8 +1785,8 @@ server.registerTool("set_auto_renew", {
|
|
|
1769
1785
|
server.registerTool("renew_service", {
|
|
1770
1786
|
description: "Manually renew a service for a specific period. COST WARNING: this charges the account balance / creates an invoice immediately, and renewal payments are NON-REFUNDABLE once confirmed. Verify the service and period with the user before calling. Payment object: { payment: 1, successUrl: '', cancelUrl: '' } for balance payment. Requires services:manage and a full-access API key.",
|
|
1771
1787
|
inputSchema: {
|
|
1772
|
-
orderNo:
|
|
1773
|
-
period:
|
|
1788
|
+
orderNo: serviceOrderNoSchema,
|
|
1789
|
+
period: positiveIdSchema.describe("Period ID from get_period_options"),
|
|
1774
1790
|
payment: z
|
|
1775
1791
|
.object({
|
|
1776
1792
|
payment: z
|
|
@@ -1799,16 +1815,16 @@ server.registerTool("get_order_plans", {
|
|
|
1799
1815
|
.describe("Required service product: firecracker (Linux VPS microVM), vds (Cloud VPS/KVM with Linux, Windows, or BSD), vps (Container VPS/Linux), or ds (dedicated server). Enum order is not a recommendation."),
|
|
1800
1816
|
},
|
|
1801
1817
|
}, async ({ type }) => {
|
|
1802
|
-
const { data } = await apiRequest("GET", `/order/configuration/${type}/plans`);
|
|
1818
|
+
const { data } = await apiRequest("GET", `/order/configuration/${encodeURIComponent(type)}/plans`);
|
|
1803
1819
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1804
1820
|
});
|
|
1805
1821
|
server.registerTool("get_order_options", {
|
|
1806
1822
|
description: "Get configurable options (OS, resources, periods) for a plan",
|
|
1807
1823
|
inputSchema: {
|
|
1808
|
-
plan:
|
|
1824
|
+
plan: positiveIdSchema.describe("Plan ID from get_order_plans"),
|
|
1809
1825
|
},
|
|
1810
1826
|
}, async ({ plan }) => {
|
|
1811
|
-
const { data } = await apiRequest("GET", `/order/configuration/${plan}/options`);
|
|
1827
|
+
const { data } = await apiRequest("GET", `/order/configuration/${encodeURIComponent(String(plan))}/options`);
|
|
1812
1828
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1813
1829
|
});
|
|
1814
1830
|
server.registerTool("order_service", {
|
|
@@ -1821,8 +1837,8 @@ server.registerTool("order_service", {
|
|
|
1821
1837
|
"sshKey and rootPassword are mutually exclusive — provide one or the other.",
|
|
1822
1838
|
].join(" "),
|
|
1823
1839
|
inputSchema: {
|
|
1824
|
-
plan:
|
|
1825
|
-
os:
|
|
1840
|
+
plan: positiveIdSchema.describe("Plan ID from get_order_plans"),
|
|
1841
|
+
os: positiveIdSchema.optional().describe("OS version ID from get_order_options"),
|
|
1826
1842
|
rootPassword: z
|
|
1827
1843
|
.string()
|
|
1828
1844
|
.optional()
|
|
@@ -1831,7 +1847,7 @@ server.registerTool("order_service", {
|
|
|
1831
1847
|
.number()
|
|
1832
1848
|
.optional()
|
|
1833
1849
|
.describe("SSH key ID from list_ssh_keys to deploy. Mutually exclusive with rootPassword"),
|
|
1834
|
-
period:
|
|
1850
|
+
period: positiveIdSchema.optional().describe("Billing period ID from get_order_options"),
|
|
1835
1851
|
resources: z
|
|
1836
1852
|
.array(z.number())
|
|
1837
1853
|
.optional()
|
|
@@ -1864,7 +1880,10 @@ server.registerTool("order_service", {
|
|
|
1864
1880
|
body.period = period;
|
|
1865
1881
|
if (resources)
|
|
1866
1882
|
body.resources = resources;
|
|
1867
|
-
const { data: quoteData } = await apiRequest("POST", "/order/configuration/quote", body, { "Idempotency-Key": idempotencyKey });
|
|
1883
|
+
const { status: quoteStatus, data: quoteData } = await apiRequest("POST", "/order/configuration/quote", body, { "Idempotency-Key": idempotencyKey });
|
|
1884
|
+
if (quoteStatus >= 400 || quoteData?.success === false) {
|
|
1885
|
+
return { content: [{ type: "text", text: formatJson(quoteData) }], isError: true };
|
|
1886
|
+
}
|
|
1868
1887
|
const quoteToken = quoteData.quoteToken;
|
|
1869
1888
|
if (!quoteToken) {
|
|
1870
1889
|
throw new Error("Order quote did not return quoteToken");
|
|
@@ -1877,7 +1896,7 @@ server.registerTool("order_service", {
|
|
|
1877
1896
|
server.registerTool("get_backup_status", {
|
|
1878
1897
|
description: "Get backup status and configuration for a service Requires services:read when called with an API key.",
|
|
1879
1898
|
inputSchema: {
|
|
1880
|
-
orderNo:
|
|
1899
|
+
orderNo: serviceOrderNoSchema,
|
|
1881
1900
|
},
|
|
1882
1901
|
}, async ({ orderNo }) => {
|
|
1883
1902
|
const { data } = await apiRequest("GET", svc(orderNo, "backup/status"));
|
|
@@ -1886,7 +1905,7 @@ server.registerTool("get_backup_status", {
|
|
|
1886
1905
|
server.registerTool("get_backup_history", {
|
|
1887
1906
|
description: "Get backup history for a service Requires services:read when called with an API key.",
|
|
1888
1907
|
inputSchema: {
|
|
1889
|
-
orderNo:
|
|
1908
|
+
orderNo: serviceOrderNoSchema,
|
|
1890
1909
|
},
|
|
1891
1910
|
}, async ({ orderNo }) => {
|
|
1892
1911
|
const { data } = await apiRequest("GET", svc(orderNo, "backup/history"));
|
|
@@ -1895,7 +1914,7 @@ server.registerTool("get_backup_history", {
|
|
|
1895
1914
|
server.registerTool("create_backup", {
|
|
1896
1915
|
description: "Create a new backup. Returns noty UUID for tracking. First call get_backup_status to see available period dates and price. Backup is a paid operation (price shown in get_backup_status). Requires services:manage and a full-access API key.",
|
|
1897
1916
|
inputSchema: {
|
|
1898
|
-
orderNo:
|
|
1917
|
+
orderNo: serviceOrderNoSchema,
|
|
1899
1918
|
period: z
|
|
1900
1919
|
.string()
|
|
1901
1920
|
.describe("Backup date in YYYY-MM-DD format. Must be one of the dates from get_backup_status options (up to 7 days in the past)"),
|
|
@@ -1936,10 +1955,10 @@ server.registerTool("list_ssh_keys", {
|
|
|
1936
1955
|
server.registerTool("get_ssh_key", {
|
|
1937
1956
|
description: "Get a specific SSH key by ID",
|
|
1938
1957
|
inputSchema: {
|
|
1939
|
-
id:
|
|
1958
|
+
id: positiveIdSchema.describe("SSH key ID"),
|
|
1940
1959
|
},
|
|
1941
1960
|
}, async ({ id }) => {
|
|
1942
|
-
const { data } = await apiRequest("GET", `/account/ssh-keys/${id}`);
|
|
1961
|
+
const { data } = await apiRequest("GET", `/account/ssh-keys/${encodeURIComponent(String(id))}`);
|
|
1943
1962
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1944
1963
|
});
|
|
1945
1964
|
server.registerTool("create_ssh_key", {
|
|
@@ -1958,10 +1977,10 @@ server.registerTool("create_ssh_key", {
|
|
|
1958
1977
|
server.registerTool("delete_ssh_key", {
|
|
1959
1978
|
description: "Delete an SSH key from the account",
|
|
1960
1979
|
inputSchema: {
|
|
1961
|
-
id:
|
|
1980
|
+
id: positiveIdSchema.describe("SSH key ID"),
|
|
1962
1981
|
},
|
|
1963
1982
|
}, async ({ id }) => {
|
|
1964
|
-
const { data } = await apiRequest("DELETE", `/account/ssh-keys/${id}`);
|
|
1983
|
+
const { data } = await apiRequest("DELETE", `/account/ssh-keys/${encodeURIComponent(String(id))}`);
|
|
1965
1984
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1966
1985
|
});
|
|
1967
1986
|
// --- API Keys ---
|
|
@@ -1975,30 +1994,30 @@ server.registerTool("list_api_keys", {
|
|
|
1975
1994
|
server.registerTool("get_api_key", {
|
|
1976
1995
|
description: "Get metadata for one active API key: scope, granular scopes, paid_scopes, paid_operations_enabled, daily_spend_limit_eur, monthly_spend_limit_eur, allowed IPs, expiry and rate_limit. For API-key callers only the calling key's own ID is accessible — any other id answers 403 apiKeyForbidden, even when it does not exist. The full key and stored secrets are never returned. Read-only.",
|
|
1977
1996
|
inputSchema: {
|
|
1978
|
-
id:
|
|
1997
|
+
id: positiveIdSchema.describe("API key ID from list_api_keys (the calling key's own ID)"),
|
|
1979
1998
|
},
|
|
1980
1999
|
}, async ({ id }) => {
|
|
1981
|
-
const { data } = await apiRequest("GET", `/account/api-keys/${id}`);
|
|
2000
|
+
const { data } = await apiRequest("GET", `/account/api-keys/${encodeURIComponent(String(id))}`);
|
|
1982
2001
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1983
2002
|
});
|
|
1984
2003
|
server.registerTool("get_api_key_activity", {
|
|
1985
2004
|
description: "Get the recorded request activity for this API key: recent matched route patterns and statuses, resolved and separately labelled caller-claimed source addresses, endpoint/source totals, and retained daily history. API-key callers can inspect only the same key that authenticated the MCP connection. Key values, headers, query strings, path values, and request bodies are never returned. To protect the control plane during abusive bursts, audit writes are capped per key per minute and retained detail has a separate per-key row ceiling; totals describe recorded rows and are not a billing ledger.",
|
|
1986
2005
|
inputSchema: {
|
|
1987
|
-
id:
|
|
2006
|
+
id: positiveIdSchema.describe("API key ID from list_api_keys (the calling key's own ID)"),
|
|
1988
2007
|
limit: z.number().int().min(1).max(200).optional().describe("Recent request rows to return (default 50, maximum 200)"),
|
|
1989
2008
|
},
|
|
1990
2009
|
}, async ({ id, limit }) => {
|
|
1991
2010
|
const query = limit === undefined ? "" : `?limit=${encodeURIComponent(String(limit))}`;
|
|
1992
|
-
const { data } = await apiRequest("GET", `/account/api-keys/${id}/activity${query}`);
|
|
2011
|
+
const { data } = await apiRequest("GET", `/account/api-keys/${encodeURIComponent(String(id))}/activity${query}`);
|
|
1993
2012
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1994
2013
|
});
|
|
1995
2014
|
server.registerTool("get_api_key_inference_usage", {
|
|
1996
2015
|
description: "Get exact paid VPSnet AI usage for this inference API key: committed and currently reserved spend for the UTC day and month, request and token totals by public VPSnet model profile, configured spend limits, remaining spend, and notification preferences. API-key callers can inspect only the same key that authenticated the MCP connection. Internal provider model names are never returned. This is read-only and does not change limits, notifications, or billing.",
|
|
1997
2016
|
inputSchema: {
|
|
1998
|
-
id:
|
|
2017
|
+
id: positiveIdSchema.describe("Inference API key ID from list_api_keys (the calling key's own ID)"),
|
|
1999
2018
|
},
|
|
2000
2019
|
}, async ({ id }) => {
|
|
2001
|
-
const { data } = await apiRequest("GET", `/account/api-keys/${id}/inference-usage`);
|
|
2020
|
+
const { data } = await apiRequest("GET", `/account/api-keys/${encodeURIComponent(String(id))}/inference-usage`);
|
|
2002
2021
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2003
2022
|
});
|
|
2004
2023
|
// --- TLS certificates ---
|
|
@@ -2711,10 +2730,10 @@ server.registerTool("list_domains", {
|
|
|
2711
2730
|
server.registerTool("get_domain", {
|
|
2712
2731
|
description: "Get one owned domain with current nameservers and any pending domain action. Requires domains:read when using an API key.",
|
|
2713
2732
|
inputSchema: {
|
|
2714
|
-
domain_id:
|
|
2733
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2715
2734
|
},
|
|
2716
2735
|
}, async ({ domain_id }) => {
|
|
2717
|
-
const { data } = await apiRequest("GET", `/account/domains/${domain_id}`);
|
|
2736
|
+
const { data } = await apiRequest("GET", `/account/domains/${encodeURIComponent(String(domain_id))}`);
|
|
2718
2737
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2719
2738
|
});
|
|
2720
2739
|
server.registerTool("list_domain_tlds", {
|
|
@@ -2727,13 +2746,13 @@ server.registerTool("list_domain_tlds", {
|
|
|
2727
2746
|
server.registerTool("set_domain_nameservers", {
|
|
2728
2747
|
description: "Queue an asynchronous nameserver change for an owned domain. Nameservers must be hostnames, not IP addresses; same-domain nameserver IP records are managed separately. This does not manage DNS records or PTR. Requires domains:manage when using an API key.",
|
|
2729
2748
|
inputSchema: {
|
|
2730
|
-
domain_id:
|
|
2749
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2731
2750
|
nameservers: z.array(nameserverHostnameSchema).min(2).max(13).describe("2-13 nameserver hostnames, e.g. ns1.vpsnet.com, ns2.vpsnet.com. IP addresses are not accepted."),
|
|
2732
2751
|
idempotencyKey: idempotencyKeySchema.optional().describe("Optional idempotency key for the queued domain action"),
|
|
2733
2752
|
},
|
|
2734
2753
|
}, async ({ domain_id, nameservers, idempotencyKey }) => {
|
|
2735
2754
|
const headers = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined;
|
|
2736
|
-
const { data } = await apiRequest("POST", `/account/domains/${domain_id}/nameservers`, {
|
|
2755
|
+
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/nameservers`, {
|
|
2737
2756
|
nameservers,
|
|
2738
2757
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
2739
2758
|
}, headers);
|
|
@@ -2742,48 +2761,48 @@ server.registerTool("set_domain_nameservers", {
|
|
|
2742
2761
|
server.registerTool("list_domain_glue_records", {
|
|
2743
2762
|
description: "List same-domain nameserver IP records for an owned domain, e.g. ns1.example.com -> 203.0.113.10. Requires domains:read when using an API key.",
|
|
2744
2763
|
inputSchema: {
|
|
2745
|
-
domain_id:
|
|
2764
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2746
2765
|
},
|
|
2747
2766
|
}, async ({ domain_id }) => {
|
|
2748
|
-
const { data } = await apiRequest("GET", `/account/domains/${domain_id}/glue`);
|
|
2767
|
+
const { data } = await apiRequest("GET", `/account/domains/${encodeURIComponent(String(domain_id))}/glue`);
|
|
2749
2768
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2750
2769
|
});
|
|
2751
2770
|
server.registerTool("create_domain_glue_record", {
|
|
2752
2771
|
description: "Queue creating or updating a same-domain nameserver IP record for an owned domain. Hostname must be below the domain, addresses must be public IPs, and this does not create DNS A/AAAA records or PTR. Requires domains:manage when using an API key.",
|
|
2753
2772
|
inputSchema: {
|
|
2754
|
-
domain_id:
|
|
2773
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2755
2774
|
hostname: glueHostnameSchema,
|
|
2756
2775
|
addresses: z.array(gluePublicIpSchema).min(1).max(13).describe("Public IPv4/IPv6 addresses for this nameserver host"),
|
|
2757
2776
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2758
2777
|
},
|
|
2759
2778
|
}, async ({ domain_id, hostname, addresses, idempotencyKey }) => {
|
|
2760
|
-
const { data } = await apiRequest("POST", `/account/domains/${domain_id}/glue`, { hostname, addresses, idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2779
|
+
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/glue`, { hostname, addresses, idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2761
2780
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2762
2781
|
});
|
|
2763
2782
|
server.registerTool("delete_domain_glue_record", {
|
|
2764
2783
|
description: "Queue deleting a same-domain nameserver IP record for an owned domain. Remove or change domain nameserver delegation first if the hostname is still delegated. Requires domains:manage when using an API key.",
|
|
2765
2784
|
inputSchema: {
|
|
2766
|
-
domain_id:
|
|
2767
|
-
record_id:
|
|
2785
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2786
|
+
record_id: positiveIdSchema.describe("Record ID from list_domain_glue_records"),
|
|
2768
2787
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2769
2788
|
},
|
|
2770
2789
|
}, async ({ domain_id, record_id, idempotencyKey }) => {
|
|
2771
|
-
const { data } = await apiRequest("DELETE", `/account/domains/${domain_id}/glue/${record_id}`, { idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2790
|
+
const { data } = await apiRequest("DELETE", `/account/domains/${encodeURIComponent(String(domain_id))}/glue/${encodeURIComponent(String(record_id))}`, { idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2772
2791
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2773
2792
|
});
|
|
2774
2793
|
server.registerTool("get_domain_parent_ds", {
|
|
2775
2794
|
description: "List parent-zone DNSSEC DS records for an owned domain. Requires domains:read when using an API key.",
|
|
2776
2795
|
inputSchema: {
|
|
2777
|
-
domain_id:
|
|
2796
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2778
2797
|
},
|
|
2779
2798
|
}, async ({ domain_id }) => {
|
|
2780
|
-
const { data } = await apiRequest("GET", `/account/domains/${domain_id}/dnssec-ds`);
|
|
2799
|
+
const { data } = await apiRequest("GET", `/account/domains/${encodeURIComponent(String(domain_id))}/dnssec-ds`);
|
|
2781
2800
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2782
2801
|
});
|
|
2783
2802
|
server.registerTool("add_domain_parent_ds", {
|
|
2784
2803
|
description: "Queue adding DNSSEC DS records at the parent zone for an owned domain. Requires domains:manage when using an API key.",
|
|
2785
2804
|
inputSchema: {
|
|
2786
|
-
domain_id:
|
|
2805
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2787
2806
|
ds: z
|
|
2788
2807
|
.string()
|
|
2789
2808
|
.min(8)
|
|
@@ -2791,7 +2810,7 @@ server.registerTool("add_domain_parent_ds", {
|
|
|
2791
2810
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2792
2811
|
},
|
|
2793
2812
|
}, async ({ domain_id, ds, idempotencyKey }) => {
|
|
2794
|
-
const { data } = await apiRequest("POST", `/account/domains/${domain_id}/dnssec-ds`,
|
|
2813
|
+
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/dnssec-ds`,
|
|
2795
2814
|
// The API's preferred key is ds_records; bare `ds` is only a
|
|
2796
2815
|
// compatibility fallback, so do not depend on it.
|
|
2797
2816
|
{ ds_records: ds, idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
@@ -2800,7 +2819,7 @@ server.registerTool("add_domain_parent_ds", {
|
|
|
2800
2819
|
server.registerTool("delete_domain_parent_ds", {
|
|
2801
2820
|
description: "Queue deleting DNSSEC DS records at the parent zone for an owned domain. Requires domains:manage when using an API key.",
|
|
2802
2821
|
inputSchema: {
|
|
2803
|
-
domain_id:
|
|
2822
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2804
2823
|
ds: z
|
|
2805
2824
|
.string()
|
|
2806
2825
|
.min(8)
|
|
@@ -2808,7 +2827,7 @@ server.registerTool("delete_domain_parent_ds", {
|
|
|
2808
2827
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2809
2828
|
},
|
|
2810
2829
|
}, async ({ domain_id, ds, idempotencyKey }) => {
|
|
2811
|
-
const { data } = await apiRequest("DELETE", `/account/domains/${domain_id}/dnssec-ds`,
|
|
2830
|
+
const { data } = await apiRequest("DELETE", `/account/domains/${encodeURIComponent(String(domain_id))}/dnssec-ds`,
|
|
2812
2831
|
// The API's preferred key is ds_records; bare `ds` is only a
|
|
2813
2832
|
// compatibility fallback, so do not depend on it.
|
|
2814
2833
|
{ ds_records: ds, idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
@@ -2817,7 +2836,7 @@ server.registerTool("delete_domain_parent_ds", {
|
|
|
2817
2836
|
server.registerTool("check_domain_availability", {
|
|
2818
2837
|
description: "Check read-only domain availability. Requires domains:read when using an API key.",
|
|
2819
2838
|
inputSchema: {
|
|
2820
|
-
domain: z.string().describe("Domain to check, e.g. example.lt or example.com"),
|
|
2839
|
+
domain: z.string().min(1).max(253).describe("Domain to check, e.g. example.lt or example.com"),
|
|
2821
2840
|
},
|
|
2822
2841
|
}, async ({ domain }) => {
|
|
2823
2842
|
const { data } = await apiRequest("GET", `/account/domains/check?domain=${encodeURIComponent(domain)}`);
|
|
@@ -2865,20 +2884,20 @@ server.registerTool("create_domain_contact", {
|
|
|
2865
2884
|
server.registerTool("update_domain_contact", {
|
|
2866
2885
|
description: "Update an existing domain contact. Requires domains:manage when using an API key.",
|
|
2867
2886
|
inputSchema: {
|
|
2868
|
-
id:
|
|
2887
|
+
id: positiveIdSchema.describe("Domain contact ID"),
|
|
2869
2888
|
...domainContactInputSchema,
|
|
2870
2889
|
},
|
|
2871
2890
|
}, async ({ id, ...input }) => {
|
|
2872
|
-
const { data } = await apiRequest("POST", `/account/domains/contacts/${id}`, input);
|
|
2891
|
+
const { data } = await apiRequest("POST", `/account/domains/contacts/${encodeURIComponent(String(id))}`, input);
|
|
2873
2892
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2874
2893
|
});
|
|
2875
2894
|
server.registerTool("delete_domain_contact", {
|
|
2876
2895
|
description: "Delete an unused domain contact. Contacts referenced by domains are rejected. Requires domains:manage when using an API key.",
|
|
2877
2896
|
inputSchema: {
|
|
2878
|
-
id:
|
|
2897
|
+
id: positiveIdSchema.describe("Domain contact ID"),
|
|
2879
2898
|
},
|
|
2880
2899
|
}, async ({ id }) => {
|
|
2881
|
-
const { data } = await apiRequest("DELETE", `/account/domains/contacts/${id}`);
|
|
2900
|
+
const { data } = await apiRequest("DELETE", `/account/domains/contacts/${encodeURIComponent(String(id))}`);
|
|
2882
2901
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2883
2902
|
});
|
|
2884
2903
|
server.registerTool("quote_domain_register", {
|
|
@@ -2886,10 +2905,10 @@ server.registerTool("quote_domain_register", {
|
|
|
2886
2905
|
inputSchema: {
|
|
2887
2906
|
domain: z.string().describe("Domain to register, e.g. example.lt"),
|
|
2888
2907
|
years: z.number().optional().describe("Registration period in years"),
|
|
2889
|
-
registrant_contact_id:
|
|
2890
|
-
admin_contact_id:
|
|
2891
|
-
tech_contact_id:
|
|
2892
|
-
billing_contact_id:
|
|
2908
|
+
registrant_contact_id: positiveIdSchema.optional().describe("Registrant contact ID; defaults to account default contact"),
|
|
2909
|
+
admin_contact_id: positiveIdSchema.optional().describe("Admin contact ID"),
|
|
2910
|
+
tech_contact_id: positiveIdSchema.optional().describe("Technical contact ID"),
|
|
2911
|
+
billing_contact_id: positiveIdSchema.optional().describe("Billing contact ID"),
|
|
2893
2912
|
nameservers: z.array(nameserverHostnameSchema).optional().describe("2-13 nameserver hostnames; defaults to ns1/ns2.vpsnet.com. IP addresses are not accepted."),
|
|
2894
2913
|
whois_privacy: whoisPrivacySchema,
|
|
2895
2914
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this quote; sent as Idempotency-Key"),
|
|
@@ -2904,10 +2923,10 @@ server.registerTool("quote_domain_transfer", {
|
|
|
2904
2923
|
inputSchema: {
|
|
2905
2924
|
domain: z.string().describe("Domain to transfer"),
|
|
2906
2925
|
authCode: z.string().describe("Transfer auth/EPP code"),
|
|
2907
|
-
registrant_contact_id:
|
|
2908
|
-
admin_contact_id:
|
|
2909
|
-
tech_contact_id:
|
|
2910
|
-
billing_contact_id:
|
|
2926
|
+
registrant_contact_id: positiveIdSchema.optional().describe("Registrant contact ID; defaults to account default contact"),
|
|
2927
|
+
admin_contact_id: positiveIdSchema.optional().describe("Admin contact ID"),
|
|
2928
|
+
tech_contact_id: positiveIdSchema.optional().describe("Technical contact ID"),
|
|
2929
|
+
billing_contact_id: positiveIdSchema.optional().describe("Billing contact ID"),
|
|
2911
2930
|
nameservers: z.array(nameserverHostnameSchema).optional().describe("2-13 nameserver hostnames; defaults to ns1/ns2.vpsnet.com. IP addresses are not accepted."),
|
|
2912
2931
|
whois_privacy: whoisPrivacySchema,
|
|
2913
2932
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this quote; sent as Idempotency-Key"),
|
|
@@ -2930,10 +2949,10 @@ server.registerTool("confirm_domain_register", {
|
|
|
2930
2949
|
inputSchema: {
|
|
2931
2950
|
domain: z.string().describe("Domain to register, exactly as quoted"),
|
|
2932
2951
|
years: z.number().optional().describe("Registration period in years, exactly as quoted"),
|
|
2933
|
-
registrant_contact_id:
|
|
2934
|
-
admin_contact_id:
|
|
2935
|
-
tech_contact_id:
|
|
2936
|
-
billing_contact_id:
|
|
2952
|
+
registrant_contact_id: positiveIdSchema.optional().describe("Registrant contact ID used in quote"),
|
|
2953
|
+
admin_contact_id: positiveIdSchema.optional().describe("Admin contact ID used in quote"),
|
|
2954
|
+
tech_contact_id: positiveIdSchema.optional().describe("Technical contact ID used in quote"),
|
|
2955
|
+
billing_contact_id: positiveIdSchema.optional().describe("Billing contact ID used in quote"),
|
|
2937
2956
|
nameservers: z.array(nameserverHostnameSchema).optional().describe("Nameserver hostnames used in quote"),
|
|
2938
2957
|
whois_privacy: whoisPrivacySchema,
|
|
2939
2958
|
idempotencyKey: idempotencyKeySchema.describe("Same idempotencyKey used for quote"),
|
|
@@ -2950,10 +2969,10 @@ server.registerTool("confirm_domain_transfer", {
|
|
|
2950
2969
|
inputSchema: {
|
|
2951
2970
|
domain: z.string().describe("Domain to transfer, exactly as quoted"),
|
|
2952
2971
|
authCode: z.string().describe("Transfer auth/EPP code used in quote; stored encrypted server-side after confirm"),
|
|
2953
|
-
registrant_contact_id:
|
|
2954
|
-
admin_contact_id:
|
|
2955
|
-
tech_contact_id:
|
|
2956
|
-
billing_contact_id:
|
|
2972
|
+
registrant_contact_id: positiveIdSchema.optional().describe("Registrant contact ID used in quote"),
|
|
2973
|
+
admin_contact_id: positiveIdSchema.optional().describe("Admin contact ID used in quote"),
|
|
2974
|
+
tech_contact_id: positiveIdSchema.optional().describe("Technical contact ID used in quote"),
|
|
2975
|
+
billing_contact_id: positiveIdSchema.optional().describe("Billing contact ID used in quote"),
|
|
2957
2976
|
nameservers: z.array(nameserverHostnameSchema).optional().describe("Nameserver hostnames used in quote"),
|
|
2958
2977
|
whois_privacy: whoisPrivacySchema,
|
|
2959
2978
|
idempotencyKey: idempotencyKeySchema.describe("Same idempotencyKey used for quote"),
|
|
@@ -2968,7 +2987,7 @@ server.registerTool("confirm_domain_transfer", {
|
|
|
2968
2987
|
server.registerTool("quote_domain_renew", {
|
|
2969
2988
|
description: "Create a VPSNet-priced renewal quote for an owned domain. Renewal pricing follows the existing domain record. Requires paid domains:renew scope/caps for API keys.",
|
|
2970
2989
|
inputSchema: {
|
|
2971
|
-
domain_id:
|
|
2990
|
+
domain_id: positiveIdSchema.optional().describe("Owned domain ID"),
|
|
2972
2991
|
domain: z.string().optional().describe("Owned domain name if domain_id is not used"),
|
|
2973
2992
|
years: z.number().optional().describe("Renewal period in years"),
|
|
2974
2993
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this quote; sent as Idempotency-Key"),
|
|
@@ -2981,7 +3000,7 @@ server.registerTool("quote_domain_renew", {
|
|
|
2981
3000
|
server.registerTool("confirm_domain_renew", {
|
|
2982
3001
|
description: "Confirm a quoted domain renewal and pay from the VPSNet account. Domain renewal payments are non-refundable once confirmed and the renewal is queued only after payment succeeds. Requires the same idempotencyKey and quoteToken from quote_domain_renew.",
|
|
2983
3002
|
inputSchema: {
|
|
2984
|
-
domain_id:
|
|
3003
|
+
domain_id: positiveIdSchema.optional().describe("Owned domain ID used in quote"),
|
|
2985
3004
|
domain: z.string().optional().describe("Owned domain name used in quote"),
|
|
2986
3005
|
years: z.number().optional().describe("Renewal period used in quote"),
|
|
2987
3006
|
idempotencyKey: idempotencyKeySchema.describe("Same idempotencyKey used for quote"),
|
|
@@ -2996,7 +3015,7 @@ server.registerTool("confirm_domain_renew", {
|
|
|
2996
3015
|
server.registerTool("quote_domain_restore", {
|
|
2997
3016
|
description: "Create a restore quote for an owned domain in redemption. The final price is returned before payment. Requires paid domains:renew scope/caps for API keys.",
|
|
2998
3017
|
inputSchema: {
|
|
2999
|
-
domain_id:
|
|
3018
|
+
domain_id: positiveIdSchema.optional().describe("Owned domain ID"),
|
|
3000
3019
|
domain: z.string().optional().describe("Owned domain name if domain_id is not used"),
|
|
3001
3020
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this quote; sent as Idempotency-Key"),
|
|
3002
3021
|
},
|
|
@@ -3008,7 +3027,7 @@ server.registerTool("quote_domain_restore", {
|
|
|
3008
3027
|
server.registerTool("confirm_domain_restore", {
|
|
3009
3028
|
description: "Confirm a quoted domain restore and pay from the VPSNet account. Domain restore payments are non-refundable once confirmed and the restore is queued only after payment succeeds. Requires the same idempotencyKey and quoteToken from quote_domain_restore.",
|
|
3010
3029
|
inputSchema: {
|
|
3011
|
-
domain_id:
|
|
3030
|
+
domain_id: positiveIdSchema.optional().describe("Owned domain ID used in quote"),
|
|
3012
3031
|
domain: z.string().optional().describe("Owned domain name used in quote"),
|
|
3013
3032
|
idempotencyKey: idempotencyKeySchema.describe("Same idempotencyKey used for quote"),
|
|
3014
3033
|
quoteToken: z.string().min(32).describe("quoteToken returned by quote_domain_restore"),
|
|
@@ -3022,12 +3041,12 @@ server.registerTool("confirm_domain_restore", {
|
|
|
3022
3041
|
server.registerTool("set_domain_auto_renew", {
|
|
3023
3042
|
description: "Enable or disable automatic renewal for an owned domain. Auto-renew charges the VPSNet account; automatic domain renewal payments are non-refundable once confirmed.",
|
|
3024
3043
|
inputSchema: {
|
|
3025
|
-
domain_id:
|
|
3044
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
3026
3045
|
enabled: z.boolean().describe("Whether automatic renewal should be enabled"),
|
|
3027
3046
|
renewal_period_years: z.number().min(1).max(10).optional().describe("Renewal period in years; defaults to the current domain setting"),
|
|
3028
3047
|
},
|
|
3029
3048
|
}, async ({ domain_id, enabled, renewal_period_years }) => {
|
|
3030
|
-
const { data } = await apiRequest("POST", `/account/domains/${domain_id}/auto-renew`, {
|
|
3049
|
+
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/auto-renew`, {
|
|
3031
3050
|
enabled,
|
|
3032
3051
|
...(renewal_period_years ? { renewal_period_years } : {}),
|
|
3033
3052
|
});
|
|
@@ -3036,26 +3055,26 @@ server.registerTool("set_domain_auto_renew", {
|
|
|
3036
3055
|
server.registerTool("get_registrar_lock", {
|
|
3037
3056
|
description: "Get the registrar transfer-lock status for an owned domain (locked / unlocked / unsupported). A locked domain cannot be transferred away until unlocked. Changing the lock is intentionally not available via API key (use the control panel). Requires domains:read when using an API key.",
|
|
3038
3057
|
inputSchema: {
|
|
3039
|
-
domain_id:
|
|
3058
|
+
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
3040
3059
|
},
|
|
3041
3060
|
}, async ({ domain_id }) => {
|
|
3042
|
-
const { data } = await apiRequest("GET", `/account/domains/${domain_id}/registrar-lock`);
|
|
3061
|
+
const { data } = await apiRequest("GET", `/account/domains/${encodeURIComponent(String(domain_id))}/registrar-lock`);
|
|
3043
3062
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3044
3063
|
});
|
|
3045
3064
|
server.registerTool("list_service_dns_options", {
|
|
3046
3065
|
description: "List the DNS attach options for a service: its public IPv4/IPv6 addresses, the account's editable forward DNS zones, and DNS records already pointing at the service. Use before attach_service_dns_record. Requires dns:read when using an API key.",
|
|
3047
3066
|
inputSchema: {
|
|
3048
|
-
orderNo:
|
|
3067
|
+
orderNo: serviceOrderNoSchema,
|
|
3049
3068
|
},
|
|
3050
3069
|
}, async ({ orderNo }) => {
|
|
3051
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/domains`);
|
|
3070
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/domains`);
|
|
3052
3071
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3053
3072
|
});
|
|
3054
3073
|
server.registerTool("attach_service_dns_record", {
|
|
3055
3074
|
description: "Point a DNS name at a service in one call: creates an A/AAAA/CNAME record in an owned forward DNS zone using the service's own IP (A/AAAA default to the service IP when ip is omitted; the ip, when given, must belong to the service). Not for secondary/suspended zones. For arbitrary record content use upsert_dns_record instead. Requires dns:write when using an API key.",
|
|
3056
3075
|
inputSchema: {
|
|
3057
|
-
orderNo:
|
|
3058
|
-
zone_id:
|
|
3076
|
+
orderNo: serviceOrderNoSchema,
|
|
3077
|
+
zone_id: positiveIdSchema.describe("Owned forward DNS zone ID from list_dns_zones"),
|
|
3059
3078
|
name: z
|
|
3060
3079
|
.string()
|
|
3061
3080
|
.describe("Record name inside the zone, e.g. 'www' or '@' for the apex"),
|
|
@@ -3078,64 +3097,64 @@ server.registerTool("attach_service_dns_record", {
|
|
|
3078
3097
|
body.target = target;
|
|
3079
3098
|
if (ttl !== undefined)
|
|
3080
3099
|
body.ttl = ttl;
|
|
3081
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/domains`, body);
|
|
3100
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/domains`, body);
|
|
3082
3101
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3083
3102
|
});
|
|
3084
3103
|
server.registerTool("list_snapshots", {
|
|
3085
3104
|
description: "List disk snapshots for a Cloud VPS (KVM/VDS) service, with the snapshot billing policy (free window, then billed per GB while kept) and a usage summary. Firecracker VPS uses list_firecracker_snapshots instead. Requires services:read when called with an API key.",
|
|
3086
3105
|
inputSchema: {
|
|
3087
|
-
orderNo:
|
|
3106
|
+
orderNo: serviceOrderNoSchema,
|
|
3088
3107
|
},
|
|
3089
3108
|
}, async ({ orderNo }) => {
|
|
3090
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/snapshots`);
|
|
3109
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/snapshots`);
|
|
3091
3110
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3092
3111
|
});
|
|
3093
3112
|
server.registerTool("create_snapshot", {
|
|
3094
3113
|
description: "Create a disk snapshot of a Cloud VPS (KVM/VDS) service. Free for a short window, then billed per GB while kept (see list_snapshots policy). Take a snapshot before any risky or automated change — it's free for an initial window, so it's cheap insurance you can roll back to. DELETE the snapshot once the change succeeds and you no longer need it — after the free window it is billed per GB while kept (Cloud VPS snapshots do NOT auto-expire), so never leave snapshots lying around. Only one snapshot action can run at a time; snapshot count is limited per service. Requires services:manage and a full-access API key.",
|
|
3095
3114
|
inputSchema: {
|
|
3096
|
-
orderNo:
|
|
3115
|
+
orderNo: serviceOrderNoSchema,
|
|
3097
3116
|
description: z.string().optional().describe("Optional snapshot description"),
|
|
3098
3117
|
},
|
|
3099
3118
|
}, async ({ orderNo, description }) => {
|
|
3100
3119
|
const body = {};
|
|
3101
3120
|
if (description !== undefined)
|
|
3102
3121
|
body.description = description;
|
|
3103
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/snapshots`, body);
|
|
3122
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/snapshots`, body);
|
|
3104
3123
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3105
3124
|
});
|
|
3106
3125
|
server.registerTool("rollback_snapshot", {
|
|
3107
3126
|
description: "Roll a Cloud VPS (KVM/VDS) service back to a disk snapshot. DESTRUCTIVE: disk state after the snapshot is lost. Confirm with the user before calling. Tip: take a snapshot before any risky or automated change — it's free for an initial window, so it's cheap insurance you can roll back to. DELETE the snapshot once the change succeeds and you no longer need it — after the free window it is billed per GB while kept (Cloud VPS snapshots do NOT auto-expire), so never leave snapshots lying around. Requires services:manage and a full-access API key.",
|
|
3108
3127
|
inputSchema: {
|
|
3109
|
-
orderNo:
|
|
3110
|
-
snapname:
|
|
3128
|
+
orderNo: serviceOrderNoSchema,
|
|
3129
|
+
snapname: snapshotNameSchema.describe("Snapshot name from list_snapshots"),
|
|
3111
3130
|
},
|
|
3112
3131
|
}, async ({ orderNo, snapname }) => {
|
|
3113
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/snapshots/${encodeURIComponent(snapname)}/rollback`);
|
|
3132
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/snapshots/${encodeURIComponent(snapname)}/rollback`);
|
|
3114
3133
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3115
3134
|
});
|
|
3116
3135
|
server.registerTool("delete_snapshot", {
|
|
3117
3136
|
description: "Delete a Cloud VPS (KVM/VDS) disk snapshot. Requires services:manage and a full-access API key.",
|
|
3118
3137
|
inputSchema: {
|
|
3119
|
-
orderNo:
|
|
3120
|
-
snapname:
|
|
3138
|
+
orderNo: serviceOrderNoSchema,
|
|
3139
|
+
snapname: snapshotNameSchema.describe("Snapshot name from list_snapshots"),
|
|
3121
3140
|
},
|
|
3122
3141
|
}, async ({ orderNo, snapname }) => {
|
|
3123
|
-
const { data } = await apiRequest("DELETE", `/account/services/${orderNo}/snapshots/${encodeURIComponent(snapname)}`);
|
|
3142
|
+
const { data } = await apiRequest("DELETE", `/account/services/${encodeURIComponent(orderNo)}/snapshots/${encodeURIComponent(snapname)}`);
|
|
3124
3143
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3125
3144
|
});
|
|
3126
3145
|
server.registerTool("list_firecracker_snapshots", {
|
|
3127
3146
|
description: "List temporary snapshots for a Firecracker VPS service, including billing state (free window, then a per-GB keep rate) and expiry. Requires services:read when called with an API key.",
|
|
3128
3147
|
inputSchema: {
|
|
3129
|
-
orderNo:
|
|
3148
|
+
orderNo: serviceOrderNoSchema,
|
|
3130
3149
|
},
|
|
3131
3150
|
}, async ({ orderNo }) => {
|
|
3132
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/firecracker/snapshots`);
|
|
3151
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/firecracker/snapshots`);
|
|
3133
3152
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3134
3153
|
});
|
|
3135
3154
|
server.registerTool("create_firecracker_snapshot", {
|
|
3136
3155
|
description: "Create a temporary snapshot of a Firecracker VPS. Free for a short window, then billed per GB while kept until its automatic expiry. Take a snapshot before any risky or automated change, and delete it once the change succeeds to stop keep billing early. Check list_firecracker_snapshots for the exact policy and expiry fields. Requires services:manage and a full-access API key.",
|
|
3137
3156
|
inputSchema: {
|
|
3138
|
-
orderNo:
|
|
3157
|
+
orderNo: serviceOrderNoSchema,
|
|
3139
3158
|
description: z
|
|
3140
3159
|
.string()
|
|
3141
3160
|
.optional()
|
|
@@ -3145,45 +3164,45 @@ server.registerTool("create_firecracker_snapshot", {
|
|
|
3145
3164
|
const body = {};
|
|
3146
3165
|
if (description !== undefined)
|
|
3147
3166
|
body.description = description;
|
|
3148
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/firecracker/snapshots`, body);
|
|
3167
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/firecracker/snapshots`, body);
|
|
3149
3168
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3150
3169
|
});
|
|
3151
3170
|
server.registerTool("rollback_firecracker_snapshot", {
|
|
3152
3171
|
description: "Roll a Firecracker VPS back to a temporary snapshot. DESTRUCTIVE: disk state after the snapshot is lost. Confirm with the user before calling. Firecracker snapshots expire automatically but remain billed after the free window until deletion or expiry, so delete an unneeded snapshot to stop keep billing early. Requires services:manage and a full-access API key.",
|
|
3153
3172
|
inputSchema: {
|
|
3154
|
-
orderNo:
|
|
3155
|
-
snapshot_id:
|
|
3173
|
+
orderNo: serviceOrderNoSchema,
|
|
3174
|
+
snapshot_id: positiveIdSchema.describe("Snapshot ID from list_firecracker_snapshots"),
|
|
3156
3175
|
},
|
|
3157
3176
|
}, async ({ orderNo, snapshot_id }) => {
|
|
3158
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/firecracker/snapshots/${snapshot_id}/rollback`);
|
|
3177
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/firecracker/snapshots/${encodeURIComponent(String(snapshot_id))}/rollback`);
|
|
3159
3178
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3160
3179
|
});
|
|
3161
3180
|
server.registerTool("delete_firecracker_snapshot", {
|
|
3162
3181
|
description: "Delete a Firecracker VPS temporary snapshot (stops its keep billing). Requires services:manage and a full-access API key.",
|
|
3163
3182
|
inputSchema: {
|
|
3164
|
-
orderNo:
|
|
3165
|
-
snapshot_id:
|
|
3183
|
+
orderNo: serviceOrderNoSchema,
|
|
3184
|
+
snapshot_id: positiveIdSchema.describe("Snapshot ID from list_firecracker_snapshots"),
|
|
3166
3185
|
},
|
|
3167
3186
|
}, async ({ orderNo, snapshot_id }) => {
|
|
3168
|
-
const { data } = await apiRequest("DELETE", `/account/services/${orderNo}/firecracker/snapshots/${snapshot_id}`);
|
|
3187
|
+
const { data } = await apiRequest("DELETE", `/account/services/${encodeURIComponent(orderNo)}/firecracker/snapshots/${encodeURIComponent(String(snapshot_id))}`);
|
|
3169
3188
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3170
3189
|
});
|
|
3171
3190
|
server.registerTool("get_restore_status", {
|
|
3172
3191
|
description: "Get the unified restore state for a service: retention days, restore price, and any restore request in progress. Cloud VPS and Firecracker VPS have automatic daily off-node backups restored through this flow. Requires services:read plus the services:restore paid scope when called with an API key (the response includes account balance).",
|
|
3173
3192
|
inputSchema: {
|
|
3174
|
-
orderNo:
|
|
3193
|
+
orderNo: serviceOrderNoSchema,
|
|
3175
3194
|
},
|
|
3176
3195
|
}, async ({ orderNo }) => {
|
|
3177
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/restore/status`);
|
|
3196
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/restore/status`);
|
|
3178
3197
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3179
3198
|
});
|
|
3180
3199
|
server.registerTool("list_restore_points", {
|
|
3181
3200
|
description: "List available backup restore points for a service (automatic off-node backups). Use a point id with request_restore. Requires services:read when called with an API key.",
|
|
3182
3201
|
inputSchema: {
|
|
3183
|
-
orderNo:
|
|
3202
|
+
orderNo: serviceOrderNoSchema,
|
|
3184
3203
|
},
|
|
3185
3204
|
}, async ({ orderNo }) => {
|
|
3186
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/restore/points`);
|
|
3205
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/restore/points`);
|
|
3187
3206
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3188
3207
|
});
|
|
3189
3208
|
/**
|
|
@@ -3233,8 +3252,8 @@ function restoreConfirmRefusal(data) {
|
|
|
3233
3252
|
server.registerTool("request_restore", {
|
|
3234
3253
|
description: "PAID: restore a service from a backup point. Charges the restore price (+VAT) from the ACCOUNT BALANCE immediately and overwrites the WHOLE service disk with the backup content — everything written since that point is lost. DESTRUCTIVE and billed. This is a confirmation call, not a request for a price: read get_restore_status first, show the user the exact backup point and that status's exact total_charged, tell them the disk is replaced, and obtain explicit approval for BOTH the charge and the replacement. Only then set acknowledge_data_replacement and acknowledge_restore_charge, and pass the very figure you disclosed as expected_total_charged. The tool runs the backend's quote → confirm flow itself under one Idempotency-Key (generated per call unless idempotencyKey is given), so a single call is a single paid attempt. If the price has moved since you disclosed it, nothing is charged: the restore is refused as restoreQuoteChanged, and you must show the user the new total and get their approval again. Requires services:read, a full-access API key with paid operations enabled, the services:restore paid scope, and configured spend caps.",
|
|
3235
3254
|
inputSchema: {
|
|
3236
|
-
orderNo:
|
|
3237
|
-
backup_point_id:
|
|
3255
|
+
orderNo: serviceOrderNoSchema,
|
|
3256
|
+
backup_point_id: positiveIdSchema.describe("Restore point ID from list_restore_points"),
|
|
3238
3257
|
acknowledge_data_replacement: z
|
|
3239
3258
|
.literal(true)
|
|
3240
3259
|
.describe("True only after the user explicitly confirms that the whole service disk is overwritten with this backup point and that everything written since it is lost"),
|
|
@@ -3277,7 +3296,7 @@ server.registerTool("request_restore", {
|
|
|
3277
3296
|
expectedTotalCharged: expected_total_charged,
|
|
3278
3297
|
};
|
|
3279
3298
|
const key = idempotencyKey ?? randomUUID();
|
|
3280
|
-
const quote = await apiRequest("POST", `/account/services/${orderNo}/restore/requests/quote`, { backup_point_id }, { "Idempotency-Key": key });
|
|
3299
|
+
const quote = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/restore/requests/quote`, { backup_point_id }, { "Idempotency-Key": key });
|
|
3281
3300
|
const quoteToken = quote.data
|
|
3282
3301
|
?.quoteToken;
|
|
3283
3302
|
if (!quoteToken) {
|
|
@@ -3291,7 +3310,7 @@ server.registerTool("request_restore", {
|
|
|
3291
3310
|
// The key was already spent on an earlier attempt. Confirm without a
|
|
3292
3311
|
// token: the backend resolves the same key to the existing restore
|
|
3293
3312
|
// request and replays it without charging again.
|
|
3294
|
-
const replay = await apiRequest("POST", `/account/services/${orderNo}/restore/requests`, { backup_point_id, ...confirmation }, { "Idempotency-Key": key });
|
|
3313
|
+
const replay = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/restore/requests`, { backup_point_id, ...confirmation }, { "Idempotency-Key": key });
|
|
3295
3314
|
// restoreInput() runs ahead of the replay lookup, so a replay is refused
|
|
3296
3315
|
// by the same fence as a first attempt and needs the same explanation.
|
|
3297
3316
|
return (restoreConfirmRefusal(replay.data)
|
|
@@ -3321,7 +3340,7 @@ server.registerTool("request_restore", {
|
|
|
3321
3340
|
}],
|
|
3322
3341
|
};
|
|
3323
3342
|
}
|
|
3324
|
-
const { data } = await apiRequest("POST", `/account/services/${orderNo}/restore/requests`, { backup_point_id, quoteToken, ...confirmation }, { "Idempotency-Key": key, "X-Quote-Token": quoteToken });
|
|
3343
|
+
const { data } = await apiRequest("POST", `/account/services/${encodeURIComponent(orderNo)}/restore/requests`, { backup_point_id, quoteToken, ...confirmation }, { "Idempotency-Key": key, "X-Quote-Token": quoteToken });
|
|
3325
3344
|
// The pre-check above only knows the total this quote returned; the price
|
|
3326
3345
|
// can still move before the confirm lands, and the server checks it again.
|
|
3327
3346
|
return (restoreConfirmRefusal(data)
|
|
@@ -3340,7 +3359,7 @@ server.registerTool("list_restore_file_points", {
|
|
|
3340
3359
|
idempotentHint: true,
|
|
3341
3360
|
},
|
|
3342
3361
|
}, async ({ orderNo }) => {
|
|
3343
|
-
const { status, data } = await apiRequest("GET", svc(
|
|
3362
|
+
const { status, data } = await apiRequest("GET", svc(orderNo, "restore/files/points"));
|
|
3344
3363
|
return {
|
|
3345
3364
|
content: [{
|
|
3346
3365
|
type: "text",
|
|
@@ -3393,7 +3412,7 @@ server.registerTool("browse_restore_files", {
|
|
|
3393
3412
|
// unfiltered listing is never returned in a search's place, because a
|
|
3394
3413
|
// caller would trust a file list that silently ignored the search term.
|
|
3395
3414
|
if (filter !== undefined) {
|
|
3396
|
-
const probe = await apiRequest("GET", svc(
|
|
3415
|
+
const probe = await apiRequest("GET", svc(orderNo, "restore/files/points"));
|
|
3397
3416
|
if (probe.status < 200
|
|
3398
3417
|
|| probe.status >= 300
|
|
3399
3418
|
|| readSearchAvailable(probe.data) !== true) {
|
|
@@ -3411,7 +3430,7 @@ server.registerTool("browse_restore_files", {
|
|
|
3411
3430
|
};
|
|
3412
3431
|
}
|
|
3413
3432
|
}
|
|
3414
|
-
const { status, data } = await apiRequest("POST", svc(
|
|
3433
|
+
const { status, data } = await apiRequest("POST", svc(orderNo, "restore/files/browses"), fileBrowseRequestBody({
|
|
3415
3434
|
backupPointId,
|
|
3416
3435
|
sourceBrowseId,
|
|
3417
3436
|
directoryEntryId,
|
|
@@ -3438,7 +3457,7 @@ server.registerTool("get_restore_file_browse", {
|
|
|
3438
3457
|
idempotentHint: true,
|
|
3439
3458
|
},
|
|
3440
3459
|
}, async ({ orderNo, browse_id }) => {
|
|
3441
|
-
const { status, data } = await apiRequest("GET", svc(
|
|
3460
|
+
const { status, data } = await apiRequest("GET", svc(orderNo, `restore/files/browses/${encodeURIComponent(browse_id)}`));
|
|
3442
3461
|
return {
|
|
3443
3462
|
content: [{
|
|
3444
3463
|
type: "text",
|
|
@@ -3449,10 +3468,10 @@ server.registerTool("get_restore_file_browse", {
|
|
|
3449
3468
|
server.registerTool("get_guest_agent_status", {
|
|
3450
3469
|
description: "Check whether the QEMU guest agent is running inside a Cloud VPS (KVM/VDS). Useful before OS-level operations that depend on the agent. Requires services:read when called with an API key.",
|
|
3451
3470
|
inputSchema: {
|
|
3452
|
-
orderNo:
|
|
3471
|
+
orderNo: serviceOrderNoSchema,
|
|
3453
3472
|
},
|
|
3454
3473
|
}, async ({ orderNo }) => {
|
|
3455
|
-
const { data } = await apiRequest("GET", `/account/services/${orderNo}/guest-agent`);
|
|
3474
|
+
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}/guest-agent`);
|
|
3456
3475
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3457
3476
|
});
|
|
3458
3477
|
const functionBodyFromInput = (input) => {
|
|
@@ -3479,7 +3498,7 @@ const functionBodyFromInput = (input) => {
|
|
|
3479
3498
|
};
|
|
3480
3499
|
const functionFieldsSchema = {
|
|
3481
3500
|
name: z.string().optional().describe("Function name"),
|
|
3482
|
-
runtime_os_id:
|
|
3501
|
+
runtime_os_id: positiveIdSchema.optional().describe("Runtime OS ID (see list_functions response for available runtimes)"),
|
|
3483
3502
|
entrypoint: z.string().optional().describe("Entrypoint command/handler"),
|
|
3484
3503
|
description: z.string().optional().describe("Description (max 2000 chars)"),
|
|
3485
3504
|
code: z.string().optional().describe("Function source code"),
|
|
@@ -3501,10 +3520,10 @@ server.registerTool("list_functions", {
|
|
|
3501
3520
|
server.registerTool("get_function", {
|
|
3502
3521
|
description: "Get one Firecracker Function with code, config, and webhook details.",
|
|
3503
3522
|
inputSchema: {
|
|
3504
|
-
function_id:
|
|
3523
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3505
3524
|
},
|
|
3506
3525
|
}, async ({ function_id }) => {
|
|
3507
|
-
const { data } = await apiRequest("GET", `/account/firecracker/functions/${function_id}`);
|
|
3526
|
+
const { data } = await apiRequest("GET", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`);
|
|
3508
3527
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3509
3528
|
});
|
|
3510
3529
|
server.registerTool("create_function", {
|
|
@@ -3517,27 +3536,27 @@ server.registerTool("create_function", {
|
|
|
3517
3536
|
server.registerTool("update_function", {
|
|
3518
3537
|
description: "Update a Firecracker Function's code or configuration. Call get_function first. If integrity.unreadable_fields names code or environment, the backend refuses the update unless the user explicitly approves replacing every unavailable value from a trusted copy and acknowledge_unreadable_replacement=true is passed. Leave that flag false for ordinary updates.",
|
|
3519
3538
|
inputSchema: {
|
|
3520
|
-
function_id:
|
|
3539
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3521
3540
|
...functionFieldsSchema,
|
|
3522
3541
|
acknowledge_unreadable_replacement: z.boolean().optional().describe("Explicit customer approval to replace code or environment named by integrity.unreadable_fields; default false"),
|
|
3523
3542
|
},
|
|
3524
3543
|
}, async ({ function_id, ...input }) => {
|
|
3525
|
-
const { data } = await apiRequest("POST", `/account/firecracker/functions/${function_id}`, functionBodyFromInput(input));
|
|
3544
|
+
const { data } = await apiRequest("POST", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`, functionBodyFromInput(input));
|
|
3526
3545
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3527
3546
|
});
|
|
3528
3547
|
server.registerTool("delete_function", {
|
|
3529
3548
|
description: "Delete a Firecracker Function.",
|
|
3530
3549
|
inputSchema: {
|
|
3531
|
-
function_id:
|
|
3550
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3532
3551
|
},
|
|
3533
3552
|
}, async ({ function_id }) => {
|
|
3534
|
-
const { data } = await apiRequest("DELETE", `/account/firecracker/functions/${function_id}`);
|
|
3553
|
+
const { data } = await apiRequest("DELETE", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`);
|
|
3535
3554
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3536
3555
|
});
|
|
3537
3556
|
server.registerTool("invoke_function", {
|
|
3538
3557
|
description: "Invoke a Firecracker Function. PAID per invocation (CPU/memory usage billed from account). With wait=true the call blocks and returns the result; otherwise poll list_function_invocations. One Idempotency-Key is generated per tool call unless idempotency_key is supplied; reuse an explicit key only for an exact retry so the same logical step is not double-run or double-billed (response may include replayed=true).",
|
|
3539
3558
|
inputSchema: {
|
|
3540
|
-
function_id:
|
|
3559
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3541
3560
|
input: z
|
|
3542
3561
|
.string()
|
|
3543
3562
|
.optional()
|
|
@@ -3561,26 +3580,26 @@ server.registerTool("invoke_function", {
|
|
|
3561
3580
|
if (wait !== undefined)
|
|
3562
3581
|
body.wait = wait;
|
|
3563
3582
|
const key = idempotency_key ?? randomUUID();
|
|
3564
|
-
const { data } = await apiRequest("POST", `/account/firecracker/functions/${function_id}/invoke`, body, { "Idempotency-Key": key });
|
|
3583
|
+
const { data } = await apiRequest("POST", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}/invoke`, body, { "Idempotency-Key": key });
|
|
3565
3584
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3566
3585
|
});
|
|
3567
3586
|
server.registerTool("list_function_invocations", {
|
|
3568
3587
|
description: "List invocations of a Firecracker Function with status, duration, and cost.",
|
|
3569
3588
|
inputSchema: {
|
|
3570
|
-
function_id:
|
|
3589
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3571
3590
|
},
|
|
3572
3591
|
}, async ({ function_id }) => {
|
|
3573
|
-
const { data } = await apiRequest("GET", `/account/firecracker/functions/${function_id}/invocations`);
|
|
3592
|
+
const { data } = await apiRequest("GET", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}/invocations`);
|
|
3574
3593
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3575
3594
|
});
|
|
3576
3595
|
server.registerTool("get_function_invocation", {
|
|
3577
3596
|
description: "Get one invocation of a Firecracker Function including output/logs and usage cost.",
|
|
3578
3597
|
inputSchema: {
|
|
3579
|
-
function_id:
|
|
3580
|
-
invocation_id:
|
|
3598
|
+
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3599
|
+
invocation_id: invocationIdSchema.describe("Invocation ID from list_function_invocations"),
|
|
3581
3600
|
},
|
|
3582
3601
|
}, async ({ function_id, invocation_id }) => {
|
|
3583
|
-
const { data } = await apiRequest("GET", `/account/firecracker/functions/${function_id}/invocations/${encodeURIComponent(invocation_id)}`);
|
|
3602
|
+
const { data } = await apiRequest("GET", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}/invocations/${encodeURIComponent(invocation_id)}`);
|
|
3584
3603
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3585
3604
|
});
|
|
3586
3605
|
// --- On-demand server sessions (stable temp_vm API identifiers) ---
|
|
@@ -3807,34 +3826,34 @@ server.registerTool("create_dns_zone", {
|
|
|
3807
3826
|
server.registerTool("get_dns_zone", {
|
|
3808
3827
|
description: "Get a DNS zone and desired records. Requires dns:read when using an API key.",
|
|
3809
3828
|
inputSchema: {
|
|
3810
|
-
zone_id:
|
|
3829
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3811
3830
|
},
|
|
3812
3831
|
}, async ({ zone_id }) => {
|
|
3813
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}`);
|
|
3832
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}`);
|
|
3814
3833
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3815
3834
|
});
|
|
3816
3835
|
server.registerTool("get_dns_zone_diagnostics", {
|
|
3817
3836
|
description: "Run customer-facing DNS health diagnostics for a zone: delegation, public SOA, DNSSEC signal, and common record hygiene checks. Requires dns:read when using an API key.",
|
|
3818
3837
|
inputSchema: {
|
|
3819
|
-
zone_id:
|
|
3838
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3820
3839
|
},
|
|
3821
3840
|
}, async ({ zone_id }) => {
|
|
3822
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/diagnostics`);
|
|
3841
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/diagnostics`);
|
|
3823
3842
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3824
3843
|
});
|
|
3825
3844
|
server.registerTool("export_dns_zone", {
|
|
3826
3845
|
description: "Export a native forward DNS zone's desired-state records as a BIND-style zone file. System-managed SOA/apex NS/DNSSEC wire records and PTR/reverse DNS are not exported here. Requires dns:read when using an API key.",
|
|
3827
3846
|
inputSchema: {
|
|
3828
|
-
zone_id:
|
|
3847
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3829
3848
|
},
|
|
3830
3849
|
}, async ({ zone_id }) => {
|
|
3831
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/export`);
|
|
3850
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/export`);
|
|
3832
3851
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3833
3852
|
});
|
|
3834
3853
|
server.registerTool("import_dns_zone", {
|
|
3835
3854
|
description: "Import BIND-style forward DNS records into a native zone. Set replace=true to replace non-system desired-state records; false upserts imported records. PTR/reverse DNS, SOA, DNSSEC wire records, apex NS, and LUA are skipped or rejected by the API. Requires dns:write when using an API key.",
|
|
3836
3855
|
inputSchema: {
|
|
3837
|
-
zone_id:
|
|
3856
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3838
3857
|
zonefile: z
|
|
3839
3858
|
.string()
|
|
3840
3859
|
.min(1)
|
|
@@ -3846,7 +3865,7 @@ server.registerTool("import_dns_zone", {
|
|
|
3846
3865
|
.describe("If true, delete existing non-system desired-state records before importing. Defaults to false."),
|
|
3847
3866
|
},
|
|
3848
3867
|
}, async ({ zone_id, zonefile, replace }) => {
|
|
3849
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/import`, {
|
|
3868
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/import`, {
|
|
3850
3869
|
zonefile,
|
|
3851
3870
|
...(replace !== undefined ? { replace } : {}),
|
|
3852
3871
|
});
|
|
@@ -3855,18 +3874,17 @@ server.registerTool("import_dns_zone", {
|
|
|
3855
3874
|
server.registerTool("list_dns_templates", {
|
|
3856
3875
|
description: "List backend-defined DNS record templates for a native forward zone, such as web service, Google Workspace, Microsoft 365, mail security, Null MX and CAA lockdown. Requires dns:read when using an API key.",
|
|
3857
3876
|
inputSchema: {
|
|
3858
|
-
zone_id:
|
|
3877
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3859
3878
|
},
|
|
3860
3879
|
}, async ({ zone_id }) => {
|
|
3861
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/templates`);
|
|
3880
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/templates`);
|
|
3862
3881
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3863
3882
|
});
|
|
3864
3883
|
server.registerTool("apply_dns_template", {
|
|
3865
3884
|
description: "Apply a backend-defined DNS template to a native forward zone. Records still pass the same API validation, quotas and conflict rules as manual record writes. Prefer preview=true first to show the user exactly which records a template would write before changing the zone. Requires dns:write when using an API key.",
|
|
3866
3885
|
inputSchema: {
|
|
3867
|
-
zone_id:
|
|
3868
|
-
template:
|
|
3869
|
-
.string()
|
|
3886
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3887
|
+
template: dnsTemplateIdSchema
|
|
3870
3888
|
.describe("Template ID returned by list_dns_templates, e.g. web_service, null_mx, google_workspace, microsoft_365, mail_security, caa_letsencrypt"),
|
|
3871
3889
|
parameters: z
|
|
3872
3890
|
.record(z.union([z.string(), z.number(), z.boolean()]))
|
|
@@ -3882,7 +3900,7 @@ server.registerTool("apply_dns_template", {
|
|
|
3882
3900
|
.describe("true to replace existing records that conflict with the template. Destructive: confirm the exact conflicts with the user first, ideally from a preview run."),
|
|
3883
3901
|
},
|
|
3884
3902
|
}, async ({ zone_id, template, parameters, preview, overwrite }) => {
|
|
3885
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/templates/${encodeURIComponent(template)}`, {
|
|
3903
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/templates/${encodeURIComponent(template)}`, {
|
|
3886
3904
|
...(parameters !== undefined ? { parameters } : {}),
|
|
3887
3905
|
...(preview !== undefined ? { preview } : {}),
|
|
3888
3906
|
...(overwrite !== undefined ? { overwrite } : {}),
|
|
@@ -3892,43 +3910,43 @@ server.registerTool("apply_dns_template", {
|
|
|
3892
3910
|
server.registerTool("delete_dns_zone", {
|
|
3893
3911
|
description: "Delete a forward DNS zone. Published zones are queued for removal from the managed DNS platform. Requires dns:write when using an API key.",
|
|
3894
3912
|
inputSchema: {
|
|
3895
|
-
zone_id:
|
|
3913
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3896
3914
|
},
|
|
3897
3915
|
}, async ({ zone_id }) => {
|
|
3898
|
-
const { data } = await apiRequest("DELETE", `/account/dns/zones/${zone_id}`);
|
|
3916
|
+
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}`);
|
|
3899
3917
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3900
3918
|
});
|
|
3901
3919
|
server.registerTool("verify_dns_zone", {
|
|
3902
3920
|
description: "Check the zone's ownership TXT value (_vpsnet-dns.<zone>) and publish the zone when it matches: publicly, or served authoritatively by the nameservers the parent zone delegated the name to when the zone was created (the previous DNS provider) - so a domain already pointed at ns1/ns2.vpsnet.com is verified by adding the TXT at the previous provider, never by moving the nameservers back. Delegation to VPSnet nameservers alone is never proof. A refusal (verificationMissing) returns verification.checks (every place checked and what was found) and verification.next_step: add_txt | add_txt_at_previous_provider | add_txt_in_parent_zone | contact_support (only VPSnet support can confirm the domain; the zone is kept, not deleted, while the domain points at VPSnet). lookupUnavailable (HTTP 503) means the platform could not run the DNS check - retry later; it is not a missing record. Requires dns:write when using an API key.",
|
|
3903
3921
|
inputSchema: {
|
|
3904
|
-
zone_id:
|
|
3922
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3905
3923
|
},
|
|
3906
3924
|
}, async ({ zone_id }) => {
|
|
3907
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/verify`);
|
|
3925
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/verify`);
|
|
3908
3926
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3909
3927
|
});
|
|
3910
3928
|
server.registerTool("reissue_dns_zone_verification", {
|
|
3911
3929
|
description: "Replace a pending DNS zone's ownership TXT value when the one returned by create_dns_zone was lost (only its hash is stored). Returns the new value once in verificationRecord; the previous value stops being accepted, so the TXT record must be updated to the new value. Only for zones in pending_verification. Rate limited to 5 per minute. Requires dns:write when using an API key.",
|
|
3912
3930
|
inputSchema: {
|
|
3913
|
-
zone_id:
|
|
3931
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3914
3932
|
},
|
|
3915
3933
|
}, async ({ zone_id }) => {
|
|
3916
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/verification/reissue`);
|
|
3934
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/verification/reissue`);
|
|
3917
3935
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3918
3936
|
});
|
|
3919
3937
|
server.registerTool("get_dnssec", {
|
|
3920
3938
|
description: "Get DNSSEC state and public DNSKEY/DS material for a native DNS zone. Requires dns:read when using an API key.",
|
|
3921
3939
|
inputSchema: {
|
|
3922
|
-
zone_id:
|
|
3940
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3923
3941
|
},
|
|
3924
3942
|
}, async ({ zone_id }) => {
|
|
3925
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/dnssec`);
|
|
3943
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/dnssec`);
|
|
3926
3944
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3927
3945
|
});
|
|
3928
3946
|
server.registerTool("set_dnssec", {
|
|
3929
3947
|
description: "Enable or disable DNSSEC signing for a native DNS zone. If parent DS records still exist outside VPSNet, remove them first and set parent_ds_removed=true before disabling signing. Secondary-zone DNSSEC is controlled on the primary server. Requires dnssec:manage when using an API key.",
|
|
3930
3948
|
inputSchema: {
|
|
3931
|
-
zone_id:
|
|
3949
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3932
3950
|
enabled: z.boolean().describe("true to enable/sign the zone, false to disable/remove signing"),
|
|
3933
3951
|
parent_ds_removed: z
|
|
3934
3952
|
.boolean()
|
|
@@ -3936,7 +3954,7 @@ server.registerTool("set_dnssec", {
|
|
|
3936
3954
|
.describe("When disabling DNSSEC for a zone whose parent DS is managed outside VPSNet, set true only after removing the parent DS records."),
|
|
3937
3955
|
},
|
|
3938
3956
|
}, async ({ zone_id, enabled, parent_ds_removed }) => {
|
|
3939
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/dnssec`, {
|
|
3957
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/dnssec`, {
|
|
3940
3958
|
enabled,
|
|
3941
3959
|
parent_ds_removed,
|
|
3942
3960
|
});
|
|
@@ -3945,7 +3963,7 @@ server.registerTool("set_dnssec", {
|
|
|
3945
3963
|
server.registerTool("upsert_dns_record", {
|
|
3946
3964
|
description: "Create or replace a forward DNS record in desired state for a native zone. PTR and *.arpa are rejected; use change_rdns for reverse DNS. Requires dns:write when using an API key.",
|
|
3947
3965
|
inputSchema: {
|
|
3948
|
-
zone_id:
|
|
3966
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3949
3967
|
name: z
|
|
3950
3968
|
.string()
|
|
3951
3969
|
.describe("Record name. Use @ for apex, or a relative name inside the zone."),
|
|
@@ -3967,14 +3985,14 @@ server.registerTool("upsert_dns_record", {
|
|
|
3967
3985
|
body.ttl = ttl;
|
|
3968
3986
|
if (comment !== undefined)
|
|
3969
3987
|
body.comment = comment;
|
|
3970
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/records`, body);
|
|
3988
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/records`, body);
|
|
3971
3989
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3972
3990
|
});
|
|
3973
3991
|
server.registerTool("update_dns_record", {
|
|
3974
3992
|
description: "Edit an existing forward DNS record by its ID (PUT) — change value, TTL, or comment without replacing it. System-managed records cannot be edited. Requires dns:write when using an API key.",
|
|
3975
3993
|
inputSchema: {
|
|
3976
|
-
zone_id:
|
|
3977
|
-
record_id:
|
|
3994
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3995
|
+
record_id: positiveIdSchema.describe("DNS record ID to update"),
|
|
3978
3996
|
name: z
|
|
3979
3997
|
.string()
|
|
3980
3998
|
.describe("Record name. Use @ for apex, or a relative name inside the zone."),
|
|
@@ -3996,17 +4014,17 @@ server.registerTool("update_dns_record", {
|
|
|
3996
4014
|
body.ttl = ttl;
|
|
3997
4015
|
if (comment !== undefined)
|
|
3998
4016
|
body.comment = comment;
|
|
3999
|
-
const { data } = await apiRequest("PUT", `/account/dns/zones/${zone_id}/records/${record_id}`, body);
|
|
4017
|
+
const { data } = await apiRequest("PUT", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/records/${encodeURIComponent(String(record_id))}`, body);
|
|
4000
4018
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4001
4019
|
});
|
|
4002
4020
|
server.registerTool("delete_dns_record", {
|
|
4003
4021
|
description: "Delete a forward DNS record from desired state. Requires dns:write when using an API key.",
|
|
4004
4022
|
inputSchema: {
|
|
4005
|
-
zone_id:
|
|
4006
|
-
record_id:
|
|
4023
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4024
|
+
record_id: positiveIdSchema.describe("DNS record ID"),
|
|
4007
4025
|
},
|
|
4008
4026
|
}, async ({ zone_id, record_id }) => {
|
|
4009
|
-
const { data } = await apiRequest("DELETE", `/account/dns/zones/${zone_id}/records/${record_id}`);
|
|
4027
|
+
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/records/${encodeURIComponent(String(record_id))}`);
|
|
4010
4028
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4011
4029
|
});
|
|
4012
4030
|
server.registerTool("get_dns_service_status", {
|
|
@@ -4019,25 +4037,25 @@ server.registerTool("get_dns_service_status", {
|
|
|
4019
4037
|
server.registerTool("get_dns_zone_history", {
|
|
4020
4038
|
description: "Get the recent change history for a DNS zone and its records (action, record, who, when). Useful for auditing what changed. Requires dns:read when using an API key.",
|
|
4021
4039
|
inputSchema: {
|
|
4022
|
-
zone_id:
|
|
4040
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4023
4041
|
},
|
|
4024
4042
|
}, async ({ zone_id }) => {
|
|
4025
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/history`);
|
|
4043
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/history`);
|
|
4026
4044
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4027
4045
|
});
|
|
4028
4046
|
server.registerTool("list_ddns_tokens", {
|
|
4029
4047
|
description: "List DNS updater tokens for a DNS zone (DDNS A/AAAA and ACME DNS-01 TXT). Returned data never includes the full token value. Requires dns:read when using an API key.",
|
|
4030
4048
|
inputSchema: {
|
|
4031
|
-
zone_id:
|
|
4049
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4032
4050
|
},
|
|
4033
4051
|
}, async ({ zone_id }) => {
|
|
4034
|
-
const { data } = await apiRequest("GET", `/account/dns/zones/${zone_id}/ddns-tokens`);
|
|
4052
|
+
const { data } = await apiRequest("GET", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/ddns-tokens`);
|
|
4035
4053
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4036
4054
|
});
|
|
4037
4055
|
server.registerTool("create_ddns_token", {
|
|
4038
4056
|
description: "Create a narrow DNS updater token for one hostname/pattern inside a verified customer-owned zone. purpose=ddns allows A/AAAA updater use; purpose=acme allows TXT only under _acme-challenge for DNS-01. Set allow_from to restrict updater source IPs/CIDRs. The full token is returned once. Requires dns:write when using an API key.",
|
|
4039
4057
|
inputSchema: {
|
|
4040
|
-
zone_id:
|
|
4058
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4041
4059
|
purpose: z
|
|
4042
4060
|
.enum(["ddns", "acme"])
|
|
4043
4061
|
.optional()
|
|
@@ -4074,46 +4092,46 @@ server.registerTool("create_ddns_token", {
|
|
|
4074
4092
|
body.rate_limit_per_minute = rate_limit_per_minute;
|
|
4075
4093
|
if (expires_at)
|
|
4076
4094
|
body.expires_at = expires_at;
|
|
4077
|
-
const { data } = await apiRequest("POST", `/account/dns/zones/${zone_id}/ddns-tokens`, body);
|
|
4095
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/ddns-tokens`, body);
|
|
4078
4096
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4079
4097
|
});
|
|
4080
4098
|
server.registerTool("revoke_ddns_token", {
|
|
4081
4099
|
description: "Revoke a DNS updater token (DDNS or ACME). Requires dns:write when using an API key.",
|
|
4082
4100
|
inputSchema: {
|
|
4083
|
-
zone_id:
|
|
4084
|
-
token_id:
|
|
4101
|
+
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4102
|
+
token_id: positiveIdSchema.describe("DDNS token ID"),
|
|
4085
4103
|
},
|
|
4086
4104
|
}, async ({ zone_id, token_id }) => {
|
|
4087
|
-
const { data } = await apiRequest("DELETE", `/account/dns/zones/${zone_id}/ddns-tokens/${token_id}`);
|
|
4105
|
+
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/ddns-tokens/${encodeURIComponent(String(token_id))}`);
|
|
4088
4106
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4089
4107
|
});
|
|
4090
4108
|
// --- Billing ---
|
|
4091
4109
|
server.registerTool("list_invoices", {
|
|
4092
4110
|
description: "List invoices with pagination",
|
|
4093
4111
|
inputSchema: {
|
|
4094
|
-
page:
|
|
4112
|
+
page: positiveIdSchema.optional().describe("Page number"),
|
|
4095
4113
|
},
|
|
4096
4114
|
}, async ({ page }) => {
|
|
4097
|
-
const query = page ? `?page=${page}` : "";
|
|
4115
|
+
const query = page ? `?page=${encodeURIComponent(String(page))}` : "";
|
|
4098
4116
|
const { data } = await apiRequest("GET", `/account/history/invoices${query}`);
|
|
4099
4117
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4100
4118
|
});
|
|
4101
4119
|
server.registerTool("get_invoice", {
|
|
4102
4120
|
description: "Get a specific invoice by hash",
|
|
4103
4121
|
inputSchema: {
|
|
4104
|
-
hash:
|
|
4122
|
+
hash: invoiceHashSchema.describe("Invoice hash from list_invoices"),
|
|
4105
4123
|
},
|
|
4106
4124
|
}, async ({ hash }) => {
|
|
4107
|
-
const { data } = await apiRequest("GET", `/account/history/invoices/${hash}`);
|
|
4125
|
+
const { data } = await apiRequest("GET", `/account/history/invoices/${encodeURIComponent(hash)}`);
|
|
4108
4126
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4109
4127
|
});
|
|
4110
4128
|
server.registerTool("list_payments", {
|
|
4111
4129
|
description: "List payment history with pagination",
|
|
4112
4130
|
inputSchema: {
|
|
4113
|
-
page:
|
|
4131
|
+
page: positiveIdSchema.optional().describe("Page number"),
|
|
4114
4132
|
},
|
|
4115
4133
|
}, async ({ page }) => {
|
|
4116
|
-
const query = page ? `?page=${page}` : "";
|
|
4134
|
+
const query = page ? `?page=${encodeURIComponent(String(page))}` : "";
|
|
4117
4135
|
const { data } = await apiRequest("GET", `/account/history/payments${query}`);
|
|
4118
4136
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4119
4137
|
});
|
|
@@ -4135,15 +4153,12 @@ server.registerTool("get_management_history", {
|
|
|
4135
4153
|
server.registerTool("get_usage_statements", {
|
|
4136
4154
|
description: "Get itemized usage-billing statements for the account (per-period totals by family, e.g. Firecracker Functions usage, VDS snapshots, AI premium). This is where metered/usage-based charges show up, separate from invoices. Paginated.",
|
|
4137
4155
|
inputSchema: {
|
|
4138
|
-
page:
|
|
4139
|
-
.number()
|
|
4140
|
-
.int()
|
|
4141
|
-
.positive()
|
|
4156
|
+
page: positiveIdSchema
|
|
4142
4157
|
.optional()
|
|
4143
4158
|
.describe("Page number for pagination (default 1)"),
|
|
4144
4159
|
},
|
|
4145
4160
|
}, async ({ page }) => {
|
|
4146
|
-
const query = page !== undefined ? `?page=${page}` : "";
|
|
4161
|
+
const query = page !== undefined ? `?page=${encodeURIComponent(String(page))}` : "";
|
|
4147
4162
|
const { data } = await apiRequest("GET", `/account/history/usage-statements${query}`);
|
|
4148
4163
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4149
4164
|
});
|
|
@@ -4156,7 +4171,7 @@ server.registerTool("get_pricing", {
|
|
|
4156
4171
|
.describe("Service type: vps, vds, ds, or firecracker"),
|
|
4157
4172
|
},
|
|
4158
4173
|
}, async ({ type }) => {
|
|
4159
|
-
const { data } = await apiRequest("GET", `/public/prices/${type}/plans`);
|
|
4174
|
+
const { data } = await apiRequest("GET", `/public/prices/${encodeURIComponent(type)}/plans`);
|
|
4160
4175
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4161
4176
|
});
|
|
4162
4177
|
server.registerTool("get_system_status", {
|