vpsnet-mcp 2.3.15 → 2.6.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 CHANGED
@@ -671,7 +671,7 @@ everything else.
671
671
  | Tool | Description | Scope | Paid |
672
672
  |------|-------------|-------|------|
673
673
  | `list_services` | List all active VPS services | `services:read` | free |
674
- | `get_service` | Get detailed info for a service | `services:read` | free |
674
+ | `get_service` | Read service details and the next readiness check; keep provisioning, guest access and application health separate | `services:read` | free |
675
675
  | `get_service_graphs` | Get one graph series: metric (cpu, ram, ssd, net, io, …) and period (5m … 1y) | `services:read` | free |
676
676
  | `get_event` | Read an async action's state by its `event` id or `noty` UUID | `services:read` or `account:read` | free |
677
677
  | `wait_for_event` | Poll an async action until completed or error (or a timeout) | `services:read` or `account:read` | free |
@@ -707,6 +707,8 @@ everything else.
707
707
  | `confirm_application_compose_adoption` | Confirm the exact source takeover after explicit approval | `applications:manage` | free |
708
708
  | `install_application` | Queue a confirmed, version-pinned managed installation | `applications:manage` | free |
709
709
  | `configure_application_access` | Queue a confirmed platform-hostname, private, public-IP, managed-HTTPS, or customer-managed external-HTTPS access change | `applications:manage` | free |
710
+ | `get_application_server_routes` | Read customer-hostname server routes, fallback, revision, per-route state and DNS status | `applications:read` | free |
711
+ | `configure_application_server_routes` | Replace the complete confirmed set of customer-hostname routes to local services (and optional fallback web server) with automatic certificates | `applications:manage` | free |
710
712
  | `configure_application_resource_thresholds` | Replace confirmed non-enforcing application resource thresholds and reached/recovered email preference | `applications:manage` | free |
711
713
  | `manage_application` | Compatibility tool for older clients; use the individual actions below | `applications:manage` | free |
712
714
  | `reconcile_application` | Reconcile an application with its declared configuration | `applications:manage` | free |
@@ -799,6 +801,28 @@ everything else.
799
801
  | `browse_restore_files` | Queue a backup directory listing job without restoring files | `services:read` | free |
800
802
  | `get_restore_file_browse` | Poll a queued backup directory listing (free, read-only) | `services:read` | free |
801
803
 
804
+ ### Service files
805
+ Firecracker servers need the updated guest agent and Cloud VPS (KVM) the VDS file-manager switch; `files:read` / `files:manage` are explicit grants (an empty permission list does not include them).
806
+
807
+ | Tool | Description | Scope | Paid |
808
+ |------|-------------|-------|------|
809
+ | `get_service_file_capabilities` | Check whether the file manager can serve a server now, with the verbatim unavailable reason, limits and transfer mode | `files:read` | free |
810
+ | `list_service_files` | List one directory page inside a running server | `files:read` | free |
811
+ | `stat_service_file` | Describe one entry, optionally with its SHA-256 | `files:read` | free |
812
+ | `read_service_file` | View a text file (up to 1 MiB) or an image (up to 4 MiB), or read a 1 MiB window of any file | `files:read` | free |
813
+ | `search_service_files` | Find entries by name (substring or glob) below a directory | `files:read` | free |
814
+ | `get_service_file_download_url` | Mint a one-file download URL bound to the caller's address | `files:read` | free |
815
+ | `download_service_file` | Download a server file to a local path on the MCP machine, size-checked, with its SHA-256 | `files:read` | free |
816
+ | `get_service_file_operation` | Poll a file operation that answered 202 (slow guest, recursive delete, extraction) | `files:read` | free |
817
+ | `create_service_directory` | Create a directory, optionally with missing parents | `files:manage` | free |
818
+ | `move_service_file` | Rename or move one entry (confirmation required) | `files:manage` | free |
819
+ | `delete_service_files` | **Destructive:** permanently delete files or directories (confirmation required) | `files:manage` | free |
820
+ | `write_service_file` | Write a small file (up to 1 MiB) with compare-and-swap | `files:manage` | free |
821
+ | `upload_service_file` | Upload a local file or base64 content in one call: chunked, resumable transfer, atomic commit and SHA-256 check | `files:manage` | free |
822
+ | `list_service_file_uploads` | List the service's open uploads | `files:manage` | free |
823
+ | `abort_service_file_upload` | Abort one open upload and remove its staging file | `files:manage` | free |
824
+ | `extract_service_file_archive` | Firecracker: extract a zip/tar/tar.gz/.gz archive into a new directory | `files:manage` | free |
825
+
802
826
  ### SSH Keys
803
827
  | Tool | Description | Scope | Paid |
804
828
  |------|-------------|-------|------|
