vpsnet-mcp 2.3.16 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -30
- package/build/api.js +44 -0
- package/build/application-contract.js +6 -5
- package/build/auth-failure.js +5 -5
- package/build/custom-project-contract.js +3 -3
- package/build/dns-zone-contract.js +11 -0
- package/build/file-restore-contract.js +1 -1
- package/build/index.js +168 -38
- package/build/networking-tools.js +453 -43
- package/build/read-only-context.js +3 -0
- package/build/remote-orders.js +7 -6
- package/build/remote-policy.js +40 -7
- package/build/remote-tools.js +1 -1
- package/build/scope-catalog.js +8 -8
- package/build/service-files.js +584 -0
- package/build/service-reinstall-contract.js +8 -0
- package/build/service-rescue-contract.js +1 -1
- package/build/tool-schema.js +16 -3
- package/build/update-info.js +1 -1
- package/package.json +5 -2
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { installToolInputSchemas } from "./tool-schema.js";
|
|
2
|
+
import { installToolInputSchemas, unboundedInteger } from "./tool-schema.js";
|
|
3
|
+
import { createDnsZoneDescription } from "./dns-zone-contract.js";
|
|
3
4
|
import { registerOrderTools } from "./remote-orders.js";
|
|
4
5
|
import { registerRemoteTools } from "./remote-tools.js";
|
|
5
6
|
import { registerReadOnlyContext } from "./read-only-context.js";
|
|
@@ -8,10 +9,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
10
|
import { pathToFileURL } from "node:url";
|
|
10
11
|
import { resolve } from "node:path";
|
|
12
|
+
import { reinstallDescription } from "./service-reinstall-contract.js";
|
|
11
13
|
import { installRemotePolicy, installToolTitles, remoteInstructions, orderInstructions } from "./remote-policy.js";
|
|
12
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
15
|
import { readFileSync } from "node:fs";
|
|
14
16
|
import { registerNetworkingTools } from "./networking-tools.js";
|
|
17
|
+
import { registerServiceFileTools } from "./service-files.js";
|
|
15
18
|
import { isIP } from "node:net";
|
|
16
19
|
import { z } from "zod";
|
|
17
20
|
import { apiRequest, formatJson, initializeStdioApi } from "./api.js";
|
|
@@ -47,9 +50,10 @@ export function createVpsnetServer(options = {}) {
|
|
|
47
50
|
instructions: options.remoteScopes ? (options.remoteProfile === "orders" ? orderInstructions(options.remoteScopes) : remoteInstructions) : [
|
|
48
51
|
"This MCP server controls VPSnet.com services, including VPS service management, DNS zones, domain registration, domain contacts, API keys, billing, and related paid actions.",
|
|
49
52
|
"Use the tool descriptions and API-key scopes to choose the correct surface; do not assume this server is limited to VPS-only operations.",
|
|
50
|
-
"Auth: every request authenticates with the X-API-KEY header (your VPSNet API key).
|
|
53
|
+
"Auth: every request authenticates with the X-API-KEY header (your VPSNet API key). Each API key may make 300 requests per 60 seconds by default, and some operations have lower limits; on HTTP 429 wait the retry_after seconds given in the result (the API also sends Retry-After) before retrying.",
|
|
51
54
|
"This server needs a MANAGEMENT API key (scope full, or read for GET-only use). An AI-scoped key is issued only for VPSnet AI assistant inference and is refused on every account route, so no tool here can work with one. When a tool result carries auth_problem, read its reason and fix and relay them instead of reporting a bare 'Unauthorized'.",
|
|
52
|
-
"Service tools are scope-gated for API keys: service reads (lists, details, graphs, history, options, backup/snapshot/restore-point listings) require services:read; service mutations (start/stop/restart/console, suspend/resume, hostname, rDNS, root password, title, IPv6, extra settings, SSH key deploy, OS reinstall, plan change, renewal, auto-renew, backups, snapshots, firewall flush) require services:manage plus a full-access key; rescue requires services:rescue; paid whole-service restore additionally requires the services:restore paid scope. A key whose granular scope list was left empty keeps everything its access level (full/read) already allows,
|
|
55
|
+
"Service tools are scope-gated for API keys: service reads (lists, details, graphs, history, options, backup/snapshot/restore-point listings) require services:read; service mutations (start/stop/restart/console, suspend/resume, hostname, rDNS, root password, title, IPv6, extra settings, SSH key deploy, OS reinstall, plan change, renewal, auto-renew, backups, snapshots, firewall flush) require services:manage plus a full-access key; rescue requires services:rescue; paid whole-service restore additionally requires the services:restore paid scope. A key whose granular scope list was left empty keeps everything its access level (full/read) already allows, except the permissions that must be added to a key on purpose: networking:read/manage (private networks, the server firewall, port forwarding, public addresses, load balancers, VPN), files:read/manage and the App Cloud permissions. An older full key without a permission list therefore reaches no networking tool until networking:read or networking:manage is added.",
|
|
56
|
+
"Service file manager tools (get_service_file_capabilities, list/stat/read/search, download/upload, write, mkdir, move, delete, extract) need an EXPLICIT files:read or files:manage grant on the API key (an empty permission list does not include them; changes need a full-access key). Call get_service_file_capabilities first and relay an unavailable reason verbatim: Firecracker servers need the updated guest agent (agent_update_required) and Cloud VPS (KVM) needs the VDS file-manager switch (feature_disabled). Writes are compare-and-swap: pass the version or sha256 you read. Slow guests are polled inside the tools; continue a returned operationId with get_service_file_operation instead of repeating a change.",
|
|
53
57
|
"",
|
|
54
58
|
"## Ordering a new VPS",
|
|
55
59
|
"Flow: get_order_plans → get_order_options(plan) → order_service.",
|
|
@@ -60,7 +64,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
60
64
|
"rootPassword rules: 6-40 chars, alphanumeric only, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
|
|
61
65
|
"sshKey and rootPassword are mutually exclusive — provide one or the other (or neither for auto-generated password).",
|
|
62
66
|
"A paid receipt is not a running server. Read get_service for the original order and follow follow_up: pending activation, finalization and capacity waits require polling the same order. About 10 seconds is a suggested polling interval, never a boot ETA. Check at most 6 times before a progress update; if still pending, retain the key and order number for a later check. Do not reorder, repay, restart or promise a background callback. A running VM, guest access and a healthy application are separate outcomes; KVM and Windows setup may take longer and must be observed.",
|
|
63
|
-
"deploy_ssh_key is
|
|
67
|
+
"deploy_ssh_key is asynchronous: follow its noty UUID with wait_for_event until it reports completed before attempting SSH to any server (not just new ones).",
|
|
68
|
+
"Without an API key, ChatGPT, Claude, Gemini and Grok can use the hosted connector https://mcp.vpsnet.com/mcp instead (OAuth sign-in to the VPSnet account; the customer picks the servers and permissions). Connections are created and removed on https://www.vpsnet.com/en/management/account/ai-connections; API keys are made on https://www.vpsnet.com/en/management/account/api-keys.",
|
|
64
69
|
"",
|
|
65
70
|
"## Deployment capabilities (unordered)",
|
|
66
71
|
"Managed Applications, manual SSH, DNS, APIs, and other tools are peer capabilities. Their order in this prompt or the tool list carries no priority. Choose the path that best matches the user's requested outcome, target support, existing state, and explicit constraints.",
|
|
@@ -71,6 +76,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
71
76
|
"Uninstall permanently deletes the managed containers, configuration, saved credentials, and application data. Existing server backups are retained. Set acknowledge_data_loss=true only after the user explicitly confirms that loss.",
|
|
72
77
|
"install_application and application lifecycle tools are asynchronous. A queued response is not proof that the application is healthy; poll get_application_installation and inspect get_application_events.",
|
|
73
78
|
"Use configure_application_access to change an installed application's access mode. Read the installation first and pass its current revision. When access.request.schema_version=2, submit every exact access.endpoints key once and match the user's requested service by endpoint service and port; array order and primary=true are metadata, not a recommendation. platform_https allocates an opaque VPSnet hostname with automatic DNS and HTTPS; private has no public listener; public_http uses the server's public IP over HTTP; managed_https uses a selected VPSnet-managed DNS zone and may add additional_addresses such as the bare domain without www; external_https records a customer-managed HTTPS address and does not configure or validate DNS, TLS, or the customer's reverse proxy.",
|
|
79
|
+
"Server routes send a customer's own hostnames to the customer's own local services on a Managed Applications server (hostname to 127.0.0.1:port, automatic certificate after verified hostname ownership and DNS readiness). Use get_application_server_routes, then configure_application_server_routes with expected_revision and the COMPLETE route set (omitted routes are removed); it needs at least one installed managed application. A customer web server already on ports 80/443 must be pointed at local ports or moved and declared as fallback.",
|
|
74
80
|
"get_application_installation includes bounded application and per-container resource history plus optional display thresholds when the worker supports those measurements. Use configure_application_resource_thresholds only at the user's request. Thresholds highlight CPU, memory, network-per-minute, or restart-delta samples but do not reserve or enforce resources, trigger server actions, or affect billing. Email is disabled by default; when the user enables it, VPSnet sends one reached and one recovered message per threshold episode without repeating while the metric remains high.",
|
|
75
81
|
"Use list_application_registry_credentials only for non-secret private registry credential metadata. Exact custom HTTPS registry hostnames are supported. Registry token creation and rotation are intentionally unavailable through MCP because secrets must not enter model prompts or tool arguments; use the VPSnet panel or direct REST API.",
|
|
76
82
|
"Customer recipes are customer-owned Compose definitions, distinct from VPSnet catalog blueprints. Validation resolves mutable image tags once and returns an immutable digest-pinned Compose definition before checking it on the target worker. Recipe creation and revision tools freeze that exact validated definition; installation runs an exact revision. Export is available only for customer recipes and never for VPSnet catalog recipes.",
|
|
@@ -82,7 +88,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
82
88
|
"Installation list and detail responses carry a capabilities block for the service's platform: data_restore, console, compose_adoption, custom_projects, and log_service_filter. Treat it as the authority on what is possible for that installation. Do not offer selective data restore when data_restore is false, do not offer Compose adoption or customer recipes when their flags are false, and do not pass a per-service log filter when log_service_filter is false. A separate access.capabilities.can_configure flag governs whether the access mode may be changed.",
|
|
83
89
|
"Never repeat application variable values in summaries or approval text. Refer only to variable names, especially for passwords, tokens, and secrets.",
|
|
84
90
|
"Use manual SSH deployment when it best matches the requested result or the user explicitly requests a custom installation. Do not present a manual deployment as a VPSnet-managed catalog installation.",
|
|
85
|
-
"Service rescue is a separate recovery path for Firecracker VPS and Cloud VPS. Always call get_service_rescue first. Entering rescue restarts the service; call enter_service_rescue only after the user explicitly approves the restart and exact image returned by the capability response. Firecracker exposes the customer filesystem at /mnt/customer; Cloud VPS uses the operator recovery ISO and noVNC. Exit restores the exact original boot and running/stopped state. If exit returns needs_attention, retry exit_service_rescue with the same idempotency key. Never ask for or invent provider nodes, VM IDs, ISO paths, operation nonces, or rollback configuration.",
|
|
91
|
+
"Service rescue is a separate recovery path for Firecracker VPS and Cloud VPS, offered only while VPSnet has it switched on. Always call get_service_rescue first; capability.enabled false means it is not offered now. Entering rescue restarts the service; call enter_service_rescue only after the user explicitly approves the restart and exact image returned by the capability response. Firecracker exposes the customer filesystem at /mnt/customer; Cloud VPS uses the operator recovery ISO and noVNC. Exit restores the exact original boot and running/stopped state. If exit returns needs_attention, retry exit_service_rescue with the same idempotency key. Never ask for or invent provider nodes, VM IDs, ISO paths, operation nonces, or rollback configuration.",
|
|
86
92
|
"",
|
|
87
93
|
"### Manual or custom deployment inside a VPS",
|
|
88
94
|
"Use only the access needed for the user's task. Keep local sandbox and approval controls enabled; request a specific missing capability instead of unrestricted agent access.",
|
|
@@ -169,6 +175,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
169
175
|
if (options.remoteScopes)
|
|
170
176
|
installRemotePolicy(server, options.remoteScopes, options.remoteProfile);
|
|
171
177
|
registerNetworkingTools(server);
|
|
178
|
+
// Local-path transfers and address-bound download URLs stay on stdio; the hosted connector gets the API-only file tools.
|
|
179
|
+
registerServiceFileTools(server, undefined, options.remoteScopes ? "remote" : "stdio");
|
|
172
180
|
// Helper to build service settings path
|
|
173
181
|
const svc = (orderNo, action) => `/account/services/${encodeURIComponent(serviceOrderNoSchema.parse(orderNo))}/${action}`;
|
|
174
182
|
const idempotencyKeySchema = z
|
|
@@ -221,7 +229,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
221
229
|
const applicationOrderNoSchema = serviceOrderNoSchema;
|
|
222
230
|
const applicationInstallationIdSchema = z
|
|
223
231
|
.string()
|
|
224
|
-
.
|
|
232
|
+
.guid()
|
|
225
233
|
.describe("Managed application installation UUID");
|
|
226
234
|
const applicationSlugSchema = z
|
|
227
235
|
.string()
|
|
@@ -429,7 +437,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
429
437
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
430
438
|
});
|
|
431
439
|
server.registerTool("get_service_rescue", {
|
|
432
|
-
description: "Get operator rescue capability and the current durable rescue session for one owned Firecracker VPS or Cloud VPS. Requires services:read. Provider identifiers, ISO paths, nonces, and rollback state are never returned.",
|
|
440
|
+
description: "Get operator rescue capability and the current durable rescue session for one owned Firecracker VPS or Cloud VPS. Requires services:read. Provider identifiers, ISO paths, nonces, and rollback state are never returned. VPSnet can switch rescue off: capability.enabled false means it is not offered now; say so and do not call enter_service_rescue.",
|
|
433
441
|
inputSchema: {
|
|
434
442
|
orderNo: serviceOrderNoSchema,
|
|
435
443
|
},
|
|
@@ -449,7 +457,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
449
457
|
};
|
|
450
458
|
});
|
|
451
459
|
server.registerTool("enter_service_rescue", {
|
|
452
|
-
description: "Restart one owned active Firecracker VPS or Cloud VPS into an operator-owned rescue system. Read get_service_rescue first and use an exact advertised image ID. This interrupts every running service. Call only after the user explicitly confirms the target service, rescue image, and reboot. Requires services:rescue, full API-key access, and a fresh idempotencyKey.",
|
|
460
|
+
description: "Restart one owned active Firecracker VPS or Cloud VPS into an operator-owned rescue system. Read get_service_rescue first and use an exact advertised image ID. This interrupts every running service. Call only after the user explicitly confirms the target service, rescue image, and reboot. Requires services:rescue, full API-key access, and a fresh idempotencyKey. Rescue may be switched off by VPSnet: while get_service_rescue reports capability.enabled false this tool sends nothing and answers serviceRescueImageNotAdvertised (the API itself answers 503 serviceRescueUnavailable).",
|
|
453
461
|
inputSchema: {
|
|
454
462
|
orderNo: serviceOrderNoSchema,
|
|
455
463
|
image: serviceRescueImageIdSchema,
|
|
@@ -500,7 +508,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
500
508
|
orderNo: serviceOrderNoSchema,
|
|
501
509
|
rescue_session_id: z
|
|
502
510
|
.string()
|
|
503
|
-
.
|
|
511
|
+
.guid()
|
|
504
512
|
.describe("Exact current session UUID from get_service_rescue"),
|
|
505
513
|
acknowledge_restart: z
|
|
506
514
|
.literal(true)
|
|
@@ -804,7 +812,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
804
812
|
inputSchema: {
|
|
805
813
|
orderNo: applicationOrderNoSchema,
|
|
806
814
|
installation_id: applicationInstallationIdSchema,
|
|
807
|
-
inspection_id: z.string().
|
|
815
|
+
inspection_id: z.string().guid(),
|
|
808
816
|
kind: z.enum(["health", "logs"]),
|
|
809
817
|
},
|
|
810
818
|
annotations: { title: "Get application inspection", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -818,7 +826,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
818
826
|
});
|
|
819
827
|
server.registerTool("get_application_recipe_validation", {
|
|
820
828
|
description: "Poll the exact persisted customer recipe validation returned by validate_application_recipe or a pending recipe creation/revision. This GET does not resolve tags or create another validation. Retain the initial resolved_compose_yaml; after this ID succeeds with valid=true, resume creation using that exact pinned document and validation_id. Requires applications:read.",
|
|
821
|
-
inputSchema: { orderNo: applicationOrderNoSchema, validation_id: z.string().
|
|
829
|
+
inputSchema: { orderNo: applicationOrderNoSchema, validation_id: z.string().guid() },
|
|
822
830
|
annotations: { title: "Get application recipe validation", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
823
831
|
}, async ({ orderNo, validation_id }) => {
|
|
824
832
|
const { status, data } = await apiRequest("GET", applicationPath(orderNo, `custom-projects/validations/${encodeURIComponent(validation_id)}`));
|
|
@@ -829,7 +837,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
829
837
|
});
|
|
830
838
|
server.registerTool("get_service_container_discovery", {
|
|
831
839
|
description: "Poll the exact persisted service container discovery returned by discover_service_containers when it remains pending. This GET does not create another discovery and never returns environment values or mounts. Keep the service and discovery UUID until succeeded, failed or expired. Requires applications:read.",
|
|
832
|
-
inputSchema: { orderNo: applicationOrderNoSchema, discovery_id: z.string().
|
|
840
|
+
inputSchema: { orderNo: applicationOrderNoSchema, discovery_id: z.string().guid() },
|
|
833
841
|
annotations: { title: "Get service container discovery", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
834
842
|
}, async ({ orderNo, discovery_id }) => {
|
|
835
843
|
const { status, data } = await apiRequest("GET", applicationPath(orderNo, `container-discoveries/${encodeURIComponent(discovery_id)}`));
|
|
@@ -1093,7 +1101,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1093
1101
|
orderNo: applicationOrderNoSchema,
|
|
1094
1102
|
name: customProjectNameSchema,
|
|
1095
1103
|
compose_yaml: customProjectComposeSchema,
|
|
1096
|
-
validation_id: z.string().
|
|
1104
|
+
validation_id: z.string().guid().optional().describe("Resume only the exact successful validation for this service and digest-pinned compose_yaml; retain the initial resolved document"),
|
|
1097
1105
|
env: customProjectEnvironmentSchema,
|
|
1098
1106
|
secret_names: customProjectSecretNamesSchema,
|
|
1099
1107
|
registry_credential_ids: customProjectRegistryCredentialIdsSchema,
|
|
@@ -1152,7 +1160,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1152
1160
|
orderNo: applicationOrderNoSchema,
|
|
1153
1161
|
project_id: customProjectIdSchema,
|
|
1154
1162
|
compose_yaml: customProjectComposeSchema,
|
|
1155
|
-
validation_id: z.string().
|
|
1163
|
+
validation_id: z.string().guid().optional().describe("Resume only the exact successful validation for this service and digest-pinned compose_yaml; retain the initial resolved document"),
|
|
1156
1164
|
env: customProjectEnvironmentSchema,
|
|
1157
1165
|
secret_names: customProjectSecretNamesSchema,
|
|
1158
1166
|
registry_credential_ids: customProjectRegistryCredentialIdsSchema,
|
|
@@ -1284,7 +1292,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1284
1292
|
description: "Prepare and poll a short-lived, read-only takeover candidate for one exact unmanaged Compose project from a fresh discovery. The worker verifies container identity, immutable image identity, restart policy, Compose safety, and exclusive local named-volume ownership. Returns only a scrubbed Compose file, variable names, and bounded counts; never variable values, source paths, commands, or unrestricted labels. This tool does not stop or replace containers. Requires applications:manage.",
|
|
1285
1293
|
inputSchema: {
|
|
1286
1294
|
orderNo: applicationOrderNoSchema,
|
|
1287
|
-
discovery_id: z.string().
|
|
1295
|
+
discovery_id: z.string().guid().describe("Fresh discovery UUID returned by discover_service_containers"),
|
|
1288
1296
|
compose_project: composeProjectLabelSchema,
|
|
1289
1297
|
},
|
|
1290
1298
|
annotations: {
|
|
@@ -1439,6 +1447,90 @@ export function createVpsnetServer(options = {}) {
|
|
|
1439
1447
|
],
|
|
1440
1448
|
};
|
|
1441
1449
|
});
|
|
1450
|
+
server.registerTool("get_application_server_routes", {
|
|
1451
|
+
description: "Get the server routes of one owned Managed Applications server: customer-owned hostnames routed by the in-guest HTTPS front door to the customer's own local services (for example app.example.com to 127.0.0.1:3000) with an automatic certificate, the optional fallback (the customer's own web server on local ports), the current revision, applied revision, overall state, per-route state and DNS status (ok, missing, elsewhere, unknown, with the A/AAAA records to create), availability and limits. Read this before configure_application_server_routes and pass its revision as expected_revision. Requires applications:read.",
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
orderNo: applicationOrderNoSchema,
|
|
1454
|
+
},
|
|
1455
|
+
annotations: {
|
|
1456
|
+
title: "Get application server routes",
|
|
1457
|
+
readOnlyHint: true,
|
|
1458
|
+
destructiveHint: false,
|
|
1459
|
+
idempotentHint: true,
|
|
1460
|
+
},
|
|
1461
|
+
}, async ({ orderNo }) => {
|
|
1462
|
+
const { data } = await apiRequest("GET", applicationPath(orderNo, "server-routes"));
|
|
1463
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1464
|
+
});
|
|
1465
|
+
const serverRouteHostnameSchema = z
|
|
1466
|
+
.string()
|
|
1467
|
+
.min(1)
|
|
1468
|
+
.max(253)
|
|
1469
|
+
.describe("Customer-owned hostname, for example app.example.com");
|
|
1470
|
+
const serverRoutePortSchema = z
|
|
1471
|
+
.number()
|
|
1472
|
+
.int()
|
|
1473
|
+
.min(1024)
|
|
1474
|
+
.max(65535)
|
|
1475
|
+
.refine((port) => ![10080, 10443, 19090].includes(port), {
|
|
1476
|
+
message: "Ports 10080, 10443 and 19090 are reserved",
|
|
1477
|
+
});
|
|
1478
|
+
server.registerTool("configure_application_server_routes", {
|
|
1479
|
+
description: "Queue a change of the server routes of one owned Managed Applications server. This REPLACES the COMPLETE set: read get_application_server_routes first, then send the full list of wanted routes and the fallback; a route or fallback that is omitted is removed. Each route sends one customer hostname to 127.0.0.1:<port> on the same server over http or https (ports 1024-65535 except 10080, 10443 and 19090; no host field, upstream is always local; at most 64 routes). The hostname needs verified ownership evidence as well as DNS pointing at the server; pointing DNS alone does not prove ownership. get_application_server_routes reports DNS, per-route state/errors and records to create. Preserve backend refusals, including applicationServerRoutesDeliveredCertificates when delivered certificates prevent enabling routes. Removed hostname claims are not released until the change is applied. The server must have at least one installed managed application. Unknown hostnames are never sent to an application (HTTP 421 / TLS unrecognized_name). Coexistence: if the customer runs their own web server (Caddy, nginx, Traefik) on ports 80/443, either point it at local ports instead, or move it to local ports and declare it as fallback ({http_port?, https_port?}); the fallback receives every hostname the front door does not serve (HTTP proxied with its Host header, HTTPS passed through unterminated). Pass expected_revision from the read; a stale revision is refused with applicationServerRoutesRevisionConflict. Confirm the exact full set with the user first. A queued response (202, action type configure_routes) must be verified by polling get_application_server_routes until state is active. Requires applications:manage and is not a paid API-key operation.",
|
|
1480
|
+
inputSchema: {
|
|
1481
|
+
orderNo: applicationOrderNoSchema,
|
|
1482
|
+
expected_revision: z
|
|
1483
|
+
.number()
|
|
1484
|
+
.int()
|
|
1485
|
+
.min(0)
|
|
1486
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
1487
|
+
.describe("Current revision returned by get_application_server_routes"),
|
|
1488
|
+
routes: z
|
|
1489
|
+
.array(z.object({
|
|
1490
|
+
hostname: serverRouteHostnameSchema,
|
|
1491
|
+
upstream: z.object({
|
|
1492
|
+
scheme: z.enum(["http", "https"]),
|
|
1493
|
+
port: serverRoutePortSchema,
|
|
1494
|
+
}),
|
|
1495
|
+
}))
|
|
1496
|
+
.max(64)
|
|
1497
|
+
.describe("The COMPLETE wanted set of routes; omitting a route removes it. An empty array removes all routes."),
|
|
1498
|
+
fallback: z
|
|
1499
|
+
.object({
|
|
1500
|
+
http_port: serverRoutePortSchema.optional(),
|
|
1501
|
+
https_port: serverRoutePortSchema.optional(),
|
|
1502
|
+
})
|
|
1503
|
+
.nullable()
|
|
1504
|
+
.optional()
|
|
1505
|
+
.describe("The customer's own web server on local ports, receiving every hostname the front door does not serve. null or omitted removes the fallback."),
|
|
1506
|
+
idempotencyKey: idempotencyKeySchema.describe("Client-global unique key. Reuse it only to replay this exact request; never reuse it for another service or request."),
|
|
1507
|
+
confirmed: z
|
|
1508
|
+
.literal(true)
|
|
1509
|
+
.describe("True only after the user confirmed the complete route set"),
|
|
1510
|
+
},
|
|
1511
|
+
annotations: {
|
|
1512
|
+
title: "Configure application server routes",
|
|
1513
|
+
readOnlyHint: false,
|
|
1514
|
+
destructiveHint: true,
|
|
1515
|
+
idempotentHint: true,
|
|
1516
|
+
openWorldHint: true,
|
|
1517
|
+
},
|
|
1518
|
+
}, async ({ orderNo, expected_revision, routes, fallback, idempotencyKey, confirmed, }) => {
|
|
1519
|
+
const { status, data } = await apiRequest("PUT", applicationPath(orderNo, "server-routes"), {
|
|
1520
|
+
expectedRevision: expected_revision,
|
|
1521
|
+
routes,
|
|
1522
|
+
fallback: fallback ?? null,
|
|
1523
|
+
confirmed,
|
|
1524
|
+
}, { "Idempotency-Key": idempotencyKey });
|
|
1525
|
+
return {
|
|
1526
|
+
content: [
|
|
1527
|
+
{
|
|
1528
|
+
type: "text",
|
|
1529
|
+
text: safeApplicationMutationResult(status, data, orderNo),
|
|
1530
|
+
},
|
|
1531
|
+
],
|
|
1532
|
+
};
|
|
1533
|
+
});
|
|
1442
1534
|
async function performApplicationLifecycleAction({ orderNo, installation_id, action, idempotencyKey, expected_blueprint_version, expected_upstream_version, acknowledge_data_loss, }) {
|
|
1443
1535
|
if (action === "uninstall" && acknowledge_data_loss !== true) {
|
|
1444
1536
|
return {
|
|
@@ -1760,7 +1852,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1760
1852
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1761
1853
|
});
|
|
1762
1854
|
server.registerTool("flush_iptables", {
|
|
1763
|
-
description: "Flush iptables rules on VPS (useful when locked out) Requires services:manage and a full-access API key.",
|
|
1855
|
+
description: "Flush iptables rules on VPS (useful when locked out) Requires services:manage and a full-access API key. It clears the rules inside the server only; the public firewall that VPSnet applies to the server's public interface on its node is separate and is not changed by this.",
|
|
1764
1856
|
inputSchema: {
|
|
1765
1857
|
orderNo: serviceOrderNoSchema,
|
|
1766
1858
|
},
|
|
@@ -1816,7 +1908,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1816
1908
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1817
1909
|
});
|
|
1818
1910
|
server.registerTool("deploy_ssh_key", {
|
|
1819
|
-
description: "Deploy an SSH key to VPS. Returns a noty UUID; follow it with wait_for_event
|
|
1911
|
+
description: "Deploy an SSH key to VPS. Returns a noty UUID; follow it with wait_for_event until it reports completed 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. Remote OAuth requires vm:exec; direct API-key use requires services:manage and a full-access key.",
|
|
1820
1912
|
inputSchema: {
|
|
1821
1913
|
orderNo: serviceOrderNoSchema,
|
|
1822
1914
|
ssh_key: positiveIdSchema.describe("SSH key ID from list_ssh_keys"),
|
|
@@ -1825,6 +1917,38 @@ export function createVpsnetServer(options = {}) {
|
|
|
1825
1917
|
const { data } = await apiRequest("POST", svc(orderNo, "change-ssh-key"), { ssh_key });
|
|
1826
1918
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1827
1919
|
});
|
|
1920
|
+
server.registerTool("list_service_ssh_keys", {
|
|
1921
|
+
description: "List which account SSH keys are in root's authorized_keys on one owned Cloud VPS or Firecracker VPS, as the server last reported: per key deployed (true, false, or null when the server was never read) and a removal running or ended in the last 15 minutes; inventory carries source, observedAt, refreshing (a Firecracker VPS answers asynchronously), readFailure, passwordLogin and the count of other keys. refresh=true asks the server again. Container VPS and dedicated servers answer supported false with unsupportedReason. Free. Requires services:read when called with an API key.",
|
|
1922
|
+
inputSchema: {
|
|
1923
|
+
orderNo: serviceOrderNoSchema,
|
|
1924
|
+
refresh: z.boolean().optional().describe("Ask the server again even when the last read is fresh"),
|
|
1925
|
+
},
|
|
1926
|
+
annotations: {
|
|
1927
|
+
title: "List SSH keys on a server",
|
|
1928
|
+
readOnlyHint: true,
|
|
1929
|
+
destructiveHint: false,
|
|
1930
|
+
idempotentHint: true,
|
|
1931
|
+
},
|
|
1932
|
+
}, async ({ orderNo, refresh }) => {
|
|
1933
|
+
const { data } = await apiRequest("GET", svc(orderNo, "ssh-keys") + (refresh === true ? "?refresh=1" : ""));
|
|
1934
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1935
|
+
});
|
|
1936
|
+
server.registerTool("remove_service_ssh_key", {
|
|
1937
|
+
description: "Remove one account SSH key (ssh_key from list_ssh_keys) from root's authorized_keys on one owned running Cloud VPS or Firecracker VPS; every line with that key goes, nothing else changes, and the key stays in the account. Free. Confirm with the user first. A key that is not on the server answers state absent. Refused with 409 sshKeyRemoveLastKey when it is the server's last permanent key and root cannot log in with a password: add another key first. Cloud VPS answers at once (removed or absent); Firecracker VPS answers 202 pending and list_service_ssh_keys shows the result in the key's removal. One removal at a time per server, at most 10 per minute; 423 while the server's two-factor lock is on. Requires services:manage and a full-access API key.",
|
|
1938
|
+
inputSchema: {
|
|
1939
|
+
orderNo: serviceOrderNoSchema,
|
|
1940
|
+
ssh_key: positiveIdSchema.describe("SSH key ID from list_ssh_keys"),
|
|
1941
|
+
},
|
|
1942
|
+
annotations: {
|
|
1943
|
+
title: "Remove an SSH key from a server",
|
|
1944
|
+
readOnlyHint: false,
|
|
1945
|
+
destructiveHint: true,
|
|
1946
|
+
idempotentHint: true,
|
|
1947
|
+
},
|
|
1948
|
+
}, async ({ orderNo, ssh_key }) => {
|
|
1949
|
+
const { data } = await apiRequest("DELETE", svc(orderNo, `ssh-keys/${ssh_key}`));
|
|
1950
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1951
|
+
});
|
|
1828
1952
|
// --- OS Reinstall ---
|
|
1829
1953
|
server.registerTool("get_os_options", {
|
|
1830
1954
|
description: "Get available OS templates for reinstall Requires services:read when called with an API key.",
|
|
@@ -1836,18 +1960,18 @@ export function createVpsnetServer(options = {}) {
|
|
|
1836
1960
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1837
1961
|
});
|
|
1838
1962
|
server.registerTool("reinstall_os", {
|
|
1839
|
-
description:
|
|
1963
|
+
description: reinstallDescription(false),
|
|
1840
1964
|
inputSchema: {
|
|
1841
1965
|
orderNo: serviceOrderNoSchema,
|
|
1842
1966
|
confirmReinstall: z.boolean().optional().describe("Dedicated only: true only after explicit user approval to reinstall this exact server and erase all its disks; never inferred or defaulted."),
|
|
1843
|
-
sshKey: positiveIdSchema.optional().describe("
|
|
1967
|
+
sshKey: positiveIdSchema.optional().describe("Account public SSH key ID from list_ssh_keys. Mutually exclusive with rootPassword. Dedicated: omitted reuses the installed key when one exists."),
|
|
1844
1968
|
osVersion: z
|
|
1845
1969
|
.number()
|
|
1846
1970
|
.describe("OS version ID from get_os_options"),
|
|
1847
1971
|
rootPassword: z
|
|
1848
1972
|
.string()
|
|
1849
1973
|
.optional()
|
|
1850
|
-
.describe("New root password
|
|
1974
|
+
.describe("New root password for VPS or dedicated reinstall; mutually exclusive with sshKey. Omit to reuse a dedicated server's installed key when available, otherwise the backend generates a password. 6-40 chars, alphanumeric, uppercase + lowercase + digit."),
|
|
1851
1975
|
confirmSnapshotDelete: z
|
|
1852
1976
|
.boolean()
|
|
1853
1977
|
.optional()
|
|
@@ -1859,6 +1983,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
1859
1983
|
idempotentHint: false,
|
|
1860
1984
|
},
|
|
1861
1985
|
}, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete, confirmReinstall, sshKey }) => {
|
|
1986
|
+
if (rootPassword !== undefined && sshKey !== undefined)
|
|
1987
|
+
throw new Error("Choose an account sshKey or rootPassword, not both.");
|
|
1862
1988
|
const options = await apiRequest("GET", svc(orderNo, "change-os"));
|
|
1863
1989
|
const capability = options.data;
|
|
1864
1990
|
if (options.status >= 400 || capability?.success !== true) {
|
|
@@ -1868,15 +1994,13 @@ export function createVpsnetServer(options = {}) {
|
|
|
1868
1994
|
if (capability.dedicated === true) {
|
|
1869
1995
|
if (confirmReinstall !== true)
|
|
1870
1996
|
throw new Error("Dedicated reinstall requires explicit user approval of all-disk data loss and confirmReinstall=true.");
|
|
1871
|
-
if (rootPassword !== undefined)
|
|
1872
|
-
throw new Error("Dedicated reinstall does not support rootPassword; use an account sshKey or reuse the installed key.");
|
|
1873
1997
|
body.confirmReinstall = true;
|
|
1874
|
-
if (sshKey !== undefined)
|
|
1875
|
-
body.sshKey = sshKey;
|
|
1876
1998
|
}
|
|
1877
|
-
else if (confirmReinstall !== undefined
|
|
1878
|
-
throw new Error("confirmReinstall
|
|
1999
|
+
else if (confirmReinstall !== undefined) {
|
|
2000
|
+
throw new Error("confirmReinstall is a dedicated-only reinstall field; inspect get_os_options for this service.");
|
|
1879
2001
|
}
|
|
2002
|
+
if (sshKey !== undefined)
|
|
2003
|
+
body.sshKey = sshKey;
|
|
1880
2004
|
if (rootPassword)
|
|
1881
2005
|
body.rootPassword = rootPassword;
|
|
1882
2006
|
if (confirmSnapshotDelete === true)
|
|
@@ -2269,7 +2393,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2269
2393
|
});
|
|
2270
2394
|
// --- API Keys ---
|
|
2271
2395
|
server.registerTool("list_api_keys", {
|
|
2272
|
-
description: "Show the API key this request authenticated with: scope (read/full/ai), granular scopes, paid_scopes, paid_operations_enabled, daily_spend_limit_eur, monthly_spend_limit_eur, allowed IPs, expiry and rate_limit (requests per
|
|
2396
|
+
description: "Show the API key this request authenticated with: scope (read/full/ai), granular scopes, paid_scopes, paid_operations_enabled, daily_spend_limit_eur, monthly_spend_limit_eur, allowed IPs, expiry and rate_limit (a stored value the limiter does not read: every key may make 300 requests per 60 seconds by default, fewer on some operations), plus permission_options listing every scope the panel can grant. The backend scopes this endpoint to the calling key itself (apiKeyScopedToSelf), so other keys on the account are never listed here. By design an API key can never create, change or revoke API keys (403 apiKeyForbidden, reason api_keys_cannot_manage_api_keys): that is done in the VPSnet panel while signed in. Read-only.",
|
|
2273
2397
|
inputSchema: {},
|
|
2274
2398
|
}, async () => {
|
|
2275
2399
|
const { data } = await apiRequest("GET", "/account/api-keys");
|
|
@@ -2285,7 +2409,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2285
2409
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2286
2410
|
});
|
|
2287
2411
|
server.registerTool("get_api_key_activity", {
|
|
2288
|
-
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.",
|
|
2412
|
+
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, and this server needs a management key: a management key has no inference usage of its own and an inference key is refused on every account route, so read inference usage in the VPSnet panel. 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.",
|
|
2289
2413
|
inputSchema: {
|
|
2290
2414
|
id: positiveIdSchema.describe("API key ID from list_api_keys (the calling key's own ID)"),
|
|
2291
2415
|
limit: z.number().int().min(1).max(200).optional().describe("Recent request rows to return (default 50, maximum 200)"),
|
|
@@ -2296,7 +2420,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2296
2420
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2297
2421
|
});
|
|
2298
2422
|
server.registerTool("get_api_key_inference_usage", {
|
|
2299
|
-
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.",
|
|
2423
|
+
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, and this server needs a management key: a management key has no inference usage of its own and an inference key is refused on every account route, so read inference usage in the VPSnet panel. Internal provider model names are never returned. This is read-only and does not change limits, notifications, or billing.",
|
|
2300
2424
|
inputSchema: {
|
|
2301
2425
|
id: positiveIdSchema.describe("Inference API key ID from list_api_keys (the calling key's own ID)"),
|
|
2302
2426
|
},
|
|
@@ -2307,7 +2431,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2307
2431
|
// --- TLS certificates ---
|
|
2308
2432
|
const certificatePaymentSchema = z
|
|
2309
2433
|
.object({
|
|
2310
|
-
payment:
|
|
2434
|
+
payment: unboundedInteger().positive().describe("Payment method ID; use 1 for account balance"),
|
|
2311
2435
|
successUrl: z.string().max(2048).describe("Approved VPSnet redirect URL, or an empty string"),
|
|
2312
2436
|
cancelUrl: z.string().max(2048).describe("Approved VPSnet redirect URL, or an empty string"),
|
|
2313
2437
|
})
|
|
@@ -3098,7 +3222,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3098
3222
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3099
3223
|
});
|
|
3100
3224
|
server.registerTool("get_domain", {
|
|
3101
|
-
description: "Get one owned domain with current nameservers and any pending domain action. Requires domains:read when using an API key.",
|
|
3225
|
+
description: "Get one owned domain with current nameservers and any pending domain action. An automatic DNSSEC move may report dnssec_transition steps with waiting/retrying states; queued or waiting is not completed. Requires domains:read when using an API key.",
|
|
3102
3226
|
inputSchema: {
|
|
3103
3227
|
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
3104
3228
|
},
|
|
@@ -3114,7 +3238,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3114
3238
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3115
3239
|
});
|
|
3116
3240
|
server.registerTool("set_domain_nameservers", {
|
|
3117
|
-
description: "Queue an asynchronous registrar nameserver change for an owned domain. When applied, the parent zone delegates the domain to the new public nameservers, replacing the prior delegation. 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.",
|
|
3241
|
+
description: "Queue an asynchronous registrar nameserver change for an owned domain. When applied, the parent zone delegates the domain to the new public nameservers, replacing the prior delegation. Nameservers must be hostnames, not IP addresses; same-domain nameserver IP records are managed separately. This does not manage DNS records or PTR. Follow returned dnssec_transition steps until done; preserve parentDsUnreadable or parentDsRemovalRequired refusals and do not bypass an active transition with manual DS edits. Requires domains:manage when using an API key.",
|
|
3118
3242
|
inputSchema: {
|
|
3119
3243
|
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
3120
3244
|
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."),
|
|
@@ -4202,7 +4326,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4202
4326
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4203
4327
|
});
|
|
4204
4328
|
server.registerTool("create_dns_zone", {
|
|
4205
|
-
description:
|
|
4329
|
+
description: createDnsZoneDescription(),
|
|
4206
4330
|
inputSchema: {
|
|
4207
4331
|
zone: z.string().describe("Zone name, e.g. example.com"),
|
|
4208
4332
|
kind: dnsZoneKindSchema
|
|
@@ -4227,7 +4351,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4227
4351
|
},
|
|
4228
4352
|
annotations: {
|
|
4229
4353
|
readOnlyHint: false,
|
|
4230
|
-
destructiveHint:
|
|
4354
|
+
destructiveHint: true,
|
|
4231
4355
|
openWorldHint: true,
|
|
4232
4356
|
},
|
|
4233
4357
|
}, async ({ zone, kind, secondary_masters, secondary_tsig_key_name, secondary_tsig_algorithm, secondary_tsig_secret }) => {
|
|
@@ -4312,7 +4436,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4312
4436
|
template: dnsTemplateIdSchema
|
|
4313
4437
|
.describe("Template ID returned by list_dns_templates, e.g. web_service, null_mx, google_workspace, microsoft_365, mail_security, caa_letsencrypt"),
|
|
4314
4438
|
parameters: z
|
|
4315
|
-
.record(z.union([z.string(), z.number(), z.boolean()]))
|
|
4439
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
4316
4440
|
.optional()
|
|
4317
4441
|
.describe("Template parameters, e.g. { ipv4, ipv6, www, mx_target, ttl } depending on the template"),
|
|
4318
4442
|
preview: z
|
|
@@ -4334,13 +4458,19 @@ export function createVpsnetServer(options = {}) {
|
|
|
4334
4458
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4335
4459
|
});
|
|
4336
4460
|
server.registerTool("delete_dns_zone", {
|
|
4337
|
-
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.",
|
|
4461
|
+
description: "Delete a forward DNS zone. Published zones are queued for removal from the managed DNS platform; the zone's own records are kept on the deletion record, so re-creating the same name in the same account within 7 days publishes it again with those records. A published zone that the domain is delegated to VPSnet for (get_dns_zone: delegation.state=platform) answers for the whole internet: deleting it stops the domain resolving worldwide (websites, e-mail). The API refuses such a delete with HTTP 409 deleteRequiresConfirmation (reason zone_serves_domain, confirm_field confirm_outage, the delegated nameservers) and deletes nothing. Tell the user exactly that and call again with confirm_outage=true only after the user explicitly confirmed the outage; never set it by default or infer it. Requires dns:write when using an API key.",
|
|
4338
4462
|
inputSchema: {
|
|
4339
4463
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4464
|
+
confirm_outage: z
|
|
4465
|
+
.boolean()
|
|
4466
|
+
.optional()
|
|
4467
|
+
.default(false)
|
|
4468
|
+
.describe("true only after the user explicitly confirmed that deleting this zone stops the domain resolving worldwide; required when the API answered deleteRequiresConfirmation. Default false."),
|
|
4340
4469
|
},
|
|
4341
4470
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4342
|
-
}, async ({ zone_id }) => {
|
|
4343
|
-
const
|
|
4471
|
+
}, async ({ zone_id, confirm_outage }) => {
|
|
4472
|
+
const query = confirm_outage === true ? "?confirm_outage=true" : "";
|
|
4473
|
+
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}${query}`);
|
|
4344
4474
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4345
4475
|
});
|
|
4346
4476
|
server.registerTool("reapply_dns_zone", {
|