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/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.0" }, {
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
- .describe("Tenant-owned service order number, e.g. VP88318");
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 = z
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: z.string().describe("Order number, e.g. VP57068"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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(encodeURIComponent(orderNo), "rescue"));
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(encodeURIComponent(orderNo), "rescue"));
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(encodeURIComponent(orderNo), "rescue"), serviceRescueEnterRequestBody(image), { "Idempotency-Key": idempotencyKey });
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(encodeURIComponent(orderNo), "rescue"));
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(encodeURIComponent(orderNo), "rescue"), undefined, { "Idempotency-Key": idempotencyKey });
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
1661
- ssh_key: z.number().describe("SSH key ID from list_ssh_keys"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
1710
- plan: z.number().describe("Plan ID from get_plan_options"),
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: z.string().describe("Order number"),
1720
- plan: z.number().describe("Plan ID from get_plan_options"),
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: z.string().describe("Order number"),
1733
- plan: z.number().describe("Plan ID from get_plan_options"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
1773
- period: z.number().describe("Period ID from get_period_options"),
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: z.number().describe("Plan ID from get_order_plans"),
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: z.number().describe("Plan ID from get_order_plans"),
1825
- os: z.number().optional().describe("OS version ID from get_order_options"),
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: z.number().optional().describe("Billing period ID from get_order_options"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.number().describe("SSH key 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: z.number().describe("SSH key 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: z.number().int().positive().describe("API key ID from list_api_keys (the calling key's own 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: z.number().int().positive().describe("API key ID from list_api_keys (the calling key's own 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: z.number().int().positive().describe("Inference API key ID from list_api_keys (the calling key's own 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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
2767
- record_id: z.number().describe("Record ID from list_domain_glue_records"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Domain contact 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: z.number().describe("Domain contact 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: z.number().optional().describe("Registrant contact ID; defaults to account default contact"),
2890
- admin_contact_id: z.number().optional().describe("Admin contact ID"),
2891
- tech_contact_id: z.number().optional().describe("Technical contact ID"),
2892
- billing_contact_id: z.number().optional().describe("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: z.number().optional().describe("Registrant contact ID; defaults to account default contact"),
2908
- admin_contact_id: z.number().optional().describe("Admin contact ID"),
2909
- tech_contact_id: z.number().optional().describe("Technical contact ID"),
2910
- billing_contact_id: z.number().optional().describe("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: z.number().optional().describe("Registrant contact ID used in quote"),
2934
- admin_contact_id: z.number().optional().describe("Admin contact ID used in quote"),
2935
- tech_contact_id: z.number().optional().describe("Technical contact ID used in quote"),
2936
- billing_contact_id: z.number().optional().describe("Billing contact ID used in quote"),
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: z.number().optional().describe("Registrant contact ID used in quote"),
2954
- admin_contact_id: z.number().optional().describe("Admin contact ID used in quote"),
2955
- tech_contact_id: z.number().optional().describe("Technical contact ID used in quote"),
2956
- billing_contact_id: z.number().optional().describe("Billing contact ID used in quote"),
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: z.number().optional().describe("Owned 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: z.number().optional().describe("Owned domain ID used in quote"),
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: z.number().optional().describe("Owned 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: z.number().optional().describe("Owned domain ID used in quote"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.number().describe("Owned domain ID from list_domains"),
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: z.string().describe("Order number, e.g. VP57068"),
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: z.string().describe("Order number of the service, e.g. VP57068"),
3058
- zone_id: z.number().describe("Owned forward DNS zone ID from list_dns_zones"),
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: z.string().describe("Order number, e.g. VD12345"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
3110
- snapname: z.string().describe("Snapshot name from list_snapshots"),
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: z.string().describe("Order number"),
3120
- snapname: z.string().describe("Snapshot name from list_snapshots"),
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: z.string().describe("Order number, e.g. VP57068"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
3155
- snapshot_id: z.number().describe("Snapshot ID from list_firecracker_snapshots"),
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: z.string().describe("Order number"),
3165
- snapshot_id: z.number().describe("Snapshot ID from list_firecracker_snapshots"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
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: z.string().describe("Order number"),
3237
- backup_point_id: z.number().describe("Restore point ID from list_restore_points"),
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(encodeURIComponent(orderNo), "restore/files/points"));
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(encodeURIComponent(orderNo), "restore/files/points"));
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(encodeURIComponent(orderNo), "restore/files/browses"), fileBrowseRequestBody({
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(encodeURIComponent(orderNo), `restore/files/browses/${encodeURIComponent(browse_id)}`));
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: z.string().describe("Order number"),
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: z.number().optional().describe("Runtime OS ID (see list_functions response for available runtimes)"),
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: z.number().describe("Function ID from list_functions"),
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: z.number().describe("Function ID from list_functions"),
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: z.number().describe("Function ID from list_functions"),
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: z.number().describe("Function ID from list_functions"),
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: z.number().describe("Function ID from list_functions"),
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: z.number().describe("Function ID from list_functions"),
3580
- invocation_id: z.string().describe("Invocation ID from list_function_invocations"),
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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS zone ID"),
3868
- template: z
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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS zone ID"),
3977
- record_id: z.number().describe("DNS record ID to update"),
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: z.number().describe("DNS zone ID"),
4006
- record_id: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS 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: z.number().describe("DNS zone ID"),
4084
- token_id: z.number().describe("DDNS 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: z.number().optional().describe("Page number"),
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: z.string().describe("Invoice 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: z.number().optional().describe("Page number"),
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: z
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", {