@@ -929,7 +953,7 @@ short-lived quote token.
929
953
  | `list_service_dns_options` | List owned zones and service addresses available for DNS attachment | `dns:read` | free |
930
954
  | `attach_service_dns_record` | Point an owned-zone name at one service address | `dns:write` | free |
931
955
  | `list_dns_zones` | List forward DNS zones owned by the account | `dns:read` | free |
932
- | `create_dns_zone` | Create a native or secondary zone awaiting ownership TXT verification | `dns:write` | free |
956
+ | `create_dns_zone` | Create a zone; qualifying ownership may restore and publish it. Hosted: native only; local stdio: native or secondary | `dns:write` | free |
933
957
  | `get_dns_zone` | Get desired records with IDs and a recoverable ownership TXT value | `dns:read` | free |
934
958
  | `reapply_dns_zone` | Retry publication of a published DNS zone whose apply_state is failed. | `dns:write` | free |
935
959
  | `get_dns_zone_diagnostics` | Inspect delegation, SOA, DNSSEC, and record hygiene | `dns:read` | free |
@@ -1005,7 +1029,7 @@ enabled.
1005
1029
  ### Guest Agent
1006
1030
  | Tool | Description | Scope | Paid |
1007
1031
  |------|-------------|-------|------|
1008
- | `get_guest_agent_status` | Check QEMU guest-agent availability on a Cloud VPS | `services:read` | free |
1032
+ | `get_guest_agent_status` | Read guest-agent availability on Cloud VPS or Firecracker; this does not establish application readiness | `services:read` | free |
1009
1033
 
1010
1034
  ### History
1011
1035
  | Tool | Description | Scope | Paid |
@@ -1032,7 +1056,8 @@ resource. DNS changes are polled through the network's `settings.dns_change`.
1032
1056
  New private/public load balancers, VPNs and public addresses use an explicit
1033
1057
  quote → common checkout → order-status flow. Quote tools always send
1034
1058
  `prepare_after_payment=true`: no provisioning happens before payment.
1035
- Show the quote's exact EUR `gross_amount`, obtain approval, and pass the same
1059
+ Choose `period_months` only from the live catalog's offered terms (omission is one month).
1060
+ Show the quoted term and exact EUR `gross_amount`, obtain approval, and pass the same
1036
1061
  `idempotencyKey`, `quote_id` and `quoteToken` to `order_network_product`.
1037
1062
  API keys additionally need full access, enabled paid operations,
1038
1063
  `networking:order`, and spend limits. Read `get_network_payment_methods` and
@@ -1060,12 +1085,28 @@ not an alternative to checkout.
1060
1085
  Device configuration pickup is a one-time **management** action, even though
1061
1086
  its HTTP method is GET. Generate the reveal key pair outside MCP, submit only
1062
1087
  the X25519 public key, save the returned sealed ciphertext, and decrypt it
1063
- outside the conversation. Neither private keys nor plaintext WireGuard
1064
- configuration are requested or decrypted by these tools. Never automatically
1088
+ outside the conversation. Neither private keys, plaintext WireGuard configuration nor IPsec PSKs are
1089
+ requested or decrypted by these tools. Router recipes retain a literal PSK
1090
+ placeholder; fill it only in the customer's external configuration workflow. Never automatically
1065
1091
  repeat a pickup after an uncertain response.
1066
1092
 
1067
- The current backend has no customer network rename, existing-port fixed-IP
1068
- edit, appliance retry, or native site-to-site API in this release. Fixed DHCP
1093
+ For personal devices use WireGuard; IPsec IKEv2 is a site-to-site option only.
1094
+ Read the selected gateway's `capabilities.site_ipsec` and `ipsec_profiles`
1095
+ before selecting the protocol, vendor or cipher mode. A listed tool does not
1096
+ prove an older gateway image supports IPsec. Read router recipes with
1097
+ `get_network_vpn_peer_recipe`; don't use a different vendor profile than the
1098
+ one used to create the peer unless the returned API guidance supports it.
1099
+ For IPsec, connected means an established SA and the handshake timestamp is a
1100
+ CHILD-SA rekey; WireGuard uses its recent-handshake status.
1101
+
1102
+ Device `grant_scope=network` authorizes eligible current and future servers in
1103
+ the attached private network, still subject to network security policy. Obtain
1104
+ that broader authorization explicitly. Selected-server access uses
1105
+ `grant_scope=servers`; replacing grants without a scope also selects servers,
1106
+ so inspect and preserve the intended scope when editing an existing device.
1107
+
1108
+ This release exposes no customer network rename, existing-port fixed-IP edit,
1109
+ or appliance restart/rebuild tools. Fixed DHCP
1069
1110
  addresses are selected at attach; removing/recreating an attachment requires
1070
1111
  separate approval and is not represented as an in-place edit. Legacy unpaid
1071
1112
  LB/VPN create routes, legacy stock-before-payment purchases and enabling
@@ -1096,7 +1137,7 @@ feature gate.
1096
1137
  | `attach_private_network_port` | Attach an owned service; omit ipv4 for automatic allocation or pass a fixed usable address | `networking:manage` | free |
1097
1138
  | `detach_private_network_port` | Detach the owned server port | `networking:manage` | free |
1098
1139
  | `retry_private_network_port` | Retry the failed current port operation with the PORT generation and any required restart approval. | `networking:manage` | free |
1099
- | `get_network_vpn_summary` | Read the network's VPN summary, including truthful absence | `networking:read` | free |
1140
+ | `get_network_vpn_summary` | Read the network's VPN summary, including truthful absence; connected locations are managed on the gateway | `networking:read` | free |
1100
1141
  | `get_network_routed_egress` | Read this network's NAT/internet access, public address and qualification state. | `networking:read` | free |
1101
1142
  | `set_network_routed_egress` | Request the currently supported NAT transition for an owned network; the backend may refuse mode changes | `networking:manage` | free |
1102
1143
  | `get_security_group_capabilities` | Read security-group availability and whether the network default policy can be changed. | `networking:read` | free |
@@ -1123,17 +1164,22 @@ feature gate.
1123
1164
  | `add_network_load_balancer_listener` | Add a listener and optional initial members using currently offered protocol capabilities | `networking:manage` | free |
1124
1165
  | `remove_network_load_balancer_listener` | Remove a listener and its members; deleting the last listener is refused. | `networking:manage` | free |
1125
1166
  | `list_network_vpn_gateways` | List owned VPN gateways, their device peers and safe status | `networking:read` | free |
1126
- | `get_network_vpn_gateway` | Read one owned VPN gateway, current generation, device peers, grant candidates and attachment capability. | `networking:read` | free |
1167
+ | `get_network_vpn_gateway` | Read one owned VPN gateway, protocol-specific peer status, IPsec capabilities/profiles, device grant scopes and site options | `networking:read` | free |
1127
1168
  | `delete_network_vpn_gateway` | Request teardown of an owned VPN gateway; all devices lose connectivity | `networking:manage` | free |
1128
1169
  | `attach_network_vpn_gateway` | Attach or move an account VPN to an owned network only when standalone attachment is advertised | `networking:manage` | free |
1129
1170
  | `detach_network_vpn_gateway` | Detach an account VPN from its network when supported; it keeps its paid period and devices but recreates the gateway | `networking:manage` | free |
1130
1171
  | `set_vpn_server_access` | Choose accept or groups_only for the VPN's access to attached servers when this capability is available | `networking:manage` | free |
1131
- | `create_network_vpn_peer` | Add a device with an externally generated one-time PUBLIC reveal key | `networking:manage` | free |
1172
+ | `create_network_vpn_peer` | Add a device, or a connected location (site-to-site office router), with an externally generated one-time PUBLIC reveal key | `networking:manage` | free |
1132
1173
  | `take_network_vpn_peer_config` | Consume the device's sealed configuration ONCE | `networking:manage` | free |
1133
1174
  | `revoke_network_vpn_peer` | Revoke one device using the VPN GATEWAY generation | `networking:manage` | free |
1134
1175
  | `set_network_vpn_peer_grants` | Replace the device's complete reachable-server list when standalone VPN is available | `networking:manage` | free |
1135
1176
  | `get_network_vpn_egress_options` | Read supported VPN Internet exits and qualification | `networking:read` | free |
1136
1177
  | `set_network_vpn_peer_egress` | Set a device's private-only, inherited NAT, or advertised chosen exit policy | `networking:manage` | free |
1178
+ | `get_network_vpn_peer_recipe` | Read public IPsec router instructions with a PSK placeholder; no secret retrieval | `networking:read` | free |
1179
+ | `get_network_vpn_peer_history` | Read a device's or connected location's traffic and handshake history (24 h raw, 7 d, 30 d, 13 months) | `networking:read` | free |
1180
+ | `set_network_vpn_site_options` | Switch hub-and-spoke between the locations connected to a VPN | `networking:manage` | free |
1181
+ | `set_network_vpn_site_alert` | Switch the office-down e-mail of one connected location | `networking:manage` | free |
1182
+ | `set_network_site_route_mode` | Choose DHCP routes or an internal router for a network's routes to connected locations | `networking:manage` | free |
1137
1183
  | `list_network_products` | Read current paid products, live tier names, prices and purchase capability; public proxy load balancer 3/5-node offerings must come from this catalog. | `networking:read` | free |
1138
1184
  | `list_network_product_periods` | Read owned product periods, renewal availability and lifecycle facts. | `networking:read` | free |
1139
1185
  | `quote_network_product` | Create the network product's exact paid quote for COMMON CHECKOUT | `networking:manage` | Quote: `networking:order` |
package/build/api.js CHANGED
@@ -166,6 +166,50 @@ semantics) {
166
166
  // real cause instead of an unactionable "Unauthorized".
167
167
  return { status: res.status, data: authorizationContext() ? data : annotateAuthFailure(res.status, data) };
168
168
  }
169
+ /**
170
+ * Send raw bytes (an upload chunk) to an account API route with the caller's
171
+ * credentials. Same base, path, credential, redirect and timeout rules as
172
+ * apiRequest; only the request body differs.
173
+ */
174
+ export async function apiRawRequest(method, path, bytes, extraHeaders = {}) {
175
+ validateApiPath(path);
176
+ const url = `${resolveApiBase()}${path}`;
177
+ const headers = {
178
+ Accept: "application/json",
179
+ "Content-Type": "application/octet-stream",
180
+ ...extraHeaders,
181
+ ...apiAuthorization(resolveApiKey),
182
+ };
183
+ const timeoutMs = resolveApiTimeoutMs();
184
+ const controller = new AbortController();
185
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
186
+ let res;
187
+ let text;
188
+ try {
189
+ res = await fetch(url, { method, headers, body: bytes, redirect: "error", signal: controller.signal });
190
+ text = await res.text();
191
+ }
192
+ catch (error) {
193
+ if (error instanceof Error && error.name === "AbortError") {
194
+ throw new Error(`VPSnet API request timed out after ${timeoutMs}ms: ${method} ${path}`);
195
+ }
196
+ throw error;
197
+ }
198
+ finally {
199
+ clearTimeout(timeout);
200
+ }
201
+ let data;
202
+ try {
203
+ data = text ? JSON.parse(text) : null;
204
+ }
205
+ catch {
206
+ return { status: res.status, data: res.ok ? { success: false, error: "The VPSnet API answered with a non-JSON body." }
207
+ : describeNonApiResponse(res.status, res.headers.get("content-type"), text, new URL(url).host, false) };
208
+ }
209
+ if (res.ok === false)
210
+ data = withHttpStatus(res.status, data, res.headers.get("retry-after"));
211
+ return { status: res.status, data: authorizationContext() ? data : annotateAuthFailure(res.status, data) };
212
+ }
169
213
  /**
170
214
  * Structured tool error for a non-2xx answer whose body is not JSON.
171
215
  */
@@ -0,0 +1,11 @@
1
+ /** Describes both creation paths in DnsZonesController::store, including restoration. */
2
+ export function createDnsZoneDescription(hosted = false) {
3
+ return [
4
+ hosted
5
+ ? "Create a native forward DNS zone in the connected VPSnet account. Secondary zones and their TSIG credentials must be configured in the VPSnet control panel; this hosted tool does not accept authentication secrets."
6
+ : "Create a native or secondary forward DNS zone in the authenticated VPSnet account. Secondary zones transfer records from public primary DNS servers and require TSIG.",
7
+ "Normally creates pending_verification. With qualifying current ownership proof, the API may instead restore a recently deleted zone and queue immediate publication. Publication can delete stale zone candidates for the same name, including their DNS records and DDNS tokens. Existing live zones are refused; this does not register a domain or change registrar nameservers.",
8
+ "Read the returned zone.status and apply_state: queued publication is not proof of live DNS service. When verification is needed, verificationRecord contains the public ownership TXT challenge (_vpsnet-dns.<zone>). Follow zone.verification.next_step and warnings for its location, then call verify_dns_zone. Keep nameservers already delegated to VPSnet. A previous-provider or parent-zone location may be required.",
9
+ "If the TXT value is missing, first read get_dns_zone.zone.verification.record.value; reissue_dns_zone_verification rotates it only when unrecoverable or requested by the customer. Requires dns:write.",
10
+ ].join(" ");
11
+ }
package/build/index.js CHANGED
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { installToolInputSchemas } 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";
6
+ import { registerReadOnlyContext } from "./read-only-context.js";
7
+ import { purchaseReadiness, serviceReadiness, serviceResultOutput } from "./service-readiness.js";
5
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
10
  import { pathToFileURL } from "node:url";
8
11
  import { resolve } from "node:path";
12
+ import { reinstallDescription } from "./service-reinstall-contract.js";
9
13
  import { installRemotePolicy, installToolTitles, remoteInstructions, orderInstructions } from "./remote-policy.js";
10
14
  import { createHash, randomUUID } from "node:crypto";
11
15
  import { readFileSync } from "node:fs";
12
16
  import { registerNetworkingTools } from "./networking-tools.js";
17
+ import { registerServiceFileTools } from "./service-files.js";
13
18
  import { isIP } from "node:net";
14
19
  import { z } from "zod";
15
20
  import { apiRequest, formatJson, initializeStdioApi } from "./api.js";
@@ -48,6 +53,7 @@ export function createVpsnetServer(options = {}) {
48
53
  "Auth: every request authenticates with the X-API-KEY header (your VPSNet API key). Requests are rate-limited — on HTTP 429, back off and retry after a short delay rather than hammering the endpoint.",
49
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'.",
50
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, so scopes only ever narrow a key.",
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.",
51
57
  "",
52
58
  "## Ordering a new VPS",
53
59
  "Flow: get_order_plans → get_order_options(plan) → order_service.",
@@ -57,7 +63,7 @@ export function createVpsnetServer(options = {}) {
57
63
  "Resources is an array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
58
64
  "rootPassword rules: 6-40 chars, alphanumeric only, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
59
65
  "sshKey and rootPassword are mutually exclusive — provide one or the other (or neither for auto-generated password).",
60
- "After placing an order, wait 10-30 seconds before attempting SSH — the VPS needs time to boot and start SSH daemon.",
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.",
61
67
  "deploy_ssh_key is also async — wait 15-30 seconds after deploying a key before attempting SSH to any VPS (not just new ones).",
62
68
  "",
63
69
  "## Deployment capabilities (unordered)",
@@ -69,6 +75,7 @@ export function createVpsnetServer(options = {}) {
69
75
  "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.",
70
76
  "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.",
71
77
  "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.",
78
+ "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.",
72
79
  "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.",
73
80
  "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.",
74
81
  "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.",
@@ -167,6 +174,8 @@ export function createVpsnetServer(options = {}) {
167
174
  if (options.remoteScopes)
168
175
  installRemotePolicy(server, options.remoteScopes, options.remoteProfile);
169
176
  registerNetworkingTools(server);
177
+ // Local-path transfers and address-bound download URLs stay on stdio; the hosted connector gets the API-only file tools.
178
+ registerServiceFileTools(server, undefined, options.remoteScopes ? "remote" : "stdio");
170
179
  // Helper to build service settings path
171
180
  const svc = (orderNo, action) => `/account/services/${encodeURIComponent(serviceOrderNoSchema.parse(orderNo))}/${action}`;
172
181
  const idempotencyKeySchema = z
@@ -347,13 +356,15 @@ export function createVpsnetServer(options = {}) {
347
356
  return { content: [{ type: "text", text: formatJson(data) }] };
348
357
  });
349
358
  server.registerTool("get_service", {
350
- description: "Get detailed info for a service by order number. stateProcess.waitingReason=public_ip_inventory means the paid CREATE awaits public IP inventory and resumes automatically; poll this same service without a new order, payment or manual retry. stateProcess.attention=true requires operator attention. The legacy stateProccess alias carries the same process data. 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.",
359
+ description: "Get detailed info and follow_up readiness guidance for the original service order. Payment, VM provisioning, guest-agent availability and application health are separate. Pending activation, finalizing or active stateProcess means keep polling this same order; about 10 seconds is an interval, not an ETA. Check at most 6 times before a progress update, then retain the order for the next check. stateProcess.waitingReason=public_ip_inventory resumes automatically; never reorder, pay again or manually restart. stateProcess.attention=true requires operator attention. The legacy stateProccess alias carries the same process data. Unknown guest-agent status alone does not prove the VM is down. Resource-usage rows with available=false contain compatibility placeholders, not measurements. Requires services:read when called with an API key.",
351
360
  inputSchema: {
352
361
  orderNo: serviceOrderNoSchema,
353
362
  },
363
+ outputSchema: serviceResultOutput,
354
364
  }, async ({ orderNo }) => {
355
- const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}`);
356
- return { content: [{ type: "text", text: formatJson(data) }] };
365
+ const { status, data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}`);
366
+ const output = serviceReadiness(data, status, orderNo, options.remoteScopes !== undefined);
367
+ return { content: [{ type: "text", text: formatJson(output) }], structuredContent: output };
357
368
  });
358
369
  server.registerTool("get_service_graphs", {
359
370
  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.",
@@ -1435,6 +1446,90 @@ export function createVpsnetServer(options = {}) {
1435
1446
  ],
1436
1447
  };
1437
1448
  });
1449
+ server.registerTool("get_application_server_routes", {
1450
+ 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.",
1451
+ inputSchema: {
1452
+ orderNo: applicationOrderNoSchema,
1453
+ },
1454
+ annotations: {
1455
+ title: "Get application server routes",
1456
+ readOnlyHint: true,
1457
+ destructiveHint: false,
1458
+ idempotentHint: true,
1459
+ },
1460
+ }, async ({ orderNo }) => {
1461
+ const { data } = await apiRequest("GET", applicationPath(orderNo, "server-routes"));
1462
+ return { content: [{ type: "text", text: formatJson(data) }] };
1463
+ });
1464
+ const serverRouteHostnameSchema = z
1465
+ .string()
1466
+ .min(1)
1467
+ .max(253)
1468
+ .describe("Customer-owned hostname, for example app.example.com");
1469
+ const serverRoutePortSchema = z
1470
+ .number()
1471
+ .int()
1472
+ .min(1024)
1473
+ .max(65535)
1474
+ .refine((port) => ![10080, 10443, 19090].includes(port), {
1475
+ message: "Ports 10080, 10443 and 19090 are reserved",
1476
+ });
1477
+ server.registerTool("configure_application_server_routes", {
1478
+ 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.",
1479
+ inputSchema: {
1480
+ orderNo: applicationOrderNoSchema,
1481
+ expected_revision: z
1482
+ .number()
1483
+ .int()
1484
+ .min(0)
1485
+ .max(Number.MAX_SAFE_INTEGER)
1486
+ .describe("Current revision returned by get_application_server_routes"),
1487
+ routes: z
1488
+ .array(z.object({
1489
+ hostname: serverRouteHostnameSchema,
1490
+ upstream: z.object({
1491
+ scheme: z.enum(["http", "https"]),
1492
+ port: serverRoutePortSchema,
1493
+ }),
1494
+ }))
1495
+ .max(64)
1496
+ .describe("The COMPLETE wanted set of routes; omitting a route removes it. An empty array removes all routes."),
1497
+ fallback: z
1498
+ .object({
1499
+ http_port: serverRoutePortSchema.optional(),
1500
+ https_port: serverRoutePortSchema.optional(),
1501
+ })
1502
+ .nullable()
1503
+ .optional()
1504
+ .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."),
1505
+ idempotencyKey: idempotencyKeySchema.describe("Client-global unique key. Reuse it only to replay this exact request; never reuse it for another service or request."),
1506
+ confirmed: z
1507
+ .literal(true)
1508
+ .describe("True only after the user confirmed the complete route set"),
1509
+ },
1510
+ annotations: {
1511
+ title: "Configure application server routes",
1512
+ readOnlyHint: false,
1513
+ destructiveHint: true,
1514
+ idempotentHint: true,
1515
+ openWorldHint: true,
1516
+ },
1517
+ }, async ({ orderNo, expected_revision, routes, fallback, idempotencyKey, confirmed, }) => {
1518
+ const { status, data } = await apiRequest("PUT", applicationPath(orderNo, "server-routes"), {
1519
+ expectedRevision: expected_revision,
1520
+ routes,
1521
+ fallback: fallback ?? null,
1522
+ confirmed,
1523
+ }, { "Idempotency-Key": idempotencyKey });
1524
+ return {
1525
+ content: [
1526
+ {
1527
+ type: "text",
1528
+ text: safeApplicationMutationResult(status, data, orderNo),
1529
+ },
1530
+ ],
1531
+ };
1532
+ });
1438
1533
  async function performApplicationLifecycleAction({ orderNo, installation_id, action, idempotencyKey, expected_blueprint_version, expected_upstream_version, acknowledge_data_loss, }) {
1439
1534
  if (action === "uninstall" && acknowledge_data_loss !== true) {
1440
1535
  return {
@@ -1832,18 +1927,18 @@ export function createVpsnetServer(options = {}) {
1832
1927
  return { content: [{ type: "text", text: formatJson(data) }] };
1833
1928
  });
1834
1929
  server.registerTool("reinstall_os", {
1835
- description: "Reinstall OS on a VPS or qualified dedicated server. WARNING: destroys existing data; confirm with the user before reinstalling. Cloud VPS and Firecracker VPS: reinstall also deletes existing snapshots, so they cannot provide rollback after reinstall. Read get_os_options.snapshotDeletion first; when snapshots exist, set confirmSnapshotDelete to true only after the user explicitly confirms their deletion. A pending snapshot action blocks reinstall until it finishes. Container VPS has no snapshots. Dedicated servers erase every disk and have no snapshot rollback; get_os_options reports dedicated=true and confirmRequired=true. Pass confirmReinstall=true only after the user explicitly approves reinstalling that exact dedicated server and losing all disk data. Dedicated reinstall supports an optional sshKey from list_ssh_keys (otherwise the prior install key is reused), never rootPassword, and returns a reinstall job ID. VPS reinstall returns a noty UUID for acceptance; follow get_service_history for completion. Password rules: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. OAuth requires services:reinstall; API keys require full access.",
1930
+ description: reinstallDescription(false),
1836
1931
  inputSchema: {
1837
1932
  orderNo: serviceOrderNoSchema,
1838
1933
  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."),
1839
- sshKey: positiveIdSchema.optional().describe("Dedicated only: account SSH key ID from list_ssh_keys; omitted reuses the installed key."),
1934
+ 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."),
1840
1935
  osVersion: z
1841
1936
  .number()
1842
1937
  .describe("OS version ID from get_os_options"),
1843
1938
  rootPassword: z
1844
1939
  .string()
1845
1940
  .optional()
1846
- .describe("New root password (auto-generated if omitted). 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit"),
1941
+ .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."),
1847
1942
  confirmSnapshotDelete: z
1848
1943
  .boolean()
1849
1944
  .optional()
@@ -1855,6 +1950,8 @@ export function createVpsnetServer(options = {}) {
1855
1950
  idempotentHint: false,
1856
1951
  },
1857
1952
  }, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete, confirmReinstall, sshKey }) => {
1953
+ if (rootPassword !== undefined && sshKey !== undefined)
1954
+ throw new Error("Choose an account sshKey or rootPassword, not both.");
1858
1955
  const options = await apiRequest("GET", svc(orderNo, "change-os"));
1859
1956
  const capability = options.data;
1860
1957
  if (options.status >= 400 || capability?.success !== true) {
@@ -1864,15 +1961,13 @@ export function createVpsnetServer(options = {}) {
1864
1961
  if (capability.dedicated === true) {
1865
1962
  if (confirmReinstall !== true)
1866
1963
  throw new Error("Dedicated reinstall requires explicit user approval of all-disk data loss and confirmReinstall=true.");
1867
- if (rootPassword !== undefined)
1868
- throw new Error("Dedicated reinstall does not support rootPassword; use an account sshKey or reuse the installed key.");
1869
1964
  body.confirmReinstall = true;
1870
- if (sshKey !== undefined)
1871
- body.sshKey = sshKey;
1872
1965
  }
1873
- else if (confirmReinstall !== undefined || sshKey !== undefined) {
1874
- throw new Error("confirmReinstall and sshKey are dedicated-only reinstall fields; inspect get_os_options for this service.");
1966
+ else if (confirmReinstall !== undefined) {
1967
+ throw new Error("confirmReinstall is a dedicated-only reinstall field; inspect get_os_options for this service.");
1875
1968
  }
1969
+ if (sshKey !== undefined)
1970
+ body.sshKey = sshKey;
1876
1971
  if (rootPassword)
1877
1972
  body.rootPassword = rootPassword;
1878
1973
  if (confirmSnapshotDelete === true)
@@ -2020,6 +2115,7 @@ export function createVpsnetServer(options = {}) {
2020
2115
  "Payment object for balance: { payment: 1, successUrl: '', cancelUrl: '' }.",
2021
2116
  "First call returns confirmationRequired and an exact gross EUR quote without charging. Show the amount and billing period, obtain explicit approval, then call again with the unchanged order configuration and idempotencyKey, returned quoteToken, expected_total_eur and acknowledge_charge=true. The API key must have paid scope/caps enabled. Never replace the key after an uncertain payment; inspect that original attempt in the VPSnet panel. Quotes are retained in this MCP process; after a restart inspect the original attempt in the panel rather than submitting another order.",
2022
2117
  "Payment acceptance is not server readiness. Poll get_service for the returned order number. stateProcess.waitingReason=public_ip_inventory resumes the same paid CREATE automatically when inventory returns; do not place or pay for another order. stateProcess.attention=true needs operator attention.",
2118
+ "Follow follow_up for the same service. Poll about every 10 seconds, never treating that interval as a boot ETA; after at most 6 checks report pending progress and retain the original key/order. Do not automatically restart or promise a completion callback. Running VM, usable guest access and application health are separate checks.",
2023
2119
  "Resources: array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
2024
2120
  "rootPassword: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Example: 'MyPass123'.",
2025
2121
  "sshKey and rootPassword are mutually exclusive — provide one or the other.",
@@ -2103,7 +2199,8 @@ export function createVpsnetServer(options = {}) {
2103
2199
  confirmationRequired: true, idempotencyKey,
2104
2200
  message: "The original quote is no longer valid. The API refused confirmation before charging. Inspect this original attempt in the VPSnet panel and obtain approval of any changed price or configuration; do not retry an uncertain purchase with a new key." }) }] };
2105
2201
  }
2106
- return { ...(status >= 400 ? { isError: true } : {}), content: [{ type: "text", text: formatJson(data) }] };
2202
+ const output = purchaseReadiness(data, status, idempotencyKey, true, false);
2203
+ return { ...(status >= 400 ? { isError: true } : {}), content: [{ type: "text", text: formatJson(output) }], structuredContent: output };
2107
2204
  }
2108
2205
  catch {
2109
2206
  return refusal("purchaseOutcomeUnknown", "The confirmation outcome is unknown. Inspect this original attempt in the VPSnet panel. Do not submit another order or use a new key.");
@@ -3092,7 +3189,7 @@ export function createVpsnetServer(options = {}) {
3092
3189
  return { content: [{ type: "text", text: formatJson(data) }] };
3093
3190
  });
3094
3191
  server.registerTool("get_domain", {
3095
- description: "Get one owned domain with current nameservers and any pending domain action. Requires domains:read when using an API key.",
3192
+ 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.",
3096
3193
  inputSchema: {
3097
3194
  domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
3098
3195
  },
@@ -3108,7 +3205,7 @@ export function createVpsnetServer(options = {}) {
3108
3205
  return { content: [{ type: "text", text: formatJson(data) }] };
3109
3206
  });
3110
3207
  server.registerTool("set_domain_nameservers", {
3111
- 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.",
3208
+ 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.",
3112
3209
  inputSchema: {
3113
3210
  domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
3114
3211
  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."),
@@ -3840,7 +3937,7 @@ export function createVpsnetServer(options = {}) {
3840
3937
  };
3841
3938
  });
3842
3939
  server.registerTool("get_guest_agent_status", {
3843
- 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.",
3940
+ description: "Read guest-agent availability for Cloud VPS (KVM/VDS) or Firecracker. KVM probes its QEMU agent; Firecracker reads worker-reported health. This is separate from VM provisioning and application health. Unknown is advisory, not proof the server failed; unavailable gates only operations that need the agent. Delegated Linux tools do not provide Windows PowerShell or RDP verification. Requires services:read when called with an API key.",
3844
3941
  inputSchema: {
3845
3942
  orderNo: serviceOrderNoSchema,
3846
3943
  },
@@ -4196,7 +4293,7 @@ export function createVpsnetServer(options = {}) {
4196
4293
  return { content: [{ type: "text", text: formatJson(data) }] };
4197
4294
  });
4198
4295
  server.registerTool("create_dns_zone", {
4199
- description: "Create a forward DNS zone in pending verification state. Native zones are managed here; secondary zones AXFR from public primary DNS servers and require TSIG. Returns the ownership TXT in verificationRecord (name _vpsnet-dns.<zone>). If the value is lost, read get_dns_zone.zone.verification.record.value first; use reissue_dns_zone_verification only when it is null/unrecoverable or the customer requests rotation. The API records the nameservers the parent zone delegates the name to, so the TXT can still be proven at that previous provider after the domain is pointed at VPSnet. warnings: delegatedToPlatform (the domain already points at VPSnet nameservers - keep them, do not point it back) or parentServedByPlatform (the TXT goes in the parent zone); zone.verification.next_step says what to do next. Add the TXT at that location, then call verify_dns_zone. Requires dns:write when using an API key.",
4296
+ description: createDnsZoneDescription(),
4200
4297
  inputSchema: {
4201
4298
  zone: z.string().describe("Zone name, e.g. example.com"),
4202
4299
  kind: dnsZoneKindSchema
@@ -4221,7 +4318,7 @@ export function createVpsnetServer(options = {}) {
4221
4318
  },
4222
4319
  annotations: {
4223
4320
  readOnlyHint: false,
4224
- destructiveHint: false,
4321
+ destructiveHint: true,
4225
4322
  openWorldHint: true,
4226
4323
  },
4227
4324
  }, async ({ zone, kind, secondary_masters, secondary_tsig_key_name, secondary_tsig_algorithm, secondary_tsig_secret }) => {
@@ -4328,13 +4425,19 @@ export function createVpsnetServer(options = {}) {
4328
4425
  return { content: [{ type: "text", text: formatJson(data) }] };
4329
4426
  });
4330
4427
  server.registerTool("delete_dns_zone", {
4331
- 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.",
4428
+ 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.",
4332
4429
  inputSchema: {
4333
4430
  zone_id: positiveIdSchema.describe("DNS zone ID"),
4431
+ confirm_outage: z
4432
+ .boolean()
4433
+ .optional()
4434
+ .default(false)
4435
+ .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."),
4334
4436
  },
4335
4437
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4336
- }, async ({ zone_id }) => {
4337
- const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}`);
4438
+ }, async ({ zone_id, confirm_outage }) => {
4439
+ const query = confirm_outage === true ? "?confirm_outage=true" : "";
4440
+ const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}${query}`);
4338
4441
  return { content: [{ type: "text", text: formatJson(data) }] };
4339
4442
  });
4340
4443
  server.registerTool("reapply_dns_zone", {
@@ -4639,7 +4742,8 @@ export function createVpsnetServer(options = {}) {
4639
4742
  if (options.remoteScopes)
4640
4743
  registerRemoteTools(server);
4641
4744
  if (options.remoteProfile === "orders")
4642
- registerOrderTools(server);
4745
+ registerOrderTools(server, options.remoteScopes);
4746
+ registerReadOnlyContext(server);
4643
4747
  return server;
4644
4748
  }
4645
4749
  // --- Start server ---